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,705 @@
1
+ """Interactive terminal UI for browsing and installing MCP servers.
2
+
3
+ Provides a beautiful split-panel interface for browsing categories and servers
4
+ with live preview of server details and one-click installation.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ import sys
10
+ import time
11
+ from typing import List, Optional
12
+
13
+ from prompt_toolkit.application import Application
14
+ from prompt_toolkit.key_binding import KeyBindings
15
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
16
+ from prompt_toolkit.layout.controls import FormattedTextControl
17
+ from prompt_toolkit.widgets import Frame
18
+
19
+ from code_puppy.messaging import emit_error, emit_info, emit_warning
20
+ from code_puppy.tools.command_runner import set_awaiting_user_input
21
+
22
+ from .catalog_server_installer import (
23
+ install_catalog_server,
24
+ prompt_for_server_config,
25
+ )
26
+ from .custom_server_form import run_custom_server_form
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ PAGE_SIZE = 12 # Items per page
31
+
32
+ # Special category for custom servers
33
+ CUSTOM_SERVER_CATEGORY = "➕ Custom Server"
34
+
35
+
36
+ class MCPInstallMenu:
37
+ """Interactive TUI for browsing and installing MCP servers."""
38
+
39
+ def __init__(self, manager):
40
+ """Initialize the MCP server browser menu.
41
+
42
+ Args:
43
+ manager: MCP manager instance for server installation
44
+ """
45
+ self.manager = manager
46
+ self.catalog = None
47
+ self.categories: List[str] = []
48
+ self.current_category: Optional[str] = None
49
+ self.current_servers: List = []
50
+
51
+ # State management
52
+ self.view_mode = "categories" # "categories" or "servers"
53
+ self.selected_category_idx = 0
54
+ self.selected_server_idx = 0
55
+ self.current_page = 0
56
+ self.result = None # Track installation result
57
+
58
+ # Pending server for configuration
59
+ self.pending_server = None
60
+
61
+ # UI controls
62
+ self.menu_control = None
63
+ self.preview_control = None
64
+
65
+ # Initialize catalog
66
+ self._initialize_catalog()
67
+
68
+ def _initialize_catalog(self):
69
+ """Initialize the MCP server catalog with error handling."""
70
+ try:
71
+ from code_puppy.mcp_.server_registry_catalog import catalog
72
+
73
+ self.catalog = catalog
74
+ # Add custom server option as first category
75
+ self.categories = [CUSTOM_SERVER_CATEGORY] + self.catalog.list_categories()
76
+ if len(self.categories) <= 1: # Only custom category
77
+ emit_error("No categories found in server catalog")
78
+ except ImportError as e:
79
+ emit_error(f"Server catalog not available: {e}")
80
+ # Still allow custom servers even if catalog fails
81
+ self.categories = [CUSTOM_SERVER_CATEGORY]
82
+ except Exception as e:
83
+ emit_error(f"Error loading server catalog: {e}")
84
+ self.categories = [CUSTOM_SERVER_CATEGORY]
85
+
86
+ def _get_current_category(self) -> Optional[str]:
87
+ """Get the currently selected category."""
88
+ if 0 <= self.selected_category_idx < len(self.categories):
89
+ return self.categories[self.selected_category_idx]
90
+ return None
91
+
92
+ def _get_current_server(self):
93
+ """Get the currently selected server."""
94
+ if self.view_mode == "servers" and self.current_servers:
95
+ if 0 <= self.selected_server_idx < len(self.current_servers):
96
+ return self.current_servers[self.selected_server_idx]
97
+ return None
98
+
99
+ def _get_category_icon(self, category: str) -> str:
100
+ """Get an icon for a category."""
101
+ if category == CUSTOM_SERVER_CATEGORY:
102
+ return "➕"
103
+ icons = {
104
+ "Code": "💻",
105
+ "Storage": "💾",
106
+ "Database": "🗄️",
107
+ "Documentation": "📝",
108
+ "DevOps": "🔧",
109
+ "Monitoring": "📊",
110
+ "Package Management": "📦",
111
+ "Communication": "💬",
112
+ "AI": "🤖",
113
+ "Search": "🔍",
114
+ "Development": "🛠️",
115
+ "Cloud": "☁️",
116
+ }
117
+ return icons.get(category, "📁")
118
+
119
+ def _is_custom_server_selected(self) -> bool:
120
+ """Check if the custom server category is selected."""
121
+ return (
122
+ self.view_mode == "categories"
123
+ and self.selected_category_idx == 0
124
+ and len(self.categories) > 0
125
+ and self.categories[0] == CUSTOM_SERVER_CATEGORY
126
+ )
127
+
128
+ def _render_category_list(self) -> List:
129
+ """Render the category list panel."""
130
+ lines = []
131
+
132
+ lines.append(("bold cyan", " 📂 CATEGORIES"))
133
+ lines.append(("", "\n\n"))
134
+
135
+ if not self.categories:
136
+ lines.append(("fg:yellow", " No categories available."))
137
+ lines.append(("", "\n\n"))
138
+ self._render_navigation_hints(lines)
139
+ return lines
140
+
141
+ # Show categories for current page
142
+ total_pages = (len(self.categories) + PAGE_SIZE - 1) // PAGE_SIZE
143
+ start_idx = self.current_page * PAGE_SIZE
144
+ end_idx = min(start_idx + PAGE_SIZE, len(self.categories))
145
+
146
+ for i in range(start_idx, end_idx):
147
+ category = self.categories[i]
148
+ is_selected = i == self.selected_category_idx
149
+ icon = self._get_category_icon(category)
150
+
151
+ prefix = " > " if is_selected else " "
152
+
153
+ # Custom server category doesn't have a count
154
+ if category == CUSTOM_SERVER_CATEGORY:
155
+ label = f"{prefix}{icon} Custom Server (JSON)"
156
+ if is_selected:
157
+ lines.append(("fg:ansibrightgreen bold", label))
158
+ else:
159
+ lines.append(("fg:ansigreen", label))
160
+ else:
161
+ # Count servers in category
162
+ server_count = (
163
+ len(self.catalog.get_by_category(category)) if self.catalog else 0
164
+ )
165
+ label = f"{prefix}{icon} {category} ({server_count})"
166
+ if is_selected:
167
+ lines.append(("fg:ansibrightcyan bold", label))
168
+ else:
169
+ lines.append(("fg:ansibrightblack", label))
170
+
171
+ lines.append(("", "\n"))
172
+
173
+ lines.append(("", "\n"))
174
+ if total_pages > 1:
175
+ lines.append(
176
+ ("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
177
+ )
178
+ lines.append(("", "\n"))
179
+
180
+ self._render_navigation_hints(lines)
181
+ return lines
182
+
183
+ def _render_server_list(self) -> List:
184
+ """Render the server list panel."""
185
+ lines = []
186
+
187
+ if not self.current_category:
188
+ lines.append(("fg:yellow", " No category selected."))
189
+ lines.append(("", "\n\n"))
190
+ self._render_navigation_hints(lines)
191
+ return lines
192
+
193
+ icon = self._get_category_icon(self.current_category)
194
+ lines.append(("bold cyan", f" {icon} {self.current_category.upper()}"))
195
+ lines.append(("", "\n\n"))
196
+
197
+ if not self.current_servers:
198
+ lines.append(("fg:yellow", " No servers in this category."))
199
+ lines.append(("", "\n\n"))
200
+ self._render_navigation_hints(lines)
201
+ return lines
202
+
203
+ # Show servers for current page
204
+ total_pages = (len(self.current_servers) + PAGE_SIZE - 1) // PAGE_SIZE
205
+ start_idx = self.current_page * PAGE_SIZE
206
+ end_idx = min(start_idx + PAGE_SIZE, len(self.current_servers))
207
+
208
+ for i in range(start_idx, end_idx):
209
+ server = self.current_servers[i]
210
+ is_selected = i == self.selected_server_idx
211
+
212
+ # Create indicator icons
213
+ icons = []
214
+ if server.verified:
215
+ icons.append("✓")
216
+ if server.popular:
217
+ icons.append("⭐")
218
+
219
+ icon_str = " ".join(icons) + " " if icons else ""
220
+
221
+ prefix = " > " if is_selected else " "
222
+ label = f"{prefix}{icon_str}{server.display_name}"
223
+
224
+ if is_selected:
225
+ lines.append(("fg:ansibrightcyan bold", label))
226
+ else:
227
+ lines.append(("fg:ansibrightblack", label))
228
+
229
+ lines.append(("", "\n"))
230
+
231
+ lines.append(("", "\n"))
232
+ if total_pages > 1:
233
+ lines.append(
234
+ ("fg:ansibrightblack", f" Page {self.current_page + 1}/{total_pages}")
235
+ )
236
+ lines.append(("", "\n"))
237
+
238
+ self._render_navigation_hints(lines)
239
+ return lines
240
+
241
+ def _render_navigation_hints(self, lines: List):
242
+ """Render navigation hints at the bottom of the list panel."""
243
+ lines.append(("", "\n"))
244
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
245
+ lines.append(("", "Navigate "))
246
+ lines.append(("fg:ansibrightblack", "←/→ "))
247
+ lines.append(("", "Page\n"))
248
+ if self.view_mode == "categories":
249
+ lines.append(("fg:green", " Enter "))
250
+ lines.append(("", "Browse Servers\n"))
251
+ else:
252
+ lines.append(("fg:green", " Enter "))
253
+ lines.append(("", "Install Server\n"))
254
+ lines.append(("fg:ansibrightblack", " Esc/Back "))
255
+ lines.append(("", "Back\n"))
256
+ lines.append(("fg:ansired", " Ctrl+C "))
257
+ lines.append(("", "Cancel"))
258
+
259
+ def _render_details(self) -> List:
260
+ """Render the details panel."""
261
+ lines = []
262
+
263
+ lines.append(("bold cyan", " 📋 DETAILS"))
264
+ lines.append(("", "\n\n"))
265
+
266
+ if self.view_mode == "categories":
267
+ category = self._get_current_category()
268
+ if not category:
269
+ lines.append(("fg:yellow", " No category selected."))
270
+ return lines
271
+
272
+ # Special handling for custom server category
273
+ if category == CUSTOM_SERVER_CATEGORY:
274
+ return self._render_custom_server_details()
275
+
276
+ icon = self._get_category_icon(category)
277
+ lines.append(("bold", f" {icon} {category}"))
278
+ lines.append(("", "\n\n"))
279
+
280
+ # Show servers in this category
281
+ servers = self.catalog.get_by_category(category) if self.catalog else []
282
+ lines.append(("fg:ansibrightblack", f" {len(servers)} servers available"))
283
+ lines.append(("", "\n\n"))
284
+
285
+ # Show popular servers in this category
286
+ popular = [s for s in servers if s.popular]
287
+ if popular:
288
+ lines.append(("bold", " ⭐ Popular:"))
289
+ lines.append(("", "\n"))
290
+ for server in popular[:5]:
291
+ lines.append(("fg:ansibrightblack", f" • {server.display_name}"))
292
+ lines.append(("", "\n"))
293
+
294
+ else: # servers view
295
+ server = self._get_current_server()
296
+ if not server:
297
+ lines.append(("fg:yellow", " No server selected."))
298
+ return lines
299
+
300
+ # Server name with indicators
301
+ indicators = []
302
+ if server.verified:
303
+ indicators.append("✓ Verified")
304
+ if server.popular:
305
+ indicators.append("⭐ Popular")
306
+
307
+ lines.append(("bold", f" {server.display_name}"))
308
+ lines.append(("", "\n"))
309
+
310
+ if indicators:
311
+ lines.append(("fg:green", f" {' | '.join(indicators)}"))
312
+ lines.append(("", "\n"))
313
+
314
+ lines.append(("", "\n"))
315
+
316
+ # Description
317
+ lines.append(("bold", " Description:"))
318
+ lines.append(("", "\n"))
319
+ # Wrap description
320
+ desc = server.description or "No description available"
321
+ # Simple word wrap
322
+ words = desc.split()
323
+ line = " "
324
+ for word in words:
325
+ if len(line) + len(word) > 50:
326
+ lines.append(("fg:ansibrightblack", line))
327
+ lines.append(("", "\n"))
328
+ line = " " + word + " "
329
+ else:
330
+ line += word + " "
331
+ if line.strip():
332
+ lines.append(("fg:ansibrightblack", line))
333
+ lines.append(("", "\n"))
334
+
335
+ lines.append(("", "\n"))
336
+
337
+ # Type
338
+ lines.append(("bold", " Type:"))
339
+ lines.append(("", "\n"))
340
+ type_icons = {"stdio": "📟", "http": "🌐", "sse": "📡"}
341
+ type_icon = type_icons.get(server.type, "❓")
342
+ lines.append(("fg:ansibrightblack", f" {type_icon} {server.type}"))
343
+ lines.append(("", "\n\n"))
344
+
345
+ # Tags
346
+ if server.tags:
347
+ lines.append(("bold", " Tags:"))
348
+ lines.append(("", "\n"))
349
+ tag_line = " " + ", ".join(server.tags[:6])
350
+ lines.append(("fg:ansicyan", tag_line))
351
+ lines.append(("", "\n\n"))
352
+
353
+ # Requirements
354
+ requirements = server.get_requirements()
355
+
356
+ # Environment variables
357
+ env_vars = server.get_environment_vars()
358
+ if env_vars:
359
+ lines.append(("bold", " 🔑 Environment Variables:"))
360
+ lines.append(("", "\n"))
361
+ for var in env_vars:
362
+ # Check if already set
363
+ is_set = os.environ.get(var)
364
+ if is_set:
365
+ lines.append(("fg:green", f" ✓ {var}"))
366
+ else:
367
+ lines.append(("fg:yellow", f" ○ {var}"))
368
+ lines.append(("", "\n"))
369
+ lines.append(("", "\n"))
370
+
371
+ # Command line args
372
+ cmd_args = server.get_command_line_args()
373
+ if cmd_args:
374
+ lines.append(("bold", " ⚙️ Configuration:"))
375
+ lines.append(("", "\n"))
376
+ for arg in cmd_args:
377
+ name = arg.get("name", "unknown")
378
+ required = arg.get("required", True)
379
+ default = arg.get("default", "")
380
+ marker = "*" if required else "?"
381
+ default_str = f" [{default}]" if default else ""
382
+ lines.append(
383
+ ("fg:ansibrightblack", f" {marker} {name}{default_str}")
384
+ )
385
+ lines.append(("", "\n"))
386
+ lines.append(("", "\n"))
387
+
388
+ # Required tools
389
+ required_tools = requirements.required_tools
390
+ if required_tools:
391
+ lines.append(("bold", " 🛠️ Required Tools:"))
392
+ lines.append(("", "\n"))
393
+ lines.append(("fg:ansibrightblack", f" {', '.join(required_tools)}"))
394
+ lines.append(("", "\n\n"))
395
+
396
+ # Example usage
397
+ if server.example_usage:
398
+ lines.append(("bold", " 💡 Example:"))
399
+ lines.append(("", "\n"))
400
+ lines.append(("fg:ansibrightblack", f" {server.example_usage}"))
401
+ lines.append(("", "\n"))
402
+
403
+ return lines
404
+
405
+ def _render_custom_server_details(self) -> List:
406
+ """Render details for the custom server option."""
407
+ lines = []
408
+
409
+ lines.append(("bold cyan", " 📋 DETAILS"))
410
+ lines.append(("", "\n\n"))
411
+
412
+ lines.append(("bold green", " ➕ Add Custom MCP Server"))
413
+ lines.append(("", "\n\n"))
414
+
415
+ lines.append(("fg:ansibrightblack", " Add your own MCP server by providing"))
416
+ lines.append(("", "\n"))
417
+ lines.append(("fg:ansibrightblack", " a JSON configuration."))
418
+ lines.append(("", "\n\n"))
419
+
420
+ lines.append(("bold", " 📟 Supported Types:"))
421
+ lines.append(("", "\n\n"))
422
+
423
+ lines.append(("fg:ansicyan bold", " 1. stdio"))
424
+ lines.append(("", "\n"))
425
+ lines.append(("fg:ansibrightblack", " Runs a local command (npx, python,"))
426
+ lines.append(("", "\n"))
427
+ lines.append(("fg:ansibrightblack", " uvx, etc.) and communicates via"))
428
+ lines.append(("", "\n"))
429
+ lines.append(("fg:ansibrightblack", " stdin/stdout."))
430
+ lines.append(("", "\n\n"))
431
+
432
+ lines.append(("fg:ansicyan bold", " 2. http"))
433
+ lines.append(("", "\n"))
434
+ lines.append(("fg:ansibrightblack", " Connects to an HTTP endpoint that"))
435
+ lines.append(("", "\n"))
436
+ lines.append(("fg:ansibrightblack", " implements the MCP protocol."))
437
+ lines.append(("", "\n\n"))
438
+
439
+ lines.append(("fg:ansicyan bold", " 3. sse"))
440
+ lines.append(("", "\n"))
441
+ lines.append(("fg:ansibrightblack", " Connects via Server-Sent Events"))
442
+ lines.append(("", "\n"))
443
+ lines.append(("fg:ansibrightblack", " for real-time streaming."))
444
+ lines.append(("", "\n\n"))
445
+
446
+ lines.append(("bold", " 💡 Press Enter to configure"))
447
+ lines.append(("", "\n"))
448
+
449
+ return lines
450
+
451
+ def update_display(self):
452
+ """Update the display based on current state."""
453
+ if self.view_mode == "categories":
454
+ self.menu_control.text = self._render_category_list()
455
+ else:
456
+ self.menu_control.text = self._render_server_list()
457
+
458
+ self.preview_control.text = self._render_details()
459
+
460
+ def _enter_category(self):
461
+ """Enter the selected category to view its servers."""
462
+ category = self._get_current_category()
463
+ if not category:
464
+ return
465
+
466
+ # Handle custom server selection
467
+ if category == CUSTOM_SERVER_CATEGORY:
468
+ self.result = "pending_custom"
469
+ return # Signal to exit and prompt for custom config
470
+
471
+ if not self.catalog:
472
+ return
473
+
474
+ self.current_category = category
475
+ self.current_servers = self.catalog.get_by_category(category)
476
+ self.view_mode = "servers"
477
+ self.selected_server_idx = 0
478
+ self.current_page = 0
479
+ self.update_display()
480
+
481
+ def _go_back_to_categories(self):
482
+ """Go back to categories view."""
483
+ self.view_mode = "categories"
484
+ self.current_category = None
485
+ self.current_servers = []
486
+ self.selected_server_idx = 0
487
+ self.current_page = 0
488
+ self.update_display()
489
+
490
+ def _select_current_server(self):
491
+ """Select the current server for installation."""
492
+ server = self._get_current_server()
493
+ if server:
494
+ self.pending_server = server
495
+ self.result = "pending_install"
496
+
497
+ def _handle_up(self):
498
+ """Handle up arrow key."""
499
+ if self.view_mode == "categories":
500
+ if self.selected_category_idx > 0:
501
+ self.selected_category_idx -= 1
502
+ self.current_page = self.selected_category_idx // PAGE_SIZE
503
+ else:
504
+ if self.selected_server_idx > 0:
505
+ self.selected_server_idx -= 1
506
+ self.current_page = self.selected_server_idx // PAGE_SIZE
507
+ self.update_display()
508
+
509
+ def _handle_down(self):
510
+ """Handle down arrow key."""
511
+ if self.view_mode == "categories":
512
+ if self.selected_category_idx < len(self.categories) - 1:
513
+ self.selected_category_idx += 1
514
+ self.current_page = self.selected_category_idx // PAGE_SIZE
515
+ else:
516
+ if self.selected_server_idx < len(self.current_servers) - 1:
517
+ self.selected_server_idx += 1
518
+ self.current_page = self.selected_server_idx // PAGE_SIZE
519
+ self.update_display()
520
+
521
+ def _handle_left(self):
522
+ """Handle left arrow key (previous page)."""
523
+ if self.current_page > 0:
524
+ self.current_page -= 1
525
+ if self.view_mode == "categories":
526
+ self.selected_category_idx = self.current_page * PAGE_SIZE
527
+ else:
528
+ self.selected_server_idx = self.current_page * PAGE_SIZE
529
+ self.update_display()
530
+
531
+ def _handle_right(self):
532
+ """Handle right arrow key (next page)."""
533
+ if self.view_mode == "categories":
534
+ total_items = len(self.categories)
535
+ else:
536
+ total_items = len(self.current_servers)
537
+
538
+ total_pages = (total_items + PAGE_SIZE - 1) // PAGE_SIZE
539
+ if self.current_page < total_pages - 1:
540
+ self.current_page += 1
541
+ if self.view_mode == "categories":
542
+ self.selected_category_idx = self.current_page * PAGE_SIZE
543
+ else:
544
+ self.selected_server_idx = self.current_page * PAGE_SIZE
545
+ self.update_display()
546
+
547
+ def _handle_enter(self):
548
+ """Handle enter key. Returns True if app should exit."""
549
+ if self.view_mode == "categories":
550
+ self._enter_category()
551
+ if self.result == "pending_custom":
552
+ return True
553
+ elif self.view_mode == "servers":
554
+ self._select_current_server()
555
+ return True
556
+ return False
557
+
558
+ def _handle_back(self):
559
+ """Handle escape/backspace key."""
560
+ if self.view_mode == "servers":
561
+ self._go_back_to_categories()
562
+
563
+ def _reload_mcp_servers(self):
564
+ """Attempt to reload MCP servers after installation."""
565
+ try:
566
+ from code_puppy.agent import reload_mcp_servers
567
+
568
+ reload_mcp_servers()
569
+ except ImportError:
570
+ pass
571
+
572
+ def run(self) -> bool:
573
+ """Run the interactive MCP server browser (synchronous).
574
+
575
+ Returns:
576
+ True if a server was installed, False otherwise
577
+ """
578
+ if not self.categories:
579
+ emit_warning("No MCP server catalog available.")
580
+ return False
581
+
582
+ # Build UI
583
+ self.menu_control = FormattedTextControl(text="")
584
+ self.preview_control = FormattedTextControl(text="")
585
+
586
+ menu_window = Window(
587
+ content=self.menu_control, wrap_lines=True, width=Dimension(weight=35)
588
+ )
589
+ preview_window = Window(
590
+ content=self.preview_control, wrap_lines=True, width=Dimension(weight=65)
591
+ )
592
+
593
+ menu_frame = Frame(menu_window, width=Dimension(weight=35), title="Browse")
594
+ preview_frame = Frame(
595
+ preview_window, width=Dimension(weight=65), title="Details"
596
+ )
597
+
598
+ root_container = VSplit([menu_frame, preview_frame])
599
+
600
+ # Key bindings
601
+ kb = KeyBindings()
602
+
603
+ @kb.add("up")
604
+ def _(event): # pragma: no cover
605
+ self._handle_up()
606
+
607
+ @kb.add("down")
608
+ def _(event): # pragma: no cover
609
+ self._handle_down()
610
+
611
+ @kb.add("left")
612
+ def _(event): # pragma: no cover
613
+ self._handle_left()
614
+
615
+ @kb.add("right")
616
+ def _(event): # pragma: no cover
617
+ self._handle_right()
618
+
619
+ @kb.add("enter")
620
+ def _(event): # pragma: no cover
621
+ if self._handle_enter():
622
+ event.app.exit()
623
+
624
+ @kb.add("escape")
625
+ def _(event): # pragma: no cover
626
+ self._handle_back()
627
+
628
+ @kb.add("backspace")
629
+ def _(event): # pragma: no cover
630
+ self._handle_back()
631
+
632
+ @kb.add("c-c")
633
+ def _(event): # pragma: no cover
634
+ event.app.exit()
635
+
636
+ layout = Layout(root_container)
637
+ app = Application(
638
+ layout=layout,
639
+ key_bindings=kb,
640
+ full_screen=False,
641
+ mouse_support=False,
642
+ )
643
+
644
+ set_awaiting_user_input(True)
645
+
646
+ # Enter alternate screen buffer
647
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
648
+ sys.stdout.write("\033[2J\033[H") # Clear and home
649
+ sys.stdout.flush()
650
+ time.sleep(0.05)
651
+
652
+ try:
653
+ # Initial display
654
+ self.update_display()
655
+
656
+ # Clear the current buffer
657
+ sys.stdout.write("\033[2J\033[H")
658
+ sys.stdout.flush()
659
+
660
+ # Run application
661
+ app.run(in_thread=True)
662
+
663
+ finally:
664
+ # Exit alternate screen buffer
665
+ sys.stdout.write("\033[?1049l")
666
+ sys.stdout.flush()
667
+ set_awaiting_user_input(False)
668
+
669
+ # Clear exit message (unless we're about to prompt for more input)
670
+ if self.result not in ("pending_custom", "pending_install"):
671
+ emit_info("✓ Exited MCP server browser")
672
+
673
+ # Handle custom server after TUI exits
674
+ if self.result == "pending_custom":
675
+ success = run_custom_server_form(self.manager)
676
+ if success:
677
+ self._reload_mcp_servers()
678
+ return success
679
+
680
+ # Handle catalog server installation after TUI exits
681
+ if self.result == "pending_install" and self.pending_server:
682
+ config = prompt_for_server_config(self.manager, self.pending_server)
683
+ if config:
684
+ success = install_catalog_server(
685
+ self.manager, self.pending_server, config
686
+ )
687
+ if success:
688
+ self._reload_mcp_servers()
689
+ return success
690
+ return False
691
+
692
+ return False
693
+
694
+
695
+ def run_mcp_install_menu(manager) -> bool:
696
+ """Run the MCP install menu.
697
+
698
+ Args:
699
+ manager: MCP manager instance
700
+
701
+ Returns:
702
+ True if a server was installed, False otherwise
703
+ """
704
+ menu = MCPInstallMenu(manager)
705
+ return menu.run()