code-puppy 0.0.302__py3-none-any.whl → 0.0.335__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 (87) hide show
  1. code_puppy/agents/base_agent.py +343 -35
  2. code_puppy/chatgpt_codex_client.py +283 -0
  3. code_puppy/cli_runner.py +898 -0
  4. code_puppy/command_line/add_model_menu.py +23 -1
  5. code_puppy/command_line/autosave_menu.py +271 -35
  6. code_puppy/command_line/colors_menu.py +520 -0
  7. code_puppy/command_line/command_handler.py +8 -2
  8. code_puppy/command_line/config_commands.py +82 -10
  9. code_puppy/command_line/core_commands.py +70 -7
  10. code_puppy/command_line/diff_menu.py +5 -0
  11. code_puppy/command_line/mcp/custom_server_form.py +4 -0
  12. code_puppy/command_line/mcp/edit_command.py +3 -1
  13. code_puppy/command_line/mcp/handler.py +7 -2
  14. code_puppy/command_line/mcp/install_command.py +8 -3
  15. code_puppy/command_line/mcp/install_menu.py +5 -1
  16. code_puppy/command_line/mcp/logs_command.py +173 -64
  17. code_puppy/command_line/mcp/restart_command.py +7 -2
  18. code_puppy/command_line/mcp/search_command.py +10 -4
  19. code_puppy/command_line/mcp/start_all_command.py +16 -6
  20. code_puppy/command_line/mcp/start_command.py +3 -1
  21. code_puppy/command_line/mcp/status_command.py +2 -1
  22. code_puppy/command_line/mcp/stop_all_command.py +5 -1
  23. code_puppy/command_line/mcp/stop_command.py +3 -1
  24. code_puppy/command_line/mcp/wizard_utils.py +10 -4
  25. code_puppy/command_line/model_settings_menu.py +58 -7
  26. code_puppy/command_line/motd.py +13 -7
  27. code_puppy/command_line/onboarding_slides.py +180 -0
  28. code_puppy/command_line/onboarding_wizard.py +340 -0
  29. code_puppy/command_line/prompt_toolkit_completion.py +16 -2
  30. code_puppy/command_line/session_commands.py +11 -4
  31. code_puppy/config.py +106 -17
  32. code_puppy/http_utils.py +155 -196
  33. code_puppy/keymap.py +8 -0
  34. code_puppy/main.py +5 -828
  35. code_puppy/mcp_/__init__.py +17 -0
  36. code_puppy/mcp_/blocking_startup.py +61 -32
  37. code_puppy/mcp_/config_wizard.py +5 -1
  38. code_puppy/mcp_/managed_server.py +23 -3
  39. code_puppy/mcp_/manager.py +65 -0
  40. code_puppy/mcp_/mcp_logs.py +224 -0
  41. code_puppy/messaging/__init__.py +20 -4
  42. code_puppy/messaging/bus.py +64 -0
  43. code_puppy/messaging/markdown_patches.py +57 -0
  44. code_puppy/messaging/messages.py +16 -0
  45. code_puppy/messaging/renderers.py +21 -9
  46. code_puppy/messaging/rich_renderer.py +113 -67
  47. code_puppy/messaging/spinner/console_spinner.py +34 -0
  48. code_puppy/model_factory.py +271 -45
  49. code_puppy/model_utils.py +57 -48
  50. code_puppy/models.json +21 -7
  51. code_puppy/plugins/__init__.py +12 -0
  52. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  53. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  54. code_puppy/plugins/antigravity_oauth/antigravity_model.py +612 -0
  55. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  56. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  57. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  58. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  59. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  60. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  61. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  62. code_puppy/plugins/antigravity_oauth/transport.py +595 -0
  63. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  64. code_puppy/plugins/chatgpt_oauth/config.py +5 -1
  65. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
  66. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +5 -3
  67. code_puppy/plugins/chatgpt_oauth/test_plugin.py +26 -11
  68. code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
  69. code_puppy/plugins/claude_code_oauth/register_callbacks.py +30 -0
  70. code_puppy/plugins/claude_code_oauth/utils.py +1 -0
  71. code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -118
  72. code_puppy/plugins/shell_safety/register_callbacks.py +44 -3
  73. code_puppy/prompts/codex_system_prompt.md +310 -0
  74. code_puppy/pydantic_patches.py +131 -0
  75. code_puppy/reopenable_async_client.py +8 -8
  76. code_puppy/terminal_utils.py +291 -0
  77. code_puppy/tools/agent_tools.py +34 -9
  78. code_puppy/tools/command_runner.py +344 -27
  79. code_puppy/tools/file_operations.py +33 -45
  80. code_puppy/uvx_detection.py +242 -0
  81. {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models.json +21 -7
  82. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/METADATA +30 -1
  83. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/RECORD +87 -64
  84. {code_puppy-0.0.302.data → code_puppy-0.0.335.data}/data/code_puppy/models_dev_api.json +0 -0
  85. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/WHEEL +0 -0
  86. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/entry_points.txt +0 -0
  87. {code_puppy-0.0.302.dist-info → code_puppy-0.0.335.dist-info}/licenses/LICENSE +0 -0
@@ -1,14 +1,21 @@
1
1
  """
2
- MCP Logs Command - Shows recent events/logs for a server.
2
+ MCP Logs Command - Shows server logs from persistent log files.
3
3
  """
4
4
 
5
5
  import logging
6
- from datetime import datetime
7
6
  from typing import List, Optional
8
7
 
9
- from rich.table import Table
8
+ from rich.panel import Panel
9
+ from rich.syntax import Syntax
10
10
  from rich.text import Text
11
11
 
12
+ from code_puppy.mcp_.mcp_logs import (
13
+ clear_logs,
14
+ get_log_file_path,
15
+ get_log_stats,
16
+ list_servers_with_logs,
17
+ read_logs,
18
+ )
12
19
  from code_puppy.messaging import emit_error, emit_info
13
20
 
14
21
  from .base import MCPCommandBase
@@ -22,105 +29,207 @@ class LogsCommand(MCPCommandBase):
22
29
  """
23
30
  Command handler for showing MCP server logs.
24
31
 
25
- Shows recent events/logs for a specific MCP server with configurable limit.
32
+ Shows logs from persistent log files stored in ~/.code_puppy/mcp_logs/.
26
33
  """
27
34
 
28
35
  def execute(self, args: List[str], group_id: Optional[str] = None) -> None:
29
36
  """
30
- Show recent events/logs for a server.
37
+ Show logs for a server.
38
+
39
+ Usage:
40
+ /mcp logs - List servers with logs
41
+ /mcp logs <server_name> - Show last 50 lines
42
+ /mcp logs <server_name> 100 - Show last 100 lines
43
+ /mcp logs <server_name> all - Show all logs
44
+ /mcp logs <server_name> --clear - Clear logs for server
31
45
 
32
46
  Args:
33
- args: Command arguments, expects [server_name] and optional [limit]
47
+ args: Command arguments
34
48
  group_id: Optional message group ID for grouping related messages
35
49
  """
36
50
  if group_id is None:
37
51
  group_id = self.generate_group_id()
38
52
 
53
+ # No args - list servers with logs
39
54
  if not args:
40
- emit_info("Usage: /mcp logs <server_name> [limit]", message_group=group_id)
55
+ self._list_servers_with_logs(group_id)
41
56
  return
42
57
 
43
58
  server_name = args[0]
44
- limit = 10 # Default limit
59
+
60
+ # Check for --clear flag
61
+ if len(args) > 1 and args[1] == "--clear":
62
+ self._clear_logs(server_name, group_id)
63
+ return
64
+
65
+ # Determine number of lines
66
+ lines = 50 # Default
67
+ show_all = False
45
68
 
46
69
  if len(args) > 1:
47
- try:
48
- limit = int(args[1])
49
- if limit <= 0 or limit > 100:
70
+ if args[1].lower() == "all":
71
+ show_all = True
72
+ else:
73
+ try:
74
+ lines = int(args[1])
75
+ if lines <= 0:
76
+ emit_info(
77
+ "Lines must be positive, using default: 50",
78
+ message_group=group_id,
79
+ )
80
+ lines = 50
81
+ except ValueError:
50
82
  emit_info(
51
- "Limit must be between 1 and 100, using default: 10",
83
+ f"Invalid number '{args[1]}', using default: 50",
52
84
  message_group=group_id,
53
85
  )
54
- limit = 10
55
- except ValueError:
56
- emit_info(
57
- f"Invalid limit '{args[1]}', using default: 10",
58
- message_group=group_id,
59
- )
60
86
 
87
+ self._show_logs(server_name, lines if not show_all else None, group_id)
88
+
89
+ def _list_servers_with_logs(self, group_id: str) -> None:
90
+ """List all servers that have log files."""
91
+ servers = list_servers_with_logs()
92
+
93
+ if not servers:
94
+ emit_info(
95
+ "📋 No MCP server logs found.\n"
96
+ "Logs are created when servers are started.",
97
+ message_group=group_id,
98
+ )
99
+ return
100
+
101
+ lines = ["📋 **Servers with logs:**\n"]
102
+
103
+ for server in servers:
104
+ stats = get_log_stats(server)
105
+ size_kb = stats["total_size_bytes"] / 1024
106
+ size_str = (
107
+ f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb / 1024:.1f} MB"
108
+ )
109
+ rotated = (
110
+ f" (+{stats['rotated_count']} rotated)"
111
+ if stats["rotated_count"]
112
+ else ""
113
+ )
114
+ lines.append(
115
+ f" • **{server}** - {stats['line_count']} lines, {size_str}{rotated}"
116
+ )
117
+
118
+ lines.append("\n**Usage:** `/mcp logs <server_name> [lines|all]`")
119
+
120
+ emit_info("\n".join(lines), message_group=group_id)
121
+
122
+ def _show_logs(self, server_name: str, lines: Optional[int], group_id: str) -> None:
123
+ """
124
+ Show logs for a specific server.
125
+
126
+ Args:
127
+ server_name: Name of the server
128
+ lines: Number of lines to show, or None for all
129
+ group_id: Message group ID
130
+ """
61
131
  try:
62
- # Find server by name
132
+ # Verify server exists in manager
63
133
  server_id = find_server_id_by_name(self.manager, server_name)
64
134
  if not server_id:
65
- emit_info(f"Server '{server_name}' not found", message_group=group_id)
66
- suggest_similar_servers(self.manager, server_name, group_id=group_id)
67
- return
135
+ # Server not configured, but might have logs from before
136
+ stats = get_log_stats(server_name)
137
+ if not stats["exists"]:
138
+ emit_info(
139
+ f"Server '{server_name}' not found and has no logs.",
140
+ message_group=group_id,
141
+ )
142
+ suggest_similar_servers(
143
+ self.manager, server_name, group_id=group_id
144
+ )
145
+ return
68
146
 
69
- # Get server status which includes recent events
70
- status = self.manager.get_server_status(server_id)
147
+ # Read logs
148
+ log_lines = read_logs(server_name, lines=lines)
71
149
 
72
- if not status.get("exists", True):
150
+ if not log_lines:
73
151
  emit_info(
74
- f"Server '{server_name}' status not available",
152
+ f"📋 No logs found for server: **{server_name}**\n"
153
+ f"Log file: `{get_log_file_path(server_name)}`",
75
154
  message_group=group_id,
76
155
  )
77
156
  return
78
157
 
79
- recent_events = status.get("recent_events", [])
80
-
81
- if not recent_events:
82
- emit_info(
83
- f"No recent events for server: {server_name}",
84
- message_group=group_id,
158
+ # Get stats for header
159
+ stats = get_log_stats(server_name)
160
+ total_lines = stats["line_count"]
161
+ showing = len(log_lines)
162
+
163
+ # Format header
164
+ if lines is None:
165
+ header = f"📋 Logs for {server_name} (all {total_lines} lines)"
166
+ else:
167
+ header = (
168
+ f"📋 Logs for {server_name} (last {showing} of {total_lines} lines)"
85
169
  )
86
- return
87
170
 
88
- # Show events in a table
89
- table = Table(title=f"📋 Recent Events for {server_name} (last {limit})")
90
- table.add_column("Time", style="dim", no_wrap=True)
91
- table.add_column("Event", style="cyan")
92
- table.add_column("Details", style="dim")
171
+ # Format log content with syntax highlighting
172
+ log_content = "\n".join(log_lines)
173
+
174
+ # Create a panel with the logs
175
+ syntax = Syntax(
176
+ log_content,
177
+ "log",
178
+ theme="monokai",
179
+ word_wrap=True,
180
+ line_numbers=False,
181
+ )
93
182
 
94
- # Take only the requested number of events
95
- events_to_show = (
96
- recent_events[-limit:] if len(recent_events) > limit else recent_events
183
+ panel = Panel(
184
+ syntax,
185
+ title=header,
186
+ subtitle=f"Log file: {get_log_file_path(server_name)}",
187
+ border_style="dim",
97
188
  )
98
189
 
99
- for event in reversed(events_to_show): # Show newest first
100
- timestamp = datetime.fromisoformat(event["timestamp"])
101
- time_str = timestamp.strftime("%H:%M:%S")
102
- event_type = event["event_type"]
103
-
104
- # Format details
105
- details = event.get("details", {})
106
- details_str = details.get("message", "")
107
- if not details_str and "error" in details:
108
- details_str = str(details["error"])
109
-
110
- # Color code event types
111
- event_style = "cyan"
112
- if "error" in event_type.lower():
113
- event_style = "red"
114
- elif event_type in ["started", "enabled", "registered"]:
115
- event_style = "green"
116
- elif event_type in ["stopped", "disabled"]:
117
- event_style = "yellow"
118
-
119
- table.add_row(
120
- time_str, Text(event_type, style=event_style), details_str or "-"
190
+ emit_info(panel, message_group=group_id)
191
+
192
+ # Show hint for more options
193
+ if lines is not None and showing < total_lines:
194
+ emit_info(
195
+ Text.from_markup(
196
+ f"[dim]💡 Use `/mcp logs {server_name} all` to see all logs, "
197
+ f"or `/mcp logs {server_name} <number>` for specific count[/dim]"
198
+ ),
199
+ message_group=group_id,
121
200
  )
122
- emit_info(table, message_group=group_id)
123
201
 
124
202
  except Exception as e:
125
203
  logger.error(f"Error getting logs for server '{server_name}': {e}")
126
204
  emit_error(f"Error getting logs: {e}", message_group=group_id)
205
+
206
+ def _clear_logs(self, server_name: str, group_id: str) -> None:
207
+ """
208
+ Clear logs for a specific server.
209
+
210
+ Args:
211
+ server_name: Name of the server
212
+ group_id: Message group ID
213
+ """
214
+ try:
215
+ stats = get_log_stats(server_name)
216
+
217
+ if not stats["exists"] and stats["rotated_count"] == 0:
218
+ emit_info(
219
+ f"No logs to clear for server: {server_name}",
220
+ message_group=group_id,
221
+ )
222
+ return
223
+
224
+ # Clear the logs
225
+ clear_logs(server_name, include_rotated=True)
226
+
227
+ cleared_count = 1 + stats["rotated_count"]
228
+ emit_info(
229
+ f"🗑️ Cleared {cleared_count} log file(s) for **{server_name}**",
230
+ message_group=group_id,
231
+ )
232
+
233
+ except Exception as e:
234
+ logger.error(f"Error clearing logs for server '{server_name}': {e}")
235
+ emit_error(f"Error clearing logs: {e}", message_group=group_id)
@@ -5,6 +5,8 @@ MCP Restart Command - Restarts a specific MCP server.
5
5
  import logging
6
6
  from typing import List, Optional
7
7
 
8
+ from rich.text import Text
9
+
8
10
  from code_puppy.messaging import emit_info
9
11
 
10
12
  from .base import MCPCommandBase
@@ -72,7 +74,9 @@ class RestartCommand(MCPCommandBase):
72
74
  # Update MCP tool cache immediately so token counts reflect the change
73
75
  agent.update_mcp_tool_cache_sync()
74
76
  emit_info(
75
- "[dim]Agent reloaded with updated servers[/dim]",
77
+ Text.from_markup(
78
+ "[dim]Agent reloaded with updated servers[/dim]"
79
+ ),
76
80
  message_group=group_id,
77
81
  )
78
82
  except Exception as e:
@@ -91,5 +95,6 @@ class RestartCommand(MCPCommandBase):
91
95
  except Exception as e:
92
96
  logger.error(f"Error restarting server '{server_name}': {e}")
93
97
  emit_info(
94
- f"[red]Failed to restart server: {e}[/red]", message_group=group_id
98
+ Text.from_markup(f"[red]Failed to restart server: {e}[/red]"),
99
+ message_group=group_id,
95
100
  )
@@ -6,6 +6,7 @@ import logging
6
6
  from typing import List, Optional
7
7
 
8
8
  from rich.table import Table
9
+ from rich.text import Text
9
10
 
10
11
  from code_puppy.messaging import emit_info, emit_system_message, emit_warning
11
12
 
@@ -99,19 +100,24 @@ class SearchCommand(MCPCommandBase):
99
100
  emit_system_message(table, message_group=group_id)
100
101
  emit_info("\n✓ = Verified ⭐ = Popular", message_group=group_id)
101
102
  emit_info(
102
- "[yellow]To install:[/yellow] /mcp install <id>", message_group=group_id
103
+ Text.from_markup("[yellow]To install:[/yellow] /mcp install <id>"),
104
+ message_group=group_id,
103
105
  )
104
106
  emit_info(
105
- "[yellow]For details:[/yellow] /mcp search <specific-term>",
107
+ Text.from_markup(
108
+ "[yellow]For details:[/yellow] /mcp search <specific-term>"
109
+ ),
106
110
  message_group=group_id,
107
111
  )
108
112
 
109
113
  except ImportError:
110
114
  emit_info(
111
- "[red]Server registry not available[/red]", message_group=group_id
115
+ Text.from_markup("[red]Server registry not available[/red]"),
116
+ message_group=group_id,
112
117
  )
113
118
  except Exception as e:
114
119
  logger.error(f"Error searching server registry: {e}")
115
120
  emit_info(
116
- f"[red]Error searching servers: {e}[/red]", message_group=group_id
121
+ Text.from_markup(f"[red]Error searching servers: {e}[/red]"),
122
+ message_group=group_id,
117
123
  )
@@ -6,6 +6,8 @@ import logging
6
6
  import time
7
7
  from typing import List, Optional
8
8
 
9
+ from rich.text import Text
10
+
9
11
  from code_puppy.mcp_.managed_server import ServerState
10
12
  from code_puppy.messaging import emit_info
11
13
 
@@ -67,20 +69,23 @@ class StartAllCommand(MCPCommandBase):
67
69
  if success:
68
70
  started_count += 1
69
71
  emit_info(
70
- f" [green]✓ Started: {server_name}[/green]",
72
+ Text.from_markup(f" [green]✓ Started: {server_name}[/green]"),
71
73
  message_group=group_id,
72
74
  )
73
75
  else:
74
76
  failed_count += 1
75
77
  emit_info(
76
- f" [red]✗ Failed: {server_name}[/red]", message_group=group_id
78
+ Text.from_markup(f" [red]✗ Failed: {server_name}[/red]"),
79
+ message_group=group_id,
77
80
  )
78
81
 
79
82
  # Summary
80
83
  emit_info("", message_group=group_id)
81
84
  if started_count > 0:
82
85
  emit_info(
83
- f"[green]Started {started_count} server(s)[/green]",
86
+ Text.from_markup(
87
+ f"[green]Started {started_count} server(s)[/green]"
88
+ ),
84
89
  message_group=group_id,
85
90
  )
86
91
  if already_running > 0:
@@ -90,7 +95,9 @@ class StartAllCommand(MCPCommandBase):
90
95
  )
91
96
  if failed_count > 0:
92
97
  emit_info(
93
- f"[yellow]Failed to start {failed_count} server(s)[/yellow]",
98
+ Text.from_markup(
99
+ f"[yellow]Failed to start {failed_count} server(s)[/yellow]"
100
+ ),
94
101
  message_group=group_id,
95
102
  )
96
103
 
@@ -112,7 +119,9 @@ class StartAllCommand(MCPCommandBase):
112
119
  # Update MCP tool cache immediately so token counts reflect the change
113
120
  agent.update_mcp_tool_cache_sync()
114
121
  emit_info(
115
- "[dim]Agent reloaded with updated servers[/dim]",
122
+ Text.from_markup(
123
+ "[dim]Agent reloaded with updated servers[/dim]"
124
+ ),
116
125
  message_group=group_id,
117
126
  )
118
127
  except Exception as e:
@@ -121,5 +130,6 @@ class StartAllCommand(MCPCommandBase):
121
130
  except Exception as e:
122
131
  logger.error(f"Error starting all servers: {e}")
123
132
  emit_info(
124
- f"[red]Failed to start servers: {e}[/red]", message_group=group_id
133
+ Text.from_markup(f"[red]Failed to start servers: {e}[/red]"),
134
+ message_group=group_id,
125
135
  )
@@ -6,6 +6,8 @@ import logging
6
6
  import time
7
7
  from typing import List, Optional
8
8
 
9
+ from rich.text import Text
10
+
9
11
  from code_puppy.messaging import emit_error, emit_info, emit_success
10
12
 
11
13
  from ...agents import get_current_agent
@@ -36,7 +38,7 @@ class StartCommand(MCPCommandBase):
36
38
 
37
39
  if not args:
38
40
  emit_info(
39
- "[yellow]Usage: /mcp start <server_name>[/yellow]",
41
+ Text.from_markup("[yellow]Usage: /mcp start <server_name>[/yellow]"),
40
42
  message_group=group_id,
41
43
  )
42
44
  return
@@ -7,6 +7,7 @@ from datetime import datetime
7
7
  from typing import List, Optional
8
8
 
9
9
  from rich.panel import Panel
10
+ from rich.text import Text
10
11
 
11
12
  from code_puppy.mcp_.managed_server import ServerState
12
13
  from code_puppy.messaging import emit_error, emit_info
@@ -158,7 +159,7 @@ class StatusCommand(MCPCommandBase):
158
159
  status_lines.append(f"[bold]Metadata:[/bold] {len(metadata)} keys")
159
160
 
160
161
  # Create and show the panel
161
- panel_content = "\n".join(status_lines)
162
+ panel_content = Text.from_markup("\n".join(status_lines))
162
163
  panel = Panel(
163
164
  panel_content, title=f"🔌 {server_name} Status", border_style="cyan"
164
165
  )
@@ -6,6 +6,8 @@ import logging
6
6
  import time
7
7
  from typing import List, Optional
8
8
 
9
+ from rich.text import Text
10
+
9
11
  from code_puppy.mcp_.managed_server import ServerState
10
12
  from code_puppy.messaging import emit_info
11
13
 
@@ -97,7 +99,9 @@ class StopAllCommand(MCPCommandBase):
97
99
  # Update MCP tool cache immediately so token counts reflect the change
98
100
  agent.update_mcp_tool_cache_sync()
99
101
  emit_info(
100
- "[dim]Agent reloaded with updated servers[/dim]",
102
+ Text.from_markup(
103
+ "[dim]Agent reloaded with updated servers[/dim]"
104
+ ),
101
105
  message_group=group_id,
102
106
  )
103
107
  except Exception as e:
@@ -5,6 +5,8 @@ MCP Stop Command - Stops a specific MCP server.
5
5
  import logging
6
6
  from typing import List, Optional
7
7
 
8
+ from rich.text import Text
9
+
8
10
  from code_puppy.messaging import emit_error, emit_info
9
11
 
10
12
  from ...agents import get_current_agent
@@ -35,7 +37,7 @@ class StopCommand(MCPCommandBase):
35
37
 
36
38
  if not args:
37
39
  emit_info(
38
- "[yellow]Usage: /mcp stop <server_name>[/yellow]",
40
+ Text.from_markup("[yellow]Usage: /mcp stop <server_name>[/yellow]"),
39
41
  message_group=group_id,
40
42
  )
41
43
  return
@@ -7,6 +7,8 @@ Provides interactive functionality for installing and configuring MCP servers.
7
7
  import logging
8
8
  from typing import Any, Dict, Optional
9
9
 
10
+ from rich.text import Text
11
+
10
12
  from code_puppy.messaging import emit_error, emit_info, emit_prompt
11
13
 
12
14
  # Configure logging
@@ -51,7 +53,7 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
51
53
  required_env_vars = selected_server.get_environment_vars()
52
54
  if required_env_vars:
53
55
  emit_info(
54
- "\n[yellow]Required Environment Variables:[/yellow]",
56
+ Text.from_markup("\n[yellow]Required Environment Variables:[/yellow]"),
55
57
  message_group=group_id,
56
58
  )
57
59
  for var in required_env_vars:
@@ -61,7 +63,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
61
63
  current_value = os.environ.get(var, "")
62
64
  if current_value:
63
65
  emit_info(
64
- f" {var}: [green]Already set[/green]", message_group=group_id
66
+ Text.from_markup(f" {var}: [green]Already set[/green]"),
67
+ message_group=group_id,
65
68
  )
66
69
  env_vars[var] = current_value
67
70
  else:
@@ -73,7 +76,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
73
76
  required_cmd_args = selected_server.get_command_line_args()
74
77
  if required_cmd_args:
75
78
  emit_info(
76
- "\n[yellow]Command Line Arguments:[/yellow]", message_group=group_id
79
+ Text.from_markup("\n[yellow]Command Line Arguments:[/yellow]"),
80
+ message_group=group_id,
77
81
  )
78
82
  for arg_config in required_cmd_args:
79
83
  name = arg_config.get("name", "")
@@ -312,7 +316,9 @@ def install_server_from_catalog(
312
316
  json.dump(data, f, indent=2)
313
317
 
314
318
  emit_info(
315
- f"[green]✓ Successfully installed server: {server_name}[/green]",
319
+ Text.from_markup(
320
+ f"[green]✓ Successfully installed server: {server_name}[/green]"
321
+ ),
316
322
  message_group=group_id,
317
323
  )
318
324
  emit_info(
@@ -58,7 +58,7 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
58
58
  "name": "Reasoning Effort",
59
59
  "description": "Controls how much effort GPT-5 models spend on reasoning. Higher = more thorough but slower.",
60
60
  "type": "choice",
61
- "choices": ["low", "medium", "high"],
61
+ "choices": ["minimal", "low", "medium", "high", "xhigh"],
62
62
  "default": "medium",
63
63
  },
64
64
  "verbosity": {
@@ -72,7 +72,7 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
72
72
  "name": "Extended Thinking",
73
73
  "description": "Enable Claude's extended thinking mode for complex reasoning tasks.",
74
74
  "type": "boolean",
75
- "default": False,
75
+ "default": True,
76
76
  },
77
77
  "budget_tokens": {
78
78
  "name": "Thinking Budget (tokens)",
@@ -84,6 +84,12 @@ SETTING_DEFINITIONS: Dict[str, Dict] = {
84
84
  "default": 10000,
85
85
  "format": "{:.0f}",
86
86
  },
87
+ "interleaved_thinking": {
88
+ "name": "Interleaved Thinking",
89
+ "description": "Enable thinking between tool calls (Claude 4 only: Opus 4.5, Opus 4.1, Opus 4, Sonnet 4). Adds beta header. WARNING: On Vertex/Bedrock, this FAILS for non-Claude 4 models!",
90
+ "type": "boolean",
91
+ "default": False,
92
+ },
87
93
  }
88
94
 
89
95
 
@@ -93,6 +99,42 @@ def _load_all_model_names() -> List[str]:
93
99
  return list(models_config.keys())
94
100
 
95
101
 
102
+ def _get_setting_choices(
103
+ setting_key: str, model_name: Optional[str] = None
104
+ ) -> List[str]:
105
+ """Get the available choices for a setting, filtered by model capabilities.
106
+
107
+ For reasoning_effort, only codex models support 'xhigh' - regular GPT-5.2
108
+ models are capped at 'high'.
109
+
110
+ Args:
111
+ setting_key: The setting name (e.g., 'reasoning_effort', 'verbosity')
112
+ model_name: Optional model name to filter choices for
113
+
114
+ Returns:
115
+ List of valid choices for this setting and model combination.
116
+ """
117
+ setting_def = SETTING_DEFINITIONS.get(setting_key, {})
118
+ if setting_def.get("type") != "choice":
119
+ return []
120
+
121
+ base_choices = setting_def.get("choices", [])
122
+
123
+ # For reasoning_effort, filter 'xhigh' based on model support
124
+ if setting_key == "reasoning_effort" and model_name:
125
+ models_config = ModelFactory.load_config()
126
+ model_config = models_config.get(model_name, {})
127
+
128
+ # Check if model supports xhigh reasoning
129
+ supports_xhigh = model_config.get("supports_xhigh_reasoning", False)
130
+
131
+ if not supports_xhigh:
132
+ # Remove xhigh from choices for non-codex models
133
+ return [c for c in base_choices if c != "xhigh"]
134
+
135
+ return base_choices
136
+
137
+
96
138
  class ModelSettingsMenu:
97
139
  """Interactive TUI for model settings configuration.
98
140
 
@@ -427,7 +469,8 @@ class ModelSettingsMenu:
427
469
  if setting_def.get("type") == "choice":
428
470
  lines.append(("bold", " Options:"))
429
471
  lines.append(("", "\n"))
430
- choices = setting_def.get("choices", [])
472
+ # Get filtered choices based on model capabilities
473
+ choices = _get_setting_choices(setting_key, self.selected_model)
431
474
  lines.append(
432
475
  (
433
476
  "fg:ansibrightblack",
@@ -514,8 +557,11 @@ class ModelSettingsMenu:
514
557
  if current is not None:
515
558
  self.edit_value = current
516
559
  elif setting_def.get("type") == "choice":
517
- # For choice settings, start with the default
518
- self.edit_value = setting_def.get("default", setting_def["choices"][0])
560
+ # For choice settings, start with the default (using filtered choices)
561
+ choices = _get_setting_choices(setting_key, self.selected_model)
562
+ self.edit_value = setting_def.get(
563
+ "default", choices[0] if choices else None
564
+ )
519
565
  elif setting_def.get("type") == "boolean":
520
566
  # For boolean settings, start with the default
521
567
  self.edit_value = setting_def.get("default", False)
@@ -541,8 +587,8 @@ class ModelSettingsMenu:
541
587
  setting_def = SETTING_DEFINITIONS[setting_key]
542
588
 
543
589
  if setting_def.get("type") == "choice":
544
- # Cycle through choices
545
- choices = setting_def["choices"]
590
+ # Cycle through filtered choices based on model capabilities
591
+ choices = _get_setting_choices(setting_key, self.selected_model)
546
592
  current_idx = (
547
593
  choices.index(self.edit_value) if self.edit_value in choices else 0
548
594
  )
@@ -789,6 +835,11 @@ class ModelSettingsMenu:
789
835
  sys.stdout.flush()
790
836
  set_awaiting_user_input(False)
791
837
 
838
+ # Clear exit message
839
+ from code_puppy.messaging import emit_info
840
+
841
+ emit_info("✓ Exited model settings")
842
+
792
843
  return self.result_changed
793
844
 
794
845