code-puppy 0.0.169__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  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 +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/http_utils.py CHANGED
@@ -4,27 +4,82 @@ HTTP utilities module for code-puppy.
4
4
  This module provides functions for creating properly configured HTTP clients.
5
5
  """
6
6
 
7
+ import asyncio
7
8
  import os
8
9
  import socket
9
- from typing import Dict, Optional, Union
10
+ import time
11
+ from dataclasses import dataclass
12
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
10
13
 
11
14
  import httpx
12
- import requests
13
- from tenacity import retry_if_exception_type, stop_after_attempt, wait_exponential
14
15
 
15
- try:
16
- from pydantic_ai.retries import (
17
- AsyncTenacityTransport,
18
- RetryConfig,
19
- TenacityTransport,
20
- wait_retry_after,
16
+ if TYPE_CHECKING:
17
+ import requests
18
+ from code_puppy.config import get_http2
19
+
20
+
21
+ @dataclass
22
+ class ProxyConfig:
23
+ """Configuration for proxy and SSL settings."""
24
+
25
+ verify: Union[bool, str, None]
26
+ trust_env: bool
27
+ proxy_url: str | None
28
+ disable_retry: bool
29
+ http2_enabled: bool
30
+
31
+
32
+ def _resolve_proxy_config(verify: Union[bool, str, None] = None) -> ProxyConfig:
33
+ """Resolve proxy, SSL, and retry settings from environment.
34
+
35
+ This centralizes the logic for detecting proxies, determining SSL verification,
36
+ and checking if retry transport should be disabled.
37
+ """
38
+ if verify is None:
39
+ verify = get_cert_bundle_path()
40
+
41
+ http2_enabled = get_http2()
42
+
43
+ disable_retry = os.environ.get(
44
+ "CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
45
+ ).lower() in ("1", "true", "yes")
46
+
47
+ has_proxy = bool(
48
+ os.environ.get("HTTP_PROXY")
49
+ or os.environ.get("HTTPS_PROXY")
50
+ or os.environ.get("http_proxy")
51
+ or os.environ.get("https_proxy")
52
+ )
53
+
54
+ # Determine trust_env and verify based on proxy/retry settings
55
+ if disable_retry:
56
+ # Test mode: disable SSL verification for proxy testing
57
+ verify = False
58
+ trust_env = True
59
+ elif has_proxy:
60
+ # Production proxy: keep SSL verification enabled
61
+ trust_env = True
62
+ else:
63
+ trust_env = False
64
+
65
+ # Extract proxy URL
66
+ proxy_url = None
67
+ if has_proxy:
68
+ proxy_url = (
69
+ os.environ.get("HTTPS_PROXY")
70
+ or os.environ.get("https_proxy")
71
+ or os.environ.get("HTTP_PROXY")
72
+ or os.environ.get("http_proxy")
73
+ )
74
+
75
+ return ProxyConfig(
76
+ verify=verify,
77
+ trust_env=trust_env,
78
+ proxy_url=proxy_url,
79
+ disable_retry=disable_retry,
80
+ http2_enabled=http2_enabled,
21
81
  )
22
- except ImportError:
23
- # Fallback if pydantic_ai.retries is not available
24
- AsyncTenacityTransport = None
25
- RetryConfig = None
26
- TenacityTransport = None
27
- wait_retry_after = None
82
+
28
83
 
29
84
  try:
30
85
  from .reopenable_async_client import ReopenableAsyncClient
@@ -32,14 +87,104 @@ except ImportError:
32
87
  ReopenableAsyncClient = None
33
88
 
34
89
  try:
35
- from .messaging import emit_info
90
+ from .messaging import emit_info, emit_warning
36
91
  except ImportError:
37
92
  # Fallback if messaging system is not available
38
93
  def emit_info(content: str, **metadata):
39
94
  pass # No-op if messaging system is not available
40
95
 
96
+ def emit_warning(content: str, **metadata):
97
+ pass
98
+
99
+
100
+ class RetryingAsyncClient(httpx.AsyncClient):
101
+ """AsyncClient with built-in rate limit handling (429) and retries.
102
+
103
+ This replaces the Tenacity transport with a more direct subclass implementation,
104
+ which plays nicer with proxies and custom transports (like Antigravity).
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ retry_status_codes: tuple = (429, 502, 503, 504),
110
+ max_retries: int = 5,
111
+ **kwargs,
112
+ ):
113
+ super().__init__(**kwargs)
114
+ self.retry_status_codes = retry_status_codes
115
+ self.max_retries = max_retries
116
+
117
+ async def send(self, request: httpx.Request, **kwargs: Any) -> httpx.Response:
118
+ """Send request with automatic retries for rate limits and server errors."""
119
+ last_response = None
120
+ last_exception = None
41
121
 
42
- def get_cert_bundle_path() -> str:
122
+ for attempt in range(self.max_retries + 1):
123
+ try:
124
+ response = await super().send(request, **kwargs)
125
+ last_response = response
126
+
127
+ # Check for retryable status
128
+ if response.status_code not in self.retry_status_codes:
129
+ return response
130
+
131
+ # Close response if we're going to retry
132
+ await response.aclose()
133
+
134
+ # Determine wait time
135
+ wait_time = 1.0 * (
136
+ 2**attempt
137
+ ) # Default exponential backoff: 1s, 2s, 4s...
138
+
139
+ # Check Retry-After header
140
+ retry_after = response.headers.get("Retry-After")
141
+ if retry_after:
142
+ try:
143
+ wait_time = float(retry_after)
144
+ except ValueError:
145
+ # Try parsing http-date
146
+ from email.utils import parsedate_to_datetime
147
+
148
+ try:
149
+ date = parsedate_to_datetime(retry_after)
150
+ wait_time = date.timestamp() - time.time()
151
+ except Exception:
152
+ pass
153
+
154
+ # Cap wait time
155
+ wait_time = max(0.5, min(wait_time, 60.0))
156
+
157
+ if attempt < self.max_retries:
158
+ emit_info(
159
+ f"HTTP retry: {response.status_code} received. Waiting {wait_time:.1f}s (attempt {attempt + 1}/{self.max_retries})"
160
+ )
161
+ await asyncio.sleep(wait_time)
162
+
163
+ except (httpx.ConnectError, httpx.ReadTimeout, httpx.PoolTimeout) as e:
164
+ last_exception = e
165
+ wait_time = 1.0 * (2**attempt)
166
+ if attempt < self.max_retries:
167
+ emit_warning(
168
+ f"HTTP connection error: {e}. Retrying in {wait_time}s..."
169
+ )
170
+ await asyncio.sleep(wait_time)
171
+ else:
172
+ raise
173
+ except Exception:
174
+ raise
175
+
176
+ # Return last response (even if it's an error status)
177
+ if last_response:
178
+ return last_response
179
+
180
+ # Should catch this in loop, but just in case
181
+ if last_exception:
182
+ raise last_exception
183
+
184
+ return last_response
185
+
186
+
187
+ def get_cert_bundle_path() -> str | None:
43
188
  # First check if SSL_CERT_FILE environment variable is set
44
189
  ssl_cert_file = os.environ.get("SSL_CERT_FILE")
45
190
  if ssl_cert_file and os.path.exists(ssl_cert_file):
@@ -55,31 +200,18 @@ def create_client(
55
200
  if verify is None:
56
201
  verify = get_cert_bundle_path()
57
202
 
203
+ # Check if HTTP/2 is enabled in config
204
+ http2_enabled = get_http2()
205
+
58
206
  # If retry components are available, create a client with retry transport
59
- if TenacityTransport and RetryConfig and wait_retry_after:
60
- def should_retry_status(response):
61
- """Raise exceptions for retryable HTTP status codes."""
62
- if response.status_code in retry_status_codes:
63
- emit_info(f"HTTP retry: Retrying request due to status code {response.status_code}")
64
- response.raise_for_status()
65
-
66
- transport = TenacityTransport(
67
- config=RetryConfig(
68
- retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
69
- wait=wait_retry_after(
70
- fallback_strategy=wait_exponential(multiplier=1, max=60),
71
- max_wait=300
72
- ),
73
- stop=stop_after_attempt(10),
74
- reraise=True
75
- ),
76
- validate_response=should_retry_status
77
- )
78
-
79
- return httpx.Client(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
80
- else:
81
- # Fallback to regular client if retry components are not available
82
- return httpx.Client(verify=verify, headers=headers or {}, timeout=timeout)
207
+ # Note: TenacityTransport was removed. For now we just return a standard client.
208
+ # Future TODO: Implement RetryingClient(httpx.Client) if needed.
209
+ return httpx.Client(
210
+ verify=verify,
211
+ headers=headers or {},
212
+ timeout=timeout,
213
+ http2=http2_enabled,
214
+ )
83
215
 
84
216
 
85
217
  def create_async_client(
@@ -88,41 +220,36 @@ def create_async_client(
88
220
  headers: Optional[Dict[str, str]] = None,
89
221
  retry_status_codes: tuple = (429, 502, 503, 504),
90
222
  ) -> httpx.AsyncClient:
91
- if verify is None:
92
- verify = get_cert_bundle_path()
93
-
94
- # If retry components are available, create a client with retry transport
95
- if AsyncTenacityTransport and RetryConfig and wait_retry_after:
96
- def should_retry_status(response):
97
- """Raise exceptions for retryable HTTP status codes."""
98
- if response.status_code in retry_status_codes:
99
- emit_info(f"HTTP retry: Retrying request due to status code {response.status_code}")
100
- response.raise_for_status()
101
-
102
- transport = AsyncTenacityTransport(
103
- config=RetryConfig(
104
- retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
105
- wait=wait_retry_after(
106
- fallback_strategy=wait_exponential(multiplier=1, max=60),
107
- max_wait=300
108
- ),
109
- stop=stop_after_attempt(10),
110
- reraise=True
111
- ),
112
- validate_response=should_retry_status
223
+ config = _resolve_proxy_config(verify)
224
+
225
+ if not config.disable_retry:
226
+ return RetryingAsyncClient(
227
+ retry_status_codes=retry_status_codes,
228
+ proxy=config.proxy_url,
229
+ verify=config.verify,
230
+ headers=headers or {},
231
+ timeout=timeout,
232
+ http2=config.http2_enabled,
233
+ trust_env=config.trust_env,
113
234
  )
114
-
115
- return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
116
235
  else:
117
- # Fallback to regular client if retry components are not available
118
- return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
236
+ return httpx.AsyncClient(
237
+ proxy=config.proxy_url,
238
+ verify=config.verify,
239
+ headers=headers or {},
240
+ timeout=timeout,
241
+ http2=config.http2_enabled,
242
+ trust_env=config.trust_env,
243
+ )
119
244
 
120
245
 
121
246
  def create_requests_session(
122
247
  timeout: float = 5.0,
123
248
  verify: Union[bool, str] = None,
124
249
  headers: Optional[Dict[str, str]] = None,
125
- ) -> requests.Session:
250
+ ) -> "requests.Session":
251
+ import requests
252
+
126
253
  session = requests.Session()
127
254
 
128
255
  if verify is None:
@@ -164,50 +291,39 @@ def create_reopenable_async_client(
164
291
  headers: Optional[Dict[str, str]] = None,
165
292
  retry_status_codes: tuple = (429, 502, 503, 504),
166
293
  ) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
167
- if verify is None:
168
- verify = get_cert_bundle_path()
169
-
170
- # If retry components are available, create a client with retry transport
171
- if AsyncTenacityTransport and RetryConfig and wait_retry_after:
172
- def should_retry_status(response):
173
- """Raise exceptions for retryable HTTP status codes."""
174
- if response.status_code in retry_status_codes:
175
- emit_info(f"HTTP retry: Retrying request due to status code {response.status_code}")
176
- response.raise_for_status()
177
-
178
- transport = AsyncTenacityTransport(
179
- config=RetryConfig(
180
- retry=lambda e: isinstance(e, httpx.HTTPStatusError) and e.response.status_code in retry_status_codes,
181
- wait=wait_retry_after(
182
- fallback_strategy=wait_exponential(multiplier=1, max=60),
183
- max_wait=300
184
- ),
185
- stop=stop_after_attempt(10),
186
- reraise=True
187
- ),
188
- validate_response=should_retry_status
294
+ config = _resolve_proxy_config(verify)
295
+
296
+ base_kwargs = {
297
+ "proxy": config.proxy_url,
298
+ "verify": config.verify,
299
+ "headers": headers or {},
300
+ "timeout": timeout,
301
+ "http2": config.http2_enabled,
302
+ "trust_env": config.trust_env,
303
+ }
304
+
305
+ if ReopenableAsyncClient is not None:
306
+ client_class = (
307
+ RetryingAsyncClient if not config.disable_retry else httpx.AsyncClient
189
308
  )
190
-
191
- if ReopenableAsyncClient is not None:
192
- return ReopenableAsyncClient(
193
- transport=transport, verify=verify, headers=headers or {}, timeout=timeout
194
- )
195
- else:
196
- # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
197
- return httpx.AsyncClient(transport=transport, verify=verify, headers=headers or {}, timeout=timeout)
309
+ kwargs = {**base_kwargs, "client_class": client_class}
310
+ if not config.disable_retry:
311
+ kwargs["retry_status_codes"] = retry_status_codes
312
+ return ReopenableAsyncClient(**kwargs)
198
313
  else:
199
- # Fallback to regular clients if retry components are not available
200
- if ReopenableAsyncClient is not None:
201
- return ReopenableAsyncClient(
202
- verify=verify, headers=headers or {}, timeout=timeout
314
+ # Fallback to RetryingAsyncClient or plain AsyncClient
315
+ if not config.disable_retry:
316
+ return RetryingAsyncClient(
317
+ retry_status_codes=retry_status_codes, **base_kwargs
203
318
  )
204
319
  else:
205
- # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
206
- return httpx.AsyncClient(verify=verify, headers=headers or {}, timeout=timeout)
320
+ return httpx.AsyncClient(**base_kwargs)
207
321
 
208
322
 
209
323
  def is_cert_bundle_available() -> bool:
210
324
  cert_path = get_cert_bundle_path()
325
+ if cert_path is None:
326
+ return False
211
327
  return os.path.exists(cert_path) and os.path.isfile(cert_path)
212
328
 
213
329
 
code_puppy/keymap.py ADDED
@@ -0,0 +1,128 @@
1
+ """Keymap configuration for code-puppy.
2
+
3
+ This module handles configurable keyboard shortcuts, starting with the
4
+ cancel_agent_key feature that allows users to override Ctrl+C with a
5
+ different key for cancelling agent tasks.
6
+ """
7
+
8
+ # Character codes for Ctrl+letter combinations (Ctrl+A = 0x01, Ctrl+Z = 0x1A)
9
+ KEY_CODES: dict[str, str] = {
10
+ "ctrl+a": "\x01",
11
+ "ctrl+b": "\x02",
12
+ "ctrl+c": "\x03",
13
+ "ctrl+d": "\x04",
14
+ "ctrl+e": "\x05",
15
+ "ctrl+f": "\x06",
16
+ "ctrl+g": "\x07",
17
+ "ctrl+h": "\x08",
18
+ "ctrl+i": "\x09",
19
+ "ctrl+j": "\x0a",
20
+ "ctrl+k": "\x0b",
21
+ "ctrl+l": "\x0c",
22
+ "ctrl+m": "\x0d",
23
+ "ctrl+n": "\x0e",
24
+ "ctrl+o": "\x0f",
25
+ "ctrl+p": "\x10",
26
+ "ctrl+q": "\x11",
27
+ "ctrl+r": "\x12",
28
+ "ctrl+s": "\x13",
29
+ "ctrl+t": "\x14",
30
+ "ctrl+u": "\x15",
31
+ "ctrl+v": "\x16",
32
+ "ctrl+w": "\x17",
33
+ "ctrl+x": "\x18",
34
+ "ctrl+y": "\x19",
35
+ "ctrl+z": "\x1a",
36
+ "escape": "\x1b",
37
+ }
38
+
39
+ # Valid keys for cancel_agent_key configuration
40
+ # NOTE: "escape" is excluded because it conflicts with ANSI escape sequences
41
+ # (arrow keys, F-keys, etc. all start with \x1b)
42
+ VALID_CANCEL_KEYS: set[str] = {
43
+ "ctrl+c",
44
+ "ctrl+k",
45
+ "ctrl+q",
46
+ }
47
+
48
+ DEFAULT_CANCEL_AGENT_KEY: str = "ctrl+c"
49
+
50
+
51
+ class KeymapError(Exception):
52
+ """Exception raised for keymap configuration errors."""
53
+
54
+
55
+ def get_cancel_agent_key() -> str:
56
+ """Get the configured cancel agent key from config.
57
+
58
+ On Windows when launched via uvx, this automatically returns "ctrl+k"
59
+ to work around uvx capturing Ctrl+C before it reaches Python.
60
+
61
+ Returns:
62
+ The key name (e.g., "ctrl+c", "ctrl+k") from config,
63
+ or the default if not configured.
64
+ """
65
+ from code_puppy.config import get_value
66
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
67
+
68
+ # On Windows + uvx, force ctrl+k to bypass uvx's SIGINT capture
69
+ if should_use_alternate_cancel_key():
70
+ return "ctrl+k"
71
+
72
+ key = get_value("cancel_agent_key")
73
+ if key is None or key.strip() == "":
74
+ return DEFAULT_CANCEL_AGENT_KEY
75
+ return key.strip().lower()
76
+
77
+
78
+ def validate_cancel_agent_key() -> None:
79
+ """Validate the configured cancel agent key.
80
+
81
+ Raises:
82
+ KeymapError: If the configured key is invalid.
83
+ """
84
+ key = get_cancel_agent_key()
85
+ if key not in VALID_CANCEL_KEYS:
86
+ valid_keys_str = ", ".join(sorted(VALID_CANCEL_KEYS))
87
+ raise KeymapError(
88
+ f"Invalid cancel_agent_key '{key}' in puppy.cfg. "
89
+ f"Valid options are: {valid_keys_str}"
90
+ )
91
+
92
+
93
+ def cancel_agent_uses_signal() -> bool:
94
+ """Check if the cancel agent key uses SIGINT (Ctrl+C).
95
+
96
+ Returns:
97
+ True if the cancel key is ctrl+c (uses SIGINT handler),
98
+ False if it uses keyboard listener approach.
99
+ """
100
+ return get_cancel_agent_key() == "ctrl+c"
101
+
102
+
103
+ def get_cancel_agent_char_code() -> str:
104
+ """Get the character code for the cancel agent key.
105
+
106
+ Returns:
107
+ The character code (e.g., "\x0b" for ctrl+k).
108
+
109
+ Raises:
110
+ KeymapError: If the key is not found in KEY_CODES.
111
+ """
112
+ key = get_cancel_agent_key()
113
+ if key not in KEY_CODES:
114
+ raise KeymapError(f"Unknown key '{key}' - no character code mapping found.")
115
+ return KEY_CODES[key]
116
+
117
+
118
+ def get_cancel_agent_display_name() -> str:
119
+ """Get a human-readable display name for the cancel agent key.
120
+
121
+ Returns:
122
+ A formatted display name like "Ctrl+K".
123
+ """
124
+ key = get_cancel_agent_key()
125
+ if key.startswith("ctrl+"):
126
+ letter = key.split("+")[1].upper()
127
+ return f"Ctrl+{letter}"
128
+ return key.upper()