code-puppy 0.0.337__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,
@@ -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"
@@ -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)
@@ -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.337
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
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
@@ -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
@@ -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.337.data/data/code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
178
- code_puppy-0.0.337.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
179
- code_puppy-0.0.337.dist-info/METADATA,sha256=F1DCPuk3QS0E2t1o_HRId-DiNTtOs29kcmSueGwHUHk,28854
180
- code_puppy-0.0.337.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
181
- code_puppy-0.0.337.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
182
- code_puppy-0.0.337.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
183
- code_puppy-0.0.337.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,,