code-puppy 0.0.332__py3-none-any.whl → 0.0.333__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.
@@ -75,8 +75,6 @@ from code_puppy.model_factory import ModelFactory, make_model_settings
75
75
  from code_puppy.summarization_agent import run_summarization_sync
76
76
  from code_puppy.tools.agent_tools import _active_subagent_tasks
77
77
  from code_puppy.tools.command_runner import (
78
- _add_windows_ctrl_handler,
79
- _remove_windows_ctrl_handler,
80
78
  is_awaiting_user_input,
81
79
  )
82
80
 
@@ -1384,15 +1382,14 @@ class BaseAgent(ABC):
1384
1382
  def _print_thinking_banner() -> None:
1385
1383
  """Print the THINKING banner with spinner pause and line clear."""
1386
1384
  nonlocal did_stream_anything
1387
- import sys
1388
1385
  import time
1389
1386
 
1390
1387
  from code_puppy.config import get_banner_color
1391
1388
 
1392
1389
  pause_all_spinners()
1393
1390
  time.sleep(0.1) # Delay to let spinner fully clear
1394
- sys.stdout.write("\r\x1b[K") # Clear line
1395
- sys.stdout.flush()
1391
+ # Clear line and print newline before banner
1392
+ console.print(" " * 50, end="\r")
1396
1393
  console.print() # Newline before banner
1397
1394
  # Bold banner with configurable color and lightning bolt
1398
1395
  thinking_color = get_banner_color("thinking")
@@ -1402,21 +1399,19 @@ class BaseAgent(ABC):
1402
1399
  ),
1403
1400
  end="",
1404
1401
  )
1405
- sys.stdout.flush()
1406
1402
  did_stream_anything = True
1407
1403
 
1408
1404
  def _print_response_banner() -> None:
1409
1405
  """Print the AGENT RESPONSE banner with spinner pause and line clear."""
1410
1406
  nonlocal did_stream_anything
1411
- import sys
1412
1407
  import time
1413
1408
 
1414
1409
  from code_puppy.config import get_banner_color
1415
1410
 
1416
1411
  pause_all_spinners()
1417
1412
  time.sleep(0.1) # Delay to let spinner fully clear
1418
- sys.stdout.write("\r\x1b[K") # Clear line
1419
- sys.stdout.flush()
1413
+ # Clear line and print newline before banner
1414
+ console.print(" " * 50, end="\r")
1420
1415
  console.print() # Newline before banner
1421
1416
  response_color = get_banner_color("agent_response")
1422
1417
  console.print(
@@ -1424,7 +1419,6 @@ class BaseAgent(ABC):
1424
1419
  f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
1425
1420
  )
1426
1421
  )
1427
- sys.stdout.flush()
1428
1422
  did_stream_anything = True
1429
1423
 
1430
1424
  async for event in events:
@@ -1448,8 +1442,8 @@ class BaseAgent(ABC):
1448
1442
  # Buffer initial content if present
1449
1443
  if part.content and part.content.strip():
1450
1444
  text_buffer[event.index].append(part.content)
1451
- # Use len(content) / 3 for token estimation (more accurate than chunk counting)
1452
- token_count[event.index] += len(part.content) // 3
1445
+ # Count chunks (each part counts as 1)
1446
+ token_count[event.index] += 1
1453
1447
  elif isinstance(part, ToolCallPart):
1454
1448
  streaming_parts.add(event.index)
1455
1449
  tool_parts.add(event.index)
@@ -1467,24 +1461,20 @@ class BaseAgent(ABC):
1467
1461
  if delta.content_delta:
1468
1462
  # For text parts, show token counter then render at end
1469
1463
  if event.index in text_parts:
1470
- import sys
1471
-
1472
1464
  # Print banner on first content
1473
1465
  if event.index not in banner_printed:
1474
1466
  _print_response_banner()
1475
1467
  banner_printed.add(event.index)
1476
1468
  # Accumulate text for final markdown render
1477
1469
  text_buffer[event.index].append(delta.content_delta)
1478
- # Use len(content) / 3 for token estimation
1479
- token_count[event.index] += (
1480
- len(delta.content_delta) // 3
1481
- )
1482
- # Update token counter in place (single line)
1470
+ # Count chunks received
1471
+ token_count[event.index] += 1
1472
+ # Update chunk counter in place (single line)
1483
1473
  count = token_count[event.index]
1484
- sys.stdout.write(
1485
- f"\r\x1b[K ⏳ Receiving... {count} tokens"
1474
+ console.print(
1475
+ f" ⏳ Receiving... {count} chunks ",
1476
+ end="\r",
1486
1477
  )
1487
- sys.stdout.flush()
1488
1478
  else:
1489
1479
  # For thinking parts, stream immediately (dim)
1490
1480
  if event.index not in banner_printed:
@@ -1493,34 +1483,30 @@ class BaseAgent(ABC):
1493
1483
  escaped = escape(delta.content_delta)
1494
1484
  console.print(f"[dim]{escaped}[/dim]", end="")
1495
1485
  elif isinstance(delta, ToolCallPartDelta):
1496
- import sys
1497
-
1498
- # For tool calls, show token counter (use string repr for estimation)
1499
- token_count[event.index] += len(str(delta)) // 3
1486
+ # For tool calls, count chunks received
1487
+ token_count[event.index] += 1
1500
1488
  # Get tool name if available
1501
1489
  tool_name = getattr(delta, "tool_name_delta", "")
1502
1490
  count = token_count[event.index]
1503
1491
  # Display with tool wrench icon and tool name
1504
1492
  if tool_name:
1505
- sys.stdout.write(
1506
- f"\r\x1b[K 🔧 Calling {tool_name}... {count} tokens"
1493
+ console.print(
1494
+ f" 🔧 Calling {tool_name}... {count} chunks ",
1495
+ end="\r",
1507
1496
  )
1508
1497
  else:
1509
- sys.stdout.write(
1510
- f"\r\x1b[K 🔧 Calling tool... {count} tokens"
1498
+ console.print(
1499
+ f" 🔧 Calling tool... {count} chunks ",
1500
+ end="\r",
1511
1501
  )
1512
- sys.stdout.flush()
1513
1502
 
1514
1503
  # PartEndEvent - finish the streaming with a newline
1515
1504
  elif isinstance(event, PartEndEvent):
1516
1505
  if event.index in streaming_parts:
1517
- import sys
1518
-
1519
1506
  # For text parts, clear counter line and render markdown
1520
1507
  if event.index in text_parts:
1521
- # Clear the token counter line
1522
- sys.stdout.write("\r\x1b[K")
1523
- sys.stdout.flush()
1508
+ # Clear the chunk counter line by printing spaces and returning
1509
+ console.print(" " * 50, end="\r")
1524
1510
  # Render the final markdown nicely
1525
1511
  if event.index in text_buffer:
1526
1512
  try:
@@ -1530,11 +1516,10 @@ class BaseAgent(ABC):
1530
1516
  except Exception:
1531
1517
  pass
1532
1518
  del text_buffer[event.index]
1533
- # For tool parts, clear the token counter line
1519
+ # For tool parts, clear the chunk counter line
1534
1520
  elif event.index in tool_parts:
1535
- # Clear the token counter line
1536
- sys.stdout.write("\r\x1b[K")
1537
- sys.stdout.flush()
1521
+ # Clear the chunk counter line by printing spaces and returning
1522
+ console.print(" " * 50, end="\r")
1538
1523
  # For thinking parts, just print newline
1539
1524
  elif event.index in banner_printed:
1540
1525
  console.print() # Final newline after streaming
@@ -1953,7 +1938,12 @@ class BaseAgent(ABC):
1953
1938
  def graceful_sigint_handler(_sig, _frame):
1954
1939
  # When using keyboard-based cancel, SIGINT should be a no-op
1955
1940
  # (just show a hint to user about the configured cancel key)
1941
+ # Also reset terminal to prevent bricking on Windows+uvx
1956
1942
  from code_puppy.keymap import get_cancel_agent_display_name
1943
+ from code_puppy.terminal_utils import reset_windows_terminal_full
1944
+
1945
+ # Reset terminal state first to prevent bricking
1946
+ reset_windows_terminal_full()
1957
1947
 
1958
1948
  cancel_key = get_cancel_agent_display_name()
1959
1949
  emit_info(f"Use {cancel_key} to cancel the agent task.")
@@ -1961,12 +1951,6 @@ class BaseAgent(ABC):
1961
1951
  original_handler = None
1962
1952
  key_listener_stop_event = None
1963
1953
  _key_listener_thread = None
1964
- windows_ctrl_handler = None
1965
-
1966
- # Check if we're on Windows
1967
- import sys
1968
-
1969
- is_windows = sys.platform.startswith("win")
1970
1954
 
1971
1955
  try:
1972
1956
  if cancel_agent_uses_signal():
@@ -1974,27 +1958,10 @@ class BaseAgent(ABC):
1974
1958
  original_handler = signal.signal(
1975
1959
  signal.SIGINT, keyboard_interrupt_handler
1976
1960
  )
1977
- # On Windows, also use SetConsoleCtrlHandler for reliable Ctrl+C with uvx
1978
- if is_windows:
1979
- windows_ctrl_handler = _add_windows_ctrl_handler(
1980
- schedule_agent_cancel
1981
- )
1982
1961
  else:
1983
1962
  # Use keyboard listener for agent cancellation
1984
1963
  # Set a graceful SIGINT handler that shows a hint
1985
1964
  original_handler = signal.signal(signal.SIGINT, graceful_sigint_handler)
1986
- # On Windows, SetConsoleCtrlHandler should also show the hint
1987
- if is_windows:
1988
-
1989
- def graceful_ctrl_handler():
1990
- from code_puppy.keymap import get_cancel_agent_display_name
1991
-
1992
- cancel_key = get_cancel_agent_display_name()
1993
- emit_info(f"Use {cancel_key} to cancel the agent task.")
1994
-
1995
- windows_ctrl_handler = _add_windows_ctrl_handler(
1996
- graceful_ctrl_handler
1997
- )
1998
1965
  # Spawn keyboard listener with the cancel agent callback
1999
1966
  key_listener_stop_event = threading.Event()
2000
1967
  _key_listener_thread = self._spawn_ctrl_x_key_listener(
@@ -2024,17 +1991,8 @@ class BaseAgent(ABC):
2024
1991
  # Stop keyboard listener if it was started
2025
1992
  if key_listener_stop_event is not None:
2026
1993
  key_listener_stop_event.set()
2027
- # Remove Windows console handler
2028
- if windows_ctrl_handler is not None:
2029
- _remove_windows_ctrl_handler(windows_ctrl_handler)
2030
1994
  # Restore original signal handler
2031
1995
  if (
2032
1996
  original_handler is not None
2033
1997
  ): # Explicit None check - SIG_DFL can be 0/falsy!
2034
1998
  signal.signal(signal.SIGINT, original_handler)
2035
- # Windows-specific: Reset terminal after Ctrl+C to prevent corruption
2036
- # This fixes the issue where Enter key shows as 'm' after interrupting with uvx
2037
- if is_windows:
2038
- from code_puppy.terminal_utils import reset_windows_terminal_full
2039
-
2040
- reset_windows_terminal_full()
code_puppy/cli_runner.py CHANGED
@@ -171,6 +171,45 @@ async def main():
171
171
  emit_error(str(e))
172
172
  sys.exit(1)
173
173
 
174
+ # Show uvx detection notice if we're on Windows + uvx
175
+ # Also disable Ctrl+C at the console level to prevent terminal bricking
176
+ try:
177
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
178
+
179
+ if should_use_alternate_cancel_key():
180
+ from code_puppy.terminal_utils import (
181
+ disable_windows_ctrl_c,
182
+ set_keep_ctrl_c_disabled,
183
+ )
184
+
185
+ # Disable Ctrl+C at the console input level
186
+ # This prevents Ctrl+C from being processed as a signal at all
187
+ disable_windows_ctrl_c()
188
+
189
+ # Set flag to keep it disabled (prompt_toolkit may re-enable it)
190
+ set_keep_ctrl_c_disabled(True)
191
+
192
+ # Use print directly - emit_system_message can get cleared by ANSI codes
193
+ print(
194
+ "🔧 Detected uvx launch on Windows - using Ctrl+K for cancellation "
195
+ "(Ctrl+C is disabled to prevent terminal issues)"
196
+ )
197
+
198
+ # Also install a SIGINT handler as backup
199
+ import signal
200
+
201
+ from code_puppy.terminal_utils import reset_windows_terminal_full
202
+
203
+ def _uvx_protective_sigint_handler(_sig, _frame):
204
+ """Protective SIGINT handler for Windows+uvx."""
205
+ reset_windows_terminal_full()
206
+ # Re-disable Ctrl+C in case something re-enabled it
207
+ disable_windows_ctrl_c()
208
+
209
+ signal.signal(signal.SIGINT, _uvx_protective_sigint_handler)
210
+ except ImportError:
211
+ pass # uvx_detection module not available, ignore
212
+
174
213
  # Load API keys from puppy.cfg into environment variables
175
214
  from code_puppy.config import load_api_keys_to_environment
176
215
 
@@ -421,10 +460,6 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
421
460
  current_agent_task = None
422
461
 
423
462
  while True:
424
- # Windows-specific: Aggressively reset terminal at the start of every loop
425
- # This fixes terminal corruption after Ctrl+C, especially when running via uvx
426
- reset_windows_terminal_full()
427
-
428
463
  from code_puppy.agents.agent_manager import get_current_agent
429
464
  from code_puppy.messaging import emit_info
430
465
 
@@ -444,6 +479,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
444
479
  task = await get_input_with_combined_completion(
445
480
  get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
446
481
  )
482
+
483
+ # Windows+uvx: Re-disable Ctrl+C after prompt_toolkit
484
+ # (prompt_toolkit restores console mode which re-enables Ctrl+C)
485
+ try:
486
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
487
+
488
+ ensure_ctrl_c_disabled()
489
+ except ImportError:
490
+ pass
447
491
  except ImportError:
448
492
  # Fall back to basic input if prompt_toolkit is not available
449
493
  task = input(">>> ")
@@ -608,8 +652,14 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
608
652
  # Check if the task was cancelled (but don't show message if we just killed processes)
609
653
  if result is None:
610
654
  # Windows-specific: Reset terminal state after cancellation
611
- # Use full reset (ANSI + console mode) to fix stdin after Ctrl+C
612
- reset_windows_terminal_full()
655
+ reset_windows_terminal_ansi()
656
+ # Re-disable Ctrl+C if needed (uvx mode)
657
+ try:
658
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
659
+
660
+ ensure_ctrl_c_disabled()
661
+ except ImportError:
662
+ pass
613
663
  continue
614
664
  # Get the structured response
615
665
  agent_response = result.output
@@ -650,6 +700,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
650
700
 
651
701
  auto_save_session_if_enabled()
652
702
 
703
+ # Re-disable Ctrl+C if needed (uvx mode) - must be done after
704
+ # each iteration as various operations may restore console mode
705
+ try:
706
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
707
+
708
+ ensure_ctrl_c_disabled()
709
+ except ImportError:
710
+ pass
711
+
653
712
 
654
713
  def prettier_code_blocks():
655
714
  """Configure Rich to use prettier code block rendering."""
code_puppy/keymap.py CHANGED
@@ -55,11 +55,19 @@ class KeymapError(Exception):
55
55
  def get_cancel_agent_key() -> str:
56
56
  """Get the configured cancel agent key from config.
57
57
 
58
+ On Windows when launched via uvx, this automatically returns "ctrl+k"
59
+ to work around uvx capturing Ctrl+C before it reaches Python.
60
+
58
61
  Returns:
59
62
  The key name (e.g., "ctrl+c", "ctrl+k") from config,
60
63
  or the default if not configured.
61
64
  """
62
65
  from code_puppy.config import get_value
66
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
67
+
68
+ # On Windows + uvx, force ctrl+k to bypass uvx's SIGINT capture
69
+ if should_use_alternate_cancel_key():
70
+ return "ctrl+k"
63
71
 
64
72
  key = get_value("cancel_agent_key")
65
73
  if key is None or key.strip() == "":
@@ -6,6 +6,10 @@ Handles Windows console mode resets and Unix terminal sanity restoration.
6
6
  import platform
7
7
  import subprocess
8
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
9
13
 
10
14
 
11
15
  def reset_windows_terminal_ansi() -> None:
@@ -86,21 +90,36 @@ def reset_windows_console_mode() -> None:
86
90
  pass # Silently ignore errors - best effort reset
87
91
 
88
92
 
89
- def reset_windows_terminal_full() -> None:
90
- """Perform a full Windows terminal reset (console mode + ANSI).
93
+ def flush_windows_keyboard_buffer() -> None:
94
+ """Flush the Windows keyboard buffer.
91
95
 
92
- Combines both console mode reset and ANSI reset for complete
93
- terminal state restoration after interrupts.
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
94
101
 
95
- IMPORTANT: Console mode must be reset FIRST to re-enable
96
- ENABLE_VIRTUAL_TERMINAL_PROCESSING, otherwise the ANSI escape
97
- sequences will be printed as literal text (e.g., '[0m').
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.
98
116
  """
99
117
  if platform.system() != "Windows":
100
118
  return
101
119
 
102
- reset_windows_console_mode() # Must be first! Enables ANSI processing
103
- reset_windows_terminal_ansi() # Now ANSI escapes will be interpreted
120
+ reset_windows_terminal_ansi()
121
+ reset_windows_console_mode()
122
+ flush_windows_keyboard_buffer()
104
123
 
105
124
 
106
125
  def reset_unix_terminal() -> None:
@@ -128,3 +147,145 @@ def reset_terminal() -> None:
128
147
  reset_windows_terminal_full()
129
148
  else:
130
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
@@ -44,70 +44,9 @@ def _truncate_line(line: str) -> str:
44
44
  if sys.platform.startswith("win"):
45
45
  import msvcrt
46
46
 
47
- # Load kernel32 for PeekNamedPipe and SetConsoleCtrlHandler
47
+ # Load kernel32 for PeekNamedPipe
48
48
  _kernel32 = ctypes.windll.kernel32
49
49
 
50
- # SetConsoleCtrlHandler types for Ctrl+C handling on Windows
51
- # This is more reliable than signal.SIGINT when running under uvx
52
- _CTRL_C_EVENT = 0
53
- _CTRL_BREAK_EVENT = 1
54
- _HANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_ulong)
55
-
56
- # Track registered handlers to prevent garbage collection
57
- _registered_console_handlers: list = []
58
-
59
- def _add_windows_ctrl_handler(callback: Callable[[], None]) -> Optional[Callable]:
60
- """Register a Windows console control handler for Ctrl+C/Ctrl+Break.
61
-
62
- Args:
63
- callback: Function to call when Ctrl+C or Ctrl+Break is pressed.
64
- Should take no arguments.
65
-
66
- Returns:
67
- The wrapped handler function (needed for removal), or None if failed.
68
- """
69
-
70
- def handler(ctrl_type: int) -> int:
71
- if ctrl_type in (_CTRL_C_EVENT, _CTRL_BREAK_EVENT):
72
- try:
73
- callback()
74
- except Exception:
75
- pass
76
- return 1 # TRUE = we handled it, don't pass to next handler
77
- return 0 # FALSE = let next handler deal with it
78
-
79
- # Wrap in WINFUNCTYPE to make it callable from C
80
- wrapped = _HANDLER_ROUTINE(handler)
81
- # Keep reference to prevent GC
82
- _registered_console_handlers.append(wrapped)
83
-
84
- try:
85
- if _kernel32.SetConsoleCtrlHandler(wrapped, True):
86
- return wrapped
87
- except Exception:
88
- pass
89
- return None
90
-
91
- def _remove_windows_ctrl_handler(handler: Callable) -> bool:
92
- """Remove a previously registered Windows console control handler.
93
-
94
- Args:
95
- handler: The handler returned by _add_windows_ctrl_handler.
96
-
97
- Returns:
98
- True if successfully removed, False otherwise.
99
- """
100
- if handler is None:
101
- return False
102
- try:
103
- result = _kernel32.SetConsoleCtrlHandler(handler, False)
104
- # Clean up our reference
105
- if handler in _registered_console_handlers:
106
- _registered_console_handlers.remove(handler)
107
- return bool(result)
108
- except Exception:
109
- return False
110
-
111
50
  def _win32_pipe_has_data(pipe) -> bool:
112
51
  """Check if a Windows pipe has data available without blocking.
113
52
 
@@ -148,15 +87,8 @@ if sys.platform.startswith("win"):
148
87
  except (ValueError, OSError, ctypes.ArgumentError):
149
88
  # Handle closed, invalid, or other errors
150
89
  return False
151
-
152
90
  else:
153
- # Non-Windows: provide no-op stubs
154
- def _add_windows_ctrl_handler(callback: Callable[[], None]) -> Optional[Callable]:
155
- return None
156
-
157
- def _remove_windows_ctrl_handler(handler: Callable) -> bool:
158
- return False
159
-
91
+ # POSIX stub - not used, but keeps the code clean
160
92
  def _win32_pipe_has_data(pipe) -> bool:
161
93
  return False
162
94
 
@@ -174,7 +106,6 @@ _USER_KILLED_PROCESSES = set()
174
106
  _SHELL_CTRL_X_STOP_EVENT: Optional[threading.Event] = None
175
107
  _SHELL_CTRL_X_THREAD: Optional[threading.Thread] = None
176
108
  _ORIGINAL_SIGINT_HANDLER = None
177
- _WINDOWS_CTRL_HANDLER = None # For SetConsoleCtrlHandler on Windows
178
109
 
179
110
  # Stop event to signal reader threads to terminate
180
111
  _READER_STOP_EVENT: Optional[threading.Event] = None
@@ -504,10 +435,8 @@ def _shell_command_keyboard_context():
504
435
  1. Disables the agent's Ctrl-C handler (so it doesn't cancel the agent)
505
436
  2. Enables a Ctrl-X listener to kill the running shell process
506
437
  3. Restores the original Ctrl-C handler when done
507
- 4. On Windows, uses SetConsoleCtrlHandler for reliable Ctrl+C with uvx
508
438
  """
509
439
  global _SHELL_CTRL_X_STOP_EVENT, _SHELL_CTRL_X_THREAD, _ORIGINAL_SIGINT_HANDLER
510
- global _WINDOWS_CTRL_HANDLER
511
440
 
512
441
  # Handler for Ctrl-X: kill all running shell processes
513
442
  def handle_ctrl_x_press() -> None:
@@ -515,15 +444,11 @@ def _shell_command_keyboard_context():
515
444
  kill_all_running_shell_processes()
516
445
 
517
446
  # Handler for Ctrl-C during shell execution: just kill the shell process, don't cancel agent
518
- def shell_ctrl_c_callback():
447
+ def shell_sigint_handler(_sig, _frame):
519
448
  """During shell execution, Ctrl-C kills the shell but doesn't cancel the agent."""
520
449
  emit_warning("\n🛑 Ctrl-C detected! Interrupting shell command...")
521
450
  kill_all_running_shell_processes()
522
451
 
523
- def shell_sigint_handler(_sig, _frame):
524
- """Signal handler wrapper for SIGINT."""
525
- shell_ctrl_c_callback()
526
-
527
452
  # Set up Ctrl-X listener
528
453
  _SHELL_CTRL_X_STOP_EVENT = threading.Event()
529
454
  _SHELL_CTRL_X_THREAD = _spawn_ctrl_x_key_listener(
@@ -531,12 +456,7 @@ def _shell_command_keyboard_context():
531
456
  handle_ctrl_x_press,
532
457
  )
533
458
 
534
- # On Windows, use SetConsoleCtrlHandler for reliable Ctrl+C handling
535
- # This works even when running under uvx where SIGINT doesn't propagate properly
536
- if sys.platform.startswith("win"):
537
- _WINDOWS_CTRL_HANDLER = _add_windows_ctrl_handler(shell_ctrl_c_callback)
538
-
539
- # Also set SIGINT handler (works on POSIX, may work on some Windows setups)
459
+ # Replace SIGINT handler temporarily
540
460
  try:
541
461
  _ORIGINAL_SIGINT_HANDLER = signal.signal(signal.SIGINT, shell_sigint_handler)
542
462
  except (ValueError, OSError):
@@ -556,10 +476,6 @@ def _shell_command_keyboard_context():
556
476
  except Exception:
557
477
  pass
558
478
 
559
- # Remove Windows console handler
560
- if _WINDOWS_CTRL_HANDLER is not None:
561
- _remove_windows_ctrl_handler(_WINDOWS_CTRL_HANDLER)
562
-
563
479
  # Restore original SIGINT handler
564
480
  if _ORIGINAL_SIGINT_HANDLER is not None:
565
481
  try:
@@ -571,7 +487,6 @@ def _shell_command_keyboard_context():
571
487
  _SHELL_CTRL_X_STOP_EVENT = None
572
488
  _SHELL_CTRL_X_THREAD = None
573
489
  _ORIGINAL_SIGINT_HANDLER = None
574
- _WINDOWS_CTRL_HANDLER = None
575
490
 
576
491
 
577
492
  def run_shell_command_streaming(
@@ -0,0 +1,242 @@
1
+ """Detect if code-puppy was launched via uvx on Windows.
2
+
3
+ This module provides utilities to detect the launch method of code-puppy,
4
+ specifically to handle signal differences when running via uvx on Windows.
5
+
6
+ On Windows, when launched via `uvx code-puppy`, Ctrl+C (SIGINT) gets captured
7
+ by uvx's process handling before reaching our Python process. To work around
8
+ this, we detect the uvx launch scenario and switch to Ctrl+K for cancellation.
9
+
10
+ Note: This issue is specific to uvx.exe, NOT uv.exe. Running via `uv run`
11
+ handles SIGINT correctly on Windows.
12
+
13
+ On non-Windows platforms, this is not an issue - Ctrl+C works fine with uvx.
14
+ """
15
+
16
+ import os
17
+ import platform
18
+ import sys
19
+ from functools import lru_cache
20
+ from typing import Optional
21
+
22
+ # Cache the detection result - it won't change during runtime
23
+ _uvx_detection_cache: Optional[bool] = None
24
+
25
+
26
+ def _get_parent_process_name_psutil(pid: int) -> Optional[str]:
27
+ """Get parent process name using psutil (if available).
28
+
29
+ Args:
30
+ pid: Process ID to get parent name for
31
+
32
+ Returns:
33
+ Parent process name (lowercase) or None if not found
34
+ """
35
+ try:
36
+ import psutil
37
+
38
+ proc = psutil.Process(pid)
39
+ parent = proc.parent()
40
+ if parent:
41
+ return parent.name().lower()
42
+ except Exception:
43
+ pass
44
+ return None
45
+
46
+
47
+ def _get_parent_process_chain_psutil() -> list[str]:
48
+ """Get the entire parent process chain using psutil.
49
+
50
+ Returns:
51
+ List of process names from current process up to init/System
52
+ """
53
+ chain = []
54
+ try:
55
+ import psutil
56
+
57
+ proc = psutil.Process(os.getpid())
58
+ while proc:
59
+ chain.append(proc.name().lower())
60
+ parent = proc.parent()
61
+ if parent is None or parent.pid in (0, proc.pid):
62
+ break
63
+ proc = parent
64
+ except Exception:
65
+ pass
66
+ return chain
67
+
68
+
69
+ def _get_parent_process_chain_windows_ctypes() -> list[str]:
70
+ """Get parent process chain on Windows using ctypes (no external deps).
71
+
72
+ This is a fallback when psutil is not available.
73
+
74
+ Returns:
75
+ List of process names from current process up to System
76
+ """
77
+ if platform.system() != "Windows":
78
+ return []
79
+
80
+ chain = []
81
+ try:
82
+ import ctypes
83
+ from ctypes import wintypes
84
+
85
+ # Windows API constants
86
+ TH32CS_SNAPPROCESS = 0x00000002
87
+ INVALID_HANDLE_VALUE = -1
88
+
89
+ class PROCESSENTRY32(ctypes.Structure):
90
+ _fields_ = [
91
+ ("dwSize", wintypes.DWORD),
92
+ ("cntUsage", wintypes.DWORD),
93
+ ("th32ProcessID", wintypes.DWORD),
94
+ ("th32DefaultHeapID", ctypes.POINTER(wintypes.ULONG)),
95
+ ("th32ModuleID", wintypes.DWORD),
96
+ ("cntThreads", wintypes.DWORD),
97
+ ("th32ParentProcessID", wintypes.DWORD),
98
+ ("pcPriClassBase", wintypes.LONG),
99
+ ("dwFlags", wintypes.DWORD),
100
+ ("szExeFile", ctypes.c_char * 260),
101
+ ]
102
+
103
+ kernel32 = ctypes.windll.kernel32
104
+
105
+ # Take a snapshot of all processes
106
+ snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
107
+ if snapshot == INVALID_HANDLE_VALUE:
108
+ return chain
109
+
110
+ try:
111
+ # Build a map of PID -> (parent_pid, exe_name)
112
+ process_map: dict[int, tuple[int, str]] = {}
113
+ pe = PROCESSENTRY32()
114
+ pe.dwSize = ctypes.sizeof(PROCESSENTRY32)
115
+
116
+ if kernel32.Process32First(snapshot, ctypes.byref(pe)):
117
+ while True:
118
+ pid = pe.th32ProcessID
119
+ parent_pid = pe.th32ParentProcessID
120
+ exe_name = pe.szExeFile.decode("utf-8", errors="ignore").lower()
121
+ process_map[pid] = (parent_pid, exe_name)
122
+
123
+ if not kernel32.Process32Next(snapshot, ctypes.byref(pe)):
124
+ break
125
+
126
+ # Traverse from current PID up the parent chain
127
+ current_pid = os.getpid()
128
+ visited = set() # Prevent infinite loops
129
+
130
+ while current_pid in process_map and current_pid not in visited:
131
+ visited.add(current_pid)
132
+ parent_pid, exe_name = process_map[current_pid]
133
+ chain.append(exe_name)
134
+
135
+ if parent_pid == 0 or parent_pid == current_pid:
136
+ break
137
+ current_pid = parent_pid
138
+
139
+ finally:
140
+ kernel32.CloseHandle(snapshot)
141
+
142
+ except Exception:
143
+ pass
144
+
145
+ return chain
146
+
147
+
148
+ def _get_parent_process_chain() -> list[str]:
149
+ """Get the parent process chain using best available method.
150
+
151
+ Returns:
152
+ List of process names from current process up to init/System
153
+ """
154
+ # Try psutil first (more reliable, cross-platform)
155
+ try:
156
+ import psutil # noqa: F401
157
+
158
+ return _get_parent_process_chain_psutil()
159
+ except ImportError:
160
+ pass
161
+
162
+ # Fall back to ctypes on Windows
163
+ if platform.system() == "Windows":
164
+ return _get_parent_process_chain_windows_ctypes()
165
+
166
+ return []
167
+
168
+
169
+ def _is_uvx_in_chain(chain: list[str]) -> bool:
170
+ """Check if uvx is in the process chain.
171
+
172
+ Note: We only check for uvx.exe, NOT uv.exe. The uv.exe binary
173
+ (used by `uv run`) handles SIGINT correctly on Windows, but
174
+ uvx.exe captures it before it reaches Python.
175
+
176
+ Args:
177
+ chain: List of process names (lowercase)
178
+
179
+ Returns:
180
+ True if uvx.exe is found in the chain
181
+ """
182
+ # Only uvx.exe has the SIGINT issue, not uv.exe
183
+ uvx_names = {"uvx.exe", "uvx"}
184
+ return any(name in uvx_names for name in chain)
185
+
186
+
187
+ @lru_cache(maxsize=1)
188
+ def is_launched_via_uvx() -> bool:
189
+ """Detect if code-puppy was launched via uvx.
190
+
191
+ Traverses the parent process chain to find uvx.exe or uv.exe.
192
+ Result is cached for the lifetime of the process.
193
+
194
+ Returns:
195
+ True if launched via uvx, False otherwise
196
+ """
197
+ chain = _get_parent_process_chain()
198
+ return _is_uvx_in_chain(chain)
199
+
200
+
201
+ def is_windows() -> bool:
202
+ """Check if we're running on Windows.
203
+
204
+ Returns:
205
+ True if running on Windows, False otherwise
206
+ """
207
+ return platform.system() == "Windows"
208
+
209
+
210
+ def should_use_alternate_cancel_key() -> bool:
211
+ """Determine if we should use an alternate cancel key (Ctrl+K) instead of Ctrl+C.
212
+
213
+ This returns True when:
214
+ - Running on Windows AND
215
+ - Launched via uvx
216
+
217
+ In this scenario, Ctrl+C is captured by uvx before reaching Python,
218
+ so we need to use a different key (Ctrl+K) for agent cancellation.
219
+
220
+ Returns:
221
+ True if alternate cancel key should be used, False otherwise
222
+ """
223
+ return is_windows() and is_launched_via_uvx()
224
+
225
+
226
+ def get_uvx_detection_info() -> dict:
227
+ """Get diagnostic information about uvx detection.
228
+
229
+ Useful for debugging and testing.
230
+
231
+ Returns:
232
+ Dictionary with detection details
233
+ """
234
+ chain = _get_parent_process_chain()
235
+ return {
236
+ "is_windows": is_windows(),
237
+ "is_launched_via_uvx": is_launched_via_uvx(),
238
+ "should_use_alternate_cancel_key": should_use_alternate_cancel_key(),
239
+ "parent_process_chain": chain,
240
+ "current_pid": os.getpid(),
241
+ "python_executable": sys.executable,
242
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.332
3
+ Version: 0.0.333
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -3,12 +3,12 @@ code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
3
3
  code_puppy/callbacks.py,sha256=hqTV--dNxG5vwWWm3MrEjmb8MZuHFFdmHePl23NXPHk,8621
4
4
  code_puppy/chatgpt_codex_client.py,sha256=Om0ANB_kpHubhCwNzF9ENf8RvKBqs0IYzBLl_SNw0Vk,9833
5
5
  code_puppy/claude_cache_client.py,sha256=hZr_YtXZSQvBoJFtRbbecKucYqJgoMopqUmm0IxFYGY,6071
6
- code_puppy/cli_runner.py,sha256=M3UlDgH4h8IPCfxwadElztGKhaNe47FNZJtxtcoVbBg,29875
6
+ code_puppy/cli_runner.py,sha256=VkDaANmywBNoj6wCzaLlI1Z2yFC98U4BqHmSIcV4HCw,32150
7
7
  code_puppy/config.py,sha256=qqeJrQP7gqADqeYqVzfksP7NYGROLrBQCuYic5PuQfY,52295
8
8
  code_puppy/error_logging.py,sha256=a80OILCUtJhexI6a9GM-r5LqIdjvSRzggfgPp2jv1X0,3297
9
9
  code_puppy/gemini_code_assist.py,sha256=KGS7sO5OLc83nDF3xxS-QiU6vxW9vcm6hmzilu79Ef8,13867
10
10
  code_puppy/http_utils.py,sha256=w5mWYIGIWJZJvgvMahXs9BmdidoJvGn4CASDRY88a8o,13414
11
- code_puppy/keymap.py,sha256=Uzvq7HB-6inTjKox-90JWzuijztRdWqhJpfTDZVy5no,3235
11
+ code_puppy/keymap.py,sha256=IvMkTlB_bIqOWpbTpmftkdyjhtD5todXuEIw1zCZ4u0,3584
12
12
  code_puppy/main.py,sha256=82r3vZy_XcyEsenLn82BnUusaoyL3Bpm_Th_jKgqecE,273
13
13
  code_puppy/model_factory.py,sha256=H_a5nX462Q-dhX3g3ZY7dmBCIAUOd1aOSZa4HMxF1o4,34191
14
14
  code_puppy/model_utils.py,sha256=NU8W8NW5F7QS_PXHaLeh55Air1koUV7IVYFP7Rz3XpY,3615
@@ -21,7 +21,8 @@ code_puppy/round_robin_model.py,sha256=kSawwPUiPgg0yg8r4AAVgvjzsWkptxpSORd75-HP7
21
21
  code_puppy/session_storage.py,sha256=T4hOsAl9z0yz2JZCptjJBOnN8fCmkLZx5eLy1hTdv6Q,9631
22
22
  code_puppy/status_display.py,sha256=qHzIQGAPEa2_-4gQSg7_rE1ihOosBq8WO73MWFNmmlo,8938
23
23
  code_puppy/summarization_agent.py,sha256=6Pu_Wp_rF-HAhoX9u2uXTabRVkOZUYwRoMP1lzNS4ew,4485
24
- code_puppy/terminal_utils.py,sha256=P6ProVD_3nHW0zHNRIMWTcg8HvBk9e0ir5bhUgJpT5k,3921
24
+ code_puppy/terminal_utils.py,sha256=CxcNLfPwTDblI0AEtEwhfZ4DTfqqwHjM_A10QslaMBk,8220
25
+ code_puppy/uvx_detection.py,sha256=tP9X9Nvzow--KIqtqjgrHQkSxMJ3EevfoaeoB9VLY2o,7224
25
26
  code_puppy/version_checker.py,sha256=aq2Mwxl1CR9sEFBgrPt3OQOowLOBUp9VaQYWJhuUv8Q,1780
26
27
  code_puppy/agents/__init__.py,sha256=PtPB7Z5MSwmUKipgt_qxvIuGggcuVaYwNbnp1UP4tPc,518
27
28
  code_puppy/agents/agent_c_reviewer.py,sha256=1kO_89hcrhlS4sJ6elDLSEx-h43jAaWGgvIL0SZUuKo,8214
@@ -39,7 +40,7 @@ code_puppy/agents/agent_qa_expert.py,sha256=5Ikb4U3SZQknUEfwlHZiyZXKqnffnOTQagr_
39
40
  code_puppy/agents/agent_qa_kitten.py,sha256=5PeFFSwCFlTUvP6h5bGntx0xv5NmRwBiw0HnMqY8nLI,9107
40
41
  code_puppy/agents/agent_security_auditor.py,sha256=SpiYNA0XAsIwBj7S2_EQPRslRUmF_-b89pIJyW7DYtY,12022
41
42
  code_puppy/agents/agent_typescript_reviewer.py,sha256=vsnpp98xg6cIoFAEJrRTUM_i4wLEWGm5nJxs6fhHobM,10275
42
- code_puppy/agents/base_agent.py,sha256=32nNAGnBMUXLrI-5qTGpEWNEGW_gIN1ELQP1eH_lBCo,84509
43
+ code_puppy/agents/base_agent.py,sha256=r_znuUZJMv97Lh8zeSdS_KJzVGe7X3rAgBk3NZpIO7I,82855
43
44
  code_puppy/agents/json_agent.py,sha256=lhopDJDoiSGHvD8A6t50hi9ZBoNRKgUywfxd0Po_Dzc,4886
44
45
  code_puppy/agents/prompt_reviewer.py,sha256=JJrJ0m5q0Puxl8vFsyhAbY9ftU9n6c6UxEVdNct1E-Q,5558
45
46
  code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
@@ -144,7 +145,7 @@ code_puppy/plugins/shell_safety/register_callbacks.py,sha256=W3v664RR48Fdbbbltf_
144
145
  code_puppy/prompts/codex_system_prompt.md,sha256=hEFTCziroLqZmqNle5kG34A8kvTteOWezCiVrAEKhE0,24400
145
146
  code_puppy/tools/__init__.py,sha256=BVTZ85jLHgDANwOnUSOz3UDlp8VQDq4DoGF23BRlyWw,6032
146
147
  code_puppy/tools/agent_tools.py,sha256=snBI6FlFtR03CbYKXwu53R48c_fRSuDIwcNdVUruLcA,21020
147
- code_puppy/tools/command_runner.py,sha256=miofNbQ0B62B_iaBpa4ZiifaUk-kgGWWGHWOW-CRzaA,47524
148
+ code_puppy/tools/command_runner.py,sha256=WLesijwbXEsnyuIJvWZHbVVyoUAPQcTWJbz31pXPSi0,44325
148
149
  code_puppy/tools/common.py,sha256=IboS6sbwN4a3FzHdfsZJtEFiyDUCszevI6LpH14ydEk,40561
149
150
  code_puppy/tools/file_modifications.py,sha256=vz9n7R0AGDSdLUArZr_55yJLkyI30M8zreAppxIx02M,29380
150
151
  code_puppy/tools/file_operations.py,sha256=CqhpuBnOFOcQCIYXOujskxq2VMLWYJhibYrH0YcPSfA,35692
@@ -159,10 +160,10 @@ code_puppy/tools/browser/browser_scripts.py,sha256=sNb8eLEyzhasy5hV4B9OjM8yIVMLV
159
160
  code_puppy/tools/browser/browser_workflows.py,sha256=nitW42vCf0ieTX1gLabozTugNQ8phtoFzZbiAhw1V90,6491
160
161
  code_puppy/tools/browser/camoufox_manager.py,sha256=RZjGOEftE5sI_tsercUyXFSZI2wpStXf-q0PdYh2G3I,8680
161
162
  code_puppy/tools/browser/vqa_agent.py,sha256=DBn9HKloILqJSTSdNZzH_PYWT0B2h9VwmY6akFQI_uU,2913
162
- code_puppy-0.0.332.data/data/code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
163
- code_puppy-0.0.332.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
164
- code_puppy-0.0.332.dist-info/METADATA,sha256=_5P2HAkEUyhzQqQu0AxUJpB8Nn9-8tD4KtbB3m2Ei_4,28854
165
- code_puppy-0.0.332.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
166
- code_puppy-0.0.332.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
167
- code_puppy-0.0.332.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
168
- code_puppy-0.0.332.dist-info/RECORD,,
163
+ code_puppy-0.0.333.data/data/code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
164
+ code_puppy-0.0.333.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
165
+ code_puppy-0.0.333.dist-info/METADATA,sha256=VP7p42x7DQGz8JDM0nIF7MXuY41YkD0wIxBINx4334w,28854
166
+ code_puppy-0.0.333.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
167
+ code_puppy-0.0.333.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
168
+ code_puppy-0.0.333.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
169
+ code_puppy-0.0.333.dist-info/RECORD,,