code-puppy 0.0.135__py3-none-any.whl → 0.0.137__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 (61) 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/config.py +5 -5
  28. code_puppy/main.py +23 -17
  29. code_puppy/mcp/__init__.py +42 -16
  30. code_puppy/mcp/async_lifecycle.py +51 -49
  31. code_puppy/mcp/blocking_startup.py +125 -113
  32. code_puppy/mcp/captured_stdio_server.py +63 -70
  33. code_puppy/mcp/circuit_breaker.py +63 -47
  34. code_puppy/mcp/config_wizard.py +169 -136
  35. code_puppy/mcp/dashboard.py +79 -71
  36. code_puppy/mcp/error_isolation.py +147 -100
  37. code_puppy/mcp/examples/retry_example.py +55 -42
  38. code_puppy/mcp/health_monitor.py +152 -141
  39. code_puppy/mcp/managed_server.py +100 -93
  40. code_puppy/mcp/manager.py +168 -156
  41. code_puppy/mcp/registry.py +148 -110
  42. code_puppy/mcp/retry_manager.py +63 -61
  43. code_puppy/mcp/server_registry_catalog.py +271 -225
  44. code_puppy/mcp/status_tracker.py +80 -80
  45. code_puppy/mcp/system_tools.py +47 -52
  46. code_puppy/messaging/message_queue.py +20 -13
  47. code_puppy/messaging/renderers.py +30 -15
  48. code_puppy/state_management.py +103 -0
  49. code_puppy/tui/app.py +64 -7
  50. code_puppy/tui/components/chat_view.py +3 -3
  51. code_puppy/tui/components/human_input_modal.py +12 -8
  52. code_puppy/tui/screens/__init__.py +2 -2
  53. code_puppy/tui/screens/mcp_install_wizard.py +208 -179
  54. code_puppy/tui/tests/test_agent_command.py +3 -3
  55. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/METADATA +1 -1
  56. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/RECORD +60 -42
  57. code_puppy/command_line/mcp_commands.py +0 -1789
  58. {code_puppy-0.0.135.data → code_puppy-0.0.137.data}/data/code_puppy/models.json +0 -0
  59. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/WHEEL +0 -0
  60. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/entry_points.txt +0 -0
  61. {code_puppy-0.0.135.dist-info → code_puppy-0.0.137.dist-info}/licenses/LICENSE +0 -0
@@ -7,10 +7,10 @@ within the same task, allowing servers to start and stay running.
7
7
 
8
8
  import asyncio
9
9
  import logging
10
- from typing import Dict, Optional, Any, Union
11
- from datetime import datetime
12
- from dataclasses import dataclass
13
10
  from contextlib import AsyncExitStack
11
+ from dataclasses import dataclass
12
+ from datetime import datetime
13
+ from typing import Any, Dict, Optional, Union
14
14
 
15
15
  from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP
16
16
 
@@ -20,7 +20,7 @@ logger = logging.getLogger(__name__)
20
20
  @dataclass
21
21
  class ManagedServerContext:
22
22
  """Represents a managed MCP server with its async context."""
23
-
23
+
24
24
  server_id: str
25
25
  server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]
26
26
  exit_stack: AsyncExitStack
@@ -31,32 +31,32 @@ class ManagedServerContext:
31
31
  class AsyncServerLifecycleManager:
32
32
  """
33
33
  Manages MCP server lifecycles asynchronously.
34
-
34
+
35
35
  This properly maintains async contexts within the same task,
36
36
  allowing servers to start and stay running independently of agents.
37
37
  """
38
-
38
+
39
39
  def __init__(self):
40
40
  """Initialize the async lifecycle manager."""
41
41
  self._servers: Dict[str, ManagedServerContext] = {}
42
42
  self._lock = asyncio.Lock()
43
43
  logger.info("AsyncServerLifecycleManager initialized")
44
-
44
+
45
45
  async def start_server(
46
46
  self,
47
47
  server_id: str,
48
- server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]
48
+ server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP],
49
49
  ) -> bool:
50
50
  """
51
51
  Start an MCP server and maintain its context.
52
-
52
+
53
53
  This creates a dedicated task that enters the server's context
54
54
  and keeps it alive until explicitly stopped.
55
-
55
+
56
56
  Args:
57
57
  server_id: Unique identifier for the server
58
58
  server: The pydantic-ai MCP server instance
59
-
59
+
60
60
  Returns:
61
61
  True if server started successfully, False otherwise
62
62
  """
@@ -68,18 +68,20 @@ class AsyncServerLifecycleManager:
68
68
  return True
69
69
  else:
70
70
  # Server exists but not running, clean it up
71
- logger.warning(f"Server {server_id} exists but not running, cleaning up")
71
+ logger.warning(
72
+ f"Server {server_id} exists but not running, cleaning up"
73
+ )
72
74
  await self._stop_server_internal(server_id)
73
-
75
+
74
76
  # Create a task that will manage this server's lifecycle
75
77
  task = asyncio.create_task(
76
78
  self._server_lifecycle_task(server_id, server),
77
- name=f"mcp_server_{server_id}"
79
+ name=f"mcp_server_{server_id}",
78
80
  )
79
-
81
+
80
82
  # Wait briefly for the server to start
81
83
  await asyncio.sleep(0.1)
82
-
84
+
83
85
  # Check if task failed immediately
84
86
  if task.done():
85
87
  try:
@@ -87,29 +89,29 @@ class AsyncServerLifecycleManager:
87
89
  except Exception as e:
88
90
  logger.error(f"Failed to start server {server_id}: {e}")
89
91
  return False
90
-
92
+
91
93
  logger.info(f"Server {server_id} starting in background task")
92
94
  return True
93
-
95
+
94
96
  async def _server_lifecycle_task(
95
97
  self,
96
98
  server_id: str,
97
- server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP]
99
+ server: Union[MCPServerSSE, MCPServerStdio, MCPServerStreamableHTTP],
98
100
  ) -> None:
99
101
  """
100
102
  Task that manages a server's lifecycle.
101
-
103
+
102
104
  This task enters the server's context and keeps it alive
103
105
  until the server is stopped or an error occurs.
104
106
  """
105
107
  exit_stack = AsyncExitStack()
106
-
108
+
107
109
  try:
108
110
  logger.info(f"Starting server lifecycle for {server_id}")
109
-
111
+
110
112
  # Enter the server's context
111
113
  await exit_stack.enter_async_context(server)
112
-
114
+
113
115
  # Store the managed context
114
116
  async with self._lock:
115
117
  self._servers[server_id] = ManagedServerContext(
@@ -117,20 +119,20 @@ class AsyncServerLifecycleManager:
117
119
  server=server,
118
120
  exit_stack=exit_stack,
119
121
  start_time=datetime.now(),
120
- task=asyncio.current_task()
122
+ task=asyncio.current_task(),
121
123
  )
122
-
124
+
123
125
  logger.info(f"Server {server_id} started successfully")
124
-
126
+
125
127
  # Keep the task alive until cancelled
126
128
  while True:
127
129
  await asyncio.sleep(1)
128
-
130
+
129
131
  # Check if server is still running
130
132
  if not server.is_running:
131
133
  logger.warning(f"Server {server_id} stopped unexpectedly")
132
134
  break
133
-
135
+
134
136
  except asyncio.CancelledError:
135
137
  logger.info(f"Server {server_id} lifecycle task cancelled")
136
138
  raise
@@ -139,29 +141,29 @@ class AsyncServerLifecycleManager:
139
141
  finally:
140
142
  # Clean up the context
141
143
  await exit_stack.aclose()
142
-
144
+
143
145
  # Remove from managed servers
144
146
  async with self._lock:
145
147
  if server_id in self._servers:
146
148
  del self._servers[server_id]
147
-
149
+
148
150
  logger.info(f"Server {server_id} lifecycle ended")
149
-
151
+
150
152
  async def stop_server(self, server_id: str) -> bool:
151
153
  """
152
154
  Stop a running MCP server.
153
-
155
+
154
156
  This cancels the lifecycle task, which properly exits the context.
155
-
157
+
156
158
  Args:
157
159
  server_id: ID of the server to stop
158
-
160
+
159
161
  Returns:
160
162
  True if server was stopped, False if not found
161
163
  """
162
164
  async with self._lock:
163
165
  return await self._stop_server_internal(server_id)
164
-
166
+
165
167
  async def _stop_server_internal(self, server_id: str) -> bool:
166
168
  """
167
169
  Internal method to stop a server (must be called with lock held).
@@ -169,38 +171,38 @@ class AsyncServerLifecycleManager:
169
171
  if server_id not in self._servers:
170
172
  logger.warning(f"Server {server_id} not found")
171
173
  return False
172
-
174
+
173
175
  context = self._servers[server_id]
174
-
176
+
175
177
  # Cancel the lifecycle task
176
178
  # This will cause the task to exit and clean up properly
177
179
  context.task.cancel()
178
-
180
+
179
181
  try:
180
182
  await context.task
181
183
  except asyncio.CancelledError:
182
184
  pass # Expected
183
-
185
+
184
186
  logger.info(f"Stopped server {server_id}")
185
187
  return True
186
-
188
+
187
189
  def is_running(self, server_id: str) -> bool:
188
190
  """
189
191
  Check if a server is running.
190
-
192
+
191
193
  Args:
192
194
  server_id: ID of the server
193
-
195
+
194
196
  Returns:
195
197
  True if server is running, False otherwise
196
198
  """
197
199
  context = self._servers.get(server_id)
198
200
  return context.server.is_running if context else False
199
-
201
+
200
202
  def list_servers(self) -> Dict[str, Dict[str, Any]]:
201
203
  """
202
204
  List all running servers.
203
-
205
+
204
206
  Returns:
205
207
  Dictionary of server IDs to server info
206
208
  """
@@ -211,17 +213,17 @@ class AsyncServerLifecycleManager:
211
213
  "type": context.server.__class__.__name__,
212
214
  "is_running": context.server.is_running,
213
215
  "uptime_seconds": uptime,
214
- "start_time": context.start_time.isoformat()
216
+ "start_time": context.start_time.isoformat(),
215
217
  }
216
218
  return servers
217
-
219
+
218
220
  async def stop_all(self) -> None:
219
221
  """Stop all running servers."""
220
222
  server_ids = list(self._servers.keys())
221
-
223
+
222
224
  for server_id in server_ids:
223
225
  await self.stop_server(server_id)
224
-
226
+
225
227
  logger.info("All MCP servers stopped")
226
228
 
227
229
 
@@ -234,4 +236,4 @@ def get_lifecycle_manager() -> AsyncServerLifecycleManager:
234
236
  global _lifecycle_manager
235
237
  if _lifecycle_manager is None:
236
238
  _lifecycle_manager = AsyncServerLifecycleManager()
237
- return _lifecycle_manager
239
+ return _lifecycle_manager