codepp 0.0.437__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 +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -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 +453 -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 +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- 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 +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- 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 +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -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 +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -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 +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -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 +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -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/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -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/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
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()
|
code_puppy/main.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Main entry point for Code Puppy CLI.
|
|
2
|
+
|
|
3
|
+
This module re-exports the main_entry function from cli_runner for backwards compatibility.
|
|
4
|
+
All the actual logic lives in cli_runner.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from code_puppy.cli_runner import main_entry
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
main_entry()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""MCP (Model Context Protocol) management system for Code Puppy.
|
|
2
|
+
|
|
3
|
+
Note: Be careful not to create circular imports with config_wizard.py.
|
|
4
|
+
config_wizard.py imports ServerConfig and get_mcp_manager directly from
|
|
5
|
+
.manager to avoid circular dependencies with this package __init__.py
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .circuit_breaker import CircuitBreaker, CircuitOpenError, CircuitState
|
|
9
|
+
from .config_wizard import MCPConfigWizard, run_add_wizard
|
|
10
|
+
from .dashboard import MCPDashboard
|
|
11
|
+
from .error_isolation import (
|
|
12
|
+
ErrorCategory,
|
|
13
|
+
ErrorStats,
|
|
14
|
+
MCPErrorIsolator,
|
|
15
|
+
QuarantinedServerError,
|
|
16
|
+
get_error_isolator,
|
|
17
|
+
)
|
|
18
|
+
from .managed_server import ManagedMCPServer, ServerConfig, ServerState
|
|
19
|
+
from .manager import MCPManager, ServerInfo, get_mcp_manager
|
|
20
|
+
from .mcp_logs import (
|
|
21
|
+
clear_logs,
|
|
22
|
+
get_log_file_path,
|
|
23
|
+
get_log_stats,
|
|
24
|
+
get_mcp_logs_dir,
|
|
25
|
+
list_servers_with_logs,
|
|
26
|
+
read_logs,
|
|
27
|
+
write_log,
|
|
28
|
+
)
|
|
29
|
+
from .registry import ServerRegistry
|
|
30
|
+
from .retry_manager import RetryManager, RetryStats, get_retry_manager, retry_mcp_call
|
|
31
|
+
from .status_tracker import Event, ServerStatusTracker
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ManagedMCPServer",
|
|
35
|
+
"ServerConfig",
|
|
36
|
+
"ServerState",
|
|
37
|
+
"ServerStatusTracker",
|
|
38
|
+
"Event",
|
|
39
|
+
"MCPManager",
|
|
40
|
+
"ServerInfo",
|
|
41
|
+
"get_mcp_manager",
|
|
42
|
+
"ServerRegistry",
|
|
43
|
+
"MCPErrorIsolator",
|
|
44
|
+
"ErrorStats",
|
|
45
|
+
"ErrorCategory",
|
|
46
|
+
"QuarantinedServerError",
|
|
47
|
+
"get_error_isolator",
|
|
48
|
+
"CircuitBreaker",
|
|
49
|
+
"CircuitState",
|
|
50
|
+
"CircuitOpenError",
|
|
51
|
+
"RetryManager",
|
|
52
|
+
"RetryStats",
|
|
53
|
+
"get_retry_manager",
|
|
54
|
+
"retry_mcp_call",
|
|
55
|
+
"MCPDashboard",
|
|
56
|
+
"MCPConfigWizard",
|
|
57
|
+
"run_add_wizard",
|
|
58
|
+
# Log management
|
|
59
|
+
"get_mcp_logs_dir",
|
|
60
|
+
"get_log_file_path",
|
|
61
|
+
"read_logs",
|
|
62
|
+
"write_log",
|
|
63
|
+
"clear_logs",
|
|
64
|
+
"list_servers_with_logs",
|
|
65
|
+
"get_log_stats",
|
|
66
|
+
]
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async server lifecycle management using pydantic-ai's context managers.
|
|
3
|
+
|
|
4
|
+
This module properly manages MCP server lifecycles by maintaining async contexts
|
|
5
|
+
within the same task, allowing servers to start and stay running.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from contextlib import AsyncExitStack
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Dict, Optional, Union
|
|
14
|
+
|
|
15
|
+
from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ManagedServerContext:
|
|
22
|
+
"""Represents a managed MCP server with its async context."""
|
|
23
|
+
|
|
24
|
+
server_id: str
|
|
25
|
+
server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]
|
|
26
|
+
exit_stack: AsyncExitStack
|
|
27
|
+
start_time: datetime
|
|
28
|
+
task: asyncio.Task # The task that manages this server's lifecycle
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncServerLifecycleManager:
|
|
32
|
+
"""
|
|
33
|
+
Manages MCP server lifecycles asynchronously.
|
|
34
|
+
|
|
35
|
+
This properly maintains async contexts within the same task,
|
|
36
|
+
allowing servers to start and stay running independently of agents.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
"""Initialize the async lifecycle manager."""
|
|
41
|
+
self._servers: Dict[str, ManagedServerContext] = {}
|
|
42
|
+
self._lock = asyncio.Lock()
|
|
43
|
+
logger.info("AsyncServerLifecycleManager initialized")
|
|
44
|
+
|
|
45
|
+
async def start_server(
|
|
46
|
+
self,
|
|
47
|
+
server_id: str,
|
|
48
|
+
server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP],
|
|
49
|
+
) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Start an MCP server and maintain its context.
|
|
52
|
+
|
|
53
|
+
This creates a dedicated task that enters the server's context
|
|
54
|
+
and keeps it alive until explicitly stopped.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
server_id: Unique identifier for the server
|
|
58
|
+
server: The pydantic-ai MCP server instance
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if server started successfully, False otherwise
|
|
62
|
+
"""
|
|
63
|
+
async with self._lock:
|
|
64
|
+
# Check if already running
|
|
65
|
+
if server_id in self._servers:
|
|
66
|
+
if self._servers[server_id].server.is_running:
|
|
67
|
+
logger.info(f"Server {server_id} is already running")
|
|
68
|
+
return True
|
|
69
|
+
else:
|
|
70
|
+
# Server exists but not running, clean it up
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"Server {server_id} exists but not running, cleaning up"
|
|
73
|
+
)
|
|
74
|
+
await self._stop_server_internal(server_id)
|
|
75
|
+
|
|
76
|
+
# Create an event so we know when the server is actually registered
|
|
77
|
+
ready_event = asyncio.Event()
|
|
78
|
+
|
|
79
|
+
# Create a task that will manage this server's lifecycle
|
|
80
|
+
task = asyncio.create_task(
|
|
81
|
+
self._server_lifecycle_task(server_id, server, ready_event),
|
|
82
|
+
name=f"mcp_server_{server_id}",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Release the lock while waiting for the server to become ready
|
|
86
|
+
try:
|
|
87
|
+
await asyncio.wait_for(ready_event.wait(), timeout=10.0)
|
|
88
|
+
except asyncio.TimeoutError:
|
|
89
|
+
logger.error(f"Timed out waiting for server {server_id} to start")
|
|
90
|
+
if task.done():
|
|
91
|
+
try:
|
|
92
|
+
await task
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Server {server_id} task failed: {e}")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Check if task failed during startup
|
|
98
|
+
if task.done():
|
|
99
|
+
try:
|
|
100
|
+
await task
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"Failed to start server {server_id}: {e}")
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
logger.info(f"Server {server_id} started successfully")
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
async def _server_lifecycle_task(
|
|
109
|
+
self,
|
|
110
|
+
server_id: str,
|
|
111
|
+
server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP],
|
|
112
|
+
ready_event: asyncio.Event,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Task that manages a server's lifecycle.
|
|
116
|
+
|
|
117
|
+
This task enters the server's context and keeps it alive
|
|
118
|
+
until the server is stopped or an error occurs.
|
|
119
|
+
"""
|
|
120
|
+
exit_stack = AsyncExitStack()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
logger.info(f"Starting server lifecycle for {server_id}")
|
|
124
|
+
logger.info(
|
|
125
|
+
f"Server {server_id} _running_count before enter: {getattr(server, '_running_count', 'N/A')}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Enter the server's context
|
|
129
|
+
await exit_stack.enter_async_context(server)
|
|
130
|
+
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Server {server_id} _running_count after enter: {getattr(server, '_running_count', 'N/A')}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Store the managed context
|
|
136
|
+
async with self._lock:
|
|
137
|
+
self._servers[server_id] = ManagedServerContext(
|
|
138
|
+
server_id=server_id,
|
|
139
|
+
server=server,
|
|
140
|
+
exit_stack=exit_stack,
|
|
141
|
+
start_time=datetime.now(),
|
|
142
|
+
task=asyncio.current_task(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Signal that the server is registered and ready
|
|
146
|
+
ready_event.set()
|
|
147
|
+
|
|
148
|
+
logger.info(
|
|
149
|
+
f"Server {server_id} started successfully and stored in _servers"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Keep the task alive until cancelled
|
|
153
|
+
loop_count = 0
|
|
154
|
+
while True:
|
|
155
|
+
await asyncio.sleep(1)
|
|
156
|
+
loop_count += 1
|
|
157
|
+
|
|
158
|
+
# Check if server is still running
|
|
159
|
+
running_count = getattr(server, "_running_count", "N/A")
|
|
160
|
+
is_running = server.is_running
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"Server {server_id} heartbeat #{loop_count}: "
|
|
163
|
+
f"is_running={is_running}, _running_count={running_count}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if not is_running:
|
|
167
|
+
logger.warning(
|
|
168
|
+
f"Server {server_id} stopped unexpectedly! "
|
|
169
|
+
f"_running_count={running_count}"
|
|
170
|
+
)
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
except asyncio.CancelledError:
|
|
174
|
+
logger.info(f"Server {server_id} lifecycle task cancelled")
|
|
175
|
+
raise
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Error in server {server_id} lifecycle: {e}", exc_info=True)
|
|
178
|
+
finally:
|
|
179
|
+
running_count = getattr(server, "_running_count", "N/A")
|
|
180
|
+
logger.info(
|
|
181
|
+
f"Server {server_id} lifecycle ending, _running_count={running_count}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Clean up the context
|
|
185
|
+
await exit_stack.aclose()
|
|
186
|
+
|
|
187
|
+
running_count_after = getattr(server, "_running_count", "N/A")
|
|
188
|
+
logger.info(
|
|
189
|
+
f"Server {server_id} context closed, _running_count={running_count_after}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Remove from managed servers
|
|
193
|
+
async with self._lock:
|
|
194
|
+
if server_id in self._servers:
|
|
195
|
+
del self._servers[server_id]
|
|
196
|
+
|
|
197
|
+
logger.info(f"Server {server_id} lifecycle ended")
|
|
198
|
+
|
|
199
|
+
async def stop_server(self, server_id: str) -> bool:
|
|
200
|
+
"""
|
|
201
|
+
Stop a running MCP server.
|
|
202
|
+
|
|
203
|
+
This cancels the lifecycle task, which properly exits the context.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
server_id: ID of the server to stop
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if server was stopped, False if not found
|
|
210
|
+
"""
|
|
211
|
+
async with self._lock:
|
|
212
|
+
return await self._stop_server_internal(server_id)
|
|
213
|
+
|
|
214
|
+
async def _stop_server_internal(self, server_id: str) -> bool:
|
|
215
|
+
"""
|
|
216
|
+
Internal method to stop a server (must be called with lock held).
|
|
217
|
+
"""
|
|
218
|
+
if server_id not in self._servers:
|
|
219
|
+
logger.warning(f"Server {server_id} not found")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
context = self._servers[server_id]
|
|
223
|
+
|
|
224
|
+
# Cancel the lifecycle task
|
|
225
|
+
# This will cause the task to exit and clean up properly
|
|
226
|
+
context.task.cancel()
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
await context.task
|
|
230
|
+
except asyncio.CancelledError:
|
|
231
|
+
pass # Expected
|
|
232
|
+
|
|
233
|
+
logger.info(f"Stopped server {server_id}")
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
def is_running(self, server_id: str) -> bool:
|
|
237
|
+
"""
|
|
238
|
+
Check if a server is running.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
server_id: ID of the server
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
True if server is running, False otherwise
|
|
245
|
+
"""
|
|
246
|
+
context = self._servers.get(server_id)
|
|
247
|
+
return context.server.is_running if context else False
|
|
248
|
+
|
|
249
|
+
def list_servers(self) -> Dict[str, Dict[str, Any]]:
|
|
250
|
+
"""
|
|
251
|
+
List all running servers.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dictionary of server IDs to server info
|
|
255
|
+
"""
|
|
256
|
+
servers = {}
|
|
257
|
+
for server_id, context in self._servers.items():
|
|
258
|
+
uptime = (datetime.now() - context.start_time).total_seconds()
|
|
259
|
+
servers[server_id] = {
|
|
260
|
+
"type": context.server.__class__.__name__,
|
|
261
|
+
"is_running": context.server.is_running,
|
|
262
|
+
"uptime_seconds": uptime,
|
|
263
|
+
"start_time": context.start_time.isoformat(),
|
|
264
|
+
}
|
|
265
|
+
return servers
|
|
266
|
+
|
|
267
|
+
async def stop_all(self) -> None:
|
|
268
|
+
"""Stop all running servers."""
|
|
269
|
+
server_ids = list(self._servers.keys())
|
|
270
|
+
|
|
271
|
+
for server_id in server_ids:
|
|
272
|
+
await self.stop_server(server_id)
|
|
273
|
+
|
|
274
|
+
logger.info("All MCP servers stopped")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# Global singleton instance
|
|
278
|
+
_lifecycle_manager: Optional[AsyncServerLifecycleManager] = None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_lifecycle_manager() -> AsyncServerLifecycleManager:
|
|
282
|
+
"""Get the global lifecycle manager instance."""
|
|
283
|
+
global _lifecycle_manager
|
|
284
|
+
if _lifecycle_manager is None:
|
|
285
|
+
_lifecycle_manager = AsyncServerLifecycleManager()
|
|
286
|
+
return _lifecycle_manager
|