code-puppy 0.0.287__py3-none-any.whl → 0.0.323__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 (110) hide show
  1. code_puppy/__init__.py +3 -1
  2. code_puppy/agents/agent_code_puppy.py +5 -4
  3. code_puppy/agents/agent_creator_agent.py +22 -18
  4. code_puppy/agents/agent_manager.py +2 -2
  5. code_puppy/agents/base_agent.py +496 -102
  6. code_puppy/callbacks.py +8 -0
  7. code_puppy/chatgpt_codex_client.py +283 -0
  8. code_puppy/cli_runner.py +795 -0
  9. code_puppy/command_line/add_model_menu.py +19 -16
  10. code_puppy/command_line/attachments.py +10 -5
  11. code_puppy/command_line/autosave_menu.py +269 -41
  12. code_puppy/command_line/colors_menu.py +515 -0
  13. code_puppy/command_line/command_handler.py +10 -24
  14. code_puppy/command_line/config_commands.py +106 -25
  15. code_puppy/command_line/core_commands.py +32 -20
  16. code_puppy/command_line/mcp/add_command.py +3 -16
  17. code_puppy/command_line/mcp/base.py +0 -3
  18. code_puppy/command_line/mcp/catalog_server_installer.py +15 -15
  19. code_puppy/command_line/mcp/custom_server_form.py +66 -5
  20. code_puppy/command_line/mcp/custom_server_installer.py +17 -17
  21. code_puppy/command_line/mcp/edit_command.py +15 -22
  22. code_puppy/command_line/mcp/handler.py +7 -2
  23. code_puppy/command_line/mcp/help_command.py +2 -2
  24. code_puppy/command_line/mcp/install_command.py +10 -14
  25. code_puppy/command_line/mcp/install_menu.py +2 -6
  26. code_puppy/command_line/mcp/list_command.py +2 -2
  27. code_puppy/command_line/mcp/logs_command.py +174 -65
  28. code_puppy/command_line/mcp/remove_command.py +2 -2
  29. code_puppy/command_line/mcp/restart_command.py +7 -2
  30. code_puppy/command_line/mcp/search_command.py +16 -10
  31. code_puppy/command_line/mcp/start_all_command.py +16 -6
  32. code_puppy/command_line/mcp/start_command.py +12 -10
  33. code_puppy/command_line/mcp/status_command.py +4 -5
  34. code_puppy/command_line/mcp/stop_all_command.py +5 -1
  35. code_puppy/command_line/mcp/stop_command.py +6 -4
  36. code_puppy/command_line/mcp/test_command.py +2 -2
  37. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  38. code_puppy/command_line/model_settings_menu.py +53 -7
  39. code_puppy/command_line/motd.py +1 -1
  40. code_puppy/command_line/pin_command_completion.py +82 -7
  41. code_puppy/command_line/prompt_toolkit_completion.py +32 -9
  42. code_puppy/command_line/session_commands.py +11 -4
  43. code_puppy/config.py +217 -53
  44. code_puppy/error_logging.py +118 -0
  45. code_puppy/gemini_code_assist.py +385 -0
  46. code_puppy/keymap.py +126 -0
  47. code_puppy/main.py +5 -745
  48. code_puppy/mcp_/__init__.py +17 -0
  49. code_puppy/mcp_/blocking_startup.py +63 -36
  50. code_puppy/mcp_/captured_stdio_server.py +1 -1
  51. code_puppy/mcp_/config_wizard.py +4 -4
  52. code_puppy/mcp_/dashboard.py +15 -6
  53. code_puppy/mcp_/managed_server.py +25 -5
  54. code_puppy/mcp_/manager.py +65 -0
  55. code_puppy/mcp_/mcp_logs.py +224 -0
  56. code_puppy/mcp_/registry.py +6 -6
  57. code_puppy/messaging/__init__.py +184 -2
  58. code_puppy/messaging/bus.py +610 -0
  59. code_puppy/messaging/commands.py +167 -0
  60. code_puppy/messaging/markdown_patches.py +57 -0
  61. code_puppy/messaging/message_queue.py +3 -3
  62. code_puppy/messaging/messages.py +470 -0
  63. code_puppy/messaging/renderers.py +43 -141
  64. code_puppy/messaging/rich_renderer.py +900 -0
  65. code_puppy/messaging/spinner/console_spinner.py +39 -2
  66. code_puppy/model_factory.py +292 -53
  67. code_puppy/model_utils.py +57 -48
  68. code_puppy/models.json +19 -5
  69. code_puppy/plugins/__init__.py +152 -10
  70. code_puppy/plugins/chatgpt_oauth/config.py +20 -12
  71. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +5 -6
  72. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +3 -3
  73. code_puppy/plugins/chatgpt_oauth/test_plugin.py +30 -13
  74. code_puppy/plugins/chatgpt_oauth/utils.py +180 -65
  75. code_puppy/plugins/claude_code_oauth/config.py +15 -11
  76. code_puppy/plugins/claude_code_oauth/register_callbacks.py +28 -0
  77. code_puppy/plugins/claude_code_oauth/utils.py +6 -1
  78. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  79. code_puppy/plugins/oauth_puppy_html.py +3 -0
  80. code_puppy/plugins/shell_safety/agent_shell_safety.py +1 -134
  81. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  82. code_puppy/plugins/shell_safety/register_callbacks.py +77 -3
  83. code_puppy/prompts/codex_system_prompt.md +310 -0
  84. code_puppy/pydantic_patches.py +131 -0
  85. code_puppy/session_storage.py +2 -1
  86. code_puppy/status_display.py +7 -5
  87. code_puppy/terminal_utils.py +126 -0
  88. code_puppy/tools/agent_tools.py +131 -70
  89. code_puppy/tools/browser/browser_control.py +10 -14
  90. code_puppy/tools/browser/browser_interactions.py +20 -28
  91. code_puppy/tools/browser/browser_locators.py +27 -29
  92. code_puppy/tools/browser/browser_navigation.py +9 -9
  93. code_puppy/tools/browser/browser_screenshot.py +12 -14
  94. code_puppy/tools/browser/browser_scripts.py +17 -29
  95. code_puppy/tools/browser/browser_workflows.py +24 -25
  96. code_puppy/tools/browser/camoufox_manager.py +22 -26
  97. code_puppy/tools/command_runner.py +410 -88
  98. code_puppy/tools/common.py +51 -38
  99. code_puppy/tools/file_modifications.py +98 -24
  100. code_puppy/tools/file_operations.py +113 -202
  101. code_puppy/version_checker.py +28 -13
  102. {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models.json +19 -5
  103. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/METADATA +3 -8
  104. code_puppy-0.0.323.dist-info/RECORD +168 -0
  105. code_puppy/tui_state.py +0 -55
  106. code_puppy-0.0.287.dist-info/RECORD +0 -153
  107. {code_puppy-0.0.287.data → code_puppy-0.0.323.data}/data/code_puppy/models_dev_api.json +0 -0
  108. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/WHEEL +0 -0
  109. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/entry_points.txt +0 -0
  110. {code_puppy-0.0.287.dist-info → code_puppy-0.0.323.dist-info}/licenses/LICENSE +0 -0
code_puppy/main.py CHANGED
@@ -1,750 +1,10 @@
1
- import argparse
2
- import asyncio
3
- import os
4
- import platform
5
- import subprocess
6
- import sys
7
- import time
8
- import traceback
9
- from pathlib import Path
1
+ """Main entry point for Code Puppy CLI.
10
2
 
11
- from pydantic_ai import _agent_graph
12
-
13
- _agent_graph._clean_message_history = lambda messages: messages
14
-
15
- from dbos import DBOS, DBOSConfig
16
- from rich.console import Console, ConsoleOptions, RenderResult
17
- from rich.markdown import CodeBlock, Markdown
18
- from rich.syntax import Syntax
19
- from rich.text import Text
20
-
21
- from code_puppy import __version__, callbacks, plugins
22
- from code_puppy.agents import get_current_agent
23
- from code_puppy.command_line.attachments import parse_prompt_attachments
24
- from code_puppy.config import (
25
- AUTOSAVE_DIR,
26
- COMMAND_HISTORY_FILE,
27
- DBOS_DATABASE_URL,
28
- ensure_config_exists,
29
- finalize_autosave_session,
30
- get_use_dbos,
31
- initialize_command_history_file,
32
- save_command_to_history,
33
- )
34
- from code_puppy.http_utils import find_available_port
35
- from code_puppy.messaging import emit_info
36
- from code_puppy.tools.common import console
37
-
38
- # message_history_accumulator and prune_interrupted_tool_calls have been moved to BaseAgent class
39
- from code_puppy.version_checker import default_version_mismatch_behavior
40
-
41
- plugins.load_plugin_callbacks()
42
-
43
-
44
- async def main():
45
- parser = argparse.ArgumentParser(description="Code Puppy - A code generation agent")
46
- parser.add_argument(
47
- "--version",
48
- "-v",
49
- action="version",
50
- version=f"{__version__}",
51
- help="Show version and exit",
52
- )
53
- parser.add_argument(
54
- "--interactive",
55
- "-i",
56
- action="store_true",
57
- help="Run in interactive mode",
58
- )
59
- parser.add_argument(
60
- "--prompt",
61
- "-p",
62
- type=str,
63
- help="Execute a single prompt and exit (no interactive mode)",
64
- )
65
- parser.add_argument(
66
- "--agent",
67
- "-a",
68
- type=str,
69
- help="Specify which agent to use (e.g., --agent code-puppy)",
70
- )
71
- parser.add_argument(
72
- "--model",
73
- "-m",
74
- type=str,
75
- help="Specify which model to use (e.g., --model gpt-5)",
76
- )
77
- parser.add_argument(
78
- "command", nargs="*", help="Run a single command (deprecated, use -p instead)"
79
- )
80
- args = parser.parse_args()
81
- from rich.console import Console
82
-
83
- from code_puppy.messaging import (
84
- SynchronousInteractiveRenderer,
85
- get_global_queue,
86
- )
87
-
88
- message_queue = get_global_queue()
89
- display_console = Console() # Separate console for rendering messages
90
- message_renderer = SynchronousInteractiveRenderer(message_queue, display_console)
91
- message_renderer.start()
92
-
93
- initialize_command_history_file()
94
- from code_puppy.messaging import emit_system_message
95
-
96
- # Show the awesome Code Puppy logo only in interactive mode (never in TUI mode)
97
- # Always check both command line args AND runtime TUI state for safety
98
- if args.interactive:
99
- try:
100
- import pyfiglet
101
-
102
- intro_lines = pyfiglet.figlet_format(
103
- "CODE PUPPY", font="ansi_shadow"
104
- ).split("\n")
105
-
106
- # Simple blue to green gradient (top to bottom)
107
- gradient_colors = ["bright_blue", "bright_cyan", "bright_green"]
108
- emit_system_message("\n\n")
109
-
110
- lines = []
111
- # Apply gradient line by line
112
- for line_num, line in enumerate(intro_lines):
113
- if line.strip():
114
- # Use line position to determine color (top blue, middle cyan, bottom green)
115
- color_idx = min(line_num // 2, len(gradient_colors) - 1)
116
- color = gradient_colors[color_idx]
117
- lines.append(f"[{color}]{line}[/{color}]")
118
- else:
119
- lines.append("")
120
- emit_system_message("\n".join(lines))
121
- except ImportError:
122
- emit_system_message("🐶 Code Puppy is Loading...")
123
-
124
- available_port = find_available_port()
125
- if available_port is None:
126
- error_msg = "Error: No available ports in range 8090-9010!"
127
- emit_system_message(f"[bold red]{error_msg}[/bold red]")
128
- return
129
-
130
- # Early model setting if specified via command line
131
- # This happens before ensure_config_exists() to ensure config is set up correctly
132
- early_model = None
133
- if args.model:
134
- early_model = args.model.strip()
135
- from code_puppy.config import set_model_name
136
-
137
- set_model_name(early_model)
138
-
139
- ensure_config_exists()
140
-
141
- # Load API keys from puppy.cfg into environment variables
142
- from code_puppy.config import load_api_keys_to_environment
143
-
144
- load_api_keys_to_environment()
145
-
146
- # Handle model validation from command line (validation happens here, setting was earlier)
147
- if args.model:
148
- from code_puppy.config import _validate_model_exists
149
-
150
- model_name = args.model.strip()
151
- try:
152
- # Validate that the model exists in models.json
153
- if not _validate_model_exists(model_name):
154
- from code_puppy.model_factory import ModelFactory
155
-
156
- models_config = ModelFactory.load_config()
157
- available_models = list(models_config.keys()) if models_config else []
158
-
159
- emit_system_message(
160
- f"[bold red]Error:[/bold red] Model '{model_name}' not found"
161
- )
162
- emit_system_message(f"Available models: {', '.join(available_models)}")
163
- sys.exit(1)
164
-
165
- # Model is valid, show confirmation (already set earlier)
166
- emit_system_message(f"🎯 Using model: {model_name}")
167
- except Exception as e:
168
- emit_system_message(
169
- f"[bold red]Error validating model:[/bold red] {str(e)}"
170
- )
171
- sys.exit(1)
172
-
173
- # Handle agent selection from command line
174
- if args.agent:
175
- from code_puppy.agents.agent_manager import (
176
- get_available_agents,
177
- set_current_agent,
178
- )
179
-
180
- agent_name = args.agent.lower()
181
- try:
182
- # First check if the agent exists by getting available agents
183
- available_agents = get_available_agents()
184
- if agent_name not in available_agents:
185
- emit_system_message(
186
- f"[bold red]Error:[/bold red] Agent '{agent_name}' not found"
187
- )
188
- emit_system_message(
189
- f"Available agents: {', '.join(available_agents.keys())}"
190
- )
191
- sys.exit(1)
192
-
193
- # Agent exists, set it
194
- set_current_agent(agent_name)
195
- emit_system_message(f"🤖 Using agent: {agent_name}")
196
- except Exception as e:
197
- emit_system_message(f"[bold red]Error setting agent:[/bold red] {str(e)}")
198
- sys.exit(1)
199
-
200
- current_version = __version__
201
-
202
- no_version_update = os.getenv("NO_VERSION_UPDATE", "").lower() in (
203
- "1",
204
- "true",
205
- "yes",
206
- "on",
207
- )
208
- if no_version_update:
209
- version_msg = f"Current version: {current_version}"
210
- update_disabled_msg = (
211
- "Update phase disabled because NO_VERSION_UPDATE is set to 1 or true"
212
- )
213
- emit_system_message(version_msg)
214
- emit_system_message(f"[dim]{update_disabled_msg}[/dim]")
215
- else:
216
- if len(callbacks.get_callbacks("version_check")):
217
- await callbacks.on_version_check(current_version)
218
- else:
219
- default_version_mismatch_behavior(current_version)
220
-
221
- await callbacks.on_startup()
222
-
223
- # Initialize DBOS if not disabled
224
- if get_use_dbos():
225
- # Append a Unix timestamp in ms to the version for uniqueness
226
- dbos_app_version = os.environ.get(
227
- "DBOS_APP_VERSION", f"{current_version}-{int(time.time() * 1000)}"
228
- )
229
- dbos_config: DBOSConfig = {
230
- "name": "dbos-code-puppy",
231
- "system_database_url": DBOS_DATABASE_URL,
232
- "run_admin_server": False,
233
- "conductor_key": os.environ.get(
234
- "DBOS_CONDUCTOR_KEY"
235
- ), # Optional, if set in env, connect to conductor
236
- "log_level": os.environ.get(
237
- "DBOS_LOG_LEVEL", "ERROR"
238
- ), # Default to ERROR level to suppress verbose logs
239
- "application_version": dbos_app_version, # Match DBOS app version to Code Puppy version
240
- }
241
- try:
242
- DBOS(config=dbos_config)
243
- DBOS.launch()
244
- except Exception as e:
245
- emit_system_message(f"[bold red]Error initializing DBOS:[/bold red] {e}")
246
- sys.exit(1)
247
- else:
248
- pass
249
-
250
- global shutdown_flag
251
- shutdown_flag = False
252
- try:
253
- initial_command = None
254
- prompt_only_mode = False
255
-
256
- if args.prompt:
257
- initial_command = args.prompt
258
- prompt_only_mode = True
259
- elif args.command:
260
- initial_command = " ".join(args.command)
261
- prompt_only_mode = False
262
-
263
- if prompt_only_mode:
264
- await execute_single_prompt(initial_command, message_renderer)
265
- else:
266
- # Default to interactive mode (no args = same as -i)
267
- await interactive_mode(message_renderer, initial_command=initial_command)
268
- finally:
269
- if message_renderer:
270
- message_renderer.stop()
271
- await callbacks.on_shutdown()
272
- if get_use_dbos():
273
- DBOS.destroy()
274
-
275
-
276
- # Add the file handling functionality for interactive mode
277
- async def interactive_mode(message_renderer, initial_command: str = None) -> None:
278
- from code_puppy.command_line.command_handler import handle_command
279
-
280
- """Run the agent in interactive mode."""
281
-
282
- display_console = message_renderer.console
283
- from code_puppy.messaging import emit_info, emit_system_message
284
-
285
- emit_system_message(
286
- "[dim]Type '/exit' or '/quit' to exit the interactive mode.[/dim]"
287
- )
288
- emit_system_message("[dim]Type 'clear' to reset the conversation history.[/dim]")
289
- emit_system_message("[dim]Type /help to view all commands[/dim]")
290
- emit_system_message(
291
- "[dim]Type [bold blue]@[/bold blue] for path completion, or [bold blue]/model[/bold blue] to pick a model. Toggle multiline with [bold blue]Alt+M[/bold blue] or [bold blue]F2[/bold blue]; newline: [bold blue]Ctrl+J[/bold blue].[/dim]"
292
- )
293
- emit_system_message(
294
- "[dim]Press [bold red]Ctrl+C[/bold red] during processing to cancel the current task or inference. Use [bold red]Ctrl+X[/bold red] to interrupt running shell commands.[/dim]"
295
- )
296
- emit_system_message(
297
- "[dim]Use [bold blue]/autosave_load[/bold blue] to manually load a previous autosave session.[/dim]"
298
- )
299
- emit_system_message(
300
- "[dim]Use [bold blue]/diff[/bold blue] to configure diff highlighting colors for file changes.[/dim]"
301
- )
302
- try:
303
- from code_puppy.command_line.motd import print_motd
304
-
305
- print_motd(console, force=False)
306
- except Exception as e:
307
- from code_puppy.messaging import emit_warning
308
-
309
- emit_warning(f"MOTD error: {e}")
310
-
311
- # Initialize the runtime agent manager
312
- if initial_command:
313
- from code_puppy.agents import get_current_agent
314
- from code_puppy.messaging import emit_info, emit_system_message
315
-
316
- agent = get_current_agent()
317
- emit_info(
318
- f"[bold blue]Processing initial command:[/bold blue] {initial_command}"
319
- )
320
-
321
- try:
322
- # Check if any tool is waiting for user input before showing spinner
323
- try:
324
- from code_puppy.tools.command_runner import is_awaiting_user_input
325
-
326
- awaiting_input = is_awaiting_user_input()
327
- except ImportError:
328
- awaiting_input = False
329
-
330
- # Run with or without spinner based on whether we're awaiting input
331
- response, agent_task = await run_prompt_with_attachments(
332
- agent,
333
- initial_command,
334
- spinner_console=display_console,
335
- use_spinner=not awaiting_input,
336
- )
337
- if response is not None:
338
- agent_response = response.output
339
-
340
- # Update the agent's message history with the complete conversation
341
- # including the final assistant response
342
- if hasattr(response, "all_messages"):
343
- agent.set_message_history(list(response.all_messages()))
344
-
345
- emit_system_message(
346
- f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response}"
347
- )
348
- emit_system_message("\n" + "=" * 50)
349
- emit_info("[bold green]🐶 Continuing in Interactive Mode[/bold green]")
350
- emit_system_message(
351
- "Your command and response are preserved in the conversation history."
352
- )
353
- emit_system_message("=" * 50 + "\n")
354
-
355
- except Exception as e:
356
- from code_puppy.messaging import emit_error
357
-
358
- emit_error(f"Error processing initial command: {str(e)}")
359
-
360
- # Check if prompt_toolkit is installed
361
- try:
362
- from code_puppy.command_line.prompt_toolkit_completion import (
363
- get_input_with_combined_completion,
364
- get_prompt_with_active_model,
365
- )
366
- except ImportError:
367
- from code_puppy.messaging import emit_warning
368
-
369
- emit_warning("Warning: prompt_toolkit not installed. Installing now...")
370
- try:
371
- import subprocess
372
-
373
- subprocess.check_call(
374
- [sys.executable, "-m", "pip", "install", "prompt_toolkit"]
375
- )
376
- from code_puppy.messaging import emit_success
377
-
378
- emit_success("Successfully installed prompt_toolkit")
379
- from code_puppy.command_line.prompt_toolkit_completion import (
380
- get_input_with_combined_completion,
381
- get_prompt_with_active_model,
382
- )
383
- except Exception as e:
384
- from code_puppy.messaging import emit_error, emit_warning
385
-
386
- emit_error(f"Error installing prompt_toolkit: {e}")
387
- emit_warning("Falling back to basic input without tab completion")
388
-
389
- # Autosave loading is now manual - use /autosave_load command
390
-
391
- # Track the current agent task for cancellation on quit
392
- current_agent_task = None
393
-
394
- while True:
395
- from code_puppy.agents.agent_manager import get_current_agent
396
- from code_puppy.messaging import emit_info
397
-
398
- # Get the custom prompt from the current agent, or use default
399
- current_agent = get_current_agent()
400
- user_prompt = current_agent.get_user_prompt() or "Enter your coding task:"
401
-
402
- emit_info(f"[dim][bold blue]{user_prompt}\n[/bold blue][/dim]")
403
-
404
- try:
405
- # Use prompt_toolkit for enhanced input with path completion
406
- try:
407
- # Use the async version of get_input_with_combined_completion
408
- task = await get_input_with_combined_completion(
409
- get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
410
- )
411
- except ImportError:
412
- # Fall back to basic input if prompt_toolkit is not available
413
- task = input(">>> ")
414
-
415
- except (KeyboardInterrupt, EOFError):
416
- # Handle Ctrl+C or Ctrl+D
417
- from code_puppy.messaging import emit_warning
418
-
419
- emit_warning("\nInput cancelled")
420
- continue
421
-
422
- # Check for exit commands (plain text or command form)
423
- if task.strip().lower() in ["exit", "quit"] or task.strip().lower() in [
424
- "/exit",
425
- "/quit",
426
- ]:
427
- import asyncio
428
-
429
- from code_puppy.messaging import emit_success
430
-
431
- emit_success("Goodbye!")
432
-
433
- # Cancel any running agent task for clean shutdown
434
- if current_agent_task and not current_agent_task.done():
435
- emit_info("Cancelling running agent task...")
436
- current_agent_task.cancel()
437
- try:
438
- await current_agent_task
439
- except asyncio.CancelledError:
440
- pass # Expected when cancelling
441
-
442
- # The renderer is stopped in the finally block of main().
443
- break
444
-
445
- # Check for clear command (supports both `clear` and `/clear`)
446
- if task.strip().lower() in ("clear", "/clear"):
447
- from code_puppy.messaging import (
448
- emit_info,
449
- emit_system_message,
450
- emit_warning,
451
- )
452
-
453
- agent = get_current_agent()
454
- new_session_id = finalize_autosave_session()
455
- agent.clear_message_history()
456
- emit_warning("Conversation history cleared!")
457
- emit_system_message(
458
- "[dim]The agent will not remember previous interactions.[/dim]"
459
- )
460
- emit_info(f"[dim]Auto-save session rotated to: {new_session_id}[/dim]")
461
- continue
462
-
463
- # Parse attachments first so leading paths aren't misread as commands
464
- processed_for_commands = parse_prompt_attachments(task)
465
- cleaned_for_commands = (processed_for_commands.prompt or "").strip()
466
-
467
- # Handle / commands based on cleaned prompt (after stripping attachments)
468
- if cleaned_for_commands.startswith("/"):
469
- try:
470
- command_result = handle_command(cleaned_for_commands)
471
- except Exception as e:
472
- from code_puppy.messaging import emit_error
473
-
474
- emit_error(f"Command error: {e}")
475
- # Continue interactive loop instead of exiting
476
- continue
477
- if command_result is True:
478
- continue
479
- elif isinstance(command_result, str):
480
- if command_result == "__AUTOSAVE_LOAD__":
481
- # Handle async autosave loading
482
- try:
483
- # Check if we're in a real interactive terminal
484
- # (not pexpect/tests) - interactive picker requires proper TTY
485
- use_interactive_picker = (
486
- sys.stdin.isatty() and sys.stdout.isatty()
487
- )
488
-
489
- # Allow environment variable override for tests
490
- if os.getenv("CODE_PUPPY_NO_TUI") == "1":
491
- use_interactive_picker = False
492
-
493
- if use_interactive_picker:
494
- # Use interactive picker for terminal sessions
495
- from code_puppy.agents.agent_manager import (
496
- get_current_agent,
497
- )
498
- from code_puppy.command_line.autosave_menu import (
499
- interactive_autosave_picker,
500
- )
501
- from code_puppy.config import (
502
- set_current_autosave_from_session_name,
503
- )
504
- from code_puppy.messaging import (
505
- emit_error,
506
- emit_success,
507
- emit_warning,
508
- )
509
- from code_puppy.session_storage import (
510
- load_session,
511
- restore_autosave_interactively,
512
- )
513
-
514
- chosen_session = await interactive_autosave_picker()
515
-
516
- if not chosen_session:
517
- emit_warning("Autosave load cancelled")
518
- continue
519
-
520
- # Load the session
521
- base_dir = Path(AUTOSAVE_DIR)
522
- history = load_session(chosen_session, base_dir)
523
-
524
- agent = get_current_agent()
525
- agent.set_message_history(history)
526
-
527
- # Set current autosave session
528
- set_current_autosave_from_session_name(chosen_session)
529
-
530
- total_tokens = sum(
531
- agent.estimate_tokens_for_message(msg)
532
- for msg in history
533
- )
534
- session_path = base_dir / f"{chosen_session}.pkl"
535
-
536
- emit_success(
537
- f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
538
- f"📁 From: {session_path}"
539
- )
540
- else:
541
- # Fall back to old text-based picker for tests/non-TTY environments
542
- await restore_autosave_interactively(Path(AUTOSAVE_DIR))
543
-
544
- except Exception as e:
545
- from code_puppy.messaging import emit_error
546
-
547
- emit_error(f"Failed to load autosave: {e}")
548
- continue
549
- else:
550
- # Command returned a prompt to execute
551
- task = command_result
552
- elif command_result is False:
553
- # Command not recognized, continue with normal processing
554
- pass
555
-
556
- if task.strip():
557
- # Write to the secret file for permanent history with timestamp
558
- save_command_to_history(task)
559
-
560
- try:
561
- prettier_code_blocks()
562
-
563
- # No need to get agent directly - use manager's run methods
564
-
565
- # Use our custom helper to enable attachment handling with spinner support
566
- result, current_agent_task = await run_prompt_with_attachments(
567
- current_agent,
568
- task,
569
- spinner_console=message_renderer.console,
570
- )
571
- # Check if the task was cancelled (but don't show message if we just killed processes)
572
- if result is None:
573
- continue
574
- # Get the structured response
575
- agent_response = result.output
576
- from code_puppy.messaging import emit_info
577
-
578
- emit_system_message(
579
- f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response}"
580
- )
581
-
582
- # Update the agent's message history with the complete conversation
583
- # including the final assistant response. The history_processors callback
584
- # may not capture the final message, so we use result.all_messages()
585
- # to ensure the autosave includes the complete conversation.
586
- if hasattr(result, "all_messages"):
587
- current_agent.set_message_history(list(result.all_messages()))
588
-
589
- # Ensure console output is flushed before next prompt
590
- # This fixes the issue where prompt doesn't appear after agent response
591
- display_console.file.flush() if hasattr(
592
- display_console.file, "flush"
593
- ) else None
594
- import time
595
-
596
- time.sleep(0.1) # Brief pause to ensure all messages are rendered
597
-
598
- except Exception:
599
- from code_puppy.messaging.queue_console import get_queue_console
600
-
601
- get_queue_console().print_exception()
602
-
603
- # Auto-save session if enabled (moved outside the try block to avoid being swallowed)
604
- from code_puppy.config import auto_save_session_if_enabled
605
-
606
- auto_save_session_if_enabled()
607
-
608
-
609
- def prettier_code_blocks():
610
- class SimpleCodeBlock(CodeBlock):
611
- def __rich_console__(
612
- self, console: Console, options: ConsoleOptions
613
- ) -> RenderResult:
614
- code = str(self.text).rstrip()
615
- yield Text(self.lexer_name, style="dim")
616
- syntax = Syntax(
617
- code,
618
- self.lexer_name,
619
- theme=self.theme,
620
- background_color="default",
621
- line_numbers=True,
622
- )
623
- yield syntax
624
- yield Text(f"/{self.lexer_name}", style="dim")
625
-
626
- Markdown.elements["fence"] = SimpleCodeBlock
627
-
628
-
629
- async def run_prompt_with_attachments(
630
- agent,
631
- raw_prompt: str,
632
- *,
633
- spinner_console=None,
634
- use_spinner: bool = True,
635
- ):
636
- """Run the agent after parsing CLI attachments for image/document support.
637
-
638
- Returns:
639
- tuple: (result, task) where result is the agent response and task is the asyncio task
640
- """
641
- import asyncio
642
-
643
- from code_puppy.messaging import emit_system_message, emit_warning
644
-
645
- processed_prompt = parse_prompt_attachments(raw_prompt)
646
-
647
- for warning in processed_prompt.warnings:
648
- emit_warning(warning)
649
-
650
- summary_parts = []
651
- if processed_prompt.attachments:
652
- summary_parts.append(f"binary files: {len(processed_prompt.attachments)}")
653
- if processed_prompt.link_attachments:
654
- summary_parts.append(f"urls: {len(processed_prompt.link_attachments)}")
655
- if summary_parts:
656
- emit_system_message(
657
- "[dim]Attachments detected -> " + ", ".join(summary_parts) + "[/dim]"
658
- )
659
-
660
- if not processed_prompt.prompt:
661
- emit_warning(
662
- "Prompt is empty after removing attachments; add instructions and retry."
663
- )
664
- return None, None
665
-
666
- attachments = [attachment.content for attachment in processed_prompt.attachments]
667
- link_attachments = [link.url_part for link in processed_prompt.link_attachments]
668
-
669
- # Create the agent task first so we can track and cancel it
670
- agent_task = asyncio.create_task(
671
- agent.run_with_mcp(
672
- processed_prompt.prompt,
673
- attachments=attachments,
674
- link_attachments=link_attachments,
675
- )
676
- )
677
-
678
- if use_spinner and spinner_console is not None:
679
- from code_puppy.messaging.spinner import ConsoleSpinner
680
-
681
- with ConsoleSpinner(console=spinner_console):
682
- try:
683
- result = await agent_task
684
- return result, agent_task
685
- except asyncio.CancelledError:
686
- emit_info("Agent task cancelled")
687
- return None, agent_task
688
- else:
689
- try:
690
- result = await agent_task
691
- return result, agent_task
692
- except asyncio.CancelledError:
693
- emit_info("Agent task cancelled")
694
- return None, agent_task
695
-
696
-
697
- async def execute_single_prompt(prompt: str, message_renderer) -> None:
698
- """Execute a single prompt and exit (for -p flag)."""
699
- from code_puppy.messaging import emit_info, emit_system_message
700
-
701
- emit_info(f"[bold blue]Executing prompt:[/bold blue] {prompt}")
702
-
703
- try:
704
- # Get agent through runtime manager and use helper for attachments
705
- agent = get_current_agent()
706
- response = await run_prompt_with_attachments(
707
- agent,
708
- prompt,
709
- spinner_console=message_renderer.console,
710
- )
711
- if response is None:
712
- return
713
-
714
- agent_response = response.output
715
- emit_system_message(
716
- f"\n[bold purple]AGENT RESPONSE: [/bold purple]\n{agent_response}"
717
- )
718
-
719
- except asyncio.CancelledError:
720
- from code_puppy.messaging import emit_warning
721
-
722
- emit_warning("Execution cancelled by user")
723
- except Exception as e:
724
- from code_puppy.messaging import emit_error
725
-
726
- emit_error(f"Error executing prompt: {str(e)}")
727
-
728
-
729
- def main_entry():
730
- """Entry point for the installed CLI tool."""
731
- try:
732
- asyncio.run(main())
733
- except KeyboardInterrupt:
734
- print(traceback.format_exc())
735
- if get_use_dbos():
736
- DBOS.destroy()
737
- return 0
738
- finally:
739
- # Reset terminal on Unix-like systems (not Windows)
740
- if platform.system() != "Windows":
741
- try:
742
- # Reset terminal to sanity state
743
- subprocess.run(["reset"], check=True, capture_output=True)
744
- except (subprocess.CalledProcessError, FileNotFoundError):
745
- # Silently fail if reset command isn't available
746
- pass
3
+ This module re-exports the main_entry function from cli_runner for backwards compatibility.
4
+ All the actual logic lives in cli_runner.py.
5
+ """
747
6
 
7
+ from code_puppy.cli_runner import main_entry
748
8
 
749
9
  if __name__ == "__main__":
750
10
  main_entry()