code-puppy 0.0.169__py3-none-any.whl → 0.0.366__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +8 -8
  3. code_puppy/agents/agent_c_reviewer.py +155 -0
  4. code_puppy/agents/agent_code_puppy.py +9 -2
  5. code_puppy/agents/agent_code_reviewer.py +90 -0
  6. code_puppy/agents/agent_cpp_reviewer.py +132 -0
  7. code_puppy/agents/agent_creator_agent.py +48 -9
  8. code_puppy/agents/agent_golang_reviewer.py +151 -0
  9. code_puppy/agents/agent_javascript_reviewer.py +160 -0
  10. code_puppy/agents/agent_manager.py +146 -199
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +90 -0
  15. code_puppy/agents/agent_qa_expert.py +163 -0
  16. code_puppy/agents/agent_qa_kitten.py +208 -0
  17. code_puppy/agents/agent_security_auditor.py +181 -0
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +166 -0
  20. code_puppy/agents/base_agent.py +1713 -1
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/json_agent.py +12 -1
  23. code_puppy/agents/pack/__init__.py +34 -0
  24. code_puppy/agents/pack/bloodhound.py +304 -0
  25. code_puppy/agents/pack/husky.py +321 -0
  26. code_puppy/agents/pack/retriever.py +393 -0
  27. code_puppy/agents/pack/shepherd.py +348 -0
  28. code_puppy/agents/pack/terrier.py +287 -0
  29. code_puppy/agents/pack/watchdog.py +367 -0
  30. code_puppy/agents/prompt_reviewer.py +145 -0
  31. code_puppy/agents/subagent_stream_handler.py +276 -0
  32. code_puppy/api/__init__.py +13 -0
  33. code_puppy/api/app.py +169 -0
  34. code_puppy/api/main.py +21 -0
  35. code_puppy/api/pty_manager.py +446 -0
  36. code_puppy/api/routers/__init__.py +12 -0
  37. code_puppy/api/routers/agents.py +36 -0
  38. code_puppy/api/routers/commands.py +217 -0
  39. code_puppy/api/routers/config.py +74 -0
  40. code_puppy/api/routers/sessions.py +232 -0
  41. code_puppy/api/templates/terminal.html +361 -0
  42. code_puppy/api/websocket.py +154 -0
  43. code_puppy/callbacks.py +174 -4
  44. code_puppy/chatgpt_codex_client.py +283 -0
  45. code_puppy/claude_cache_client.py +586 -0
  46. code_puppy/cli_runner.py +916 -0
  47. code_puppy/command_line/add_model_menu.py +1079 -0
  48. code_puppy/command_line/agent_menu.py +395 -0
  49. code_puppy/command_line/attachments.py +395 -0
  50. code_puppy/command_line/autosave_menu.py +605 -0
  51. code_puppy/command_line/clipboard.py +527 -0
  52. code_puppy/command_line/colors_menu.py +520 -0
  53. code_puppy/command_line/command_handler.py +233 -627
  54. code_puppy/command_line/command_registry.py +150 -0
  55. code_puppy/command_line/config_commands.py +715 -0
  56. code_puppy/command_line/core_commands.py +792 -0
  57. code_puppy/command_line/diff_menu.py +863 -0
  58. code_puppy/command_line/load_context_completion.py +15 -22
  59. code_puppy/command_line/mcp/base.py +1 -4
  60. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  61. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  62. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  63. code_puppy/command_line/mcp/edit_command.py +148 -0
  64. code_puppy/command_line/mcp/handler.py +9 -4
  65. code_puppy/command_line/mcp/help_command.py +6 -5
  66. code_puppy/command_line/mcp/install_command.py +16 -27
  67. code_puppy/command_line/mcp/install_menu.py +685 -0
  68. code_puppy/command_line/mcp/list_command.py +3 -3
  69. code_puppy/command_line/mcp/logs_command.py +174 -65
  70. code_puppy/command_line/mcp/remove_command.py +2 -2
  71. code_puppy/command_line/mcp/restart_command.py +12 -4
  72. code_puppy/command_line/mcp/search_command.py +17 -11
  73. code_puppy/command_line/mcp/start_all_command.py +22 -13
  74. code_puppy/command_line/mcp/start_command.py +50 -31
  75. code_puppy/command_line/mcp/status_command.py +6 -7
  76. code_puppy/command_line/mcp/stop_all_command.py +11 -8
  77. code_puppy/command_line/mcp/stop_command.py +11 -10
  78. code_puppy/command_line/mcp/test_command.py +2 -2
  79. code_puppy/command_line/mcp/utils.py +1 -1
  80. code_puppy/command_line/mcp/wizard_utils.py +22 -18
  81. code_puppy/command_line/mcp_completion.py +174 -0
  82. code_puppy/command_line/model_picker_completion.py +89 -30
  83. code_puppy/command_line/model_settings_menu.py +884 -0
  84. code_puppy/command_line/motd.py +14 -8
  85. code_puppy/command_line/onboarding_slides.py +179 -0
  86. code_puppy/command_line/onboarding_wizard.py +340 -0
  87. code_puppy/command_line/pin_command_completion.py +329 -0
  88. code_puppy/command_line/prompt_toolkit_completion.py +626 -75
  89. code_puppy/command_line/session_commands.py +296 -0
  90. code_puppy/command_line/utils.py +54 -0
  91. code_puppy/config.py +1181 -51
  92. code_puppy/error_logging.py +118 -0
  93. code_puppy/gemini_code_assist.py +385 -0
  94. code_puppy/gemini_model.py +602 -0
  95. code_puppy/http_utils.py +220 -104
  96. code_puppy/keymap.py +128 -0
  97. code_puppy/main.py +5 -594
  98. code_puppy/{mcp → mcp_}/__init__.py +17 -0
  99. code_puppy/{mcp → mcp_}/async_lifecycle.py +35 -4
  100. code_puppy/{mcp → mcp_}/blocking_startup.py +70 -43
  101. code_puppy/{mcp → mcp_}/captured_stdio_server.py +2 -2
  102. code_puppy/{mcp → mcp_}/config_wizard.py +5 -5
  103. code_puppy/{mcp → mcp_}/dashboard.py +15 -6
  104. code_puppy/{mcp → mcp_}/examples/retry_example.py +4 -1
  105. code_puppy/{mcp → mcp_}/managed_server.py +66 -39
  106. code_puppy/{mcp → mcp_}/manager.py +146 -52
  107. code_puppy/mcp_/mcp_logs.py +224 -0
  108. code_puppy/{mcp → mcp_}/registry.py +6 -6
  109. code_puppy/{mcp → mcp_}/server_registry_catalog.py +25 -8
  110. code_puppy/messaging/__init__.py +199 -2
  111. code_puppy/messaging/bus.py +610 -0
  112. code_puppy/messaging/commands.py +167 -0
  113. code_puppy/messaging/markdown_patches.py +57 -0
  114. code_puppy/messaging/message_queue.py +17 -48
  115. code_puppy/messaging/messages.py +500 -0
  116. code_puppy/messaging/queue_console.py +1 -24
  117. code_puppy/messaging/renderers.py +43 -146
  118. code_puppy/messaging/rich_renderer.py +1027 -0
  119. code_puppy/messaging/spinner/__init__.py +33 -5
  120. code_puppy/messaging/spinner/console_spinner.py +92 -52
  121. code_puppy/messaging/spinner/spinner_base.py +29 -0
  122. code_puppy/messaging/subagent_console.py +461 -0
  123. code_puppy/model_factory.py +686 -80
  124. code_puppy/model_utils.py +167 -0
  125. code_puppy/models.json +86 -104
  126. code_puppy/models_dev_api.json +1 -0
  127. code_puppy/models_dev_parser.py +592 -0
  128. code_puppy/plugins/__init__.py +164 -10
  129. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  130. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  131. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  132. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  133. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  134. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  135. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  136. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  137. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  138. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  139. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  140. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  141. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  142. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  143. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  144. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  145. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  146. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  147. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  148. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  149. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  150. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  151. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  152. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  153. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  154. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  155. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  156. code_puppy/plugins/example_custom_command/README.md +280 -0
  157. code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
  158. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  159. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  160. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  161. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  162. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  163. code_puppy/plugins/oauth_puppy_html.py +228 -0
  164. code_puppy/plugins/shell_safety/__init__.py +6 -0
  165. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  166. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  167. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  168. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  169. code_puppy/prompts/codex_system_prompt.md +310 -0
  170. code_puppy/pydantic_patches.py +131 -0
  171. code_puppy/reopenable_async_client.py +8 -8
  172. code_puppy/round_robin_model.py +10 -15
  173. code_puppy/session_storage.py +294 -0
  174. code_puppy/status_display.py +21 -4
  175. code_puppy/summarization_agent.py +52 -14
  176. code_puppy/terminal_utils.py +418 -0
  177. code_puppy/tools/__init__.py +139 -6
  178. code_puppy/tools/agent_tools.py +548 -49
  179. code_puppy/tools/browser/__init__.py +37 -0
  180. code_puppy/tools/browser/browser_control.py +289 -0
  181. code_puppy/tools/browser/browser_interactions.py +545 -0
  182. code_puppy/tools/browser/browser_locators.py +640 -0
  183. code_puppy/tools/browser/browser_manager.py +316 -0
  184. code_puppy/tools/browser/browser_navigation.py +251 -0
  185. code_puppy/tools/browser/browser_screenshot.py +179 -0
  186. code_puppy/tools/browser/browser_scripts.py +462 -0
  187. code_puppy/tools/browser/browser_workflows.py +221 -0
  188. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  189. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  190. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  191. code_puppy/tools/browser/terminal_tools.py +525 -0
  192. code_puppy/tools/command_runner.py +941 -153
  193. code_puppy/tools/common.py +1146 -6
  194. code_puppy/tools/display.py +84 -0
  195. code_puppy/tools/file_modifications.py +288 -89
  196. code_puppy/tools/file_operations.py +352 -266
  197. code_puppy/tools/subagent_context.py +158 -0
  198. code_puppy/uvx_detection.py +242 -0
  199. code_puppy/version_checker.py +30 -11
  200. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  201. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  202. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/METADATA +184 -67
  203. code_puppy-0.0.366.dist-info/RECORD +217 -0
  204. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  205. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +1 -0
  206. code_puppy/agent.py +0 -231
  207. code_puppy/agents/agent_orchestrator.json +0 -26
  208. code_puppy/agents/runtime_manager.py +0 -272
  209. code_puppy/command_line/mcp/add_command.py +0 -183
  210. code_puppy/command_line/meta_command_handler.py +0 -153
  211. code_puppy/message_history_processor.py +0 -490
  212. code_puppy/messaging/spinner/textual_spinner.py +0 -101
  213. code_puppy/state_management.py +0 -200
  214. code_puppy/tui/__init__.py +0 -10
  215. code_puppy/tui/app.py +0 -986
  216. code_puppy/tui/components/__init__.py +0 -21
  217. code_puppy/tui/components/chat_view.py +0 -550
  218. code_puppy/tui/components/command_history_modal.py +0 -218
  219. code_puppy/tui/components/copy_button.py +0 -139
  220. code_puppy/tui/components/custom_widgets.py +0 -63
  221. code_puppy/tui/components/human_input_modal.py +0 -175
  222. code_puppy/tui/components/input_area.py +0 -167
  223. code_puppy/tui/components/sidebar.py +0 -309
  224. code_puppy/tui/components/status_bar.py +0 -182
  225. code_puppy/tui/messages.py +0 -27
  226. code_puppy/tui/models/__init__.py +0 -8
  227. code_puppy/tui/models/chat_message.py +0 -25
  228. code_puppy/tui/models/command_history.py +0 -89
  229. code_puppy/tui/models/enums.py +0 -24
  230. code_puppy/tui/screens/__init__.py +0 -15
  231. code_puppy/tui/screens/help.py +0 -130
  232. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  233. code_puppy/tui/screens/settings.py +0 -290
  234. code_puppy/tui/screens/tools.py +0 -74
  235. code_puppy-0.0.169.data/data/code_puppy/models.json +0 -128
  236. code_puppy-0.0.169.dist-info/RECORD +0 -112
  237. /code_puppy/{mcp → mcp_}/circuit_breaker.py +0 -0
  238. /code_puppy/{mcp → mcp_}/error_isolation.py +0 -0
  239. /code_puppy/{mcp → mcp_}/health_monitor.py +0 -0
  240. /code_puppy/{mcp → mcp_}/retry_manager.py +0 -0
  241. /code_puppy/{mcp → mcp_}/status_tracker.py +0 -0
  242. /code_puppy/{mcp → mcp_}/system_tools.py +0 -0
  243. {code_puppy-0.0.169.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -7,9 +7,10 @@ from datetime import datetime
7
7
  from typing import List, Optional
8
8
 
9
9
  from rich.panel import Panel
10
+ from rich.text import Text
10
11
 
11
- from code_puppy.mcp.managed_server import ServerState
12
- from code_puppy.messaging import emit_info
12
+ from code_puppy.mcp_.managed_server import ServerState
13
+ from code_puppy.messaging import emit_error, emit_info
13
14
 
14
15
  from .base import MCPCommandBase
15
16
  from .list_command import ListCommand
@@ -117,7 +118,7 @@ class StatusCommand(MCPCommandBase):
117
118
 
118
119
  # Check async lifecycle manager status if available
119
120
  try:
120
- from code_puppy.mcp.async_lifecycle import get_lifecycle_manager
121
+ from code_puppy.mcp_.async_lifecycle import get_lifecycle_manager
121
122
 
122
123
  lifecycle_mgr = get_lifecycle_manager()
123
124
  if lifecycle_mgr.is_running(server_id):
@@ -158,7 +159,7 @@ class StatusCommand(MCPCommandBase):
158
159
  status_lines.append(f"[bold]Metadata:[/bold] {len(metadata)} keys")
159
160
 
160
161
  # Create and show the panel
161
- panel_content = "\n".join(status_lines)
162
+ panel_content = Text.from_markup("\n".join(status_lines))
162
163
  panel = Panel(
163
164
  panel_content, title=f"🔌 {server_name} Status", border_style="cyan"
164
165
  )
@@ -180,6 +181,4 @@ class StatusCommand(MCPCommandBase):
180
181
  logger.error(
181
182
  f"Error getting detailed status for server '{server_name}': {e}"
182
183
  )
183
- emit_info(
184
- f"[red]Error getting server status: {e}[/red]", message_group=group_id
185
- )
184
+ emit_error(f"Error getting server status: {e}", message_group=group_id)
@@ -6,9 +6,12 @@ import logging
6
6
  import time
7
7
  from typing import List, Optional
8
8
 
9
- from code_puppy.mcp.managed_server import ServerState
9
+ from rich.text import Text
10
+
11
+ from code_puppy.mcp_.managed_server import ServerState
10
12
  from code_puppy.messaging import emit_info
11
13
 
14
+ from ...agents import get_current_agent
12
15
  from .base import MCPCommandBase
13
16
 
14
17
  # Configure logging
@@ -91,14 +94,14 @@ class StopAllCommand(MCPCommandBase):
91
94
  pass # No async loop, servers will stop when needed
92
95
 
93
96
  try:
94
- from code_puppy.agents.runtime_manager import (
95
- get_runtime_agent_manager,
96
- )
97
-
98
- manager = get_runtime_agent_manager()
99
- manager.reload_agent()
97
+ agent = get_current_agent()
98
+ agent.reload_code_generation_agent()
99
+ # Update MCP tool cache immediately so token counts reflect the change
100
+ agent.update_mcp_tool_cache_sync()
100
101
  emit_info(
101
- "[dim]Agent reloaded with updated servers[/dim]",
102
+ Text.from_markup(
103
+ "[dim]Agent reloaded with updated servers[/dim]"
104
+ ),
102
105
  message_group=group_id,
103
106
  )
104
107
  except Exception as e:
@@ -5,8 +5,11 @@ MCP Stop Command - Stops a specific MCP server.
5
5
  import logging
6
6
  from typing import List, Optional
7
7
 
8
- from code_puppy.messaging import emit_info
8
+ from rich.text import Text
9
9
 
10
+ from code_puppy.messaging import emit_error, emit_info
11
+
12
+ from ...agents import get_current_agent
10
13
  from .base import MCPCommandBase
11
14
  from .utils import find_server_id_by_name, suggest_similar_servers
12
15
 
@@ -34,7 +37,7 @@ class StopCommand(MCPCommandBase):
34
37
 
35
38
  if not args:
36
39
  emit_info(
37
- "[yellow]Usage: /mcp stop <server_name>[/yellow]",
40
+ Text.from_markup("[yellow]Usage: /mcp stop <server_name>[/yellow]"),
38
41
  message_group=group_id,
39
42
  )
40
43
  return
@@ -57,14 +60,12 @@ class StopCommand(MCPCommandBase):
57
60
 
58
61
  # Reload the agent to remove the disabled server
59
62
  try:
60
- from code_puppy.agents.runtime_manager import (
61
- get_runtime_agent_manager,
62
- )
63
-
64
- manager = get_runtime_agent_manager()
65
- manager.reload_agent()
63
+ agent = get_current_agent()
64
+ agent.reload_code_generation_agent()
65
+ # Update MCP tool cache immediately so token counts reflect the change
66
+ agent.update_mcp_tool_cache_sync()
66
67
  emit_info(
67
- "[dim]Agent reloaded with updated servers[/dim]",
68
+ "Agent reloaded with updated servers",
68
69
  message_group=group_id,
69
70
  )
70
71
  except Exception as e:
@@ -76,4 +77,4 @@ class StopCommand(MCPCommandBase):
76
77
 
77
78
  except Exception as e:
78
79
  logger.error(f"Error stopping server '{server_name}': {e}")
79
- emit_info(f"[red]Failed to stop server: {e}[/red]", message_group=group_id)
80
+ emit_error(f"Failed to stop server: {e}", message_group=group_id)
@@ -5,7 +5,7 @@ MCP Test Command - Tests connectivity to a specific MCP server.
5
5
  import logging
6
6
  from typing import List, Optional
7
7
 
8
- from code_puppy.messaging import emit_info
8
+ from code_puppy.messaging import emit_error, emit_info
9
9
 
10
10
  from .base import MCPCommandBase
11
11
  from .utils import find_server_id_by_name, suggest_similar_servers
@@ -104,4 +104,4 @@ class TestCommand(MCPCommandBase):
104
104
 
105
105
  except Exception as e:
106
106
  logger.error(f"Error testing server '{server_name}': {e}")
107
- emit_info(f"[red]Error testing server: {e}[/red]", message_group=group_id)
107
+ emit_error(f"Error testing server: {e}", message_group=group_id)
@@ -8,7 +8,7 @@ from typing import Optional
8
8
 
9
9
  from rich.text import Text
10
10
 
11
- from code_puppy.mcp.managed_server import ServerState
11
+ from code_puppy.mcp_.managed_server import ServerState
12
12
 
13
13
 
14
14
  def format_state_indicator(state: ServerState) -> Text:
@@ -7,7 +7,9 @@ Provides interactive functionality for installing and configuring MCP servers.
7
7
  import logging
8
8
  from typing import Any, Dict, Optional
9
9
 
10
- from code_puppy.messaging import emit_info, emit_prompt
10
+ from rich.text import Text
11
+
12
+ from code_puppy.messaging import emit_error, emit_info, emit_prompt
11
13
 
12
14
  # Configure logging
13
15
  logger = logging.getLogger(__name__)
@@ -51,7 +53,7 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
51
53
  required_env_vars = selected_server.get_environment_vars()
52
54
  if required_env_vars:
53
55
  emit_info(
54
- "\n[yellow]Required Environment Variables:[/yellow]",
56
+ Text.from_markup("\n[yellow]Required Environment Variables:[/yellow]"),
55
57
  message_group=group_id,
56
58
  )
57
59
  for var in required_env_vars:
@@ -61,7 +63,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
61
63
  current_value = os.environ.get(var, "")
62
64
  if current_value:
63
65
  emit_info(
64
- f" {var}: [green]Already set[/green]", message_group=group_id
66
+ Text.from_markup(f" {var}: [green]Already set[/green]"),
67
+ message_group=group_id,
65
68
  )
66
69
  env_vars[var] = current_value
67
70
  else:
@@ -73,7 +76,8 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
73
76
  required_cmd_args = selected_server.get_command_line_args()
74
77
  if required_cmd_args:
75
78
  emit_info(
76
- "\n[yellow]Command Line Arguments:[/yellow]", message_group=group_id
79
+ Text.from_markup("\n[yellow]Command Line Arguments:[/yellow]"),
80
+ message_group=group_id,
77
81
  )
78
82
  for arg_config in required_cmd_args:
79
83
  name = arg_config.get("name", "")
@@ -101,11 +105,11 @@ def run_interactive_install_wizard(manager, group_id: str) -> bool:
101
105
  )
102
106
 
103
107
  except ImportError:
104
- emit_info("[red]Server catalog not available[/red]", message_group=group_id)
108
+ emit_error("Server catalog not available", message_group=group_id)
105
109
  return False
106
110
  except Exception as e:
107
111
  logger.error(f"Error in interactive wizard: {e}")
108
- emit_info(f"[red]Wizard error: {e}[/red]", message_group=group_id)
112
+ emit_error(f"Wizard error: {e}", message_group=group_id)
109
113
  return False
110
114
 
111
115
 
@@ -118,13 +122,11 @@ def interactive_server_selection(group_id: str):
118
122
  # This is a simplified version - the full implementation would have
119
123
  # category browsing, search, etc. For now, we'll just show popular servers
120
124
  try:
121
- from code_puppy.mcp.server_registry_catalog import catalog
125
+ from code_puppy.mcp_.server_registry_catalog import catalog
122
126
 
123
127
  servers = catalog.get_popular(10)
124
128
  if not servers:
125
- emit_info(
126
- "[red]No servers available in catalog[/red]", message_group=group_id
127
- )
129
+ emit_info("No servers available in catalog", message_group=group_id)
128
130
  return None
129
131
 
130
132
  emit_info("Popular MCP Servers:", message_group=group_id)
@@ -156,10 +158,10 @@ def interactive_server_selection(group_id: str):
156
158
  if 0 <= index < len(servers):
157
159
  return servers[index]
158
160
  else:
159
- emit_info("[red]Invalid selection[/red]", message_group=group_id)
161
+ emit_error("Invalid selection", message_group=group_id)
160
162
  return None
161
163
  except ValueError:
162
- emit_info("[red]Invalid input[/red]", message_group=group_id)
164
+ emit_error("Invalid input", message_group=group_id)
163
165
  return None
164
166
 
165
167
  except Exception as e:
@@ -215,7 +217,7 @@ def interactive_configure_server(
215
217
  if env_vars:
216
218
  emit_info("Environment Variables:", message_group=group_id)
217
219
  for var, value in env_vars.items():
218
- emit_info(f" {var}: [hidden]{value}[/hidden]", message_group=group_id)
220
+ emit_info(f" {var}: ***", message_group=group_id)
219
221
 
220
222
  if cmd_args:
221
223
  emit_info("Command Line Arguments:", message_group=group_id)
@@ -234,7 +236,7 @@ def interactive_configure_server(
234
236
 
235
237
  except Exception as e:
236
238
  logger.error(f"Error configuring server: {e}")
237
- emit_info(f"[red]Configuration error: {e}[/red]", message_group=group_id)
239
+ emit_error(f"Configuration error: {e}", message_group=group_id)
238
240
  return False
239
241
 
240
242
 
@@ -256,7 +258,7 @@ def install_server_from_catalog(
256
258
  import os
257
259
 
258
260
  from code_puppy.config import MCP_SERVERS_FILE
259
- from code_puppy.mcp.managed_server import ServerConfig
261
+ from code_puppy.mcp_.managed_server import ServerConfig
260
262
 
261
263
  # Set environment variables in the current environment
262
264
  for var, value in env_vars.items():
@@ -288,7 +290,7 @@ def install_server_from_catalog(
288
290
 
289
291
  if not server_id:
290
292
  emit_info(
291
- "[red]Failed to register server with manager[/red]",
293
+ "Failed to register server with manager",
292
294
  message_group=group_id,
293
295
  )
294
296
  return False
@@ -314,7 +316,9 @@ def install_server_from_catalog(
314
316
  json.dump(data, f, indent=2)
315
317
 
316
318
  emit_info(
317
- f"[green]✓ Successfully installed server: {server_name}[/green]",
319
+ Text.from_markup(
320
+ f"[green]✓ Successfully installed server: {server_name}[/green]"
321
+ ),
318
322
  message_group=group_id,
319
323
  )
320
324
  emit_info(
@@ -326,5 +330,5 @@ def install_server_from_catalog(
326
330
 
327
331
  except Exception as e:
328
332
  logger.error(f"Error installing server: {e}")
329
- emit_info(f"[red]Installation failed: {e}[/red]", message_group=group_id)
333
+ emit_error(f"Installation failed: {e}", message_group=group_id)
330
334
  return False
@@ -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
@@ -6,7 +6,7 @@ from prompt_toolkit.completion import Completer, Completion
6
6
  from prompt_toolkit.document import Document
7
7
  from prompt_toolkit.history import FileHistory
8
8
 
9
- from code_puppy.config import get_model_name, set_model_name
9
+ from code_puppy.config import get_global_model_name, set_model_name
10
10
  from code_puppy.model_factory import ModelFactory
11
11
 
12
12
 
@@ -21,21 +21,32 @@ def get_active_model():
21
21
  Returns the active model from the config using get_model_name().
22
22
  This ensures consistency across the codebase by always using the config value.
23
23
  """
24
- return get_model_name()
24
+ return get_global_model_name()
25
25
 
26
26
 
27
27
  def set_active_model(model_name: str):
28
28
  """
29
29
  Sets the active model name by updating the config (for persistence).
30
30
  """
31
+ from code_puppy.messaging import emit_info, emit_warning
32
+
31
33
  set_model_name(model_name)
32
- # Reload agent globally
34
+ # Reload the currently active agent so the new model takes effect immediately
33
35
  try:
34
- from code_puppy.agent import reload_code_generation_agent
35
-
36
- reload_code_generation_agent() # This will reload dynamically everywhere
37
- except Exception:
38
- pass # If reload fails, agent will still be switched next interpreter run
36
+ from code_puppy.agents import get_current_agent
37
+
38
+ current_agent = get_current_agent()
39
+ # JSON agents may need to refresh their config before reload
40
+ if hasattr(current_agent, "refresh_config"):
41
+ try:
42
+ current_agent.refresh_config()
43
+ except Exception:
44
+ # Non-fatal, continue to reload
45
+ ...
46
+ current_agent.reload_code_generation_agent()
47
+ emit_info("Active agent reloaded")
48
+ except Exception as e:
49
+ emit_warning(f"Model changed but agent reload failed: {e}")
39
50
 
40
51
 
41
52
  class ModelNameCompleter(Completer):
@@ -54,13 +65,31 @@ class ModelNameCompleter(Completer):
54
65
  text = document.text
55
66
  cursor_position = document.cursor_position
56
67
  text_before_cursor = text[:cursor_position]
57
- if self.trigger not in text_before_cursor:
68
+
69
+ # Only trigger if /model is at the very beginning of the line and has a space after it
70
+ stripped_text = text_before_cursor.lstrip()
71
+ if not stripped_text.startswith(self.trigger + " "):
58
72
  return
59
- symbol_pos = text_before_cursor.rfind(self.trigger)
60
- text_after_trigger = text_before_cursor[symbol_pos + len(self.trigger) :]
73
+
74
+ # Find where /model actually starts (after any leading whitespace)
75
+ symbol_pos = text_before_cursor.find(self.trigger)
76
+ text_after_trigger = text_before_cursor[
77
+ symbol_pos + len(self.trigger) + 1 :
78
+ ].lstrip()
61
79
  start_position = -(len(text_after_trigger))
80
+
81
+ # Filter model names based on what's typed after /model (case-insensitive)
62
82
  for model_name in self.model_names:
63
- meta = "Model (selected)" if model_name == get_active_model() else "Model"
83
+ if text_after_trigger and not model_name.lower().startswith(
84
+ text_after_trigger.lower()
85
+ ):
86
+ continue # Skip models that don't match the typed text
87
+
88
+ meta = (
89
+ "Model (selected)"
90
+ if model_name.lower() == get_active_model().lower()
91
+ else "Model"
92
+ )
64
93
  yield Completion(
65
94
  model_name,
66
95
  start_position=start_position,
@@ -72,32 +101,62 @@ class ModelNameCompleter(Completer):
72
101
  def update_model_in_input(text: str) -> Optional[str]:
73
102
  # If input starts with /model or /m and a model name, set model and strip it out
74
103
  content = text.strip()
75
-
76
- # Check for /model command
77
- if content.startswith("/model"):
78
- rest = content[6:].strip() # Remove '/model'
79
- for model in load_model_names():
80
- if rest == model:
104
+ model_names = load_model_names()
105
+
106
+ # Check for /model command (require space after /model, case-insensitive)
107
+ if content.lower().startswith("/model "):
108
+ # Find the actual /model command (case-insensitive)
109
+ model_cmd = content.split(" ", 1)[0] # Get the command part
110
+ rest = content[len(model_cmd) :].strip() # Remove the actual command
111
+
112
+ # Look for a model name at the start of rest (case-insensitive)
113
+ for model in model_names:
114
+ if rest.lower().startswith(model.lower()):
115
+ # Found a matching model - now extract it properly
81
116
  set_active_model(model)
82
- # Remove /model from the input
83
- idx = text.find("/model" + model)
117
+
118
+ # Find the actual model name in the original text (preserving case)
119
+ # We need to find where the model ends in the original rest string
120
+ model_end_idx = len(model)
121
+
122
+ # Build the full command+model part to remove
123
+ cmd_and_model_pattern = model_cmd + " " + rest[:model_end_idx]
124
+ idx = text.find(cmd_and_model_pattern)
84
125
  if idx != -1:
85
126
  new_text = (
86
- text[:idx] + text[idx + len("/model" + model) :]
127
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
87
128
  ).strip()
88
129
  return new_text
89
-
90
- # Check for /m command
91
- elif content.startswith("/m "):
92
- rest = content[3:].strip() # Remove '/m '
93
- for model in load_model_names():
94
- if rest == model:
130
+ return None
131
+
132
+ # Check for /m command (case-insensitive)
133
+ elif content.lower().startswith("/m ") and not content.lower().startswith(
134
+ "/model "
135
+ ):
136
+ # Find the actual /m command (case-insensitive)
137
+ m_cmd = content.split(" ", 1)[0] # Get the command part
138
+ rest = content[len(m_cmd) :].strip() # Remove the actual command
139
+
140
+ # Look for a model name at the start of rest (case-insensitive)
141
+ for model in model_names:
142
+ if rest.lower().startswith(model.lower()):
143
+ # Found a matching model - now extract it properly
95
144
  set_active_model(model)
96
- # Remove /m from the input
97
- idx = text.find("/m " + model)
145
+
146
+ # Find the actual model name in the original text (preserving case)
147
+ # We need to find where the model ends in the original rest string
148
+ model_end_idx = len(model)
149
+
150
+ # Build the full command+model part to remove
151
+ # Handle space variations in the original text
152
+ cmd_and_model_pattern = m_cmd + " " + rest[:model_end_idx]
153
+ idx = text.find(cmd_and_model_pattern)
98
154
  if idx != -1:
99
- new_text = (text[:idx] + text[idx + len("/m " + model) :]).strip()
155
+ new_text = (
156
+ text[:idx] + text[idx + len(cmd_and_model_pattern) :]
157
+ ).strip()
100
158
  return new_text
159
+ return None
101
160
 
102
161
  return None
103
162