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.
Files changed (60) hide show
  1. code_puppy/agent.py +15 -17
  2. code_puppy/agents/agent_manager.py +320 -9
  3. code_puppy/agents/base_agent.py +58 -2
  4. code_puppy/agents/runtime_manager.py +68 -42
  5. code_puppy/command_line/command_handler.py +82 -33
  6. code_puppy/command_line/mcp/__init__.py +10 -0
  7. code_puppy/command_line/mcp/add_command.py +183 -0
  8. code_puppy/command_line/mcp/base.py +35 -0
  9. code_puppy/command_line/mcp/handler.py +133 -0
  10. code_puppy/command_line/mcp/help_command.py +146 -0
  11. code_puppy/command_line/mcp/install_command.py +176 -0
  12. code_puppy/command_line/mcp/list_command.py +94 -0
  13. code_puppy/command_line/mcp/logs_command.py +126 -0
  14. code_puppy/command_line/mcp/remove_command.py +82 -0
  15. code_puppy/command_line/mcp/restart_command.py +92 -0
  16. code_puppy/command_line/mcp/search_command.py +117 -0
  17. code_puppy/command_line/mcp/start_all_command.py +126 -0
  18. code_puppy/command_line/mcp/start_command.py +98 -0
  19. code_puppy/command_line/mcp/status_command.py +185 -0
  20. code_puppy/command_line/mcp/stop_all_command.py +109 -0
  21. code_puppy/command_line/mcp/stop_command.py +79 -0
  22. code_puppy/command_line/mcp/test_command.py +107 -0
  23. code_puppy/command_line/mcp/utils.py +129 -0
  24. code_puppy/command_line/mcp/wizard_utils.py +259 -0
  25. code_puppy/command_line/model_picker_completion.py +21 -4
  26. code_puppy/command_line/prompt_toolkit_completion.py +9 -0
  27. code_puppy/main.py +23 -17
  28. code_puppy/mcp/__init__.py +42 -16
  29. code_puppy/mcp/async_lifecycle.py +51 -49
  30. code_puppy/mcp/blocking_startup.py +125 -113
  31. code_puppy/mcp/captured_stdio_server.py +63 -70
  32. code_puppy/mcp/circuit_breaker.py +63 -47
  33. code_puppy/mcp/config_wizard.py +169 -136
  34. code_puppy/mcp/dashboard.py +79 -71
  35. code_puppy/mcp/error_isolation.py +147 -100
  36. code_puppy/mcp/examples/retry_example.py +55 -42
  37. code_puppy/mcp/health_monitor.py +152 -141
  38. code_puppy/mcp/managed_server.py +100 -97
  39. code_puppy/mcp/manager.py +168 -156
  40. code_puppy/mcp/registry.py +148 -110
  41. code_puppy/mcp/retry_manager.py +63 -61
  42. code_puppy/mcp/server_registry_catalog.py +271 -225
  43. code_puppy/mcp/status_tracker.py +80 -80
  44. code_puppy/mcp/system_tools.py +47 -52
  45. code_puppy/messaging/message_queue.py +20 -13
  46. code_puppy/messaging/renderers.py +30 -15
  47. code_puppy/state_management.py +103 -0
  48. code_puppy/tui/app.py +64 -7
  49. code_puppy/tui/components/chat_view.py +3 -3
  50. code_puppy/tui/components/human_input_modal.py +12 -8
  51. code_puppy/tui/screens/__init__.py +2 -2
  52. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  53. code_puppy/tui/tests/test_agent_command.py +3 -3
  54. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/METADATA +1 -1
  55. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/RECORD +59 -41
  56. code_puppy/command_line/mcp_commands.py +0 -1789
  57. {code_puppy-0.0.134.data → code_puppy-0.0.136.data}/data/code_puppy/models.json +0 -0
  58. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/WHEEL +0 -0
  59. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/entry_points.txt +0 -0
  60. {code_puppy-0.0.134.dist-info → code_puppy-0.0.136.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,82 @@
1
+ """
2
+ MCP Remove Command - Removes an MCP server.
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
+
12
+ from .base import MCPCommandBase
13
+ from .utils import find_server_id_by_name, suggest_similar_servers
14
+
15
+ # Configure logging
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class RemoveCommand(MCPCommandBase):
20
+ """
21
+ Command handler for removing MCP servers.
22
+
23
+ Removes a specific MCP server from the manager and configuration.
24
+ """
25
+
26
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
27
+ """
28
+ Remove an MCP server.
29
+
30
+ Args:
31
+ args: Command arguments, expects [server_name]
32
+ group_id: Optional message group ID for grouping related messages
33
+ """
34
+ if group_id is None:
35
+ group_id = self.generate_group_id()
36
+
37
+ if not args:
38
+ emit_info("Usage: /mcp remove <server_name>", message_group=group_id)
39
+ return
40
+
41
+ server_name = args[0]
42
+
43
+ try:
44
+ # Find server by name
45
+ server_id = find_server_id_by_name(self.manager, server_name)
46
+ if not server_id:
47
+ emit_info(f"Server '{server_name}' not found", message_group=group_id)
48
+ suggest_similar_servers(self.manager, server_name, group_id=group_id)
49
+ return
50
+
51
+ # Actually remove the server
52
+ success = self.manager.remove_server(server_id)
53
+
54
+ if success:
55
+ emit_info(f"✓ Removed server: {server_name}", message_group=group_id)
56
+
57
+ # Also remove from mcp_servers.json
58
+ from code_puppy.config import MCP_SERVERS_FILE
59
+
60
+ if os.path.exists(MCP_SERVERS_FILE):
61
+ try:
62
+ with open(MCP_SERVERS_FILE, "r") as f:
63
+ data = json.load(f)
64
+ servers = data.get("mcp_servers", {})
65
+
66
+ # Remove the server if it exists
67
+ if server_name in servers:
68
+ del servers[server_name]
69
+
70
+ # Save back
71
+ with open(MCP_SERVERS_FILE, "w") as f:
72
+ json.dump(data, f, indent=2)
73
+ except Exception as e:
74
+ logger.warning(f"Could not update mcp_servers.json: {e}")
75
+ else:
76
+ emit_info(
77
+ f"✗ Failed to remove server: {server_name}", message_group=group_id
78
+ )
79
+
80
+ except Exception as e:
81
+ logger.error(f"Error removing server '{server_name}': {e}")
82
+ emit_info(f"[red]Error removing server: {e}[/red]", message_group=group_id)
@@ -0,0 +1,92 @@
1
+ """
2
+ MCP Restart Command - Restarts a specific MCP server.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Optional
7
+
8
+ from code_puppy.messaging import emit_info
9
+
10
+ from .base import MCPCommandBase
11
+ from .utils import find_server_id_by_name, suggest_similar_servers
12
+
13
+ # Configure logging
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class RestartCommand(MCPCommandBase):
18
+ """
19
+ Command handler for restarting MCP servers.
20
+
21
+ Stops, reloads configuration, and starts a specific MCP server.
22
+ """
23
+
24
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
25
+ """
26
+ Restart a specific MCP server.
27
+
28
+ Args:
29
+ args: Command arguments, expects [server_name]
30
+ group_id: Optional message group ID for grouping related messages
31
+ """
32
+ if group_id is None:
33
+ group_id = self.generate_group_id()
34
+
35
+ if not args:
36
+ emit_info("Usage: /mcp restart <server_name>", message_group=group_id)
37
+ return
38
+
39
+ server_name = args[0]
40
+
41
+ try:
42
+ # Find server by name
43
+ server_id = find_server_id_by_name(self.manager, server_name)
44
+ if not server_id:
45
+ emit_info(f"Server '{server_name}' not found", message_group=group_id)
46
+ suggest_similar_servers(self.manager, server_name, group_id=group_id)
47
+ return
48
+
49
+ # Stop the server first
50
+ emit_info(f"Stopping server: {server_name}", message_group=group_id)
51
+ self.manager.stop_server_sync(server_id)
52
+
53
+ # Then reload and start it
54
+ emit_info("Reloading configuration...", message_group=group_id)
55
+ reload_success = self.manager.reload_server(server_id)
56
+
57
+ if reload_success:
58
+ emit_info(f"Starting server: {server_name}", message_group=group_id)
59
+ start_success = self.manager.start_server_sync(server_id)
60
+
61
+ if start_success:
62
+ emit_info(
63
+ f"✓ Restarted server: {server_name}", message_group=group_id
64
+ )
65
+
66
+ # Reload the agent to pick up the server changes
67
+ try:
68
+ from code_puppy.agent import get_code_generation_agent
69
+
70
+ get_code_generation_agent(force_reload=True)
71
+ emit_info(
72
+ "[dim]Agent reloaded with updated servers[/dim]",
73
+ message_group=group_id,
74
+ )
75
+ except Exception as e:
76
+ logger.warning(f"Could not reload agent: {e}")
77
+ else:
78
+ emit_info(
79
+ f"✗ Failed to start server after reload: {server_name}",
80
+ message_group=group_id,
81
+ )
82
+ else:
83
+ emit_info(
84
+ f"✗ Failed to reload server configuration: {server_name}",
85
+ message_group=group_id,
86
+ )
87
+
88
+ except Exception as e:
89
+ logger.error(f"Error restarting server '{server_name}': {e}")
90
+ emit_info(
91
+ f"[red]Failed to restart server: {e}[/red]", message_group=group_id
92
+ )
@@ -0,0 +1,117 @@
1
+ """
2
+ MCP Search Command - Searches for pre-configured MCP servers in the registry.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Optional
7
+
8
+ from rich.table import Table
9
+
10
+ from code_puppy.messaging import emit_info, emit_system_message
11
+
12
+ from .base import MCPCommandBase
13
+
14
+ # Configure logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class SearchCommand(MCPCommandBase):
19
+ """
20
+ Command handler for searching MCP server registry.
21
+
22
+ Searches for pre-configured MCP servers with optional query terms.
23
+ """
24
+
25
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
26
+ """
27
+ Search for pre-configured MCP servers in the registry.
28
+
29
+ Args:
30
+ args: Search query terms
31
+ group_id: Optional message group ID for grouping related messages
32
+ """
33
+ if group_id is None:
34
+ group_id = self.generate_group_id()
35
+
36
+ try:
37
+ from code_puppy.mcp.server_registry_catalog import catalog
38
+
39
+ if not args:
40
+ # Show popular servers if no query
41
+ emit_info(
42
+ "[bold cyan]Popular MCP Servers:[/bold cyan]\n",
43
+ message_group=group_id,
44
+ )
45
+ servers = catalog.get_popular(15)
46
+ else:
47
+ query = " ".join(args)
48
+ emit_info(
49
+ f"[bold cyan]Searching for: {query}[/bold cyan]\n",
50
+ message_group=group_id,
51
+ )
52
+ servers = catalog.search(query)
53
+
54
+ if not servers:
55
+ emit_info(
56
+ "[yellow]No servers found matching your search[/yellow]",
57
+ message_group=group_id,
58
+ )
59
+ emit_info(
60
+ "Try: /mcp search database, /mcp search file, /mcp search git",
61
+ message_group=group_id,
62
+ )
63
+ return
64
+
65
+ # Create results table
66
+ table = Table(show_header=True, header_style="bold magenta")
67
+ table.add_column("ID", style="cyan", width=20)
68
+ table.add_column("Name", style="green")
69
+ table.add_column("Category", style="yellow")
70
+ table.add_column("Description", style="white")
71
+ table.add_column("Tags", style="dim")
72
+
73
+ for server in servers[:20]: # Limit to 20 results
74
+ tags = ", ".join(server.tags[:3]) # Show first 3 tags
75
+ if len(server.tags) > 3:
76
+ tags += "..."
77
+
78
+ # Add verified/popular indicators
79
+ indicators = []
80
+ if server.verified:
81
+ indicators.append("✓")
82
+ if server.popular:
83
+ indicators.append("⭐")
84
+ name_display = server.display_name
85
+ if indicators:
86
+ name_display += f" {''.join(indicators)}"
87
+
88
+ table.add_row(
89
+ server.id,
90
+ name_display,
91
+ server.category,
92
+ server.description[:50] + "..."
93
+ if len(server.description) > 50
94
+ else server.description,
95
+ tags,
96
+ )
97
+
98
+ # The first message established the group, subsequent messages will auto-group
99
+ emit_system_message(table, message_group=group_id)
100
+ emit_info("\n[dim]✓ = Verified ⭐ = Popular[/dim]", message_group=group_id)
101
+ emit_info(
102
+ "[yellow]To install:[/yellow] /mcp install <id>", message_group=group_id
103
+ )
104
+ emit_info(
105
+ "[yellow]For details:[/yellow] /mcp search <specific-term>",
106
+ message_group=group_id,
107
+ )
108
+
109
+ except ImportError:
110
+ emit_info(
111
+ "[red]Server registry not available[/red]", message_group=group_id
112
+ )
113
+ except Exception as e:
114
+ logger.error(f"Error searching server registry: {e}")
115
+ emit_info(
116
+ f"[red]Error searching servers: {e}[/red]", message_group=group_id
117
+ )
@@ -0,0 +1,126 @@
1
+ """
2
+ MCP Start All Command - Starts all registered MCP servers.
3
+ """
4
+
5
+ import logging
6
+ import time
7
+ from typing import List, Optional
8
+
9
+ from code_puppy.mcp.managed_server import ServerState
10
+ from code_puppy.messaging import emit_info
11
+
12
+ from .base import MCPCommandBase
13
+
14
+ # Configure logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class StartAllCommand(MCPCommandBase):
19
+ """
20
+ Command handler for starting all MCP servers.
21
+
22
+ Starts all registered MCP servers and provides a summary of results.
23
+ """
24
+
25
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
26
+ """
27
+ Start all registered MCP servers.
28
+
29
+ Args:
30
+ args: Command arguments (unused)
31
+ group_id: Optional message group ID for grouping related messages
32
+ """
33
+ if group_id is None:
34
+ group_id = self.generate_group_id()
35
+
36
+ try:
37
+ servers = self.manager.list_servers()
38
+
39
+ if not servers:
40
+ emit_info(
41
+ "[yellow]No servers registered[/yellow]", message_group=group_id
42
+ )
43
+ return
44
+
45
+ started_count = 0
46
+ failed_count = 0
47
+ already_running = 0
48
+
49
+ emit_info(f"Starting {len(servers)} servers...", message_group=group_id)
50
+
51
+ for server_info in servers:
52
+ server_id = server_info.id
53
+ server_name = server_info.name
54
+
55
+ # Skip if already running
56
+ if server_info.state == ServerState.RUNNING:
57
+ already_running += 1
58
+ emit_info(
59
+ f" • {server_name}: already running", message_group=group_id
60
+ )
61
+ continue
62
+
63
+ # Try to start the server
64
+ success = self.manager.start_server_sync(server_id)
65
+
66
+ if success:
67
+ started_count += 1
68
+ emit_info(
69
+ f" [green]✓ Started: {server_name}[/green]",
70
+ message_group=group_id,
71
+ )
72
+ else:
73
+ failed_count += 1
74
+ emit_info(
75
+ f" [red]✗ Failed: {server_name}[/red]", message_group=group_id
76
+ )
77
+
78
+ # Summary
79
+ emit_info("", message_group=group_id)
80
+ if started_count > 0:
81
+ emit_info(
82
+ f"[green]Started {started_count} server(s)[/green]",
83
+ message_group=group_id,
84
+ )
85
+ if already_running > 0:
86
+ emit_info(
87
+ f"{already_running} server(s) already running",
88
+ message_group=group_id,
89
+ )
90
+ if failed_count > 0:
91
+ emit_info(
92
+ f"[yellow]Failed to start {failed_count} server(s)[/yellow]",
93
+ message_group=group_id,
94
+ )
95
+
96
+ # Reload agent if any servers were started
97
+ if started_count > 0:
98
+ # Give async tasks a moment to complete before reloading agent
99
+ try:
100
+ import asyncio
101
+
102
+ asyncio.get_running_loop() # Check if in async context
103
+ # If we're in async context, wait a bit for servers to start
104
+ time.sleep(0.5) # Small delay to let async tasks progress
105
+ except RuntimeError:
106
+ pass # No async loop, servers will start when agent uses them
107
+
108
+ try:
109
+ from code_puppy.agents.runtime_manager import (
110
+ get_runtime_agent_manager,
111
+ )
112
+
113
+ manager = get_runtime_agent_manager()
114
+ manager.reload_agent()
115
+ emit_info(
116
+ "[dim]Agent reloaded with updated servers[/dim]",
117
+ message_group=group_id,
118
+ )
119
+ except Exception as e:
120
+ logger.warning(f"Could not reload agent: {e}")
121
+
122
+ except Exception as e:
123
+ logger.error(f"Error starting all servers: {e}")
124
+ emit_info(
125
+ f"[red]Failed to start servers: {e}[/red]", message_group=group_id
126
+ )
@@ -0,0 +1,98 @@
1
+ """
2
+ MCP Start Command - Starts a specific MCP server.
3
+ """
4
+
5
+ import logging
6
+ import time
7
+ from typing import List, Optional
8
+
9
+ from code_puppy.messaging import emit_info
10
+
11
+ from .base import MCPCommandBase
12
+ from .utils import find_server_id_by_name, suggest_similar_servers
13
+
14
+ # Configure logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class StartCommand(MCPCommandBase):
19
+ """
20
+ Command handler for starting MCP servers.
21
+
22
+ Starts a specific MCP server by name and reloads the agent.
23
+ """
24
+
25
+ def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
26
+ """
27
+ Start a specific MCP server.
28
+
29
+ Args:
30
+ args: Command arguments, expects [server_name]
31
+ group_id: Optional message group ID for grouping related messages
32
+ """
33
+ if group_id is None:
34
+ group_id = self.generate_group_id()
35
+
36
+ if not args:
37
+ emit_info(
38
+ "[yellow]Usage: /mcp start <server_name>[/yellow]",
39
+ message_group=group_id,
40
+ )
41
+ return
42
+
43
+ server_name = args[0]
44
+
45
+ try:
46
+ # Find server by name
47
+ server_id = find_server_id_by_name(self.manager, server_name)
48
+ if not server_id:
49
+ emit_info(
50
+ f"[red]Server '{server_name}' not found[/red]",
51
+ message_group=group_id,
52
+ )
53
+ suggest_similar_servers(self.manager, server_name, group_id=group_id)
54
+ return
55
+
56
+ # Start the server (enable and start process)
57
+ success = self.manager.start_server_sync(server_id)
58
+
59
+ if success:
60
+ # This and subsequent messages will auto-group with the first message
61
+ emit_info(
62
+ f"[green]✓ Started server: {server_name}[/green]",
63
+ message_group=group_id,
64
+ )
65
+
66
+ # Give async tasks a moment to complete
67
+ try:
68
+ import asyncio
69
+
70
+ asyncio.get_running_loop() # Check if in async context
71
+ # If we're in async context, wait a bit for server to start
72
+ time.sleep(0.5) # Small delay to let async tasks progress
73
+ except RuntimeError:
74
+ pass # No async loop, server will start when agent uses it
75
+
76
+ # Reload the agent to pick up the newly enabled server
77
+ try:
78
+ from code_puppy.agents.runtime_manager import (
79
+ get_runtime_agent_manager,
80
+ )
81
+
82
+ manager = get_runtime_agent_manager()
83
+ manager.reload_agent()
84
+ emit_info(
85
+ "[dim]Agent reloaded with updated servers[/dim]",
86
+ message_group=group_id,
87
+ )
88
+ except Exception as e:
89
+ logger.warning(f"Could not reload agent: {e}")
90
+ else:
91
+ emit_info(
92
+ f"[red]✗ Failed to start server: {server_name}[/red]",
93
+ message_group=group_id,
94
+ )
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error starting server '{server_name}': {e}")
98
+ emit_info(f"[red]Failed to start server: {e}[/red]", message_group=group_id)