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
@@ -54,13 +54,15 @@ class ReopenableAsyncClient:
54
54
  if self._stream_context:
55
55
  return await self._stream_context.__aexit__(exc_type, exc_val, exc_tb)
56
56
 
57
- def __init__(self, **kwargs):
57
+ def __init__(self, client_class=None, **kwargs):
58
58
  """
59
59
  Initialize the ReopenableAsyncClient.
60
60
 
61
61
  Args:
62
- **kwargs: All arguments that would be passed to httpx.AsyncClient()
62
+ client_class: Class to use for creating the internal client (defaults to httpx.AsyncClient)
63
+ **kwargs: All arguments that would be passed to the client constructor
63
64
  """
65
+ self._client_class = client_class or httpx.AsyncClient
64
66
  self._client_kwargs = kwargs.copy()
65
67
  self._client: Optional[httpx.AsyncClient] = None
66
68
  self._is_closed = True
@@ -70,7 +72,7 @@ class ReopenableAsyncClient:
70
72
  Ensure the underlying client is open and ready to use.
71
73
 
72
74
  Returns:
73
- The active httpx.AsyncClient instance
75
+ The active client instance
74
76
 
75
77
  Raises:
76
78
  RuntimeError: If client cannot be opened
@@ -80,12 +82,12 @@ class ReopenableAsyncClient:
80
82
  return self._client
81
83
 
82
84
  async def _create_client(self) -> None:
83
- """Create a new httpx.AsyncClient with the stored configuration."""
85
+ """Create a new client with the stored configuration."""
84
86
  if self._client is not None and not self._is_closed:
85
87
  # Close existing client first
86
88
  await self._client.aclose()
87
89
 
88
- self._client = httpx.AsyncClient(**self._client_kwargs)
90
+ self._client = self._client_class(**self._client_kwargs)
89
91
  self._is_closed = False
90
92
 
91
93
  async def reopen(self) -> None:
@@ -171,14 +173,12 @@ class ReopenableAsyncClient:
171
173
  """
172
174
  if self._client is None or self._is_closed:
173
175
  # Create a temporary client just for building the request
174
- temp_client = httpx.AsyncClient(**self._client_kwargs)
176
+ temp_client = self._client_class(**self._client_kwargs)
175
177
  try:
176
178
  request = temp_client.build_request(method, url, **kwargs)
177
179
  return request
178
180
  finally:
179
181
  # Clean up the temporary client synchronously if possible
180
- # Note: This might leave a connection open, but it's better than
181
- # making this method async just for building requests
182
182
  pass
183
183
  return self._client.build_request(method, url, **kwargs)
184
184
 
@@ -0,0 +1,291 @@
1
+ """Terminal utilities for cross-platform terminal state management.
2
+
3
+ Handles Windows console mode resets and Unix terminal sanity restoration.
4
+ """
5
+
6
+ import platform
7
+ import subprocess
8
+ import sys
9
+ from typing import Callable, Optional
10
+
11
+ # Store the original console ctrl handler so we can restore it if needed
12
+ _original_ctrl_handler: Optional[Callable] = None
13
+
14
+
15
+ def reset_windows_terminal_ansi() -> None:
16
+ """Reset ANSI formatting on Windows stdout/stderr.
17
+
18
+ This is a lightweight reset that just clears ANSI escape sequences.
19
+ Use this for quick resets after output operations.
20
+ """
21
+ if platform.system() != "Windows":
22
+ return
23
+
24
+ try:
25
+ sys.stdout.write("\x1b[0m") # Reset ANSI formatting
26
+ sys.stdout.flush()
27
+ sys.stderr.write("\x1b[0m")
28
+ sys.stderr.flush()
29
+ except Exception:
30
+ pass # Silently ignore errors - best effort reset
31
+
32
+
33
+ def reset_windows_console_mode() -> None:
34
+ """Full Windows console mode reset using ctypes.
35
+
36
+ This resets both stdout and stdin console modes to restore proper
37
+ terminal behavior after interrupts (Ctrl+C, Ctrl+D). Without this,
38
+ the terminal can become unresponsive (can't type characters).
39
+ """
40
+ if platform.system() != "Windows":
41
+ return
42
+
43
+ try:
44
+ import ctypes
45
+
46
+ kernel32 = ctypes.windll.kernel32
47
+
48
+ # Reset stdout
49
+ STD_OUTPUT_HANDLE = -11
50
+ handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
51
+
52
+ # Enable virtual terminal processing and line input
53
+ mode = ctypes.c_ulong()
54
+ kernel32.GetConsoleMode(handle, ctypes.byref(mode))
55
+
56
+ # Console mode flags for stdout
57
+ ENABLE_PROCESSED_OUTPUT = 0x0001
58
+ ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
59
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
60
+
61
+ new_mode = (
62
+ mode.value
63
+ | ENABLE_PROCESSED_OUTPUT
64
+ | ENABLE_WRAP_AT_EOL_OUTPUT
65
+ | ENABLE_VIRTUAL_TERMINAL_PROCESSING
66
+ )
67
+ kernel32.SetConsoleMode(handle, new_mode)
68
+
69
+ # Reset stdin
70
+ STD_INPUT_HANDLE = -10
71
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
72
+
73
+ # Console mode flags for stdin
74
+ ENABLE_LINE_INPUT = 0x0002
75
+ ENABLE_ECHO_INPUT = 0x0004
76
+ ENABLE_PROCESSED_INPUT = 0x0001
77
+
78
+ stdin_mode = ctypes.c_ulong()
79
+ kernel32.GetConsoleMode(stdin_handle, ctypes.byref(stdin_mode))
80
+
81
+ new_stdin_mode = (
82
+ stdin_mode.value
83
+ | ENABLE_LINE_INPUT
84
+ | ENABLE_ECHO_INPUT
85
+ | ENABLE_PROCESSED_INPUT
86
+ )
87
+ kernel32.SetConsoleMode(stdin_handle, new_stdin_mode)
88
+
89
+ except Exception:
90
+ pass # Silently ignore errors - best effort reset
91
+
92
+
93
+ def flush_windows_keyboard_buffer() -> None:
94
+ """Flush the Windows keyboard buffer.
95
+
96
+ Clears any pending keyboard input that could interfere with
97
+ subsequent input operations after an interrupt.
98
+ """
99
+ if platform.system() != "Windows":
100
+ return
101
+
102
+ try:
103
+ import msvcrt
104
+
105
+ while msvcrt.kbhit():
106
+ msvcrt.getch()
107
+ except Exception:
108
+ pass # Silently ignore errors - best effort flush
109
+
110
+
111
+ def reset_windows_terminal_full() -> None:
112
+ """Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer).
113
+
114
+ Combines ANSI reset, console mode reset, and keyboard buffer flush
115
+ for complete terminal state restoration after interrupts.
116
+ """
117
+ if platform.system() != "Windows":
118
+ return
119
+
120
+ reset_windows_terminal_ansi()
121
+ reset_windows_console_mode()
122
+ flush_windows_keyboard_buffer()
123
+
124
+
125
+ def reset_unix_terminal() -> None:
126
+ """Reset Unix/Linux/macOS terminal to sane state.
127
+
128
+ Uses the `reset` command to restore terminal sanity.
129
+ Silently fails if the command isn't available.
130
+ """
131
+ if platform.system() == "Windows":
132
+ return
133
+
134
+ try:
135
+ subprocess.run(["reset"], check=True, capture_output=True)
136
+ except (subprocess.CalledProcessError, FileNotFoundError):
137
+ pass # Silently fail if reset command isn't available
138
+
139
+
140
+ def reset_terminal() -> None:
141
+ """Cross-platform terminal reset.
142
+
143
+ Automatically detects the platform and performs the appropriate
144
+ terminal reset operation.
145
+ """
146
+ if platform.system() == "Windows":
147
+ reset_windows_terminal_full()
148
+ else:
149
+ reset_unix_terminal()
150
+
151
+
152
+ def disable_windows_ctrl_c() -> bool:
153
+ """Disable Ctrl+C processing at the Windows console input level.
154
+
155
+ This removes ENABLE_PROCESSED_INPUT from stdin, which prevents
156
+ Ctrl+C from being interpreted as a signal at all. Instead, it
157
+ becomes just a regular character (^C) that gets ignored.
158
+
159
+ This is more reliable than SetConsoleCtrlHandler because it
160
+ prevents Ctrl+C from being processed before it reaches any handler.
161
+
162
+ Returns:
163
+ True if successfully disabled, False otherwise.
164
+ """
165
+ global _original_ctrl_handler
166
+
167
+ if platform.system() != "Windows":
168
+ return False
169
+
170
+ try:
171
+ import ctypes
172
+
173
+ kernel32 = ctypes.windll.kernel32
174
+
175
+ # Get stdin handle
176
+ STD_INPUT_HANDLE = -10
177
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
178
+
179
+ # Get current console mode
180
+ mode = ctypes.c_ulong()
181
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
182
+ return False
183
+
184
+ # Save original mode for potential restoration
185
+ _original_ctrl_handler = mode.value
186
+
187
+ # Console mode flags
188
+ ENABLE_PROCESSED_INPUT = 0x0001 # This makes Ctrl+C generate signals
189
+
190
+ # Remove ENABLE_PROCESSED_INPUT to disable Ctrl+C signal generation
191
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
192
+
193
+ if kernel32.SetConsoleMode(stdin_handle, new_mode):
194
+ return True
195
+ return False
196
+
197
+ except Exception:
198
+ return False
199
+
200
+
201
+ def enable_windows_ctrl_c() -> bool:
202
+ """Re-enable Ctrl+C at the Windows console level.
203
+
204
+ Restores the original console mode saved by disable_windows_ctrl_c().
205
+
206
+ Returns:
207
+ True if successfully re-enabled, False otherwise.
208
+ """
209
+ global _original_ctrl_handler
210
+
211
+ if platform.system() != "Windows":
212
+ return False
213
+
214
+ if _original_ctrl_handler is None:
215
+ return True # Nothing to restore
216
+
217
+ try:
218
+ import ctypes
219
+
220
+ kernel32 = ctypes.windll.kernel32
221
+
222
+ # Get stdin handle
223
+ STD_INPUT_HANDLE = -10
224
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
225
+
226
+ # Restore original mode
227
+ if kernel32.SetConsoleMode(stdin_handle, _original_ctrl_handler):
228
+ _original_ctrl_handler = None
229
+ return True
230
+ return False
231
+
232
+ except Exception:
233
+ return False
234
+
235
+
236
+ # Flag to track if we should keep Ctrl+C disabled
237
+ _keep_ctrl_c_disabled: bool = False
238
+
239
+
240
+ def set_keep_ctrl_c_disabled(value: bool) -> None:
241
+ """Set whether Ctrl+C should be kept disabled.
242
+
243
+ When True, ensure_ctrl_c_disabled() will re-disable Ctrl+C
244
+ even if something else (like prompt_toolkit) re-enables it.
245
+ """
246
+ global _keep_ctrl_c_disabled
247
+ _keep_ctrl_c_disabled = value
248
+
249
+
250
+ def ensure_ctrl_c_disabled() -> bool:
251
+ """Ensure Ctrl+C is disabled if it should be.
252
+
253
+ Call this after operations that might restore console mode
254
+ (like prompt_toolkit input).
255
+
256
+ Returns:
257
+ True if Ctrl+C is now disabled (or wasn't needed), False on error.
258
+ """
259
+ if not _keep_ctrl_c_disabled:
260
+ return True
261
+
262
+ if platform.system() != "Windows":
263
+ return True
264
+
265
+ try:
266
+ import ctypes
267
+
268
+ kernel32 = ctypes.windll.kernel32
269
+
270
+ # Get stdin handle
271
+ STD_INPUT_HANDLE = -10
272
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
273
+
274
+ # Get current console mode
275
+ mode = ctypes.c_ulong()
276
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
277
+ return False
278
+
279
+ # Console mode flags
280
+ ENABLE_PROCESSED_INPUT = 0x0001
281
+
282
+ # Check if Ctrl+C processing is enabled
283
+ if mode.value & ENABLE_PROCESSED_INPUT:
284
+ # Disable it
285
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
286
+ return bool(kernel32.SetConsoleMode(stdin_handle, new_mode))
287
+
288
+ return True # Already disabled
289
+
290
+ except Exception:
291
+ return False
@@ -27,8 +27,9 @@ from code_puppy.messaging import (
27
27
  SubAgentResponseMessage,
28
28
  emit_error,
29
29
  emit_info,
30
- emit_system_message,
31
30
  get_message_bus,
31
+ get_session_context,
32
+ set_session_context,
32
33
  )
33
34
  from code_puppy.model_factory import ModelFactory, make_model_settings
34
35
  from code_puppy.tools.common import generate_group_id
@@ -207,6 +208,7 @@ class AgentInfo(BaseModel):
207
208
 
208
209
  name: str
209
210
  display_name: str
211
+ description: str
210
212
 
211
213
 
212
214
  class ListAgentsOutput(BaseModel):
@@ -242,29 +244,44 @@ def register_list_agents(agent):
242
244
  # Generate a group ID for this tool execution
243
245
  group_id = generate_group_id("list_agents")
244
246
 
247
+ from rich.text import Text
248
+
249
+ from code_puppy.config import get_banner_color
250
+
251
+ list_agents_color = get_banner_color("list_agents")
245
252
  emit_info(
246
- "\n[bold white on blue] LIST AGENTS [/bold white on blue]",
253
+ Text.from_markup(
254
+ f"\n[bold white on {list_agents_color}] LIST AGENTS [/bold white on {list_agents_color}]"
255
+ ),
247
256
  message_group=group_id,
248
257
  )
249
258
 
250
259
  try:
251
- from code_puppy.agents import get_available_agents
260
+ from code_puppy.agents import get_agent_descriptions, get_available_agents
252
261
 
253
- # Get available agents from the agent manager
262
+ # Get available agents and their descriptions from the agent manager
254
263
  agents_dict = get_available_agents()
264
+ descriptions_dict = get_agent_descriptions()
255
265
 
256
266
  # Convert to list of AgentInfo objects
257
267
  agents = [
258
- AgentInfo(name=name, display_name=display_name)
268
+ AgentInfo(
269
+ name=name,
270
+ display_name=display_name,
271
+ description=descriptions_dict.get(name, "No description available"),
272
+ )
259
273
  for name, display_name in agents_dict.items()
260
274
  ]
261
275
 
262
- # Display the agents in the console
276
+ # Accumulate output into a single string and emit once
277
+ # Use Text.from_markup() to pass a Rich object that won't be escaped
278
+ lines = []
263
279
  for agent_item in agents:
264
- emit_system_message(
265
- f"- [bold]{agent_item.name}[/bold]: {agent_item.display_name}",
266
- message_group=group_id,
280
+ lines.append(
281
+ f"- [bold]{agent_item.name}[/bold]: {agent_item.display_name}\n"
282
+ f" [dim]{agent_item.description}[/dim]"
267
283
  )
284
+ emit_info(Text.from_markup("\n".join(lines)), message_group=group_id)
268
285
 
269
286
  return ListAgentsOutput(agents=agents)
270
287
 
@@ -407,6 +424,10 @@ def register_invoke_agent(agent):
407
424
  )
408
425
  )
409
426
 
427
+ # Save current session context and set the new one for this sub-agent
428
+ previous_session_id = get_session_context()
429
+ set_session_context(session_id)
430
+
410
431
  try:
411
432
  # Load the specified agent config
412
433
  agent_config = load_agent(agent_name)
@@ -543,4 +564,8 @@ def register_invoke_agent(agent):
543
564
  error=error_msg,
544
565
  )
545
566
 
567
+ finally:
568
+ # Restore the previous session context
569
+ set_session_context(previous_session_id)
570
+
546
571
  return invoke_agent