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.
Files changed (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. 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