code-puppy 0.0.325__py3-none-any.whl → 0.0.341__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 (52) hide show
  1. code_puppy/agents/base_agent.py +110 -124
  2. code_puppy/claude_cache_client.py +208 -2
  3. code_puppy/cli_runner.py +152 -32
  4. code_puppy/command_line/add_model_menu.py +4 -0
  5. code_puppy/command_line/autosave_menu.py +23 -24
  6. code_puppy/command_line/clipboard.py +527 -0
  7. code_puppy/command_line/colors_menu.py +5 -0
  8. code_puppy/command_line/config_commands.py +24 -1
  9. code_puppy/command_line/core_commands.py +85 -0
  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/install_menu.py +5 -1
  13. code_puppy/command_line/model_settings_menu.py +5 -0
  14. code_puppy/command_line/motd.py +13 -7
  15. code_puppy/command_line/onboarding_slides.py +180 -0
  16. code_puppy/command_line/onboarding_wizard.py +340 -0
  17. code_puppy/command_line/prompt_toolkit_completion.py +118 -0
  18. code_puppy/config.py +3 -2
  19. code_puppy/http_utils.py +201 -279
  20. code_puppy/keymap.py +10 -8
  21. code_puppy/mcp_/managed_server.py +7 -11
  22. code_puppy/messaging/messages.py +3 -0
  23. code_puppy/messaging/rich_renderer.py +114 -22
  24. code_puppy/model_factory.py +102 -15
  25. code_puppy/models.json +2 -2
  26. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  27. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  28. code_puppy/plugins/antigravity_oauth/antigravity_model.py +668 -0
  29. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  30. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  31. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  32. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  33. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  34. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  35. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  36. code_puppy/plugins/antigravity_oauth/transport.py +664 -0
  37. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  38. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
  39. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
  40. code_puppy/plugins/claude_code_oauth/utils.py +126 -7
  41. code_puppy/reopenable_async_client.py +8 -8
  42. code_puppy/terminal_utils.py +295 -3
  43. code_puppy/tools/command_runner.py +43 -54
  44. code_puppy/tools/common.py +3 -9
  45. code_puppy/uvx_detection.py +242 -0
  46. {code_puppy-0.0.325.data → code_puppy-0.0.341.data}/data/code_puppy/models.json +2 -2
  47. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/METADATA +26 -49
  48. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/RECORD +52 -36
  49. {code_puppy-0.0.325.data → code_puppy-0.0.341.data}/data/code_puppy/models_dev_api.json +0 -0
  50. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/WHEEL +0 -0
  51. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/entry_points.txt +0 -0
  52. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/licenses/LICENSE +0 -0
@@ -3,9 +3,17 @@
3
3
  Handles Windows console mode resets and Unix terminal sanity restoration.
4
4
  """
5
5
 
6
+ import os
6
7
  import platform
7
8
  import subprocess
8
9
  import sys
10
+ from typing import TYPE_CHECKING, Callable, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from rich.console import Console
14
+
15
+ # Store the original console ctrl handler so we can restore it if needed
16
+ _original_ctrl_handler: Optional[Callable] = None
9
17
 
10
18
 
11
19
  def reset_windows_terminal_ansi() -> None:
@@ -86,17 +94,36 @@ def reset_windows_console_mode() -> None:
86
94
  pass # Silently ignore errors - best effort reset
87
95
 
88
96
 
97
+ def flush_windows_keyboard_buffer() -> None:
98
+ """Flush the Windows keyboard buffer.
99
+
100
+ Clears any pending keyboard input that could interfere with
101
+ subsequent input operations after an interrupt.
102
+ """
103
+ if platform.system() != "Windows":
104
+ return
105
+
106
+ try:
107
+ import msvcrt
108
+
109
+ while msvcrt.kbhit():
110
+ msvcrt.getch()
111
+ except Exception:
112
+ pass # Silently ignore errors - best effort flush
113
+
114
+
89
115
  def reset_windows_terminal_full() -> None:
90
- """Perform a full Windows terminal reset (ANSI + console mode).
116
+ """Perform a full Windows terminal reset (ANSI + console mode + keyboard buffer).
91
117
 
92
- Combines both ANSI reset and console mode reset for complete
93
- terminal state restoration after interrupts.
118
+ Combines ANSI reset, console mode reset, and keyboard buffer flush
119
+ for complete terminal state restoration after interrupts.
94
120
  """
95
121
  if platform.system() != "Windows":
96
122
  return
97
123
 
98
124
  reset_windows_terminal_ansi()
99
125
  reset_windows_console_mode()
126
+ flush_windows_keyboard_buffer()
100
127
 
101
128
 
102
129
  def reset_unix_terminal() -> None:
@@ -124,3 +151,268 @@ def reset_terminal() -> None:
124
151
  reset_windows_terminal_full()
125
152
  else:
126
153
  reset_unix_terminal()
154
+
155
+
156
+ def disable_windows_ctrl_c() -> bool:
157
+ """Disable Ctrl+C processing at the Windows console input level.
158
+
159
+ This removes ENABLE_PROCESSED_INPUT from stdin, which prevents
160
+ Ctrl+C from being interpreted as a signal at all. Instead, it
161
+ becomes just a regular character (^C) that gets ignored.
162
+
163
+ This is more reliable than SetConsoleCtrlHandler because it
164
+ prevents Ctrl+C from being processed before it reaches any handler.
165
+
166
+ Returns:
167
+ True if successfully disabled, False otherwise.
168
+ """
169
+ global _original_ctrl_handler
170
+
171
+ if platform.system() != "Windows":
172
+ return False
173
+
174
+ try:
175
+ import ctypes
176
+
177
+ kernel32 = ctypes.windll.kernel32
178
+
179
+ # Get stdin handle
180
+ STD_INPUT_HANDLE = -10
181
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
182
+
183
+ # Get current console mode
184
+ mode = ctypes.c_ulong()
185
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
186
+ return False
187
+
188
+ # Save original mode for potential restoration
189
+ _original_ctrl_handler = mode.value
190
+
191
+ # Console mode flags
192
+ ENABLE_PROCESSED_INPUT = 0x0001 # This makes Ctrl+C generate signals
193
+
194
+ # Remove ENABLE_PROCESSED_INPUT to disable Ctrl+C signal generation
195
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
196
+
197
+ if kernel32.SetConsoleMode(stdin_handle, new_mode):
198
+ return True
199
+ return False
200
+
201
+ except Exception:
202
+ return False
203
+
204
+
205
+ def enable_windows_ctrl_c() -> bool:
206
+ """Re-enable Ctrl+C at the Windows console level.
207
+
208
+ Restores the original console mode saved by disable_windows_ctrl_c().
209
+
210
+ Returns:
211
+ True if successfully re-enabled, False otherwise.
212
+ """
213
+ global _original_ctrl_handler
214
+
215
+ if platform.system() != "Windows":
216
+ return False
217
+
218
+ if _original_ctrl_handler is None:
219
+ return True # Nothing to restore
220
+
221
+ try:
222
+ import ctypes
223
+
224
+ kernel32 = ctypes.windll.kernel32
225
+
226
+ # Get stdin handle
227
+ STD_INPUT_HANDLE = -10
228
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
229
+
230
+ # Restore original mode
231
+ if kernel32.SetConsoleMode(stdin_handle, _original_ctrl_handler):
232
+ _original_ctrl_handler = None
233
+ return True
234
+ return False
235
+
236
+ except Exception:
237
+ return False
238
+
239
+
240
+ # Flag to track if we should keep Ctrl+C disabled
241
+ _keep_ctrl_c_disabled: bool = False
242
+
243
+
244
+ def set_keep_ctrl_c_disabled(value: bool) -> None:
245
+ """Set whether Ctrl+C should be kept disabled.
246
+
247
+ When True, ensure_ctrl_c_disabled() will re-disable Ctrl+C
248
+ even if something else (like prompt_toolkit) re-enables it.
249
+ """
250
+ global _keep_ctrl_c_disabled
251
+ _keep_ctrl_c_disabled = value
252
+
253
+
254
+ def ensure_ctrl_c_disabled() -> bool:
255
+ """Ensure Ctrl+C is disabled if it should be.
256
+
257
+ Call this after operations that might restore console mode
258
+ (like prompt_toolkit input).
259
+
260
+ Returns:
261
+ True if Ctrl+C is now disabled (or wasn't needed), False on error.
262
+ """
263
+ if not _keep_ctrl_c_disabled:
264
+ return True
265
+
266
+ if platform.system() != "Windows":
267
+ return True
268
+
269
+ try:
270
+ import ctypes
271
+
272
+ kernel32 = ctypes.windll.kernel32
273
+
274
+ # Get stdin handle
275
+ STD_INPUT_HANDLE = -10
276
+ stdin_handle = kernel32.GetStdHandle(STD_INPUT_HANDLE)
277
+
278
+ # Get current console mode
279
+ mode = ctypes.c_ulong()
280
+ if not kernel32.GetConsoleMode(stdin_handle, ctypes.byref(mode)):
281
+ return False
282
+
283
+ # Console mode flags
284
+ ENABLE_PROCESSED_INPUT = 0x0001
285
+
286
+ # Check if Ctrl+C processing is enabled
287
+ if mode.value & ENABLE_PROCESSED_INPUT:
288
+ # Disable it
289
+ new_mode = mode.value & ~ENABLE_PROCESSED_INPUT
290
+ return bool(kernel32.SetConsoleMode(stdin_handle, new_mode))
291
+
292
+ return True # Already disabled
293
+
294
+ except Exception:
295
+ return False
296
+
297
+
298
+ def detect_truecolor_support() -> bool:
299
+ """Detect if the terminal supports truecolor (24-bit color).
300
+
301
+ Checks multiple indicators:
302
+ 1. COLORTERM environment variable (most reliable)
303
+ 2. TERM environment variable patterns
304
+ 3. Rich's Console color_system detection as fallback
305
+
306
+ Returns:
307
+ True if truecolor is supported, False otherwise.
308
+ """
309
+ # Check COLORTERM - this is the most reliable indicator
310
+ colorterm = os.environ.get("COLORTERM", "").lower()
311
+ if colorterm in ("truecolor", "24bit"):
312
+ return True
313
+
314
+ # Check TERM for known truecolor-capable terminals
315
+ term = os.environ.get("TERM", "").lower()
316
+ truecolor_terms = (
317
+ "xterm-direct",
318
+ "xterm-truecolor",
319
+ "iterm2",
320
+ "vte-256color", # Many modern terminals set this
321
+ )
322
+ if any(t in term for t in truecolor_terms):
323
+ return True
324
+
325
+ # Some terminals like iTerm2, Kitty, Alacritty set specific env vars
326
+ if os.environ.get("ITERM_SESSION_ID"):
327
+ return True
328
+ if os.environ.get("KITTY_WINDOW_ID"):
329
+ return True
330
+ if os.environ.get("ALACRITTY_SOCKET"):
331
+ return True
332
+ if os.environ.get("WT_SESSION"): # Windows Terminal
333
+ return True
334
+
335
+ # Use Rich's detection as a fallback
336
+ try:
337
+ from rich.console import Console
338
+
339
+ console = Console(force_terminal=True)
340
+ color_system = console.color_system
341
+ return color_system == "truecolor"
342
+ except Exception:
343
+ pass
344
+
345
+ return False
346
+
347
+
348
+ def print_truecolor_warning(console: Optional["Console"] = None) -> None:
349
+ """Print a big fat red warning if truecolor is not supported.
350
+
351
+ Args:
352
+ console: Optional Rich Console instance. If None, creates a new one.
353
+ """
354
+ if detect_truecolor_support():
355
+ return # All good, no warning needed
356
+
357
+ if console is None:
358
+ try:
359
+ from rich.console import Console
360
+
361
+ console = Console()
362
+ except ImportError:
363
+ # Rich not available, fall back to plain print
364
+ print("\n" + "=" * 70)
365
+ print("⚠️ WARNING: TERMINAL DOES NOT SUPPORT TRUECOLOR (24-BIT COLOR)")
366
+ print("=" * 70)
367
+ print("Code Puppy looks best with truecolor support.")
368
+ print("Consider using a modern terminal like:")
369
+ print(" • iTerm2 (macOS)")
370
+ print(" • Windows Terminal (Windows)")
371
+ print(" • Kitty, Alacritty, or any modern terminal emulator")
372
+ print("")
373
+ print("You can also try setting: export COLORTERM=truecolor")
374
+ print("")
375
+ print("Note: The built-in macOS Terminal.app does not support truecolor")
376
+ print("(Sequoia and earlier). You'll need a different terminal app.")
377
+ print("=" * 70 + "\n")
378
+ return
379
+
380
+ # Get detected color system for diagnostic info
381
+ color_system = console.color_system or "unknown"
382
+
383
+ # Build the warning box
384
+ warning_lines = [
385
+ "",
386
+ "[bold bright_red on red]" + "━" * 72 + "[/]",
387
+ "[bold bright_red on red]┃[/][bold bright_white on red]"
388
+ + " " * 70
389
+ + "[/][bold bright_red on red]┃[/]",
390
+ "[bold bright_red on red]┃[/][bold bright_white on red] ⚠️ WARNING: TERMINAL DOES NOT SUPPORT TRUECOLOR (24-BIT COLOR) ⚠️ [/][bold bright_red on red]┃[/]",
391
+ "[bold bright_red on red]┃[/][bold bright_white on red]"
392
+ + " " * 70
393
+ + "[/][bold bright_red on red]┃[/]",
394
+ "[bold bright_red on red]" + "━" * 72 + "[/]",
395
+ "",
396
+ f"[yellow]Detected color system:[/] [bold]{color_system}[/]",
397
+ "",
398
+ "[bold white]Code Puppy uses rich colors and will look degraded without truecolor.[/]",
399
+ "",
400
+ "[cyan]Consider using a modern terminal emulator:[/]",
401
+ " [green]•[/] [bold]iTerm2[/] (macOS) - https://iterm2.com",
402
+ " [green]•[/] [bold]Windows Terminal[/] (Windows) - Built into Windows 11",
403
+ " [green]•[/] [bold]Kitty[/] - https://sw.kovidgoyal.net/kitty",
404
+ " [green]•[/] [bold]Alacritty[/] - https://alacritty.org",
405
+ " [green]•[/] [bold]Warp[/] (macOS) - https://warp.dev",
406
+ "",
407
+ "[cyan]Or try setting the COLORTERM environment variable:[/]",
408
+ " [dim]export COLORTERM=truecolor[/]",
409
+ "",
410
+ "[dim italic]Note: The built-in macOS Terminal.app does not support truecolor (Sequoia and earlier).[/]",
411
+ "[dim italic]Setting COLORTERM=truecolor won't help - you'll need a different terminal app.[/]",
412
+ "",
413
+ "[bold bright_red]" + "─" * 72 + "[/]",
414
+ "",
415
+ ]
416
+
417
+ for line in warning_lines:
418
+ console.print(line)
@@ -9,7 +9,7 @@ import threading
9
9
  import time
10
10
  import traceback
11
11
  from contextlib import contextmanager
12
- from typing import Callable, Literal, Optional, Set
12
+ from typing import Callable, List, Literal, Optional, Set
13
13
 
14
14
  from pydantic import BaseModel
15
15
  from pydantic_ai import RunContext
@@ -192,11 +192,6 @@ def kill_all_running_shell_processes() -> int:
192
192
  """Kill all currently tracked running shell processes and stop reader threads.
193
193
 
194
194
  Returns the number of processes signaled.
195
-
196
- Implementation notes:
197
- - Atomically snapshot and clear the registry to prevent race conditions
198
- - Deduplicate by PID to ensure each process is killed at most once
199
- - Let exceptions from _kill_process_group propagate (tests expect this)
200
195
  """
201
196
  global _READER_STOP_EVENT
202
197
 
@@ -204,52 +199,30 @@ def kill_all_running_shell_processes() -> int:
204
199
  if _READER_STOP_EVENT:
205
200
  _READER_STOP_EVENT.set()
206
201
 
207
- # Atomically take snapshot and clear registry
208
- # This prevents other threads from seeing/processing the same processes
202
+ procs: list[subprocess.Popen]
209
203
  with _RUNNING_PROCESSES_LOCK:
210
- procs_snapshot = list(_RUNNING_PROCESSES)
211
- _RUNNING_PROCESSES.clear()
212
-
213
- # Deduplicate by pid to ensure at-most-one kill per process
214
- seen_pids: set = set()
215
- killed_count = 0
216
-
217
- for proc in procs_snapshot:
218
- if proc is None:
219
- continue
220
-
221
- pid = getattr(proc, "pid", None)
222
- key = pid if pid is not None else id(proc)
223
-
224
- if key in seen_pids:
225
- continue
226
- seen_pids.add(key)
227
-
228
- # Close pipes first to unblock readline()
204
+ procs = list(_RUNNING_PROCESSES)
205
+ count = 0
206
+ for p in procs:
229
207
  try:
230
- if proc.stdout and not proc.stdout.closed:
231
- proc.stdout.close()
232
- if proc.stderr and not proc.stderr.closed:
233
- proc.stderr.close()
234
- if proc.stdin and not proc.stdin.closed:
235
- proc.stdin.close()
236
- except (OSError, ValueError):
237
- pass
238
-
239
- # Only attempt to kill processes that are still running
240
- if proc.poll() is None:
241
- # Let exceptions bubble up (tests expect this behavior)
242
- _kill_process_group(proc)
243
- killed_count += 1
244
-
245
- # Track user-killed PIDs
246
- if pid is not None:
247
- try:
248
- _USER_KILLED_PROCESSES.add(pid)
249
- except Exception:
250
- pass # Non-fatal bookkeeping
208
+ # Close pipes first to unblock readline()
209
+ try:
210
+ if p.stdout and not p.stdout.closed:
211
+ p.stdout.close()
212
+ if p.stderr and not p.stderr.closed:
213
+ p.stderr.close()
214
+ if p.stdin and not p.stdin.closed:
215
+ p.stdin.close()
216
+ except (OSError, ValueError):
217
+ pass
251
218
 
252
- return killed_count
219
+ if p.poll() is None:
220
+ _kill_process_group(p)
221
+ count += 1
222
+ _USER_KILLED_PROCESSES.add(p.pid)
223
+ finally:
224
+ _unregister_process(p)
225
+ return count
253
226
 
254
227
 
255
228
  def get_running_shell_process_count() -> int:
@@ -907,6 +880,7 @@ async def run_shell_command(
907
880
  command=command,
908
881
  cwd=cwd,
909
882
  timeout=0, # No timeout for background processes
883
+ background=True,
910
884
  )
911
885
  )
912
886
 
@@ -1104,12 +1078,21 @@ class ReasoningOutput(BaseModel):
1104
1078
 
1105
1079
 
1106
1080
  def share_your_reasoning(
1107
- context: RunContext, reasoning: str, next_steps: str | None = None
1081
+ context: RunContext, reasoning: str, next_steps: str | List[str] | None = None
1108
1082
  ) -> ReasoningOutput:
1083
+ # Handle list of next steps by formatting them
1084
+ formatted_next_steps = next_steps
1085
+ if isinstance(next_steps, list):
1086
+ formatted_next_steps = "\n".join(
1087
+ [f"{i + 1}. {step}" for i, step in enumerate(next_steps)]
1088
+ )
1089
+
1109
1090
  # Emit structured AgentReasoningMessage for the UI
1110
1091
  reasoning_msg = AgentReasoningMessage(
1111
1092
  reasoning=reasoning,
1112
- next_steps=next_steps if next_steps and next_steps.strip() else None,
1093
+ next_steps=formatted_next_steps
1094
+ if formatted_next_steps and formatted_next_steps.strip()
1095
+ else None,
1113
1096
  )
1114
1097
  get_message_bus().emit(reasoning_msg)
1115
1098
 
@@ -1197,7 +1180,9 @@ def register_agent_share_your_reasoning(agent):
1197
1180
 
1198
1181
  @agent.tool
1199
1182
  def agent_share_your_reasoning(
1200
- context: RunContext, reasoning: str = "", next_steps: str | None = None
1183
+ context: RunContext,
1184
+ reasoning: str = "",
1185
+ next_steps: str | List[str] | None = None,
1201
1186
  ) -> ReasoningOutput:
1202
1187
  """Share the agent's current reasoning and planned next steps with the user.
1203
1188
 
@@ -1211,8 +1196,8 @@ def register_agent_share_your_reasoning(agent):
1211
1196
  reasoning for the current situation. This should be clear,
1212
1197
  comprehensive, and explain the 'why' behind decisions.
1213
1198
  next_steps: Planned upcoming actions or steps
1214
- the agent intends to take. Can be None if no specific next steps
1215
- are determined. Defaults to None.
1199
+ the agent intends to take. Can be a string or a list of strings.
1200
+ Can be None if no specific next steps are determined. Defaults to None.
1216
1201
 
1217
1202
  Returns:
1218
1203
  ReasoningOutput: A simple response object containing:
@@ -1223,6 +1208,10 @@ def register_agent_share_your_reasoning(agent):
1223
1208
  >>> next_steps = "First, I'll list the directory contents, then read key files"
1224
1209
  >>> result = agent_share_your_reasoning(ctx, reasoning, next_steps)
1225
1210
 
1211
+ >>> # Using a list for next steps
1212
+ >>> next_steps_list = ["List files", "Read README.md", "Run tests"]
1213
+ >>> result = agent_share_your_reasoning(ctx, reasoning, next_steps_list)
1214
+
1226
1215
  Best Practice:
1227
1216
  Use this tool frequently to maintain transparency. Call it:
1228
1217
  - Before starting complex operations
@@ -727,15 +727,9 @@ def _format_diff_with_syntax_highlighting(
727
727
  result.append("\n")
728
728
  continue
729
729
 
730
- # Handle diff headers specially
731
- if line.startswith("---"):
732
- result.append(line, style="yellow")
733
- elif line.startswith("+++"):
734
- result.append(line, style="yellow")
735
- elif line.startswith("@@"):
736
- result.append(line, style="cyan")
737
- elif line.startswith(("diff ", "index ")):
738
- result.append(line, style="dim")
730
+ # Skip diff headers - they're redundant noise since we show the filename in the banner
731
+ if line.startswith(("---", "+++", "@@", "diff ", "index ")):
732
+ continue
739
733
  else:
740
734
  # Determine line type and extract code content
741
735
  if line.startswith("-"):