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,174 @@
1
+ import logging
2
+ from typing import Iterable
3
+
4
+ from prompt_toolkit.completion import Completer, Completion
5
+ from prompt_toolkit.document import Document
6
+
7
+ # Configure logging
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def load_server_names():
12
+ """Load server names from the MCP manager."""
13
+ try:
14
+ from code_puppy.mcp_.manager import MCPManager
15
+
16
+ manager = MCPManager()
17
+ servers = manager.list_servers()
18
+ return [server.name for server in servers]
19
+ except Exception as e:
20
+ logger.debug(f"Could not load server names: {e}")
21
+ return []
22
+
23
+
24
+ class MCPCompleter(Completer):
25
+ """
26
+ A completer that triggers on '/mcp' to show available MCP subcommands
27
+ and server names where appropriate.
28
+ """
29
+
30
+ def __init__(self, trigger: str = "/mcp"):
31
+ self.trigger = trigger
32
+
33
+ # Define all available MCP subcommands
34
+ # Subcommands that take server names as arguments
35
+ self.server_subcommands = {
36
+ "start": "Start a specific MCP server",
37
+ "stop": "Stop a specific MCP server",
38
+ "restart": "Restart a specific MCP server",
39
+ "status": "Show status of a specific MCP server",
40
+ "logs": "Show logs for a specific MCP server",
41
+ "edit": "Edit an existing MCP server config",
42
+ "remove": "Remove an MCP server",
43
+ }
44
+
45
+ # Subcommands that don't take server names
46
+ self.general_subcommands = {
47
+ "list": "List all registered MCP servers",
48
+ "start-all": "Start all MCP servers",
49
+ "stop-all": "Stop all MCP servers",
50
+ "test": "Test MCP server connection",
51
+ "add": "Add a new MCP server",
52
+ "install": "Install MCP servers from a list",
53
+ "search": "Search for available MCP servers",
54
+ "help": "Show help for MCP commands",
55
+ }
56
+
57
+ # All subcommands combined for completion when no subcommand is typed yet
58
+ self.all_subcommands = {**self.server_subcommands, **self.general_subcommands}
59
+
60
+ # Cache server names to avoid repeated lookups
61
+ self._server_names_cache = None
62
+ self._cache_timestamp = None
63
+
64
+ def _get_server_names(self):
65
+ """Get server names with caching."""
66
+ import time
67
+
68
+ # Cache for 30 seconds to avoid repeated manager calls
69
+ current_time = time.time()
70
+ if (
71
+ self._server_names_cache is None
72
+ or self._cache_timestamp is None
73
+ or current_time - self._cache_timestamp > 30
74
+ ):
75
+ self._server_names_cache = load_server_names()
76
+ self._cache_timestamp = current_time
77
+
78
+ return self._server_names_cache or []
79
+
80
+ def get_completions(
81
+ self, document: Document, complete_event
82
+ ) -> Iterable[Completion]:
83
+ text = document.text
84
+ cursor_position = document.cursor_position
85
+ text_before_cursor = text[:cursor_position]
86
+
87
+ # Only trigger if /mcp is at the very beginning of the line
88
+ stripped_text = text_before_cursor.lstrip()
89
+ if not stripped_text.startswith(self.trigger):
90
+ return
91
+
92
+ # Find where /mcp actually starts (after any leading whitespace)
93
+ mcp_pos = text_before_cursor.find(self.trigger)
94
+ mcp_end = mcp_pos + len(self.trigger)
95
+
96
+ # Require a space after /mcp before showing completions
97
+ if mcp_end >= len(text_before_cursor) or text_before_cursor[mcp_end] != " ":
98
+ return
99
+
100
+ # Extract everything after /mcp (and after the space)
101
+ after_mcp = text_before_cursor[mcp_end + 1 :].strip()
102
+
103
+ # If nothing after /mcp, show all available subcommands
104
+ if not after_mcp:
105
+ for subcommand, description in sorted(self.all_subcommands.items()):
106
+ yield Completion(
107
+ subcommand,
108
+ start_position=0,
109
+ display=subcommand,
110
+ display_meta=description,
111
+ )
112
+ return
113
+
114
+ # Parse what's been typed after /mcp
115
+ # Split by space but be careful with what we're currently typing
116
+ parts = after_mcp.split()
117
+
118
+ # Priority: Check for server name completion first when appropriate
119
+ # This handles cases like '/mcp start ' where the space indicates ready for server name
120
+ if len(parts) >= 1:
121
+ subcommand = parts[0].lower()
122
+
123
+ # Only complete server names for specific subcommands
124
+ if subcommand in self.server_subcommands:
125
+ # Case 1: Exactly the subcommand followed by a space (ready for server name)
126
+ if len(parts) == 1 and text.endswith(" "):
127
+ partial_server = ""
128
+ start_position = 0
129
+
130
+ server_names = self._get_server_names()
131
+ for server_name in sorted(server_names):
132
+ yield Completion(
133
+ server_name,
134
+ start_position=start_position,
135
+ display=server_name,
136
+ display_meta="MCP Server",
137
+ )
138
+ return
139
+
140
+ # Case 2: Subcommand + partial server name (require space after subcommand)
141
+ elif len(parts) == 2 and cursor_position > (
142
+ mcp_end + 1 + len(subcommand) + 1
143
+ ):
144
+ partial_server = parts[1]
145
+ start_position = -(len(partial_server))
146
+
147
+ server_names = self._get_server_names()
148
+ for server_name in sorted(server_names):
149
+ if server_name.lower().startswith(partial_server.lower()):
150
+ yield Completion(
151
+ server_name,
152
+ start_position=start_position,
153
+ display=server_name,
154
+ display_meta="MCP Server",
155
+ )
156
+ return
157
+
158
+ # If we only have one part and haven't returned above, show subcommand completions
159
+ # This includes cases like '/mcp start' where they might want 'start-all'
160
+ # But NOT when there's a space after the subcommand (which indicates they want arguments)
161
+ if len(parts) == 1 and not text.endswith(" "):
162
+ partial_subcommand = parts[0]
163
+ for subcommand, description in sorted(self.all_subcommands.items()):
164
+ if subcommand.startswith(partial_subcommand):
165
+ yield Completion(
166
+ subcommand,
167
+ start_position=-(len(partial_subcommand)),
168
+ display=subcommand,
169
+ display_meta=description,
170
+ )
171
+ return
172
+
173
+ # For general subcommands, we don't provide argument completion
174
+ # They may have their own specific completions in the future
@@ -0,0 +1,197 @@
1
+ import os
2
+ from typing import Iterable, Optional
3
+
4
+ from prompt_toolkit import PromptSession
5
+ from prompt_toolkit.completion import Completer, Completion
6
+ from prompt_toolkit.document import Document
7
+ from prompt_toolkit.history import FileHistory
8
+
9
+ from code_puppy.config import get_global_model_name
10
+ from code_puppy.model_factory import ModelFactory
11
+ from code_puppy.model_switching import set_model_and_reload_agent
12
+
13
+
14
+ def load_model_names():
15
+ """Load model names from the config that's fetched from the endpoint."""
16
+ models_config = ModelFactory.load_config()
17
+ return list(models_config.keys())
18
+
19
+
20
+ def get_active_model():
21
+ """
22
+ Returns the active model from the config using get_model_name().
23
+ This ensures consistency across the codebase by always using the config value.
24
+ """
25
+ return get_global_model_name()
26
+
27
+
28
+ def set_active_model(model_name: str):
29
+ """
30
+ Sets the active model name by updating the config (for persistence).
31
+ """
32
+ set_model_and_reload_agent(model_name)
33
+
34
+
35
+ class ModelNameCompleter(Completer):
36
+ """
37
+ A completer that triggers on '/model' to show available models from models.json.
38
+ Only '/model' (not just '/') will trigger the dropdown.
39
+ """
40
+
41
+ def __init__(self, trigger: str = "/model"):
42
+ self.trigger = trigger
43
+ self.model_names = load_model_names()
44
+
45
+ def get_completions(
46
+ self, document: Document, complete_event
47
+ ) -> Iterable[Completion]:
48
+ text = document.text
49
+ cursor_position = document.cursor_position
50
+ text_before_cursor = text[:cursor_position]
51
+
52
+ # Only trigger if /model is at the very beginning of the line and has a space after it
53
+ stripped_text = text_before_cursor.lstrip()
54
+ if not stripped_text.startswith(self.trigger + " "):
55
+ return
56
+
57
+ # Find where /model actually starts (after any leading whitespace)
58
+ symbol_pos = text_before_cursor.find(self.trigger)
59
+ text_after_trigger = text_before_cursor[
60
+ symbol_pos + len(self.trigger) + 1 :
61
+ ].lstrip()
62
+ start_position = -(len(text_after_trigger))
63
+
64
+ # Filter model names based on what's typed after /model (case-insensitive)
65
+ for model_name in self.model_names:
66
+ if text_after_trigger and not model_name.lower().startswith(
67
+ text_after_trigger.lower()
68
+ ):
69
+ continue # Skip models that don't match the typed text
70
+
71
+ meta = (
72
+ "Model (selected)"
73
+ if model_name.lower() == get_active_model().lower()
74
+ else "Model"
75
+ )
76
+ yield Completion(
77
+ model_name,
78
+ start_position=start_position,
79
+ display=model_name,
80
+ display_meta=meta,
81
+ )
82
+
83
+
84
+ def _find_matching_model(rest: str, model_names: list[str]) -> Optional[str]:
85
+ """
86
+ Find the best matching model for the given input.
87
+
88
+ Priority:
89
+ 1. Exact match (case-insensitive)
90
+ 2. Input starts with a model name (longest/most specific wins)
91
+ 3. Model starts with input (prefix/completion match, longest wins)
92
+ """
93
+ rest_lower = rest.lower()
94
+
95
+ # First check for exact match
96
+ for model in model_names:
97
+ if rest_lower == model.lower():
98
+ return model
99
+
100
+ # Sort by length (longest first) so more specific matches win
101
+ sorted_models = sorted(model_names, key=len, reverse=True)
102
+
103
+ # Check if input starts with a model name (e.g. "gpt-5 tell me a joke")
104
+ for model in sorted_models:
105
+ model_lower = model.lower()
106
+ if rest_lower.startswith(model_lower) and (
107
+ len(rest_lower) == len(model_lower) or rest_lower[len(model_lower)] == " "
108
+ ):
109
+ return model
110
+
111
+ # Check for prefix/completion match (input is partial model name)
112
+ for model in sorted_models:
113
+ if model.lower().startswith(rest_lower):
114
+ return model
115
+
116
+ return None
117
+
118
+
119
+ def update_model_in_input(text: str) -> Optional[str]:
120
+ # If input starts with /model or /m and a model name, set model and strip it out
121
+ content = text.strip()
122
+ model_names = load_model_names()
123
+
124
+ # Check for /model command (require space after /model, case-insensitive)
125
+ if content.lower().startswith("/model "):
126
+ # Find the actual /model command (case-insensitive)
127
+ model_cmd = content.split(" ", 1)[0] # Get the command part
128
+ rest = content[len(model_cmd) :].strip() # Remove the actual command
129
+
130
+ # Find the best matching model
131
+ model = _find_matching_model(rest, model_names)
132
+ if model:
133
+ # Found a matching model - now extract it properly
134
+ set_active_model(model)
135
+
136
+ # Find the actual model name in the original text (preserving case)
137
+ # We need to find where the model ends in the original rest string
138
+ model_end_idx = len(model)
139
+
140
+ # Build the full command+model part to remove
141
+ cmd_and_model_pattern = model_cmd + " " + rest[:model_end_idx]
142
+ idx = text.find(cmd_and_model_pattern)
143
+ if idx != -1:
144
+ new_text = (
145
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
146
+ ).strip()
147
+ return new_text
148
+ return None
149
+
150
+ # Check for /m command (case-insensitive)
151
+ elif content.lower().startswith("/m ") and not content.lower().startswith(
152
+ "/model "
153
+ ):
154
+ # Find the actual /m command (case-insensitive)
155
+ m_cmd = content.split(" ", 1)[0] # Get the command part
156
+ rest = content[len(m_cmd) :].strip() # Remove the actual command
157
+
158
+ # Find the best matching model
159
+ model = _find_matching_model(rest, model_names)
160
+ if model:
161
+ # Found a matching model - now extract it properly
162
+ set_active_model(model)
163
+
164
+ # Find the actual model name in the original text (preserving case)
165
+ # We need to find where the model ends in the original rest string
166
+ model_end_idx = len(model)
167
+
168
+ # Build the full command+model part to remove
169
+ # Handle space variations in the original text
170
+ cmd_and_model_pattern = m_cmd + " " + rest[:model_end_idx]
171
+ idx = text.find(cmd_and_model_pattern)
172
+ if idx != -1:
173
+ new_text = (
174
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
175
+ ).strip()
176
+ return new_text
177
+ return None
178
+
179
+ return None
180
+
181
+
182
+ async def get_input_with_model_completion(
183
+ prompt_str: str = ">>> ",
184
+ trigger: str = "/model",
185
+ history_file: Optional[str] = None,
186
+ ) -> str:
187
+ history = FileHistory(os.path.expanduser(history_file)) if history_file else None
188
+ session = PromptSession(
189
+ completer=ModelNameCompleter(trigger),
190
+ history=history,
191
+ complete_while_typing=True,
192
+ )
193
+ text = await session.prompt_async(prompt_str)
194
+ possibly_stripped = update_model_in_input(text)
195
+ if possibly_stripped is not None:
196
+ return possibly_stripped
197
+ return text