code-puppy 0.0.214__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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  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 +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -1,803 +0,0 @@
1
- """
2
- MCP Install Wizard Screen - TUI interface for installing MCP servers.
3
- """
4
-
5
- import json
6
- import os
7
-
8
- from textual import on
9
- from textual.app import ComposeResult
10
- from textual.containers import Container, Horizontal
11
- from textual.screen import ModalScreen
12
- from textual.widgets import Button, Input, ListItem, ListView, Static, TextArea
13
-
14
-
15
- class MCPInstallWizardScreen(ModalScreen):
16
- """Modal screen for installing MCP servers with full wizard support."""
17
-
18
- def __init__(self, **kwargs):
19
- super().__init__(**kwargs)
20
- self.selected_server = None
21
- self.env_vars = {}
22
- self.step = "search" # search -> configure -> install -> custom_json
23
- self.search_counter = 0 # Counter to ensure unique IDs
24
- self.custom_json_mode = False # Track if we're in custom JSON mode
25
-
26
- DEFAULT_CSS = """
27
- MCPInstallWizardScreen {
28
- align: center middle;
29
- }
30
-
31
- #wizard-container {
32
- width: 90%;
33
- max-width: 100;
34
- height: 80%;
35
- max-height: 40;
36
- background: $surface;
37
- border: solid $primary;
38
- padding: 1 2;
39
- layout: vertical;
40
- }
41
-
42
- #wizard-header {
43
- width: 100%;
44
- height: 3;
45
- text-align: center;
46
- color: $accent;
47
- margin-bottom: 1;
48
- }
49
-
50
- #search-container {
51
- width: 100%;
52
- height: auto;
53
- layout: vertical;
54
- }
55
-
56
- #search-input {
57
- width: 100%;
58
- margin-bottom: 1;
59
- border: solid $primary;
60
- }
61
-
62
- #results-list {
63
- width: 100%;
64
- height: 20;
65
- border: solid $primary;
66
- margin-bottom: 1;
67
- }
68
-
69
- #config-container {
70
- width: 100%;
71
- height: 1fr;
72
- layout: vertical;
73
- }
74
-
75
- #server-info {
76
- width: 100%;
77
- height: auto;
78
- max-height: 8;
79
- border: solid $success;
80
- padding: 1;
81
- margin-bottom: 1;
82
- background: $surface-lighten-1;
83
- }
84
-
85
- #env-vars-container {
86
- width: 100%;
87
- height: 1fr;
88
- layout: vertical;
89
- border: solid $warning;
90
- padding: 1;
91
- margin-bottom: 1;
92
- overflow-y: scroll;
93
- }
94
-
95
- #env-var-input {
96
- width: 100%;
97
- margin-bottom: 1;
98
- border: solid $primary;
99
- }
100
-
101
- #button-container {
102
- width: 100%;
103
- height: 4;
104
- layout: horizontal;
105
- align: center bottom;
106
- }
107
-
108
- #back-button, #next-button, #install-button, #cancel-button {
109
- width: auto;
110
- height: 3;
111
- margin: 0 1;
112
- min-width: 12;
113
- }
114
-
115
- .env-var-row {
116
- width: 100%;
117
- layout: horizontal;
118
- height: 3;
119
- margin-bottom: 1;
120
- }
121
-
122
- .env-var-label {
123
- width: 1fr;
124
- padding: 1 0;
125
- }
126
-
127
- .env-var-input {
128
- width: 2fr;
129
- border: solid $primary;
130
- }
131
-
132
- #custom-json-container {
133
- width: 100%;
134
- height: 1fr;
135
- layout: vertical;
136
- display: none;
137
- padding: 1;
138
- }
139
-
140
- #custom-json-header {
141
- width: 100%;
142
- height: 2;
143
- text-align: left;
144
- color: $warning;
145
- margin-bottom: 1;
146
- }
147
-
148
- #custom-name-input {
149
- width: 100%;
150
- margin-bottom: 1;
151
- border: solid $primary;
152
- }
153
-
154
- #custom-json-input {
155
- width: 100%;
156
- height: 1fr;
157
- border: solid $primary;
158
- margin-bottom: 1;
159
- background: $surface-darken-1;
160
- }
161
-
162
- #custom-json-button {
163
- width: auto;
164
- height: 3;
165
- margin: 0 1;
166
- min-width: 14;
167
- }
168
- """
169
-
170
- def compose(self) -> ComposeResult:
171
- """Create the wizard layout."""
172
- with Container(id="wizard-container"):
173
- yield Static("🔌 MCP Server Install Wizard", id="wizard-header")
174
-
175
- # Step 1: Search and select server
176
- with Container(id="search-container"):
177
- yield Input(
178
- placeholder="Search MCP servers (e.g. 'github', 'postgres')...",
179
- id="search-input",
180
- )
181
- yield ListView(id="results-list")
182
-
183
- # Step 2: Configure server (hidden initially)
184
- with Container(id="config-container"):
185
- yield Static("Server Configuration", id="config-header")
186
- yield Container(id="server-info")
187
- yield Container(id="env-vars-container")
188
-
189
- # Step 3: Custom JSON configuration (hidden initially)
190
- with Container(id="custom-json-container"):
191
- yield Static("📝 Custom JSON Configuration", id="custom-json-header")
192
- yield Input(
193
- placeholder="Server name (e.g. 'my-sqlite-db')",
194
- id="custom-name-input",
195
- )
196
- yield TextArea(id="custom-json-input")
197
-
198
- # Navigation buttons
199
- with Horizontal(id="button-container"):
200
- yield Button("Cancel", id="cancel-button", variant="default")
201
- yield Button("Back", id="back-button", variant="default")
202
- yield Button("Custom JSON", id="custom-json-button", variant="warning")
203
- yield Button("Next", id="next-button", variant="primary")
204
- yield Button("Install", id="install-button", variant="success")
205
-
206
- def on_mount(self) -> None:
207
- """Initialize the wizard."""
208
- self._show_search_step()
209
- self._load_popular_servers()
210
-
211
- # Focus the search input
212
- search_input = self.query_one("#search-input", Input)
213
- search_input.focus()
214
-
215
- def _show_search_step(self) -> None:
216
- """Show the search step."""
217
- self.step = "search"
218
- self.custom_json_mode = False
219
- self.query_one("#search-container").display = True
220
- self.query_one("#config-container").display = False
221
- self.query_one("#custom-json-container").display = False
222
-
223
- self.query_one("#back-button").display = False
224
- self.query_one("#custom-json-button").display = True
225
- self.query_one("#next-button").display = True
226
- self.query_one("#install-button").display = False
227
-
228
- def _show_config_step(self) -> None:
229
- """Show the configuration step."""
230
- self.step = "configure"
231
- self.custom_json_mode = False
232
- self.query_one("#search-container").display = False
233
- self.query_one("#config-container").display = True
234
- self.query_one("#custom-json-container").display = False
235
-
236
- self.query_one("#back-button").display = True
237
- self.query_one("#custom-json-button").display = False
238
- self.query_one("#next-button").display = False
239
- self.query_one("#install-button").display = True
240
-
241
- self._setup_server_config()
242
-
243
- def _show_custom_json_step(self) -> None:
244
- """Show the custom JSON configuration step."""
245
- self.step = "custom_json"
246
- self.custom_json_mode = True
247
- self.query_one("#search-container").display = False
248
- self.query_one("#config-container").display = False
249
- self.query_one("#custom-json-container").display = True
250
-
251
- self.query_one("#back-button").display = True
252
- self.query_one("#custom-json-button").display = False
253
- self.query_one("#next-button").display = False
254
- self.query_one("#install-button").display = True
255
-
256
- # Pre-populate with SQLite example
257
- name_input = self.query_one("#custom-name-input", Input)
258
- name_input.value = "my-sqlite-db"
259
-
260
- json_input = self.query_one("#custom-json-input", TextArea)
261
- json_input.text = """{
262
- "type": "stdio",
263
- "command": "npx",
264
- "args": ["-y", "@modelcontextprotocol/server-sqlite", "./database.db"],
265
- "timeout": 30
266
- }"""
267
-
268
- # Focus the name input
269
- name_input.focus()
270
-
271
- def _load_popular_servers(self) -> None:
272
- """Load all available servers into the list."""
273
- self.search_counter += 1
274
- counter = self.search_counter
275
-
276
- try:
277
- from code_puppy.mcp_.server_registry_catalog import catalog
278
-
279
- # Load ALL servers instead of just popular ones
280
- servers = catalog.servers
281
-
282
- results_list = self.query_one("#results-list", ListView)
283
- # Force clear by removing all children
284
- results_list.remove_children()
285
-
286
- if servers:
287
- # Sort servers to show popular and verified first
288
- sorted_servers = sorted(
289
- servers,
290
- key=lambda s: (not s.popular, not s.verified, s.display_name),
291
- )
292
-
293
- for i, server in enumerate(sorted_servers):
294
- indicators = []
295
- if server.verified:
296
- indicators.append("✓")
297
- if server.popular:
298
- indicators.append("⭐")
299
-
300
- display_name = f"{server.display_name} {''.join(indicators)}"
301
- description = (
302
- server.description[:60] + "..."
303
- if len(server.description) > 60
304
- else server.description
305
- )
306
-
307
- item_text = f"{display_name}\n[dim]{description}[/dim]"
308
- # Use counter to ensure globally unique IDs
309
- item = ListItem(Static(item_text), id=f"item-{counter}-{i}")
310
- item.server_data = server
311
- results_list.append(item)
312
- else:
313
- no_servers_item = ListItem(
314
- Static("No servers found"), id=f"no-results-{counter}"
315
- )
316
- results_list.append(no_servers_item)
317
-
318
- except ImportError:
319
- results_list = self.query_one("#results-list", ListView)
320
- results_list.remove_children()
321
- error_item = ListItem(
322
- Static("[red]Server registry not available[/red]"),
323
- id=f"error-{counter}",
324
- )
325
- results_list.append(error_item)
326
-
327
- @on(Input.Changed, "#search-input")
328
- def on_search_changed(self, event: Input.Changed) -> None:
329
- """Handle search input changes."""
330
- query = event.value.strip()
331
-
332
- if not query:
333
- self._load_popular_servers() # This now loads all servers
334
- return
335
-
336
- self.search_counter += 1
337
- counter = self.search_counter
338
-
339
- try:
340
- from code_puppy.mcp_.server_registry_catalog import catalog
341
-
342
- servers = catalog.search(query)
343
-
344
- results_list = self.query_one("#results-list", ListView)
345
- # Force clear by removing all children
346
- results_list.remove_children()
347
-
348
- if servers:
349
- for i, server in enumerate(servers[:15]): # Limit results
350
- indicators = []
351
- if server.verified:
352
- indicators.append("✓")
353
- if server.popular:
354
- indicators.append("⭐")
355
-
356
- display_name = f"{server.display_name} {''.join(indicators)}"
357
- description = (
358
- server.description[:60] + "..."
359
- if len(server.description) > 60
360
- else server.description
361
- )
362
-
363
- item_text = f"{display_name}\n[dim]{description}[/dim]"
364
- # Use counter to ensure globally unique IDs
365
- item = ListItem(Static(item_text), id=f"item-{counter}-{i}")
366
- item.server_data = server
367
- results_list.append(item)
368
- else:
369
- no_results_item = ListItem(
370
- Static(f"No servers found for '{query}'"),
371
- id=f"no-results-{counter}",
372
- )
373
- results_list.append(no_results_item)
374
-
375
- except ImportError:
376
- results_list = self.query_one("#results-list", ListView)
377
- results_list.remove_children()
378
- error_item = ListItem(
379
- Static("[red]Server registry not available[/red]"),
380
- id=f"error-{counter}",
381
- )
382
- results_list.append(error_item)
383
-
384
- @on(ListView.Selected, "#results-list")
385
- def on_server_selected(self, event: ListView.Selected) -> None:
386
- """Handle server selection."""
387
- if hasattr(event.item, "server_data"):
388
- self.selected_server = event.item.server_data
389
-
390
- @on(Button.Pressed, "#next-button")
391
- def on_next_clicked(self) -> None:
392
- """Handle next button click."""
393
- if self.step == "search":
394
- if self.selected_server:
395
- self._show_config_step()
396
- else:
397
- # Show error - no server selected
398
- pass
399
-
400
- @on(Button.Pressed, "#back-button")
401
- def on_back_clicked(self) -> None:
402
- """Handle back button click."""
403
- if self.step == "configure":
404
- self._show_search_step()
405
- elif self.step == "custom_json":
406
- self._show_search_step()
407
-
408
- @on(Button.Pressed, "#custom-json-button")
409
- def on_custom_json_clicked(self) -> None:
410
- """Handle custom JSON button click."""
411
- self._show_custom_json_step()
412
-
413
- @on(Button.Pressed, "#install-button")
414
- def on_install_clicked(self) -> None:
415
- """Handle install button click."""
416
- if self.step == "configure" and self.selected_server:
417
- self._install_server()
418
- elif self.step == "custom_json":
419
- self._install_custom_json()
420
-
421
- @on(Button.Pressed, "#cancel-button")
422
- def on_cancel_clicked(self) -> None:
423
- """Handle cancel button click."""
424
- self.dismiss({"success": False, "message": "Installation cancelled"})
425
-
426
- def _setup_server_config(self) -> None:
427
- """Setup the server configuration step."""
428
- if not self.selected_server:
429
- return
430
-
431
- # Show server info
432
- server_info = self.query_one("#server-info", Container)
433
- server_info.remove_children()
434
-
435
- info_text = f"""[bold]{self.selected_server.display_name}[/bold]
436
- {self.selected_server.description}
437
-
438
- [yellow]Category:[/yellow] {self.selected_server.category}
439
- [yellow]Type:[/yellow] {getattr(self.selected_server, "type", "stdio")}"""
440
-
441
- # Show requirements summary
442
- requirements = self.selected_server.get_requirements()
443
- req_items = []
444
- if requirements.required_tools:
445
- req_items.append(f"Tools: {', '.join(requirements.required_tools)}")
446
- if requirements.environment_vars:
447
- req_items.append(f"Env vars: {len(requirements.environment_vars)}")
448
- if requirements.command_line_args:
449
- req_items.append(f"Config args: {len(requirements.command_line_args)}")
450
-
451
- if req_items:
452
- info_text += f"\n[yellow]Requirements:[/yellow] {' | '.join(req_items)}"
453
-
454
- server_info.mount(Static(info_text))
455
-
456
- # Setup configuration requirements
457
- config_container = self.query_one("#env-vars-container", Container)
458
- config_container.remove_children()
459
- config_container.mount(Static("[bold]Server Configuration:[/bold]"))
460
-
461
- # Add server name input
462
- config_container.mount(Static("\n[bold blue]Server Name:[/bold blue]"))
463
- name_row = Horizontal(classes="env-var-row")
464
- config_container.mount(name_row)
465
- name_row.mount(Static("🏷️ Custom name:", classes="env-var-label"))
466
- name_input = Input(
467
- placeholder=f"Default: {self.selected_server.name}",
468
- value=self.selected_server.name,
469
- classes="env-var-input",
470
- id="server-name-input",
471
- )
472
- name_row.mount(name_input)
473
-
474
- try:
475
- # Check system requirements first
476
- self._setup_system_requirements(config_container)
477
-
478
- # Setup environment variables
479
- self._setup_environment_variables(config_container)
480
-
481
- # Setup command line arguments
482
- self._setup_command_line_args(config_container)
483
-
484
- # Show package dependencies info
485
- self._setup_package_dependencies(config_container)
486
-
487
- except Exception as e:
488
- config_container.mount(
489
- Static(f"[red]Error loading configuration: {e}[/red]")
490
- )
491
-
492
- def _setup_system_requirements(self, parent: Container) -> None:
493
- """Setup system requirements validation."""
494
- required_tools = self.selected_server.get_required_tools()
495
-
496
- if not required_tools:
497
- return
498
-
499
- parent.mount(Static("\n[bold cyan]System Tools:[/bold cyan]"))
500
-
501
- # Import here to avoid circular imports
502
- from code_puppy.mcp_.system_tools import detector
503
-
504
- tool_status = detector.detect_tools(required_tools)
505
-
506
- for tool_name, tool_info in tool_status.items():
507
- if tool_info.available:
508
- status_text = f"✅ {tool_name}"
509
- if tool_info.version:
510
- status_text += f" ({tool_info.version})"
511
- parent.mount(Static(status_text))
512
- else:
513
- status_text = f"❌ {tool_name} - {tool_info.error or 'Not found'}"
514
- parent.mount(Static(f"[red]{status_text}[/red]"))
515
-
516
- # Show installation suggestions
517
- suggestions = detector.get_installation_suggestions(tool_name)
518
- if suggestions:
519
- parent.mount(Static(f"[dim] Install: {suggestions[0]}[/dim]"))
520
-
521
- def _setup_environment_variables(self, parent: Container) -> None:
522
- """Setup environment variables inputs."""
523
- env_vars = self.selected_server.get_environment_vars()
524
-
525
- if not env_vars:
526
- return
527
-
528
- parent.mount(Static("\n[bold yellow]Environment Variables:[/bold yellow]"))
529
-
530
- for var in env_vars:
531
- # Check if already set
532
- import os
533
-
534
- current_value = os.environ.get(var, "")
535
-
536
- row_container = Horizontal(classes="env-var-row")
537
- parent.mount(row_container)
538
-
539
- status_indicator = "✅" if current_value else "📝"
540
- row_container.mount(
541
- Static(f"{status_indicator} {var}:", classes="env-var-label")
542
- )
543
-
544
- env_input = Input(
545
- placeholder=f"Enter {var} value..."
546
- if not current_value
547
- else "Already set",
548
- value=current_value,
549
- classes="env-var-input",
550
- id=f"env-{var}",
551
- )
552
- row_container.mount(env_input)
553
-
554
- def _setup_command_line_args(self, parent: Container) -> None:
555
- """Setup command line arguments inputs."""
556
- cmd_args = self.selected_server.get_command_line_args()
557
-
558
- if not cmd_args:
559
- return
560
-
561
- parent.mount(Static("\n[bold green]Command Line Arguments:[/bold green]"))
562
-
563
- for arg_config in cmd_args:
564
- name = arg_config.get("name", "")
565
- prompt = arg_config.get("prompt", name)
566
- default = arg_config.get("default", "")
567
- required = arg_config.get("required", True)
568
-
569
- row_container = Horizontal(classes="env-var-row")
570
- parent.mount(row_container)
571
-
572
- indicator = "⚡" if required else "🔧"
573
- label_text = f"{indicator} {prompt}:"
574
- if not required:
575
- label_text += " (optional)"
576
-
577
- row_container.mount(Static(label_text, classes="env-var-label"))
578
-
579
- arg_input = Input(
580
- placeholder=f"Default: {default}" if default else f"Enter {name}...",
581
- value=default,
582
- classes="env-var-input",
583
- id=f"arg-{name}",
584
- )
585
- row_container.mount(arg_input)
586
-
587
- def _setup_package_dependencies(self, parent: Container) -> None:
588
- """Setup package dependencies information."""
589
- packages = self.selected_server.get_package_dependencies()
590
-
591
- if not packages:
592
- return
593
-
594
- parent.mount(Static("\n[bold magenta]Package Dependencies:[/bold magenta]"))
595
-
596
- # Import here to avoid circular imports
597
- from code_puppy.mcp_.system_tools import detector
598
-
599
- package_status = detector.check_package_dependencies(packages)
600
-
601
- for package, available in package_status.items():
602
- if available:
603
- parent.mount(Static(f"✅ {package} (installed)"))
604
- else:
605
- parent.mount(
606
- Static(
607
- f"[yellow]📦 {package} (will be installed automatically)[/yellow]"
608
- )
609
- )
610
-
611
- def _install_server(self) -> None:
612
- """Install the selected server with configuration."""
613
- if not self.selected_server:
614
- return
615
-
616
- try:
617
- # Collect configuration inputs
618
- env_vars = {}
619
- cmd_args = {}
620
- server_name = self.selected_server.name # Default fallback
621
-
622
- all_inputs = self.query(Input)
623
-
624
- for input_widget in all_inputs:
625
- if input_widget.id == "server-name-input":
626
- custom_name = input_widget.value.strip()
627
- if custom_name:
628
- server_name = custom_name
629
- elif input_widget.id and input_widget.id.startswith("env-"):
630
- var_name = input_widget.id[4:] # Remove "env-" prefix
631
- value = input_widget.value.strip()
632
- if value:
633
- env_vars[var_name] = value
634
- elif input_widget.id and input_widget.id.startswith("arg-"):
635
- arg_name = input_widget.id[4:] # Remove "arg-" prefix
636
- value = input_widget.value.strip()
637
- if value:
638
- cmd_args[arg_name] = value
639
-
640
- # Set environment variables in the current environment
641
- for var, value in env_vars.items():
642
- os.environ[var] = value
643
-
644
- # Get server config with command line argument overrides
645
- config_dict = self.selected_server.to_server_config(server_name, **cmd_args)
646
-
647
- # Update the config with actual environment variable values
648
- if "env" in config_dict:
649
- for env_key, env_value in config_dict["env"].items():
650
- # If it's a placeholder like $GITHUB_TOKEN, replace with actual value
651
- if env_value.startswith("$"):
652
- var_name = env_value[1:] # Remove the $
653
- if var_name in env_vars:
654
- config_dict["env"][env_key] = env_vars[var_name]
655
-
656
- # Create and register the server
657
- from code_puppy.mcp_ import ServerConfig
658
- from code_puppy.mcp_.manager import get_mcp_manager
659
-
660
- server_config = ServerConfig(
661
- id=server_name,
662
- name=server_name,
663
- type=config_dict.pop("type"),
664
- enabled=True,
665
- config=config_dict,
666
- )
667
-
668
- manager = get_mcp_manager()
669
- server_id = manager.register_server(server_config)
670
-
671
- if server_id:
672
- # Save to mcp_servers.json
673
- from code_puppy.config import MCP_SERVERS_FILE
674
-
675
- if os.path.exists(MCP_SERVERS_FILE):
676
- with open(MCP_SERVERS_FILE, "r") as f:
677
- data = json.load(f)
678
- servers = data.get("mcp_servers", {})
679
- else:
680
- servers = {}
681
- data = {"mcp_servers": servers}
682
-
683
- servers[server_name] = config_dict
684
- servers[server_name]["type"] = server_config.type
685
-
686
- os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
687
- with open(MCP_SERVERS_FILE, "w") as f:
688
- json.dump(data, f, indent=2)
689
-
690
- # Reload MCP servers
691
- from code_puppy.agent import reload_mcp_servers
692
-
693
- reload_mcp_servers()
694
-
695
- self.dismiss(
696
- {
697
- "success": True,
698
- "message": f"Successfully installed '{server_name}' from {self.selected_server.display_name}",
699
- "server_name": server_name,
700
- }
701
- )
702
- else:
703
- self.dismiss({"success": False, "message": "Failed to register server"})
704
-
705
- except Exception as e:
706
- self.dismiss(
707
- {"success": False, "message": f"Installation failed: {str(e)}"}
708
- )
709
-
710
- def _install_custom_json(self) -> None:
711
- """Install server from custom JSON configuration."""
712
- try:
713
- name_input = self.query_one("#custom-name-input", Input)
714
- json_input = self.query_one("#custom-json-input", TextArea)
715
-
716
- server_name = name_input.value.strip()
717
- json_text = json_input.text.strip()
718
-
719
- if not server_name:
720
- # Show error - need a name
721
- return
722
-
723
- if not json_text:
724
- # Show error - need JSON config
725
- return
726
-
727
- # Parse JSON
728
- try:
729
- config_dict = json.loads(json_text)
730
- except json.JSONDecodeError:
731
- # Show error - invalid JSON
732
- return
733
-
734
- # Validate required fields
735
- if "type" not in config_dict:
736
- # Show error - missing type
737
- return
738
-
739
- # Extract type and create server config
740
- server_type = config_dict.pop("type")
741
-
742
- # Create and register the server
743
- from code_puppy.mcp_ import ServerConfig
744
- from code_puppy.mcp_.manager import get_mcp_manager
745
-
746
- server_config = ServerConfig(
747
- id=server_name,
748
- name=server_name,
749
- type=server_type,
750
- enabled=True,
751
- config=config_dict,
752
- )
753
-
754
- manager = get_mcp_manager()
755
- server_id = manager.register_server(server_config)
756
-
757
- if server_id:
758
- # Save to mcp_servers.json
759
- from code_puppy.config import MCP_SERVERS_FILE
760
-
761
- if os.path.exists(MCP_SERVERS_FILE):
762
- with open(MCP_SERVERS_FILE, "r") as f:
763
- data = json.load(f)
764
- servers = data.get("mcp_servers", {})
765
- else:
766
- servers = {}
767
- data = {"mcp_servers": servers}
768
-
769
- # Add the full config including type
770
- full_config = config_dict.copy()
771
- full_config["type"] = server_type
772
- servers[server_name] = full_config
773
-
774
- os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
775
- with open(MCP_SERVERS_FILE, "w") as f:
776
- json.dump(data, f, indent=2)
777
-
778
- # Reload MCP servers
779
- from code_puppy.agent import reload_mcp_servers
780
-
781
- reload_mcp_servers()
782
-
783
- self.dismiss(
784
- {
785
- "success": True,
786
- "message": f"Successfully installed custom server '{server_name}'",
787
- "server_name": server_name,
788
- }
789
- )
790
- else:
791
- self.dismiss(
792
- {"success": False, "message": "Failed to register custom server"}
793
- )
794
-
795
- except Exception as e:
796
- self.dismiss(
797
- {"success": False, "message": f"Installation failed: {str(e)}"}
798
- )
799
-
800
- def on_key(self, event) -> None:
801
- """Handle key events."""
802
- if event.key == "escape":
803
- self.on_cancel_clicked()