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
@@ -1,13 +1,13 @@
1
1
  """
2
2
  Server Status Tracker for monitoring MCP server runtime status.
3
3
 
4
- This module provides the ServerStatusTracker class that tracks the runtime
4
+ This module provides the ServerStatusTracker class that tracks the runtime
5
5
  status of MCP servers including state, metrics, and events.
6
6
  """
7
7
 
8
8
  import logging
9
9
  import threading
10
- from collections import deque, defaultdict
10
+ from collections import defaultdict, deque
11
11
  from dataclasses import dataclass
12
12
  from datetime import datetime, timedelta
13
13
  from typing import Any, Dict, List, Optional
@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
21
21
  @dataclass
22
22
  class Event:
23
23
  """Data class representing a server event."""
24
+
24
25
  timestamp: datetime
25
26
  event_type: str # "started", "stopped", "error", "health_check", etc.
26
27
  details: Dict
@@ -30,43 +31,43 @@ class Event:
30
31
  class ServerStatusTracker:
31
32
  """
32
33
  Tracks the runtime status of MCP servers including state, metrics, and events.
33
-
34
+
34
35
  This class provides in-memory storage for server states, metadata, and events
35
36
  with thread-safe operations using locks. Events are stored using collections.deque
36
37
  for automatic size limiting.
37
-
38
+
38
39
  Example usage:
39
40
  tracker = ServerStatusTracker()
40
41
  tracker.set_status("server1", ServerState.RUNNING)
41
42
  tracker.record_event("server1", "started", {"message": "Server started successfully"})
42
43
  events = tracker.get_events("server1", limit=10)
43
44
  """
44
-
45
+
45
46
  def __init__(self):
46
47
  """Initialize the status tracker with thread-safe data structures."""
47
48
  # Thread safety lock
48
49
  self._lock = threading.RLock()
49
-
50
+
50
51
  # Server states (server_id -> ServerState)
51
52
  self._server_states: Dict[str, ServerState] = {}
52
-
53
+
53
54
  # Server metadata (server_id -> key -> value)
54
55
  self._server_metadata: Dict[str, Dict[str, Any]] = defaultdict(dict)
55
-
56
+
56
57
  # Server events (server_id -> deque of events)
57
58
  # Using deque with maxlen for automatic size limiting
58
59
  self._server_events: Dict[str, deque] = defaultdict(lambda: deque(maxlen=1000))
59
-
60
+
60
61
  # Server timing information
61
62
  self._start_times: Dict[str, datetime] = {}
62
63
  self._stop_times: Dict[str, datetime] = {}
63
-
64
+
64
65
  logger.info("ServerStatusTracker initialized")
65
-
66
+
66
67
  def set_status(self, server_id: str, state: ServerState) -> None:
67
68
  """
68
69
  Set the current state of a server.
69
-
70
+
70
71
  Args:
71
72
  server_id: Unique identifier for the server
72
73
  state: New server state
@@ -74,7 +75,7 @@ class ServerStatusTracker:
74
75
  with self._lock:
75
76
  old_state = self._server_states.get(server_id)
76
77
  self._server_states[server_id] = state
77
-
78
+
78
79
  # Record state change event
79
80
  self.record_event(
80
81
  server_id,
@@ -82,29 +83,29 @@ class ServerStatusTracker:
82
83
  {
83
84
  "old_state": old_state.value if old_state else None,
84
85
  "new_state": state.value,
85
- "message": f"State changed from {old_state.value if old_state else 'unknown'} to {state.value}"
86
- }
86
+ "message": f"State changed from {old_state.value if old_state else 'unknown'} to {state.value}",
87
+ },
87
88
  )
88
-
89
+
89
90
  logger.debug(f"Server {server_id} state changed: {old_state} -> {state}")
90
-
91
+
91
92
  def get_status(self, server_id: str) -> ServerState:
92
93
  """
93
94
  Get the current state of a server.
94
-
95
+
95
96
  Args:
96
97
  server_id: Unique identifier for the server
97
-
98
+
98
99
  Returns:
99
100
  Current server state, defaults to STOPPED if not found
100
101
  """
101
102
  with self._lock:
102
103
  return self._server_states.get(server_id, ServerState.STOPPED)
103
-
104
+
104
105
  def set_metadata(self, server_id: str, key: str, value: Any) -> None:
105
106
  """
106
107
  Set metadata value for a server.
107
-
108
+
108
109
  Args:
109
110
  server_id: Unique identifier for the server
110
111
  key: Metadata key
@@ -113,10 +114,10 @@ class ServerStatusTracker:
113
114
  with self._lock:
114
115
  if server_id not in self._server_metadata:
115
116
  self._server_metadata[server_id] = {}
116
-
117
+
117
118
  old_value = self._server_metadata[server_id].get(key)
118
119
  self._server_metadata[server_id][key] = value
119
-
120
+
120
121
  # Record metadata change event
121
122
  self.record_event(
122
123
  server_id,
@@ -125,30 +126,30 @@ class ServerStatusTracker:
125
126
  "key": key,
126
127
  "old_value": old_value,
127
128
  "new_value": value,
128
- "message": f"Metadata '{key}' updated"
129
- }
129
+ "message": f"Metadata '{key}' updated",
130
+ },
130
131
  )
131
-
132
+
132
133
  logger.debug(f"Server {server_id} metadata updated: {key} = {value}")
133
-
134
+
134
135
  def get_metadata(self, server_id: str, key: str) -> Any:
135
136
  """
136
137
  Get metadata value for a server.
137
-
138
+
138
139
  Args:
139
140
  server_id: Unique identifier for the server
140
141
  key: Metadata key
141
-
142
+
142
143
  Returns:
143
144
  Metadata value or None if not found
144
145
  """
145
146
  with self._lock:
146
147
  return self._server_metadata.get(server_id, {}).get(key)
147
-
148
+
148
149
  def record_event(self, server_id: str, event_type: str, details: Dict) -> None:
149
150
  """
150
151
  Record an event for a server.
151
-
152
+
152
153
  Args:
153
154
  server_id: Unique identifier for the server
154
155
  event_type: Type of event (e.g., "started", "stopped", "error", "health_check")
@@ -158,37 +159,39 @@ class ServerStatusTracker:
158
159
  event = Event(
159
160
  timestamp=datetime.now(),
160
161
  event_type=event_type,
161
- details=details.copy() if details else {}, # Copy to prevent modification
162
- server_id=server_id
162
+ details=details.copy()
163
+ if details
164
+ else {}, # Copy to prevent modification
165
+ server_id=server_id,
163
166
  )
164
-
167
+
165
168
  # Add to deque (automatically handles size limiting)
166
169
  self._server_events[server_id].append(event)
167
-
170
+
168
171
  logger.debug(f"Event recorded for server {server_id}: {event_type}")
169
-
172
+
170
173
  def get_events(self, server_id: str, limit: int = 100) -> List[Event]:
171
174
  """
172
175
  Get recent events for a server.
173
-
176
+
174
177
  Args:
175
178
  server_id: Unique identifier for the server
176
179
  limit: Maximum number of events to return (default: 100)
177
-
180
+
178
181
  Returns:
179
182
  List of events ordered by timestamp (most recent first)
180
183
  """
181
184
  with self._lock:
182
185
  events = list(self._server_events.get(server_id, deque()))
183
-
186
+
184
187
  # Return most recent events first, limited by count
185
188
  events.reverse() # Most recent first
186
189
  return events[:limit]
187
-
190
+
188
191
  def clear_events(self, server_id: str) -> None:
189
192
  """
190
193
  Clear all events for a server.
191
-
194
+
192
195
  Args:
193
196
  server_id: Unique identifier for the server
194
197
  """
@@ -196,14 +199,14 @@ class ServerStatusTracker:
196
199
  if server_id in self._server_events:
197
200
  self._server_events[server_id].clear()
198
201
  logger.info(f"Cleared all events for server: {server_id}")
199
-
202
+
200
203
  def get_uptime(self, server_id: str) -> Optional[timedelta]:
201
204
  """
202
205
  Calculate uptime for a server based on start/stop times.
203
-
206
+
204
207
  Args:
205
208
  server_id: Unique identifier for the server
206
-
209
+
207
210
  Returns:
208
211
  Server uptime as timedelta, or None if server never started
209
212
  """
@@ -211,60 +214,57 @@ class ServerStatusTracker:
211
214
  start_time = self._start_times.get(server_id)
212
215
  if start_time is None:
213
216
  return None
214
-
217
+
215
218
  # If server is currently running, calculate from start time to now
216
219
  current_state = self.get_status(server_id)
217
220
  if current_state == ServerState.RUNNING:
218
221
  return datetime.now() - start_time
219
-
222
+
220
223
  # If server is stopped, calculate from start to stop time
221
224
  stop_time = self._stop_times.get(server_id)
222
225
  if stop_time is not None and stop_time > start_time:
223
226
  return stop_time - start_time
224
-
227
+
225
228
  # If we have start time but no valid stop time, assume currently running
226
229
  return datetime.now() - start_time
227
-
230
+
228
231
  def record_start_time(self, server_id: str) -> None:
229
232
  """
230
233
  Record the start time for a server.
231
-
234
+
232
235
  Args:
233
236
  server_id: Unique identifier for the server
234
237
  """
235
238
  with self._lock:
236
239
  start_time = datetime.now()
237
240
  self._start_times[server_id] = start_time
238
-
241
+
239
242
  # Record start event
240
243
  self.record_event(
241
244
  server_id,
242
245
  "started",
243
- {
244
- "start_time": start_time.isoformat(),
245
- "message": "Server started"
246
- }
246
+ {"start_time": start_time.isoformat(), "message": "Server started"},
247
247
  )
248
-
248
+
249
249
  logger.info(f"Recorded start time for server: {server_id}")
250
-
250
+
251
251
  def record_stop_time(self, server_id: str) -> None:
252
252
  """
253
253
  Record the stop time for a server.
254
-
254
+
255
255
  Args:
256
256
  server_id: Unique identifier for the server
257
257
  """
258
258
  with self._lock:
259
259
  stop_time = datetime.now()
260
260
  self._stop_times[server_id] = stop_time
261
-
261
+
262
262
  # Calculate final uptime
263
263
  start_time = self._start_times.get(server_id)
264
264
  uptime = None
265
265
  if start_time:
266
266
  uptime = stop_time - start_time
267
-
267
+
268
268
  # Record stop event
269
269
  self.record_event(
270
270
  server_id,
@@ -272,16 +272,16 @@ class ServerStatusTracker:
272
272
  {
273
273
  "stop_time": stop_time.isoformat(),
274
274
  "uptime_seconds": uptime.total_seconds() if uptime else None,
275
- "message": "Server stopped"
276
- }
275
+ "message": "Server stopped",
276
+ },
277
277
  )
278
-
278
+
279
279
  logger.info(f"Recorded stop time for server: {server_id}")
280
-
280
+
281
281
  def get_all_server_ids(self) -> List[str]:
282
282
  """
283
283
  Get all server IDs that have been tracked.
284
-
284
+
285
285
  Returns:
286
286
  List of all server IDs
287
287
  """
@@ -293,16 +293,16 @@ class ServerStatusTracker:
293
293
  all_ids.update(self._server_events.keys())
294
294
  all_ids.update(self._start_times.keys())
295
295
  all_ids.update(self._stop_times.keys())
296
-
296
+
297
297
  return sorted(list(all_ids))
298
-
298
+
299
299
  def get_server_summary(self, server_id: str) -> Dict[str, Any]:
300
300
  """
301
301
  Get comprehensive summary of server status.
302
-
302
+
303
303
  Args:
304
304
  server_id: Unique identifier for the server
305
-
305
+
306
306
  Returns:
307
307
  Dictionary containing current state, metadata, recent events, and uptime
308
308
  """
@@ -317,23 +317,24 @@ class ServerStatusTracker:
317
317
  "stop_time": self._stop_times.get(server_id),
318
318
  "last_event_time": (
319
319
  list(self._server_events.get(server_id, deque()))[-1].timestamp
320
- if server_id in self._server_events and len(self._server_events[server_id]) > 0
320
+ if server_id in self._server_events
321
+ and len(self._server_events[server_id]) > 0
321
322
  else None
322
- )
323
+ ),
323
324
  }
324
-
325
+
325
326
  def cleanup_old_data(self, days_to_keep: int = 7) -> None:
326
327
  """
327
328
  Clean up old data to prevent memory bloat.
328
-
329
+
329
330
  Args:
330
331
  days_to_keep: Number of days of data to keep (default: 7)
331
332
  """
332
333
  cutoff_time = datetime.now() - timedelta(days=days_to_keep)
333
-
334
+
334
335
  with self._lock:
335
336
  cleaned_servers = []
336
-
337
+
337
338
  for server_id in list(self._server_events.keys()):
338
339
  events = self._server_events[server_id]
339
340
  if events:
@@ -341,15 +342,14 @@ class ServerStatusTracker:
341
342
  original_count = len(events)
342
343
  # Convert to list, filter, then create new deque
343
344
  filtered_events = [
344
- event for event in events
345
- if event.timestamp >= cutoff_time
345
+ event for event in events if event.timestamp >= cutoff_time
346
346
  ]
347
-
347
+
348
348
  # Replace the deque with filtered events
349
349
  self._server_events[server_id] = deque(filtered_events, maxlen=1000)
350
-
350
+
351
351
  if len(filtered_events) < original_count:
352
352
  cleaned_servers.append(server_id)
353
-
353
+
354
354
  if cleaned_servers:
355
- logger.info(f"Cleaned old events for {len(cleaned_servers)} servers")
355
+ logger.info(f"Cleaned old events for {len(cleaned_servers)} servers")
@@ -4,13 +4,14 @@ System tool detection and validation for MCP server requirements.
4
4
 
5
5
  import shutil
6
6
  import subprocess
7
- from typing import Dict, List, Optional, Tuple
8
7
  from dataclasses import dataclass
8
+ from typing import Dict, List, Optional
9
9
 
10
10
 
11
11
  @dataclass
12
12
  class ToolInfo:
13
13
  """Information about a detected system tool."""
14
+
14
15
  name: str
15
16
  available: bool
16
17
  version: Optional[str] = None
@@ -20,7 +21,7 @@ class ToolInfo:
20
21
 
21
22
  class SystemToolDetector:
22
23
  """Detect and validate system tools required by MCP servers."""
23
-
24
+
24
25
  # Tool version commands
25
26
  VERSION_COMMANDS = {
26
27
  "node": ["node", "--version"],
@@ -48,112 +49,105 @@ class SystemToolDetector:
48
49
  "vim": ["vim", "--version"],
49
50
  "emacs": ["emacs", "--version"],
50
51
  }
51
-
52
+
52
53
  @classmethod
53
54
  def detect_tool(cls, tool_name: str) -> ToolInfo:
54
55
  """Detect if a tool is available and get its version."""
55
56
  # First check if tool is in PATH
56
57
  tool_path = shutil.which(tool_name)
57
-
58
+
58
59
  if not tool_path:
59
60
  return ToolInfo(
60
- name=tool_name,
61
- available=False,
62
- error=f"{tool_name} not found in PATH"
61
+ name=tool_name, available=False, error=f"{tool_name} not found in PATH"
63
62
  )
64
-
63
+
65
64
  # Try to get version
66
65
  version_cmd = cls.VERSION_COMMANDS.get(tool_name)
67
66
  version = None
68
67
  error = None
69
-
68
+
70
69
  if version_cmd:
71
70
  try:
72
71
  # Run version command
73
72
  result = subprocess.run(
74
- version_cmd,
75
- capture_output=True,
76
- text=True,
77
- timeout=10
73
+ version_cmd, capture_output=True, text=True, timeout=10
78
74
  )
79
-
75
+
80
76
  if result.returncode == 0:
81
77
  # Parse version from output
82
78
  output = result.stdout.strip() or result.stderr.strip()
83
79
  version = cls._parse_version(tool_name, output)
84
80
  else:
85
81
  error = f"Version check failed: {result.stderr.strip()}"
86
-
82
+
87
83
  except subprocess.TimeoutExpired:
88
84
  error = "Version check timed out"
89
85
  except Exception as e:
90
86
  error = f"Version check error: {str(e)}"
91
-
87
+
92
88
  return ToolInfo(
93
- name=tool_name,
94
- available=True,
95
- version=version,
96
- path=tool_path,
97
- error=error
89
+ name=tool_name, available=True, version=version, path=tool_path, error=error
98
90
  )
99
-
91
+
100
92
  @classmethod
101
93
  def detect_tools(cls, tool_names: List[str]) -> Dict[str, ToolInfo]:
102
94
  """Detect multiple tools."""
103
95
  return {name: cls.detect_tool(name) for name in tool_names}
104
-
96
+
105
97
  @classmethod
106
98
  def _parse_version(cls, tool_name: str, output: str) -> Optional[str]:
107
99
  """Parse version string from command output."""
108
100
  if not output:
109
101
  return None
110
-
102
+
111
103
  # Common version patterns
112
104
  import re
113
-
105
+
114
106
  # Try to find version pattern like "v1.2.3" or "1.2.3"
115
107
  version_patterns = [
116
- r'v?(\d+\.\d+\.\d+(?:\.\d+)?)', # Standard semver
117
- r'(\d+\.\d+\.\d+)', # Simple version
118
- r'version\s+v?(\d+\.\d+\.\d+)', # "version 1.2.3"
119
- r'v?(\d+\.\d+)', # Major.minor only
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
120
112
  ]
121
-
113
+
122
114
  for pattern in version_patterns:
123
115
  match = re.search(pattern, output, re.IGNORECASE)
124
116
  if match:
125
117
  return match.group(1)
126
-
118
+
127
119
  # If no pattern matches, return first line (common for many tools)
128
- first_line = output.split('\n')[0].strip()
120
+ first_line = output.split("\n")[0].strip()
129
121
  if len(first_line) < 100: # Reasonable length for a version string
130
122
  return first_line
131
-
123
+
132
124
  return None
133
-
125
+
134
126
  @classmethod
135
127
  def check_package_dependencies(cls, packages: List[str]) -> Dict[str, bool]:
136
128
  """Check if package dependencies are available."""
137
129
  results = {}
138
-
130
+
139
131
  for package in packages:
140
132
  available = False
141
-
133
+
142
134
  # Try different package managers/methods
143
- if package.startswith('@') or '/' in package:
135
+ if package.startswith("@") or "/" in package:
144
136
  # Likely npm package
145
137
  available = cls._check_npm_package(package)
146
- elif package in ['jupyter', 'pandas', 'numpy', 'matplotlib']:
138
+ elif package in ["jupyter", "pandas", "numpy", "matplotlib"]:
147
139
  # Python packages
148
140
  available = cls._check_python_package(package)
149
141
  else:
150
142
  # Try both npm and python
151
- available = cls._check_npm_package(package) or cls._check_python_package(package)
152
-
143
+ available = cls._check_npm_package(
144
+ package
145
+ ) or cls._check_python_package(package)
146
+
153
147
  results[package] = available
154
-
148
+
155
149
  return results
156
-
150
+
157
151
  @classmethod
158
152
  def _check_npm_package(cls, package: str) -> bool:
159
153
  """Check if an npm package is available."""
@@ -162,53 +156,54 @@ class SystemToolDetector:
162
156
  ["npm", "list", "-g", package],
163
157
  capture_output=True,
164
158
  text=True,
165
- timeout=10
159
+ timeout=10,
166
160
  )
167
161
  return result.returncode == 0
168
- except:
162
+ except Exception:
169
163
  return False
170
-
164
+
171
165
  @classmethod
172
166
  def _check_python_package(cls, package: str) -> bool:
173
167
  """Check if a Python package is available."""
174
168
  try:
175
169
  import importlib
170
+
176
171
  importlib.import_module(package)
177
172
  return True
178
173
  except ImportError:
179
174
  return False
180
-
175
+
181
176
  @classmethod
182
177
  def get_installation_suggestions(cls, tool_name: str) -> List[str]:
183
178
  """Get installation suggestions for a missing tool."""
184
179
  suggestions = {
185
180
  "node": [
186
181
  "Install Node.js from https://nodejs.org",
187
- "Or use package manager: brew install node (macOS) / sudo apt install nodejs (Ubuntu)"
182
+ "Or use package manager: brew install node (macOS) / sudo apt install nodejs (Ubuntu)",
188
183
  ],
189
184
  "npm": ["Usually comes with Node.js - install Node.js first"],
190
185
  "npx": ["Usually comes with npm 5.2+ - update npm: npm install -g npm"],
191
186
  "python": [
192
187
  "Install Python from https://python.org",
193
- "Or use package manager: brew install python (macOS) / sudo apt install python3 (Ubuntu)"
188
+ "Or use package manager: brew install python (macOS) / sudo apt install python3 (Ubuntu)",
194
189
  ],
195
190
  "python3": ["Same as python - install Python 3.x"],
196
191
  "pip": ["Usually comes with Python - try: python -m ensurepip"],
197
192
  "pip3": ["Usually comes with Python 3 - try: python3 -m ensurepip"],
198
193
  "git": [
199
194
  "Install Git from https://git-scm.com",
200
- "Or use package manager: brew install git (macOS) / sudo apt install git (Ubuntu)"
195
+ "Or use package manager: brew install git (macOS) / sudo apt install git (Ubuntu)",
201
196
  ],
202
197
  "docker": ["Install Docker from https://docker.com"],
203
198
  "java": [
204
199
  "Install OpenJDK from https://openjdk.java.net",
205
- "Or use package manager: brew install openjdk (macOS) / sudo apt install default-jdk (Ubuntu)"
200
+ "Or use package manager: brew install openjdk (macOS) / sudo apt install default-jdk (Ubuntu)",
206
201
  ],
207
202
  "jupyter": ["Install with pip: pip install jupyter"],
208
203
  }
209
-
204
+
210
205
  return suggestions.get(tool_name, [f"Please install {tool_name} manually"])
211
206
 
212
207
 
213
208
  # Global detector instance
214
- detector = SystemToolDetector()
209
+ detector = SystemToolDetector()