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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.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()
|