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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
code_puppy/http_utils.py CHANGED
@@ -4,29 +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 stop_after_attempt, wait_exponential
14
15
 
16
+ if TYPE_CHECKING:
17
+ import requests
15
18
  from code_puppy.config import get_http2
16
19
 
17
- try:
18
- from pydantic_ai.retries import (
19
- AsyncTenacityTransport,
20
- RetryConfig,
21
- TenacityTransport,
22
- wait_retry_after,
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")
23
52
  )
24
- except ImportError:
25
- # Fallback if pydantic_ai.retries is not available
26
- AsyncTenacityTransport = None
27
- RetryConfig = None
28
- TenacityTransport = None
29
- wait_retry_after = None
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,
81
+ )
82
+
30
83
 
31
84
  try:
32
85
  from .reopenable_async_client import ReopenableAsyncClient
@@ -34,14 +87,104 @@ except ImportError:
34
87
  ReopenableAsyncClient = None
35
88
 
36
89
  try:
37
- from .messaging import emit_info
90
+ from .messaging import emit_info, emit_warning
38
91
  except ImportError:
39
92
  # Fallback if messaging system is not available
40
93
  def emit_info(content: str, **metadata):
41
94
  pass # No-op if messaging system is not available
42
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
43
121
 
44
- 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:
45
188
  # First check if SSL_CERT_FILE environment variable is set
46
189
  ssl_cert_file = os.environ.get("SSL_CERT_FILE")
47
190
  if ssl_cert_file and os.path.exists(ssl_cert_file):
@@ -61,42 +204,14 @@ def create_client(
61
204
  http2_enabled = get_http2()
62
205
 
63
206
  # If retry components are available, create a client with retry transport
64
- if TenacityTransport and RetryConfig and wait_retry_after:
65
-
66
- def should_retry_status(response):
67
- """Raise exceptions for retryable HTTP status codes."""
68
- if response.status_code in retry_status_codes:
69
- emit_info(
70
- f"HTTP retry: Retrying request due to status code {response.status_code}"
71
- )
72
- return True
73
-
74
- transport = TenacityTransport(
75
- config=RetryConfig(
76
- retry=lambda e: isinstance(e, httpx.HTTPStatusError)
77
- and e.response.status_code in retry_status_codes,
78
- wait=wait_retry_after(
79
- fallback_strategy=wait_exponential(multiplier=1, max=60),
80
- max_wait=300,
81
- ),
82
- stop=stop_after_attempt(10),
83
- reraise=True,
84
- ),
85
- validate_response=should_retry_status,
86
- )
87
-
88
- return httpx.Client(
89
- transport=transport,
90
- verify=verify,
91
- headers=headers or {},
92
- timeout=timeout,
93
- http2=http2_enabled,
94
- )
95
- else:
96
- # Fallback to regular client if retry components are not available
97
- return httpx.Client(
98
- verify=verify, headers=headers or {}, timeout=timeout, http2=http2_enabled
99
- )
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
+ )
100
215
 
101
216
 
102
217
  def create_async_client(
@@ -105,45 +220,26 @@ def create_async_client(
105
220
  headers: Optional[Dict[str, str]] = None,
106
221
  retry_status_codes: tuple = (429, 502, 503, 504),
107
222
  ) -> httpx.AsyncClient:
108
- if verify is None:
109
- verify = get_cert_bundle_path()
110
-
111
- # Check if HTTP/2 is enabled in config
112
- http2_enabled = get_http2()
223
+ config = _resolve_proxy_config(verify)
113
224
 
114
- # If retry components are available, create a client with retry transport
115
- if AsyncTenacityTransport and RetryConfig and wait_retry_after:
116
-
117
- def should_retry_status(response):
118
- """Raise exceptions for retryable HTTP status codes."""
119
- if response.status_code in retry_status_codes:
120
- emit_info(
121
- f"HTTP retry: Retrying request due to status code {response.status_code}"
122
- )
123
- return True
124
-
125
- transport = AsyncTenacityTransport(
126
- config=RetryConfig(
127
- retry=lambda e: isinstance(e, httpx.HTTPStatusError)
128
- and e.response.status_code in retry_status_codes,
129
- wait=wait_retry_after(10),
130
- stop=stop_after_attempt(10),
131
- reraise=True,
132
- ),
133
- validate_response=should_retry_status,
134
- )
135
-
136
- return httpx.AsyncClient(
137
- transport=transport,
138
- verify=verify,
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,
139
230
  headers=headers or {},
140
231
  timeout=timeout,
141
- http2=http2_enabled,
232
+ http2=config.http2_enabled,
233
+ trust_env=config.trust_env,
142
234
  )
143
235
  else:
144
- # Fallback to regular client if retry components are not available
145
236
  return httpx.AsyncClient(
146
- verify=verify, headers=headers or {}, timeout=timeout, http2=http2_enabled
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,
147
243
  )
148
244
 
149
245
 
@@ -151,7 +247,9 @@ def create_requests_session(
151
247
  timeout: float = 5.0,
152
248
  verify: Union[bool, str] = None,
153
249
  headers: Optional[Dict[str, str]] = None,
154
- ) -> requests.Session:
250
+ ) -> "requests.Session":
251
+ import requests
252
+
155
253
  session = requests.Session()
156
254
 
157
255
  if verify is None:
@@ -193,75 +291,39 @@ def create_reopenable_async_client(
193
291
  headers: Optional[Dict[str, str]] = None,
194
292
  retry_status_codes: tuple = (429, 502, 503, 504),
195
293
  ) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
196
- if verify is None:
197
- verify = get_cert_bundle_path()
198
-
199
- # Check if HTTP/2 is enabled in config
200
- http2_enabled = get_http2()
201
-
202
- # If retry components are available, create a client with retry transport
203
- if AsyncTenacityTransport and RetryConfig and wait_retry_after:
204
-
205
- def should_retry_status(response):
206
- """Raise exceptions for retryable HTTP status codes."""
207
- if response.status_code in retry_status_codes:
208
- emit_info(
209
- f"HTTP retry: Retrying request due to status code {response.status_code}"
210
- )
211
- return True
212
-
213
- transport = AsyncTenacityTransport(
214
- config=RetryConfig(
215
- retry=lambda e: isinstance(e, httpx.HTTPStatusError)
216
- and e.response.status_code in retry_status_codes,
217
- wait=wait_retry_after(
218
- fallback_strategy=wait_exponential(multiplier=1, max=60),
219
- max_wait=300,
220
- ),
221
- stop=stop_after_attempt(10),
222
- reraise=True,
223
- ),
224
- 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
225
308
  )
226
-
227
- if ReopenableAsyncClient is not None:
228
- return ReopenableAsyncClient(
229
- transport=transport,
230
- verify=verify,
231
- headers=headers or {},
232
- timeout=timeout,
233
- http2=http2_enabled,
234
- )
235
- else:
236
- # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
237
- return httpx.AsyncClient(
238
- transport=transport,
239
- verify=verify,
240
- headers=headers or {},
241
- timeout=timeout,
242
- http2=http2_enabled,
243
- )
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)
244
313
  else:
245
- # Fallback to regular clients if retry components are not available
246
- if ReopenableAsyncClient is not None:
247
- return ReopenableAsyncClient(
248
- verify=verify,
249
- headers=headers or {},
250
- timeout=timeout,
251
- http2=http2_enabled,
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
252
318
  )
253
319
  else:
254
- # Fallback to regular AsyncClient if ReopenableAsyncClient is not available
255
- return httpx.AsyncClient(
256
- verify=verify,
257
- headers=headers or {},
258
- timeout=timeout,
259
- http2=http2_enabled,
260
- )
320
+ return httpx.AsyncClient(**base_kwargs)
261
321
 
262
322
 
263
323
  def is_cert_bundle_available() -> bool:
264
324
  cert_path = get_cert_bundle_path()
325
+ if cert_path is None:
326
+ return False
265
327
  return os.path.exists(cert_path) and os.path.isfile(cert_path)
266
328
 
267
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()