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
|
@@ -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)
|