code-puppy 0.0.127__py3-none-any.whl → 0.0.129__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 (35) hide show
  1. code_puppy/__init__.py +1 -0
  2. code_puppy/agent.py +65 -69
  3. code_puppy/agents/agent_code_puppy.py +0 -3
  4. code_puppy/agents/runtime_manager.py +231 -0
  5. code_puppy/command_line/command_handler.py +56 -25
  6. code_puppy/command_line/mcp_commands.py +1298 -0
  7. code_puppy/command_line/meta_command_handler.py +3 -2
  8. code_puppy/command_line/model_picker_completion.py +21 -8
  9. code_puppy/http_utils.py +1 -1
  10. code_puppy/main.py +99 -158
  11. code_puppy/mcp/__init__.py +23 -0
  12. code_puppy/mcp/async_lifecycle.py +237 -0
  13. code_puppy/mcp/circuit_breaker.py +218 -0
  14. code_puppy/mcp/config_wizard.py +437 -0
  15. code_puppy/mcp/dashboard.py +291 -0
  16. code_puppy/mcp/error_isolation.py +360 -0
  17. code_puppy/mcp/examples/retry_example.py +208 -0
  18. code_puppy/mcp/health_monitor.py +549 -0
  19. code_puppy/mcp/managed_server.py +346 -0
  20. code_puppy/mcp/manager.py +701 -0
  21. code_puppy/mcp/registry.py +412 -0
  22. code_puppy/mcp/retry_manager.py +321 -0
  23. code_puppy/mcp/server_registry_catalog.py +751 -0
  24. code_puppy/mcp/status_tracker.py +355 -0
  25. code_puppy/messaging/spinner/textual_spinner.py +6 -2
  26. code_puppy/model_factory.py +19 -4
  27. code_puppy/models.json +8 -6
  28. code_puppy/tui/app.py +19 -27
  29. code_puppy/tui/tests/test_agent_command.py +22 -15
  30. {code_puppy-0.0.127.data → code_puppy-0.0.129.data}/data/code_puppy/models.json +8 -6
  31. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/METADATA +4 -3
  32. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/RECORD +35 -19
  33. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/WHEEL +0 -0
  34. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/entry_points.txt +0 -0
  35. {code_puppy-0.0.127.dist-info → code_puppy-0.0.129.dist-info}/licenses/LICENSE +0 -0
code_puppy/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
1
  import importlib.metadata
2
2
 
3
+ # Biscuit was here! 🐶
3
4
  __version__ = importlib.metadata.version("code-puppy")
code_puppy/agent.py CHANGED
@@ -1,3 +1,4 @@
1
+ import uuid
1
2
  from pathlib import Path
2
3
  from typing import Dict, Optional
3
4
 
@@ -42,7 +43,9 @@ _code_generation_agent = None
42
43
 
43
44
 
44
45
  def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
46
+ """Load MCP servers using the new manager while maintaining backward compatibility."""
45
47
  from code_puppy.config import get_value, load_mcp_server_configs
48
+ from code_puppy.mcp import get_mcp_manager, ServerConfig
46
49
 
47
50
  # Check if MCP servers are disabled
48
51
  mcp_disabled = get_value("disable_mcp_servers")
@@ -50,86 +53,76 @@ def _load_mcp_servers(extra_headers: Optional[Dict[str, str]] = None):
50
53
  emit_system_message("[dim]MCP servers disabled via config[/dim]")
51
54
  return []
52
55
 
56
+ # Get the MCP manager singleton
57
+ manager = get_mcp_manager()
58
+
59
+ # Load configurations from legacy file for backward compatibility
53
60
  configs = load_mcp_server_configs()
54
61
  if not configs:
55
- emit_system_message("[dim]No MCP servers configured[/dim]")
56
- return []
57
- servers = []
58
- for name, conf in configs.items():
59
- server_type = conf.get("type", "sse")
60
- url = conf.get("url")
61
- timeout = conf.get("timeout", 30)
62
- server_headers = {}
63
- if extra_headers:
64
- server_headers.update(extra_headers)
65
- user_headers = conf.get("headers") or {}
66
- if isinstance(user_headers, dict) and user_headers:
62
+ # Check if manager already has servers (could be from new system)
63
+ existing_servers = manager.list_servers()
64
+ if not existing_servers:
65
+ emit_system_message("[dim]No MCP servers configured[/dim]")
66
+ return []
67
+ else:
68
+ # Register servers from legacy config with manager
69
+ for name, conf in configs.items():
67
70
  try:
68
- user_headers = resolve_env_var_in_header(user_headers)
69
- except Exception:
70
- pass
71
- server_headers.update(user_headers)
72
- http_client = None
73
-
74
- try:
75
- if server_type == "http" and url:
76
- emit_system_message(
77
- f"Registering MCP Server (HTTP) - {url} (timeout: {timeout}s, headers: {bool(server_headers)})"
78
- )
79
- http_client = create_reopenable_async_client(
80
- timeout=timeout, headers=server_headers or None, verify=False
71
+ # Convert legacy format to new ServerConfig
72
+ server_config = ServerConfig(
73
+ id=conf.get("id", f"{name}_{hash(name)}"),
74
+ name=name,
75
+ type=conf.get("type", "sse"),
76
+ enabled=conf.get("enabled", True),
77
+ config=conf
81
78
  )
82
- servers.append(
83
- MCPServerStreamableHTTP(url=url, http_client=http_client)
84
- )
85
- elif (
86
- server_type == "stdio"
87
- ): # Fixed: was "stdios" (plural), should be "stdio" (singular)
88
- command = conf.get("command")
89
- args = conf.get("args", [])
90
- timeout = conf.get(
91
- "timeout", 30
92
- ) # Default 30 seconds for stdio servers (npm downloads can be slow)
93
- if command:
94
- emit_system_message(
95
- f"Registering MCP Server (Stdio) - {command} {args} (timeout: {timeout}s)"
96
- )
97
- servers.append(MCPServerStdio(command, args=args, timeout=timeout))
79
+
80
+ # Check if server already registered
81
+ existing = manager.get_server_by_name(name)
82
+ if not existing:
83
+ # Register new server
84
+ manager.register_server(server_config)
85
+ emit_system_message(f"[dim]Registered MCP server: {name}[/dim]")
98
86
  else:
99
- emit_error(f"MCP Server '{name}' missing required 'command' field")
100
- elif server_type == "sse" and url:
101
- emit_system_message(
102
- f"Registering MCP Server (SSE) - {url} (timeout: {timeout}s, headers: {bool(server_headers)})"
103
- )
104
- # For SSE, allow long reads; only bound connect timeout
105
- http_client = create_reopenable_async_client(
106
- timeout=30, headers=server_headers or None, verify=False
107
- )
108
- servers.append(MCPServerSSE(url=url, http_client=http_client))
109
- else:
110
- emit_error(
111
- f"Invalid type '{server_type}' or missing URL for MCP server '{name}'"
112
- )
113
- except Exception as e:
114
- emit_error(f"Failed to register MCP server '{name}': {str(e)}")
115
- emit_info(f"Skipping server '{name}' and continuing with other servers...")
116
- # Continue with other servers instead of crashing
117
- continue
118
-
87
+ # Update existing server config if needed
88
+ if existing.config != server_config.config:
89
+ manager.update_server(existing.id, server_config)
90
+ emit_system_message(f"[dim]Updated MCP server: {name}[/dim]")
91
+
92
+ except Exception as e:
93
+ emit_error(f"Failed to register MCP server '{name}': {str(e)}")
94
+ continue
95
+
96
+ # Get pydantic-ai compatible servers from manager
97
+ servers = manager.get_servers_for_agent()
98
+
119
99
  if servers:
120
100
  emit_system_message(
121
- f"[green]Successfully registered {len(servers)} MCP server(s)[/green]"
101
+ f"[green]Successfully loaded {len(servers)} MCP server(s)[/green]"
122
102
  )
123
103
  else:
124
104
  emit_system_message(
125
- "[yellow]No MCP servers were successfully registered[/yellow]"
105
+ "[yellow]No MCP servers available (check if servers are enabled)[/yellow]"
126
106
  )
127
-
107
+
128
108
  return servers
129
109
 
130
110
 
131
- def reload_code_generation_agent():
111
+ def reload_mcp_servers():
112
+ """Reload MCP servers without restarting the agent."""
113
+ from code_puppy.mcp import get_mcp_manager
114
+
115
+ manager = get_mcp_manager()
116
+ # Reload configurations
117
+ _load_mcp_servers()
118
+ # Return updated servers
119
+ return manager.get_servers_for_agent()
120
+
121
+
122
+ def reload_code_generation_agent(message_group: str | None):
132
123
  """Force-reload the agent, usually after a model change."""
124
+ if message_group is None:
125
+ message_group = str(uuid.uuid4())
133
126
  global _code_generation_agent, _LAST_MODEL_NAME
134
127
  from code_puppy.config import clear_model_cache, get_model_name
135
128
  from code_puppy.agents import clear_agent_cache
@@ -139,14 +132,15 @@ def reload_code_generation_agent():
139
132
  clear_agent_cache()
140
133
 
141
134
  model_name = get_model_name()
142
- emit_info(f"[bold cyan]Loading Model: {model_name}[/bold cyan]")
135
+ emit_info(f"[bold cyan]Loading Model: {model_name}[/bold cyan]", message_group=message_group)
143
136
  models_config = ModelFactory.load_config()
144
137
  model = ModelFactory.get_model(model_name, models_config)
145
138
 
146
139
  # Get agent-specific system prompt
147
140
  agent_config = get_current_agent_config()
148
141
  emit_info(
149
- f"[bold magenta]Loading Agent: {agent_config.display_name}[/bold magenta]"
142
+ f"[bold magenta]Loading Agent: {agent_config.display_name}[/bold magenta]",
143
+ message_group=message_group
150
144
  )
151
145
 
152
146
  instructions = agent_config.get_system_prompt()
@@ -183,17 +177,19 @@ def reload_code_generation_agent():
183
177
  return _code_generation_agent
184
178
 
185
179
 
186
- def get_code_generation_agent(force_reload=False):
180
+ def get_code_generation_agent(force_reload=False, message_group: str | None = None):
187
181
  """
188
182
  Retrieve the agent with the currently configured model.
189
183
  Forces a reload if the model has changed, or if force_reload is passed.
190
184
  """
191
185
  global _code_generation_agent, _LAST_MODEL_NAME
186
+ if message_group is None:
187
+ message_group = str(uuid.uuid4())
192
188
  from code_puppy.config import get_model_name
193
189
 
194
190
  model_name = get_model_name()
195
191
  if _code_generation_agent is None or _LAST_MODEL_NAME != model_name or force_reload:
196
- return reload_code_generation_agent()
192
+ return reload_code_generation_agent(message_group)
197
193
  return _code_generation_agent
198
194
 
199
195
 
@@ -140,9 +140,6 @@ Important rules:
140
140
  Your solutions should be production-ready, maintainable, and follow best practices for the chosen language.
141
141
 
142
142
  Return your final response as a string output
143
- Walmart specific rules:
144
- - You are operating inside Walmart Global Tech! Yay!
145
- - Always use uv when working with python, and always use --index-url https://pypi.ci.artifacts.walmart.com/artifactory/api/pypi/external-pypi/simple
146
143
  """
147
144
 
148
145
  prompt_additions = callbacks.on_load_prompt()
@@ -0,0 +1,231 @@
1
+ """
2
+ Runtime agent manager that ensures proper agent instance updates.
3
+
4
+ This module provides a wrapper around the agent singleton that ensures
5
+ all references to the agent are properly updated when it's reloaded.
6
+ """
7
+
8
+ import asyncio
9
+ import signal
10
+ import uuid
11
+ from typing import Optional, Any
12
+
13
+ import mcp
14
+ from pydantic_ai import Agent
15
+ from pydantic_ai.usage import UsageLimits
16
+
17
+ from code_puppy.messaging.message_queue import emit_info, emit_warning
18
+
19
+
20
+ class RuntimeAgentManager:
21
+ """
22
+ Manages the runtime agent instance and ensures proper updates.
23
+
24
+ This class acts as a proxy that always returns the current agent instance,
25
+ ensuring that when the agent is reloaded, all code using this manager
26
+ automatically gets the updated instance.
27
+ """
28
+
29
+ def __init__(self):
30
+ """Initialize the runtime agent manager."""
31
+ self._agent: Optional[Agent] = None
32
+ self._last_model_name: Optional[str] = None
33
+
34
+ def get_agent(self, force_reload: bool = False, message_group: str = "") -> Agent:
35
+ """
36
+ Get the current agent instance.
37
+
38
+ This method always returns the most recent agent instance,
39
+ automatically handling reloads when the model changes.
40
+
41
+ Args:
42
+ force_reload: If True, force a reload of the agent
43
+
44
+ Returns:
45
+ The current agent instance
46
+ """
47
+ from code_puppy.agent import get_code_generation_agent
48
+
49
+ # Always get the current singleton - this ensures we have the latest
50
+ current_agent = get_code_generation_agent(force_reload=force_reload, message_group=message_group)
51
+ self._agent = current_agent
52
+
53
+ return self._agent
54
+
55
+ def reload_agent(self) -> Agent:
56
+ """
57
+ Force reload the agent.
58
+
59
+ This is typically called after MCP servers are started/stopped.
60
+
61
+ Returns:
62
+ The newly loaded agent instance
63
+ """
64
+ message_group = uuid.uuid4()
65
+ emit_info("[bold cyan]Reloading agent with updated configuration...[/bold cyan]", message_group=message_group)
66
+ return self.get_agent(force_reload=True, message_group=message_group)
67
+
68
+ async def run_with_mcp(self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs) -> Any:
69
+ """
70
+ Run the agent with MCP servers and full cancellation support.
71
+
72
+ This method ensures we're always using the current agent instance
73
+ and handles Ctrl+C interruption properly by creating a cancellable task.
74
+
75
+ Args:
76
+ prompt: The user prompt to process
77
+ usage_limits: Optional usage limits for the agent
78
+ **kwargs: Additional arguments to pass to agent.run (e.g., message_history)
79
+
80
+ Returns:
81
+ The agent's response
82
+
83
+ Raises:
84
+ asyncio.CancelledError: When execution is cancelled by user
85
+ """
86
+ agent = self.get_agent()
87
+ group_id = str(uuid.uuid4())
88
+ # Function to run agent with MCP
89
+ async def run_agent_task():
90
+ try:
91
+ async with agent:
92
+ return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
93
+ except* mcp.shared.exceptions.McpError as mcp_error:
94
+ emit_warning(f"MCP server error: {str(mcp_error)}", group_id=group_id)
95
+ emit_warning(f"{str(mcp_error)}", group_id=group_id)
96
+ emit_warning(f"Try disabling any malfunctioning MCP servers", group_id=group_id)
97
+ except* InterruptedError as ie:
98
+ emit_warning(f"Interrupted: {str(ie)}")
99
+ except* Exception as other_error:
100
+ # Filter out CancelledError from the exception group - let it propagate
101
+ remaining_exceptions = []
102
+ def collect_non_cancelled_exceptions(exc):
103
+ if isinstance(exc, ExceptionGroup):
104
+ for sub_exc in exc.exceptions:
105
+ collect_non_cancelled_exceptions(sub_exc)
106
+ elif not isinstance(exc, asyncio.CancelledError):
107
+ remaining_exceptions.append(exc)
108
+ emit_warning(f"Unexpected error: {str(exc)}", group_id=group_id)
109
+ emit_warning(f"{str(exc.args)}", group_id=group_id)
110
+
111
+ collect_non_cancelled_exceptions(other_error)
112
+
113
+ # If there are CancelledError exceptions in the group, re-raise them
114
+ cancelled_exceptions = []
115
+ def collect_cancelled_exceptions(exc):
116
+ if isinstance(exc, ExceptionGroup):
117
+ for sub_exc in exc.exceptions:
118
+ collect_cancelled_exceptions(sub_exc)
119
+ elif isinstance(exc, asyncio.CancelledError):
120
+ cancelled_exceptions.append(exc)
121
+
122
+ collect_cancelled_exceptions(other_error)
123
+
124
+ if cancelled_exceptions:
125
+ # Re-raise the first CancelledError to propagate cancellation
126
+ raise cancelled_exceptions[0]
127
+
128
+ # Create the task FIRST
129
+ agent_task = asyncio.create_task(run_agent_task())
130
+
131
+ # Import shell process killer
132
+ from code_puppy.tools.command_runner import kill_all_running_shell_processes
133
+
134
+ # Ensure the interrupt handler only acts once per task
135
+ handled = False
136
+
137
+ def keyboard_interrupt_handler(sig, frame):
138
+ """Signal handler for Ctrl+C - replicating exact original logic"""
139
+ nonlocal handled
140
+ if handled:
141
+ return
142
+ handled = True
143
+
144
+ # First, nuke any running shell processes triggered by tools
145
+ try:
146
+ killed = kill_all_running_shell_processes()
147
+ if killed:
148
+ emit_warning(f"Cancelled {killed} running shell process(es).")
149
+ else:
150
+ # Only cancel the agent task if no shell processes were killed
151
+ if not agent_task.done():
152
+ agent_task.cancel()
153
+ except Exception as e:
154
+ emit_warning(f"Shell kill error: {e}")
155
+ # If shell kill failed, still try to cancel the agent task
156
+ if not agent_task.done():
157
+ agent_task.cancel()
158
+ # Don't call the original handler
159
+ # This prevents the application from exiting
160
+
161
+ try:
162
+ # Save original handler and set our custom one AFTER task is created
163
+ original_handler = signal.signal(signal.SIGINT, keyboard_interrupt_handler)
164
+
165
+ # Wait for the task to complete or be cancelled
166
+ result = await agent_task
167
+ return result
168
+ except asyncio.CancelledError:
169
+ # Task was cancelled by our handler
170
+ raise
171
+ except KeyboardInterrupt:
172
+ # Handle direct keyboard interrupt during await
173
+ if not agent_task.done():
174
+ agent_task.cancel()
175
+ try:
176
+ await agent_task
177
+ except asyncio.CancelledError:
178
+ pass
179
+ raise asyncio.CancelledError()
180
+ finally:
181
+ # Restore original signal handler
182
+ if original_handler:
183
+ signal.signal(signal.SIGINT, original_handler)
184
+
185
+ async def run(self, prompt: str, usage_limits: Optional[UsageLimits] = None, **kwargs) -> Any:
186
+ """
187
+ Run the agent without explicitly managing MCP servers.
188
+
189
+ Args:
190
+ prompt: The user prompt to process
191
+ usage_limits: Optional usage limits for the agent
192
+ **kwargs: Additional arguments to pass to agent.run (e.g., message_history)
193
+
194
+ Returns:
195
+ The agent's response
196
+ """
197
+ agent = self.get_agent()
198
+ return await agent.run(prompt, usage_limits=usage_limits, **kwargs)
199
+
200
+ def __getattr__(self, name: str) -> Any:
201
+ """
202
+ Proxy all other attribute access to the current agent.
203
+
204
+ This allows the manager to be used as a drop-in replacement
205
+ for direct agent access.
206
+
207
+ Args:
208
+ name: The attribute name to access
209
+
210
+ Returns:
211
+ The attribute from the current agent
212
+ """
213
+ agent = self.get_agent()
214
+ return getattr(agent, name)
215
+
216
+
217
+ # Global singleton instance
218
+ _runtime_manager: Optional[RuntimeAgentManager] = None
219
+
220
+
221
+ def get_runtime_agent_manager() -> RuntimeAgentManager:
222
+ """
223
+ Get the global runtime agent manager instance.
224
+
225
+ Returns:
226
+ The singleton RuntimeAgentManager instance
227
+ """
228
+ global _runtime_manager
229
+ if _runtime_manager is None:
230
+ _runtime_manager = RuntimeAgentManager()
231
+ return _runtime_manager
@@ -9,23 +9,42 @@ 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
- COMMANDS_HELP = """
13
- [bold magenta]Commands Help[/bold magenta]
14
- /help, /h Show this help message
15
- /cd <dir> Change directory or show directories
16
- /agent <name> Switch to a different agent or show available agents
17
- /exit, /quit Exit interactive mode
18
- /generate-pr-description [@dir] Generate comprehensive PR description
19
- /m <model> Set active model
20
- /motd Show the latest message of the day (MOTD)
21
- /show Show puppy config key-values
22
- /compact Summarize and compact current chat history
23
- /dump_context <name> Save current message history to file
24
- /load_context <name> Load message history from file
25
- /set Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)
26
- /tools Show available tools and capabilities
27
- /<unknown> Show unknown command warning
28
- """
12
+ def get_commands_help():
13
+ """Generate commands help using Rich Text objects to avoid markup conflicts."""
14
+ from rich.text import Text
15
+
16
+ # Build help text programmatically
17
+ help_lines = []
18
+
19
+ # Title
20
+ help_lines.append(Text("Commands Help", style="bold magenta"))
21
+
22
+ # Commands - build each line programmatically
23
+ help_lines.append(Text("/help, /h", style="cyan") + Text(" Show this help message"))
24
+ help_lines.append(Text("/cd", style="cyan") + Text(" <dir> Change directory or show directories"))
25
+ help_lines.append(Text("/agent", style="cyan") + Text(" <name> Switch to a different agent or show available agents"))
26
+ help_lines.append(Text("/exit, /quit", style="cyan") + Text(" Exit interactive mode"))
27
+ help_lines.append(Text("/generate-pr-description", style="cyan") + Text(" [@dir] Generate comprehensive PR description"))
28
+ help_lines.append(Text("/model", style="cyan") + Text(" <model> Set active model"))
29
+ help_lines.append(Text("/mcp", style="cyan") + Text(" Manage MCP servers (list, start, stop, status, etc.)"))
30
+ help_lines.append(Text("/motd", style="cyan") + Text(" Show the latest message of the day (MOTD)"))
31
+ help_lines.append(Text("/show", style="cyan") + Text(" Show puppy config key-values"))
32
+ help_lines.append(Text("/compact", style="cyan") + Text(" Summarize and compact current chat history"))
33
+ help_lines.append(Text("/dump_context", style="cyan") + Text(" <name> Save current message history to file"))
34
+ help_lines.append(Text("/load_context", style="cyan") + Text(" <name> Load message history from file"))
35
+ help_lines.append(Text("/set", style="cyan") + Text(" Set puppy config key-values (e.g., /set yolo_mode true, /set compaction_strategy truncation)"))
36
+ help_lines.append(Text("/tools", style="cyan") + Text(" Show available tools and capabilities"))
37
+ help_lines.append(Text("/<unknown>", style="cyan") + Text(" Show unknown command warning"))
38
+
39
+
40
+ # Combine all lines
41
+ final_text = Text()
42
+ for i, line in enumerate(help_lines):
43
+ if i > 0:
44
+ final_text.append("\n")
45
+ final_text.append_text(line)
46
+
47
+ return final_text
29
48
 
30
49
 
31
50
  def handle_command(command: str):
@@ -220,7 +239,7 @@ def handle_command(command: str):
220
239
  set_current_agent,
221
240
  get_agent_descriptions,
222
241
  )
223
- from code_puppy.agent import get_code_generation_agent
242
+ from code_puppy.agents.runtime_manager import get_runtime_agent_manager
224
243
 
225
244
  tokens = command.split()
226
245
 
@@ -272,7 +291,8 @@ def handle_command(command: str):
272
291
 
273
292
  if set_current_agent(agent_name):
274
293
  # Reload the agent with new configuration
275
- get_code_generation_agent(force_reload=True)
294
+ manager = get_runtime_agent_manager()
295
+ manager.reload_agent()
276
296
  new_agent = get_current_agent_config()
277
297
  emit_success(
278
298
  f"Switched to agent: {new_agent.display_name}",
@@ -297,25 +317,36 @@ def handle_command(command: str):
297
317
  emit_warning("Usage: /agent [agent-name]")
298
318
  return True
299
319
 
300
- if command.startswith("/m"):
320
+ if command.startswith("/model"):
301
321
  # Try setting model and show confirmation
302
- new_input = update_model_in_input(command)
322
+ # Handle both /model and /m for backward compatibility
323
+ model_command = command.replace("/model", "/m") if command.startswith("/model") else command
324
+ new_input = update_model_in_input(model_command)
303
325
  if new_input is not None:
304
- from code_puppy.agent import get_code_generation_agent
326
+ from code_puppy.agents.runtime_manager import get_runtime_agent_manager
305
327
  from code_puppy.command_line.model_picker_completion import get_active_model
306
328
 
307
329
  model = get_active_model()
308
330
  # Make sure this is called for the test
309
- get_code_generation_agent(force_reload=True)
331
+ manager = get_runtime_agent_manager()
332
+ manager.reload_agent()
310
333
  emit_success(f"Active model set and loaded: {model}")
311
334
  return True
312
335
  # If no model matched, show available models
313
336
  model_names = load_model_names()
314
- emit_warning("Usage: /m <model-name>")
337
+ emit_warning("Usage: /model <model-name>")
315
338
  emit_warning(f"Available models: {', '.join(model_names)}")
316
339
  return True
340
+
341
+ if command.startswith("/mcp"):
342
+ from code_puppy.command_line.mcp_commands import MCPCommandHandler
343
+ handler = MCPCommandHandler()
344
+ return handler.handle_mcp_command(command)
317
345
  if command in ("/help", "/h"):
318
- emit_info(COMMANDS_HELP)
346
+ import uuid
347
+ group_id = str(uuid.uuid4())
348
+ help_text = get_commands_help()
349
+ emit_info(help_text, message_group_id=group_id)
319
350
  return True
320
351
 
321
352
  if command.startswith("/generate-pr-description"):