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
@@ -0,0 +1,688 @@
1
+ """Interactive TUI form for adding custom MCP servers.
2
+
3
+ Provides a form-based interface for configuring custom MCP servers
4
+ with inline JSON editing and live validation.
5
+ """
6
+
7
+ import json
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.filters import Condition
15
+ from prompt_toolkit.key_binding import KeyBindings
16
+ from prompt_toolkit.layout import (
17
+ Dimension,
18
+ HSplit,
19
+ Layout,
20
+ VSplit,
21
+ Window,
22
+ )
23
+ from prompt_toolkit.layout.controls import FormattedTextControl
24
+ from prompt_toolkit.lexers import PygmentsLexer
25
+ from prompt_toolkit.widgets import Frame, TextArea
26
+ from pygments.lexers.data import JsonLexer
27
+
28
+ from code_puppy.messaging import emit_info, emit_success
29
+ from code_puppy.tools.command_runner import set_awaiting_user_input
30
+
31
+ # Example configurations for each server type
32
+ CUSTOM_SERVER_EXAMPLES = {
33
+ "stdio": """{
34
+ "type": "stdio",
35
+ "command": "npx",
36
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
37
+ "env": {
38
+ "NODE_ENV": "production"
39
+ },
40
+ "timeout": 30
41
+ }""",
42
+ "http": """{
43
+ "type": "http",
44
+ "url": "http://localhost:8080/mcp",
45
+ "headers": {
46
+ "Authorization": "Bearer $MY_API_KEY",
47
+ "Content-Type": "application/json"
48
+ },
49
+ "timeout": 30
50
+ }""",
51
+ "sse": """{
52
+ "type": "sse",
53
+ "url": "http://localhost:8080/sse",
54
+ "headers": {
55
+ "Authorization": "Bearer $MY_API_KEY"
56
+ }
57
+ }""",
58
+ }
59
+
60
+ SERVER_TYPES = ["stdio", "http", "sse"]
61
+
62
+ SERVER_TYPE_DESCRIPTIONS = {
63
+ "stdio": "Local command (npx, python, uvx) via stdin/stdout",
64
+ "http": "HTTP endpoint implementing MCP protocol",
65
+ "sse": "Server-Sent Events for real-time streaming",
66
+ }
67
+
68
+
69
+ class CustomServerForm:
70
+ """Interactive TUI form for adding/editing custom MCP servers."""
71
+
72
+ def __init__(
73
+ self,
74
+ manager,
75
+ edit_mode: bool = False,
76
+ existing_name: str = "",
77
+ existing_type: str = "stdio",
78
+ existing_config: Optional[dict] = None,
79
+ ):
80
+ """Initialize the custom server form.
81
+
82
+ Args:
83
+ manager: MCP manager instance for server installation
84
+ edit_mode: If True, we're editing an existing server
85
+ existing_name: Name of existing server (for edit mode)
86
+ existing_type: Type of existing server (for edit mode)
87
+ existing_config: Existing config dict (for edit mode)
88
+ """
89
+ self.manager = manager
90
+ self.edit_mode = edit_mode
91
+ self.original_name = existing_name # Track original name for updates
92
+
93
+ # Form state
94
+ self.server_name = existing_name
95
+ self.selected_type_idx = (
96
+ SERVER_TYPES.index(existing_type) if existing_type in SERVER_TYPES else 0
97
+ )
98
+
99
+ # For edit mode, use existing config; otherwise use example
100
+ if existing_config:
101
+ self.json_config = json.dumps(existing_config, indent=2)
102
+ else:
103
+ self.json_config = CUSTOM_SERVER_EXAMPLES["stdio"]
104
+
105
+ self.validation_error: Optional[str] = None
106
+
107
+ # Focus state: 0=name, 1=type, 2=json
108
+ self.focused_field = 0
109
+
110
+ # Status message for user feedback (e.g., "Save failed: ...")
111
+ self.status_message: Optional[str] = None
112
+ self.status_is_error: bool = False
113
+
114
+ # Result
115
+ self.result = None # "installed", "cancelled", None
116
+
117
+ # UI controls
118
+ self.name_buffer = None
119
+ self.json_area = None
120
+ self.info_control = None
121
+ self.status_control = None
122
+
123
+ def _get_current_type(self) -> str:
124
+ """Get the currently selected server type."""
125
+ return SERVER_TYPES[self.selected_type_idx]
126
+
127
+ def _render_form(self) -> List:
128
+ """Render the form panel."""
129
+ lines = []
130
+
131
+ title = " ✏️ EDIT MCP SERVER" if self.edit_mode else " ➕ ADD CUSTOM MCP SERVER"
132
+ lines.append(("bold cyan", title))
133
+ lines.append(("", "\n\n"))
134
+
135
+ # Server Name field - now in separate frame below
136
+ name_style = "fg:ansibrightcyan bold" if self.focused_field == 0 else "bold"
137
+ lines.append((name_style, " 1. Server Name:"))
138
+ lines.append(("", "\n"))
139
+ if self.focused_field == 0:
140
+ lines.append(("fg:ansibrightgreen", " ▶ Type in the box below"))
141
+ else:
142
+ name_display = self.server_name if self.server_name else "(not set)"
143
+ lines.append(("fg:ansibrightblack", f" {name_display}"))
144
+
145
+ # Show name validation hint inline
146
+ name_error = self._validate_server_name(self.server_name)
147
+ if name_error and self.server_name: # Only show if there's input
148
+ lines.append(("", "\n"))
149
+ lines.append(("fg:ansiyellow", f" ⚠ {name_error}"))
150
+ lines.append(("", "\n\n"))
151
+
152
+ # Server Type field
153
+ type_style = "fg:ansibrightcyan bold" if self.focused_field == 1 else "bold"
154
+ lines.append((type_style, " 2. Server Type:"))
155
+ lines.append(("", "\n"))
156
+
157
+ type_icons = {
158
+ "stdio": "📟",
159
+ "http": "🌐",
160
+ "sse": "📡",
161
+ }
162
+
163
+ for i, server_type in enumerate(SERVER_TYPES):
164
+ is_selected = i == self.selected_type_idx
165
+ icon = type_icons.get(server_type, "")
166
+
167
+ if self.focused_field == 1 and is_selected:
168
+ lines.append(("fg:ansibrightgreen", " ▶ "))
169
+ elif is_selected:
170
+ lines.append(("fg:ansigreen", " ✓ "))
171
+ else:
172
+ lines.append(("", " "))
173
+
174
+ if is_selected:
175
+ lines.append(("fg:ansibrightcyan bold", f"{icon} {server_type}"))
176
+ else:
177
+ lines.append(("fg:ansibrightblack", f"{icon} {server_type}"))
178
+ lines.append(("", "\n"))
179
+
180
+ lines.append(("", "\n"))
181
+
182
+ # JSON Configuration field
183
+ json_style = "fg:ansibrightcyan bold" if self.focused_field == 2 else "bold"
184
+ lines.append((json_style, " 3. JSON Configuration:"))
185
+ lines.append(("", "\n"))
186
+
187
+ if self.focused_field == 2:
188
+ lines.append(("fg:ansibrightgreen", " ▶ Editing in box below"))
189
+ else:
190
+ lines.append(("fg:ansibrightblack", " (Tab to edit)"))
191
+ lines.append(("", "\n\n"))
192
+
193
+ # Validation status
194
+ if self.validation_error:
195
+ lines.append(("fg:ansired bold", f" ❌ {self.validation_error}"))
196
+ else:
197
+ lines.append(("fg:ansigreen", " ✓ Valid JSON"))
198
+ lines.append(("", "\n\n"))
199
+
200
+ # Navigation hints
201
+ lines.append(("fg:ansibrightblack", " Tab "))
202
+ lines.append(("", "Next field "))
203
+ lines.append(("fg:ansibrightblack", "Shift+Tab "))
204
+ lines.append(("", "Prev\n"))
205
+
206
+ if self.focused_field == 1:
207
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
208
+ lines.append(("", "Change type\n"))
209
+
210
+ lines.append(("fg:green bold", " Ctrl+S "))
211
+ lines.append(("", "Save & Install\n"))
212
+ lines.append(("fg:ansired", " Ctrl+C/Esc "))
213
+ lines.append(("", "Cancel"))
214
+
215
+ # Status message bar - shows feedback for user actions
216
+ if self.status_message:
217
+ lines.append(("", "\n\n"))
218
+ lines.append(("bold", " ─" * 20))
219
+ lines.append(("", "\n"))
220
+ if self.status_is_error:
221
+ lines.append(("fg:ansired bold", f" ⚠️ {self.status_message}"))
222
+ else:
223
+ lines.append(("fg:ansigreen bold", f" ✓ {self.status_message}"))
224
+
225
+ return lines
226
+
227
+ def _render_preview(self) -> List:
228
+ """Render the preview/help panel."""
229
+ lines = []
230
+
231
+ current_type = self._get_current_type()
232
+
233
+ lines.append(("bold cyan", " 📝 HELP & PREVIEW"))
234
+ lines.append(("", "\n\n"))
235
+
236
+ # Type description
237
+ lines.append(("bold", f" {current_type.upper()} Server"))
238
+ lines.append(("", "\n"))
239
+ desc = SERVER_TYPE_DESCRIPTIONS.get(current_type, "")
240
+ lines.append(("fg:ansibrightblack", f" {desc}"))
241
+ lines.append(("", "\n\n"))
242
+
243
+ # Required fields
244
+ lines.append(("bold", " Required Fields:"))
245
+ lines.append(("", "\n"))
246
+
247
+ if current_type == "stdio":
248
+ lines.append(("fg:ansicyan", ' • "command"'))
249
+ lines.append(("fg:ansibrightblack", " - executable to run"))
250
+ lines.append(("", "\n"))
251
+ lines.append(("fg:ansibrightblack", " Optional:"))
252
+ lines.append(("", "\n"))
253
+ lines.append(("fg:ansibrightblack", ' • "args" - command arguments'))
254
+ lines.append(("", "\n"))
255
+ lines.append(("fg:ansibrightblack", ' • "env" - environment variables'))
256
+ lines.append(("", "\n"))
257
+ lines.append(("fg:ansibrightblack", ' • "timeout" - seconds'))
258
+ lines.append(("", "\n"))
259
+ else: # http or sse
260
+ lines.append(("fg:ansicyan", ' • "url"'))
261
+ lines.append(("fg:ansibrightblack", " - server endpoint"))
262
+ lines.append(("", "\n"))
263
+ lines.append(("fg:ansibrightblack", " Optional:"))
264
+ lines.append(("", "\n"))
265
+ lines.append(("fg:ansibrightblack", ' • "headers" - HTTP headers'))
266
+ lines.append(("", "\n"))
267
+ lines.append(("fg:ansibrightblack", ' • "timeout" - seconds'))
268
+ lines.append(("", "\n"))
269
+
270
+ lines.append(("", "\n"))
271
+
272
+ # Example
273
+ lines.append(("bold", " Example:"))
274
+ lines.append(("", "\n"))
275
+
276
+ example = CUSTOM_SERVER_EXAMPLES.get(current_type, "{}")
277
+ for line in example.split("\n"):
278
+ lines.append(("fg:ansibrightblack", f" {line}"))
279
+ lines.append(("", "\n"))
280
+
281
+ lines.append(("", "\n"))
282
+
283
+ # Tips
284
+ lines.append(("bold", " 💡 Tips:"))
285
+ lines.append(("", "\n"))
286
+ lines.append(("fg:ansibrightblack", " • Use $ENV_VAR for secrets"))
287
+ lines.append(("", "\n"))
288
+ lines.append(("fg:ansibrightblack", " • Ctrl+N loads example"))
289
+ lines.append(("", "\n"))
290
+
291
+ return lines
292
+
293
+ def _validate_server_name(self, name: str) -> Optional[str]:
294
+ """Validate server name format.
295
+
296
+ Args:
297
+ name: Server name to validate
298
+
299
+ Returns:
300
+ Error message if invalid, None if valid
301
+ """
302
+ if not name or not name.strip():
303
+ return "Server name is required"
304
+
305
+ name = name.strip()
306
+
307
+ # Check for valid characters (alphanumeric, hyphens, underscores)
308
+ if not name.replace("-", "").replace("_", "").isalnum():
309
+ return "Name must be alphanumeric (hyphens/underscores OK)"
310
+
311
+ # Check for reasonable length
312
+ if len(name) > 64:
313
+ return "Name too long (max 64 characters)"
314
+
315
+ return None
316
+
317
+ def _validate_json(self) -> bool:
318
+ """Validate the current JSON configuration.
319
+
320
+ Returns:
321
+ True if valid, False otherwise
322
+ """
323
+ try:
324
+ config = json.loads(self.json_config)
325
+ current_type = self._get_current_type()
326
+
327
+ if current_type == "stdio":
328
+ if "command" not in config:
329
+ self.validation_error = "Missing 'command' field"
330
+ return False
331
+ elif current_type in ("http", "sse"):
332
+ if "url" not in config:
333
+ self.validation_error = "Missing 'url' field"
334
+ return False
335
+
336
+ self.validation_error = None
337
+ return True
338
+
339
+ except json.JSONDecodeError as e:
340
+ self.validation_error = f"Invalid JSON: {e.msg}"
341
+ return False
342
+
343
+ def _install_server(self) -> bool:
344
+ """Install the custom server.
345
+
346
+ Returns:
347
+ True if successful, False otherwise
348
+ """
349
+ from code_puppy.config import MCP_SERVERS_FILE
350
+ from code_puppy.mcp_.managed_server import ServerConfig
351
+
352
+ # Validate server name first
353
+ name_error = self._validate_server_name(self.server_name)
354
+ if name_error:
355
+ self.validation_error = name_error
356
+ self.status_message = f"Save failed: {name_error}"
357
+ self.status_is_error = True
358
+ return False
359
+
360
+ if not self._validate_json():
361
+ self.status_message = f"Save failed: {self.validation_error}"
362
+ self.status_is_error = True
363
+ return False
364
+
365
+ server_name = self.server_name.strip()
366
+ server_type = self._get_current_type()
367
+ config_dict = json.loads(self.json_config)
368
+
369
+ try:
370
+ # In edit mode, find the existing server and update it
371
+ if self.edit_mode and self.original_name:
372
+ existing_config = self.manager.get_server_by_name(self.original_name)
373
+ if existing_config:
374
+ # Use the existing server's ID for the update
375
+ server_config = ServerConfig(
376
+ id=existing_config.id,
377
+ name=server_name,
378
+ type=server_type,
379
+ enabled=True,
380
+ config=config_dict,
381
+ )
382
+
383
+ # Update the server in the manager
384
+ success = self.manager.update_server(
385
+ existing_config.id, server_config
386
+ )
387
+
388
+ if not success:
389
+ self.validation_error = "Failed to update server"
390
+ self.status_message = "Save failed: Could not update server"
391
+ self.status_is_error = True
392
+ return False
393
+
394
+ server_id = existing_config.id
395
+ else:
396
+ # Original server not found, treat as new registration
397
+ server_config = ServerConfig(
398
+ id=server_name,
399
+ name=server_name,
400
+ type=server_type,
401
+ enabled=True,
402
+ config=config_dict,
403
+ )
404
+ server_id = self.manager.register_server(server_config)
405
+ else:
406
+ # New server - register it
407
+ server_config = ServerConfig(
408
+ id=server_name,
409
+ name=server_name,
410
+ type=server_type,
411
+ enabled=True,
412
+ config=config_dict,
413
+ )
414
+
415
+ # Register with manager
416
+ server_id = self.manager.register_server(server_config)
417
+
418
+ if not server_id:
419
+ self.validation_error = "Failed to register server"
420
+ self.status_message = "Save failed: Could not register server (name may already exist)"
421
+ self.status_is_error = True
422
+ return False
423
+
424
+ # Save to mcp_servers.json for persistence
425
+ if os.path.exists(MCP_SERVERS_FILE):
426
+ with open(MCP_SERVERS_FILE, "r") as f:
427
+ data = json.load(f)
428
+ servers = data.get("mcp_servers", {})
429
+ else:
430
+ servers = {}
431
+ data = {"mcp_servers": servers}
432
+
433
+ # If editing and name changed, remove the old entry
434
+ if (
435
+ self.edit_mode
436
+ and self.original_name
437
+ and self.original_name != server_name
438
+ ):
439
+ if self.original_name in servers:
440
+ del servers[self.original_name]
441
+
442
+ # Add/update server with type
443
+ save_config = config_dict.copy()
444
+ save_config["type"] = server_type
445
+ servers[server_name] = save_config
446
+
447
+ # Save back
448
+ os.makedirs(os.path.dirname(MCP_SERVERS_FILE), exist_ok=True)
449
+ with open(MCP_SERVERS_FILE, "w") as f:
450
+ json.dump(data, f, indent=2)
451
+
452
+ return True
453
+
454
+ except Exception as e:
455
+ self.validation_error = f"Error: {e}"
456
+ self.status_message = f"Save failed: {e}"
457
+ self.status_is_error = True
458
+ return False
459
+
460
+ def run(self) -> bool:
461
+ """Run the custom server form.
462
+
463
+ Returns:
464
+ True if a server was installed, False otherwise
465
+ """
466
+ # Create form info control
467
+ form_control = FormattedTextControl(text="")
468
+ preview_control = FormattedTextControl(text="")
469
+
470
+ # Create name input text area (single line)
471
+ self.name_area = TextArea(
472
+ text=self.server_name, # Pre-populate with existing name in edit mode
473
+ multiline=False,
474
+ wrap_lines=False,
475
+ focusable=True,
476
+ height=1,
477
+ )
478
+
479
+ # Create JSON text area with syntax highlighting
480
+ self.json_area = TextArea(
481
+ text=self.json_config,
482
+ multiline=True,
483
+ wrap_lines=False,
484
+ scrollbar=True,
485
+ focusable=True,
486
+ height=Dimension(min=8, max=15),
487
+ lexer=PygmentsLexer(JsonLexer),
488
+ )
489
+
490
+ # Layout with form on left, preview on right
491
+ form_window = Window(content=form_control, wrap_lines=True)
492
+ preview_window = Window(content=preview_control, wrap_lines=True)
493
+
494
+ # Right panel: help/preview (narrower - 25% width)
495
+ right_panel = Frame(
496
+ preview_window,
497
+ title="Help",
498
+ width=Dimension(weight=25),
499
+ )
500
+
501
+ # Left panel gets 75% width
502
+ root_container = VSplit(
503
+ [
504
+ HSplit(
505
+ [
506
+ Frame(
507
+ form_window,
508
+ title="➕ Custom Server",
509
+ height=Dimension(min=18, weight=35),
510
+ ),
511
+ Frame(
512
+ self.name_area,
513
+ title="Server Name",
514
+ height=3,
515
+ ),
516
+ Frame(
517
+ self.json_area,
518
+ title="JSON Config (Ctrl+N for example)",
519
+ height=Dimension(min=10, weight=55),
520
+ ),
521
+ ],
522
+ width=Dimension(weight=75),
523
+ ),
524
+ right_panel,
525
+ ]
526
+ )
527
+
528
+ # Key bindings
529
+ kb = KeyBindings()
530
+
531
+ # Track which element is focused: name_area, json_area, or form (type selector)
532
+ focus_elements = [self.name_area, None, self.json_area] # None = type selector
533
+
534
+ def update_display():
535
+ # Sync values from text areas
536
+ self.server_name = self.name_area.text
537
+ self.json_config = self.json_area.text
538
+ self._validate_json()
539
+ form_control.text = self._render_form()
540
+ preview_control.text = self._render_preview()
541
+
542
+ def focus_current():
543
+ """Focus the appropriate element based on focused_field."""
544
+ element = focus_elements[self.focused_field]
545
+ if element is not None:
546
+ app.layout.focus(element)
547
+
548
+ @kb.add("tab")
549
+ def _(event):
550
+ self.focused_field = (self.focused_field + 1) % 3
551
+ update_display()
552
+ focus_current()
553
+
554
+ @kb.add("s-tab")
555
+ def _(event):
556
+ self.focused_field = (self.focused_field - 1) % 3
557
+ update_display()
558
+ focus_current()
559
+
560
+ # Only capture Up/Down when on the type selector field
561
+ # Otherwise let the TextArea handle cursor movement
562
+ is_type_selector_focused = Condition(lambda: self.focused_field == 1)
563
+
564
+ @kb.add("up", filter=is_type_selector_focused)
565
+ def handle_up(event):
566
+ if self.selected_type_idx > 0:
567
+ self.selected_type_idx -= 1
568
+ # Update JSON example when type changes
569
+ self.json_area.text = CUSTOM_SERVER_EXAMPLES[self._get_current_type()]
570
+ update_display()
571
+
572
+ @kb.add("down", filter=is_type_selector_focused)
573
+ def handle_down(event):
574
+ if self.selected_type_idx < len(SERVER_TYPES) - 1:
575
+ self.selected_type_idx += 1
576
+ # Update JSON example when type changes
577
+ self.json_area.text = CUSTOM_SERVER_EXAMPLES[self._get_current_type()]
578
+ update_display()
579
+
580
+ @kb.add("c-n", eager=True)
581
+ def _(event):
582
+ """Load example for current type (reset to example)."""
583
+ self.json_area.text = CUSTOM_SERVER_EXAMPLES[self._get_current_type()]
584
+ update_display()
585
+
586
+ @kb.add("c-s", eager=True)
587
+ def _(event):
588
+ """Save and install."""
589
+ # Sync values before install
590
+ self.server_name = self.name_area.text
591
+ self.json_config = self.json_area.text
592
+ if self._install_server():
593
+ self.result = "installed"
594
+ event.app.exit()
595
+ else:
596
+ update_display()
597
+
598
+ @kb.add("escape", eager=True)
599
+ def _(event):
600
+ self.result = "cancelled"
601
+ event.app.exit()
602
+
603
+ @kb.add("c-c", eager=True)
604
+ def _(event):
605
+ self.result = "cancelled"
606
+ event.app.exit()
607
+
608
+ # Create application - start focused on name input
609
+ layout = Layout(root_container, focused_element=self.name_area)
610
+ app = Application(
611
+ layout=layout,
612
+ key_bindings=kb,
613
+ full_screen=False,
614
+ mouse_support=True,
615
+ )
616
+
617
+ set_awaiting_user_input(True)
618
+
619
+ # Enter alternate screen buffer
620
+ sys.stdout.write("\033[?1049h")
621
+ sys.stdout.write("\033[2J\033[H")
622
+ sys.stdout.flush()
623
+ time.sleep(0.05)
624
+
625
+ try:
626
+ # Initial display
627
+ update_display()
628
+
629
+ # Clear screen
630
+ sys.stdout.write("\033[2J\033[H")
631
+ sys.stdout.flush()
632
+
633
+ # Run application
634
+ app.run(in_thread=True)
635
+
636
+ finally:
637
+ # Exit alternate screen buffer
638
+ sys.stdout.write("\033[?1049l")
639
+ sys.stdout.flush()
640
+ set_awaiting_user_input(False)
641
+
642
+ # Clear exit message if not installing
643
+ if self.result != "installed":
644
+ emit_info("✓ Exited custom server form")
645
+
646
+ # Handle result
647
+ if self.result == "installed":
648
+ if self.edit_mode:
649
+ emit_success(
650
+ f"\n ✅ Successfully updated server '{self.server_name}'!"
651
+ )
652
+ else:
653
+ emit_success(
654
+ f"\n ✅ Successfully added custom server '{self.server_name}'!"
655
+ )
656
+ emit_info(f" Use '/mcp start {self.server_name}' to start the server.\n")
657
+ return True
658
+
659
+ return False
660
+
661
+
662
+ def run_custom_server_form(
663
+ manager,
664
+ edit_mode: bool = False,
665
+ existing_name: str = "",
666
+ existing_type: str = "stdio",
667
+ existing_config: Optional[dict] = None,
668
+ ) -> bool:
669
+ """Run the custom server form.
670
+
671
+ Args:
672
+ manager: MCP manager instance
673
+ edit_mode: If True, we're editing an existing server
674
+ existing_name: Name of existing server (for edit mode)
675
+ existing_type: Type of existing server (for edit mode)
676
+ existing_config: Existing config dict (for edit mode)
677
+
678
+ Returns:
679
+ True if a server was installed/updated, False otherwise
680
+ """
681
+ form = CustomServerForm(
682
+ manager,
683
+ edit_mode=edit_mode,
684
+ existing_name=existing_name,
685
+ existing_type=existing_type,
686
+ existing_config=existing_config,
687
+ )
688
+ return form.run()