code-puppy 0.0.329__py3-none-any.whl → 0.0.331__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/cli_runner.py +7 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +15 -0
- code_puppy/plugins/claude_code_oauth/utils.py +202 -0
- code_puppy/terminal_utils.py +8 -4
- {code_puppy-0.0.329.dist-info → code_puppy-0.0.331.dist-info}/METADATA +1 -1
- {code_puppy-0.0.329.dist-info → code_puppy-0.0.331.dist-info}/RECORD +11 -11
- {code_puppy-0.0.329.data → code_puppy-0.0.331.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.329.data → code_puppy-0.0.331.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.329.dist-info → code_puppy-0.0.331.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.329.dist-info → code_puppy-0.0.331.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.329.dist-info → code_puppy-0.0.331.dist-info}/licenses/LICENSE +0 -0
code_puppy/cli_runner.py
CHANGED
|
@@ -421,6 +421,10 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
|
|
|
421
421
|
current_agent_task = None
|
|
422
422
|
|
|
423
423
|
while True:
|
|
424
|
+
# Windows-specific: Aggressively reset terminal at the start of every loop
|
|
425
|
+
# This fixes terminal corruption after Ctrl+C, especially when running via uvx
|
|
426
|
+
reset_windows_terminal_full()
|
|
427
|
+
|
|
424
428
|
from code_puppy.agents.agent_manager import get_current_agent
|
|
425
429
|
from code_puppy.messaging import emit_info
|
|
426
430
|
|
|
@@ -707,6 +711,9 @@ async def run_prompt_with_attachments(
|
|
|
707
711
|
attachments = [attachment.content for attachment in processed_prompt.attachments]
|
|
708
712
|
link_attachments = [link.url_part for link in processed_prompt.link_attachments]
|
|
709
713
|
|
|
714
|
+
# Trigger invoke_agent callbacks (e.g., Claude Code OAuth token refresh)
|
|
715
|
+
await callbacks.on_invoke_agent()
|
|
716
|
+
|
|
710
717
|
# IMPORTANT: Set the shared console on the agent so that streaming output
|
|
711
718
|
# uses the same console as the spinner. This prevents Live display conflicts
|
|
712
719
|
# that cause line duplication during markdown streaming.
|
|
@@ -25,6 +25,7 @@ from .utils import (
|
|
|
25
25
|
fetch_claude_code_models,
|
|
26
26
|
load_claude_models_filtered,
|
|
27
27
|
load_stored_tokens,
|
|
28
|
+
maybe_refresh_token,
|
|
28
29
|
prepare_oauth_context,
|
|
29
30
|
remove_claude_code_models,
|
|
30
31
|
save_tokens,
|
|
@@ -302,5 +303,19 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
|
302
303
|
return None
|
|
303
304
|
|
|
304
305
|
|
|
306
|
+
def _on_invoke_agent(*args, **kwargs) -> None:
|
|
307
|
+
"""Called before each agent invocation.
|
|
308
|
+
|
|
309
|
+
Checks if 30 minutes have passed since last token refresh and
|
|
310
|
+
refreshes the Claude Code OAuth token if needed.
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
if maybe_refresh_token():
|
|
314
|
+
logger.debug("Token refresh check completed")
|
|
315
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
316
|
+
logger.warning("Token refresh check failed: %s", exc)
|
|
317
|
+
|
|
318
|
+
|
|
305
319
|
register_callback("custom_command_help", _custom_help)
|
|
306
320
|
register_callback("custom_command", _handle_custom_command)
|
|
321
|
+
register_callback("invoke_agent", _on_invoke_agent)
|
|
@@ -23,6 +23,10 @@ from .config import (
|
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
|
+
# Token refresh tracking
|
|
27
|
+
_last_refresh_time: float = 0.0
|
|
28
|
+
REFRESH_INTERVAL_SECONDS: float = 30 * 60 # 30 minutes
|
|
29
|
+
|
|
26
30
|
|
|
27
31
|
@dataclass
|
|
28
32
|
class OAuthContext:
|
|
@@ -397,3 +401,201 @@ def remove_claude_code_models() -> int:
|
|
|
397
401
|
except Exception as exc: # pragma: no cover - defensive logging
|
|
398
402
|
logger.error("Error removing Claude Code models: %s", exc)
|
|
399
403
|
return 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _update_model_tokens(new_access_token: str) -> bool:
|
|
407
|
+
"""Update all Claude Code models with the new access token.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
new_access_token: The new access token to set
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
True if successful, False otherwise
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
claude_models = load_claude_models()
|
|
417
|
+
if not claude_models:
|
|
418
|
+
logger.debug("No models to update")
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
updated = 0
|
|
422
|
+
for _model_name, config in claude_models.items():
|
|
423
|
+
if config.get("oauth_source") == "claude-code-plugin":
|
|
424
|
+
if (
|
|
425
|
+
"custom_endpoint" in config
|
|
426
|
+
and "api_key" in config["custom_endpoint"]
|
|
427
|
+
):
|
|
428
|
+
config["custom_endpoint"]["api_key"] = new_access_token
|
|
429
|
+
updated += 1
|
|
430
|
+
|
|
431
|
+
if updated > 0:
|
|
432
|
+
if save_claude_models(claude_models):
|
|
433
|
+
logger.info("Updated %s model configurations with new token", updated)
|
|
434
|
+
return True
|
|
435
|
+
else:
|
|
436
|
+
logger.error("Failed to save updated model configurations")
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
442
|
+
logger.error("Error updating model tokens: %s", exc)
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def refresh_access_token() -> Optional[str]:
|
|
447
|
+
"""Refresh the access token using the refresh token.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
New access token if successful, None otherwise
|
|
451
|
+
"""
|
|
452
|
+
try:
|
|
453
|
+
tokens = load_stored_tokens()
|
|
454
|
+
if not tokens:
|
|
455
|
+
logger.debug("No stored tokens found for refresh")
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
if "refresh_token" not in tokens:
|
|
459
|
+
logger.debug("No refresh token available")
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
refresh_token = tokens["refresh_token"]
|
|
463
|
+
|
|
464
|
+
# Prepare refresh request
|
|
465
|
+
payload = {
|
|
466
|
+
"grant_type": "refresh_token",
|
|
467
|
+
"client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
|
|
468
|
+
"refresh_token": refresh_token,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
headers = {
|
|
472
|
+
"Content-Type": "application/json",
|
|
473
|
+
"Accept": "application/json",
|
|
474
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
logger.info("Refreshing Claude Code access token...")
|
|
478
|
+
response = requests.post(
|
|
479
|
+
CLAUDE_CODE_OAUTH_CONFIG["token_url"],
|
|
480
|
+
json=payload,
|
|
481
|
+
headers=headers,
|
|
482
|
+
timeout=30,
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if response.status_code == 200:
|
|
486
|
+
token_data = response.json()
|
|
487
|
+
|
|
488
|
+
# Update tokens with new access token and expiry
|
|
489
|
+
new_access_token = token_data.get("access_token")
|
|
490
|
+
if not new_access_token:
|
|
491
|
+
logger.error("No access_token in refresh response")
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
# Update stored tokens
|
|
495
|
+
tokens["access_token"] = new_access_token
|
|
496
|
+
|
|
497
|
+
# Update expiry if provided
|
|
498
|
+
if "expires_in" in token_data:
|
|
499
|
+
tokens["expires_at"] = time.time() + token_data["expires_in"]
|
|
500
|
+
|
|
501
|
+
# Update refresh token if a new one was provided
|
|
502
|
+
if "refresh_token" in token_data:
|
|
503
|
+
tokens["refresh_token"] = token_data["refresh_token"]
|
|
504
|
+
|
|
505
|
+
# Save updated tokens
|
|
506
|
+
if save_tokens(tokens):
|
|
507
|
+
logger.info("Claude Code access token refreshed successfully")
|
|
508
|
+
|
|
509
|
+
# Update model configurations with new token
|
|
510
|
+
_update_model_tokens(new_access_token)
|
|
511
|
+
|
|
512
|
+
return new_access_token
|
|
513
|
+
else:
|
|
514
|
+
logger.error("Failed to save refreshed tokens")
|
|
515
|
+
return None
|
|
516
|
+
else:
|
|
517
|
+
logger.warning(
|
|
518
|
+
"Token refresh failed: %s - %s",
|
|
519
|
+
response.status_code,
|
|
520
|
+
response.text,
|
|
521
|
+
)
|
|
522
|
+
return None
|
|
523
|
+
|
|
524
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
525
|
+
logger.error("Error refreshing access token: %s", exc)
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _is_using_claude_code_model() -> bool:
|
|
530
|
+
"""Check if the current agent is using a Claude Code OAuth model.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
True if currently using a claude-code-* model, False otherwise
|
|
534
|
+
"""
|
|
535
|
+
try:
|
|
536
|
+
from code_puppy.agents import get_current_agent
|
|
537
|
+
from code_puppy.model_utils import is_claude_code_model
|
|
538
|
+
|
|
539
|
+
agent = get_current_agent()
|
|
540
|
+
if agent is None:
|
|
541
|
+
return False
|
|
542
|
+
|
|
543
|
+
model_name = agent.get_model_name()
|
|
544
|
+
if not model_name:
|
|
545
|
+
return False
|
|
546
|
+
|
|
547
|
+
return is_claude_code_model(model_name)
|
|
548
|
+
except Exception as exc:
|
|
549
|
+
logger.debug("Could not determine current model: %s", exc)
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def maybe_refresh_token() -> bool:
|
|
554
|
+
"""Refresh the token if 30 minutes have passed since last refresh.
|
|
555
|
+
|
|
556
|
+
This function is designed to be called on every prompt, but will only
|
|
557
|
+
actually refresh the token if:
|
|
558
|
+
1. The current model is a Claude Code OAuth model (starts with 'claude-code-')
|
|
559
|
+
2. REFRESH_INTERVAL_SECONDS (30 min) has passed since last refresh
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
True if refresh was attempted (regardless of success), False if skipped
|
|
563
|
+
"""
|
|
564
|
+
global _last_refresh_time
|
|
565
|
+
|
|
566
|
+
# Only refresh if we're actually using a Claude Code model
|
|
567
|
+
if not _is_using_claude_code_model():
|
|
568
|
+
return False
|
|
569
|
+
|
|
570
|
+
# Check if we have tokens at all
|
|
571
|
+
tokens = load_stored_tokens()
|
|
572
|
+
if not tokens or "refresh_token" not in tokens:
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
current_time = time.time()
|
|
576
|
+
time_since_last = current_time - _last_refresh_time
|
|
577
|
+
|
|
578
|
+
if time_since_last < REFRESH_INTERVAL_SECONDS:
|
|
579
|
+
logger.debug(
|
|
580
|
+
"Skipping token refresh, %.1f minutes until next refresh",
|
|
581
|
+
(REFRESH_INTERVAL_SECONDS - time_since_last) / 60,
|
|
582
|
+
)
|
|
583
|
+
return False
|
|
584
|
+
|
|
585
|
+
logger.info(
|
|
586
|
+
"Token refresh interval reached (%.1f min since last refresh)",
|
|
587
|
+
time_since_last / 60,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Attempt refresh
|
|
591
|
+
result = refresh_access_token()
|
|
592
|
+
if result:
|
|
593
|
+
_last_refresh_time = current_time
|
|
594
|
+
logger.info("Token refresh successful, next refresh in 30 minutes")
|
|
595
|
+
else:
|
|
596
|
+
# Even on failure, update the timestamp to avoid hammering the API
|
|
597
|
+
# We'll retry on the next 30-min interval
|
|
598
|
+
_last_refresh_time = current_time
|
|
599
|
+
logger.warning("Token refresh failed, will retry in 30 minutes")
|
|
600
|
+
|
|
601
|
+
return True
|
code_puppy/terminal_utils.py
CHANGED
|
@@ -87,16 +87,20 @@ def reset_windows_console_mode() -> None:
|
|
|
87
87
|
|
|
88
88
|
|
|
89
89
|
def reset_windows_terminal_full() -> None:
|
|
90
|
-
"""Perform a full Windows terminal reset (
|
|
90
|
+
"""Perform a full Windows terminal reset (console mode + ANSI).
|
|
91
91
|
|
|
92
|
-
Combines both
|
|
92
|
+
Combines both console mode reset and ANSI reset for complete
|
|
93
93
|
terminal state restoration after interrupts.
|
|
94
|
+
|
|
95
|
+
IMPORTANT: Console mode must be reset FIRST to re-enable
|
|
96
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING, otherwise the ANSI escape
|
|
97
|
+
sequences will be printed as literal text (e.g., '[0m').
|
|
94
98
|
"""
|
|
95
99
|
if platform.system() != "Windows":
|
|
96
100
|
return
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
reset_windows_console_mode() # Must be first! Enables ANSI processing
|
|
103
|
+
reset_windows_terminal_ansi() # Now ANSI escapes will be interpreted
|
|
100
104
|
|
|
101
105
|
|
|
102
106
|
def reset_unix_terminal() -> None:
|
|
@@ -3,7 +3,7 @@ code_puppy/__main__.py,sha256=pDVssJOWP8A83iFkxMLY9YteHYat0EyWDQqMkKHpWp4,203
|
|
|
3
3
|
code_puppy/callbacks.py,sha256=hqTV--dNxG5vwWWm3MrEjmb8MZuHFFdmHePl23NXPHk,8621
|
|
4
4
|
code_puppy/chatgpt_codex_client.py,sha256=Om0ANB_kpHubhCwNzF9ENf8RvKBqs0IYzBLl_SNw0Vk,9833
|
|
5
5
|
code_puppy/claude_cache_client.py,sha256=hZr_YtXZSQvBoJFtRbbecKucYqJgoMopqUmm0IxFYGY,6071
|
|
6
|
-
code_puppy/cli_runner.py,sha256=
|
|
6
|
+
code_puppy/cli_runner.py,sha256=9ioQkdt-J0ifG_b7tGn_aulde9RIJBOIv8Df_QGROx0,29991
|
|
7
7
|
code_puppy/config.py,sha256=qqeJrQP7gqADqeYqVzfksP7NYGROLrBQCuYic5PuQfY,52295
|
|
8
8
|
code_puppy/error_logging.py,sha256=a80OILCUtJhexI6a9GM-r5LqIdjvSRzggfgPp2jv1X0,3297
|
|
9
9
|
code_puppy/gemini_code_assist.py,sha256=KGS7sO5OLc83nDF3xxS-QiU6vxW9vcm6hmzilu79Ef8,13867
|
|
@@ -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=
|
|
24
|
+
code_puppy/terminal_utils.py,sha256=P6ProVD_3nHW0zHNRIMWTcg8HvBk9e0ir5bhUgJpT5k,3921
|
|
25
25
|
code_puppy/version_checker.py,sha256=aq2Mwxl1CR9sEFBgrPt3OQOowLOBUp9VaQYWJhuUv8Q,1780
|
|
26
26
|
code_puppy/agents/__init__.py,sha256=PtPB7Z5MSwmUKipgt_qxvIuGggcuVaYwNbnp1UP4tPc,518
|
|
27
27
|
code_puppy/agents/agent_c_reviewer.py,sha256=1kO_89hcrhlS4sJ6elDLSEx-h43jAaWGgvIL0SZUuKo,8214
|
|
@@ -128,9 +128,9 @@ code_puppy/plugins/claude_code_oauth/README.md,sha256=76nHhMlhk61DZa5g0Q2fc0Atpp
|
|
|
128
128
|
code_puppy/plugins/claude_code_oauth/SETUP.md,sha256=lnGzofPLogBy3oPPFLv5_cZ7vjg_GYrIyYnF-EoTJKg,3278
|
|
129
129
|
code_puppy/plugins/claude_code_oauth/__init__.py,sha256=mCcOU-wM7LNCDjr-w-WLPzom8nTF1UNt4nqxGE6Rt0k,187
|
|
130
130
|
code_puppy/plugins/claude_code_oauth/config.py,sha256=DjGySCkvjSGZds6DYErLMAi3TItt8iSLGvyJN98nSEM,2013
|
|
131
|
-
code_puppy/plugins/claude_code_oauth/register_callbacks.py,sha256=
|
|
131
|
+
code_puppy/plugins/claude_code_oauth/register_callbacks.py,sha256=szexPtlFvCiR098wJXXvbl0PUyr330Y5TUrq7ey6Ntc,10579
|
|
132
132
|
code_puppy/plugins/claude_code_oauth/test_plugin.py,sha256=yQy4EeZl4bjrcog1d8BjknoDTRK75mRXXvkSQJYSSEM,9286
|
|
133
|
-
code_puppy/plugins/claude_code_oauth/utils.py,sha256=
|
|
133
|
+
code_puppy/plugins/claude_code_oauth/utils.py,sha256=vkkwsrcyBps25C_zvuK2Mquh5CMReQPReDRZ_gVOIZE,19901
|
|
134
134
|
code_puppy/plugins/customizable_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
135
135
|
code_puppy/plugins/customizable_commands/register_callbacks.py,sha256=zVMfIzr--hVn0IOXxIicbmgj2s-HZUgtrOc0NCDOnDw,5183
|
|
136
136
|
code_puppy/plugins/example_custom_command/README.md,sha256=5c5Zkm7CW6BDSfe3WoLU7GW6t5mjjYAbu9-_pu-b3p4,8244
|
|
@@ -159,10 +159,10 @@ code_puppy/tools/browser/browser_scripts.py,sha256=sNb8eLEyzhasy5hV4B9OjM8yIVMLV
|
|
|
159
159
|
code_puppy/tools/browser/browser_workflows.py,sha256=nitW42vCf0ieTX1gLabozTugNQ8phtoFzZbiAhw1V90,6491
|
|
160
160
|
code_puppy/tools/browser/camoufox_manager.py,sha256=RZjGOEftE5sI_tsercUyXFSZI2wpStXf-q0PdYh2G3I,8680
|
|
161
161
|
code_puppy/tools/browser/vqa_agent.py,sha256=DBn9HKloILqJSTSdNZzH_PYWT0B2h9VwmY6akFQI_uU,2913
|
|
162
|
-
code_puppy-0.0.
|
|
163
|
-
code_puppy-0.0.
|
|
164
|
-
code_puppy-0.0.
|
|
165
|
-
code_puppy-0.0.
|
|
166
|
-
code_puppy-0.0.
|
|
167
|
-
code_puppy-0.0.
|
|
168
|
-
code_puppy-0.0.
|
|
162
|
+
code_puppy-0.0.331.data/data/code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
|
|
163
|
+
code_puppy-0.0.331.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
|
|
164
|
+
code_puppy-0.0.331.dist-info/METADATA,sha256=a_tpJr_TVCZGLAjZiyqiF5mG51Kni05wqqQuzVQmy-w,28854
|
|
165
|
+
code_puppy-0.0.331.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
166
|
+
code_puppy-0.0.331.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
|
|
167
|
+
code_puppy-0.0.331.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
|
|
168
|
+
code_puppy-0.0.331.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|