newcode 0.1.1__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 (289) 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 +147 -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 +630 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +122 -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 +380 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +167 -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 +2145 -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 +296 -0
  28. code_puppy/agents/pack/husky.py +307 -0
  29. code_puppy/agents/pack/retriever.py +380 -0
  30. code_puppy/agents/pack/shepherd.py +327 -0
  31. code_puppy/agents/pack/terrier.py +281 -0
  32. code_puppy/agents/pack/watchdog.py +357 -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 +674 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +664 -0
  49. code_puppy/cli_runner.py +1038 -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 +526 -0
  57. code_puppy/command_line/command_handler.py +283 -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 +853 -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 +91 -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/skills_completion.py +160 -0
  97. code_puppy/command_line/uc_menu.py +893 -0
  98. code_puppy/command_line/utils.py +93 -0
  99. code_puppy/command_line/wiggum_state.py +78 -0
  100. code_puppy/config.py +1787 -0
  101. code_puppy/error_logging.py +133 -0
  102. code_puppy/gemini_code_assist.py +385 -0
  103. code_puppy/gemini_model.py +754 -0
  104. code_puppy/hook_engine/README.md +105 -0
  105. code_puppy/hook_engine/__init__.py +15 -0
  106. code_puppy/hook_engine/aliases.py +155 -0
  107. code_puppy/hook_engine/engine.py +195 -0
  108. code_puppy/hook_engine/executor.py +293 -0
  109. code_puppy/hook_engine/matcher.py +145 -0
  110. code_puppy/hook_engine/models.py +222 -0
  111. code_puppy/hook_engine/registry.py +106 -0
  112. code_puppy/hook_engine/validator.py +141 -0
  113. code_puppy/http_utils.py +361 -0
  114. code_puppy/keymap.py +128 -0
  115. code_puppy/main.py +10 -0
  116. code_puppy/mcp_/__init__.py +66 -0
  117. code_puppy/mcp_/async_lifecycle.py +286 -0
  118. code_puppy/mcp_/blocking_startup.py +469 -0
  119. code_puppy/mcp_/captured_stdio_server.py +275 -0
  120. code_puppy/mcp_/circuit_breaker.py +290 -0
  121. code_puppy/mcp_/config_wizard.py +507 -0
  122. code_puppy/mcp_/dashboard.py +308 -0
  123. code_puppy/mcp_/error_isolation.py +407 -0
  124. code_puppy/mcp_/examples/retry_example.py +226 -0
  125. code_puppy/mcp_/health_monitor.py +589 -0
  126. code_puppy/mcp_/managed_server.py +428 -0
  127. code_puppy/mcp_/manager.py +807 -0
  128. code_puppy/mcp_/mcp_logs.py +224 -0
  129. code_puppy/mcp_/registry.py +451 -0
  130. code_puppy/mcp_/retry_manager.py +337 -0
  131. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  132. code_puppy/mcp_/status_tracker.py +355 -0
  133. code_puppy/mcp_/system_tools.py +209 -0
  134. code_puppy/mcp_prompts/__init__.py +1 -0
  135. code_puppy/mcp_prompts/hook_creator.py +103 -0
  136. code_puppy/messaging/__init__.py +255 -0
  137. code_puppy/messaging/bus.py +613 -0
  138. code_puppy/messaging/commands.py +167 -0
  139. code_puppy/messaging/markdown_patches.py +57 -0
  140. code_puppy/messaging/message_queue.py +361 -0
  141. code_puppy/messaging/messages.py +569 -0
  142. code_puppy/messaging/queue_console.py +271 -0
  143. code_puppy/messaging/renderers.py +311 -0
  144. code_puppy/messaging/rich_renderer.py +1153 -0
  145. code_puppy/messaging/spinner/__init__.py +83 -0
  146. code_puppy/messaging/spinner/console_spinner.py +240 -0
  147. code_puppy/messaging/spinner/spinner_base.py +96 -0
  148. code_puppy/messaging/subagent_console.py +460 -0
  149. code_puppy/model_factory.py +848 -0
  150. code_puppy/model_switching.py +63 -0
  151. code_puppy/model_utils.py +168 -0
  152. code_puppy/models.json +130 -0
  153. code_puppy/models_dev_api.json +1 -0
  154. code_puppy/models_dev_parser.py +592 -0
  155. code_puppy/plugins/__init__.py +186 -0
  156. code_puppy/plugins/agent_skills/__init__.py +22 -0
  157. code_puppy/plugins/agent_skills/config.py +175 -0
  158. code_puppy/plugins/agent_skills/discovery.py +136 -0
  159. code_puppy/plugins/agent_skills/downloader.py +392 -0
  160. code_puppy/plugins/agent_skills/installer.py +22 -0
  161. code_puppy/plugins/agent_skills/metadata.py +219 -0
  162. code_puppy/plugins/agent_skills/prompt_builder.py +100 -0
  163. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  164. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  165. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  166. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  167. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  168. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  169. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  170. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  171. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  172. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  173. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  174. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  175. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  176. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  177. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  178. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  179. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  180. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  181. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  182. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  183. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  184. code_puppy/plugins/chatgpt_oauth/test_plugin.py +295 -0
  185. code_puppy/plugins/chatgpt_oauth/utils.py +499 -0
  186. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  187. code_puppy/plugins/claude_code_hooks/config.py +131 -0
  188. code_puppy/plugins/claude_code_hooks/register_callbacks.py +163 -0
  189. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  190. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  191. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  192. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  193. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  194. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  195. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  196. code_puppy/plugins/claude_code_oauth/utils.py +601 -0
  197. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  198. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  199. code_puppy/plugins/example_custom_command/README.md +280 -0
  200. code_puppy/plugins/example_custom_command/register_callbacks.py +48 -0
  201. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  202. code_puppy/plugins/file_permission_handler/register_callbacks.py +528 -0
  203. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  204. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  205. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  206. code_puppy/plugins/hook_creator/__init__.py +1 -0
  207. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  208. code_puppy/plugins/hook_manager/__init__.py +1 -0
  209. code_puppy/plugins/hook_manager/config.py +277 -0
  210. code_puppy/plugins/hook_manager/hooks_menu.py +551 -0
  211. code_puppy/plugins/hook_manager/register_callbacks.py +205 -0
  212. code_puppy/plugins/oauth_puppy_html.py +224 -0
  213. code_puppy/plugins/scheduler/__init__.py +1 -0
  214. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  215. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  216. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  217. code_puppy/plugins/shell_safety/__init__.py +6 -0
  218. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  219. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  220. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  221. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  222. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  223. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  224. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  225. code_puppy/plugins/universal_constructor/models.py +138 -0
  226. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  227. code_puppy/plugins/universal_constructor/registry.py +302 -0
  228. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  229. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  230. code_puppy/pydantic_patches.py +317 -0
  231. code_puppy/reopenable_async_client.py +232 -0
  232. code_puppy/round_robin_model.py +150 -0
  233. code_puppy/scheduler/__init__.py +41 -0
  234. code_puppy/scheduler/__main__.py +9 -0
  235. code_puppy/scheduler/cli.py +118 -0
  236. code_puppy/scheduler/config.py +126 -0
  237. code_puppy/scheduler/daemon.py +280 -0
  238. code_puppy/scheduler/executor.py +155 -0
  239. code_puppy/scheduler/platform.py +19 -0
  240. code_puppy/scheduler/platform_unix.py +22 -0
  241. code_puppy/scheduler/platform_win.py +32 -0
  242. code_puppy/session_storage.py +338 -0
  243. code_puppy/status_display.py +257 -0
  244. code_puppy/summarization_agent.py +176 -0
  245. code_puppy/terminal_utils.py +418 -0
  246. code_puppy/tools/__init__.py +470 -0
  247. code_puppy/tools/agent_tools.py +616 -0
  248. code_puppy/tools/ask_user_question/__init__.py +26 -0
  249. code_puppy/tools/ask_user_question/constants.py +73 -0
  250. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  251. code_puppy/tools/ask_user_question/handler.py +232 -0
  252. code_puppy/tools/ask_user_question/models.py +304 -0
  253. code_puppy/tools/ask_user_question/registration.py +36 -0
  254. code_puppy/tools/ask_user_question/renderers.py +309 -0
  255. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  256. code_puppy/tools/ask_user_question/theme.py +155 -0
  257. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  258. code_puppy/tools/browser/__init__.py +37 -0
  259. code_puppy/tools/browser/browser_control.py +289 -0
  260. code_puppy/tools/browser/browser_interactions.py +545 -0
  261. code_puppy/tools/browser/browser_locators.py +640 -0
  262. code_puppy/tools/browser/browser_manager.py +378 -0
  263. code_puppy/tools/browser/browser_navigation.py +251 -0
  264. code_puppy/tools/browser/browser_screenshot.py +179 -0
  265. code_puppy/tools/browser/browser_scripts.py +462 -0
  266. code_puppy/tools/browser/browser_workflows.py +221 -0
  267. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  268. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  269. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  270. code_puppy/tools/browser/terminal_tools.py +525 -0
  271. code_puppy/tools/command_runner.py +1346 -0
  272. code_puppy/tools/common.py +1409 -0
  273. code_puppy/tools/display.py +84 -0
  274. code_puppy/tools/file_modifications.py +739 -0
  275. code_puppy/tools/file_operations.py +802 -0
  276. code_puppy/tools/scheduler_tools.py +412 -0
  277. code_puppy/tools/skills_tools.py +251 -0
  278. code_puppy/tools/subagent_context.py +158 -0
  279. code_puppy/tools/tools_content.py +51 -0
  280. code_puppy/tools/universal_constructor.py +889 -0
  281. code_puppy/uvx_detection.py +242 -0
  282. code_puppy/version_checker.py +82 -0
  283. newcode-0.1.1.data/data/code_puppy/models.json +130 -0
  284. newcode-0.1.1.data/data/code_puppy/models_dev_api.json +1 -0
  285. newcode-0.1.1.dist-info/METADATA +154 -0
  286. newcode-0.1.1.dist-info/RECORD +289 -0
  287. newcode-0.1.1.dist-info/WHEEL +4 -0
  288. newcode-0.1.1.dist-info/entry_points.txt +3 -0
  289. newcode-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,601 @@
1
+ """Utility helpers for the Claude Code OAuth plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ import logging
9
+ import re
10
+ import secrets
11
+ import time
12
+ from dataclasses import dataclass
13
+ from typing import Any, Dict, List, Optional, Tuple, Union
14
+ from urllib.parse import urlencode
15
+
16
+ import requests
17
+
18
+ from .config import (
19
+ CLAUDE_CODE_OAUTH_CONFIG,
20
+ get_claude_models_path,
21
+ get_token_storage_path,
22
+ )
23
+
24
+ # Proactive refresh buffer default (seconds). Actual buffer is dynamic
25
+ # based on expires_in to avoid overly aggressive refreshes.
26
+ TOKEN_REFRESH_BUFFER_SECONDS = 300
27
+ MIN_REFRESH_BUFFER_SECONDS = 30
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ @dataclass
33
+ class OAuthContext:
34
+ """Runtime state for an in-progress OAuth flow."""
35
+
36
+ state: str
37
+ code_verifier: str
38
+ code_challenge: str
39
+ created_at: float
40
+ redirect_uri: Optional[str] = None
41
+
42
+
43
+ _oauth_context: Optional[OAuthContext] = None
44
+
45
+
46
+ def _urlsafe_b64encode(data: bytes) -> str:
47
+ return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
48
+
49
+
50
+ def _generate_code_verifier() -> str:
51
+ return _urlsafe_b64encode(secrets.token_bytes(64))
52
+
53
+
54
+ def _compute_code_challenge(code_verifier: str) -> str:
55
+ digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
56
+ return _urlsafe_b64encode(digest)
57
+
58
+
59
+ def prepare_oauth_context() -> OAuthContext:
60
+ """Create and cache a new OAuth PKCE context."""
61
+ global _oauth_context
62
+ state = secrets.token_urlsafe(32)
63
+ code_verifier = _generate_code_verifier()
64
+ code_challenge = _compute_code_challenge(code_verifier)
65
+ _oauth_context = OAuthContext(
66
+ state=state,
67
+ code_verifier=code_verifier,
68
+ code_challenge=code_challenge,
69
+ created_at=time.time(),
70
+ )
71
+ return _oauth_context
72
+
73
+
74
+ def get_oauth_context() -> Optional[OAuthContext]:
75
+ return _oauth_context
76
+
77
+
78
+ def clear_oauth_context() -> None:
79
+ global _oauth_context
80
+ _oauth_context = None
81
+
82
+
83
+ def assign_redirect_uri(context: OAuthContext, port: int) -> str:
84
+ """Assign redirect URI for the given OAuth context."""
85
+ if context is None:
86
+ raise RuntimeError("OAuth context cannot be None")
87
+
88
+ host = CLAUDE_CODE_OAUTH_CONFIG["redirect_host"].rstrip("/")
89
+ path = CLAUDE_CODE_OAUTH_CONFIG["redirect_path"].lstrip("/")
90
+ redirect_uri = f"{host}:{port}/{path}"
91
+ context.redirect_uri = redirect_uri
92
+ return redirect_uri
93
+
94
+
95
+ def build_authorization_url(context: OAuthContext) -> str:
96
+ """Return the Claude authorization URL with PKCE parameters."""
97
+ if not context.redirect_uri:
98
+ raise RuntimeError("Redirect URI has not been assigned for this OAuth context")
99
+
100
+ params = {
101
+ "response_type": "code",
102
+ "client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
103
+ "redirect_uri": context.redirect_uri,
104
+ "scope": CLAUDE_CODE_OAUTH_CONFIG["scope"],
105
+ "state": context.state,
106
+ "code": "true",
107
+ "code_challenge": context.code_challenge,
108
+ "code_challenge_method": "S256",
109
+ }
110
+ return f"{CLAUDE_CODE_OAUTH_CONFIG['auth_url']}?{urlencode(params)}"
111
+
112
+
113
+ def parse_authorization_code(raw_input: str) -> Tuple[str, Optional[str]]:
114
+ value = raw_input.strip()
115
+ if not value:
116
+ raise ValueError("Authorization code cannot be empty")
117
+
118
+ if "#" in value:
119
+ code, state = value.split("#", 1)
120
+ return code.strip(), state.strip() or None
121
+
122
+ parts = value.split()
123
+ if len(parts) == 2:
124
+ return parts[0].strip(), parts[1].strip() or None
125
+
126
+ return value, None
127
+
128
+
129
+ def load_stored_tokens() -> Optional[Dict[str, Any]]:
130
+ try:
131
+ token_path = get_token_storage_path()
132
+ if token_path.exists():
133
+ with open(token_path, "r", encoding="utf-8") as handle:
134
+ return json.load(handle)
135
+ except Exception as exc: # pragma: no cover - defensive logging
136
+ logger.error("Failed to load tokens: %s", exc)
137
+ return None
138
+
139
+
140
+ def _calculate_expires_at(expires_in: Optional[float]) -> Optional[float]:
141
+ if expires_in is None:
142
+ return None
143
+ try:
144
+ return time.time() + float(expires_in)
145
+ except (TypeError, ValueError):
146
+ return None
147
+
148
+
149
+ def _calculate_refresh_buffer(expires_in: Optional[float]) -> float:
150
+ default_buffer = float(TOKEN_REFRESH_BUFFER_SECONDS)
151
+ if expires_in is None:
152
+ return default_buffer
153
+ try:
154
+ expires_value = float(expires_in)
155
+ except (TypeError, ValueError):
156
+ return default_buffer
157
+ return min(default_buffer, max(MIN_REFRESH_BUFFER_SECONDS, expires_value * 0.1))
158
+
159
+
160
+ def _get_expires_at_value(tokens: Dict[str, Any]) -> Optional[float]:
161
+ expires_at = tokens.get("expires_at")
162
+ if expires_at is None:
163
+ return None
164
+ try:
165
+ return float(expires_at)
166
+ except (TypeError, ValueError):
167
+ return None
168
+
169
+
170
+ def _is_token_actually_expired(tokens: Dict[str, Any]) -> bool:
171
+ expires_at_value = _get_expires_at_value(tokens)
172
+ if expires_at_value is None:
173
+ return False
174
+ return time.time() >= expires_at_value
175
+
176
+
177
+ def is_token_expired(tokens: Dict[str, Any]) -> bool:
178
+ expires_at_value = _get_expires_at_value(tokens)
179
+ if expires_at_value is None:
180
+ return False
181
+ buffer_seconds = _calculate_refresh_buffer(tokens.get("expires_in"))
182
+ return time.time() >= expires_at_value - buffer_seconds
183
+
184
+
185
+ def update_claude_code_model_tokens(access_token: str) -> bool:
186
+ try:
187
+ claude_models = load_claude_models()
188
+ if not claude_models:
189
+ return False
190
+
191
+ updated = False
192
+ for config in claude_models.values():
193
+ if config.get("oauth_source") != "claude-code-plugin":
194
+ continue
195
+ custom_endpoint = config.get("custom_endpoint")
196
+ if not isinstance(custom_endpoint, dict):
197
+ continue
198
+ custom_endpoint["api_key"] = access_token
199
+ updated = True
200
+
201
+ if updated:
202
+ return save_claude_models(claude_models)
203
+ except Exception as exc: # pragma: no cover - defensive logging
204
+ logger.error("Failed to update Claude model tokens: %s", exc)
205
+ return False
206
+
207
+
208
+ def refresh_access_token(force: bool = False) -> Optional[str]:
209
+ tokens = load_stored_tokens()
210
+ if not tokens:
211
+ return None
212
+
213
+ if not force and not is_token_expired(tokens):
214
+ return tokens.get("access_token")
215
+
216
+ refresh_token = tokens.get("refresh_token")
217
+ if not refresh_token:
218
+ logger.debug("No refresh_token available")
219
+ return None
220
+
221
+ payload = {
222
+ "grant_type": "refresh_token",
223
+ "client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
224
+ "refresh_token": refresh_token,
225
+ }
226
+
227
+ headers = {
228
+ "Content-Type": "application/json",
229
+ "Accept": "application/json",
230
+ "anthropic-beta": "oauth-2025-04-20",
231
+ }
232
+
233
+ try:
234
+ response = requests.post(
235
+ CLAUDE_CODE_OAUTH_CONFIG["token_url"],
236
+ json=payload,
237
+ headers=headers,
238
+ timeout=30,
239
+ )
240
+ if response.status_code == 200:
241
+ new_tokens = response.json()
242
+ tokens["access_token"] = new_tokens.get("access_token")
243
+ tokens["refresh_token"] = new_tokens.get("refresh_token", refresh_token)
244
+ expires_in_value = new_tokens.get("expires_in")
245
+ if expires_in_value is None:
246
+ expires_in_value = tokens.get("expires_in")
247
+ if expires_in_value is not None:
248
+ tokens["expires_in"] = expires_in_value
249
+ tokens["expires_at"] = _calculate_expires_at(expires_in_value)
250
+ if save_tokens(tokens):
251
+ update_claude_code_model_tokens(tokens["access_token"])
252
+ return tokens["access_token"]
253
+ else:
254
+ logger.error(
255
+ "Token refresh failed: %s - %s", response.status_code, response.text
256
+ )
257
+ except Exception as exc: # pragma: no cover - defensive logging
258
+ logger.error("Token refresh error: %s", exc)
259
+ return None
260
+
261
+
262
+ def get_valid_access_token() -> Optional[str]:
263
+ tokens = load_stored_tokens()
264
+ if not tokens:
265
+ logger.debug("No stored Claude Code OAuth tokens found")
266
+ return None
267
+
268
+ access_token = tokens.get("access_token")
269
+ if not access_token:
270
+ logger.debug("No access_token in stored tokens")
271
+ return None
272
+
273
+ if is_token_expired(tokens):
274
+ logger.info("Claude Code OAuth token expired, attempting refresh")
275
+ refreshed = refresh_access_token()
276
+ if refreshed:
277
+ return refreshed
278
+ if not _is_token_actually_expired(tokens):
279
+ logger.warning(
280
+ "Claude Code token refresh failed; using existing access token until expiry"
281
+ )
282
+ return access_token
283
+ logger.warning("Claude Code token refresh failed")
284
+ return None
285
+
286
+ return access_token
287
+
288
+
289
+ def save_tokens(tokens: Dict[str, Any]) -> bool:
290
+ try:
291
+ token_path = get_token_storage_path()
292
+ with open(token_path, "w", encoding="utf-8") as handle:
293
+ json.dump(tokens, handle, indent=2)
294
+ token_path.chmod(0o600)
295
+ return True
296
+ except Exception as exc: # pragma: no cover - defensive logging
297
+ logger.error("Failed to save tokens: %s", exc)
298
+ return False
299
+
300
+
301
+ def load_claude_models() -> Dict[str, Any]:
302
+ try:
303
+ models_path = get_claude_models_path()
304
+ if models_path.exists():
305
+ with open(models_path, "r", encoding="utf-8") as handle:
306
+ return json.load(handle)
307
+ except Exception as exc: # pragma: no cover - defensive logging
308
+ logger.error("Failed to load Claude models: %s", exc)
309
+ return {}
310
+
311
+
312
+ def load_claude_models_filtered() -> Dict[str, Any]:
313
+ """Load Claude models and filter to only the latest versions.
314
+
315
+ This loads the stored models and applies the same filtering logic
316
+ used during saving to ensure only the latest haiku, sonnet, and opus
317
+ models are returned.
318
+ """
319
+ try:
320
+ all_models = load_claude_models()
321
+ if not all_models:
322
+ return {}
323
+
324
+ # Extract model names from the configuration
325
+ model_names = []
326
+ for name, config in all_models.items():
327
+ if config.get("oauth_source") == "claude-code-plugin":
328
+ model_names.append(config.get("name", ""))
329
+ else:
330
+ # For non-OAuth models, use the full key
331
+ model_names.append(name)
332
+
333
+ # Filter to only latest models
334
+ latest_names = set(
335
+ filter_latest_claude_models(
336
+ model_names, max_per_family={"default": 1, "opus": 3}
337
+ )
338
+ )
339
+
340
+ # Return only the filtered models
341
+ filtered_models = {}
342
+ for name, config in all_models.items():
343
+ model_name = config.get("name", name)
344
+ if model_name in latest_names:
345
+ filtered_models[name] = config
346
+
347
+ logger.info(
348
+ "Loaded %d models, filtered to %d latest models",
349
+ len(all_models),
350
+ len(filtered_models),
351
+ )
352
+ return filtered_models
353
+
354
+ except Exception as exc: # pragma: no cover - defensive logging
355
+ logger.error("Failed to load and filter Claude models: %s", exc)
356
+ return {}
357
+
358
+
359
+ def save_claude_models(models: Dict[str, Any]) -> bool:
360
+ try:
361
+ models_path = get_claude_models_path()
362
+ with open(models_path, "w", encoding="utf-8") as handle:
363
+ json.dump(models, handle, indent=2)
364
+ return True
365
+ except Exception as exc: # pragma: no cover - defensive logging
366
+ logger.error("Failed to save Claude models: %s", exc)
367
+ return False
368
+
369
+
370
+ def exchange_code_for_tokens(
371
+ auth_code: str, context: OAuthContext
372
+ ) -> Optional[Dict[str, Any]]:
373
+ if not context.redirect_uri:
374
+ raise RuntimeError("Redirect URI missing from OAuth context")
375
+
376
+ payload = {
377
+ "grant_type": "authorization_code",
378
+ "client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
379
+ "code": auth_code,
380
+ "state": context.state,
381
+ "code_verifier": context.code_verifier,
382
+ "redirect_uri": context.redirect_uri,
383
+ }
384
+
385
+ headers = {
386
+ "Content-Type": "application/json",
387
+ "Accept": "application/json",
388
+ "anthropic-beta": "oauth-2025-04-20",
389
+ }
390
+
391
+ logger.info("Exchanging code for tokens: %s", CLAUDE_CODE_OAUTH_CONFIG["token_url"])
392
+ logger.debug("Payload keys: %s", list(payload.keys()))
393
+ logger.debug("Headers: %s", headers)
394
+ try:
395
+ response = requests.post(
396
+ CLAUDE_CODE_OAUTH_CONFIG["token_url"],
397
+ json=payload,
398
+ headers=headers,
399
+ timeout=30,
400
+ )
401
+ logger.info("Token exchange response: %s", response.status_code)
402
+ logger.debug("Response body: %s", response.text)
403
+ if response.status_code == 200:
404
+ token_data = response.json()
405
+ token_data["expires_at"] = _calculate_expires_at(
406
+ token_data.get("expires_in")
407
+ )
408
+ return token_data
409
+ logger.error(
410
+ "Token exchange failed: %s - %s",
411
+ response.status_code,
412
+ response.text,
413
+ )
414
+ except Exception as exc: # pragma: no cover - defensive logging
415
+ logger.error("Token exchange error: %s", exc)
416
+ return None
417
+
418
+
419
+ def filter_latest_claude_models(
420
+ models: List[str], max_per_family: Union[int, Dict[str, int]] = 2
421
+ ) -> List[str]:
422
+ """Filter models to keep the top N latest haiku, sonnet, and opus.
423
+
424
+ Parses model names in the format claude-{family}-{major}-{minor}-{date}
425
+ and returns the top ``max_per_family`` versions of each family
426
+ (haiku, sonnet, opus), sorted newest-first.
427
+
428
+ Args:
429
+ models: List of model name strings to filter.
430
+ max_per_family: Either a single int applied to all families, or a dict
431
+ mapping family name to its limit (e.g. ``{"opus": 3}``). Families
432
+ not present in the dict fall back to ``"default"`` key, or ``2``.
433
+ """
434
+ # Collect all parsed models per family
435
+ # family -> list of (model_name, major, minor, date)
436
+ family_models: Dict[str, List[Tuple[str, int, int, int]]] = {}
437
+
438
+ for model_name in models:
439
+ if model_name == "claude-opus-4-6":
440
+ family_models.setdefault("opus", []).append((model_name, 4, 6, 20260205))
441
+ continue
442
+ # Match pattern: claude-{family}-{major}-{minor}-{date}
443
+ # Examples: claude-haiku-3-5-20241022, claude-sonnet-4-5-20250929
444
+ match = re.match(r"claude-(haiku|sonnet|opus)-(\d+)-(\d+)-(\d+)", model_name)
445
+ if not match:
446
+ # Also try pattern with dots: claude-{family}-{major}.{minor}-{date}
447
+ match = re.match(
448
+ r"claude-(haiku|sonnet|opus)-(\d+)\.(\d+)-(\d+)", model_name
449
+ )
450
+
451
+ if not match:
452
+ continue
453
+
454
+ family = match.group(1)
455
+ major = int(match.group(2))
456
+ minor = int(match.group(3))
457
+ date = int(match.group(4))
458
+
459
+ family_models.setdefault(family, []).append((model_name, major, minor, date))
460
+
461
+ # Sort each family descending and keep the top N
462
+ filtered: List[str] = []
463
+ for family, family_entries in family_models.items():
464
+ if isinstance(max_per_family, dict):
465
+ limit = max_per_family.get(family, max_per_family.get("default", 2))
466
+ else:
467
+ limit = max_per_family
468
+ family_entries.sort(key=lambda e: (e[1], e[2], e[3]), reverse=True)
469
+ for entry in family_entries[:limit]:
470
+ filtered.append(entry[0])
471
+
472
+ logger.info(
473
+ "Filtered %d models to %d latest models (max_per_family=%s): %s",
474
+ len(models),
475
+ len(filtered),
476
+ max_per_family,
477
+ filtered,
478
+ )
479
+ return filtered
480
+
481
+
482
+ def fetch_claude_code_models(access_token: str) -> Optional[List[str]]:
483
+ try:
484
+ api_url = f"{CLAUDE_CODE_OAUTH_CONFIG['api_base_url']}/v1/models"
485
+ headers = {
486
+ "Authorization": f"Bearer {access_token}",
487
+ "Content-Type": "application/json",
488
+ "anthropic-beta": "oauth-2025-04-20",
489
+ "anthropic-version": CLAUDE_CODE_OAUTH_CONFIG.get(
490
+ "anthropic_version", "2023-06-01"
491
+ ),
492
+ }
493
+ response = requests.get(api_url, headers=headers, timeout=30)
494
+ if response.status_code == 200:
495
+ data = response.json()
496
+ if isinstance(data.get("data"), list):
497
+ models: List[str] = []
498
+ for model in data["data"]:
499
+ name = model.get("id") or model.get("name")
500
+ if name:
501
+ models.append(name)
502
+ return models
503
+ else:
504
+ logger.error(
505
+ "Failed to fetch models: %s - %s",
506
+ response.status_code,
507
+ response.text,
508
+ )
509
+ except Exception as exc: # pragma: no cover - defensive logging
510
+ logger.error("Error fetching Claude Code models: %s", exc)
511
+ return None
512
+
513
+
514
+ def _build_model_entry(model_name: str, access_token: str, context_length: int) -> dict:
515
+ """Build a single model config entry for claude_models.json."""
516
+ supported_settings = [
517
+ "temperature",
518
+ "extended_thinking",
519
+ "budget_tokens",
520
+ "interleaved_thinking",
521
+ ]
522
+
523
+ # Opus 4-6 models support the effort setting
524
+ lower = model_name.lower()
525
+ if "opus-4-6" in lower or "4-6-opus" in lower:
526
+ supported_settings.append("effort")
527
+
528
+ return {
529
+ "type": "claude_code",
530
+ "name": model_name,
531
+ "custom_endpoint": {
532
+ "url": CLAUDE_CODE_OAUTH_CONFIG["api_base_url"],
533
+ "api_key": access_token,
534
+ "headers": {
535
+ "anthropic-beta": "oauth-2025-04-20,interleaved-thinking-2025-05-14",
536
+ "x-app": "cli",
537
+ "User-Agent": "claude-cli/2.0.61 (external, cli)",
538
+ },
539
+ },
540
+ "context_length": context_length,
541
+ "oauth_source": "claude-code-plugin",
542
+ "supported_settings": supported_settings,
543
+ }
544
+
545
+
546
+ def add_models_to_extra_config(models: List[str]) -> bool:
547
+ try:
548
+ # Filter to only latest haiku, sonnet, and opus models
549
+ filtered_models = filter_latest_claude_models(
550
+ models, max_per_family={"default": 1, "opus": 3}
551
+ )
552
+
553
+ # Start fresh - overwrite the file on every auth instead of loading existing
554
+ claude_models = {}
555
+ added = 0
556
+ access_token = get_valid_access_token() or ""
557
+ prefix = CLAUDE_CODE_OAUTH_CONFIG["prefix"]
558
+ default_ctx = CLAUDE_CODE_OAUTH_CONFIG["default_context_length"]
559
+ long_ctx = CLAUDE_CODE_OAUTH_CONFIG["long_context_length"]
560
+ long_ctx_models = CLAUDE_CODE_OAUTH_CONFIG["long_context_models"]
561
+
562
+ for model_name in filtered_models:
563
+ prefixed = f"{prefix}{model_name}"
564
+ claude_models[prefixed] = _build_model_entry(
565
+ model_name, access_token, default_ctx
566
+ )
567
+ added += 1
568
+
569
+ # Create a "-long" variant with extended context for eligible models
570
+ if model_name in long_ctx_models:
571
+ long_prefixed = f"{prefix}{model_name}-long"
572
+ claude_models[long_prefixed] = _build_model_entry(
573
+ model_name, access_token, long_ctx
574
+ )
575
+ added += 1
576
+
577
+ if save_claude_models(claude_models):
578
+ logger.info("Added %s Claude Code models", added)
579
+ return True
580
+ except Exception as exc: # pragma: no cover - defensive logging
581
+ logger.error("Error adding models to config: %s", exc)
582
+ return False
583
+
584
+
585
+ def remove_claude_code_models() -> int:
586
+ try:
587
+ claude_models = load_claude_models()
588
+ to_remove = [
589
+ name
590
+ for name, config in claude_models.items()
591
+ if config.get("oauth_source") == "claude-code-plugin"
592
+ ]
593
+ if not to_remove:
594
+ return 0
595
+ for model_name in to_remove:
596
+ claude_models.pop(model_name, None)
597
+ if save_claude_models(claude_models):
598
+ return len(to_remove)
599
+ except Exception as exc: # pragma: no cover - defensive logging
600
+ logger.error("Error removing Claude Code models: %s", exc)
601
+ return 0
File without changes