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,293 @@
1
+ # Import to trigger command registration
2
+ import code_puppy.command_line.config_commands # noqa: F401
3
+ import code_puppy.command_line.core_commands # noqa: F401
4
+ import code_puppy.command_line.session_commands # noqa: F401
5
+ import code_puppy.command_line.uc_menu # noqa: F401
6
+
7
+ # Global flag to track if plugins have been loaded
8
+ _PLUGINS_LOADED = False
9
+
10
+
11
+ def get_commands_help():
12
+ """Generate aligned commands help using Rich Text for safe markup.
13
+
14
+ Now dynamically generates help from the command registry!
15
+ Only shows two sections: Built-in Commands and Custom Commands.
16
+ """
17
+ from rich.text import Text
18
+
19
+ from code_puppy.command_line.command_registry import get_unique_commands
20
+
21
+ # Ensure plugins are loaded so custom help can register
22
+ _ensure_plugins_loaded()
23
+
24
+ lines: list[Text] = []
25
+ # No global header needed - user already knows they're viewing help
26
+
27
+ # Collect all built-in commands (registered + legacy)
28
+ builtin_cmds: list[tuple[str, str]] = []
29
+
30
+ # Get registered commands (all categories are built-in)
31
+ registered_commands = get_unique_commands()
32
+ for cmd_info in sorted(registered_commands, key=lambda c: c.name):
33
+ builtin_cmds.append((cmd_info.usage, cmd_info.description))
34
+
35
+ # Get custom commands from plugins
36
+ custom_entries: list[tuple[str, str]] = []
37
+ try:
38
+ from code_puppy import callbacks
39
+
40
+ custom_help_results = callbacks.on_custom_command_help()
41
+ for res in custom_help_results:
42
+ if not res:
43
+ continue
44
+ # Format 1: Tuple with (command_name, description)
45
+ if isinstance(res, tuple) and len(res) == 2:
46
+ cmd_name = str(res[0])
47
+ custom_entries.append((f"/{cmd_name}", str(res[1])))
48
+ # Format 2: List of tuples or strings
49
+ elif isinstance(res, list):
50
+ # Check if it's a list of tuples (preferred format)
51
+ if res and isinstance(res[0], tuple) and len(res[0]) == 2:
52
+ for item in res:
53
+ if isinstance(item, tuple) and len(item) == 2:
54
+ cmd_name = str(item[0])
55
+ custom_entries.append((f"/{cmd_name}", str(item[1])))
56
+ # Format 3: List of strings (legacy format)
57
+ # Extract command from first line like "/command_name - Description"
58
+ elif res and isinstance(res[0], str) and res[0].startswith("/"):
59
+ first_line = res[0]
60
+ if " - " in first_line:
61
+ parts = first_line.split(" - ", 1)
62
+ cmd_name = parts[0].lstrip("/").strip()
63
+ description = parts[1].strip()
64
+ custom_entries.append((f"/{cmd_name}", description))
65
+ except Exception:
66
+ pass
67
+
68
+ # Calculate global column width (longest command across ALL sections + padding)
69
+ all_commands = builtin_cmds + custom_entries
70
+ if all_commands:
71
+ max_cmd_width = max(len(cmd) for cmd, _ in all_commands)
72
+ column_width = max_cmd_width + 4 # Add 4 spaces padding
73
+ else:
74
+ column_width = 30
75
+
76
+ # Maximum description width before truncation (to prevent line wrapping)
77
+ max_desc_width = 80
78
+
79
+ def truncate_desc(desc: str, max_width: int) -> str:
80
+ """Truncate description if too long, add ellipsis."""
81
+ if len(desc) <= max_width:
82
+ return desc
83
+ return desc[: max_width - 3] + "..."
84
+
85
+ # Display Built-in Commands section (starts immediately, no blank line)
86
+ lines.append(Text("Built-in Commands", style="bold magenta"))
87
+ for cmd, desc in sorted(builtin_cmds, key=lambda x: x[0]):
88
+ truncated_desc = truncate_desc(desc, max_desc_width)
89
+ left = Text(cmd.ljust(column_width), style="cyan")
90
+ right = Text(truncated_desc)
91
+ line = Text()
92
+ line.append_text(left)
93
+ line.append_text(right)
94
+ lines.append(line)
95
+
96
+ # Display Custom Commands section (if any)
97
+ if custom_entries:
98
+ lines.append(Text(""))
99
+ lines.append(Text("Custom Commands", style="bold magenta"))
100
+ for cmd, desc in sorted(custom_entries, key=lambda x: x[0]):
101
+ truncated_desc = truncate_desc(desc, max_desc_width)
102
+ left = Text(cmd.ljust(column_width), style="cyan")
103
+ right = Text(truncated_desc)
104
+ line = Text()
105
+ line.append_text(left)
106
+ line.append_text(right)
107
+ lines.append(line)
108
+
109
+ # Display Shell Pass-through section
110
+ lines.append(Text(""))
111
+ lines.append(Text("Shell Pass-through", style="bold magenta"))
112
+ shell_left = Text("!<command>".ljust(column_width), style="cyan")
113
+ shell_right = Text("Run a shell command directly (e.g., !git status)")
114
+ shell_line = Text()
115
+ shell_line.append_text(shell_left)
116
+ shell_line.append_text(shell_right)
117
+ lines.append(shell_line)
118
+
119
+ final_text = Text()
120
+ for i, line in enumerate(lines):
121
+ if i > 0:
122
+ final_text.append("\n")
123
+ final_text.append_text(line)
124
+
125
+ # Add trailing newline for spacing before next prompt
126
+ final_text.append("\n")
127
+
128
+ return final_text
129
+
130
+
131
+ # ============================================================================
132
+ # IMPORT BUILT-IN COMMAND HANDLERS
133
+ # ============================================================================
134
+ # All built-in command handlers have been split into category-specific files.
135
+ # These imports trigger their registration via @register_command decorators.
136
+
137
+ # ============================================================================
138
+ # UTILITY FUNCTIONS
139
+ # ============================================================================
140
+
141
+
142
+ def _ensure_plugins_loaded() -> None:
143
+ global _PLUGINS_LOADED
144
+ if _PLUGINS_LOADED:
145
+ return
146
+ try:
147
+ from code_puppy import plugins
148
+
149
+ plugins.load_plugin_callbacks()
150
+ _PLUGINS_LOADED = True
151
+ except Exception as e:
152
+ # If plugins fail to load, continue gracefully but note it
153
+ try:
154
+ from code_puppy.messaging import emit_warning
155
+
156
+ emit_warning(f"Plugin load error: {e}")
157
+ except Exception:
158
+ pass
159
+ _PLUGINS_LOADED = True
160
+
161
+
162
+ # All command handlers moved to builtin_commands.py
163
+ # The import above triggers their registration
164
+
165
+ # ============================================================================
166
+ # MAIN COMMAND DISPATCHER
167
+ # ============================================================================
168
+
169
+ # _show_color_options has been moved to builtin_commands.py
170
+
171
+
172
+ def handle_command(command: str):
173
+ """
174
+ Handle commands prefixed with '/'.
175
+
176
+ Args:
177
+ command: The command string to handle
178
+
179
+ Returns:
180
+ True if the command was handled, False if not, or a string to be processed as user input
181
+ """
182
+ from rich.text import Text
183
+
184
+ from code_puppy.command_line.command_registry import get_command
185
+ from code_puppy.messaging import emit_info, emit_warning
186
+
187
+ _ensure_plugins_loaded()
188
+
189
+ command = command.strip()
190
+
191
+ # Check if this is a registered command
192
+ if command.startswith("/"):
193
+ # Extract command name (first word after /)
194
+ cmd_name = command[1:].split()[0] if len(command) > 1 else ""
195
+
196
+ # Try to find in registry
197
+ cmd_info = get_command(cmd_name)
198
+ if cmd_info:
199
+ # Execute the registered handler
200
+ return cmd_info.handler(command)
201
+
202
+ # ========================================================================
203
+ # LEGACY COMMAND FALLBACK
204
+ # ========================================================================
205
+ # This section is kept as a fallback mechanism for commands added in other
206
+ # branches that haven't been migrated to the registry system yet.
207
+ #
208
+ # All current commands are registered above using @register_command, so
209
+ # they won't fall through to this section.
210
+ #
211
+ # If you're rebasing and your branch adds a new command using the old
212
+ # if/elif style, it will still work! Just add your if block below.
213
+ #
214
+ # EXAMPLE: How to add a legacy command:
215
+ #
216
+ # if command.startswith("/mycommand"):
217
+ # from code_puppy.messaging import emit_info
218
+ # emit_info("My command executed!")
219
+ # return True
220
+ #
221
+ # NOTE: For new commands, please use @register_command instead (see above).
222
+ # ========================================================================
223
+
224
+ # Legacy commands from other branches/rebases go here:
225
+ # (All current commands are in the registry above)
226
+
227
+ # Example placeholder (remove this and add your command if needed):
228
+ # if command.startswith("/my_new_command"):
229
+ # from code_puppy.messaging import emit_info
230
+ # emit_info("Command executed!")
231
+ # return True
232
+
233
+ # End of legacy fallback section
234
+ # ========================================================================
235
+
236
+ # All legacy command implementations have been moved to @register_command handlers above.
237
+ # If you're adding a new command via rebase, add your if block here.
238
+
239
+ # Try plugin-provided custom commands before unknown warning
240
+ if command.startswith("/"):
241
+ # Extract command name without leading slash and arguments intact
242
+ name = command[1:].split()[0] if len(command) > 1 else ""
243
+ try:
244
+ from code_puppy import callbacks
245
+
246
+ # Import the special result class for markdown commands
247
+ try:
248
+ from code_puppy.plugins.customizable_commands.register_callbacks import (
249
+ MarkdownCommandResult,
250
+ )
251
+ except ImportError:
252
+ MarkdownCommandResult = None
253
+
254
+ results = callbacks.on_custom_command(command=command, name=name)
255
+ # Iterate through callback results; treat str as handled (no model run)
256
+ for res in results:
257
+ if res is True:
258
+ return True
259
+ if MarkdownCommandResult and isinstance(res, MarkdownCommandResult):
260
+ # Special case: markdown command that should be processed as input
261
+ # Replace the command with the markdown content and let it be processed
262
+ # This is handled by the caller, so return the content as string
263
+ return res.content
264
+ if isinstance(res, str):
265
+ # Display returned text to the user and treat as handled
266
+ try:
267
+ emit_info(res)
268
+ except Exception:
269
+ pass
270
+ return True
271
+ except Exception as e:
272
+ # Log via emit_error but do not block default handling
273
+ emit_warning(f"Custom command hook error: {e}")
274
+
275
+ if name:
276
+ emit_warning(
277
+ Text.from_markup(
278
+ f"Unknown command: {command}\n[dim]Type /help for options.[/dim]"
279
+ )
280
+ )
281
+ else:
282
+ # Show current model ONLY here
283
+ from code_puppy.command_line.model_picker_completion import get_active_model
284
+
285
+ current_model = get_active_model()
286
+ emit_info(
287
+ Text.from_markup(
288
+ f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
289
+ )
290
+ )
291
+ return True
292
+
293
+ return False
@@ -0,0 +1,150 @@
1
+ """Command registry for dynamic command discovery.
2
+
3
+ This module provides a decorator-based registration system for commands,
4
+ enabling automatic help generation and eliminating static command lists.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Callable, Dict, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class CommandInfo:
13
+ """Metadata for a registered command."""
14
+
15
+ name: str
16
+ description: str
17
+ handler: Callable[[str], bool]
18
+ usage: str = ""
19
+ aliases: List[str] = field(default_factory=list)
20
+ category: str = "core"
21
+ detailed_help: Optional[str] = None
22
+
23
+ def __post_init__(self):
24
+ """Set default usage if not provided."""
25
+ if not self.usage:
26
+ self.usage = f"/{self.name}"
27
+
28
+
29
+ # Global registry: maps command name/alias -> CommandInfo
30
+ _COMMAND_REGISTRY: Dict[str, CommandInfo] = {}
31
+
32
+
33
+ def register_command(
34
+ name: str,
35
+ description: str,
36
+ usage: str = "",
37
+ aliases: Optional[List[str]] = None,
38
+ category: str = "core",
39
+ detailed_help: Optional[str] = None,
40
+ ):
41
+ """Decorator to register a command handler.
42
+
43
+ This decorator registers a command function so it can be:
44
+ - Auto-discovered by the help system
45
+ - Invoked by handle_command() dynamically
46
+ - Grouped by category
47
+ - Documented with aliases and detailed help
48
+
49
+ Args:
50
+ name: Primary command name (without leading /)
51
+ description: Short one-line description for help text
52
+ usage: Full usage string (e.g., "/cd <dir>"). Defaults to "/{name}"
53
+ aliases: List of alternative names (without leading /)
54
+ category: Grouping category ("core", "session", "config", etc.)
55
+ detailed_help: Optional detailed help text for /help <command>
56
+
57
+ Example:
58
+ >>> @register_command(
59
+ ... name="session",
60
+ ... description="Show or rotate autosave session ID",
61
+ ... usage="/session [id|new]",
62
+ ... aliases=["s"],
63
+ ... category="session",
64
+ ... )
65
+ ... def handle_session(command: str) -> bool:
66
+ ... return True
67
+
68
+ Returns:
69
+ The decorated function, unchanged
70
+ """
71
+
72
+ def decorator(func: Callable[[str], bool]) -> Callable[[str], bool]:
73
+ # Create CommandInfo instance
74
+ cmd_info = CommandInfo(
75
+ name=name,
76
+ description=description,
77
+ handler=func,
78
+ usage=usage,
79
+ aliases=aliases or [],
80
+ category=category,
81
+ detailed_help=detailed_help,
82
+ )
83
+
84
+ # Register primary name
85
+ _COMMAND_REGISTRY[name] = cmd_info
86
+
87
+ # Register all aliases pointing to the same CommandInfo
88
+ for alias in aliases or []:
89
+ _COMMAND_REGISTRY[alias] = cmd_info
90
+
91
+ return func
92
+
93
+ return decorator
94
+
95
+
96
+ def get_all_commands() -> Dict[str, CommandInfo]:
97
+ """Get all registered commands.
98
+
99
+ Returns:
100
+ Dictionary mapping command names/aliases to CommandInfo objects.
101
+ Note: Aliases point to the same CommandInfo as their primary command.
102
+ """
103
+ return _COMMAND_REGISTRY.copy()
104
+
105
+
106
+ def get_unique_commands() -> List[CommandInfo]:
107
+ """Get unique registered commands (no duplicates from aliases).
108
+
109
+ Returns:
110
+ List of unique CommandInfo objects (one per primary command).
111
+ """
112
+ seen = set()
113
+ unique = []
114
+ for cmd_info in _COMMAND_REGISTRY.values():
115
+ # Use object id to avoid duplicates from aliases
116
+ if id(cmd_info) not in seen:
117
+ seen.add(id(cmd_info))
118
+ unique.append(cmd_info)
119
+ return unique
120
+
121
+
122
+ def get_command(name: str) -> Optional[CommandInfo]:
123
+ """Get command info by name or alias (case-insensitive).
124
+
125
+ First tries exact match for backward compatibility, then falls back to
126
+ case-insensitive matching.
127
+
128
+ Args:
129
+ name: Command name or alias (without leading /)
130
+
131
+ Returns:
132
+ CommandInfo if found, None otherwise
133
+ """
134
+ # First try exact match (for backward compatibility)
135
+ exact_match = _COMMAND_REGISTRY.get(name)
136
+ if exact_match is not None:
137
+ return exact_match
138
+
139
+ # If no exact match, try case-insensitive matching
140
+ name_lower = name.lower()
141
+ for registered_name, cmd_info in _COMMAND_REGISTRY.items():
142
+ if registered_name.lower() == name_lower:
143
+ return cmd_info
144
+
145
+ return None
146
+
147
+
148
+ def clear_registry():
149
+ """Clear all registered commands. Useful for testing."""
150
+ _COMMAND_REGISTRY.clear()