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