code-puppy 0.0.336__py3-none-any.whl → 0.0.338__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.
@@ -1353,7 +1353,6 @@ class BaseAgent(ABC):
1353
1353
  ToolCallPartDelta,
1354
1354
  )
1355
1355
  from rich.console import Console
1356
- from rich.markdown import Markdown
1357
1356
  from rich.markup import escape
1358
1357
 
1359
1358
  from code_puppy.messaging.spinner import pause_all_spinners
@@ -1375,10 +1374,17 @@ class BaseAgent(ABC):
1375
1374
  text_parts: set[int] = set() # Track which parts are text
1376
1375
  tool_parts: set[int] = set() # Track which parts are tool calls
1377
1376
  banner_printed: set[int] = set() # Track if banner was already printed
1378
- text_buffer: dict[int, list[str]] = {} # Buffer text for final markdown render
1379
1377
  token_count: dict[int, int] = {} # Track token count per text/tool part
1380
1378
  did_stream_anything = False # Track if we streamed any content
1381
1379
 
1380
+ # Termflow streaming state for text parts
1381
+ from termflow import Parser as TermflowParser
1382
+ from termflow import Renderer as TermflowRenderer
1383
+
1384
+ termflow_parsers: dict[int, TermflowParser] = {}
1385
+ termflow_renderers: dict[int, TermflowRenderer] = {}
1386
+ termflow_line_buffers: dict[int, str] = {} # Buffer incomplete lines
1387
+
1382
1388
  def _print_thinking_banner() -> None:
1383
1389
  """Print the THINKING banner with spinner pause and line clear."""
1384
1390
  nonlocal did_stream_anything
@@ -1437,13 +1443,17 @@ class BaseAgent(ABC):
1437
1443
  elif isinstance(part, TextPart):
1438
1444
  streaming_parts.add(event.index)
1439
1445
  text_parts.add(event.index)
1440
- text_buffer[event.index] = [] # Initialize buffer
1441
- token_count[event.index] = 0 # Initialize token counter
1442
- # Buffer initial content if present
1446
+ # Initialize termflow streaming for this text part
1447
+ termflow_parsers[event.index] = TermflowParser()
1448
+ termflow_renderers[event.index] = TermflowRenderer(
1449
+ output=console.file, width=console.width
1450
+ )
1451
+ termflow_line_buffers[event.index] = ""
1452
+ # Handle initial content if present
1443
1453
  if part.content and part.content.strip():
1444
- text_buffer[event.index].append(part.content)
1445
- # Count chunks (each part counts as 1)
1446
- token_count[event.index] += 1
1454
+ _print_response_banner()
1455
+ banner_printed.add(event.index)
1456
+ termflow_line_buffers[event.index] = part.content
1447
1457
  elif isinstance(part, ToolCallPart):
1448
1458
  streaming_parts.add(event.index)
1449
1459
  tool_parts.add(event.index)
@@ -1459,22 +1469,29 @@ class BaseAgent(ABC):
1459
1469
  delta = event.delta
1460
1470
  if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
1461
1471
  if delta.content_delta:
1462
- # For text parts, show token counter then render at end
1472
+ # For text parts, stream markdown with termflow
1463
1473
  if event.index in text_parts:
1464
1474
  # Print banner on first content
1465
1475
  if event.index not in banner_printed:
1466
1476
  _print_response_banner()
1467
1477
  banner_printed.add(event.index)
1468
- # Accumulate text for final markdown render
1469
- text_buffer[event.index].append(delta.content_delta)
1470
- # Count chunks received
1471
- token_count[event.index] += 1
1472
- # Update chunk counter in place (single line)
1473
- count = token_count[event.index]
1474
- console.print(
1475
- f" ⏳ Receiving... {count} chunks ",
1476
- end="\r",
1478
+
1479
+ # Add content to line buffer
1480
+ termflow_line_buffers[event.index] += (
1481
+ delta.content_delta
1477
1482
  )
1483
+
1484
+ # Process complete lines
1485
+ parser = termflow_parsers[event.index]
1486
+ renderer = termflow_renderers[event.index]
1487
+ buffer = termflow_line_buffers[event.index]
1488
+
1489
+ while "\n" in buffer:
1490
+ line, buffer = buffer.split("\n", 1)
1491
+ events_to_render = parser.parse_line(line)
1492
+ renderer.render_all(events_to_render)
1493
+
1494
+ termflow_line_buffers[event.index] = buffer
1478
1495
  else:
1479
1496
  # For thinking parts, stream immediately (dim)
1480
1497
  if event.index not in banner_printed:
@@ -1503,19 +1520,27 @@ class BaseAgent(ABC):
1503
1520
  # PartEndEvent - finish the streaming with a newline
1504
1521
  elif isinstance(event, PartEndEvent):
1505
1522
  if event.index in streaming_parts:
1506
- # For text parts, clear counter line and render markdown
1523
+ # For text parts, finalize termflow rendering
1507
1524
  if event.index in text_parts:
1508
- # Clear the chunk counter line by printing spaces and returning
1509
- console.print(" " * 50, end="\r")
1510
- # Render the final markdown nicely
1511
- if event.index in text_buffer:
1512
- try:
1513
- final_content = "".join(text_buffer[event.index])
1514
- if final_content.strip():
1515
- console.print(Markdown(final_content))
1516
- except Exception:
1517
- pass
1518
- del text_buffer[event.index]
1525
+ # Render any remaining buffered content
1526
+ if event.index in termflow_parsers:
1527
+ parser = termflow_parsers[event.index]
1528
+ renderer = termflow_renderers[event.index]
1529
+ remaining = termflow_line_buffers.get(event.index, "")
1530
+
1531
+ # Parse and render any remaining partial line
1532
+ if remaining.strip():
1533
+ events_to_render = parser.parse_line(remaining)
1534
+ renderer.render_all(events_to_render)
1535
+
1536
+ # Finalize the parser to close any open blocks
1537
+ final_events = parser.finalize()
1538
+ renderer.render_all(final_events)
1539
+
1540
+ # Clean up termflow state
1541
+ del termflow_parsers[event.index]
1542
+ del termflow_renderers[event.index]
1543
+ del termflow_line_buffers[event.index]
1519
1544
  # For tool parts, clear the chunk counter line
1520
1545
  elif event.index in tool_parts:
1521
1546
  # Clear the chunk counter line by printing spaces and returning
@@ -10,7 +10,7 @@ serialization, avoiding httpx/Pydantic internals.
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
- from typing import Any, Callable
13
+ from typing import Any, Callable, MutableMapping
14
14
 
15
15
  import httpx
16
16
 
@@ -56,7 +56,28 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
56
56
  except Exception:
57
57
  # Swallow wrapper errors; do not break real calls.
58
58
  pass
59
- return await super().send(request, *args, **kwargs)
59
+ response = await super().send(request, *args, **kwargs)
60
+ try:
61
+ if response.status_code == 401 and not request.extensions.get(
62
+ "claude_oauth_refresh_attempted"
63
+ ):
64
+ refreshed_token = self._refresh_claude_oauth_token()
65
+ if refreshed_token:
66
+ await response.aclose()
67
+ body_bytes = self._extract_body_bytes(request)
68
+ headers = dict(request.headers)
69
+ self._update_auth_headers(headers, refreshed_token)
70
+ retry_request = self.build_request(
71
+ method=request.method,
72
+ url=request.url,
73
+ headers=headers,
74
+ content=body_bytes,
75
+ )
76
+ retry_request.extensions["claude_oauth_refresh_attempted"] = True
77
+ return await super().send(retry_request, *args, **kwargs)
78
+ except Exception:
79
+ pass
80
+ return response
60
81
 
61
82
  @staticmethod
62
83
  def _extract_body_bytes(request: httpx.Request) -> bytes | None:
@@ -78,6 +99,29 @@ class ClaudeCacheAsyncClient(httpx.AsyncClient):
78
99
 
79
100
  return None
80
101
 
102
+ @staticmethod
103
+ def _update_auth_headers(
104
+ headers: MutableMapping[str, str], access_token: str
105
+ ) -> None:
106
+ bearer_value = f"Bearer {access_token}"
107
+ if "Authorization" in headers or "authorization" in headers:
108
+ headers["Authorization"] = bearer_value
109
+ elif "x-api-key" in headers or "X-API-Key" in headers:
110
+ headers["x-api-key"] = access_token
111
+ else:
112
+ headers["Authorization"] = bearer_value
113
+
114
+ def _refresh_claude_oauth_token(self) -> str | None:
115
+ try:
116
+ from code_puppy.plugins.claude_code_oauth.utils import refresh_access_token
117
+
118
+ refreshed_token = refresh_access_token(force=True)
119
+ if refreshed_token:
120
+ self._update_auth_headers(self.headers, refreshed_token)
121
+ return refreshed_token
122
+ except Exception:
123
+ return None
124
+
81
125
  @staticmethod
82
126
  def _inject_cache_control(body: bytes) -> bytes | None:
83
127
  try:
code_puppy/cli_runner.py CHANGED
@@ -17,10 +17,7 @@ import traceback
17
17
  from pathlib import Path
18
18
 
19
19
  from dbos import DBOS, DBOSConfig
20
- from rich.console import Console, ConsoleOptions, RenderResult
21
- from rich.markdown import CodeBlock, Markdown
22
- from rich.syntax import Syntax
23
- from rich.text import Text
20
+ from rich.console import Console
24
21
 
25
22
  from code_puppy import __version__, callbacks, plugins
26
23
  from code_puppy.agents import get_current_agent
@@ -43,6 +40,7 @@ from code_puppy.keymap import (
43
40
  )
44
41
  from code_puppy.messaging import emit_info
45
42
  from code_puppy.terminal_utils import (
43
+ print_truecolor_warning,
46
44
  reset_unix_terminal,
47
45
  reset_windows_terminal_ansi,
48
46
  reset_windows_terminal_full,
@@ -91,7 +89,6 @@ async def main():
91
89
  "command", nargs="*", help="Run a single command (deprecated, use -p instead)"
92
90
  )
93
91
  args = parser.parse_args()
94
- from rich.console import Console
95
92
 
96
93
  from code_puppy.messaging import (
97
94
  RichConsoleRenderer,
@@ -146,6 +143,9 @@ async def main():
146
143
  except ImportError:
147
144
  emit_system_message("🐶 Code Puppy is Loading...")
148
145
 
146
+ # Check for truecolor support and warn if not available
147
+ print_truecolor_warning(display_console)
148
+
149
149
  available_port = find_available_port()
150
150
  if available_port is None:
151
151
  emit_error("No available ports in range 8090-9010!")
@@ -679,8 +679,6 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
679
679
  save_command_to_history(task)
680
680
 
681
681
  try:
682
- prettier_code_blocks()
683
-
684
682
  # No need to get agent directly - use manager's run methods
685
683
 
686
684
  # Use our custom helper to enable attachment handling with spinner support
@@ -750,28 +748,6 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
750
748
  pass
751
749
 
752
750
 
753
- def prettier_code_blocks():
754
- """Configure Rich to use prettier code block rendering."""
755
-
756
- class SimpleCodeBlock(CodeBlock):
757
- def __rich_console__(
758
- self, console: Console, options: ConsoleOptions
759
- ) -> RenderResult:
760
- code = str(self.text).rstrip()
761
- yield Text(self.lexer_name, style="dim")
762
- syntax = Syntax(
763
- code,
764
- self.lexer_name,
765
- theme=self.theme,
766
- background_color="default",
767
- line_numbers=True,
768
- )
769
- yield syntax
770
- yield Text(f"/{self.lexer_name}", style="dim")
771
-
772
- Markdown.elements["fence"] = SimpleCodeBlock
773
-
774
-
775
751
  async def run_prompt_with_attachments(
776
752
  agent,
777
753
  raw_prompt: str,
code_puppy/http_utils.py CHANGED
@@ -5,10 +5,10 @@ This module provides functions for creating properly configured HTTP clients.
5
5
  """
6
6
 
7
7
  import asyncio
8
- import logging
9
8
  import os
10
9
  import socket
11
10
  import time
11
+ from dataclasses import dataclass
12
12
  from typing import Any, Dict, Optional, Union
13
13
 
14
14
  import httpx
@@ -16,7 +16,69 @@ import requests
16
16
 
17
17
  from code_puppy.config import get_http2
18
18
 
19
- logger = logging.getLogger(__name__)
19
+
20
+ @dataclass
21
+ class ProxyConfig:
22
+ """Configuration for proxy and SSL settings."""
23
+
24
+ verify: Union[bool, str, None]
25
+ trust_env: bool
26
+ proxy_url: str | None
27
+ disable_retry: bool
28
+ http2_enabled: bool
29
+
30
+
31
+ def _resolve_proxy_config(verify: Union[bool, str, None] = None) -> ProxyConfig:
32
+ """Resolve proxy, SSL, and retry settings from environment.
33
+
34
+ This centralizes the logic for detecting proxies, determining SSL verification,
35
+ and checking if retry transport should be disabled.
36
+ """
37
+ if verify is None:
38
+ verify = get_cert_bundle_path()
39
+
40
+ http2_enabled = get_http2()
41
+
42
+ disable_retry = os.environ.get(
43
+ "CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
44
+ ).lower() in ("1", "true", "yes")
45
+
46
+ has_proxy = bool(
47
+ os.environ.get("HTTP_PROXY")
48
+ or os.environ.get("HTTPS_PROXY")
49
+ or os.environ.get("http_proxy")
50
+ or os.environ.get("https_proxy")
51
+ )
52
+
53
+ # Determine trust_env and verify based on proxy/retry settings
54
+ if disable_retry:
55
+ # Test mode: disable SSL verification for proxy testing
56
+ verify = False
57
+ trust_env = True
58
+ elif has_proxy:
59
+ # Production proxy: keep SSL verification enabled
60
+ trust_env = True
61
+ else:
62
+ trust_env = False
63
+
64
+ # Extract proxy URL
65
+ proxy_url = None
66
+ if has_proxy:
67
+ proxy_url = (
68
+ os.environ.get("HTTPS_PROXY")
69
+ or os.environ.get("https_proxy")
70
+ or os.environ.get("HTTP_PROXY")
71
+ or os.environ.get("http_proxy")
72
+ )
73
+
74
+ return ProxyConfig(
75
+ verify=verify,
76
+ trust_env=trust_env,
77
+ proxy_url=proxy_url,
78
+ disable_retry=disable_retry,
79
+ http2_enabled=http2_enabled,
80
+ )
81
+
20
82
 
21
83
  try:
22
84
  from .reopenable_async_client import ReopenableAsyncClient
@@ -58,14 +120,7 @@ class RetryingAsyncClient(httpx.AsyncClient):
58
120
 
59
121
  for attempt in range(self.max_retries + 1):
60
122
  try:
61
- # Clone request for retry (streams might be consumed)
62
- # But only if it's not the first attempt
63
- req_to_send = request
64
- if attempt > 0:
65
- # httpx requests are reusable, but we need to be careful with streams
66
- pass
67
-
68
- response = await super().send(req_to_send, **kwargs)
123
+ response = await super().send(request, **kwargs)
69
124
  last_response = response
70
125
 
71
126
  # Check for retryable status
@@ -128,7 +183,7 @@ class RetryingAsyncClient(httpx.AsyncClient):
128
183
  return last_response
129
184
 
130
185
 
131
- def get_cert_bundle_path() -> str:
186
+ def get_cert_bundle_path() -> str | None:
132
187
  # First check if SSL_CERT_FILE environment variable is set
133
188
  ssl_cert_file = os.environ.get("SSL_CERT_FILE")
134
189
  if ssl_cert_file and os.path.exists(ssl_cert_file):
@@ -164,66 +219,26 @@ def create_async_client(
164
219
  headers: Optional[Dict[str, str]] = None,
165
220
  retry_status_codes: tuple = (429, 502, 503, 504),
166
221
  ) -> httpx.AsyncClient:
167
- if verify is None:
168
- verify = get_cert_bundle_path()
169
-
170
- # Check if HTTP/2 is enabled in config
171
- http2_enabled = get_http2()
172
-
173
- # Check if custom retry transport should be disabled (e.g., for integration tests with proxies)
174
- disable_retry_transport = os.environ.get(
175
- "CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
176
- ).lower() in ("1", "true", "yes")
177
-
178
- # Check if proxy environment variables are set
179
- has_proxy = bool(
180
- os.environ.get("HTTP_PROXY")
181
- or os.environ.get("HTTPS_PROXY")
182
- or os.environ.get("http_proxy")
183
- or os.environ.get("https_proxy")
184
- )
185
-
186
- # When retry transport is disabled (test mode), disable SSL verification
187
- # for proxy testing. For production proxies, SSL should still be verified!
188
- if disable_retry_transport:
189
- verify = False
190
- trust_env = True
191
- elif has_proxy:
192
- # Production proxy detected - keep SSL verification enabled for security
193
- trust_env = True
194
- else:
195
- trust_env = False
196
-
197
- # Extract proxy URL if needed
198
- proxy_url = None
199
- if has_proxy:
200
- proxy_url = (
201
- os.environ.get("HTTPS_PROXY")
202
- or os.environ.get("https_proxy")
203
- or os.environ.get("HTTP_PROXY")
204
- or os.environ.get("http_proxy")
205
- )
222
+ config = _resolve_proxy_config(verify)
206
223
 
207
- # Use RetryingAsyncClient if retries are enabled
208
- if not disable_retry_transport:
224
+ if not config.disable_retry:
209
225
  return RetryingAsyncClient(
210
226
  retry_status_codes=retry_status_codes,
211
- proxy=proxy_url,
212
- verify=verify,
227
+ proxy=config.proxy_url,
228
+ verify=config.verify,
213
229
  headers=headers or {},
214
230
  timeout=timeout,
215
- http2=http2_enabled,
216
- trust_env=trust_env,
231
+ http2=config.http2_enabled,
232
+ trust_env=config.trust_env,
217
233
  )
218
234
  else:
219
- # Regular client for testing
220
235
  return httpx.AsyncClient(
221
- proxy=proxy_url,
222
- verify=verify,
236
+ proxy=config.proxy_url,
237
+ verify=config.verify,
223
238
  headers=headers or {},
224
239
  timeout=timeout,
225
- http2=http2_enabled,
226
- trust_env=trust_env,
240
+ http2=config.http2_enabled,
241
+ trust_env=config.trust_env,
227
242
  )
228
243
 
229
244
 
@@ -273,85 +288,33 @@ def create_reopenable_async_client(
273
288
  headers: Optional[Dict[str, str]] = None,
274
289
  retry_status_codes: tuple = (429, 502, 503, 504),
275
290
  ) -> Union[ReopenableAsyncClient, httpx.AsyncClient]:
276
- if verify is None:
277
- verify = get_cert_bundle_path()
278
-
279
- # Check if HTTP/2 is enabled in config
280
- http2_enabled = get_http2()
281
-
282
- # Check if custom retry transport should be disabled (e.g., for integration tests with proxies)
283
- disable_retry_transport = os.environ.get(
284
- "CODE_PUPPY_DISABLE_RETRY_TRANSPORT", ""
285
- ).lower() in ("1", "true", "yes")
286
-
287
- # Check if proxy environment variables are set
288
- has_proxy = bool(
289
- os.environ.get("HTTP_PROXY")
290
- or os.environ.get("HTTPS_PROXY")
291
- or os.environ.get("http_proxy")
292
- or os.environ.get("https_proxy")
293
- )
294
-
295
- # When retry transport is disabled (test mode), disable SSL verification
296
- if disable_retry_transport:
297
- verify = False
298
- trust_env = True
299
- elif has_proxy:
300
- trust_env = True
301
- else:
302
- trust_env = False
291
+ config = _resolve_proxy_config(verify)
303
292
 
304
- # Extract proxy URL if needed
305
- proxy_url = None
306
- if has_proxy:
307
- proxy_url = (
308
- os.environ.get("HTTPS_PROXY")
309
- or os.environ.get("https_proxy")
310
- or os.environ.get("HTTP_PROXY")
311
- or os.environ.get("http_proxy")
312
- )
293
+ base_kwargs = {
294
+ "proxy": config.proxy_url,
295
+ "verify": config.verify,
296
+ "headers": headers or {},
297
+ "timeout": timeout,
298
+ "http2": config.http2_enabled,
299
+ "trust_env": config.trust_env,
300
+ }
313
301
 
314
302
  if ReopenableAsyncClient is not None:
315
- # Use RetryingAsyncClient if retries are enabled
316
303
  client_class = (
317
- RetryingAsyncClient if not disable_retry_transport else httpx.AsyncClient
304
+ RetryingAsyncClient if not config.disable_retry else httpx.AsyncClient
318
305
  )
319
-
320
- # Pass retry config only if using RetryingAsyncClient
321
- kwargs = {
322
- "proxy": proxy_url,
323
- "verify": verify,
324
- "headers": headers or {},
325
- "timeout": timeout,
326
- "http2": http2_enabled,
327
- "trust_env": trust_env,
328
- }
329
-
330
- if not disable_retry_transport:
306
+ kwargs = {**base_kwargs, "client_class": client_class}
307
+ if not config.disable_retry:
331
308
  kwargs["retry_status_codes"] = retry_status_codes
332
-
333
- return ReopenableAsyncClient(client_class=client_class, **kwargs)
309
+ return ReopenableAsyncClient(**kwargs)
334
310
  else:
335
- # Fallback to RetryingAsyncClient
336
- if not disable_retry_transport:
311
+ # Fallback to RetryingAsyncClient or plain AsyncClient
312
+ if not config.disable_retry:
337
313
  return RetryingAsyncClient(
338
- retry_status_codes=retry_status_codes,
339
- proxy=proxy_url,
340
- verify=verify,
341
- headers=headers or {},
342
- timeout=timeout,
343
- http2=http2_enabled,
344
- trust_env=trust_env,
314
+ retry_status_codes=retry_status_codes, **base_kwargs
345
315
  )
346
316
  else:
347
- return httpx.AsyncClient(
348
- proxy=proxy_url,
349
- verify=verify,
350
- headers=headers or {},
351
- timeout=timeout,
352
- http2=http2_enabled,
353
- trust_env=trust_env,
354
- )
317
+ return httpx.AsyncClient(**base_kwargs)
355
318
 
356
319
 
357
320
  def is_cert_bundle_available() -> bool:
@@ -209,6 +209,9 @@ class ShellStartMessage(BaseMessage):
209
209
  default=None, description="Working directory for the command"
210
210
  )
211
211
  timeout: int = Field(default=60, description="Timeout in seconds")
212
+ background: bool = Field(
213
+ default=False, description="Whether command runs in background mode"
214
+ )
212
215
 
213
216
 
214
217
  class ShellLineMessage(BaseMessage):
@@ -620,15 +620,25 @@ class RichConsoleRenderer:
620
620
  safe_command = escape_rich_markup(msg.command)
621
621
  # Header showing command is starting
622
622
  banner = self._format_banner("shell_command", "SHELL COMMAND")
623
- self._console.print(f"\n{banner} 🚀 [dim]$ {safe_command}[/dim]")
623
+
624
+ # Add background indicator if running in background mode
625
+ if msg.background:
626
+ self._console.print(
627
+ f"\n{banner} 🚀 [dim]$ {safe_command}[/dim] [bold magenta][BACKGROUND 🌙][/bold magenta]"
628
+ )
629
+ else:
630
+ self._console.print(f"\n{banner} 🚀 [dim]$ {safe_command}[/dim]")
624
631
 
625
632
  # Show working directory if specified
626
633
  if msg.cwd:
627
634
  safe_cwd = escape_rich_markup(msg.cwd)
628
635
  self._console.print(f"[dim]📂 Working directory: {safe_cwd}[/dim]")
629
636
 
630
- # Show timeout
631
- self._console.print(f"[dim]⏱ Timeout: {msg.timeout}s[/dim]")
637
+ # Show timeout or background status
638
+ if msg.background:
639
+ self._console.print("[dim]⏱ Runs detached (no timeout)[/dim]")
640
+ else:
641
+ self._console.print(f"[dim]⏱ Timeout: {msg.timeout}s[/dim]")
632
642
 
633
643
  def _render_shell_line(self, msg: ShellLineMessage) -> None:
634
644
  """Render shell output line preserving ANSI codes."""
@@ -388,6 +388,20 @@ class ModelFactory:
388
388
  return AnthropicModel(model_name=model_config["name"], provider=provider)
389
389
  elif model_type == "claude_code":
390
390
  url, headers, verify, api_key = get_custom_config(model_config)
391
+ if model_config.get("oauth_source") == "claude-code-plugin":
392
+ try:
393
+ from code_puppy.plugins.claude_code_oauth.utils import (
394
+ get_valid_access_token,
395
+ )
396
+
397
+ refreshed_token = get_valid_access_token()
398
+ if refreshed_token:
399
+ api_key = refreshed_token
400
+ custom_endpoint = model_config.get("custom_endpoint")
401
+ if isinstance(custom_endpoint, dict):
402
+ custom_endpoint["api_key"] = refreshed_token
403
+ except ImportError:
404
+ pass
391
405
  if not api_key:
392
406
  emit_warning(
393
407
  f"API key is not set for Claude Code endpoint; skipping model '{model_config.get('name')}'."
@@ -663,6 +677,8 @@ class ModelFactory:
663
677
  f"API key is not set for Cerebras endpoint; skipping model '{model_config.get('name')}'."
664
678
  )
665
679
  return None
680
+ # Add Cerebras 3rd party integration header
681
+ headers["X-Cerebras-3rd-Party-Integration"] = "code-puppy"
666
682
  client = create_async_client(headers=headers, verify=verify)
667
683
  provider_args = dict(
668
684
  api_key=api_key,
code_puppy/models.json CHANGED
@@ -55,9 +55,9 @@
55
55
  "supported_settings": ["reasoning_effort", "verbosity"],
56
56
  "supports_xhigh_reasoning": true
57
57
  },
58
- "Cerebras-GLM-4.6": {
58
+ "Cerebras-GLM-4.7": {
59
59
  "type": "cerebras",
60
- "name": "zai-glm-4.6",
60
+ "name": "zai-glm-4.7",
61
61
  "custom_endpoint": {
62
62
  "url": "https://api.cerebras.ai/v1",
63
63
  "api_key": "$CEREBRAS_API_KEY"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import json
4
5
  import logging
5
6
  from collections.abc import AsyncIterator
@@ -75,7 +76,16 @@ class AntigravityModel(GoogleModel):
75
76
  system_parts.append({"text": part.content})
76
77
  elif isinstance(part, UserPromptPart):
77
78
  # Use parent's _map_user_prompt
78
- message_parts.extend(await self._map_user_prompt(part))
79
+ mapped_parts = await self._map_user_prompt(part)
80
+ # Sanitize bytes to base64 for JSON serialization
81
+ for mp in mapped_parts:
82
+ if "inline_data" in mp and "data" in mp["inline_data"]:
83
+ data = mp["inline_data"]["data"]
84
+ if isinstance(data, bytes):
85
+ mp["inline_data"]["data"] = base64.b64encode(
86
+ data
87
+ ).decode("utf-8")
88
+ message_parts.extend(mapped_parts)
79
89
  elif isinstance(part, ToolReturnPart):
80
90
  message_parts.append(
81
91
  {
@@ -542,8 +552,13 @@ def _antigravity_content_model_response(
542
552
 
543
553
  elif isinstance(item, FilePart):
544
554
  content = item.content
555
+ # Ensure data is base64 string, not bytes
556
+ data_val = content.data
557
+ if isinstance(data_val, bytes):
558
+ data_val = base64.b64encode(data_val).decode("utf-8")
559
+
545
560
  inline_data_dict: BlobDict = {
546
- "data": content.data,
561
+ "data": data_val,
547
562
  "mime_type": content.media_type,
548
563
  }
549
564
  part["inline_data"] = inline_data_dict
@@ -21,6 +21,8 @@ from .config import (
21
21
  get_token_storage_path,
22
22
  )
23
23
 
24
+ TOKEN_REFRESH_BUFFER_SECONDS = 60
25
+
24
26
  logger = logging.getLogger(__name__)
25
27
 
26
28
 
@@ -132,6 +134,124 @@ def load_stored_tokens() -> Optional[Dict[str, Any]]:
132
134
  return None
133
135
 
134
136
 
137
+ def _calculate_expires_at(expires_in: Optional[float]) -> Optional[float]:
138
+ if expires_in is None:
139
+ return None
140
+ try:
141
+ return time.time() + float(expires_in)
142
+ except (TypeError, ValueError):
143
+ return None
144
+
145
+
146
+ def is_token_expired(tokens: Dict[str, Any]) -> bool:
147
+ expires_at = tokens.get("expires_at")
148
+ if expires_at is None:
149
+ return False
150
+ try:
151
+ expires_at_value = float(expires_at)
152
+ except (TypeError, ValueError):
153
+ return False
154
+ return time.time() >= expires_at_value - TOKEN_REFRESH_BUFFER_SECONDS
155
+
156
+
157
+ def update_claude_code_model_tokens(access_token: str) -> bool:
158
+ try:
159
+ claude_models = load_claude_models()
160
+ if not claude_models:
161
+ return False
162
+
163
+ updated = False
164
+ for config in claude_models.values():
165
+ if config.get("oauth_source") != "claude-code-plugin":
166
+ continue
167
+ custom_endpoint = config.get("custom_endpoint")
168
+ if not isinstance(custom_endpoint, dict):
169
+ continue
170
+ custom_endpoint["api_key"] = access_token
171
+ updated = True
172
+
173
+ if updated:
174
+ return save_claude_models(claude_models)
175
+ except Exception as exc: # pragma: no cover - defensive logging
176
+ logger.error("Failed to update Claude model tokens: %s", exc)
177
+ return False
178
+
179
+
180
+ def refresh_access_token(force: bool = False) -> Optional[str]:
181
+ tokens = load_stored_tokens()
182
+ if not tokens:
183
+ return None
184
+
185
+ if not force and not is_token_expired(tokens):
186
+ return tokens.get("access_token")
187
+
188
+ refresh_token = tokens.get("refresh_token")
189
+ if not refresh_token:
190
+ logger.debug("No refresh_token available")
191
+ return None
192
+
193
+ payload = {
194
+ "grant_type": "refresh_token",
195
+ "client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
196
+ "refresh_token": refresh_token,
197
+ }
198
+
199
+ headers = {
200
+ "Content-Type": "application/json",
201
+ "Accept": "application/json",
202
+ "anthropic-beta": "oauth-2025-04-20",
203
+ }
204
+
205
+ try:
206
+ response = requests.post(
207
+ CLAUDE_CODE_OAUTH_CONFIG["token_url"],
208
+ json=payload,
209
+ headers=headers,
210
+ timeout=30,
211
+ )
212
+ if response.status_code == 200:
213
+ new_tokens = response.json()
214
+ tokens["access_token"] = new_tokens.get("access_token")
215
+ tokens["refresh_token"] = new_tokens.get("refresh_token", refresh_token)
216
+ if "expires_in" in new_tokens:
217
+ tokens["expires_in"] = new_tokens["expires_in"]
218
+ tokens["expires_at"] = _calculate_expires_at(
219
+ new_tokens.get("expires_in")
220
+ )
221
+ if save_tokens(tokens):
222
+ update_claude_code_model_tokens(tokens["access_token"])
223
+ return tokens["access_token"]
224
+ else:
225
+ logger.error(
226
+ "Token refresh failed: %s - %s", response.status_code, response.text
227
+ )
228
+ except Exception as exc: # pragma: no cover - defensive logging
229
+ logger.error("Token refresh error: %s", exc)
230
+ return None
231
+
232
+
233
+ def get_valid_access_token() -> Optional[str]:
234
+ tokens = load_stored_tokens()
235
+ if not tokens:
236
+ logger.debug("No stored Claude Code OAuth tokens found")
237
+ return None
238
+
239
+ access_token = tokens.get("access_token")
240
+ if not access_token:
241
+ logger.debug("No access_token in stored tokens")
242
+ return None
243
+
244
+ if is_token_expired(tokens):
245
+ logger.info("Claude Code OAuth token expired, attempting refresh")
246
+ refreshed = refresh_access_token()
247
+ if refreshed:
248
+ return refreshed
249
+ logger.warning("Claude Code token refresh failed")
250
+ return None
251
+
252
+ return access_token
253
+
254
+
135
255
  def save_tokens(tokens: Dict[str, Any]) -> bool:
136
256
  try:
137
257
  token_path = get_token_storage_path()
@@ -243,7 +363,11 @@ def exchange_code_for_tokens(
243
363
  logger.info("Token exchange response: %s", response.status_code)
244
364
  logger.debug("Response body: %s", response.text)
245
365
  if response.status_code == 200:
246
- return response.json()
366
+ token_data = response.json()
367
+ token_data["expires_at"] = _calculate_expires_at(
368
+ token_data.get("expires_in")
369
+ )
370
+ return token_data
247
371
  logger.error(
248
372
  "Token exchange failed: %s - %s",
249
373
  response.status_code,
@@ -341,12 +465,7 @@ def add_models_to_extra_config(models: List[str]) -> bool:
341
465
  # Start fresh - overwrite the file on every auth instead of loading existing
342
466
  claude_models = {}
343
467
  added = 0
344
- tokens = load_stored_tokens()
345
-
346
- # Handle case where tokens are None or empty
347
- access_token = ""
348
- if tokens and "access_token" in tokens:
349
- access_token = tokens["access_token"]
468
+ access_token = get_valid_access_token() or ""
350
469
 
351
470
  for model_name in filtered_models:
352
471
  prefixed = f"{CLAUDE_CODE_OAUTH_CONFIG['prefix']}{model_name}"
@@ -3,10 +3,14 @@
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
9
- from typing import Callable, Optional
10
+ from typing import TYPE_CHECKING, Callable, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from rich.console import Console
10
14
 
11
15
  # Store the original console ctrl handler so we can restore it if needed
12
16
  _original_ctrl_handler: Optional[Callable] = None
@@ -289,3 +293,126 @@ def ensure_ctrl_c_disabled() -> bool:
289
293
 
290
294
  except Exception:
291
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)
@@ -880,6 +880,7 @@ async def run_shell_command(
880
880
  command=command,
881
881
  cwd=cwd,
882
882
  timeout=0, # No timeout for background processes
883
+ background=True,
883
884
  )
884
885
  )
885
886
 
@@ -55,9 +55,9 @@
55
55
  "supported_settings": ["reasoning_effort", "verbosity"],
56
56
  "supports_xhigh_reasoning": true
57
57
  },
58
- "Cerebras-GLM-4.6": {
58
+ "Cerebras-GLM-4.7": {
59
59
  "type": "cerebras",
60
- "name": "zai-glm-4.6",
60
+ "name": "zai-glm-4.7",
61
61
  "custom_endpoint": {
62
62
  "url": "https://api.cerebras.ai/v1",
63
63
  "api_key": "$CEREBRAS_API_KEY"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.336
3
+ Version: 0.0.338
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
@@ -34,6 +34,7 @@ Requires-Dist: rich>=13.4.2
34
34
  Requires-Dist: ripgrep==14.1.0
35
35
  Requires-Dist: ruff>=0.11.11
36
36
  Requires-Dist: tenacity>=8.2.0
37
+ Requires-Dist: termflow-md>=0.1.6
37
38
  Requires-Dist: uvicorn>=0.30.0
38
39
  Description-Content-Type: text/markdown
39
40
 
@@ -106,12 +107,7 @@ uvx code-puppy -i
106
107
  # Install UV if you don't have it
107
108
  curl -LsSf https://astral.sh/uv/install.sh | sh
108
109
 
109
- # Set UV to always use managed Python (one-time setup)
110
- echo 'export UV_MANAGED_PYTHON=1' >> ~/.zshrc # or ~/.bashrc
111
- source ~/.zshrc # or ~/.bashrc
112
-
113
- # Install and run code-puppy
114
- uvx code-puppy -i
110
+ uvx code-puppy
115
111
  ```
116
112
 
117
113
  #### Windows
@@ -122,73 +118,15 @@ On Windows, we recommend installing code-puppy as a global tool for the best exp
122
118
  # Install UV if you don't have it (run in PowerShell as Admin)
123
119
  powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
124
120
 
125
- # Install code-puppy as a global tool
126
- uv tool install code-puppy
127
-
128
- # Run code-puppy
129
- code-puppy -i
130
- ```
131
-
132
- **Why `uv tool install` on Windows?** Running with `uvx` creates an extra process layer that can interfere with keyboard signal handling (Ctrl+C, Ctrl+X). Installing as a tool runs code-puppy directly for reliable cancellation.
133
-
134
- #### Upgrading
135
-
136
- ```bash
137
- # Upgrade code-puppy to the latest version
138
- uv tool upgrade code-puppy
139
-
140
- # Or upgrade all installed tools
141
- uv tool upgrade --all
142
- ```
143
-
144
- UV will automatically download the latest compatible Python version (3.11+) if your system doesn't have one.
145
-
146
- ### pip (Alternative)
147
-
148
- ```bash
149
- pip install code-puppy
121
+ uvx code-puppy
150
122
  ```
151
123
 
152
- *Note: pip installation requires your system Python to be 3.11 or newer.*
153
-
154
- ### Permanent Python Management
155
-
156
- To make UV always use managed Python versions (recommended):
157
-
158
- ```bash
159
- # Set environment variable permanently
160
- echo 'export UV_MANAGED_PYTHON=1' >> ~/.zshrc # or ~/.bashrc
161
- source ~/.zshrc # or ~/.bashrc
162
-
163
- # Now all UV commands will prefer managed Python installations
164
- uvx code-puppy # No need for --managed-python flag anymore
165
- ```
124
+ ## Changelog (By Kittylog!)
166
125
 
167
- ### Verifying Python Version
168
-
169
- ```bash
170
- # Check which Python UV will use
171
- uv python find
172
-
173
- # Or check the current project's Python
174
- uv run python --version
175
- ```
126
+ [📋 View the full changelog on Kittylog](https://kittylog.app/c/mpfaffenberger/code_puppy)
176
127
 
177
128
  ## Usage
178
129
 
179
- ### Custom Commands
180
- Create markdown files in `.claude/commands/`, `.github/prompts/`, or `.agents/commands/` to define custom slash commands. The filename becomes the command name and the content runs as a prompt.
181
-
182
- ```bash
183
- # Create a custom command
184
- echo "# Code Review
185
-
186
- Please review this code for security issues." > .claude/commands/review.md
187
-
188
- # Use it in Code Puppy
189
- /review with focus on authentication
190
- ```
191
-
192
130
  ### Adding Models from models.dev 🆕
193
131
 
194
132
  While there are several models configured right out of the box from providers like Synthetic, Cerebras, OpenAI, Google, and Anthropic, Code Puppy integrates with [models.dev](https://models.dev) to let you browse and add models from **65+ providers** with a single command:
@@ -256,6 +194,18 @@ The following environment variables control DBOS behavior:
256
194
  - `DBOS_SYSTEM_DATABASE_URL`: Database URL used by DBOS. Can point to a local SQLite file or a Postgres instance. Example: `postgresql://postgres:dbos@localhost:5432/postgres`. Default: `dbos_store.sqlite` file in the config directory.
257
195
  - `DBOS_APP_VERSION`: If set, Code Puppy uses it as the [DBOS application version](https://docs.dbos.dev/architecture#application-and-workflow-versions) and automatically tries to recover pending workflows for this version. Default: Code Puppy version + Unix timestamp in millisecond (disable automatic recovery).
258
196
 
197
+ ### Custom Commands
198
+ Create markdown files in `.claude/commands/`, `.github/prompts/`, or `.agents/commands/` to define custom slash commands. The filename becomes the command name and the content runs as a prompt.
199
+
200
+ ```bash
201
+ # Create a custom command
202
+ echo "# Code Review
203
+
204
+ Please review this code for security issues." > .claude/commands/review.md
205
+
206
+ # Use it in Code Puppy
207
+ /review with focus on authentication
208
+ ```
259
209
 
260
210
  ## Requirements
261
211
 
@@ -275,9 +225,6 @@ For examples and more information about agent rules, visit [https://agent.md](ht
275
225
 
276
226
  Use the `/mcp` command to manage MCP (list, start, stop, status, etc.)
277
227
 
278
- Watch this video for examples! https://www.youtube.com/watch?v=1t1zEetOqlo
279
-
280
-
281
228
  ## Round Robin Model Distribution
282
229
 
283
230
  Code Puppy supports **Round Robin model distribution** to help you overcome rate limits and distribute load across multiple AI models. This feature automatically cycles through configured models with each request, maximizing your API usage while staying within rate limits.
@@ -2,17 +2,17 @@ code_puppy/__init__.py,sha256=xMPewo9RNHb3yfFNIk5WCbv2cvSPtJOCgK2-GqLbNnU,373
2
2
  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
- code_puppy/claude_cache_client.py,sha256=hZr_YtXZSQvBoJFtRbbecKucYqJgoMopqUmm0IxFYGY,6071
6
- code_puppy/cli_runner.py,sha256=4iosJ_zXv9WcG4764yFW-VOLjE3B7Og9yclp6Q4kaSQ,33875
5
+ code_puppy/claude_cache_client.py,sha256=QaucFONE0InS1GANCZwMFx-7sEptbZfjVzb_CgjvuUo,7949
6
+ code_puppy/cli_runner.py,sha256=E7C2pCWof4JgNcCRgMlodeTs2DyWh0NaB7_a_nwafM0,33117
7
7
  code_puppy/config.py,sha256=RlnrLkyFXm7h2Htf8rQA7vqoAyzLPMrESle417uLmFw,52373
8
8
  code_puppy/error_logging.py,sha256=a80OILCUtJhexI6a9GM-r5LqIdjvSRzggfgPp2jv1X0,3297
9
9
  code_puppy/gemini_code_assist.py,sha256=KGS7sO5OLc83nDF3xxS-QiU6vxW9vcm6hmzilu79Ef8,13867
10
- code_puppy/http_utils.py,sha256=BV_bf0i5iPh7HC7T1vlsbz3hvlVwdIzwpS2pfBBKPRo,11823
10
+ code_puppy/http_utils.py,sha256=H3N5Qz2B1CcsGUYOycGWAqoNMr2P1NCVluKX3aRwRqI,10358
11
11
  code_puppy/keymap.py,sha256=IvMkTlB_bIqOWpbTpmftkdyjhtD5todXuEIw1zCZ4u0,3584
12
12
  code_puppy/main.py,sha256=82r3vZy_XcyEsenLn82BnUusaoyL3Bpm_Th_jKgqecE,273
13
- code_puppy/model_factory.py,sha256=Djhp_ukLTMNi8UdZsodhIU6D2X4DmJXnjZy_bzfce3k,37517
13
+ code_puppy/model_factory.py,sha256=BSGHZlwtF7jkYz2qFG9oJglG-NnfmbsQXbx4I6stXW0,38313
14
14
  code_puppy/model_utils.py,sha256=NU8W8NW5F7QS_PXHaLeh55Air1koUV7IVYFP7Rz3XpY,3615
15
- code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
15
+ code_puppy/models.json,sha256=FMQdE_yvP_8y0xxt3K918UkFL9cZMYAqW1SfXcQkU_k,3105
16
16
  code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
17
17
  code_puppy/models_dev_parser.py,sha256=8ndmWrsSyKbXXpRZPXc0w6TfWMuCcgaHiMifmlaBaPc,20611
18
18
  code_puppy/pydantic_patches.py,sha256=YecAEeCOjSIwIBu2O5vEw72atMSL37cXGrbEuukI07o,4582
@@ -21,7 +21,7 @@ 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=CxcNLfPwTDblI0AEtEwhfZ4DTfqqwHjM_A10QslaMBk,8220
24
+ code_puppy/terminal_utils.py,sha256=TaS19x7EZqudlBUAQwLMzBMNxBHBNInvQQREXqRGtkM,12984
25
25
  code_puppy/uvx_detection.py,sha256=tP9X9Nvzow--KIqtqjgrHQkSxMJ3EevfoaeoB9VLY2o,7224
26
26
  code_puppy/version_checker.py,sha256=aq2Mwxl1CR9sEFBgrPt3OQOowLOBUp9VaQYWJhuUv8Q,1780
27
27
  code_puppy/agents/__init__.py,sha256=PtPB7Z5MSwmUKipgt_qxvIuGggcuVaYwNbnp1UP4tPc,518
@@ -40,7 +40,7 @@ code_puppy/agents/agent_qa_expert.py,sha256=5Ikb4U3SZQknUEfwlHZiyZXKqnffnOTQagr_
40
40
  code_puppy/agents/agent_qa_kitten.py,sha256=5PeFFSwCFlTUvP6h5bGntx0xv5NmRwBiw0HnMqY8nLI,9107
41
41
  code_puppy/agents/agent_security_auditor.py,sha256=SpiYNA0XAsIwBj7S2_EQPRslRUmF_-b89pIJyW7DYtY,12022
42
42
  code_puppy/agents/agent_typescript_reviewer.py,sha256=vsnpp98xg6cIoFAEJrRTUM_i4wLEWGm5nJxs6fhHobM,10275
43
- code_puppy/agents/base_agent.py,sha256=r_znuUZJMv97Lh8zeSdS_KJzVGe7X3rAgBk3NZpIO7I,82855
43
+ code_puppy/agents/base_agent.py,sha256=FsjSw4i4YYVi9iyxviy6Y0aRGsm9ALvkOLbAfjcGAmc,83923
44
44
  code_puppy/agents/json_agent.py,sha256=lhopDJDoiSGHvD8A6t50hi9ZBoNRKgUywfxd0Po_Dzc,4886
45
45
  code_puppy/agents/prompt_reviewer.py,sha256=JJrJ0m5q0Puxl8vFsyhAbY9ftU9n6c6UxEVdNct1E-Q,5558
46
46
  code_puppy/command_line/__init__.py,sha256=y7WeRemfYppk8KVbCGeAIiTuiOszIURCDjOMZv_YRmU,45
@@ -112,10 +112,10 @@ code_puppy/messaging/bus.py,sha256=TbdltJ0D5tqnaE4irq1fcXllDYm-mQ_SiX1IFm-S4sw,2
112
112
  code_puppy/messaging/commands.py,sha256=77CtKVNaF5KS3Xyzd0ccDAisZWQxL3weVEt3J-SfYxo,5464
113
113
  code_puppy/messaging/markdown_patches.py,sha256=dMIJozzJChuHa8QNMSEz_kC-dyt7kZiDLZ7rjthbcmg,1626
114
114
  code_puppy/messaging/message_queue.py,sha256=e-viZxacBoNSxRJnCJ4hU4vzsSI3oX_rN58RwhJKFfU,11825
115
- code_puppy/messaging/messages.py,sha256=vNenUTgeZLk2clH4-BLcj44vpzDMmAsOrbwn_msaCVw,16351
115
+ code_puppy/messaging/messages.py,sha256=F7RwMHeQrIk-8kuSSBU76wBq1NGuLb2H5cJrSMTC3XM,16464
116
116
  code_puppy/messaging/queue_console.py,sha256=T0U_V1tdN6hd9DLokp-HCk0mhu8Ivpfajha368CBZrU,9983
117
117
  code_puppy/messaging/renderers.py,sha256=GHVtMnxE1pJ-yrcRjacY81JcjlHRz3UVHzp-ohN-CGE,12058
118
- code_puppy/messaging/rich_renderer.py,sha256=YMj5bdZSjufzFL6kNUuNhsoI_NfXUfvWBQ1FDWPw2Ho,37480
118
+ code_puppy/messaging/rich_renderer.py,sha256=FiT1e5S8nNQte0E6CMFQ3KyTixadkgKSjp1hcZXtyOE,37892
119
119
  code_puppy/messaging/spinner/__init__.py,sha256=KpK5tJqq9YnN3wklqvdH0BQmuwYnT83Mp4tPfQa9RqI,1664
120
120
  code_puppy/messaging/spinner/console_spinner.py,sha256=YIReuWPD01YPy58FqWdMDWj2QhauTUxKo675Ub4-eDA,8451
121
121
  code_puppy/messaging/spinner/spinner_base.py,sha256=JiQDAhCfwrWUFunb8Xcj1caEl34JJY7Bcio7mDeckSc,2694
@@ -123,7 +123,7 @@ code_puppy/plugins/__init__.py,sha256=gWgrXWoFpl-3Mxz2DAvxKW6SkCWrOnw-hKsY9O7nHc
123
123
  code_puppy/plugins/oauth_puppy_html.py,sha256=Wpa-V_NlRiBAvo_OXHuR7wvOH_jSt8L9HSFGiab6xI0,13058
124
124
  code_puppy/plugins/antigravity_oauth/__init__.py,sha256=1miHihSqRNXO20Vh_Gn9M3Aa2szh0gtdSCaKKj9nq0Q,362
125
125
  code_puppy/plugins/antigravity_oauth/accounts.py,sha256=GQit2-K24bsopmTZyscFUq3M0cAEO5WutHWnipVdgz8,14304
126
- code_puppy/plugins/antigravity_oauth/antigravity_model.py,sha256=FgZtXHsyTIX0wcJB7ivcdFd5DpRLqDFO_idcvMuDKtc,25336
126
+ code_puppy/plugins/antigravity_oauth/antigravity_model.py,sha256=g0_nXnMg288CvBE48CFgZ-iAqlYbtbi1dcB4Up6cYYc,26115
127
127
  code_puppy/plugins/antigravity_oauth/config.py,sha256=BoQgqf5I2XoHWnBBo9vhCIc_XwPj9Mbp0Z95ygWwt78,1362
128
128
  code_puppy/plugins/antigravity_oauth/constants.py,sha256=qsrA10JJvzNuY0OobvvwCQcoGpILBninllcUUMKkUrQ,4644
129
129
  code_puppy/plugins/antigravity_oauth/oauth.py,sha256=ZHXJtZP63l6brOpX1WdLfuUClIleA79-4y36YUJc6Wo,15137
@@ -145,7 +145,7 @@ code_puppy/plugins/claude_code_oauth/__init__.py,sha256=mCcOU-wM7LNCDjr-w-WLPzom
145
145
  code_puppy/plugins/claude_code_oauth/config.py,sha256=DjGySCkvjSGZds6DYErLMAi3TItt8iSLGvyJN98nSEM,2013
146
146
  code_puppy/plugins/claude_code_oauth/register_callbacks.py,sha256=g8sl-i7jIOF6OFALeaLqTF3mS4tD8GR_FCzvPjVw2js,10165
147
147
  code_puppy/plugins/claude_code_oauth/test_plugin.py,sha256=yQy4EeZl4bjrcog1d8BjknoDTRK75mRXXvkSQJYSSEM,9286
148
- code_puppy/plugins/claude_code_oauth/utils.py,sha256=wDaOU21zB3y6PWkuMXwE4mFjQuffyDae-vXysPTS-w8,13438
148
+ code_puppy/plugins/claude_code_oauth/utils.py,sha256=TVgz5aFd2GFPHSiG9NnOYiw-y6KRkWwt_SZxmMpwMIY,17243
149
149
  code_puppy/plugins/customizable_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
150
  code_puppy/plugins/customizable_commands/register_callbacks.py,sha256=zVMfIzr--hVn0IOXxIicbmgj2s-HZUgtrOc0NCDOnDw,5183
151
151
  code_puppy/plugins/example_custom_command/README.md,sha256=5c5Zkm7CW6BDSfe3WoLU7GW6t5mjjYAbu9-_pu-b3p4,8244
@@ -159,7 +159,7 @@ code_puppy/plugins/shell_safety/register_callbacks.py,sha256=W3v664RR48Fdbbbltf_
159
159
  code_puppy/prompts/codex_system_prompt.md,sha256=hEFTCziroLqZmqNle5kG34A8kvTteOWezCiVrAEKhE0,24400
160
160
  code_puppy/tools/__init__.py,sha256=BVTZ85jLHgDANwOnUSOz3UDlp8VQDq4DoGF23BRlyWw,6032
161
161
  code_puppy/tools/agent_tools.py,sha256=snBI6FlFtR03CbYKXwu53R48c_fRSuDIwcNdVUruLcA,21020
162
- code_puppy/tools/command_runner.py,sha256=4RlgPWWE2Mk35t69QJWR4Md4etMGLR2rFSZAKrNohU4,44917
162
+ code_puppy/tools/command_runner.py,sha256=3qXVnVTaBPia6y2D29As47_TRKgpyCj82yMFK-8UUYc,44954
163
163
  code_puppy/tools/common.py,sha256=IboS6sbwN4a3FzHdfsZJtEFiyDUCszevI6LpH14ydEk,40561
164
164
  code_puppy/tools/file_modifications.py,sha256=vz9n7R0AGDSdLUArZr_55yJLkyI30M8zreAppxIx02M,29380
165
165
  code_puppy/tools/file_operations.py,sha256=CqhpuBnOFOcQCIYXOujskxq2VMLWYJhibYrH0YcPSfA,35692
@@ -174,10 +174,10 @@ code_puppy/tools/browser/browser_scripts.py,sha256=sNb8eLEyzhasy5hV4B9OjM8yIVMLV
174
174
  code_puppy/tools/browser/browser_workflows.py,sha256=nitW42vCf0ieTX1gLabozTugNQ8phtoFzZbiAhw1V90,6491
175
175
  code_puppy/tools/browser/camoufox_manager.py,sha256=RZjGOEftE5sI_tsercUyXFSZI2wpStXf-q0PdYh2G3I,8680
176
176
  code_puppy/tools/browser/vqa_agent.py,sha256=DBn9HKloILqJSTSdNZzH_PYWT0B2h9VwmY6akFQI_uU,2913
177
- code_puppy-0.0.336.data/data/code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
178
- code_puppy-0.0.336.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
179
- code_puppy-0.0.336.dist-info/METADATA,sha256=K0YyE2O8F9-nMuMdbs2rAovJHJmtQox98_r4beIjGPA,28854
180
- code_puppy-0.0.336.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
181
- code_puppy-0.0.336.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
182
- code_puppy-0.0.336.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
183
- code_puppy-0.0.336.dist-info/RECORD,,
177
+ code_puppy-0.0.338.data/data/code_puppy/models.json,sha256=FMQdE_yvP_8y0xxt3K918UkFL9cZMYAqW1SfXcQkU_k,3105
178
+ code_puppy-0.0.338.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
179
+ code_puppy-0.0.338.dist-info/METADATA,sha256=kNsGvoJiQpXWHOgGVtrwjkyYvFo8sVRj6xtxXRr83Lw,27520
180
+ code_puppy-0.0.338.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
181
+ code_puppy-0.0.338.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
182
+ code_puppy-0.0.338.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
183
+ code_puppy-0.0.338.dist-info/RECORD,,