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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.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
|
+
}
|
code_puppy/version_checker.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
"""Version checking utilities for Code Puppy."""
|
|
2
|
+
|
|
1
3
|
import httpx
|
|
2
4
|
|
|
3
|
-
from code_puppy.
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|