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 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
@@ -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 (ANSI + console mode).
90
+ """Perform a full Windows terminal reset (console mode + ANSI).
91
91
 
92
- Combines both ANSI reset and console mode reset for complete
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
- reset_windows_terminal_ansi()
99
- reset_windows_console_mode()
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-puppy
3
- Version: 0.0.329
3
+ Version: 0.0.331
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
@@ -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=qIwmg3Di2dkxoRxsyd6o1uqd_wqkbOAe1OLQpzw80BU,29666
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=oa8SF7Pel4o15Erd7a7vA7Y9wi6SJx196qxXITebDqA,3650
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=0NeX1hhkYIlVfPmjZ1xmcf1yueDAJh_FMUmvJlxSO-E,10057
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=wDaOU21zB3y6PWkuMXwE4mFjQuffyDae-vXysPTS-w8,13438
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.329.data/data/code_puppy/models.json,sha256=IPABdOrDw2OZJxa0XGBWSWmBRerV6_pIEmKVLRtUbAk,3105
163
- code_puppy-0.0.329.data/data/code_puppy/models_dev_api.json,sha256=wHjkj-IM_fx1oHki6-GqtOoCrRMR0ScK0f-Iz0UEcy8,548187
164
- code_puppy-0.0.329.dist-info/METADATA,sha256=PacPOzPdJBJx5yDJC2H5IJHS_Otpm7J0dGeyoDburBA,28854
165
- code_puppy-0.0.329.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
166
- code_puppy-0.0.329.dist-info/entry_points.txt,sha256=Tp4eQC99WY3HOKd3sdvb22vZODRq0XkZVNpXOag_KdI,91
167
- code_puppy-0.0.329.dist-info/licenses/LICENSE,sha256=31u8x0SPgdOq3izJX41kgFazWsM43zPEF9eskzqbJMY,1075
168
- code_puppy-0.0.329.dist-info/RECORD,,
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,,