code-puppy 0.0.169__py3-none-any.whl → 0.0.366__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 (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,916 @@
1
+ """CLI runner for Code Puppy.
2
+
3
+ Contains the main application logic, interactive mode, and entry point.
4
+ """
5
+
6
+ # Apply pydantic-ai patches BEFORE any pydantic-ai imports
7
+ from code_puppy.pydantic_patches import apply_all_patches
8
+
9
+ apply_all_patches()
10
+
11
+ import argparse
12
+ import asyncio
13
+ import os
14
+ import sys
15
+ import time
16
+ import traceback
17
+ from pathlib import Path
18
+
19
+ from dbos import DBOS, DBOSConfig
20
+ from rich.console import Console
21
+
22
+ from code_puppy import __version__, callbacks, plugins
23
+ from code_puppy.agents import get_current_agent
24
+ from code_puppy.command_line.attachments import parse_prompt_attachments
25
+ from code_puppy.command_line.clipboard import get_clipboard_manager
26
+ from code_puppy.config import (
27
+ AUTOSAVE_DIR,
28
+ COMMAND_HISTORY_FILE,
29
+ DBOS_DATABASE_URL,
30
+ ensure_config_exists,
31
+ finalize_autosave_session,
32
+ get_use_dbos,
33
+ initialize_command_history_file,
34
+ save_command_to_history,
35
+ )
36
+ from code_puppy.http_utils import find_available_port
37
+ from code_puppy.keymap import (
38
+ KeymapError,
39
+ get_cancel_agent_display_name,
40
+ validate_cancel_agent_key,
41
+ )
42
+ from code_puppy.messaging import emit_info
43
+ from code_puppy.terminal_utils import (
44
+ print_truecolor_warning,
45
+ reset_unix_terminal,
46
+ reset_windows_terminal_ansi,
47
+ reset_windows_terminal_full,
48
+ )
49
+ from code_puppy.tools.common import console
50
+ from code_puppy.version_checker import default_version_mismatch_behavior
51
+
52
+ plugins.load_plugin_callbacks()
53
+
54
+
55
+ async def main():
56
+ """Main async entry point for Code Puppy CLI."""
57
+ parser = argparse.ArgumentParser(description="Code Puppy - A code generation agent")
58
+ parser.add_argument(
59
+ "--version",
60
+ "-v",
61
+ action="version",
62
+ version=f"{__version__}",
63
+ help="Show version and exit",
64
+ )
65
+ parser.add_argument(
66
+ "--interactive",
67
+ "-i",
68
+ action="store_true",
69
+ help="Run in interactive mode",
70
+ )
71
+ parser.add_argument(
72
+ "--prompt",
73
+ "-p",
74
+ type=str,
75
+ help="Execute a single prompt and exit (no interactive mode)",
76
+ )
77
+ parser.add_argument(
78
+ "--agent",
79
+ "-a",
80
+ type=str,
81
+ help="Specify which agent to use (e.g., --agent code-puppy)",
82
+ )
83
+ parser.add_argument(
84
+ "--model",
85
+ "-m",
86
+ type=str,
87
+ help="Specify which model to use (e.g., --model gpt-5)",
88
+ )
89
+ parser.add_argument(
90
+ "command", nargs="*", help="Run a single command (deprecated, use -p instead)"
91
+ )
92
+ args = parser.parse_args()
93
+
94
+ from code_puppy.messaging import (
95
+ RichConsoleRenderer,
96
+ SynchronousInteractiveRenderer,
97
+ get_global_queue,
98
+ get_message_bus,
99
+ )
100
+
101
+ # Create a shared console for both renderers
102
+ display_console = Console()
103
+
104
+ # Legacy renderer for backward compatibility (emits via get_global_queue)
105
+ message_queue = get_global_queue()
106
+ message_renderer = SynchronousInteractiveRenderer(message_queue, display_console)
107
+ message_renderer.start()
108
+
109
+ # New MessageBus renderer for structured messages (tools emit here)
110
+ message_bus = get_message_bus()
111
+ bus_renderer = RichConsoleRenderer(message_bus, display_console)
112
+ bus_renderer.start()
113
+
114
+ initialize_command_history_file()
115
+ from code_puppy.messaging import emit_error, emit_system_message
116
+
117
+ # Show the awesome Code Puppy logo when entering interactive mode
118
+ # This happens when: no -p flag (prompt-only mode) is used
119
+ # The logo should appear for both `code-puppy` and `code-puppy -i`
120
+ if not args.prompt:
121
+ try:
122
+ import pyfiglet
123
+
124
+ intro_lines = pyfiglet.figlet_format(
125
+ "CODE PUPPY", font="ansi_shadow"
126
+ ).split("\n")
127
+
128
+ # Simple blue to green gradient (top to bottom)
129
+ gradient_colors = ["bright_blue", "bright_cyan", "bright_green"]
130
+ display_console.print("\n")
131
+
132
+ lines = []
133
+ # Apply gradient line by line
134
+ for line_num, line in enumerate(intro_lines):
135
+ if line.strip():
136
+ # Use line position to determine color (top blue, middle cyan, bottom green)
137
+ color_idx = min(line_num // 2, len(gradient_colors) - 1)
138
+ color = gradient_colors[color_idx]
139
+ lines.append(f"[{color}]{line}[/{color}]")
140
+ else:
141
+ lines.append("")
142
+ # Print directly to console to avoid the 'dim' style from emit_system_message
143
+ display_console.print("\n".join(lines))
144
+ except ImportError:
145
+ emit_system_message("🐶 Code Puppy is Loading...")
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
+
150
+ available_port = find_available_port()
151
+ if available_port is None:
152
+ emit_error("No available ports in range 8090-9010!")
153
+ return
154
+
155
+ # Early model setting if specified via command line
156
+ # This happens before ensure_config_exists() to ensure config is set up correctly
157
+ early_model = None
158
+ if args.model:
159
+ early_model = args.model.strip()
160
+ from code_puppy.config import set_model_name
161
+
162
+ set_model_name(early_model)
163
+
164
+ ensure_config_exists()
165
+
166
+ # Validate cancel_agent_key configuration early
167
+ try:
168
+ validate_cancel_agent_key()
169
+ except KeymapError as e:
170
+ from code_puppy.messaging import emit_error
171
+
172
+ emit_error(str(e))
173
+ sys.exit(1)
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
+
214
+ # Load API keys from puppy.cfg into environment variables
215
+ from code_puppy.config import load_api_keys_to_environment
216
+
217
+ load_api_keys_to_environment()
218
+
219
+ # Handle model validation from command line (validation happens here, setting was earlier)
220
+ if args.model:
221
+ from code_puppy.config import _validate_model_exists
222
+
223
+ model_name = args.model.strip()
224
+ try:
225
+ # Validate that the model exists in models.json
226
+ if not _validate_model_exists(model_name):
227
+ from code_puppy.model_factory import ModelFactory
228
+
229
+ models_config = ModelFactory.load_config()
230
+ available_models = list(models_config.keys()) if models_config else []
231
+
232
+ emit_error(f"Model '{model_name}' not found")
233
+ emit_system_message(f"Available models: {', '.join(available_models)}")
234
+ sys.exit(1)
235
+
236
+ # Model is valid, show confirmation (already set earlier)
237
+ emit_system_message(f"🎯 Using model: {model_name}")
238
+ except Exception as e:
239
+ emit_error(f"Error validating model: {str(e)}")
240
+ sys.exit(1)
241
+
242
+ # Handle agent selection from command line
243
+ if args.agent:
244
+ from code_puppy.agents.agent_manager import (
245
+ get_available_agents,
246
+ set_current_agent,
247
+ )
248
+
249
+ agent_name = args.agent.lower()
250
+ try:
251
+ # First check if the agent exists by getting available agents
252
+ available_agents = get_available_agents()
253
+ if agent_name not in available_agents:
254
+ emit_error(f"Agent '{agent_name}' not found")
255
+ emit_system_message(
256
+ f"Available agents: {', '.join(available_agents.keys())}"
257
+ )
258
+ sys.exit(1)
259
+
260
+ # Agent exists, set it
261
+ set_current_agent(agent_name)
262
+ emit_system_message(f"🤖 Using agent: {agent_name}")
263
+ except Exception as e:
264
+ emit_error(f"Error setting agent: {str(e)}")
265
+ sys.exit(1)
266
+
267
+ current_version = __version__
268
+
269
+ no_version_update = os.getenv("NO_VERSION_UPDATE", "").lower() in (
270
+ "1",
271
+ "true",
272
+ "yes",
273
+ "on",
274
+ )
275
+ if no_version_update:
276
+ version_msg = f"Current version: {current_version}"
277
+ update_disabled_msg = (
278
+ "Update phase disabled because NO_VERSION_UPDATE is set to 1 or true"
279
+ )
280
+ emit_system_message(version_msg)
281
+ emit_system_message(update_disabled_msg)
282
+ else:
283
+ if len(callbacks.get_callbacks("version_check")):
284
+ await callbacks.on_version_check(current_version)
285
+ else:
286
+ default_version_mismatch_behavior(current_version)
287
+
288
+ await callbacks.on_startup()
289
+
290
+ # Initialize DBOS if not disabled
291
+ if get_use_dbos():
292
+ # Append a Unix timestamp in ms to the version for uniqueness
293
+ dbos_app_version = os.environ.get(
294
+ "DBOS_APP_VERSION", f"{current_version}-{int(time.time() * 1000)}"
295
+ )
296
+ dbos_config: DBOSConfig = {
297
+ "name": "dbos-code-puppy",
298
+ "system_database_url": DBOS_DATABASE_URL,
299
+ "run_admin_server": False,
300
+ "conductor_key": os.environ.get(
301
+ "DBOS_CONDUCTOR_KEY"
302
+ ), # Optional, if set in env, connect to conductor
303
+ "log_level": os.environ.get(
304
+ "DBOS_LOG_LEVEL", "ERROR"
305
+ ), # Default to ERROR level to suppress verbose logs
306
+ "application_version": dbos_app_version, # Match DBOS app version to Code Puppy version
307
+ }
308
+ try:
309
+ DBOS(config=dbos_config)
310
+ DBOS.launch()
311
+ except Exception as e:
312
+ emit_error(f"Error initializing DBOS: {e}")
313
+ sys.exit(1)
314
+ else:
315
+ pass
316
+
317
+ global shutdown_flag
318
+ shutdown_flag = False
319
+ try:
320
+ initial_command = None
321
+ prompt_only_mode = False
322
+
323
+ if args.prompt:
324
+ initial_command = args.prompt
325
+ prompt_only_mode = True
326
+ elif args.command:
327
+ initial_command = " ".join(args.command)
328
+ prompt_only_mode = False
329
+
330
+ if prompt_only_mode:
331
+ await execute_single_prompt(initial_command, message_renderer)
332
+ else:
333
+ # Default to interactive mode (no args = same as -i)
334
+ await interactive_mode(message_renderer, initial_command=initial_command)
335
+ finally:
336
+ if message_renderer:
337
+ message_renderer.stop()
338
+ if bus_renderer:
339
+ bus_renderer.stop()
340
+ await callbacks.on_shutdown()
341
+ if get_use_dbos():
342
+ DBOS.destroy()
343
+
344
+
345
+ async def interactive_mode(message_renderer, initial_command: str = None) -> None:
346
+ """Run the agent in interactive mode."""
347
+ from code_puppy.command_line.command_handler import handle_command
348
+
349
+ display_console = message_renderer.console
350
+ from code_puppy.messaging import emit_info, emit_system_message
351
+
352
+ emit_system_message("Type '/exit' or '/quit' to exit the interactive mode.")
353
+ emit_system_message("Type 'clear' to reset the conversation history.")
354
+ emit_system_message("Type /help to view all commands")
355
+ emit_system_message(
356
+ "Type @ for path completion, or /model to pick a model. Toggle multiline with Alt+M or F2; newline: Ctrl+J."
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
+ )
365
+ cancel_key = get_cancel_agent_display_name()
366
+ emit_system_message(
367
+ f"Press {cancel_key} during processing to cancel the current task or inference. Use Ctrl+X to interrupt running shell commands."
368
+ )
369
+ emit_system_message(
370
+ "Use /autosave_load to manually load a previous autosave session."
371
+ )
372
+ emit_system_message(
373
+ "Use /diff to configure diff highlighting colors for file changes."
374
+ )
375
+ emit_system_message("To re-run the tutorial, use /tutorial.")
376
+ try:
377
+ from code_puppy.command_line.motd import print_motd
378
+
379
+ print_motd(console, force=False)
380
+ except Exception as e:
381
+ from code_puppy.messaging import emit_warning
382
+
383
+ emit_warning(f"MOTD error: {e}")
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
+
389
+ # Initialize the runtime agent manager
390
+ if initial_command:
391
+ from code_puppy.agents import get_current_agent
392
+ from code_puppy.messaging import emit_info, emit_success, emit_system_message
393
+
394
+ agent = get_current_agent()
395
+ emit_info(f"Processing initial command: {initial_command}")
396
+
397
+ try:
398
+ # Check if any tool is waiting for user input before showing spinner
399
+ try:
400
+ from code_puppy.tools.command_runner import is_awaiting_user_input
401
+
402
+ awaiting_input = is_awaiting_user_input()
403
+ except ImportError:
404
+ awaiting_input = False
405
+
406
+ # Run with or without spinner based on whether we're awaiting input
407
+ response, agent_task = await run_prompt_with_attachments(
408
+ agent,
409
+ initial_command,
410
+ spinner_console=display_console,
411
+ use_spinner=not awaiting_input,
412
+ )
413
+ if response is not None:
414
+ agent_response = response.output
415
+
416
+ # Update the agent's message history with the complete conversation
417
+ # including the final assistant response
418
+ if hasattr(response, "all_messages"):
419
+ agent.set_message_history(list(response.all_messages()))
420
+
421
+ # Emit structured message for proper markdown rendering
422
+ from code_puppy.messaging import get_message_bus
423
+ from code_puppy.messaging.messages import AgentResponseMessage
424
+
425
+ response_msg = AgentResponseMessage(
426
+ content=agent_response,
427
+ is_markdown=True,
428
+ )
429
+ get_message_bus().emit(response_msg)
430
+
431
+ emit_success("🐶 Continuing in Interactive Mode")
432
+ emit_system_message(
433
+ "Your command and response are preserved in the conversation history."
434
+ )
435
+
436
+ except Exception as e:
437
+ from code_puppy.messaging import emit_error
438
+
439
+ emit_error(f"Error processing initial command: {str(e)}")
440
+
441
+ # Check if prompt_toolkit is installed
442
+ try:
443
+ from code_puppy.command_line.prompt_toolkit_completion import (
444
+ get_input_with_combined_completion,
445
+ get_prompt_with_active_model,
446
+ )
447
+ except ImportError:
448
+ from code_puppy.messaging import emit_warning
449
+
450
+ emit_warning("Warning: prompt_toolkit not installed. Installing now...")
451
+ try:
452
+ import subprocess
453
+
454
+ subprocess.check_call(
455
+ [sys.executable, "-m", "pip", "install", "--quiet", "prompt_toolkit"]
456
+ )
457
+ from code_puppy.messaging import emit_success
458
+
459
+ emit_success("Successfully installed prompt_toolkit")
460
+ from code_puppy.command_line.prompt_toolkit_completion import (
461
+ get_input_with_combined_completion,
462
+ get_prompt_with_active_model,
463
+ )
464
+ except Exception as e:
465
+ from code_puppy.messaging import emit_error, emit_warning
466
+
467
+ emit_error(f"Error installing prompt_toolkit: {e}")
468
+ emit_warning("Falling back to basic input without tab completion")
469
+
470
+ # Autosave loading is now manual - use /autosave_load command
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
+
511
+ # Track the current agent task for cancellation on quit
512
+ current_agent_task = None
513
+
514
+ while True:
515
+ from code_puppy.agents.agent_manager import get_current_agent
516
+ from code_puppy.messaging import emit_info
517
+
518
+ # Get the custom prompt from the current agent, or use default
519
+ current_agent = get_current_agent()
520
+ user_prompt = current_agent.get_user_prompt() or "Enter your coding task:"
521
+
522
+ emit_info(f"{user_prompt}\n")
523
+
524
+ try:
525
+ # Use prompt_toolkit for enhanced input with path completion
526
+ try:
527
+ # Windows-specific: Reset terminal state before prompting
528
+ reset_windows_terminal_ansi()
529
+
530
+ # Use the async version of get_input_with_combined_completion
531
+ task = await get_input_with_combined_completion(
532
+ get_prompt_with_active_model(), history_file=COMMAND_HISTORY_FILE
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
543
+ except ImportError:
544
+ # Fall back to basic input if prompt_toolkit is not available
545
+ task = input(">>> ")
546
+
547
+ except (KeyboardInterrupt, EOFError):
548
+ # Handle Ctrl+C or Ctrl+D
549
+ # Windows-specific: Reset terminal state after interrupt to prevent
550
+ # the terminal from becoming unresponsive (can't type characters)
551
+ reset_windows_terminal_full()
552
+ from code_puppy.messaging import emit_warning
553
+
554
+ emit_warning("\nInput cancelled")
555
+ continue
556
+
557
+ # Check for exit commands (plain text or command form)
558
+ if task.strip().lower() in ["exit", "quit"] or task.strip().lower() in [
559
+ "/exit",
560
+ "/quit",
561
+ ]:
562
+ import asyncio
563
+
564
+ from code_puppy.messaging import emit_success
565
+
566
+ emit_success("Goodbye!")
567
+
568
+ # Cancel any running agent task for clean shutdown
569
+ if current_agent_task and not current_agent_task.done():
570
+ emit_info("Cancelling running agent task...")
571
+ current_agent_task.cancel()
572
+ try:
573
+ await current_agent_task
574
+ except asyncio.CancelledError:
575
+ pass # Expected when cancelling
576
+
577
+ # The renderer is stopped in the finally block of main().
578
+ break
579
+
580
+ # Check for clear command (supports both `clear` and `/clear`)
581
+ if task.strip().lower() in ("clear", "/clear"):
582
+ from code_puppy.command_line.clipboard import get_clipboard_manager
583
+ from code_puppy.messaging import (
584
+ emit_info,
585
+ emit_system_message,
586
+ emit_warning,
587
+ )
588
+
589
+ agent = get_current_agent()
590
+ new_session_id = finalize_autosave_session()
591
+ agent.clear_message_history()
592
+ emit_warning("Conversation history cleared!")
593
+ emit_system_message("The agent will not remember previous interactions.")
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)")
602
+ continue
603
+
604
+ # Parse attachments first so leading paths aren't misread as commands
605
+ processed_for_commands = parse_prompt_attachments(task)
606
+ cleaned_for_commands = (processed_for_commands.prompt or "").strip()
607
+
608
+ # Handle / commands based on cleaned prompt (after stripping attachments)
609
+ if cleaned_for_commands.startswith("/"):
610
+ try:
611
+ command_result = handle_command(cleaned_for_commands)
612
+ except Exception as e:
613
+ from code_puppy.messaging import emit_error
614
+
615
+ emit_error(f"Command error: {e}")
616
+ # Continue interactive loop instead of exiting
617
+ continue
618
+ if command_result is True:
619
+ continue
620
+ elif isinstance(command_result, str):
621
+ if command_result == "__AUTOSAVE_LOAD__":
622
+ # Handle async autosave loading
623
+ try:
624
+ # Check if we're in a real interactive terminal
625
+ # (not pexpect/tests) - interactive picker requires proper TTY
626
+ use_interactive_picker = (
627
+ sys.stdin.isatty() and sys.stdout.isatty()
628
+ )
629
+
630
+ # Allow environment variable override for tests
631
+ if os.getenv("CODE_PUPPY_NO_TUI") == "1":
632
+ use_interactive_picker = False
633
+
634
+ if use_interactive_picker:
635
+ # Use interactive picker for terminal sessions
636
+ from code_puppy.agents.agent_manager import (
637
+ get_current_agent,
638
+ )
639
+ from code_puppy.command_line.autosave_menu import (
640
+ interactive_autosave_picker,
641
+ )
642
+ from code_puppy.config import (
643
+ set_current_autosave_from_session_name,
644
+ )
645
+ from code_puppy.messaging import (
646
+ emit_error,
647
+ emit_success,
648
+ emit_warning,
649
+ )
650
+ from code_puppy.session_storage import (
651
+ load_session,
652
+ restore_autosave_interactively,
653
+ )
654
+
655
+ chosen_session = await interactive_autosave_picker()
656
+
657
+ if not chosen_session:
658
+ emit_warning("Autosave load cancelled")
659
+ continue
660
+
661
+ # Load the session
662
+ base_dir = Path(AUTOSAVE_DIR)
663
+ history = load_session(chosen_session, base_dir)
664
+
665
+ agent = get_current_agent()
666
+ agent.set_message_history(history)
667
+
668
+ # Set current autosave session
669
+ set_current_autosave_from_session_name(chosen_session)
670
+
671
+ total_tokens = sum(
672
+ agent.estimate_tokens_for_message(msg)
673
+ for msg in history
674
+ )
675
+ session_path = base_dir / f"{chosen_session}.pkl"
676
+
677
+ emit_success(
678
+ f"✅ Autosave loaded: {len(history)} messages ({total_tokens} tokens)\n"
679
+ f"📁 From: {session_path}"
680
+ )
681
+ else:
682
+ # Fall back to old text-based picker for tests/non-TTY environments
683
+ await restore_autosave_interactively(Path(AUTOSAVE_DIR))
684
+
685
+ except Exception as e:
686
+ from code_puppy.messaging import emit_error
687
+
688
+ emit_error(f"Failed to load autosave: {e}")
689
+ continue
690
+ else:
691
+ # Command returned a prompt to execute
692
+ task = command_result
693
+ elif command_result is False:
694
+ # Command not recognized, continue with normal processing
695
+ pass
696
+
697
+ if task.strip():
698
+ # Write to the secret file for permanent history with timestamp
699
+ save_command_to_history(task)
700
+
701
+ try:
702
+ # No need to get agent directly - use manager's run methods
703
+
704
+ # Use our custom helper to enable attachment handling with spinner support
705
+ result, current_agent_task = await run_prompt_with_attachments(
706
+ current_agent,
707
+ task,
708
+ spinner_console=message_renderer.console,
709
+ )
710
+ # Check if the task was cancelled (but don't show message if we just killed processes)
711
+ if result is None:
712
+ # Windows-specific: Reset terminal state after cancellation
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
721
+ continue
722
+ # Get the structured response
723
+ agent_response = result.output
724
+
725
+ # Emit structured message for proper markdown rendering
726
+ from code_puppy.messaging import get_message_bus
727
+ from code_puppy.messaging.messages import AgentResponseMessage
728
+
729
+ response_msg = AgentResponseMessage(
730
+ content=agent_response,
731
+ is_markdown=True,
732
+ )
733
+ get_message_bus().emit(response_msg)
734
+
735
+ # Update the agent's message history with the complete conversation
736
+ # including the final assistant response. The history_processors callback
737
+ # may not capture the final message, so we use result.all_messages()
738
+ # to ensure the autosave includes the complete conversation.
739
+ if hasattr(result, "all_messages"):
740
+ current_agent.set_message_history(list(result.all_messages()))
741
+
742
+ # Ensure console output is flushed before next prompt
743
+ # This fixes the issue where prompt doesn't appear after agent response
744
+ display_console.file.flush() if hasattr(
745
+ display_console.file, "flush"
746
+ ) else None
747
+ import time
748
+
749
+ time.sleep(0.1) # Brief pause to ensure all messages are rendered
750
+
751
+ except Exception:
752
+ from code_puppy.messaging.queue_console import get_queue_console
753
+
754
+ get_queue_console().print_exception()
755
+
756
+ # Auto-save session if enabled (moved outside the try block to avoid being swallowed)
757
+ from code_puppy.config import auto_save_session_if_enabled
758
+
759
+ auto_save_session_if_enabled()
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
765
+
766
+ ensure_ctrl_c_disabled()
767
+ except ImportError:
768
+ pass
769
+
770
+
771
+ async def run_prompt_with_attachments(
772
+ agent,
773
+ raw_prompt: str,
774
+ *,
775
+ spinner_console=None,
776
+ use_spinner: bool = True,
777
+ ):
778
+ """Run the agent after parsing CLI attachments for image/document support.
779
+
780
+ Returns:
781
+ tuple: (result, task) where result is the agent response and task is the asyncio task
782
+ """
783
+ import asyncio
784
+ import re
785
+
786
+ from code_puppy.messaging import emit_system_message, emit_warning
787
+
788
+ processed_prompt = parse_prompt_attachments(raw_prompt)
789
+
790
+ for warning in processed_prompt.warnings:
791
+ emit_warning(warning)
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
801
+ summary_parts = []
802
+ if 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)}")
806
+ if processed_prompt.link_attachments:
807
+ summary_parts.append(f"urls: {len(processed_prompt.link_attachments)}")
808
+ if summary_parts:
809
+ emit_system_message("Attachments detected -> " + ", ".join(summary_parts))
810
+
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:
819
+ emit_warning(
820
+ "Prompt is empty after removing attachments; add instructions and retry."
821
+ )
822
+ return None, None
823
+
824
+ # Combine file attachments with clipboard images
825
+ attachments = [attachment.content for attachment in processed_prompt.attachments]
826
+ attachments.extend(clipboard_images) # Add clipboard images
827
+
828
+ link_attachments = [link.url_part for link in processed_prompt.link_attachments]
829
+
830
+ # IMPORTANT: Set the shared console for streaming output so it
831
+ # uses the same console as the spinner. This prevents Live display conflicts
832
+ # that cause line duplication during markdown streaming.
833
+ from code_puppy.agents.event_stream_handler import set_streaming_console
834
+
835
+ set_streaming_console(spinner_console)
836
+
837
+ # Create the agent task first so we can track and cancel it
838
+ agent_task = asyncio.create_task(
839
+ agent.run_with_mcp(
840
+ cleaned_prompt, # Use cleaned prompt (clipboard placeholders removed)
841
+ attachments=attachments,
842
+ link_attachments=link_attachments,
843
+ )
844
+ )
845
+
846
+ if use_spinner and spinner_console is not None:
847
+ from code_puppy.messaging.spinner import ConsoleSpinner
848
+
849
+ with ConsoleSpinner(console=spinner_console):
850
+ try:
851
+ result = await agent_task
852
+ return result, agent_task
853
+ except asyncio.CancelledError:
854
+ emit_info("Agent task cancelled")
855
+ return None, agent_task
856
+ else:
857
+ try:
858
+ result = await agent_task
859
+ return result, agent_task
860
+ except asyncio.CancelledError:
861
+ emit_info("Agent task cancelled")
862
+ return None, agent_task
863
+
864
+
865
+ async def execute_single_prompt(prompt: str, message_renderer) -> None:
866
+ """Execute a single prompt and exit (for -p flag)."""
867
+ from code_puppy.messaging import emit_info
868
+
869
+ emit_info(f"Executing prompt: {prompt}")
870
+
871
+ try:
872
+ # Get agent through runtime manager and use helper for attachments
873
+ agent = get_current_agent()
874
+ response = await run_prompt_with_attachments(
875
+ agent,
876
+ prompt,
877
+ spinner_console=message_renderer.console,
878
+ )
879
+ if response is None:
880
+ return
881
+
882
+ agent_response = response.output
883
+
884
+ # Emit structured message for proper markdown rendering
885
+ from code_puppy.messaging import get_message_bus
886
+ from code_puppy.messaging.messages import AgentResponseMessage
887
+
888
+ response_msg = AgentResponseMessage(
889
+ content=agent_response,
890
+ is_markdown=True,
891
+ )
892
+ get_message_bus().emit(response_msg)
893
+
894
+ except asyncio.CancelledError:
895
+ from code_puppy.messaging import emit_warning
896
+
897
+ emit_warning("Execution cancelled by user")
898
+ except Exception as e:
899
+ from code_puppy.messaging import emit_error
900
+
901
+ emit_error(f"Error executing prompt: {str(e)}")
902
+
903
+
904
+ def main_entry():
905
+ """Entry point for the installed CLI tool."""
906
+ try:
907
+ asyncio.run(main())
908
+ except KeyboardInterrupt:
909
+ # Note: Using sys.stderr for crash output - messaging system may not be available
910
+ sys.stderr.write(traceback.format_exc())
911
+ if get_use_dbos():
912
+ DBOS.destroy()
913
+ return 0
914
+ finally:
915
+ # Reset terminal on Unix-like systems (not Windows)
916
+ reset_unix_terminal()