code-puppy 0.0.336__py3-none-any.whl → 0.0.341__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/agents/base_agent.py +79 -31
- code_puppy/claude_cache_client.py +208 -2
- code_puppy/cli_runner.py +49 -32
- 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/prompt_toolkit_completion.py +118 -0
- code_puppy/http_utils.py +93 -130
- code_puppy/mcp_/managed_server.py +7 -11
- 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/command_runner.py +1 -0
- code_puppy/tools/common.py +3 -9
- {code_puppy-0.0.336.data → code_puppy-0.0.341.data}/data/code_puppy/models.json +2 -2
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.341.dist-info}/METADATA +19 -71
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.341.dist-info}/RECORD +25 -24
- {code_puppy-0.0.336.data → code_puppy-0.0.341.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.341.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.341.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.336.dist-info → code_puppy-0.0.341.dist-info}/licenses/LICENSE +0 -0
|
@@ -27,6 +27,10 @@ from code_puppy.command_line.attachments import (
|
|
|
27
27
|
_detect_path_tokens,
|
|
28
28
|
_tokenise,
|
|
29
29
|
)
|
|
30
|
+
from code_puppy.command_line.clipboard import (
|
|
31
|
+
capture_clipboard_image_to_pending,
|
|
32
|
+
has_image_in_clipboard,
|
|
33
|
+
)
|
|
30
34
|
from code_puppy.command_line.command_registry import get_unique_commands
|
|
31
35
|
from code_puppy.command_line.file_path_completion import FilePathCompleter
|
|
32
36
|
from code_puppy.command_line.load_context_completion import LoadContextCompleter
|
|
@@ -644,6 +648,120 @@ async def get_input_with_combined_completion(
|
|
|
644
648
|
else:
|
|
645
649
|
event.current_buffer.validate_and_handle()
|
|
646
650
|
|
|
651
|
+
# Handle bracketed paste (triggered by most terminal Cmd+V / Ctrl+V)
|
|
652
|
+
# This is the PRIMARY paste handler - works with Cmd+V on macOS terminals
|
|
653
|
+
@bindings.add(Keys.BracketedPaste)
|
|
654
|
+
def handle_bracketed_paste(event):
|
|
655
|
+
"""Handle bracketed paste - works with Cmd+V on macOS terminals."""
|
|
656
|
+
# The pasted data is in event.data
|
|
657
|
+
pasted_data = event.data
|
|
658
|
+
|
|
659
|
+
# Check if clipboard has an image (the pasted text might just be empty or a file path)
|
|
660
|
+
try:
|
|
661
|
+
if has_image_in_clipboard():
|
|
662
|
+
placeholder = capture_clipboard_image_to_pending()
|
|
663
|
+
if placeholder:
|
|
664
|
+
event.app.current_buffer.insert_text(placeholder + " ")
|
|
665
|
+
# The placeholder itself is visible feedback - no need for extra output
|
|
666
|
+
# Use bell for audible feedback (works in most terminals)
|
|
667
|
+
event.app.output.bell()
|
|
668
|
+
return # Don't also paste the text data
|
|
669
|
+
except Exception:
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
# No image - insert the pasted text as normal, sanitizing Windows newlines
|
|
673
|
+
if pasted_data:
|
|
674
|
+
# Normalize Windows line endings to Unix style
|
|
675
|
+
sanitized_data = pasted_data.replace("\r\n", "\n").replace("\r", "\n")
|
|
676
|
+
event.app.current_buffer.insert_text(sanitized_data)
|
|
677
|
+
|
|
678
|
+
# Fallback Ctrl+V for terminals without bracketed paste support
|
|
679
|
+
@bindings.add("c-v", eager=True)
|
|
680
|
+
def handle_smart_paste(event):
|
|
681
|
+
"""Handle Ctrl+V - auto-detect image vs text in clipboard."""
|
|
682
|
+
try:
|
|
683
|
+
# Check for image first
|
|
684
|
+
if has_image_in_clipboard():
|
|
685
|
+
placeholder = capture_clipboard_image_to_pending()
|
|
686
|
+
if placeholder:
|
|
687
|
+
event.app.current_buffer.insert_text(placeholder + " ")
|
|
688
|
+
# The placeholder itself is visible feedback - no need for extra output
|
|
689
|
+
# Use bell for audible feedback (works in most terminals)
|
|
690
|
+
event.app.output.bell()
|
|
691
|
+
return # Don't also paste text
|
|
692
|
+
except Exception:
|
|
693
|
+
pass # Fall through to text paste on any error
|
|
694
|
+
|
|
695
|
+
# No image (or error) - do normal text paste
|
|
696
|
+
# prompt_toolkit doesn't have built-in paste, so we handle it manually
|
|
697
|
+
try:
|
|
698
|
+
import platform
|
|
699
|
+
import subprocess
|
|
700
|
+
|
|
701
|
+
text = None
|
|
702
|
+
system = platform.system()
|
|
703
|
+
|
|
704
|
+
if system == "Darwin": # macOS
|
|
705
|
+
result = subprocess.run(
|
|
706
|
+
["pbpaste"], capture_output=True, text=True, timeout=2
|
|
707
|
+
)
|
|
708
|
+
if result.returncode == 0:
|
|
709
|
+
text = result.stdout
|
|
710
|
+
elif system == "Windows":
|
|
711
|
+
# Windows - use powershell
|
|
712
|
+
result = subprocess.run(
|
|
713
|
+
["powershell", "-command", "Get-Clipboard"],
|
|
714
|
+
capture_output=True,
|
|
715
|
+
text=True,
|
|
716
|
+
timeout=2,
|
|
717
|
+
)
|
|
718
|
+
if result.returncode == 0:
|
|
719
|
+
text = result.stdout
|
|
720
|
+
else: # Linux
|
|
721
|
+
# Try xclip first, then xsel
|
|
722
|
+
for cmd in [
|
|
723
|
+
["xclip", "-selection", "clipboard", "-o"],
|
|
724
|
+
["xsel", "--clipboard", "--output"],
|
|
725
|
+
]:
|
|
726
|
+
try:
|
|
727
|
+
result = subprocess.run(
|
|
728
|
+
cmd, capture_output=True, text=True, timeout=2
|
|
729
|
+
)
|
|
730
|
+
if result.returncode == 0:
|
|
731
|
+
text = result.stdout
|
|
732
|
+
break
|
|
733
|
+
except FileNotFoundError:
|
|
734
|
+
continue
|
|
735
|
+
|
|
736
|
+
if text:
|
|
737
|
+
# Normalize Windows line endings to Unix style
|
|
738
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
739
|
+
# Strip trailing newline that clipboard tools often add
|
|
740
|
+
text = text.rstrip("\n")
|
|
741
|
+
event.app.current_buffer.insert_text(text)
|
|
742
|
+
except Exception:
|
|
743
|
+
pass # Silently fail if text paste doesn't work
|
|
744
|
+
|
|
745
|
+
# F3 - dedicated image paste (shows error if no image)
|
|
746
|
+
@bindings.add("f3")
|
|
747
|
+
def handle_image_paste_f3(event):
|
|
748
|
+
"""Handle F3 - paste image from clipboard (image-only, shows error if none)."""
|
|
749
|
+
try:
|
|
750
|
+
if has_image_in_clipboard():
|
|
751
|
+
placeholder = capture_clipboard_image_to_pending()
|
|
752
|
+
if placeholder:
|
|
753
|
+
event.app.current_buffer.insert_text(placeholder + " ")
|
|
754
|
+
# The placeholder itself is visible feedback
|
|
755
|
+
# Use bell for audible feedback (works in most terminals)
|
|
756
|
+
event.app.output.bell()
|
|
757
|
+
else:
|
|
758
|
+
# Insert a transient message that user can delete
|
|
759
|
+
event.app.current_buffer.insert_text("[⚠️ no image in clipboard] ")
|
|
760
|
+
event.app.output.bell()
|
|
761
|
+
except Exception:
|
|
762
|
+
event.app.current_buffer.insert_text("[❌ clipboard error] ")
|
|
763
|
+
event.app.output.bell()
|
|
764
|
+
|
|
647
765
|
session = PromptSession(
|
|
648
766
|
completer=completer,
|
|
649
767
|
history=history,
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
304
|
+
RetryingAsyncClient if not config.disable_retry else httpx.AsyncClient
|
|
318
305
|
)
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
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:
|
|
@@ -222,18 +222,14 @@ class ManagedMCPServer:
|
|
|
222
222
|
http_kwargs["timeout"] = config["timeout"]
|
|
223
223
|
if "read_timeout" in config:
|
|
224
224
|
http_kwargs["read_timeout"] = config["read_timeout"]
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if isinstance(v, str):
|
|
232
|
-
resolved_headers[k] = os.path.expandvars(v)
|
|
233
|
-
else:
|
|
234
|
-
resolved_headers[k] = v
|
|
235
|
-
http_kwargs["headers"] = resolved_headers
|
|
225
|
+
|
|
226
|
+
# Handle http_client vs headers (mutually exclusive)
|
|
227
|
+
if "http_client" in config:
|
|
228
|
+
# Use provided http_client
|
|
229
|
+
http_kwargs["http_client"] = config["http_client"]
|
|
230
|
+
elif config.get("headers"):
|
|
236
231
|
# Create HTTP client if headers are provided but no client specified
|
|
232
|
+
http_kwargs["http_client"] = self._get_http_client()
|
|
237
233
|
|
|
238
234
|
self._pydantic_server = MCPServerStreamableHTTP(
|
|
239
235
|
**http_kwargs, process_tool_call=process_tool_call
|
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
|