code-puppy 0.0.336__py3-none-any.whl → 0.0.348__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.
- code_puppy/agents/base_agent.py +41 -224
- code_puppy/agents/event_stream_handler.py +257 -0
- code_puppy/claude_cache_client.py +208 -2
- code_puppy/cli_runner.py +53 -35
- code_puppy/command_line/add_model_menu.py +8 -9
- code_puppy/command_line/autosave_menu.py +18 -24
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/core_commands.py +34 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
- code_puppy/command_line/mcp/custom_server_form.py +54 -19
- code_puppy/command_line/mcp/custom_server_installer.py +8 -9
- code_puppy/command_line/mcp/handler.py +0 -2
- code_puppy/command_line/mcp/help_command.py +1 -5
- code_puppy/command_line/mcp/start_command.py +36 -18
- code_puppy/command_line/onboarding_slides.py +0 -1
- code_puppy/command_line/prompt_toolkit_completion.py +124 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/http_utils.py +93 -130
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/managed_server.py +49 -24
- code_puppy/mcp_/manager.py +81 -52
- code_puppy/messaging/message_queue.py +11 -23
- code_puppy/messaging/messages.py +3 -0
- code_puppy/messaging/rich_renderer.py +13 -3
- code_puppy/model_factory.py +16 -0
- code_puppy/models.json +2 -2
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +17 -2
- code_puppy/plugins/claude_code_oauth/utils.py +126 -7
- code_puppy/terminal_utils.py +128 -1
- code_puppy/tools/agent_tools.py +66 -13
- code_puppy/tools/command_runner.py +1 -0
- code_puppy/tools/common.py +3 -9
- {code_puppy-0.0.336.data → code_puppy-0.0.348.data}/data/code_puppy/models.json +2 -2
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/METADATA +19 -71
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/RECORD +39 -38
- code_puppy/command_line/mcp/add_command.py +0 -170
- {code_puppy-0.0.336.data → code_puppy-0.0.348.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.348.dist-info}/licenses/LICENSE +0 -0
|
@@ -329,31 +329,19 @@ def emit_divider(content: str = "─" * 100 + "\n", **metadata):
|
|
|
329
329
|
|
|
330
330
|
|
|
331
331
|
def emit_prompt(prompt_text: str, timeout: float = None) -> str:
|
|
332
|
-
"""Emit a human input request and wait for response.
|
|
333
|
-
# TUI mode has been removed, always use interactive mode input
|
|
334
|
-
if True:
|
|
335
|
-
# Emit the prompt as a message for display
|
|
336
|
-
from code_puppy.messaging import emit_info
|
|
332
|
+
"""Emit a human input request and wait for response.
|
|
337
333
|
|
|
338
|
-
|
|
334
|
+
Uses safe_input for cross-platform compatibility, especially on Windows
|
|
335
|
+
where raw input() can fail after prompt_toolkit Applications.
|
|
336
|
+
"""
|
|
337
|
+
from code_puppy.command_line.utils import safe_input
|
|
338
|
+
from code_puppy.messaging import emit_info
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
console = Console()
|
|
346
|
-
response = console.input("[cyan]>>> [/cyan]")
|
|
347
|
-
return response
|
|
348
|
-
except Exception:
|
|
349
|
-
# Fallback to basic input
|
|
350
|
-
response = input(">>> ")
|
|
351
|
-
return response
|
|
352
|
-
|
|
353
|
-
# In TUI mode, use the queue system
|
|
354
|
-
queue = get_global_queue()
|
|
355
|
-
prompt_id = queue.create_prompt_request(prompt_text)
|
|
356
|
-
return queue.wait_for_prompt_response(prompt_id, timeout)
|
|
340
|
+
emit_info(prompt_text)
|
|
341
|
+
|
|
342
|
+
# Use safe_input which resets Windows console state before reading
|
|
343
|
+
response = safe_input(">>> ")
|
|
344
|
+
return response
|
|
357
345
|
|
|
358
346
|
|
|
359
347
|
def provide_prompt_response(prompt_id: str, response: str):
|
code_puppy/messaging/messages.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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."""
|
code_puppy/model_factory.py
CHANGED
|
@@ -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.
|
|
58
|
+
"Cerebras-GLM-4.7": {
|
|
59
59
|
"type": "cerebras",
|
|
60
|
-
"name": "zai-glm-4.
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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}"
|
code_puppy/terminal_utils.py
CHANGED
|
@@ -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)
|
code_puppy/tools/agent_tools.py
CHANGED
|
@@ -21,6 +21,7 @@ from code_puppy.config import (
|
|
|
21
21
|
DATA_DIR,
|
|
22
22
|
get_message_limit,
|
|
23
23
|
get_use_dbos,
|
|
24
|
+
get_value,
|
|
24
25
|
)
|
|
25
26
|
from code_puppy.messaging import (
|
|
26
27
|
SubAgentInvocationMessage,
|
|
@@ -471,39 +472,90 @@ def register_invoke_agent(agent):
|
|
|
471
472
|
subagent_name = f"temp-invoke-agent-{session_id}"
|
|
472
473
|
model_settings = make_model_settings(model_name)
|
|
473
474
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
instructions=instructions,
|
|
477
|
-
output_type=str,
|
|
478
|
-
retries=3,
|
|
479
|
-
history_processors=[agent_config.message_history_accumulator],
|
|
480
|
-
model_settings=model_settings,
|
|
481
|
-
)
|
|
475
|
+
# Get MCP servers for sub-agents (same as main agent)
|
|
476
|
+
from code_puppy.mcp_ import get_mcp_manager
|
|
482
477
|
|
|
483
|
-
|
|
484
|
-
|
|
478
|
+
mcp_servers = []
|
|
479
|
+
mcp_disabled = get_value("disable_mcp_servers")
|
|
480
|
+
if not (
|
|
481
|
+
mcp_disabled and str(mcp_disabled).lower() in ("1", "true", "yes", "on")
|
|
482
|
+
):
|
|
483
|
+
manager = get_mcp_manager()
|
|
484
|
+
mcp_servers = manager.get_servers_for_agent()
|
|
485
485
|
|
|
486
|
-
|
|
487
|
-
|
|
486
|
+
# Get the event_stream_handler for streaming output
|
|
487
|
+
from code_puppy.agents.event_stream_handler import event_stream_handler
|
|
488
488
|
|
|
489
489
|
if get_use_dbos():
|
|
490
490
|
from pydantic_ai.durable_exec.dbos import DBOSAgent
|
|
491
491
|
|
|
492
|
-
|
|
492
|
+
# For DBOS, create agent without MCP servers (to avoid serialization issues)
|
|
493
|
+
# and add them at runtime
|
|
494
|
+
temp_agent = Agent(
|
|
495
|
+
model=model,
|
|
496
|
+
instructions=instructions,
|
|
497
|
+
output_type=str,
|
|
498
|
+
retries=3,
|
|
499
|
+
toolsets=[], # MCP servers added separately for DBOS
|
|
500
|
+
history_processors=[agent_config.message_history_accumulator],
|
|
501
|
+
model_settings=model_settings,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Register the tools that the agent needs
|
|
505
|
+
from code_puppy.tools import register_tools_for_agent
|
|
506
|
+
|
|
507
|
+
agent_tools = agent_config.get_available_tools()
|
|
508
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
509
|
+
|
|
510
|
+
# Wrap with DBOS - pass event_stream_handler for streaming output
|
|
511
|
+
dbos_agent = DBOSAgent(
|
|
512
|
+
temp_agent,
|
|
513
|
+
name=subagent_name,
|
|
514
|
+
event_stream_handler=event_stream_handler,
|
|
515
|
+
)
|
|
493
516
|
temp_agent = dbos_agent
|
|
494
517
|
|
|
518
|
+
# Store MCP servers to add at runtime
|
|
519
|
+
subagent_mcp_servers = mcp_servers
|
|
520
|
+
else:
|
|
521
|
+
# Non-DBOS path - include MCP servers directly in the agent
|
|
522
|
+
temp_agent = Agent(
|
|
523
|
+
model=model,
|
|
524
|
+
instructions=instructions,
|
|
525
|
+
output_type=str,
|
|
526
|
+
retries=3,
|
|
527
|
+
toolsets=mcp_servers,
|
|
528
|
+
history_processors=[agent_config.message_history_accumulator],
|
|
529
|
+
model_settings=model_settings,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Register the tools that the agent needs
|
|
533
|
+
from code_puppy.tools import register_tools_for_agent
|
|
534
|
+
|
|
535
|
+
agent_tools = agent_config.get_available_tools()
|
|
536
|
+
register_tools_for_agent(temp_agent, agent_tools)
|
|
537
|
+
|
|
538
|
+
subagent_mcp_servers = None
|
|
539
|
+
|
|
495
540
|
# Run the temporary agent with the provided prompt as an asyncio task
|
|
496
541
|
# Pass the message_history from the session to continue the conversation
|
|
497
542
|
workflow_id = None # Track for potential cancellation
|
|
498
543
|
if get_use_dbos():
|
|
499
544
|
# Generate a unique workflow ID for DBOS - ensures no collisions in back-to-back calls
|
|
500
545
|
workflow_id = _generate_dbos_workflow_id(group_id)
|
|
546
|
+
|
|
547
|
+
# Add MCP servers to the DBOS agent's toolsets
|
|
548
|
+
# (temp_agent is discarded after this invocation, so no need to restore)
|
|
549
|
+
if subagent_mcp_servers:
|
|
550
|
+
temp_agent._toolsets = temp_agent._toolsets + subagent_mcp_servers
|
|
551
|
+
|
|
501
552
|
with SetWorkflowID(workflow_id):
|
|
502
553
|
task = asyncio.create_task(
|
|
503
554
|
temp_agent.run(
|
|
504
555
|
prompt,
|
|
505
556
|
message_history=message_history,
|
|
506
557
|
usage_limits=UsageLimits(request_limit=get_message_limit()),
|
|
558
|
+
event_stream_handler=event_stream_handler,
|
|
507
559
|
)
|
|
508
560
|
)
|
|
509
561
|
_active_subagent_tasks.add(task)
|
|
@@ -513,6 +565,7 @@ def register_invoke_agent(agent):
|
|
|
513
565
|
prompt,
|
|
514
566
|
message_history=message_history,
|
|
515
567
|
usage_limits=UsageLimits(request_limit=get_message_limit()),
|
|
568
|
+
event_stream_handler=event_stream_handler,
|
|
516
569
|
)
|
|
517
570
|
)
|
|
518
571
|
_active_subagent_tasks.add(task)
|
code_puppy/tools/common.py
CHANGED
|
@@ -727,15 +727,9 @@ def _format_diff_with_syntax_highlighting(
|
|
|
727
727
|
result.append("\n")
|
|
728
728
|
continue
|
|
729
729
|
|
|
730
|
-
#
|
|
731
|
-
if line.startswith("---"):
|
|
732
|
-
|
|
733
|
-
elif line.startswith("+++"):
|
|
734
|
-
result.append(line, style="yellow")
|
|
735
|
-
elif line.startswith("@@"):
|
|
736
|
-
result.append(line, style="cyan")
|
|
737
|
-
elif line.startswith(("diff ", "index ")):
|
|
738
|
-
result.append(line, style="dim")
|
|
730
|
+
# Skip diff headers - they're redundant noise since we show the filename in the banner
|
|
731
|
+
if line.startswith(("---", "+++", "@@", "diff ", "index ")):
|
|
732
|
+
continue
|
|
739
733
|
else:
|
|
740
734
|
# Determine line type and extract code content
|
|
741
735
|
if line.startswith("-"):
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"supported_settings": ["reasoning_effort", "verbosity"],
|
|
56
56
|
"supports_xhigh_reasoning": true
|
|
57
57
|
},
|
|
58
|
-
"Cerebras-GLM-4.
|
|
58
|
+
"Cerebras-GLM-4.7": {
|
|
59
59
|
"type": "cerebras",
|
|
60
|
-
"name": "zai-glm-4.
|
|
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"
|