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
@@ -0,0 +1,355 @@
1
+ """
2
+ Server Status Tracker for monitoring MCP server runtime status.
3
+
4
+ This module provides the ServerStatusTracker class that tracks the runtime
5
+ status of MCP servers including state, metrics, and events.
6
+ """
7
+
8
+ import logging
9
+ import threading
10
+ from collections import defaultdict, deque
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timedelta
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ from .managed_server import ServerState
16
+
17
+ # Configure logging
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class Event:
23
+ """Data class representing a server event."""
24
+
25
+ timestamp: datetime
26
+ event_type: str # "started", "stopped", "error", "health_check", etc.
27
+ details: Dict
28
+ server_id: str
29
+
30
+
31
+ class ServerStatusTracker:
32
+ """
33
+ Tracks the runtime status of MCP servers including state, metrics, and events.
34
+
35
+ This class provides in-memory storage for server states, metadata, and events
36
+ with thread-safe operations using locks. Events are stored using collections.deque
37
+ for automatic size limiting.
38
+
39
+ Example usage:
40
+ tracker = ServerStatusTracker()
41
+ tracker.set_status("server1", ServerState.RUNNING)
42
+ tracker.record_event("server1", "started", {"message": "Server started successfully"})
43
+ events = tracker.get_events("server1", limit=10)
44
+ """
45
+
46
+ def __init__(self):
47
+ """Initialize the status tracker with thread-safe data structures."""
48
+ # Thread safety lock
49
+ self._lock = threading.RLock()
50
+
51
+ # Server states (server_id -> ServerState)
52
+ self._server_states: Dict[str, ServerState] = {}
53
+
54
+ # Server metadata (server_id -> key -> value)
55
+ self._server_metadata: Dict[str, Dict[str, Any]] = defaultdict(dict)
56
+
57
+ # Server events (server_id -> deque of events)
58
+ # Using deque with maxlen for automatic size limiting
59
+ self._server_events: Dict[str, deque] = defaultdict(lambda: deque(maxlen=1000))
60
+
61
+ # Server timing information
62
+ self._start_times: Dict[str, datetime] = {}
63
+ self._stop_times: Dict[str, datetime] = {}
64
+
65
+ logger.info("ServerStatusTracker initialized")
66
+
67
+ def set_status(self, server_id: str, state: ServerState) -> None:
68
+ """
69
+ Set the current state of a server.
70
+
71
+ Args:
72
+ server_id: Unique identifier for the server
73
+ state: New server state
74
+ """
75
+ with self._lock:
76
+ old_state = self._server_states.get(server_id)
77
+ self._server_states[server_id] = state
78
+
79
+ # Record state change event
80
+ self.record_event(
81
+ server_id,
82
+ "state_change",
83
+ {
84
+ "old_state": old_state.value if old_state else None,
85
+ "new_state": state.value,
86
+ "message": f"State changed from {old_state.value if old_state else 'unknown'} to {state.value}",
87
+ },
88
+ )
89
+
90
+ logger.debug(f"Server {server_id} state changed: {old_state} -> {state}")
91
+
92
+ def get_status(self, server_id: str) -> ServerState:
93
+ """
94
+ Get the current state of a server.
95
+
96
+ Args:
97
+ server_id: Unique identifier for the server
98
+
99
+ Returns:
100
+ Current server state, defaults to STOPPED if not found
101
+ """
102
+ with self._lock:
103
+ return self._server_states.get(server_id, ServerState.STOPPED)
104
+
105
+ def set_metadata(self, server_id: str, key: str, value: Any) -> None:
106
+ """
107
+ Set metadata value for a server.
108
+
109
+ Args:
110
+ server_id: Unique identifier for the server
111
+ key: Metadata key
112
+ value: Metadata value (can be any type)
113
+ """
114
+ with self._lock:
115
+ if server_id not in self._server_metadata:
116
+ self._server_metadata[server_id] = {}
117
+
118
+ old_value = self._server_metadata[server_id].get(key)
119
+ self._server_metadata[server_id][key] = value
120
+
121
+ # Record metadata change event
122
+ self.record_event(
123
+ server_id,
124
+ "metadata_update",
125
+ {
126
+ "key": key,
127
+ "old_value": old_value,
128
+ "new_value": value,
129
+ "message": f"Metadata '{key}' updated",
130
+ },
131
+ )
132
+
133
+ logger.debug(f"Server {server_id} metadata updated: {key} = {value}")
134
+
135
+ def get_metadata(self, server_id: str, key: str) -> Any:
136
+ """
137
+ Get metadata value for a server.
138
+
139
+ Args:
140
+ server_id: Unique identifier for the server
141
+ key: Metadata key
142
+
143
+ Returns:
144
+ Metadata value or None if not found
145
+ """
146
+ with self._lock:
147
+ return self._server_metadata.get(server_id, {}).get(key)
148
+
149
+ def record_event(self, server_id: str, event_type: str, details: Dict) -> None:
150
+ """
151
+ Record an event for a server.
152
+
153
+ Args:
154
+ server_id: Unique identifier for the server
155
+ event_type: Type of event (e.g., "started", "stopped", "error", "health_check")
156
+ details: Dictionary containing event details
157
+ """
158
+ with self._lock:
159
+ event = Event(
160
+ timestamp=datetime.now(),
161
+ event_type=event_type,
162
+ details=details.copy()
163
+ if details
164
+ else {}, # Copy to prevent modification
165
+ server_id=server_id,
166
+ )
167
+
168
+ # Add to deque (automatically handles size limiting)
169
+ self._server_events[server_id].append(event)
170
+
171
+ logger.debug(f"Event recorded for server {server_id}: {event_type}")
172
+
173
+ def get_events(self, server_id: str, limit: int = 100) -> List[Event]:
174
+ """
175
+ Get recent events for a server.
176
+
177
+ Args:
178
+ server_id: Unique identifier for the server
179
+ limit: Maximum number of events to return (default: 100)
180
+
181
+ Returns:
182
+ List of events ordered by timestamp (most recent first)
183
+ """
184
+ with self._lock:
185
+ events = list(self._server_events.get(server_id, deque()))
186
+
187
+ # Return most recent events first, limited by count
188
+ events.reverse() # Most recent first
189
+ return events[:limit]
190
+
191
+ def clear_events(self, server_id: str) -> None:
192
+ """
193
+ Clear all events for a server.
194
+
195
+ Args:
196
+ server_id: Unique identifier for the server
197
+ """
198
+ with self._lock:
199
+ if server_id in self._server_events:
200
+ self._server_events[server_id].clear()
201
+ logger.info(f"Cleared all events for server: {server_id}")
202
+
203
+ def get_uptime(self, server_id: str) -> Optional[timedelta]:
204
+ """
205
+ Calculate uptime for a server based on start/stop times.
206
+
207
+ Args:
208
+ server_id: Unique identifier for the server
209
+
210
+ Returns:
211
+ Server uptime as timedelta, or None if server never started
212
+ """
213
+ with self._lock:
214
+ start_time = self._start_times.get(server_id)
215
+ if start_time is None:
216
+ return None
217
+
218
+ # If server is currently running, calculate from start time to now
219
+ current_state = self.get_status(server_id)
220
+ if current_state == ServerState.RUNNING:
221
+ return datetime.now() - start_time
222
+
223
+ # If server is stopped, calculate from start to stop time
224
+ stop_time = self._stop_times.get(server_id)
225
+ if stop_time is not None and stop_time > start_time:
226
+ return stop_time - start_time
227
+
228
+ # If we have start time but no valid stop time, assume currently running
229
+ return datetime.now() - start_time
230
+
231
+ def record_start_time(self, server_id: str) -> None:
232
+ """
233
+ Record the start time for a server.
234
+
235
+ Args:
236
+ server_id: Unique identifier for the server
237
+ """
238
+ with self._lock:
239
+ start_time = datetime.now()
240
+ self._start_times[server_id] = start_time
241
+
242
+ # Record start event
243
+ self.record_event(
244
+ server_id,
245
+ "started",
246
+ {"start_time": start_time.isoformat(), "message": "Server started"},
247
+ )
248
+
249
+ logger.info(f"Recorded start time for server: {server_id}")
250
+
251
+ def record_stop_time(self, server_id: str) -> None:
252
+ """
253
+ Record the stop time for a server.
254
+
255
+ Args:
256
+ server_id: Unique identifier for the server
257
+ """
258
+ with self._lock:
259
+ stop_time = datetime.now()
260
+ self._stop_times[server_id] = stop_time
261
+
262
+ # Calculate final uptime
263
+ start_time = self._start_times.get(server_id)
264
+ uptime = None
265
+ if start_time:
266
+ uptime = stop_time - start_time
267
+
268
+ # Record stop event
269
+ self.record_event(
270
+ server_id,
271
+ "stopped",
272
+ {
273
+ "stop_time": stop_time.isoformat(),
274
+ "uptime_seconds": uptime.total_seconds() if uptime else None,
275
+ "message": "Server stopped",
276
+ },
277
+ )
278
+
279
+ logger.info(f"Recorded stop time for server: {server_id}")
280
+
281
+ def get_all_server_ids(self) -> List[str]:
282
+ """
283
+ Get all server IDs that have been tracked.
284
+
285
+ Returns:
286
+ List of all server IDs
287
+ """
288
+ with self._lock:
289
+ # Combine all sources of server IDs
290
+ all_ids = set()
291
+ all_ids.update(self._server_states.keys())
292
+ all_ids.update(self._server_metadata.keys())
293
+ all_ids.update(self._server_events.keys())
294
+ all_ids.update(self._start_times.keys())
295
+ all_ids.update(self._stop_times.keys())
296
+
297
+ return sorted(list(all_ids))
298
+
299
+ def get_server_summary(self, server_id: str) -> Dict[str, Any]:
300
+ """
301
+ Get comprehensive summary of server status.
302
+
303
+ Args:
304
+ server_id: Unique identifier for the server
305
+
306
+ Returns:
307
+ Dictionary containing current state, metadata, recent events, and uptime
308
+ """
309
+ with self._lock:
310
+ return {
311
+ "server_id": server_id,
312
+ "state": self.get_status(server_id).value,
313
+ "metadata": self._server_metadata.get(server_id, {}).copy(),
314
+ "recent_events_count": len(self._server_events.get(server_id, deque())),
315
+ "uptime": self.get_uptime(server_id),
316
+ "start_time": self._start_times.get(server_id),
317
+ "stop_time": self._stop_times.get(server_id),
318
+ "last_event_time": (
319
+ list(self._server_events.get(server_id, deque()))[-1].timestamp
320
+ if server_id in self._server_events
321
+ and len(self._server_events[server_id]) > 0
322
+ else None
323
+ ),
324
+ }
325
+
326
+ def cleanup_old_data(self, days_to_keep: int = 7) -> None:
327
+ """
328
+ Clean up old data to prevent memory bloat.
329
+
330
+ Args:
331
+ days_to_keep: Number of days of data to keep (default: 7)
332
+ """
333
+ cutoff_time = datetime.now() - timedelta(days=days_to_keep)
334
+
335
+ with self._lock:
336
+ cleaned_servers = []
337
+
338
+ for server_id in list(self._server_events.keys()):
339
+ events = self._server_events[server_id]
340
+ if events:
341
+ # Filter out old events
342
+ original_count = len(events)
343
+ # Convert to list, filter, then create new deque
344
+ filtered_events = [
345
+ event for event in events if event.timestamp >= cutoff_time
346
+ ]
347
+
348
+ # Replace the deque with filtered events
349
+ self._server_events[server_id] = deque(filtered_events, maxlen=1000)
350
+
351
+ if len(filtered_events) < original_count:
352
+ cleaned_servers.append(server_id)
353
+
354
+ if cleaned_servers:
355
+ logger.info(f"Cleaned old events for {len(cleaned_servers)} servers")
@@ -0,0 +1,209 @@
1
+ """
2
+ System tool detection and validation for MCP server requirements.
3
+ """
4
+
5
+ import shutil
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from typing import Dict, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class ToolInfo:
13
+ """Information about a detected system tool."""
14
+
15
+ name: str
16
+ available: bool
17
+ version: Optional[str] = None
18
+ path: Optional[str] = None
19
+ error: Optional[str] = None
20
+
21
+
22
+ class SystemToolDetector:
23
+ """Detect and validate system tools required by MCP servers."""
24
+
25
+ # Tool version commands
26
+ VERSION_COMMANDS = {
27
+ "node": ["node", "--version"],
28
+ "npm": ["npm", "--version"],
29
+ "npx": ["npx", "--version"],
30
+ "python": ["python", "--version"],
31
+ "python3": ["python3", "--version"],
32
+ "pip": ["pip", "--version"],
33
+ "pip3": ["pip3", "--version"],
34
+ "git": ["git", "--version"],
35
+ "docker": ["docker", "--version"],
36
+ "java": ["java", "-version"],
37
+ "go": ["go", "version"],
38
+ "rust": ["rustc", "--version"],
39
+ "cargo": ["cargo", "--version"],
40
+ "julia": ["julia", "--version"],
41
+ "R": ["R", "--version"],
42
+ "php": ["php", "--version"],
43
+ "ruby": ["ruby", "--version"],
44
+ "perl": ["perl", "--version"],
45
+ "swift": ["swift", "--version"],
46
+ "dotnet": ["dotnet", "--version"],
47
+ "jupyter": ["jupyter", "--version"],
48
+ "code": ["code", "--version"], # VS Code
49
+ "vim": ["vim", "--version"],
50
+ "emacs": ["emacs", "--version"],
51
+ }
52
+
53
+ @classmethod
54
+ def detect_tool(cls, tool_name: str) -> ToolInfo:
55
+ """Detect if a tool is available and get its version."""
56
+ # First check if tool is in PATH
57
+ tool_path = shutil.which(tool_name)
58
+
59
+ if not tool_path:
60
+ return ToolInfo(
61
+ name=tool_name, available=False, error=f"{tool_name} not found in PATH"
62
+ )
63
+
64
+ # Try to get version
65
+ version_cmd = cls.VERSION_COMMANDS.get(tool_name)
66
+ version = None
67
+ error = None
68
+
69
+ if version_cmd:
70
+ try:
71
+ # Run version command
72
+ result = subprocess.run(
73
+ version_cmd, capture_output=True, text=True, timeout=10
74
+ )
75
+
76
+ if result.returncode == 0:
77
+ # Parse version from output
78
+ output = result.stdout.strip() or result.stderr.strip()
79
+ version = cls._parse_version(tool_name, output)
80
+ else:
81
+ error = f"Version check failed: {result.stderr.strip()}"
82
+
83
+ except subprocess.TimeoutExpired:
84
+ error = "Version check timed out"
85
+ except Exception as e:
86
+ error = f"Version check error: {str(e)}"
87
+
88
+ return ToolInfo(
89
+ name=tool_name, available=True, version=version, path=tool_path, error=error
90
+ )
91
+
92
+ @classmethod
93
+ def detect_tools(cls, tool_names: List[str]) -> Dict[str, ToolInfo]:
94
+ """Detect multiple tools."""
95
+ return {name: cls.detect_tool(name) for name in tool_names}
96
+
97
+ @classmethod
98
+ def _parse_version(cls, tool_name: str, output: str) -> Optional[str]:
99
+ """Parse version string from command output."""
100
+ if not output:
101
+ return None
102
+
103
+ # Common version patterns
104
+ import re
105
+
106
+ # Try to find version pattern like "v1.2.3" or "1.2.3"
107
+ version_patterns = [
108
+ r"v?(\d+\.\d+\.\d+(?:\.\d+)?)", # Standard semver
109
+ r"(\d+\.\d+\.\d+)", # Simple version
110
+ r"version\s+v?(\d+\.\d+\.\d+)", # "version 1.2.3"
111
+ r"v?(\d+\.\d+)", # Major.minor only
112
+ ]
113
+
114
+ for pattern in version_patterns:
115
+ match = re.search(pattern, output, re.IGNORECASE)
116
+ if match:
117
+ return match.group(1)
118
+
119
+ # If no pattern matches, return first line (common for many tools)
120
+ first_line = output.split("\n")[0].strip()
121
+ if len(first_line) < 100: # Reasonable length for a version string
122
+ return first_line
123
+
124
+ return None
125
+
126
+ @classmethod
127
+ def check_package_dependencies(cls, packages: List[str]) -> Dict[str, bool]:
128
+ """Check if package dependencies are available."""
129
+ results = {}
130
+
131
+ for package in packages:
132
+ available = False
133
+
134
+ # Try different package managers/methods
135
+ if package.startswith("@") or "/" in package:
136
+ # Likely npm package
137
+ available = cls._check_npm_package(package)
138
+ elif package in ["jupyter", "pandas", "numpy", "matplotlib"]:
139
+ # Python packages
140
+ available = cls._check_python_package(package)
141
+ else:
142
+ # Try both npm and python
143
+ available = cls._check_npm_package(
144
+ package
145
+ ) or cls._check_python_package(package)
146
+
147
+ results[package] = available
148
+
149
+ return results
150
+
151
+ @classmethod
152
+ def _check_npm_package(cls, package: str) -> bool:
153
+ """Check if an npm package is available."""
154
+ try:
155
+ result = subprocess.run(
156
+ ["npm", "list", "-g", package],
157
+ capture_output=True,
158
+ text=True,
159
+ timeout=10,
160
+ )
161
+ return result.returncode == 0
162
+ except Exception:
163
+ return False
164
+
165
+ @classmethod
166
+ def _check_python_package(cls, package: str) -> bool:
167
+ """Check if a Python package is available."""
168
+ try:
169
+ import importlib
170
+
171
+ importlib.import_module(package)
172
+ return True
173
+ except ImportError:
174
+ return False
175
+
176
+ @classmethod
177
+ def get_installation_suggestions(cls, tool_name: str) -> List[str]:
178
+ """Get installation suggestions for a missing tool."""
179
+ suggestions = {
180
+ "node": [
181
+ "Install Node.js from https://nodejs.org",
182
+ "Or use package manager: brew install node (macOS) / sudo apt install nodejs (Ubuntu)",
183
+ ],
184
+ "npm": ["Usually comes with Node.js - install Node.js first"],
185
+ "npx": ["Usually comes with npm 5.2+ - update npm: npm install -g npm"],
186
+ "python": [
187
+ "Install Python from https://python.org",
188
+ "Or use package manager: brew install python (macOS) / sudo apt install python3 (Ubuntu)",
189
+ ],
190
+ "python3": ["Same as python - install Python 3.x"],
191
+ "pip": ["Usually comes with Python - try: python -m ensurepip"],
192
+ "pip3": ["Usually comes with Python 3 - try: python3 -m ensurepip"],
193
+ "git": [
194
+ "Install Git from https://git-scm.com",
195
+ "Or use package manager: brew install git (macOS) / sudo apt install git (Ubuntu)",
196
+ ],
197
+ "docker": ["Install Docker from https://docker.com"],
198
+ "java": [
199
+ "Install OpenJDK from https://openjdk.java.net",
200
+ "Or use package manager: brew install openjdk (macOS) / sudo apt install default-jdk (Ubuntu)",
201
+ ],
202
+ "jupyter": ["Install with pip: pip install jupyter"],
203
+ }
204
+
205
+ return suggestions.get(tool_name, [f"Please install {tool_name} manually"])
206
+
207
+
208
+ # Global detector instance
209
+ detector = SystemToolDetector()
@@ -0,0 +1 @@
1
+ # MCP Prompts for Code Puppy
@@ -0,0 +1,103 @@
1
+ """
2
+ Hook Creator MCP Prompt
3
+
4
+ Simple MCP prompt that injects hook creation documentation/instructions
5
+ before user prompts for creating Code Puppy hooks.
6
+ """
7
+
8
+ HOOK_CREATION_PROMPT = """
9
+ # Creating Hooks in Code Puppy
10
+
11
+ You are helping a user create hooks for Code Puppy. Code Puppy has two hook systems:
12
+
13
+ ## System 1: Lifecycle Callbacks (Python)
14
+ Register Python functions that run at specific phases:
15
+ - `startup` - Application boot
16
+ - `shutdown` - Application exit
17
+ - `custom_command` - User types /slash command
18
+ - `pre_tool_call` - Before any tool executes
19
+ - `post_tool_call` - After a tool finishes
20
+ - `agent_run_start` - Agent run begins
21
+ - `agent_run_end` - Agent run completes
22
+ - `custom_command_help` - Build /help menu
23
+
24
+ Example lifecycle callback:
25
+ ```python
26
+ from code_puppy.callbacks import register_callback
27
+
28
+ async def _on_startup():
29
+ print("Started!")
30
+
31
+ register_callback("startup", _on_startup)
32
+ ```
33
+
34
+ ## System 2: Event-Based Hooks (Shell/JSON)
35
+ Configure shell commands responding to Code Puppy events in `.claude/settings.json`:
36
+ - `PreToolUse` - Before a tool executes (can block with exit code 2)
37
+ - `PostToolUse` - After a tool succeeds
38
+ - `SessionStart` - Session begins/resumes
39
+ - `Stop` - Claude finishes responding
40
+
41
+ Example event hook configuration:
42
+ ```json
43
+ {
44
+ "hooks": {
45
+ "PreToolUse": [
46
+ {
47
+ "matcher": "Bash",
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "command": "bash ./scripts/validate-command.sh"
52
+ }
53
+ ]
54
+ }
55
+ ]
56
+ }
57
+ }
58
+ ```
59
+
60
+ Example bash hook script:
61
+ ```bash
62
+ #!/bin/bash
63
+ INPUT=$(cat)
64
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
65
+
66
+ # Block dangerous commands
67
+ if echo "$COMMAND" | grep -q "drop table"; then
68
+ echo "Blocked: SQL injection attempt" >&2
69
+ exit 2
70
+ fi
71
+
72
+ exit 0
73
+ ```
74
+
75
+ ## Decision Tree
76
+ Use **lifecycle callbacks** for:
77
+ - Pure Python logic (initialization, cleanup, monitoring)
78
+ - Custom /commands
79
+ - Startup/shutdown tasks
80
+
81
+ Use **event-based hooks** for:
82
+ - Deterministic shell commands (validation, formatting)
83
+ - Blocking/allowing tool calls
84
+ - Pre/post-processing
85
+
86
+ When the user asks about creating a hook, ask them:
87
+ 1. What should the hook do?
88
+ 2. When should it run? (Which phase/event?)
89
+ 3. Should it be Python or shell? (Lifecycle vs Event)
90
+ 4. Then provide the exact code/config they need.
91
+ """
92
+
93
+
94
+ def inject_hook_prompt(user_message: str) -> str:
95
+ """
96
+ Inject hook creation instructions before user message.
97
+ Use this to add to the system prompt when handling hook creation requests.
98
+ """
99
+ return HOOK_CREATION_PROMPT + "\n\nUser question:\n" + user_message
100
+
101
+
102
+ if __name__ == "__main__":
103
+ print(HOOK_CREATION_PROMPT)