code-puppy 0.0.129__py3-none-any.whl → 0.0.131__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.
@@ -70,7 +70,7 @@ class ModelNameCompleter(Completer):
70
70
 
71
71
 
72
72
  def update_model_in_input(text: str) -> Optional[str]:
73
- # If input starts with /model or /m and a model name, set model and strip it out
73
+ # If input starts with /model and a model name, set model and strip it out
74
74
  content = text.strip()
75
75
 
76
76
  # Check for /model command
@@ -84,17 +84,6 @@ def update_model_in_input(text: str) -> Optional[str]:
84
84
  if idx != -1:
85
85
  new_text = (text[:idx] + text[idx + len("/model" + model) :]).strip()
86
86
  return new_text
87
- # Also check for legacy /m command for backward compatibility
88
- elif content.startswith("/m"):
89
- rest = content[2:].strip() # Remove '/m'
90
- for model in load_model_names():
91
- if rest == model:
92
- set_active_model(model)
93
- # Remove /m from the input
94
- idx = text.find("/m" + model)
95
- if idx != -1:
96
- new_text = (text[:idx] + text[idx + len("/m" + model) :]).strip()
97
- return new_text
98
87
  return None
99
88
 
100
89
 
@@ -163,7 +163,7 @@ async def get_input_with_combined_completion(
163
163
  completer = merge_completers(
164
164
  [
165
165
  FilePathCompleter(symbol="@"),
166
- ModelNameCompleter(trigger="/m"),
166
+ ModelNameCompleter(trigger="/model"),
167
167
  CDCompleter(trigger="/cd"),
168
168
  SetCompleter(trigger="/set"),
169
169
  LoadContextCompleter(trigger="/load_context"),
@@ -226,7 +226,7 @@ async def get_input_with_combined_completion(
226
226
 
227
227
 
228
228
  if __name__ == "__main__":
229
- print("Type '@' for path-completion or '/m' to pick a model. Ctrl+D to exit.")
229
+ print("Type '@' for path-completion or '/model' to pick a model. Ctrl+D to exit.")
230
230
 
231
231
  async def main():
232
232
  while True:
@@ -7,46 +7,101 @@ from typing import Dict, Optional
7
7
  from urllib.parse import urlparse
8
8
 
9
9
  from code_puppy.mcp import ServerConfig, get_mcp_manager
10
- from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
11
- from rich.prompt import Prompt, Confirm
10
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning, emit_prompt
12
11
  from rich.console import Console
13
12
 
14
13
  console = Console()
15
14
 
16
15
 
16
+ def prompt_ask(prompt_text: str, default: Optional[str] = None, choices: Optional[list] = None) -> Optional[str]:
17
+ """Helper function to replace rich.prompt.Prompt.ask with emit_prompt."""
18
+ try:
19
+ if default:
20
+ full_prompt = f"{prompt_text} [{default}]"
21
+ else:
22
+ full_prompt = prompt_text
23
+
24
+ if choices:
25
+ full_prompt += f" ({'/'.join(choices)})"
26
+
27
+ response = emit_prompt(full_prompt + ": ")
28
+
29
+ # Handle default value
30
+ if not response.strip() and default:
31
+ return default
32
+
33
+ # Handle choices validation
34
+ if choices and response.strip() and response.strip() not in choices:
35
+ emit_error(f"Invalid choice. Must be one of: {', '.join(choices)}")
36
+ return None
37
+
38
+ return response.strip() if response.strip() else None
39
+ except Exception as e:
40
+ emit_error(f"Input error: {e}")
41
+ return None
42
+
43
+
44
+ def confirm_ask(prompt_text: str, default: bool = True) -> bool:
45
+ """Helper function to replace rich.prompt.Confirm.ask with emit_prompt."""
46
+ try:
47
+ default_text = "[Y/n]" if default else "[y/N]"
48
+ response = emit_prompt(f"{prompt_text} {default_text}: ")
49
+
50
+ if not response.strip():
51
+ return default
52
+
53
+ response_lower = response.strip().lower()
54
+ if response_lower in ['y', 'yes', 'true', '1']:
55
+ return True
56
+ elif response_lower in ['n', 'no', 'false', '0']:
57
+ return False
58
+ else:
59
+ return default
60
+ except Exception as e:
61
+ emit_error(f"Input error: {e}")
62
+ return default
63
+
64
+
17
65
  class MCPConfigWizard:
18
66
  """Interactive wizard for configuring MCP servers."""
19
67
 
20
68
  def __init__(self):
21
69
  self.manager = get_mcp_manager()
22
70
 
23
- def run_wizard(self) -> Optional[ServerConfig]:
71
+ def run_wizard(self, group_id: str = None) -> Optional[ServerConfig]:
24
72
  """
25
73
  Run the interactive configuration wizard.
26
74
 
75
+ Args:
76
+ group_id: Optional message group ID for grouping related messages
77
+
27
78
  Returns:
28
79
  ServerConfig if successful, None if cancelled
29
80
  """
30
- console.print("\n[bold cyan]🧙 MCP Server Configuration Wizard[/bold cyan]\n")
81
+ if group_id is None:
82
+ import uuid
83
+ group_id = str(uuid.uuid4())
84
+
85
+ emit_info("🧙 MCP Server Configuration Wizard", message_group=group_id)
31
86
 
32
87
  # Step 1: Server name
33
- name = self.prompt_server_name()
88
+ name = self.prompt_server_name(group_id)
34
89
  if not name:
35
90
  return None
36
91
 
37
92
  # Step 2: Server type
38
- server_type = self.prompt_server_type()
93
+ server_type = self.prompt_server_type(group_id)
39
94
  if not server_type:
40
95
  return None
41
96
 
42
97
  # Step 3: Type-specific configuration
43
98
  config = {}
44
99
  if server_type == "sse":
45
- config = self.prompt_sse_config()
100
+ config = self.prompt_sse_config(group_id)
46
101
  elif server_type == "http":
47
- config = self.prompt_http_config()
102
+ config = self.prompt_http_config(group_id)
48
103
  elif server_type == "stdio":
49
- config = self.prompt_stdio_config()
104
+ config = self.prompt_stdio_config(group_id)
50
105
 
51
106
  if not config:
52
107
  return None
@@ -61,62 +116,55 @@ class MCPConfigWizard:
61
116
  )
62
117
 
63
118
  # Step 5: Show summary and confirm
64
- if self.prompt_confirmation(server_config):
119
+ if self.prompt_confirmation(server_config, group_id):
65
120
  return server_config
66
121
 
67
122
  return None
68
123
 
69
- def prompt_server_name(self) -> Optional[str]:
124
+ def prompt_server_name(self, group_id: str = None) -> Optional[str]:
70
125
  """Prompt for server name with validation."""
71
126
  while True:
72
- name = Prompt.ask(
73
- "[yellow]Enter server name[/yellow]",
74
- default=None
75
- )
127
+ name = prompt_ask("Enter server name", default=None)
76
128
 
77
129
  if not name:
78
- if not Confirm.ask("Cancel configuration?", default=False):
130
+ if not confirm_ask("Cancel configuration?", default=False):
79
131
  continue
80
132
  return None
81
133
 
82
134
  # Validate name
83
135
  if not self.validate_name(name):
84
- emit_error("Name must be alphanumeric with hyphens/underscores only")
136
+ emit_error("Name must be alphanumeric with hyphens/underscores only", message_group=group_id)
85
137
  continue
86
138
 
87
139
  # Check uniqueness
88
140
  existing = self.manager.registry.get_by_name(name)
89
141
  if existing:
90
- emit_error(f"Server '{name}' already exists")
142
+ emit_error(f"Server '{name}' already exists", message_group=group_id)
91
143
  continue
92
144
 
93
145
  return name
94
146
 
95
- def prompt_server_type(self) -> Optional[str]:
147
+ def prompt_server_type(self, group_id: str = None) -> Optional[str]:
96
148
  """Prompt for server type."""
97
- console.print("\n[cyan]Server types:[/cyan]")
98
- console.print(" [bold]sse[/bold] - Server-Sent Events (HTTP streaming)")
99
- console.print(" [bold]http[/bold] - HTTP/REST API")
100
- console.print(" [bold]stdio[/bold] - Local command (subprocess)")
149
+ emit_info("\nServer types:", message_group=group_id)
150
+ emit_info(" sse - Server-Sent Events (HTTP streaming)", message_group=group_id)
151
+ emit_info(" http - HTTP/REST API", message_group=group_id)
152
+ emit_info(" stdio - Local command (subprocess)", message_group=group_id)
101
153
 
102
154
  while True:
103
- server_type = Prompt.ask(
104
- "\n[yellow]Select server type[/yellow]",
105
- choices=["sse", "http", "stdio"],
106
- default="stdio"
107
- )
155
+ server_type = prompt_ask("Select server type", choices=["sse", "http", "stdio"], default="stdio")
108
156
 
109
157
  if server_type in ["sse", "http", "stdio"]:
110
158
  return server_type
111
159
 
112
- emit_error("Invalid type. Choose: sse, http, or stdio")
160
+ emit_error("Invalid type. Choose: sse, http, or stdio", message_group=group_id)
113
161
 
114
- def prompt_sse_config(self) -> Optional[Dict]:
162
+ def prompt_sse_config(self, group_id: str = None) -> Optional[Dict]:
115
163
  """Prompt for SSE server configuration."""
116
- console.print("\n[cyan]Configuring SSE server[/cyan]")
164
+ emit_info("Configuring SSE server", message_group=group_id)
117
165
 
118
166
  # URL
119
- url = self.prompt_url("SSE")
167
+ url = self.prompt_url("SSE", group_id)
120
168
  if not url:
121
169
  return None
122
170
 
@@ -127,16 +175,13 @@ class MCPConfigWizard:
127
175
  }
128
176
 
129
177
  # Headers (optional)
130
- if Confirm.ask("Add custom headers?", default=False):
131
- headers = self.prompt_headers()
178
+ if confirm_ask("Add custom headers?", default=False):
179
+ headers = self.prompt_headers(group_id)
132
180
  if headers:
133
181
  config["headers"] = headers
134
182
 
135
183
  # Timeout
136
- timeout_str = Prompt.ask(
137
- "Connection timeout (seconds)",
138
- default="30"
139
- )
184
+ timeout_str = prompt_ask("Connection timeout (seconds)", default="30")
140
185
  try:
141
186
  config["timeout"] = int(timeout_str)
142
187
  except ValueError:
@@ -144,12 +189,12 @@ class MCPConfigWizard:
144
189
 
145
190
  return config
146
191
 
147
- def prompt_http_config(self) -> Optional[Dict]:
192
+ def prompt_http_config(self, group_id: str = None) -> Optional[Dict]:
148
193
  """Prompt for HTTP server configuration."""
149
- console.print("\n[cyan]Configuring HTTP server[/cyan]")
194
+ emit_info("Configuring HTTP server", message_group=group_id)
150
195
 
151
196
  # URL
152
- url = self.prompt_url("HTTP")
197
+ url = self.prompt_url("HTTP", group_id)
153
198
  if not url:
154
199
  return None
155
200
 
@@ -160,16 +205,13 @@ class MCPConfigWizard:
160
205
  }
161
206
 
162
207
  # Headers (optional)
163
- if Confirm.ask("Add custom headers?", default=False):
164
- headers = self.prompt_headers()
208
+ if confirm_ask("Add custom headers?", default=False):
209
+ headers = self.prompt_headers(group_id)
165
210
  if headers:
166
211
  config["headers"] = headers
167
212
 
168
213
  # Timeout
169
- timeout_str = Prompt.ask(
170
- "Request timeout (seconds)",
171
- default="30"
172
- )
214
+ timeout_str = prompt_ask("Request timeout (seconds)", default="30")
173
215
  try:
174
216
  config["timeout"] = int(timeout_str)
175
217
  except ValueError:
@@ -177,19 +219,16 @@ class MCPConfigWizard:
177
219
 
178
220
  return config
179
221
 
180
- def prompt_stdio_config(self) -> Optional[Dict]:
222
+ def prompt_stdio_config(self, group_id: str = None) -> Optional[Dict]:
181
223
  """Prompt for Stdio server configuration."""
182
- console.print("\n[cyan]Configuring Stdio server[/cyan]")
183
- console.print("[dim]Examples:[/dim]")
184
- console.print("[dim] • npx -y @modelcontextprotocol/server-filesystem /path[/dim]")
185
- console.print("[dim] • python mcp_server.py[/dim]")
186
- console.print("[dim] • node server.js[/dim]")
224
+ emit_info("Configuring Stdio server", message_group=group_id)
225
+ emit_info("Examples:", message_group=group_id)
226
+ emit_info(" • npx -y @modelcontextprotocol/server-filesystem /path", message_group=group_id)
227
+ emit_info(" • python mcp_server.py", message_group=group_id)
228
+ emit_info(" • node server.js", message_group=group_id)
187
229
 
188
230
  # Command
189
- command = Prompt.ask(
190
- "\n[yellow]Enter command[/yellow]",
191
- default=None
192
- )
231
+ command = prompt_ask("Enter command", default=None)
193
232
 
194
233
  if not command:
195
234
  return None
@@ -202,10 +241,7 @@ class MCPConfigWizard:
202
241
  }
203
242
 
204
243
  # Arguments
205
- args_str = Prompt.ask(
206
- "Enter arguments (space-separated)",
207
- default=""
208
- )
244
+ args_str = prompt_ask("Enter arguments (space-separated)", default="")
209
245
  if args_str:
210
246
  # Simple argument parsing (handles quoted strings)
211
247
  import shlex
@@ -215,28 +251,22 @@ class MCPConfigWizard:
215
251
  config["args"] = args_str.split()
216
252
 
217
253
  # Working directory (optional)
218
- cwd = Prompt.ask(
219
- "Working directory (optional)",
220
- default=""
221
- )
254
+ cwd = prompt_ask("Working directory (optional)", default="")
222
255
  if cwd:
223
256
  import os
224
257
  if os.path.isdir(os.path.expanduser(cwd)):
225
258
  config["cwd"] = os.path.expanduser(cwd)
226
259
  else:
227
- emit_warning(f"Directory '{cwd}' not found, ignoring")
260
+ emit_warning(f"Directory '{cwd}' not found, ignoring", message_group=group_id)
228
261
 
229
262
  # Environment variables (optional)
230
- if Confirm.ask("Add environment variables?", default=False):
231
- env = self.prompt_env_vars()
263
+ if confirm_ask("Add environment variables?", default=False):
264
+ env = self.prompt_env_vars(group_id)
232
265
  if env:
233
266
  config["env"] = env
234
267
 
235
268
  # Timeout
236
- timeout_str = Prompt.ask(
237
- "Startup timeout (seconds)",
238
- default="30"
239
- )
269
+ timeout_str = prompt_ask("Startup timeout (seconds)", default="30")
240
270
  try:
241
271
  config["timeout"] = int(timeout_str)
242
272
  except ValueError:
@@ -244,58 +274,55 @@ class MCPConfigWizard:
244
274
 
245
275
  return config
246
276
 
247
- def prompt_url(self, server_type: str) -> Optional[str]:
277
+ def prompt_url(self, server_type: str, group_id: str = None) -> Optional[str]:
248
278
  """Prompt for and validate URL."""
249
279
  while True:
250
- url = Prompt.ask(
251
- f"[yellow]Enter {server_type} server URL[/yellow]",
252
- default=None
253
- )
280
+ url = prompt_ask(f"Enter {server_type} server URL", default=None)
254
281
 
255
282
  if not url:
256
- if Confirm.ask("Cancel configuration?", default=False):
283
+ if confirm_ask("Cancel configuration?", default=False):
257
284
  return None
258
285
  continue
259
286
 
260
287
  if self.validate_url(url):
261
288
  return url
262
289
 
263
- emit_error("Invalid URL. Must be http:// or https://")
290
+ emit_error("Invalid URL. Must be http:// or https://", message_group=group_id)
264
291
 
265
- def prompt_headers(self) -> Dict[str, str]:
292
+ def prompt_headers(self, group_id: str = None) -> Dict[str, str]:
266
293
  """Prompt for HTTP headers."""
267
294
  headers = {}
268
- console.print("[dim]Enter headers (format: Name: Value)[/dim]")
269
- console.print("[dim]Press Enter with empty name to finish[/dim]")
295
+ emit_info("Enter headers (format: Name: Value)", message_group=group_id)
296
+ emit_info("Press Enter with empty name to finish", message_group=group_id)
270
297
 
271
298
  while True:
272
- name = Prompt.ask("Header name", default="")
299
+ name = prompt_ask("Header name", default="")
273
300
  if not name:
274
301
  break
275
302
 
276
- value = Prompt.ask(f"Value for '{name}'", default="")
303
+ value = prompt_ask(f"Value for '{name}'", default="")
277
304
  headers[name] = value
278
305
 
279
- if not Confirm.ask("Add another header?", default=True):
306
+ if not confirm_ask("Add another header?", default=True):
280
307
  break
281
308
 
282
309
  return headers
283
310
 
284
- def prompt_env_vars(self) -> Dict[str, str]:
311
+ def prompt_env_vars(self, group_id: str = None) -> Dict[str, str]:
285
312
  """Prompt for environment variables."""
286
313
  env = {}
287
- console.print("[dim]Enter environment variables[/dim]")
288
- console.print("[dim]Press Enter with empty name to finish[/dim]")
314
+ emit_info("Enter environment variables", message_group=group_id)
315
+ emit_info("Press Enter with empty name to finish", message_group=group_id)
289
316
 
290
317
  while True:
291
- name = Prompt.ask("Variable name", default="")
318
+ name = prompt_ask("Variable name", default="")
292
319
  if not name:
293
320
  break
294
321
 
295
- value = Prompt.ask(f"Value for '{name}'", default="")
322
+ value = prompt_ask(f"Value for '{name}'", default="")
296
323
  env[name] = value
297
324
 
298
- if not Confirm.ask("Add another variable?", default=True):
325
+ if not confirm_ask("Add another variable?", default=True):
299
326
  break
300
327
 
301
328
  return env
@@ -325,7 +352,7 @@ class MCPConfigWizard:
325
352
  # Otherwise check if it's in PATH
326
353
  return shutil.which(command) is not None
327
354
 
328
- def test_connection(self, config: ServerConfig) -> bool:
355
+ def test_connection(self, config: ServerConfig, group_id: str = None) -> bool:
329
356
  """
330
357
  Test connection to the configured server.
331
358
 
@@ -335,7 +362,7 @@ class MCPConfigWizard:
335
362
  Returns:
336
363
  True if connection successful, False otherwise
337
364
  """
338
- emit_info("Testing connection...")
365
+ emit_info("Testing connection...", message_group=group_id)
339
366
 
340
367
  try:
341
368
  # Try to create the server instance
@@ -349,60 +376,67 @@ class MCPConfigWizard:
349
376
  # Try to get the pydantic server (this validates config)
350
377
  server = managed.get_pydantic_server()
351
378
  if server:
352
- emit_success("✓ Configuration valid")
379
+ emit_success("✓ Configuration valid", message_group=group_id)
353
380
  return True
354
381
 
355
- emit_error("✗ Failed to create server instance")
382
+ emit_error("✗ Failed to create server instance", message_group=group_id)
356
383
  return False
357
384
 
358
385
  except Exception as e:
359
- emit_error(f"✗ Configuration error: {e}")
386
+ emit_error(f"✗ Configuration error: {e}", message_group=group_id)
360
387
  return False
361
388
 
362
- def prompt_confirmation(self, config: ServerConfig) -> bool:
389
+ def prompt_confirmation(self, config: ServerConfig, group_id: str = None) -> bool:
363
390
  """Show summary and ask for confirmation."""
364
- console.print("\n[bold cyan]Configuration Summary:[/bold cyan]")
365
- console.print(f" [bold]Name:[/bold] {config.name}")
366
- console.print(f" [bold]Type:[/bold] {config.type}")
391
+ emit_info("Configuration Summary:", message_group=group_id)
392
+ emit_info(f" Name: {config.name}", message_group=group_id)
393
+ emit_info(f" Type: {config.type}", message_group=group_id)
367
394
 
368
395
  if config.type in ["sse", "http"]:
369
- console.print(f" [bold]URL:[/bold] {config.config.get('url')}")
396
+ emit_info(f" URL: {config.config.get('url')}", message_group=group_id)
370
397
  elif config.type == "stdio":
371
- console.print(f" [bold]Command:[/bold] {config.config.get('command')}")
398
+ emit_info(f" Command: {config.config.get('command')}", message_group=group_id)
372
399
  args = config.config.get('args', [])
373
400
  if args:
374
- console.print(f" [bold]Arguments:[/bold] {' '.join(args)}")
401
+ emit_info(f" Arguments: {' '.join(args)}", message_group=group_id)
375
402
 
376
- console.print(f" [bold]Timeout:[/bold] {config.config.get('timeout', 30)}s")
403
+ emit_info(f" Timeout: {config.config.get('timeout', 30)}s", message_group=group_id)
377
404
 
378
405
  # Test connection if requested
379
- if Confirm.ask("\n[yellow]Test connection?[/yellow]", default=True):
380
- if not self.test_connection(config):
381
- if not Confirm.ask("Continue anyway?", default=False):
406
+ if confirm_ask("Test connection?", default=True):
407
+ if not self.test_connection(config, group_id):
408
+ if not confirm_ask("Continue anyway?", default=False):
382
409
  return False
383
410
 
384
- return Confirm.ask("\n[bold green]Save this configuration?[/bold green]", default=True)
411
+ return confirm_ask("Save this configuration?", default=True)
385
412
 
386
413
 
387
- def run_add_wizard() -> bool:
414
+ def run_add_wizard(group_id: str = None) -> bool:
388
415
  """
389
416
  Run the MCP add wizard and register the server.
390
417
 
418
+ Args:
419
+ group_id: Optional message group ID for grouping related messages
420
+
391
421
  Returns:
392
422
  True if server was added, False otherwise
393
423
  """
424
+ if group_id is None:
425
+ import uuid
426
+ group_id = str(uuid.uuid4())
427
+
394
428
  wizard = MCPConfigWizard()
395
- config = wizard.run_wizard()
429
+ config = wizard.run_wizard(group_id)
396
430
 
397
431
  if config:
398
432
  try:
399
433
  manager = get_mcp_manager()
400
434
  server_id = manager.register_server(config)
401
435
 
402
- emit_success(f"\n✅ Server '{config.name}' added successfully!")
403
- emit_info(f"Server ID: {server_id}")
404
- emit_info("Use '/mcp list' to see all servers")
405
- emit_info(f"Use '/mcp start {config.name}' to start the server")
436
+ emit_success(f"\n✅ Server '{config.name}' added successfully!", message_group=group_id)
437
+ emit_info(f"Server ID: {server_id}", message_group=group_id)
438
+ emit_info("Use '/mcp list' to see all servers", message_group=group_id)
439
+ emit_info(f"Use '/mcp start {config.name}' to start the server", message_group=group_id)
406
440
 
407
441
  # Also save to mcp_servers.json for persistence
408
442
  from code_puppy.config import MCP_SERVERS_FILE, load_mcp_server_configs
@@ -426,12 +460,12 @@ def run_add_wizard() -> bool:
426
460
  with open(MCP_SERVERS_FILE, 'w') as f:
427
461
  json.dump(data, f, indent=2)
428
462
 
429
- emit_info(f"[dim]Configuration saved to {MCP_SERVERS_FILE}[/dim]")
463
+ emit_info(f"[dim]Configuration saved to {MCP_SERVERS_FILE}[/dim]", message_group=group_id)
430
464
  return True
431
465
 
432
466
  except Exception as e:
433
- emit_error(f"Failed to add server: {e}")
467
+ emit_error(f"Failed to add server: {e}", message_group=group_id)
434
468
  return False
435
469
  else:
436
- emit_warning("Configuration cancelled")
470
+ emit_warning("Configuration cancelled", message_group=group_id)
437
471
  return False