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
@@ -0,0 +1,158 @@
1
+ """Sub-agent context management with async-safe state tracking.
2
+
3
+ This module provides context-aware tracking of sub-agent execution state using
4
+ Python's contextvars for async-safe isolation. This ensures that sub-agent state
5
+ is properly isolated across different async tasks and execution contexts.
6
+
7
+ ## Why ContextVars?
8
+
9
+ ContextVars provide automatic context isolation in async environments:
10
+ - Each async task gets its own copy of the context
11
+ - State changes in one task don't affect others
12
+ - Perfect for tracking execution depth in nested agent calls
13
+ - Token-based reset ensures proper cleanup even with exceptions
14
+
15
+ ## Usage Example:
16
+
17
+ ```python
18
+ from code_puppy.tools.subagent_context import subagent_context, is_subagent
19
+
20
+ # Main agent
21
+ print(is_subagent()) # False
22
+
23
+ async def run_subagent():
24
+ with subagent_context("retriever"):
25
+ print(is_subagent()) # True
26
+ print(get_subagent_name()) # "retriever"
27
+ print(get_subagent_depth()) # 1
28
+
29
+ # Nested sub-agent
30
+ with subagent_context("terrier"):
31
+ print(get_subagent_depth()) # 2
32
+ print(get_subagent_name()) # "terrier"
33
+
34
+ # Back to parent sub-agent
35
+ print(get_subagent_name()) # "retriever"
36
+ print(get_subagent_depth()) # 1
37
+
38
+ # After context exits
39
+ print(is_subagent()) # False
40
+ ```
41
+
42
+ ## Benefits:
43
+
44
+ 1. **Async Safety**: Multiple sub-agents can run concurrently without interference
45
+ 2. **Nested Support**: Properly handles sub-agents calling other sub-agents
46
+ 3. **Clean Restoration**: Token-based reset ensures state is restored even on errors
47
+ 4. **Zero Overhead**: When not in a sub-agent context, minimal performance impact
48
+ """
49
+
50
+ from contextlib import contextmanager
51
+ from contextvars import ContextVar
52
+ from typing import Generator
53
+
54
+ __all__ = [
55
+ "subagent_context",
56
+ "is_subagent",
57
+ "get_subagent_name",
58
+ "get_subagent_depth",
59
+ ]
60
+
61
+ # Track sub-agent depth (0 = main agent, 1+ = sub-agent)
62
+ _subagent_depth: ContextVar[int] = ContextVar("subagent_depth", default=0)
63
+
64
+ # Track current sub-agent name (None = main agent)
65
+ _subagent_name: ContextVar[str | None] = ContextVar("subagent_name", default=None)
66
+
67
+
68
+ @contextmanager
69
+ def subagent_context(agent_name: str) -> Generator[None, None, None]:
70
+ """Context manager for tracking sub-agent execution.
71
+
72
+ Increments the sub-agent depth and sets the current agent name on entry,
73
+ then restores the previous state on exit. Uses token-based reset for
74
+ proper async isolation and exception safety.
75
+
76
+ Args:
77
+ agent_name: Name of the sub-agent being executed (e.g., "retriever", "husky")
78
+
79
+ Yields:
80
+ None
81
+
82
+ Example:
83
+ >>> with subagent_context("retriever"):
84
+ ... assert is_subagent() is True
85
+ ... assert get_subagent_name() == "retriever"
86
+ >>> assert is_subagent() is False
87
+
88
+ Note:
89
+ Token-based reset ensures that even if an exception occurs, the context
90
+ is properly restored. This is especially important in async environments
91
+ where multiple tasks may be running concurrently.
92
+ """
93
+ # Get current depth for incrementing
94
+ current_depth = _subagent_depth.get()
95
+
96
+ # Set new values and save tokens for restoration
97
+ depth_token = _subagent_depth.set(current_depth + 1)
98
+ name_token = _subagent_name.set(agent_name)
99
+
100
+ try:
101
+ yield
102
+ finally:
103
+ # Use token-based reset for proper async isolation
104
+ # This ensures the context is restored even if an exception occurs
105
+ _subagent_depth.reset(depth_token)
106
+ _subagent_name.reset(name_token)
107
+
108
+
109
+ def is_subagent() -> bool:
110
+ """Check if currently executing within a sub-agent context.
111
+
112
+ Returns:
113
+ True if depth > 0 (inside a sub-agent), False otherwise (main agent)
114
+
115
+ Example:
116
+ >>> is_subagent()
117
+ False
118
+ >>> with subagent_context("retriever"):
119
+ ... is_subagent()
120
+ True
121
+ """
122
+ return _subagent_depth.get() > 0
123
+
124
+
125
+ def get_subagent_name() -> str | None:
126
+ """Get the name of the current sub-agent.
127
+
128
+ Returns:
129
+ Current sub-agent name, or None if in main agent context
130
+
131
+ Example:
132
+ >>> get_subagent_name()
133
+ None
134
+ >>> with subagent_context("husky"):
135
+ ... get_subagent_name()
136
+ 'husky'
137
+ """
138
+ return _subagent_name.get()
139
+
140
+
141
+ def get_subagent_depth() -> int:
142
+ """Get the current sub-agent nesting depth.
143
+
144
+ Returns:
145
+ Current depth level (0 = main agent, 1 = first-level sub-agent,
146
+ 2 = nested sub-agent, etc.)
147
+
148
+ Example:
149
+ >>> get_subagent_depth()
150
+ 0
151
+ >>> with subagent_context("retriever"):
152
+ ... get_subagent_depth()
153
+ 1
154
+ ... with subagent_context("terrier"):
155
+ ... get_subagent_depth()
156
+ 2
157
+ """
158
+ return _subagent_depth.get()
@@ -0,0 +1,242 @@
1
+ """Detect if code-puppy was launched via uvx on Windows.
2
+
3
+ This module provides utilities to detect the launch method of code-puppy,
4
+ specifically to handle signal differences when running via uvx on Windows.
5
+
6
+ On Windows, when launched via `uvx code-puppy`, Ctrl+C (SIGINT) gets captured
7
+ by uvx's process handling before reaching our Python process. To work around
8
+ this, we detect the uvx launch scenario and switch to Ctrl+K for cancellation.
9
+
10
+ Note: This issue is specific to uvx.exe, NOT uv.exe. Running via `uv run`
11
+ handles SIGINT correctly on Windows.
12
+
13
+ On non-Windows platforms, this is not an issue - Ctrl+C works fine with uvx.
14
+ """
15
+
16
+ import os
17
+ import platform
18
+ import sys
19
+ from functools import lru_cache
20
+ from typing import Optional
21
+
22
+ # Cache the detection result - it won't change during runtime
23
+ _uvx_detection_cache: Optional[bool] = None
24
+
25
+
26
+ def _get_parent_process_name_psutil(pid: int) -> Optional[str]:
27
+ """Get parent process name using psutil (if available).
28
+
29
+ Args:
30
+ pid: Process ID to get parent name for
31
+
32
+ Returns:
33
+ Parent process name (lowercase) or None if not found
34
+ """
35
+ try:
36
+ import psutil
37
+
38
+ proc = psutil.Process(pid)
39
+ parent = proc.parent()
40
+ if parent:
41
+ return parent.name().lower()
42
+ except Exception:
43
+ pass
44
+ return None
45
+
46
+
47
+ def _get_parent_process_chain_psutil() -> list[str]:
48
+ """Get the entire parent process chain using psutil.
49
+
50
+ Returns:
51
+ List of process names from current process up to init/System
52
+ """
53
+ chain = []
54
+ try:
55
+ import psutil
56
+
57
+ proc = psutil.Process(os.getpid())
58
+ while proc:
59
+ chain.append(proc.name().lower())
60
+ parent = proc.parent()
61
+ if parent is None or parent.pid in (0, proc.pid):
62
+ break
63
+ proc = parent
64
+ except Exception:
65
+ pass
66
+ return chain
67
+
68
+
69
+ def _get_parent_process_chain_windows_ctypes() -> list[str]:
70
+ """Get parent process chain on Windows using ctypes (no external deps).
71
+
72
+ This is a fallback when psutil is not available.
73
+
74
+ Returns:
75
+ List of process names from current process up to System
76
+ """
77
+ if platform.system() != "Windows":
78
+ return []
79
+
80
+ chain = []
81
+ try:
82
+ import ctypes
83
+ from ctypes import wintypes
84
+
85
+ # Windows API constants
86
+ TH32CS_SNAPPROCESS = 0x00000002
87
+ INVALID_HANDLE_VALUE = -1
88
+
89
+ class PROCESSENTRY32(ctypes.Structure):
90
+ _fields_ = [
91
+ ("dwSize", wintypes.DWORD),
92
+ ("cntUsage", wintypes.DWORD),
93
+ ("th32ProcessID", wintypes.DWORD),
94
+ ("th32DefaultHeapID", ctypes.POINTER(wintypes.ULONG)),
95
+ ("th32ModuleID", wintypes.DWORD),
96
+ ("cntThreads", wintypes.DWORD),
97
+ ("th32ParentProcessID", wintypes.DWORD),
98
+ ("pcPriClassBase", wintypes.LONG),
99
+ ("dwFlags", wintypes.DWORD),
100
+ ("szExeFile", ctypes.c_char * 260),
101
+ ]
102
+
103
+ kernel32 = ctypes.windll.kernel32
104
+
105
+ # Take a snapshot of all processes
106
+ snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
107
+ if snapshot == INVALID_HANDLE_VALUE:
108
+ return chain
109
+
110
+ try:
111
+ # Build a map of PID -> (parent_pid, exe_name)
112
+ process_map: dict[int, tuple[int, str]] = {}
113
+ pe = PROCESSENTRY32()
114
+ pe.dwSize = ctypes.sizeof(PROCESSENTRY32)
115
+
116
+ if kernel32.Process32First(snapshot, ctypes.byref(pe)):
117
+ while True:
118
+ pid = pe.th32ProcessID
119
+ parent_pid = pe.th32ParentProcessID
120
+ exe_name = pe.szExeFile.decode("utf-8", errors="ignore").lower()
121
+ process_map[pid] = (parent_pid, exe_name)
122
+
123
+ if not kernel32.Process32Next(snapshot, ctypes.byref(pe)):
124
+ break
125
+
126
+ # Traverse from current PID up the parent chain
127
+ current_pid = os.getpid()
128
+ visited = set() # Prevent infinite loops
129
+
130
+ while current_pid in process_map and current_pid not in visited:
131
+ visited.add(current_pid)
132
+ parent_pid, exe_name = process_map[current_pid]
133
+ chain.append(exe_name)
134
+
135
+ if parent_pid == 0 or parent_pid == current_pid:
136
+ break
137
+ current_pid = parent_pid
138
+
139
+ finally:
140
+ kernel32.CloseHandle(snapshot)
141
+
142
+ except Exception:
143
+ pass
144
+
145
+ return chain
146
+
147
+
148
+ def _get_parent_process_chain() -> list[str]:
149
+ """Get the parent process chain using best available method.
150
+
151
+ Returns:
152
+ List of process names from current process up to init/System
153
+ """
154
+ # Try psutil first (more reliable, cross-platform)
155
+ try:
156
+ import psutil # noqa: F401
157
+
158
+ return _get_parent_process_chain_psutil()
159
+ except ImportError:
160
+ pass
161
+
162
+ # Fall back to ctypes on Windows
163
+ if platform.system() == "Windows":
164
+ return _get_parent_process_chain_windows_ctypes()
165
+
166
+ return []
167
+
168
+
169
+ def _is_uvx_in_chain(chain: list[str]) -> bool:
170
+ """Check if uvx is in the process chain.
171
+
172
+ Note: We only check for uvx.exe, NOT uv.exe. The uv.exe binary
173
+ (used by `uv run`) handles SIGINT correctly on Windows, but
174
+ uvx.exe captures it before it reaches Python.
175
+
176
+ Args:
177
+ chain: List of process names (lowercase)
178
+
179
+ Returns:
180
+ True if uvx.exe is found in the chain
181
+ """
182
+ # Only uvx.exe has the SIGINT issue, not uv.exe
183
+ uvx_names = {"uvx.exe", "uvx"}
184
+ return any(name in uvx_names for name in chain)
185
+
186
+
187
+ @lru_cache(maxsize=1)
188
+ def is_launched_via_uvx() -> bool:
189
+ """Detect if code-puppy was launched via uvx.
190
+
191
+ Traverses the parent process chain to find uvx.exe or uv.exe.
192
+ Result is cached for the lifetime of the process.
193
+
194
+ Returns:
195
+ True if launched via uvx, False otherwise
196
+ """
197
+ chain = _get_parent_process_chain()
198
+ return _is_uvx_in_chain(chain)
199
+
200
+
201
+ def is_windows() -> bool:
202
+ """Check if we're running on Windows.
203
+
204
+ Returns:
205
+ True if running on Windows, False otherwise
206
+ """
207
+ return platform.system() == "Windows"
208
+
209
+
210
+ def should_use_alternate_cancel_key() -> bool:
211
+ """Determine if we should use an alternate cancel key (Ctrl+K) instead of Ctrl+C.
212
+
213
+ This returns True when:
214
+ - Running on Windows AND
215
+ - Launched via uvx
216
+
217
+ In this scenario, Ctrl+C is captured by uvx before reaching Python,
218
+ so we need to use a different key (Ctrl+K) for agent cancellation.
219
+
220
+ Returns:
221
+ True if alternate cancel key should be used, False otherwise
222
+ """
223
+ return is_windows() and is_launched_via_uvx()
224
+
225
+
226
+ def get_uvx_detection_info() -> dict:
227
+ """Get diagnostic information about uvx detection.
228
+
229
+ Useful for debugging and testing.
230
+
231
+ Returns:
232
+ Dictionary with detection details
233
+ """
234
+ chain = _get_parent_process_chain()
235
+ return {
236
+ "is_windows": is_windows(),
237
+ "is_launched_via_uvx": is_launched_via_uvx(),
238
+ "should_use_alternate_cancel_key": should_use_alternate_cancel_key(),
239
+ "parent_process_chain": chain,
240
+ "current_pid": os.getpid(),
241
+ "python_executable": sys.executable,
242
+ }
@@ -1,6 +1,9 @@
1
+ """Version checking utilities for Code Puppy."""
2
+
1
3
  import httpx
2
4
 
3
- from code_puppy.tools.common import console
5
+ from code_puppy.messaging import emit_info, emit_success, emit_warning, get_message_bus
6
+ from code_puppy.messaging.messages import VersionCheckMessage
4
7
 
5
8
 
6
9
  def normalize_version(version_str):
@@ -15,21 +18,37 @@ def versions_are_equal(current, latest):
15
18
 
16
19
  def fetch_latest_version(package_name):
17
20
  try:
18
- response = httpx.get(f"https://pypi.org/pypi/{package_name}/json")
19
- response.raise_for_status() # Raise an error for bad responses
21
+ response = httpx.get(f"https://pypi.org/pypi/{package_name}/json", timeout=5.0)
22
+ response.raise_for_status()
20
23
  data = response.json()
21
24
  return data["info"]["version"]
22
25
  except Exception as e:
23
- print(f"Error fetching version: {e}")
26
+ emit_warning(f"Error fetching version: {e}")
24
27
  return None
25
28
 
26
29
 
27
30
  def default_version_mismatch_behavior(current_version):
31
+ # Defensive: ensure current_version is never None
32
+ if current_version is None:
33
+ current_version = "0.0.0-unknown"
34
+ emit_warning("Could not detect current version, using fallback")
35
+
28
36
  latest_version = fetch_latest_version("code-puppy")
29
- console.print(f"Current version: {current_version}")
30
- console.print(f"Latest version: {latest_version}")
31
- if latest_version and latest_version != current_version:
32
- console.print(
33
- f"[bold yellow]A new version of code puppy is available: {latest_version}[/bold yellow]"
34
- )
35
- console.print("[bold green]Please consider updating![/bold green]")
37
+
38
+ update_available = bool(latest_version and latest_version != current_version)
39
+
40
+ # Emit structured version check message
41
+ version_msg = VersionCheckMessage(
42
+ current_version=current_version,
43
+ latest_version=latest_version or current_version,
44
+ update_available=update_available,
45
+ )
46
+ get_message_bus().emit(version_msg)
47
+
48
+ # Also emit plain text for legacy renderer
49
+ emit_info(f"Current version: {current_version}")
50
+
51
+ if update_available:
52
+ emit_info(f"Latest version: {latest_version}")
53
+ emit_warning(f"A new version of code puppy is available: {latest_version}")
54
+ emit_success("Please consider updating!")
@@ -0,0 +1,110 @@
1
+ {
2
+ "synthetic-GLM-4.7": {
3
+ "type": "custom_openai",
4
+ "name": "hf:zai-org/GLM-4.7",
5
+ "custom_endpoint": {
6
+ "url": "https://api.synthetic.new/openai/v1/",
7
+ "api_key": "$SYN_API_KEY"
8
+ },
9
+ "context_length": 200000,
10
+ "supported_settings": ["temperature", "seed"]
11
+ },
12
+ "synthetic-MiniMax-M2.1": {
13
+ "type": "custom_openai",
14
+ "name": "hf:MiniMaxAI/MiniMax-M2.1",
15
+ "custom_endpoint": {
16
+ "url": "https://api.synthetic.new/openai/v1/",
17
+ "api_key": "$SYN_API_KEY"
18
+ },
19
+ "context_length": 195000,
20
+ "supported_settings": ["temperature", "seed"]
21
+ },
22
+ "synthetic-Kimi-K2-Thinking": {
23
+ "type": "custom_openai",
24
+ "name": "hf:moonshotai/Kimi-K2-Thinking",
25
+ "custom_endpoint": {
26
+ "url": "https://api.synthetic.new/openai/v1/",
27
+ "api_key": "$SYN_API_KEY"
28
+ },
29
+ "context_length": 262144,
30
+ "supported_settings": ["temperature", "seed"]
31
+ },
32
+ "Gemini-3": {
33
+ "type": "gemini",
34
+ "name": "gemini-3-pro-preview",
35
+ "context_length": 200000,
36
+ "supported_settings": ["temperature"]
37
+ },
38
+ "Gemini-3-Long-Context": {
39
+ "type": "gemini",
40
+ "name": "gemini-3-pro-preview",
41
+ "context_length": 1000000,
42
+ "supported_settings": ["temperature"]
43
+ },
44
+ "gpt-5.1": {
45
+ "type": "openai",
46
+ "name": "gpt-5.1",
47
+ "context_length": 272000,
48
+ "supported_settings": ["reasoning_effort", "verbosity"],
49
+ "supports_xhigh_reasoning": false
50
+ },
51
+ "gpt-5.1-codex-api": {
52
+ "type": "openai",
53
+ "name": "gpt-5.1-codex",
54
+ "context_length": 272000,
55
+ "supported_settings": ["reasoning_effort", "verbosity"],
56
+ "supports_xhigh_reasoning": true
57
+ },
58
+ "Cerebras-GLM-4.7": {
59
+ "type": "cerebras",
60
+ "name": "zai-glm-4.7",
61
+ "custom_endpoint": {
62
+ "url": "https://api.cerebras.ai/v1",
63
+ "api_key": "$CEREBRAS_API_KEY"
64
+ },
65
+ "context_length": 131072,
66
+ "supported_settings": ["temperature", "seed"]
67
+ },
68
+ "claude-4-5-haiku": {
69
+ "type": "anthropic",
70
+ "name": "claude-haiku-4-5",
71
+ "context_length": 200000,
72
+ "supported_settings": ["temperature", "extended_thinking", "budget_tokens"]
73
+ },
74
+ "claude-4-5-sonnet": {
75
+ "type": "anthropic",
76
+ "name": "claude-sonnet-4-5",
77
+ "context_length": 200000,
78
+ "supported_settings": ["temperature", "extended_thinking", "budget_tokens"]
79
+ },
80
+ "claude-4-5-opus": {
81
+ "type": "anthropic",
82
+ "name": "claude-opus-4-5",
83
+ "context_length": 200000,
84
+ "supported_settings": ["temperature", "extended_thinking", "budget_tokens", "interleaved_thinking"]
85
+ },
86
+ "zai-glm-4.6-coding": {
87
+ "type": "zai_coding",
88
+ "name": "glm-4.6",
89
+ "context_length": 200000,
90
+ "supported_settings": ["temperature"]
91
+ },
92
+ "zai-glm-4.6-api": {
93
+ "type": "zai_api",
94
+ "name": "glm-4.6",
95
+ "context_length": 200000,
96
+ "supported_settings": ["temperature"]
97
+ },
98
+ "zai-glm-4.7-coding": {
99
+ "type": "zai_coding",
100
+ "name": "glm-4.7",
101
+ "context_length": 200000,
102
+ "supported_settings": ["temperature"]
103
+ },
104
+ "zai-glm-4.7-api": {
105
+ "type": "zai_api",
106
+ "name": "glm-4.7",
107
+ "context_length": 200000,
108
+ "supported_settings": ["temperature"]
109
+ }
110
+ }