code-puppy 0.0.325__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.
Files changed (52) hide show
  1. code_puppy/agents/base_agent.py +110 -124
  2. code_puppy/claude_cache_client.py +208 -2
  3. code_puppy/cli_runner.py +152 -32
  4. code_puppy/command_line/add_model_menu.py +4 -0
  5. code_puppy/command_line/autosave_menu.py +23 -24
  6. code_puppy/command_line/clipboard.py +527 -0
  7. code_puppy/command_line/colors_menu.py +5 -0
  8. code_puppy/command_line/config_commands.py +24 -1
  9. code_puppy/command_line/core_commands.py +85 -0
  10. code_puppy/command_line/diff_menu.py +5 -0
  11. code_puppy/command_line/mcp/custom_server_form.py +4 -0
  12. code_puppy/command_line/mcp/install_menu.py +5 -1
  13. code_puppy/command_line/model_settings_menu.py +5 -0
  14. code_puppy/command_line/motd.py +13 -7
  15. code_puppy/command_line/onboarding_slides.py +180 -0
  16. code_puppy/command_line/onboarding_wizard.py +340 -0
  17. code_puppy/command_line/prompt_toolkit_completion.py +118 -0
  18. code_puppy/config.py +3 -2
  19. code_puppy/http_utils.py +201 -279
  20. code_puppy/keymap.py +10 -8
  21. code_puppy/mcp_/managed_server.py +7 -11
  22. code_puppy/messaging/messages.py +3 -0
  23. code_puppy/messaging/rich_renderer.py +114 -22
  24. code_puppy/model_factory.py +102 -15
  25. code_puppy/models.json +2 -2
  26. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  27. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  28. code_puppy/plugins/antigravity_oauth/antigravity_model.py +668 -0
  29. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  30. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  31. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  32. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  33. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  34. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  35. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  36. code_puppy/plugins/antigravity_oauth/transport.py +664 -0
  37. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  38. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +2 -0
  39. code_puppy/plugins/claude_code_oauth/register_callbacks.py +2 -0
  40. code_puppy/plugins/claude_code_oauth/utils.py +126 -7
  41. code_puppy/reopenable_async_client.py +8 -8
  42. code_puppy/terminal_utils.py +295 -3
  43. code_puppy/tools/command_runner.py +43 -54
  44. code_puppy/tools/common.py +3 -9
  45. code_puppy/uvx_detection.py +242 -0
  46. {code_puppy-0.0.325.data → code_puppy-0.0.341.data}/data/code_puppy/models.json +2 -2
  47. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/METADATA +26 -49
  48. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/RECORD +52 -36
  49. {code_puppy-0.0.325.data → code_puppy-0.0.341.data}/data/code_puppy/models_dev_api.json +0 -0
  50. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/WHEEL +0 -0
  51. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/entry_points.txt +0 -0
  52. {code_puppy-0.0.325.dist-info → code_puppy-0.0.341.dist-info}/licenses/LICENSE +0 -0
code_puppy/cli_runner.py CHANGED
@@ -17,14 +17,12 @@ 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
27
24
  from code_puppy.command_line.attachments import parse_prompt_attachments
25
+ from code_puppy.command_line.clipboard import get_clipboard_manager
28
26
  from code_puppy.config import (
29
27
  AUTOSAVE_DIR,
30
28
  COMMAND_HISTORY_FILE,
@@ -43,6 +41,7 @@ from code_puppy.keymap import (
43
41
  )
44
42
  from code_puppy.messaging import emit_info
45
43
  from code_puppy.terminal_utils import (
44
+ print_truecolor_warning,
46
45
  reset_unix_terminal,
47
46
  reset_windows_terminal_ansi,
48
47
  reset_windows_terminal_full,
@@ -91,7 +90,6 @@ async def main():
91
90
  "command", nargs="*", help="Run a single command (deprecated, use -p instead)"
92
91
  )
93
92
  args = parser.parse_args()
94
- from rich.console import Console
95
93
 
96
94
  from code_puppy.messaging import (
97
95
  RichConsoleRenderer,
@@ -146,6 +144,9 @@ async def main():
146
144
  except ImportError:
147
145
  emit_system_message("🐶 Code Puppy is Loading...")
148
146
 
147
+ # Truecolor warning moved to interactive_mode() so it prints LAST
148
+ # after all the help stuff - max visibility for the ugly red box!
149
+
149
150
  available_port = find_available_port()
150
151
  if available_port is None:
151
152
  emit_error("No available ports in range 8090-9010!")
@@ -171,6 +172,45 @@ async def main():
171
172
  emit_error(str(e))
172
173
  sys.exit(1)
173
174
 
175
+ # Show uvx detection notice if we're on Windows + uvx
176
+ # Also disable Ctrl+C at the console level to prevent terminal bricking
177
+ try:
178
+ from code_puppy.uvx_detection import should_use_alternate_cancel_key
179
+
180
+ if should_use_alternate_cancel_key():
181
+ from code_puppy.terminal_utils import (
182
+ disable_windows_ctrl_c,
183
+ set_keep_ctrl_c_disabled,
184
+ )
185
+
186
+ # Disable Ctrl+C at the console input level
187
+ # This prevents Ctrl+C from being processed as a signal at all
188
+ disable_windows_ctrl_c()
189
+
190
+ # Set flag to keep it disabled (prompt_toolkit may re-enable it)
191
+ set_keep_ctrl_c_disabled(True)
192
+
193
+ # Use print directly - emit_system_message can get cleared by ANSI codes
194
+ print(
195
+ "🔧 Detected uvx launch on Windows - using Ctrl+K for cancellation "
196
+ "(Ctrl+C is disabled to prevent terminal issues)"
197
+ )
198
+
199
+ # Also install a SIGINT handler as backup
200
+ import signal
201
+
202
+ from code_puppy.terminal_utils import reset_windows_terminal_full
203
+
204
+ def _uvx_protective_sigint_handler(_sig, _frame):
205
+ """Protective SIGINT handler for Windows+uvx."""
206
+ reset_windows_terminal_full()
207
+ # Re-disable Ctrl+C in case something re-enabled it
208
+ disable_windows_ctrl_c()
209
+
210
+ signal.signal(signal.SIGINT, _uvx_protective_sigint_handler)
211
+ except ImportError:
212
+ pass # uvx_detection module not available, ignore
213
+
174
214
  # Load API keys from puppy.cfg into environment variables
175
215
  from code_puppy.config import load_api_keys_to_environment
176
216
 
@@ -315,6 +355,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
315
355
  emit_system_message(
316
356
  "Type @ for path completion, or /model to pick a model. Toggle multiline with Alt+M or F2; newline: Ctrl+J."
317
357
  )
358
+ emit_system_message("Paste images: Ctrl+V (even on Mac!), F3, or /paste command.")
359
+ import platform
360
+
361
+ if platform.system() == "Darwin":
362
+ emit_system_message(
363
+ "💡 macOS tip: Use Ctrl+V (not Cmd+V) to paste images in terminal."
364
+ )
318
365
  cancel_key = get_cancel_agent_display_name()
319
366
  emit_system_message(
320
367
  f"Press {cancel_key} during processing to cancel the current task or inference. Use Ctrl+X to interrupt running shell commands."
@@ -325,6 +372,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
325
372
  emit_system_message(
326
373
  "Use /diff to configure diff highlighting colors for file changes."
327
374
  )
375
+ emit_system_message("To re-run the tutorial, use /tutorial.")
328
376
  try:
329
377
  from code_puppy.command_line.motd import print_motd
330
378
 
@@ -334,6 +382,10 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
334
382
 
335
383
  emit_warning(f"MOTD error: {e}")
336
384
 
385
+ # Print truecolor warning LAST so it's the most visible thing on startup
386
+ # Big ugly red box should be impossible to miss! 🔴
387
+ print_truecolor_warning(display_console)
388
+
337
389
  # Initialize the runtime agent manager
338
390
  if initial_command:
339
391
  from code_puppy.agents import get_current_agent
@@ -417,6 +469,45 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
417
469
 
418
470
  # Autosave loading is now manual - use /autosave_load command
419
471
 
472
+ # Auto-run tutorial on first startup
473
+ try:
474
+ from code_puppy.command_line.onboarding_wizard import should_show_onboarding
475
+
476
+ if should_show_onboarding():
477
+ import asyncio
478
+ import concurrent.futures
479
+
480
+ from code_puppy.command_line.onboarding_wizard import run_onboarding_wizard
481
+ from code_puppy.config import set_model_name
482
+ from code_puppy.messaging import emit_info
483
+
484
+ with concurrent.futures.ThreadPoolExecutor() as executor:
485
+ future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
486
+ result = future.result(timeout=300)
487
+
488
+ if result == "chatgpt":
489
+ emit_info("🔐 Starting ChatGPT OAuth flow...")
490
+ from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
491
+
492
+ run_oauth_flow()
493
+ set_model_name("chatgpt-gpt-5.2-codex")
494
+ elif result == "claude":
495
+ emit_info("🔐 Starting Claude Code OAuth flow...")
496
+ from code_puppy.plugins.claude_code_oauth.register_callbacks import (
497
+ _perform_authentication,
498
+ )
499
+
500
+ _perform_authentication()
501
+ set_model_name("claude-code-claude-opus-4-5-20251101")
502
+ elif result == "completed":
503
+ emit_info("🎉 Tutorial complete! Happy coding!")
504
+ elif result == "skipped":
505
+ emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
506
+ except Exception as e:
507
+ from code_puppy.messaging import emit_warning
508
+
509
+ emit_warning(f"Tutorial auto-start failed: {e}")
510
+
420
511
  # Track the current agent task for cancellation on quit
421
512
  current_agent_task = None
422
513
 
@@ -440,6 +531,15 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
440
531
  task = await get_input_with_combined_completion(
441
532
  get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
442
533
  )
534
+
535
+ # Windows+uvx: Re-disable Ctrl+C after prompt_toolkit
536
+ # (prompt_toolkit restores console mode which re-enables Ctrl+C)
537
+ try:
538
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
539
+
540
+ ensure_ctrl_c_disabled()
541
+ except ImportError:
542
+ pass
443
543
  except ImportError:
444
544
  # Fall back to basic input if prompt_toolkit is not available
445
545
  task = input(">>> ")
@@ -479,6 +579,7 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
479
579
 
480
580
  # Check for clear command (supports both `clear` and `/clear`)
481
581
  if task.strip().lower() in ("clear", "/clear"):
582
+ from code_puppy.command_line.clipboard import get_clipboard_manager
482
583
  from code_puppy.messaging import (
483
584
  emit_info,
484
585
  emit_system_message,
@@ -491,6 +592,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
491
592
  emit_warning("Conversation history cleared!")
492
593
  emit_system_message("The agent will not remember previous interactions.")
493
594
  emit_info(f"Auto-save session rotated to: {new_session_id}")
595
+
596
+ # Also clear pending clipboard images
597
+ clipboard_manager = get_clipboard_manager()
598
+ clipboard_count = clipboard_manager.get_pending_count()
599
+ clipboard_manager.clear_pending()
600
+ if clipboard_count > 0:
601
+ emit_info(f"Cleared {clipboard_count} pending clipboard image(s)")
494
602
  continue
495
603
 
496
604
  # Parse attachments first so leading paths aren't misread as commands
@@ -591,8 +699,6 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
591
699
  save_command_to_history(task)
592
700
 
593
701
  try:
594
- prettier_code_blocks()
595
-
596
702
  # No need to get agent directly - use manager's run methods
597
703
 
598
704
  # Use our custom helper to enable attachment handling with spinner support
@@ -605,6 +711,13 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
605
711
  if result is None:
606
712
  # Windows-specific: Reset terminal state after cancellation
607
713
  reset_windows_terminal_ansi()
714
+ # Re-disable Ctrl+C if needed (uvx mode)
715
+ try:
716
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
717
+
718
+ ensure_ctrl_c_disabled()
719
+ except ImportError:
720
+ pass
608
721
  continue
609
722
  # Get the structured response
610
723
  agent_response = result.output
@@ -645,27 +758,14 @@ async def interactive_mode(message_renderer, initial_command: str = None) -> Non
645
758
 
646
759
  auto_save_session_if_enabled()
647
760
 
761
+ # Re-disable Ctrl+C if needed (uvx mode) - must be done after
762
+ # each iteration as various operations may restore console mode
763
+ try:
764
+ from code_puppy.terminal_utils import ensure_ctrl_c_disabled
648
765
 
649
- def prettier_code_blocks():
650
- """Configure Rich to use prettier code block rendering."""
651
-
652
- class SimpleCodeBlock(CodeBlock):
653
- def __rich_console__(
654
- self, console: Console, options: ConsoleOptions
655
- ) -> RenderResult:
656
- code = str(self.text).rstrip()
657
- yield Text(self.lexer_name, style="dim")
658
- syntax = Syntax(
659
- code,
660
- self.lexer_name,
661
- theme=self.theme,
662
- background_color="default",
663
- line_numbers=True,
664
- )
665
- yield syntax
666
- yield Text(f"/{self.lexer_name}", style="dim")
667
-
668
- Markdown.elements["fence"] = SimpleCodeBlock
766
+ ensure_ctrl_c_disabled()
767
+ except ImportError:
768
+ pass
669
769
 
670
770
 
671
771
  async def run_prompt_with_attachments(
@@ -681,6 +781,7 @@ async def run_prompt_with_attachments(
681
781
  tuple: (result, task) where result is the agent response and task is the asyncio task
682
782
  """
683
783
  import asyncio
784
+ import re
684
785
 
685
786
  from code_puppy.messaging import emit_system_message, emit_warning
686
787
 
@@ -689,21 +790,41 @@ async def run_prompt_with_attachments(
689
790
  for warning in processed_prompt.warnings:
690
791
  emit_warning(warning)
691
792
 
793
+ # Get clipboard images and merge with file attachments
794
+ clipboard_manager = get_clipboard_manager()
795
+ clipboard_images = clipboard_manager.get_pending_images()
796
+
797
+ # Clear pending clipboard images after retrieval
798
+ clipboard_manager.clear_pending()
799
+
800
+ # Build summary of all attachments
692
801
  summary_parts = []
693
802
  if processed_prompt.attachments:
694
- summary_parts.append(f"binary files: {len(processed_prompt.attachments)}")
803
+ summary_parts.append(f"files: {len(processed_prompt.attachments)}")
804
+ if clipboard_images:
805
+ summary_parts.append(f"clipboard images: {len(clipboard_images)}")
695
806
  if processed_prompt.link_attachments:
696
807
  summary_parts.append(f"urls: {len(processed_prompt.link_attachments)}")
697
808
  if summary_parts:
698
809
  emit_system_message("Attachments detected -> " + ", ".join(summary_parts))
699
810
 
700
- if not processed_prompt.prompt:
811
+ # Clean up clipboard placeholders from the prompt text
812
+ cleaned_prompt = processed_prompt.prompt
813
+ if clipboard_images and cleaned_prompt:
814
+ cleaned_prompt = re.sub(
815
+ r"\[📋 clipboard image \d+\]\s*", "", cleaned_prompt
816
+ ).strip()
817
+
818
+ if not cleaned_prompt:
701
819
  emit_warning(
702
820
  "Prompt is empty after removing attachments; add instructions and retry."
703
821
  )
704
822
  return None, None
705
823
 
824
+ # Combine file attachments with clipboard images
706
825
  attachments = [attachment.content for attachment in processed_prompt.attachments]
826
+ attachments.extend(clipboard_images) # Add clipboard images
827
+
707
828
  link_attachments = [link.url_part for link in processed_prompt.link_attachments]
708
829
 
709
830
  # IMPORTANT: Set the shared console on the agent so that streaming output
@@ -715,7 +836,7 @@ async def run_prompt_with_attachments(
715
836
  # Create the agent task first so we can track and cancel it
716
837
  agent_task = asyncio.create_task(
717
838
  agent.run_with_mcp(
718
- processed_prompt.prompt,
839
+ cleaned_prompt, # Use cleaned prompt (clipboard placeholders removed)
719
840
  attachments=attachments,
720
841
  link_attachments=link_attachments,
721
842
  )
@@ -790,6 +911,5 @@ def main_entry():
790
911
  DBOS.destroy()
791
912
  return 0
792
913
  finally:
793
- # Reset terminal on all platforms for clean state
794
- reset_windows_terminal_full() # Safe no-op on non-Windows
914
+ # Reset terminal on Unix-like systems (not Windows)
795
915
  reset_unix_terminal()
@@ -995,6 +995,10 @@ class AddModelMenu:
995
995
  # Reset awaiting input flag
996
996
  set_awaiting_user_input(False)
997
997
 
998
+ # Clear exit message (unless we're about to prompt for more input)
999
+ if self.result not in ("pending_credentials", "pending_custom_model"):
1000
+ emit_info("✓ Exited model browser")
1001
+
998
1002
  # Handle unsupported provider
999
1003
  if self.result == "unsupported" and self.current_provider:
1000
1004
  reason = UNSUPPORTED_PROVIDERS.get(
@@ -69,12 +69,21 @@ def _get_session_entries(base_dir: Path) -> List[Tuple[str, dict]]:
69
69
 
70
70
 
71
71
  def _extract_last_user_message(history: list) -> str:
72
- """Extract the most recent user message from history."""
72
+ """Extract the most recent user message from history.
73
+
74
+ Joins all content parts from the message since messages can have
75
+ multiple parts (e.g., text + attachments, multi-part prompts).
76
+ """
73
77
  # Walk backwards through history to find last user message
74
78
  for msg in reversed(history):
79
+ content_parts = []
75
80
  for part in msg.parts:
76
81
  if hasattr(part, "content"):
77
- return part.content
82
+ content = part.content
83
+ if isinstance(content, str) and content.strip():
84
+ content_parts.append(content)
85
+ if content_parts:
86
+ return "\n\n".join(content_parts)
78
87
  return "[No messages found]"
79
88
 
80
89
 
@@ -298,19 +307,13 @@ def _render_message_browser_panel(
298
307
  # Don't override Rich's ANSI styling - use empty style
299
308
  text_color = ""
300
309
 
301
- # Truncate if too long (max 35 lines)
302
- message_lines = rendered.split("\n")[:35]
303
- is_truncated = len(rendered.split("\n")) > 35
310
+ # Show full message without truncation
311
+ message_lines = rendered.split("\n")
304
312
 
305
313
  for line in message_lines:
306
314
  lines.append((text_color, f" {line}"))
307
315
  lines.append(("", "\n"))
308
316
 
309
- if is_truncated:
310
- lines.append(("", "\n"))
311
- lines.append(("fg:yellow", " ... truncated (message too long)"))
312
- lines.append(("", "\n"))
313
-
314
317
  except Exception as e:
315
318
  lines.append(("fg:red", f" Error rendering message: {e}"))
316
319
  lines.append(("", "\n"))
@@ -359,7 +362,7 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
359
362
  lines.append(("", "\n\n"))
360
363
 
361
364
  lines.append(("bold", " Last Message:"))
362
- lines.append(("fg:ansibrightblack", " (press 'e' to browse all)"))
365
+ lines.append(("fg:ansibrightblack", " (press 'e' to browse full history)"))
363
366
  lines.append(("", "\n"))
364
367
 
365
368
  # Try to load and preview the last message
@@ -367,15 +370,11 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
367
370
  history = load_session(session_name, base_dir)
368
371
  last_message = _extract_last_user_message(history)
369
372
 
370
- # Check if original message is long (before Rich processing)
371
- original_lines = last_message.split("\n") if last_message else []
372
- is_long = len(original_lines) > 30
373
-
374
- # Render markdown with rich but strip ANSI codes
373
+ # Render markdown with rich
375
374
  console = Console(
376
375
  file=StringIO(),
377
376
  legacy_windows=False,
378
- no_color=False, # Disable ANSI color codes
377
+ no_color=False,
379
378
  force_terminal=False,
380
379
  width=76,
381
380
  )
@@ -383,19 +382,14 @@ def _render_preview_panel(base_dir: Path, entry: Optional[Tuple[str, dict]]) ->
383
382
  console.print(md)
384
383
  rendered = console.file.getvalue()
385
384
 
386
- # Truncate if too long (max 30 lines for bigger preview)
387
- message_lines = rendered.split("\n")[:30]
385
+ # Show full message without truncation
386
+ message_lines = rendered.split("\n")
388
387
 
389
388
  for line in message_lines:
390
389
  # Rich already rendered the markdown, just display it dimmed
391
390
  lines.append(("fg:ansibrightblack", f" {line}"))
392
391
  lines.append(("", "\n"))
393
392
 
394
- if is_long:
395
- lines.append(("", "\n"))
396
- lines.append(("fg:yellow", " ... truncated"))
397
- lines.append(("", "\n"))
398
-
399
393
  except Exception as e:
400
394
  lines.append(("fg:red", f" Error loading preview: {e}"))
401
395
  lines.append(("", "\n"))
@@ -603,4 +597,9 @@ async def interactive_autosave_picker() -> Optional[str]:
603
597
  # Reset awaiting input flag
604
598
  set_awaiting_user_input(False)
605
599
 
600
+ # Clear exit message
601
+ from code_puppy.messaging import emit_info
602
+
603
+ emit_info("✓ Exited session browser")
604
+
606
605
  return result[0]