codepp 0.0.437__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 (288) hide show
  1. code_puppy/__init__.py +10 -0
  2. code_puppy/__main__.py +10 -0
  3. code_puppy/agents/__init__.py +31 -0
  4. code_puppy/agents/agent_c_reviewer.py +155 -0
  5. code_puppy/agents/agent_code_puppy.py +117 -0
  6. code_puppy/agents/agent_code_reviewer.py +90 -0
  7. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  8. code_puppy/agents/agent_creator_agent.py +638 -0
  9. code_puppy/agents/agent_golang_reviewer.py +151 -0
  10. code_puppy/agents/agent_helios.py +124 -0
  11. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  12. code_puppy/agents/agent_manager.py +742 -0
  13. code_puppy/agents/agent_pack_leader.py +385 -0
  14. code_puppy/agents/agent_planning.py +165 -0
  15. code_puppy/agents/agent_python_programmer.py +169 -0
  16. code_puppy/agents/agent_python_reviewer.py +90 -0
  17. code_puppy/agents/agent_qa_expert.py +163 -0
  18. code_puppy/agents/agent_qa_kitten.py +208 -0
  19. code_puppy/agents/agent_scheduler.py +121 -0
  20. code_puppy/agents/agent_security_auditor.py +181 -0
  21. code_puppy/agents/agent_terminal_qa.py +323 -0
  22. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  23. code_puppy/agents/base_agent.py +2156 -0
  24. code_puppy/agents/event_stream_handler.py +348 -0
  25. code_puppy/agents/json_agent.py +202 -0
  26. code_puppy/agents/pack/__init__.py +34 -0
  27. code_puppy/agents/pack/bloodhound.py +304 -0
  28. code_puppy/agents/pack/husky.py +327 -0
  29. code_puppy/agents/pack/retriever.py +393 -0
  30. code_puppy/agents/pack/shepherd.py +348 -0
  31. code_puppy/agents/pack/terrier.py +287 -0
  32. code_puppy/agents/pack/watchdog.py +367 -0
  33. code_puppy/agents/prompt_reviewer.py +145 -0
  34. code_puppy/agents/subagent_stream_handler.py +276 -0
  35. code_puppy/api/__init__.py +13 -0
  36. code_puppy/api/app.py +169 -0
  37. code_puppy/api/main.py +21 -0
  38. code_puppy/api/pty_manager.py +453 -0
  39. code_puppy/api/routers/__init__.py +12 -0
  40. code_puppy/api/routers/agents.py +36 -0
  41. code_puppy/api/routers/commands.py +217 -0
  42. code_puppy/api/routers/config.py +75 -0
  43. code_puppy/api/routers/sessions.py +234 -0
  44. code_puppy/api/templates/terminal.html +361 -0
  45. code_puppy/api/websocket.py +154 -0
  46. code_puppy/callbacks.py +692 -0
  47. code_puppy/chatgpt_codex_client.py +338 -0
  48. code_puppy/claude_cache_client.py +672 -0
  49. code_puppy/cli_runner.py +1073 -0
  50. code_puppy/command_line/__init__.py +1 -0
  51. code_puppy/command_line/add_model_menu.py +1092 -0
  52. code_puppy/command_line/agent_menu.py +662 -0
  53. code_puppy/command_line/attachments.py +395 -0
  54. code_puppy/command_line/autosave_menu.py +704 -0
  55. code_puppy/command_line/clipboard.py +527 -0
  56. code_puppy/command_line/colors_menu.py +532 -0
  57. code_puppy/command_line/command_handler.py +293 -0
  58. code_puppy/command_line/command_registry.py +150 -0
  59. code_puppy/command_line/config_commands.py +719 -0
  60. code_puppy/command_line/core_commands.py +867 -0
  61. code_puppy/command_line/diff_menu.py +865 -0
  62. code_puppy/command_line/file_path_completion.py +73 -0
  63. code_puppy/command_line/load_context_completion.py +52 -0
  64. code_puppy/command_line/mcp/__init__.py +10 -0
  65. code_puppy/command_line/mcp/base.py +32 -0
  66. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  67. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  68. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  69. code_puppy/command_line/mcp/edit_command.py +148 -0
  70. code_puppy/command_line/mcp/handler.py +138 -0
  71. code_puppy/command_line/mcp/help_command.py +147 -0
  72. code_puppy/command_line/mcp/install_command.py +214 -0
  73. code_puppy/command_line/mcp/install_menu.py +705 -0
  74. code_puppy/command_line/mcp/list_command.py +94 -0
  75. code_puppy/command_line/mcp/logs_command.py +235 -0
  76. code_puppy/command_line/mcp/remove_command.py +82 -0
  77. code_puppy/command_line/mcp/restart_command.py +100 -0
  78. code_puppy/command_line/mcp/search_command.py +123 -0
  79. code_puppy/command_line/mcp/start_all_command.py +135 -0
  80. code_puppy/command_line/mcp/start_command.py +117 -0
  81. code_puppy/command_line/mcp/status_command.py +184 -0
  82. code_puppy/command_line/mcp/stop_all_command.py +112 -0
  83. code_puppy/command_line/mcp/stop_command.py +80 -0
  84. code_puppy/command_line/mcp/test_command.py +107 -0
  85. code_puppy/command_line/mcp/utils.py +129 -0
  86. code_puppy/command_line/mcp/wizard_utils.py +334 -0
  87. code_puppy/command_line/mcp_completion.py +174 -0
  88. code_puppy/command_line/model_picker_completion.py +197 -0
  89. code_puppy/command_line/model_settings_menu.py +932 -0
  90. code_puppy/command_line/motd.py +96 -0
  91. code_puppy/command_line/onboarding_slides.py +179 -0
  92. code_puppy/command_line/onboarding_wizard.py +342 -0
  93. code_puppy/command_line/pin_command_completion.py +329 -0
  94. code_puppy/command_line/prompt_toolkit_completion.py +846 -0
  95. code_puppy/command_line/session_commands.py +302 -0
  96. code_puppy/command_line/shell_passthrough.py +145 -0
  97. code_puppy/command_line/skills_completion.py +160 -0
  98. code_puppy/command_line/uc_menu.py +893 -0
  99. code_puppy/command_line/utils.py +93 -0
  100. code_puppy/command_line/wiggum_state.py +78 -0
  101. code_puppy/config.py +1770 -0
  102. code_puppy/error_logging.py +134 -0
  103. code_puppy/gemini_code_assist.py +385 -0
  104. code_puppy/gemini_model.py +754 -0
  105. code_puppy/hook_engine/README.md +105 -0
  106. code_puppy/hook_engine/__init__.py +21 -0
  107. code_puppy/hook_engine/aliases.py +155 -0
  108. code_puppy/hook_engine/engine.py +221 -0
  109. code_puppy/hook_engine/executor.py +296 -0
  110. code_puppy/hook_engine/matcher.py +156 -0
  111. code_puppy/hook_engine/models.py +240 -0
  112. code_puppy/hook_engine/registry.py +106 -0
  113. code_puppy/hook_engine/validator.py +144 -0
  114. code_puppy/http_utils.py +361 -0
  115. code_puppy/keymap.py +128 -0
  116. code_puppy/main.py +10 -0
  117. code_puppy/mcp_/__init__.py +66 -0
  118. code_puppy/mcp_/async_lifecycle.py +286 -0
  119. code_puppy/mcp_/blocking_startup.py +469 -0
  120. code_puppy/mcp_/captured_stdio_server.py +275 -0
  121. code_puppy/mcp_/circuit_breaker.py +290 -0
  122. code_puppy/mcp_/config_wizard.py +507 -0
  123. code_puppy/mcp_/dashboard.py +308 -0
  124. code_puppy/mcp_/error_isolation.py +407 -0
  125. code_puppy/mcp_/examples/retry_example.py +226 -0
  126. code_puppy/mcp_/health_monitor.py +589 -0
  127. code_puppy/mcp_/managed_server.py +428 -0
  128. code_puppy/mcp_/manager.py +807 -0
  129. code_puppy/mcp_/mcp_logs.py +224 -0
  130. code_puppy/mcp_/registry.py +451 -0
  131. code_puppy/mcp_/retry_manager.py +337 -0
  132. code_puppy/mcp_/server_registry_catalog.py +1126 -0
  133. code_puppy/mcp_/status_tracker.py +355 -0
  134. code_puppy/mcp_/system_tools.py +209 -0
  135. code_puppy/mcp_prompts/__init__.py +1 -0
  136. code_puppy/mcp_prompts/hook_creator.py +103 -0
  137. code_puppy/messaging/__init__.py +255 -0
  138. code_puppy/messaging/bus.py +613 -0
  139. code_puppy/messaging/commands.py +167 -0
  140. code_puppy/messaging/markdown_patches.py +57 -0
  141. code_puppy/messaging/message_queue.py +361 -0
  142. code_puppy/messaging/messages.py +569 -0
  143. code_puppy/messaging/queue_console.py +271 -0
  144. code_puppy/messaging/renderers.py +311 -0
  145. code_puppy/messaging/rich_renderer.py +1158 -0
  146. code_puppy/messaging/spinner/__init__.py +83 -0
  147. code_puppy/messaging/spinner/console_spinner.py +240 -0
  148. code_puppy/messaging/spinner/spinner_base.py +95 -0
  149. code_puppy/messaging/subagent_console.py +460 -0
  150. code_puppy/model_factory.py +848 -0
  151. code_puppy/model_switching.py +63 -0
  152. code_puppy/model_utils.py +168 -0
  153. code_puppy/models.json +174 -0
  154. code_puppy/models_dev_api.json +1 -0
  155. code_puppy/models_dev_parser.py +592 -0
  156. code_puppy/plugins/__init__.py +186 -0
  157. code_puppy/plugins/agent_skills/__init__.py +22 -0
  158. code_puppy/plugins/agent_skills/config.py +175 -0
  159. code_puppy/plugins/agent_skills/discovery.py +136 -0
  160. code_puppy/plugins/agent_skills/downloader.py +392 -0
  161. code_puppy/plugins/agent_skills/installer.py +22 -0
  162. code_puppy/plugins/agent_skills/metadata.py +219 -0
  163. code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
  164. code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
  165. code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
  166. code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
  167. code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
  168. code_puppy/plugins/agent_skills/skills_menu.py +781 -0
  169. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  170. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  171. code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
  172. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  173. code_puppy/plugins/antigravity_oauth/constants.py +133 -0
  174. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  175. code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
  176. code_puppy/plugins/antigravity_oauth/storage.py +288 -0
  177. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  178. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  179. code_puppy/plugins/antigravity_oauth/transport.py +863 -0
  180. code_puppy/plugins/antigravity_oauth/utils.py +168 -0
  181. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  182. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  183. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
  184. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
  185. code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
  186. code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
  187. code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
  188. code_puppy/plugins/claude_code_hooks/config.py +137 -0
  189. code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
  190. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  191. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  192. code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
  193. code_puppy/plugins/claude_code_oauth/config.py +52 -0
  194. code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
  195. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  196. code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
  197. code_puppy/plugins/claude_code_oauth/utils.py +640 -0
  198. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  199. code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
  200. code_puppy/plugins/example_custom_command/README.md +280 -0
  201. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  202. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  203. code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
  204. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  205. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  206. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  207. code_puppy/plugins/hook_creator/__init__.py +1 -0
  208. code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
  209. code_puppy/plugins/hook_manager/__init__.py +1 -0
  210. code_puppy/plugins/hook_manager/config.py +290 -0
  211. code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
  212. code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
  213. code_puppy/plugins/oauth_puppy_html.py +228 -0
  214. code_puppy/plugins/scheduler/__init__.py +1 -0
  215. code_puppy/plugins/scheduler/register_callbacks.py +88 -0
  216. code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
  217. code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
  218. code_puppy/plugins/shell_safety/__init__.py +6 -0
  219. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  220. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  221. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  222. code_puppy/plugins/synthetic_status/__init__.py +1 -0
  223. code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
  224. code_puppy/plugins/synthetic_status/status_api.py +147 -0
  225. code_puppy/plugins/universal_constructor/__init__.py +13 -0
  226. code_puppy/plugins/universal_constructor/models.py +138 -0
  227. code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
  228. code_puppy/plugins/universal_constructor/registry.py +302 -0
  229. code_puppy/plugins/universal_constructor/sandbox.py +584 -0
  230. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  231. code_puppy/pydantic_patches.py +356 -0
  232. code_puppy/reopenable_async_client.py +232 -0
  233. code_puppy/round_robin_model.py +150 -0
  234. code_puppy/scheduler/__init__.py +41 -0
  235. code_puppy/scheduler/__main__.py +9 -0
  236. code_puppy/scheduler/cli.py +118 -0
  237. code_puppy/scheduler/config.py +126 -0
  238. code_puppy/scheduler/daemon.py +280 -0
  239. code_puppy/scheduler/executor.py +155 -0
  240. code_puppy/scheduler/platform.py +19 -0
  241. code_puppy/scheduler/platform_unix.py +22 -0
  242. code_puppy/scheduler/platform_win.py +32 -0
  243. code_puppy/session_storage.py +338 -0
  244. code_puppy/status_display.py +257 -0
  245. code_puppy/summarization_agent.py +176 -0
  246. code_puppy/terminal_utils.py +418 -0
  247. code_puppy/tools/__init__.py +501 -0
  248. code_puppy/tools/agent_tools.py +603 -0
  249. code_puppy/tools/ask_user_question/__init__.py +26 -0
  250. code_puppy/tools/ask_user_question/constants.py +73 -0
  251. code_puppy/tools/ask_user_question/demo_tui.py +55 -0
  252. code_puppy/tools/ask_user_question/handler.py +232 -0
  253. code_puppy/tools/ask_user_question/models.py +304 -0
  254. code_puppy/tools/ask_user_question/registration.py +26 -0
  255. code_puppy/tools/ask_user_question/renderers.py +309 -0
  256. code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
  257. code_puppy/tools/ask_user_question/theme.py +155 -0
  258. code_puppy/tools/ask_user_question/tui_loop.py +423 -0
  259. code_puppy/tools/browser/__init__.py +37 -0
  260. code_puppy/tools/browser/browser_control.py +289 -0
  261. code_puppy/tools/browser/browser_interactions.py +545 -0
  262. code_puppy/tools/browser/browser_locators.py +640 -0
  263. code_puppy/tools/browser/browser_manager.py +378 -0
  264. code_puppy/tools/browser/browser_navigation.py +251 -0
  265. code_puppy/tools/browser/browser_screenshot.py +179 -0
  266. code_puppy/tools/browser/browser_scripts.py +462 -0
  267. code_puppy/tools/browser/browser_workflows.py +221 -0
  268. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  269. code_puppy/tools/browser/terminal_command_tools.py +534 -0
  270. code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
  271. code_puppy/tools/browser/terminal_tools.py +525 -0
  272. code_puppy/tools/command_runner.py +1346 -0
  273. code_puppy/tools/common.py +1409 -0
  274. code_puppy/tools/display.py +84 -0
  275. code_puppy/tools/file_modifications.py +886 -0
  276. code_puppy/tools/file_operations.py +802 -0
  277. code_puppy/tools/scheduler_tools.py +412 -0
  278. code_puppy/tools/skills_tools.py +244 -0
  279. code_puppy/tools/subagent_context.py +158 -0
  280. code_puppy/tools/tools_content.py +51 -0
  281. code_puppy/tools/universal_constructor.py +889 -0
  282. code_puppy/uvx_detection.py +242 -0
  283. code_puppy/version_checker.py +82 -0
  284. codepp-0.0.437.dist-info/METADATA +766 -0
  285. codepp-0.0.437.dist-info/RECORD +288 -0
  286. codepp-0.0.437.dist-info/WHEEL +4 -0
  287. codepp-0.0.437.dist-info/entry_points.txt +3 -0
  288. codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,867 @@
1
+ """Command handlers for Code Puppy - CORE commands.
2
+
3
+ This module contains @register_command decorated handlers that are automatically
4
+ discovered by the command registry system.
5
+ """
6
+
7
+ import os
8
+
9
+ from code_puppy.command_line.agent_menu import interactive_agent_picker
10
+ from code_puppy.command_line.command_registry import register_command
11
+ from code_puppy.command_line.model_picker_completion import update_model_in_input
12
+ from code_puppy.command_line.motd import print_motd
13
+ from code_puppy.command_line.utils import make_directory_table
14
+ from code_puppy.config import finalize_autosave_session
15
+ from code_puppy.messaging import emit_error, emit_info
16
+ from code_puppy.tools.tools_content import tools_content
17
+
18
+
19
+ # Import get_commands_help from command_handler to avoid circular imports
20
+ # This will be defined in command_handler.py
21
+ def get_commands_help():
22
+ """Lazy import to avoid circular dependency."""
23
+ from code_puppy.command_line.command_handler import get_commands_help as _gch
24
+
25
+ return _gch()
26
+
27
+
28
+ @register_command(
29
+ name="help",
30
+ description="Show this help message",
31
+ usage="/help, /h",
32
+ aliases=["h"],
33
+ category="core",
34
+ )
35
+ def handle_help_command(command: str) -> bool:
36
+ """Show commands help."""
37
+ import uuid
38
+
39
+ from code_puppy.messaging import emit_info
40
+
41
+ group_id = str(uuid.uuid4())
42
+ help_text = get_commands_help()
43
+ emit_info(help_text, message_group_id=group_id)
44
+ return True
45
+
46
+
47
+ @register_command(
48
+ name="cd",
49
+ description="Change directory or show directories",
50
+ usage="/cd <dir>",
51
+ category="core",
52
+ )
53
+ def handle_cd_command(command: str) -> bool:
54
+ """Change directory or list current directory."""
55
+ # Use shlex.split to handle quoted paths properly
56
+ import shlex
57
+
58
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
59
+
60
+ try:
61
+ tokens = shlex.split(command)
62
+ except ValueError:
63
+ # Fallback to simple split if shlex fails
64
+ tokens = command.split()
65
+ if len(tokens) == 1:
66
+ try:
67
+ table = make_directory_table()
68
+ emit_info(table)
69
+ except Exception as e:
70
+ emit_error(f"Error listing directory: {e}")
71
+ return True
72
+ elif len(tokens) == 2:
73
+ dirname = tokens[1]
74
+ target = os.path.expanduser(dirname)
75
+ if not os.path.isabs(target):
76
+ target = os.path.join(os.getcwd(), target)
77
+ if os.path.isdir(target):
78
+ os.chdir(target)
79
+ emit_success(f"Changed directory to: {target}")
80
+ # Reload the agent so the system prompt and project-local
81
+ # AGENT.md rules reflect the new working directory. Without
82
+ # this, the LLM keeps receiving stale path information for the
83
+ # remainder of the session (the PydanticAgent instructions are
84
+ # baked in at construction time and never refreshed otherwise).
85
+ try:
86
+ from code_puppy.agents.agent_manager import get_current_agent
87
+
88
+ get_current_agent().reload_code_generation_agent()
89
+ except Exception as e:
90
+ emit_warning(
91
+ f"Directory changed, but agent reload failed: {e}. "
92
+ "You may need to run /agent or /model to force a refresh."
93
+ )
94
+ else:
95
+ emit_error(f"Not a directory: {dirname}")
96
+ return True
97
+ return True
98
+
99
+
100
+ @register_command(
101
+ name="tools",
102
+ description="Show available tools and capabilities",
103
+ usage="/tools",
104
+ category="core",
105
+ )
106
+ def handle_tools_command(command: str) -> bool:
107
+ """Display available tools."""
108
+ from rich.markdown import Markdown
109
+
110
+ from code_puppy.messaging import emit_info
111
+
112
+ markdown_content = Markdown(tools_content)
113
+ emit_info(markdown_content)
114
+ return True
115
+
116
+
117
+ @register_command(
118
+ name="motd",
119
+ description="Show the latest message of the day (MOTD)",
120
+ usage="/motd",
121
+ category="core",
122
+ )
123
+ def handle_motd_command(command: str) -> bool:
124
+ """Show message of the day."""
125
+ try:
126
+ print_motd(force=True)
127
+ except Exception:
128
+ # Handle printing errors gracefully
129
+ pass
130
+ return True
131
+
132
+
133
+ @register_command(
134
+ name="paste",
135
+ description="Paste image from clipboard (same as F3, or Ctrl+V with image)",
136
+ usage="/paste, /clipboard, /cb",
137
+ aliases=["clipboard", "cb"],
138
+ category="core",
139
+ )
140
+ def handle_paste_command(command: str) -> bool:
141
+ """Paste an image from the clipboard into the pending attachments."""
142
+ from code_puppy.command_line.clipboard import (
143
+ capture_clipboard_image_to_pending,
144
+ get_clipboard_manager,
145
+ has_image_in_clipboard,
146
+ )
147
+ from code_puppy.messaging import emit_info, emit_success, emit_warning
148
+
149
+ if not has_image_in_clipboard():
150
+ emit_warning("No image found in clipboard")
151
+ emit_info("Copy an image (screenshot, from browser, etc.) and try again")
152
+ return True
153
+
154
+ placeholder = capture_clipboard_image_to_pending()
155
+ if placeholder:
156
+ manager = get_clipboard_manager()
157
+ count = manager.get_pending_count()
158
+ emit_success(f"📋 {placeholder}")
159
+ emit_info(f"Total pending clipboard images: {count}")
160
+ emit_info("Type your prompt and press Enter to send with the image(s)")
161
+ else:
162
+ emit_warning("Failed to capture clipboard image")
163
+
164
+ return True
165
+
166
+
167
+ @register_command(
168
+ name="tutorial",
169
+ description="Run the interactive tutorial wizard",
170
+ usage="/tutorial",
171
+ category="core",
172
+ )
173
+ def handle_tutorial_command(command: str) -> bool:
174
+ """Run the interactive tutorial wizard.
175
+
176
+ Usage:
177
+ /tutorial - Run the tutorial (can be run anytime)
178
+ """
179
+ import asyncio
180
+ import concurrent.futures
181
+
182
+ from code_puppy.command_line.onboarding_wizard import (
183
+ reset_onboarding,
184
+ run_onboarding_wizard,
185
+ )
186
+ from code_puppy.model_switching import set_model_and_reload_agent
187
+
188
+ # Always reset so user can re-run the tutorial anytime
189
+ reset_onboarding()
190
+
191
+ # Run the async wizard in a thread pool (same pattern as agent picker)
192
+ with concurrent.futures.ThreadPoolExecutor() as executor:
193
+ future = executor.submit(lambda: asyncio.run(run_onboarding_wizard()))
194
+ result = future.result(timeout=300) # 5 min timeout
195
+
196
+ if result == "chatgpt":
197
+ emit_info("🔐 Starting ChatGPT OAuth flow...")
198
+ from code_puppy.plugins.chatgpt_oauth.oauth_flow import run_oauth_flow
199
+
200
+ run_oauth_flow()
201
+ set_model_and_reload_agent("chatgpt-gpt-5.3-codex")
202
+ elif result == "claude":
203
+ emit_info("🔐 Starting Claude Code OAuth flow...")
204
+ from code_puppy.plugins.claude_code_oauth.register_callbacks import (
205
+ _perform_authentication,
206
+ )
207
+
208
+ _perform_authentication()
209
+ set_model_and_reload_agent("claude-code-claude-opus-4-6")
210
+ elif result == "completed":
211
+ emit_info("🎉 Tutorial complete! Happy coding!")
212
+ elif result == "skipped":
213
+ emit_info("⏭️ Tutorial skipped. Run /tutorial anytime!")
214
+
215
+ return True
216
+
217
+
218
+ @register_command(
219
+ name="exit",
220
+ description="Exit interactive mode",
221
+ usage="/exit, /quit",
222
+ aliases=["quit"],
223
+ category="core",
224
+ )
225
+ def handle_exit_command(command: str) -> bool:
226
+ """Exit the interactive session."""
227
+ from code_puppy.messaging import emit_success
228
+
229
+ try:
230
+ emit_success("Goodbye!")
231
+ except Exception:
232
+ # Handle emit errors gracefully
233
+ pass
234
+ # Signal to the main app that we want to exit
235
+ # The actual exit handling is done in main.py
236
+ return True
237
+
238
+
239
+ @register_command(
240
+ name="agent",
241
+ description="Switch to a different agent or show available agents",
242
+ usage="/agent <name>, /a <name>",
243
+ aliases=["a"],
244
+ category="core",
245
+ )
246
+ def handle_agent_command(command: str) -> bool:
247
+ """Handle agent switching."""
248
+ from rich.text import Text
249
+
250
+ from code_puppy.agents import (
251
+ get_agent_descriptions,
252
+ get_available_agents,
253
+ get_current_agent,
254
+ set_current_agent,
255
+ )
256
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
257
+
258
+ tokens = command.split()
259
+
260
+ if len(tokens) == 1:
261
+ # Show interactive agent picker
262
+ try:
263
+ # Run the async picker using asyncio utilities
264
+ # Since we're called from an async context but this function is sync,
265
+ # we need to carefully schedule and wait for the coroutine
266
+ import asyncio
267
+ import concurrent.futures
268
+ import uuid
269
+
270
+ # Create a new event loop in a thread and run the picker there
271
+ with concurrent.futures.ThreadPoolExecutor() as executor:
272
+ future = executor.submit(
273
+ lambda: asyncio.run(interactive_agent_picker())
274
+ )
275
+ selected_agent = future.result(timeout=300) # 5 min timeout
276
+
277
+ if selected_agent:
278
+ current_agent = get_current_agent()
279
+ # Check if we're already using this agent
280
+ if current_agent.name == selected_agent:
281
+ group_id = str(uuid.uuid4())
282
+ emit_info(
283
+ f"Already using agent: {current_agent.display_name}",
284
+ message_group=group_id,
285
+ )
286
+ return True
287
+
288
+ # Switch to the new agent
289
+ group_id = str(uuid.uuid4())
290
+ new_session_id = finalize_autosave_session()
291
+ if not set_current_agent(selected_agent):
292
+ emit_warning(
293
+ "Agent switch failed after autosave rotation. Your context was preserved.",
294
+ message_group=group_id,
295
+ )
296
+ return True
297
+
298
+ new_agent = get_current_agent()
299
+ new_agent.reload_code_generation_agent()
300
+ emit_success(
301
+ f"Switched to agent: {new_agent.display_name}",
302
+ message_group=group_id,
303
+ )
304
+ emit_info(f"{new_agent.description}", message_group=group_id)
305
+ emit_info(
306
+ Text.from_markup(
307
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
308
+ ),
309
+ message_group=group_id,
310
+ )
311
+ else:
312
+ emit_warning("Agent selection cancelled")
313
+ return True
314
+ except Exception as e:
315
+ # Fallback to old behavior if picker fails
316
+ import traceback
317
+ import uuid
318
+
319
+ emit_warning(f"Interactive picker failed: {e}")
320
+ emit_warning(f"Traceback: {traceback.format_exc()}")
321
+
322
+ # Show current agent and available agents
323
+ current_agent = get_current_agent()
324
+ available_agents = get_available_agents()
325
+ descriptions = get_agent_descriptions()
326
+
327
+ # Generate a group ID for all messages in this command
328
+ group_id = str(uuid.uuid4())
329
+
330
+ emit_info(
331
+ Text.from_markup(
332
+ f"[bold green]Current Agent:[/bold green] {current_agent.display_name}"
333
+ ),
334
+ message_group=group_id,
335
+ )
336
+ emit_info(
337
+ Text.from_markup(f"[dim]{current_agent.description}[/dim]\n"),
338
+ message_group=group_id,
339
+ )
340
+
341
+ emit_info(
342
+ Text.from_markup("[bold magenta]Available Agents:[/bold magenta]"),
343
+ message_group=group_id,
344
+ )
345
+ for name, display_name in available_agents.items():
346
+ description = descriptions.get(name, "No description")
347
+ current_marker = (
348
+ " [green]← current[/green]" if name == current_agent.name else ""
349
+ )
350
+ emit_info(
351
+ Text.from_markup(
352
+ f" [cyan]{name:<12}[/cyan] {display_name}{current_marker}"
353
+ ),
354
+ message_group=group_id,
355
+ )
356
+ emit_info(f" {description}", message_group=group_id)
357
+
358
+ emit_info(
359
+ Text.from_markup("\n[yellow]Usage:[/yellow] /agent <agent-name>"),
360
+ message_group=group_id,
361
+ )
362
+ return True
363
+
364
+ elif len(tokens) == 2:
365
+ agent_name = tokens[1].lower()
366
+
367
+ # Generate a group ID for all messages in this command
368
+ import uuid
369
+
370
+ group_id = str(uuid.uuid4())
371
+ available_agents = get_available_agents()
372
+
373
+ if agent_name not in available_agents:
374
+ emit_error(f"Agent '{agent_name}' not found", message_group=group_id)
375
+ emit_warning(
376
+ f"Available agents: {', '.join(available_agents.keys())}",
377
+ message_group=group_id,
378
+ )
379
+ return True
380
+
381
+ current_agent = get_current_agent()
382
+ if current_agent.name == agent_name:
383
+ emit_info(
384
+ f"Already using agent: {current_agent.display_name}",
385
+ message_group=group_id,
386
+ )
387
+ return True
388
+
389
+ new_session_id = finalize_autosave_session()
390
+ if not set_current_agent(agent_name):
391
+ emit_warning(
392
+ "Agent switch failed after autosave rotation. Your context was preserved.",
393
+ message_group=group_id,
394
+ )
395
+ return True
396
+
397
+ new_agent = get_current_agent()
398
+ new_agent.reload_code_generation_agent()
399
+ emit_success(
400
+ f"Switched to agent: {new_agent.display_name}",
401
+ message_group=group_id,
402
+ )
403
+ emit_info(f"{new_agent.description}", message_group=group_id)
404
+ emit_info(
405
+ Text.from_markup(
406
+ f"[dim]Auto-save session rotated to: {new_session_id}[/dim]"
407
+ ),
408
+ message_group=group_id,
409
+ )
410
+ return True
411
+ else:
412
+ emit_warning("Usage: /agent [agent-name]")
413
+ return True
414
+
415
+
416
+ async def interactive_model_picker() -> str | None:
417
+ """Show an interactive arrow-key selector to pick a model (async version).
418
+
419
+ Returns:
420
+ The selected model name, or None if cancelled
421
+ """
422
+ import asyncio
423
+ import sys
424
+
425
+ from rich.console import Console
426
+ from rich.panel import Panel
427
+ from rich.text import Text
428
+
429
+ from code_puppy.command_line.model_picker_completion import (
430
+ get_active_model,
431
+ load_model_names,
432
+ )
433
+ from code_puppy.tools.command_runner import set_awaiting_user_input
434
+ from code_puppy.tools.common import arrow_select_async
435
+
436
+ # Load available models
437
+ model_names = load_model_names()
438
+ current_model = get_active_model()
439
+
440
+ # Build choices with current model indicator
441
+ choices = []
442
+ for model_name in model_names:
443
+ if model_name == current_model:
444
+ choices.append(f"✓ {model_name} (current)")
445
+ else:
446
+ choices.append(f" {model_name}")
447
+
448
+ # Create panel content
449
+ panel_content = Text()
450
+ panel_content.append("🤖 Select a model to use\n", style="bold cyan")
451
+ panel_content.append("Current model: ", style="dim")
452
+ panel_content.append(current_model, style="bold green")
453
+
454
+ # Display panel
455
+ panel = Panel(
456
+ panel_content,
457
+ title="[bold white]Model Selection[/bold white]",
458
+ border_style="cyan",
459
+ padding=(1, 2),
460
+ )
461
+
462
+ # Pause spinners BEFORE showing panel
463
+ set_awaiting_user_input(True)
464
+ await asyncio.sleep(0.3) # Let spinners fully stop
465
+
466
+ local_console = Console()
467
+ emit_info("")
468
+ local_console.print(panel)
469
+ emit_info("")
470
+
471
+ # Flush output before prompt_toolkit takes control
472
+ sys.stdout.flush()
473
+ sys.stderr.flush()
474
+ await asyncio.sleep(0.1)
475
+
476
+ selected_model = None
477
+
478
+ try:
479
+ # Final flush
480
+ sys.stdout.flush()
481
+
482
+ # Show arrow-key selector (async version)
483
+ choice = await arrow_select_async(
484
+ "💭 Which model would you like to use?",
485
+ choices,
486
+ )
487
+
488
+ # Extract model name from choice (remove prefix and suffix)
489
+ if choice:
490
+ # Remove the "✓ " or " " prefix and " (current)" suffix if present
491
+ selected_model = choice.strip().lstrip("✓").strip()
492
+ if selected_model.endswith(" (current)"):
493
+ selected_model = selected_model[:-10].strip()
494
+
495
+ except (KeyboardInterrupt, EOFError):
496
+ emit_error("Cancelled by user")
497
+ selected_model = None
498
+
499
+ finally:
500
+ set_awaiting_user_input(False)
501
+
502
+ return selected_model
503
+
504
+
505
+ @register_command(
506
+ name="model",
507
+ description="Set active model",
508
+ usage="/model, /m <model>",
509
+ aliases=["m"],
510
+ category="core",
511
+ )
512
+ def handle_model_command(command: str) -> bool:
513
+ """Set the active model."""
514
+ import asyncio
515
+
516
+ from code_puppy.command_line.model_picker_completion import (
517
+ get_active_model,
518
+ load_model_names,
519
+ set_active_model,
520
+ )
521
+ from code_puppy.messaging import emit_success, emit_warning
522
+
523
+ tokens = command.split()
524
+
525
+ # If just /model or /m with no args, show interactive picker
526
+ if len(tokens) == 1:
527
+ try:
528
+ # Run the async picker using asyncio utilities
529
+ # Since we're called from an async context but this function is sync,
530
+ # we need to carefully schedule and wait for the coroutine
531
+ import concurrent.futures
532
+
533
+ # Create a new event loop in a thread and run the picker there
534
+ with concurrent.futures.ThreadPoolExecutor() as executor:
535
+ future = executor.submit(
536
+ lambda: asyncio.run(interactive_model_picker())
537
+ )
538
+ selected_model = future.result(timeout=300) # 5 min timeout
539
+
540
+ if selected_model:
541
+ set_active_model(selected_model)
542
+ emit_success(f"Active model set and loaded: {selected_model}")
543
+ else:
544
+ emit_warning("Model selection cancelled")
545
+ return True
546
+ except Exception as e:
547
+ # Fallback to old behavior if picker fails
548
+ import traceback
549
+
550
+ emit_warning(f"Interactive picker failed: {e}")
551
+ emit_warning(f"Traceback: {traceback.format_exc()}")
552
+ model_names = load_model_names()
553
+ emit_warning("Usage: /model <model-name> or /m <model-name>")
554
+ emit_warning(f"Available models: {', '.join(model_names)}")
555
+ return True
556
+
557
+ # Handle both /model and /m for backward compatibility
558
+ model_command = command
559
+ if command.startswith("/model"):
560
+ # Convert /model to /m for internal processing
561
+ model_command = command.replace("/model", "/m", 1)
562
+
563
+ # If model matched, set it
564
+ new_input = update_model_in_input(model_command)
565
+ if new_input is not None:
566
+ model = get_active_model()
567
+ emit_success(f"Active model set and loaded: {model}")
568
+ return True
569
+
570
+ # If no model matched, show error
571
+ model_names = load_model_names()
572
+ emit_warning("Usage: /model <model-name> or /m <model-name>")
573
+ emit_warning(f"Available models: {', '.join(model_names)}")
574
+ return True
575
+
576
+
577
+ @register_command(
578
+ name="add_model",
579
+ description="Browse and add models from models.dev catalog",
580
+ usage="/add_model",
581
+ category="core",
582
+ )
583
+ def handle_add_model_command(command: str) -> bool:
584
+ """Launch interactive model browser TUI."""
585
+ from code_puppy.command_line.add_model_menu import interactive_model_picker
586
+ from code_puppy.tools.command_runner import set_awaiting_user_input
587
+
588
+ set_awaiting_user_input(True)
589
+ try:
590
+ # interactive_model_picker is now synchronous - no async complications!
591
+ result = interactive_model_picker()
592
+
593
+ if result:
594
+ emit_info("Successfully added model configuration")
595
+ return True
596
+ except KeyboardInterrupt:
597
+ # User cancelled - this is expected behavior
598
+ return True
599
+ except Exception as e:
600
+ emit_error(f"Failed to launch model browser: {e}")
601
+ return False
602
+ finally:
603
+ set_awaiting_user_input(False)
604
+
605
+
606
+ @register_command(
607
+ name="model_settings",
608
+ description="Configure per-model settings (temperature, seed, etc.)",
609
+ usage="/model_settings [--show [model_name]]",
610
+ aliases=["ms"],
611
+ category="config",
612
+ )
613
+ def handle_model_settings_command(command: str) -> bool:
614
+ """Launch interactive model settings TUI.
615
+
616
+ Opens a TUI showing all available models. Select a model to configure
617
+ its settings (temperature, seed, etc.). ESC closes the TUI.
618
+
619
+ Use --show [model_name] to display current settings without the TUI.
620
+ """
621
+ from code_puppy.command_line.model_settings_menu import (
622
+ interactive_model_settings,
623
+ show_model_settings_summary,
624
+ )
625
+ from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
626
+ from code_puppy.tools.command_runner import set_awaiting_user_input
627
+
628
+ tokens = command.split()
629
+
630
+ # Check for --show flag to just display current settings
631
+ if "--show" in tokens:
632
+ model_name = None
633
+ for t in tokens[1:]:
634
+ if not t.startswith("--"):
635
+ model_name = t
636
+ break
637
+ show_model_settings_summary(model_name)
638
+ return True
639
+
640
+ set_awaiting_user_input(True)
641
+ try:
642
+ result = interactive_model_settings()
643
+
644
+ if result:
645
+ emit_success("Model settings updated successfully")
646
+
647
+ # Always reload the active agent so settings take effect
648
+ from code_puppy.agents import get_current_agent
649
+
650
+ try:
651
+ current_agent = get_current_agent()
652
+ current_agent.reload_code_generation_agent()
653
+ emit_info("Active agent reloaded")
654
+ except Exception as reload_error:
655
+ emit_warning(f"Agent reload failed: {reload_error}")
656
+
657
+ return True
658
+ except KeyboardInterrupt:
659
+ return True
660
+ except Exception as e:
661
+ emit_error(f"Failed to launch model settings: {e}")
662
+ return False
663
+ finally:
664
+ set_awaiting_user_input(False)
665
+
666
+
667
+ @register_command(
668
+ name="mcp",
669
+ description="Manage MCP servers (list, start, stop, status, etc.)",
670
+ usage="/mcp",
671
+ category="core",
672
+ )
673
+ def handle_mcp_command(command: str) -> bool:
674
+ """Handle MCP server management."""
675
+ from code_puppy.command_line.mcp import MCPCommandHandler
676
+
677
+ handler = MCPCommandHandler()
678
+ return handler.handle_mcp_command(command)
679
+
680
+
681
+ @register_command(
682
+ name="api",
683
+ description="Manage the Code Puppy API server",
684
+ usage="/api [start|stop|status]",
685
+ category="core",
686
+ detailed_help="Start, stop, or check status of the local FastAPI server for GUI integration.",
687
+ )
688
+ def handle_api_command(command: str) -> bool:
689
+ """Handle the /api command."""
690
+ import os
691
+ import signal
692
+ import subprocess
693
+ import sys
694
+ from pathlib import Path
695
+
696
+ from code_puppy.config import STATE_DIR
697
+ from code_puppy.messaging import emit_error, emit_info, emit_success
698
+
699
+ parts = command.split()
700
+ subcommand = parts[1] if len(parts) > 1 else "status"
701
+
702
+ pid_file = Path(STATE_DIR) / "api_server.pid"
703
+
704
+ if subcommand == "start":
705
+ # Check if already running
706
+ if pid_file.exists():
707
+ try:
708
+ pid = int(pid_file.read_text().strip())
709
+ os.kill(pid, 0) # Check if process exists
710
+ emit_info(f"API server already running (PID {pid})")
711
+ return True
712
+ except (OSError, ValueError):
713
+ pid_file.unlink(missing_ok=True) # Stale PID file
714
+
715
+ # Start the server in background
716
+ emit_info("Starting API server on http://127.0.0.1:8765 ...")
717
+ proc = subprocess.Popen(
718
+ [sys.executable, "-m", "code_puppy.api.main"],
719
+ stdout=subprocess.DEVNULL,
720
+ stderr=subprocess.DEVNULL,
721
+ start_new_session=True,
722
+ )
723
+ pid_file.parent.mkdir(parents=True, exist_ok=True)
724
+ pid_file.write_text(str(proc.pid))
725
+ emit_success(f"API server started (PID {proc.pid})")
726
+ emit_info("Docs available at http://127.0.0.1:8765/docs")
727
+ return True
728
+
729
+ elif subcommand == "stop":
730
+ if not pid_file.exists():
731
+ emit_info("API server is not running")
732
+ return True
733
+
734
+ try:
735
+ pid = int(pid_file.read_text().strip())
736
+ os.kill(pid, signal.SIGTERM)
737
+ pid_file.unlink()
738
+ emit_success(f"API server stopped (PID {pid})")
739
+ except (OSError, ValueError) as e:
740
+ pid_file.unlink(missing_ok=True)
741
+ emit_error(f"Error stopping server: {e}")
742
+ return True
743
+
744
+ elif subcommand == "status":
745
+ if not pid_file.exists():
746
+ emit_info("API server is not running")
747
+ return True
748
+
749
+ try:
750
+ pid = int(pid_file.read_text().strip())
751
+ os.kill(pid, 0) # Check if process exists
752
+ emit_success(f"API server is running (PID {pid})")
753
+ emit_info("URL: http://127.0.0.1:8765")
754
+ emit_info("Docs: http://127.0.0.1:8765/docs")
755
+ except (OSError, ValueError):
756
+ pid_file.unlink(missing_ok=True)
757
+ emit_info("API server is not running (stale PID file removed)")
758
+ return True
759
+
760
+ else:
761
+ emit_error(f"Unknown subcommand: {subcommand}")
762
+ emit_info("Usage: /api [start|stop|status]")
763
+ return True
764
+
765
+
766
+ @register_command(
767
+ name="generate-pr-description",
768
+ description="Generate comprehensive PR description",
769
+ usage="/generate-pr-description [@dir]",
770
+ category="core",
771
+ )
772
+ def handle_generate_pr_description_command(command: str) -> str:
773
+ """Generate a PR description."""
774
+ # Parse directory argument (e.g., /generate-pr-description @some/dir)
775
+ tokens = command.split()
776
+ directory_context = ""
777
+ for t in tokens:
778
+ if t.startswith("@"):
779
+ directory_context = f" Please work in the directory: {t[1:]}"
780
+ break
781
+
782
+ # Hard-coded prompt from user requirements
783
+ pr_prompt = f"""Generate a comprehensive PR description for my current branch changes. Follow these steps:
784
+
785
+ 1 Discover the changes: Use git CLI to find the base branch (usually main/master/develop) and get the list of changed files, commits, and diffs.
786
+ 2 Analyze the code: Read and analyze all modified files to understand:
787
+ • What functionality was added/changed/removed
788
+ • The technical approach and implementation details
789
+ • Any architectural or design pattern changes
790
+ • Dependencies added/removed/updated
791
+ 3 Generate a structured PR description with these sections:
792
+ • Title: Concise, descriptive title (50 chars max)
793
+ • Summary: Brief overview of what this PR accomplishes
794
+ • Changes Made: Detailed bullet points of specific changes
795
+ • Technical Details: Implementation approach, design decisions, patterns used
796
+ • Files Modified: List of key files with brief description of changes
797
+ • Testing: What was tested and how (if applicable)
798
+ • Breaking Changes: Any breaking changes (if applicable)
799
+ • Additional Notes: Any other relevant information
800
+ 4 Create a markdown file: Generate a PR_DESCRIPTION.md file with proper GitHub markdown formatting that I can directly copy-paste into GitHub's PR
801
+ description field. Use proper markdown syntax with headers, bullet points, code blocks, and formatting.
802
+ 5 Make it review-ready: Ensure the description helps reviewers understand the context, approach, and impact of the changes.
803
+ 6. If you have Github MCP, or gh cli is installed and authenticated then find the PR for the branch we analyzed and update the PR description there and then delete the PR_DESCRIPTION.md file. (If you have a better name (title) for the PR, go ahead and update the title too.{directory_context}"""
804
+
805
+ # Return the prompt to be processed by the main chat system
806
+ return pr_prompt
807
+
808
+
809
+ @register_command(
810
+ name="wiggum",
811
+ description="Loop mode: re-run the same prompt when agent finishes (like Wiggum chasing donuts 🍩)",
812
+ usage="/wiggum <prompt>",
813
+ category="core",
814
+ )
815
+ def handle_wiggum_command(command: str) -> str | bool:
816
+ """Start wiggum loop mode.
817
+
818
+ When active, the agent will automatically re-run the same prompt
819
+ after completing, resetting context each time. Use Ctrl+C to stop.
820
+
821
+ Example:
822
+ /wiggum say hello world
823
+ """
824
+ from code_puppy.command_line.wiggum_state import start_wiggum
825
+ from code_puppy.messaging import emit_info, emit_success, emit_warning
826
+
827
+ # Extract the prompt after /wiggum
828
+ parts = command.split(maxsplit=1)
829
+ if len(parts) < 2 or not parts[1].strip():
830
+ emit_warning("Usage: /wiggum <prompt>")
831
+ emit_info("Example: /wiggum say hello world")
832
+ emit_info("This will repeatedly run 'say hello world' after each completion.")
833
+ emit_info("Press Ctrl+C to stop the loop.")
834
+ return True
835
+
836
+ prompt = parts[1].strip()
837
+
838
+ # Start wiggum mode
839
+ start_wiggum(prompt)
840
+ emit_success("🍩 WIGGUM MODE ACTIVATED!")
841
+ emit_info(f"Prompt: {prompt}")
842
+ emit_info("The agent will re-loop this prompt after each completion.")
843
+ emit_info("Press Ctrl+C to stop the wiggum loop.")
844
+
845
+ # Return the prompt to execute immediately
846
+ return prompt
847
+
848
+
849
+ @register_command(
850
+ name="wiggum_stop",
851
+ description="Stop wiggum loop mode",
852
+ usage="/wiggum_stop",
853
+ aliases=["stopwiggum", "ws"],
854
+ category="core",
855
+ )
856
+ def handle_wiggum_stop_command(command: str) -> bool:
857
+ """Stop wiggum loop mode."""
858
+ from code_puppy.command_line.wiggum_state import is_wiggum_active, stop_wiggum
859
+ from code_puppy.messaging import emit_info, emit_success
860
+
861
+ if is_wiggum_active():
862
+ stop_wiggum()
863
+ emit_success("🍩 Wiggum mode stopped!")
864
+ else:
865
+ emit_info("Wiggum mode is not active.")
866
+
867
+ return True