code-puppy 0.0.134__py3-none-any.whl → 0.0.136__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/agent.py +15 -17
- code_puppy/agents/agent_manager.py +320 -9
- code_puppy/agents/base_agent.py +58 -2
- code_puppy/agents/runtime_manager.py +68 -42
- code_puppy/command_line/command_handler.py +82 -33
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/add_command.py +183 -0
- code_puppy/command_line/mcp/base.py +35 -0
- code_puppy/command_line/mcp/handler.py +133 -0
- code_puppy/command_line/mcp/help_command.py +146 -0
- code_puppy/command_line/mcp/install_command.py +176 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +126 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +92 -0
- code_puppy/command_line/mcp/search_command.py +117 -0
- code_puppy/command_line/mcp/start_all_command.py +126 -0
- code_puppy/command_line/mcp/start_command.py +98 -0
- code_puppy/command_line/mcp/status_command.py +185 -0
- code_puppy/command_line/mcp/stop_all_command.py +109 -0
- code_puppy/command_line/mcp/stop_command.py +79 -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 +259 -0
- code_puppy/command_line/model_picker_completion.py +21 -4
- code_puppy/command_line/prompt_toolkit_completion.py +9 -0
- code_puppy/main.py +23 -17
- code_puppy/mcp/__init__.py +42 -16
- code_puppy/mcp/async_lifecycle.py +51 -49
- code_puppy/mcp/blocking_startup.py +125 -113
- code_puppy/mcp/captured_stdio_server.py +63 -70
- code_puppy/mcp/circuit_breaker.py +63 -47
- code_puppy/mcp/config_wizard.py +169 -136
- code_puppy/mcp/dashboard.py +79 -71
- code_puppy/mcp/error_isolation.py +147 -100
- code_puppy/mcp/examples/retry_example.py +55 -42
- code_puppy/mcp/health_monitor.py +152 -141
- code_puppy/mcp/managed_server.py +100 -97
- code_puppy/mcp/manager.py +168 -156
- code_puppy/mcp/registry.py +148 -110
- code_puppy/mcp/retry_manager.py +63 -61
- code_puppy/mcp/server_registry_catalog.py +271 -225
- code_puppy/mcp/status_tracker.py +80 -80
- code_puppy/mcp/system_tools.py +47 -52
- code_puppy/messaging/message_queue.py +20 -13
- code_puppy/messaging/renderers.py +30 -15
- code_puppy/state_management.py +103 -0
- code_puppy/tui/app.py +64 -7
- code_puppy/tui/components/chat_view.py +3 -3
- code_puppy/tui/components/human_input_modal.py +12 -8
- code_puppy/tui/screens/__init__.py +2 -2
- code_puppy/tui/screens/mcp_install_wizard.py +208 -179
- code_puppy/tui/tests/test_agent_command.py +3 -3
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
- code_puppy/command_line/mcp_commands.py +0 -1789
- {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,8 +7,20 @@ all references to the agent are properly updated when it's reloaded.
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import signal
|
|
10
|
+
import sys
|
|
10
11
|
import uuid
|
|
11
|
-
from typing import
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
# ExceptionGroup is available in Python 3.11+
|
|
15
|
+
if sys.version_info >= (3, 11):
|
|
16
|
+
from builtins import ExceptionGroup
|
|
17
|
+
else:
|
|
18
|
+
# For Python 3.10 and below, we can define a simple fallback
|
|
19
|
+
class ExceptionGroup(Exception):
|
|
20
|
+
def __init__(self, message, exceptions):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.exceptions = exceptions
|
|
23
|
+
|
|
12
24
|
|
|
13
25
|
import mcp
|
|
14
26
|
from pydantic_ai import Agent
|
|
@@ -20,71 +32,79 @@ from code_puppy.messaging.message_queue import emit_info, emit_warning
|
|
|
20
32
|
class RuntimeAgentManager:
|
|
21
33
|
"""
|
|
22
34
|
Manages the runtime agent instance and ensures proper updates.
|
|
23
|
-
|
|
35
|
+
|
|
24
36
|
This class acts as a proxy that always returns the current agent instance,
|
|
25
37
|
ensuring that when the agent is reloaded, all code using this manager
|
|
26
38
|
automatically gets the updated instance.
|
|
27
39
|
"""
|
|
28
|
-
|
|
40
|
+
|
|
29
41
|
def __init__(self):
|
|
30
42
|
"""Initialize the runtime agent manager."""
|
|
31
43
|
self._agent: Optional[Agent] = None
|
|
32
44
|
self._last_model_name: Optional[str] = None
|
|
33
|
-
|
|
45
|
+
|
|
34
46
|
def get_agent(self, force_reload: bool = False, message_group: str = "") -> Agent:
|
|
35
47
|
"""
|
|
36
48
|
Get the current agent instance.
|
|
37
|
-
|
|
49
|
+
|
|
38
50
|
This method always returns the most recent agent instance,
|
|
39
51
|
automatically handling reloads when the model changes.
|
|
40
|
-
|
|
52
|
+
|
|
41
53
|
Args:
|
|
42
54
|
force_reload: If True, force a reload of the agent
|
|
43
|
-
|
|
55
|
+
|
|
44
56
|
Returns:
|
|
45
57
|
The current agent instance
|
|
46
58
|
"""
|
|
47
59
|
from code_puppy.agent import get_code_generation_agent
|
|
48
|
-
|
|
60
|
+
|
|
49
61
|
# Always get the current singleton - this ensures we have the latest
|
|
50
|
-
current_agent = get_code_generation_agent(
|
|
62
|
+
current_agent = get_code_generation_agent(
|
|
63
|
+
force_reload=force_reload, message_group=message_group
|
|
64
|
+
)
|
|
51
65
|
self._agent = current_agent
|
|
52
|
-
|
|
66
|
+
|
|
53
67
|
return self._agent
|
|
54
|
-
|
|
68
|
+
|
|
55
69
|
def reload_agent(self) -> Agent:
|
|
56
70
|
"""
|
|
57
71
|
Force reload the agent.
|
|
58
|
-
|
|
72
|
+
|
|
59
73
|
This is typically called after MCP servers are started/stopped.
|
|
60
|
-
|
|
74
|
+
|
|
61
75
|
Returns:
|
|
62
76
|
The newly loaded agent instance
|
|
63
77
|
"""
|
|
64
78
|
message_group = uuid.uuid4()
|
|
65
|
-
emit_info(
|
|
79
|
+
emit_info(
|
|
80
|
+
"[bold cyan]Reloading agent with updated configuration...[/bold cyan]",
|
|
81
|
+
message_group=message_group,
|
|
82
|
+
)
|
|
66
83
|
return self.get_agent(force_reload=True, message_group=message_group)
|
|
67
|
-
|
|
68
|
-
async def run_with_mcp(
|
|
84
|
+
|
|
85
|
+
async def run_with_mcp(
|
|
86
|
+
self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs
|
|
87
|
+
) -> Any:
|
|
69
88
|
"""
|
|
70
89
|
Run the agent with MCP servers and full cancellation support.
|
|
71
|
-
|
|
90
|
+
|
|
72
91
|
This method ensures we're always using the current agent instance
|
|
73
92
|
and handles Ctrl+C interruption properly by creating a cancellable task.
|
|
74
|
-
|
|
93
|
+
|
|
75
94
|
Args:
|
|
76
95
|
prompt: The user prompt to process
|
|
77
96
|
usage_limits: Optional usage limits for the agent
|
|
78
97
|
**kwargs: Additional arguments to pass to agent.run (e.g., message_history)
|
|
79
|
-
|
|
98
|
+
|
|
80
99
|
Returns:
|
|
81
100
|
The agent's response
|
|
82
|
-
|
|
101
|
+
|
|
83
102
|
Raises:
|
|
84
103
|
asyncio.CancelledError: When execution is cancelled by user
|
|
85
104
|
"""
|
|
86
105
|
agent = self.get_agent()
|
|
87
106
|
group_id = str(uuid.uuid4())
|
|
107
|
+
|
|
88
108
|
# Function to run agent with MCP
|
|
89
109
|
async def run_agent_task():
|
|
90
110
|
try:
|
|
@@ -93,12 +113,15 @@ class RuntimeAgentManager:
|
|
|
93
113
|
except* mcp.shared.exceptions.McpError as mcp_error:
|
|
94
114
|
emit_warning(f"MCP server error: {str(mcp_error)}", group_id=group_id)
|
|
95
115
|
emit_warning(f"{str(mcp_error)}", group_id=group_id)
|
|
96
|
-
emit_warning(
|
|
116
|
+
emit_warning(
|
|
117
|
+
"Try disabling any malfunctioning MCP servers", group_id=group_id
|
|
118
|
+
)
|
|
97
119
|
except* InterruptedError as ie:
|
|
98
120
|
emit_warning(f"Interrupted: {str(ie)}")
|
|
99
121
|
except* Exception as other_error:
|
|
100
122
|
# Filter out CancelledError from the exception group - let it propagate
|
|
101
123
|
remaining_exceptions = []
|
|
124
|
+
|
|
102
125
|
def collect_non_cancelled_exceptions(exc):
|
|
103
126
|
if isinstance(exc, ExceptionGroup):
|
|
104
127
|
for sub_exc in exc.exceptions:
|
|
@@ -107,40 +130,41 @@ class RuntimeAgentManager:
|
|
|
107
130
|
remaining_exceptions.append(exc)
|
|
108
131
|
emit_warning(f"Unexpected error: {str(exc)}", group_id=group_id)
|
|
109
132
|
emit_warning(f"{str(exc.args)}", group_id=group_id)
|
|
110
|
-
|
|
133
|
+
|
|
111
134
|
collect_non_cancelled_exceptions(other_error)
|
|
112
|
-
|
|
135
|
+
|
|
113
136
|
# If there are CancelledError exceptions in the group, re-raise them
|
|
114
137
|
cancelled_exceptions = []
|
|
138
|
+
|
|
115
139
|
def collect_cancelled_exceptions(exc):
|
|
116
140
|
if isinstance(exc, ExceptionGroup):
|
|
117
141
|
for sub_exc in exc.exceptions:
|
|
118
142
|
collect_cancelled_exceptions(sub_exc)
|
|
119
143
|
elif isinstance(exc, asyncio.CancelledError):
|
|
120
144
|
cancelled_exceptions.append(exc)
|
|
121
|
-
|
|
145
|
+
|
|
122
146
|
collect_cancelled_exceptions(other_error)
|
|
123
|
-
|
|
147
|
+
|
|
124
148
|
if cancelled_exceptions:
|
|
125
149
|
# Re-raise the first CancelledError to propagate cancellation
|
|
126
150
|
raise cancelled_exceptions[0]
|
|
127
|
-
|
|
151
|
+
|
|
128
152
|
# Create the task FIRST
|
|
129
153
|
agent_task = asyncio.create_task(run_agent_task())
|
|
130
|
-
|
|
154
|
+
|
|
131
155
|
# Import shell process killer
|
|
132
156
|
from code_puppy.tools.command_runner import kill_all_running_shell_processes
|
|
133
|
-
|
|
157
|
+
|
|
134
158
|
# Ensure the interrupt handler only acts once per task
|
|
135
159
|
handled = False
|
|
136
|
-
|
|
160
|
+
|
|
137
161
|
def keyboard_interrupt_handler(sig, frame):
|
|
138
162
|
"""Signal handler for Ctrl+C - replicating exact original logic"""
|
|
139
163
|
nonlocal handled
|
|
140
164
|
if handled:
|
|
141
165
|
return
|
|
142
166
|
handled = True
|
|
143
|
-
|
|
167
|
+
|
|
144
168
|
# First, nuke any running shell processes triggered by tools
|
|
145
169
|
try:
|
|
146
170
|
killed = kill_all_running_shell_processes()
|
|
@@ -157,11 +181,11 @@ class RuntimeAgentManager:
|
|
|
157
181
|
agent_task.cancel()
|
|
158
182
|
# Don't call the original handler
|
|
159
183
|
# This prevents the application from exiting
|
|
160
|
-
|
|
184
|
+
|
|
161
185
|
try:
|
|
162
186
|
# Save original handler and set our custom one AFTER task is created
|
|
163
187
|
original_handler = signal.signal(signal.SIGINT, keyboard_interrupt_handler)
|
|
164
|
-
|
|
188
|
+
|
|
165
189
|
# Wait for the task to complete or be cancelled
|
|
166
190
|
result = await agent_task
|
|
167
191
|
return result
|
|
@@ -181,32 +205,34 @@ class RuntimeAgentManager:
|
|
|
181
205
|
# Restore original signal handler
|
|
182
206
|
if original_handler:
|
|
183
207
|
signal.signal(signal.SIGINT, original_handler)
|
|
184
|
-
|
|
185
|
-
async def run(
|
|
208
|
+
|
|
209
|
+
async def run(
|
|
210
|
+
self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs
|
|
211
|
+
) -> Any:
|
|
186
212
|
"""
|
|
187
213
|
Run the agent without explicitly managing MCP servers.
|
|
188
|
-
|
|
214
|
+
|
|
189
215
|
Args:
|
|
190
216
|
prompt: The user prompt to process
|
|
191
217
|
usage_limits: Optional usage limits for the agent
|
|
192
218
|
**kwargs: Additional arguments to pass to agent.run (e.g., message_history)
|
|
193
|
-
|
|
219
|
+
|
|
194
220
|
Returns:
|
|
195
221
|
The agent's response
|
|
196
222
|
"""
|
|
197
223
|
agent = self.get_agent()
|
|
198
224
|
return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
|
|
199
|
-
|
|
225
|
+
|
|
200
226
|
def __getattr__(self, name: str) -> Any:
|
|
201
227
|
"""
|
|
202
228
|
Proxy all other attribute access to the current agent.
|
|
203
|
-
|
|
229
|
+
|
|
204
230
|
This allows the manager to be used as a drop-in replacement
|
|
205
231
|
for direct agent access.
|
|
206
|
-
|
|
232
|
+
|
|
207
233
|
Args:
|
|
208
234
|
name: The attribute name to access
|
|
209
|
-
|
|
235
|
+
|
|
210
236
|
Returns:
|
|
211
237
|
The attribute from the current agent
|
|
212
238
|
"""
|
|
@@ -221,11 +247,11 @@ _runtime_manager: Optional[RuntimeAgentManager] = None
|
|
|
221
247
|
def get_runtime_agent_manager() -> RuntimeAgentManager:
|
|
222
248
|
"""
|
|
223
249
|
Get the global runtime agent manager instance.
|
|
224
|
-
|
|
250
|
+
|
|
225
251
|
Returns:
|
|
226
252
|
The singleton RuntimeAgentManager instance
|
|
227
253
|
"""
|
|
228
254
|
global _runtime_manager
|
|
229
255
|
if _runtime_manager is None:
|
|
230
256
|
_runtime_manager = RuntimeAgentManager()
|
|
231
|
-
return _runtime_manager
|
|
257
|
+
return _runtime_manager
|
|
@@ -9,41 +9,85 @@ from code_puppy.command_line.utils import make_directory_table
|
|
|
9
9
|
from code_puppy.config import get_config_keys
|
|
10
10
|
from code_puppy.tools.tools_content import tools_content
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
def get_commands_help():
|
|
13
14
|
"""Generate commands help using Rich Text objects to avoid markup conflicts."""
|
|
14
15
|
from rich.text import Text
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
# Build help text programmatically
|
|
17
18
|
help_lines = []
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
# Title
|
|
20
21
|
help_lines.append(Text("Commands Help", style="bold magenta"))
|
|
21
|
-
|
|
22
|
-
# Commands - build each line programmatically
|
|
23
|
-
help_lines.append(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
help_lines.append(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
help_lines.append(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
help_lines.append(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
help_lines.append(
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
|
|
23
|
+
# Commands - build each line programmatically
|
|
24
|
+
help_lines.append(
|
|
25
|
+
Text("/help, /h", style="cyan") + Text(" Show this help message")
|
|
26
|
+
)
|
|
27
|
+
help_lines.append(
|
|
28
|
+
Text("/cd", style="cyan")
|
|
29
|
+
+ Text(" <dir> Change directory or show directories")
|
|
30
|
+
)
|
|
31
|
+
help_lines.append(
|
|
32
|
+
Text("/agent", style="cyan")
|
|
33
|
+
+ Text(" <name> Switch to a different agent or show available agents")
|
|
34
|
+
)
|
|
35
|
+
help_lines.append(
|
|
36
|
+
Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode")
|
|
37
|
+
)
|
|
38
|
+
help_lines.append(
|
|
39
|
+
Text("/generate-pr-description", style="cyan")
|
|
40
|
+
+ Text(" [@dir] Generate comprehensive PR description")
|
|
41
|
+
)
|
|
42
|
+
help_lines.append(
|
|
43
|
+
Text("/model, /m", style="cyan") + Text(" <model> Set active model")
|
|
44
|
+
)
|
|
45
|
+
help_lines.append(
|
|
46
|
+
Text("/mcp", style="cyan")
|
|
47
|
+
+ Text(" Manage MCP servers (list, start, stop, status, etc.)")
|
|
48
|
+
)
|
|
49
|
+
help_lines.append(
|
|
50
|
+
Text("/motd", style="cyan")
|
|
51
|
+
+ Text(" Show the latest message of the day (MOTD)")
|
|
52
|
+
)
|
|
53
|
+
help_lines.append(
|
|
54
|
+
Text("/show", style="cyan")
|
|
55
|
+
+ Text(" Show puppy config key-values")
|
|
56
|
+
)
|
|
57
|
+
help_lines.append(
|
|
58
|
+
Text("/compact", style="cyan")
|
|
59
|
+
+ Text(" Summarize and compact current chat history")
|
|
60
|
+
)
|
|
61
|
+
help_lines.append(
|
|
62
|
+
Text("/dump_context", style="cyan")
|
|
63
|
+
+ Text(" <name> Save current message history to file")
|
|
64
|
+
)
|
|
65
|
+
help_lines.append(
|
|
66
|
+
Text("/load_context", style="cyan")
|
|
67
|
+
+ Text(" <name> Load message history from file")
|
|
68
|
+
)
|
|
69
|
+
help_lines.append(
|
|
70
|
+
Text("/set", style="cyan")
|
|
71
|
+
+ Text(
|
|
72
|
+
" Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)"
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
help_lines.append(
|
|
76
|
+
Text("/tools", style="cyan")
|
|
77
|
+
+ Text(" Show available tools and capabilities")
|
|
78
|
+
)
|
|
79
|
+
help_lines.append(
|
|
80
|
+
Text("/<unknown>", style="cyan")
|
|
81
|
+
+ Text(" Show unknown command warning")
|
|
82
|
+
)
|
|
83
|
+
|
|
40
84
|
# Combine all lines
|
|
41
85
|
final_text = Text()
|
|
42
86
|
for i, line in enumerate(help_lines):
|
|
43
87
|
if i > 0:
|
|
44
88
|
final_text.append("\n")
|
|
45
89
|
final_text.append_text(line)
|
|
46
|
-
|
|
90
|
+
|
|
47
91
|
return final_text
|
|
48
92
|
|
|
49
93
|
|
|
@@ -69,9 +113,9 @@ def handle_command(command: str):
|
|
|
69
113
|
from code_puppy.config import get_compaction_strategy
|
|
70
114
|
from code_puppy.message_history_processor import (
|
|
71
115
|
estimate_tokens_for_message,
|
|
116
|
+
get_protected_token_count,
|
|
72
117
|
summarize_messages,
|
|
73
118
|
truncation,
|
|
74
|
-
get_protected_token_count,
|
|
75
119
|
)
|
|
76
120
|
from code_puppy.messaging import (
|
|
77
121
|
emit_error,
|
|
@@ -152,17 +196,16 @@ def handle_command(command: str):
|
|
|
152
196
|
return True
|
|
153
197
|
|
|
154
198
|
if command.strip().startswith("/show"):
|
|
199
|
+
from code_puppy.agents import get_current_agent_config
|
|
155
200
|
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
156
201
|
from code_puppy.config import (
|
|
202
|
+
get_compaction_strategy,
|
|
203
|
+
get_compaction_threshold,
|
|
157
204
|
get_owner_name,
|
|
158
205
|
get_protected_token_count,
|
|
159
206
|
get_puppy_name,
|
|
160
|
-
get_compaction_threshold,
|
|
161
207
|
get_yolo_mode,
|
|
162
208
|
)
|
|
163
|
-
from code_puppy.agents import get_current_agent_config
|
|
164
|
-
|
|
165
|
-
from code_puppy.config import get_compaction_strategy
|
|
166
209
|
|
|
167
210
|
puppy_name = get_puppy_name()
|
|
168
211
|
owner_name = get_owner_name()
|
|
@@ -234,10 +277,10 @@ def handle_command(command: str):
|
|
|
234
277
|
if command.startswith("/agent"):
|
|
235
278
|
# Handle agent switching
|
|
236
279
|
from code_puppy.agents import (
|
|
280
|
+
get_agent_descriptions,
|
|
237
281
|
get_available_agents,
|
|
238
282
|
get_current_agent_config,
|
|
239
283
|
set_current_agent,
|
|
240
|
-
get_agent_descriptions,
|
|
241
284
|
)
|
|
242
285
|
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
243
286
|
|
|
@@ -317,10 +360,14 @@ def handle_command(command: str):
|
|
|
317
360
|
emit_warning("Usage: /agent [agent-name]")
|
|
318
361
|
return True
|
|
319
362
|
|
|
320
|
-
if command.startswith("/model"):
|
|
363
|
+
if command.startswith("/model") or command.startswith("/m "):
|
|
321
364
|
# Try setting model and show confirmation
|
|
322
365
|
# Handle both /model and /m for backward compatibility
|
|
323
|
-
model_command = command
|
|
366
|
+
model_command = command
|
|
367
|
+
if command.startswith("/model"):
|
|
368
|
+
# Convert /model to /m for internal processing
|
|
369
|
+
model_command = command.replace("/model", "/m", 1)
|
|
370
|
+
|
|
324
371
|
new_input = update_model_in_input(model_command)
|
|
325
372
|
if new_input is not None:
|
|
326
373
|
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
@@ -334,16 +381,18 @@ def handle_command(command: str):
|
|
|
334
381
|
return True
|
|
335
382
|
# If no model matched, show available models
|
|
336
383
|
model_names = load_model_names()
|
|
337
|
-
emit_warning("Usage: /model <model-name>")
|
|
384
|
+
emit_warning("Usage: /model <model-name> or /m <model-name>")
|
|
338
385
|
emit_warning(f"Available models: {', '.join(model_names)}")
|
|
339
386
|
return True
|
|
340
|
-
|
|
387
|
+
|
|
341
388
|
if command.startswith("/mcp"):
|
|
342
|
-
from code_puppy.command_line.
|
|
389
|
+
from code_puppy.command_line.mcp import MCPCommandHandler
|
|
390
|
+
|
|
343
391
|
handler = MCPCommandHandler()
|
|
344
392
|
return handler.handle_mcp_command(command)
|
|
345
393
|
if command in ("/help", "/h"):
|
|
346
394
|
import uuid
|
|
395
|
+
|
|
347
396
|
group_id = str(uuid.uuid4())
|
|
348
397
|
help_text = get_commands_help()
|
|
349
398
|
emit_info(help_text, message_group_id=group_id)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Command Line Interface - Namespace package for MCP server management commands.
|
|
3
|
+
|
|
4
|
+
This package provides a modular command interface for managing MCP servers.
|
|
5
|
+
Each command is implemented in its own module for better maintainability.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .handler import MCPCommandHandler
|
|
9
|
+
|
|
10
|
+
__all__ = ["MCPCommandHandler"]
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Add Command - Adds new MCP servers from JSON configuration or wizard.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from code_puppy.messaging import emit_info
|
|
11
|
+
from code_puppy.state_management import is_tui_mode
|
|
12
|
+
|
|
13
|
+
from .base import MCPCommandBase
|
|
14
|
+
from .wizard_utils import run_interactive_install_wizard
|
|
15
|
+
|
|
16
|
+
# Configure logging
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AddCommand(MCPCommandBase):
|
|
21
|
+
"""
|
|
22
|
+
Command handler for adding MCP servers.
|
|
23
|
+
|
|
24
|
+
Adds new MCP servers from JSON configuration or interactive wizard.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Add a new MCP server from JSON configuration or launch wizard.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
/mcp add - Launch interactive wizard
|
|
33
|
+
/mcp add <json> - Add server from JSON config
|
|
34
|
+
|
|
35
|
+
Example JSON:
|
|
36
|
+
/mcp add {"name": "test", "type": "stdio", "command": "echo", "args": ["hello"]}
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
args: Command arguments - JSON config or empty for wizard
|
|
40
|
+
group_id: Optional message group ID for grouping related messages
|
|
41
|
+
"""
|
|
42
|
+
if group_id is None:
|
|
43
|
+
group_id = self.generate_group_id()
|
|
44
|
+
|
|
45
|
+
# Check if in TUI mode and guide user to use Ctrl+T instead
|
|
46
|
+
if is_tui_mode() and not args:
|
|
47
|
+
emit_info(
|
|
48
|
+
"💡 In TUI mode, press Ctrl+T to open the MCP Install Wizard",
|
|
49
|
+
message_group=group_id,
|
|
50
|
+
)
|
|
51
|
+
emit_info(
|
|
52
|
+
" The wizard provides a better interface for browsing and installing MCP servers.",
|
|
53
|
+
message_group=group_id,
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
if args:
|
|
59
|
+
# Parse JSON from arguments
|
|
60
|
+
json_str = " ".join(args)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
config_dict = json.loads(json_str)
|
|
64
|
+
except json.JSONDecodeError as e:
|
|
65
|
+
emit_info(f"Invalid JSON: {e}", message_group=group_id)
|
|
66
|
+
emit_info(
|
|
67
|
+
"Usage: /mcp add <json> or /mcp add (for wizard)",
|
|
68
|
+
message_group=group_id,
|
|
69
|
+
)
|
|
70
|
+
emit_info(
|
|
71
|
+
'Example: /mcp add {"name": "test", "type": "stdio", "command": "echo"}',
|
|
72
|
+
message_group=group_id,
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Validate required fields
|
|
77
|
+
if "name" not in config_dict:
|
|
78
|
+
emit_info("Missing required field: 'name'", message_group=group_id)
|
|
79
|
+
return
|
|
80
|
+
if "type" not in config_dict:
|
|
81
|
+
emit_info("Missing required field: 'type'", message_group=group_id)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# Add the server
|
|
85
|
+
success = self._add_server_from_json(config_dict, group_id)
|
|
86
|
+
|
|
87
|
+
if success:
|
|
88
|
+
# Reload MCP servers
|
|
89
|
+
try:
|
|
90
|
+
from code_puppy.agent import reload_mcp_servers
|
|
91
|
+
|
|
92
|
+
reload_mcp_servers()
|
|
93
|
+
except ImportError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
emit_info(
|
|
97
|
+
"Use '/mcp list' to see all servers", message_group=group_id
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
# No arguments - launch interactive wizard with server templates
|
|
102
|
+
success = run_interactive_install_wizard(self.manager, group_id)
|
|
103
|
+
|
|
104
|
+
if success:
|
|
105
|
+
# Reload the agent to pick up new server
|
|
106
|
+
try:
|
|
107
|
+
from code_puppy.agent import reload_mcp_servers
|
|
108
|
+
|
|
109
|
+
reload_mcp_servers()
|
|
110
|
+
except ImportError:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
except ImportError as e:
|
|
114
|
+
logger.error(f"Failed to import: {e}")
|
|
115
|
+
emit_info("Required module not available", message_group=group_id)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Error in add command: {e}")
|
|
118
|
+
emit_info(f"[red]Error adding server: {e}[/red]", message_group=group_id)
|
|
119
|
+
|
|
120
|
+
def _add_server_from_json(self, config_dict: dict, group_id: str) -> bool:
|
|
121
|
+
"""
|
|
122
|
+
Add a server from JSON configuration.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
config_dict: Server configuration dictionary
|
|
126
|
+
group_id: Message group ID
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if successful, False otherwise
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
from code_puppy.config import MCP_SERVERS_FILE
|
|
133
|
+
from code_puppy.mcp.managed_server import ServerConfig
|
|
134
|
+
|
|
135
|
+
# Extract required fields
|
|
136
|
+
name = config_dict.pop("name")
|
|
137
|
+
server_type = config_dict.pop("type")
|
|
138
|
+
enabled = config_dict.pop("enabled", True)
|
|
139
|
+
|
|
140
|
+
# Everything else goes into config
|
|
141
|
+
server_config = ServerConfig(
|
|
142
|
+
id=f"{name}_{hash(name)}",
|
|
143
|
+
name=name,
|
|
144
|
+
type=server_type,
|
|
145
|
+
enabled=enabled,
|
|
146
|
+
config=config_dict, # Remaining fields are server-specific config
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Register the server
|
|
150
|
+
server_id = self.manager.register_server(server_config)
|
|
151
|
+
|
|
152
|
+
if not server_id:
|
|
153
|
+
emit_info(f"Failed to add server '{name}'", message_group=group_id)
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
emit_info(
|
|
157
|
+
f"✅ Added server '{name}' (ID: {server_id})", message_group=group_id
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Save to mcp_servers.json for persistence
|
|
161
|
+
if os.path.exists(MCP_SERVERS_FILE):
|
|
162
|
+
with open(MCP_SERVERS_FILE, "r") as f:
|
|
163
|
+
data = json.load(f)
|
|
164
|
+
servers = data.get("mcp_servers", {})
|
|
165
|
+
else:
|
|
166
|
+
servers = {}
|
|
167
|
+
data = {"mcp_servers": servers}
|
|
168
|
+
|
|
169
|
+
# Add new server
|
|
170
|
+
servers[name] = config_dict.copy()
|
|
171
|
+
servers[name]["type"] = server_type
|
|
172
|
+
|
|
173
|
+
# Save back
|
|
174
|
+
os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
|
|
175
|
+
with open(MCP_SERVERS_FILE, "w") as f:
|
|
176
|
+
json.dump(data, f, indent=2)
|
|
177
|
+
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Error adding server from JSON: {e}")
|
|
182
|
+
emit_info(f"[red]Failed to add server: {e}[/red]", message_group=group_id)
|
|
183
|
+
return False
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Command Base Classes - Shared functionality for MCP command handlers.
|
|
3
|
+
|
|
4
|
+
Provides base classes and common utilities used across all MCP command modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from code_puppy.mcp.manager import get_mcp_manager
|
|
12
|
+
|
|
13
|
+
# Configure logging
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MCPCommandBase:
|
|
18
|
+
"""
|
|
19
|
+
Base class for MCP command handlers.
|
|
20
|
+
|
|
21
|
+
Provides common functionality like console access and MCP manager access
|
|
22
|
+
that all command handlers need.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize the base command handler."""
|
|
27
|
+
self.console = Console()
|
|
28
|
+
self.manager = get_mcp_manager()
|
|
29
|
+
logger.debug(f"Initialized {self.__class__.__name__}")
|
|
30
|
+
|
|
31
|
+
def generate_group_id(self) -> str:
|
|
32
|
+
"""Generate a unique group ID for message grouping."""
|
|
33
|
+
import uuid
|
|
34
|
+
|
|
35
|
+
return str(uuid.uuid4())
|