codepp 0.0.437__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -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 +453 -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 +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- 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 +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- 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 +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -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 +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -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 +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -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 +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -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/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -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/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
4
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
5
|
+
from prompt_toolkit.document import Document
|
|
6
|
+
|
|
7
|
+
# Configure logging
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_server_names():
|
|
12
|
+
"""Load server names from the MCP manager."""
|
|
13
|
+
try:
|
|
14
|
+
from code_puppy.mcp_.manager import MCPManager
|
|
15
|
+
|
|
16
|
+
manager = MCPManager()
|
|
17
|
+
servers = manager.list_servers()
|
|
18
|
+
return [server.name for server in servers]
|
|
19
|
+
except Exception as e:
|
|
20
|
+
logger.debug(f"Could not load server names: {e}")
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MCPCompleter(Completer):
|
|
25
|
+
"""
|
|
26
|
+
A completer that triggers on '/mcp' to show available MCP subcommands
|
|
27
|
+
and server names where appropriate.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, trigger: str = "/mcp"):
|
|
31
|
+
self.trigger = trigger
|
|
32
|
+
|
|
33
|
+
# Define all available MCP subcommands
|
|
34
|
+
# Subcommands that take server names as arguments
|
|
35
|
+
self.server_subcommands = {
|
|
36
|
+
"start": "Start a specific MCP server",
|
|
37
|
+
"stop": "Stop a specific MCP server",
|
|
38
|
+
"restart": "Restart a specific MCP server",
|
|
39
|
+
"status": "Show status of a specific MCP server",
|
|
40
|
+
"logs": "Show logs for a specific MCP server",
|
|
41
|
+
"edit": "Edit an existing MCP server config",
|
|
42
|
+
"remove": "Remove an MCP server",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Subcommands that don't take server names
|
|
46
|
+
self.general_subcommands = {
|
|
47
|
+
"list": "List all registered MCP servers",
|
|
48
|
+
"start-all": "Start all MCP servers",
|
|
49
|
+
"stop-all": "Stop all MCP servers",
|
|
50
|
+
"test": "Test MCP server connection",
|
|
51
|
+
"add": "Add a new MCP server",
|
|
52
|
+
"install": "Install MCP servers from a list",
|
|
53
|
+
"search": "Search for available MCP servers",
|
|
54
|
+
"help": "Show help for MCP commands",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# All subcommands combined for completion when no subcommand is typed yet
|
|
58
|
+
self.all_subcommands = {**self.server_subcommands, **self.general_subcommands}
|
|
59
|
+
|
|
60
|
+
# Cache server names to avoid repeated lookups
|
|
61
|
+
self._server_names_cache = None
|
|
62
|
+
self._cache_timestamp = None
|
|
63
|
+
|
|
64
|
+
def _get_server_names(self):
|
|
65
|
+
"""Get server names with caching."""
|
|
66
|
+
import time
|
|
67
|
+
|
|
68
|
+
# Cache for 30 seconds to avoid repeated manager calls
|
|
69
|
+
current_time = time.time()
|
|
70
|
+
if (
|
|
71
|
+
self._server_names_cache is None
|
|
72
|
+
or self._cache_timestamp is None
|
|
73
|
+
or current_time - self._cache_timestamp > 30
|
|
74
|
+
):
|
|
75
|
+
self._server_names_cache = load_server_names()
|
|
76
|
+
self._cache_timestamp = current_time
|
|
77
|
+
|
|
78
|
+
return self._server_names_cache or []
|
|
79
|
+
|
|
80
|
+
def get_completions(
|
|
81
|
+
self, document: Document, complete_event
|
|
82
|
+
) -> Iterable[Completion]:
|
|
83
|
+
text = document.text
|
|
84
|
+
cursor_position = document.cursor_position
|
|
85
|
+
text_before_cursor = text[:cursor_position]
|
|
86
|
+
|
|
87
|
+
# Only trigger if /mcp is at the very beginning of the line
|
|
88
|
+
stripped_text = text_before_cursor.lstrip()
|
|
89
|
+
if not stripped_text.startswith(self.trigger):
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Find where /mcp actually starts (after any leading whitespace)
|
|
93
|
+
mcp_pos = text_before_cursor.find(self.trigger)
|
|
94
|
+
mcp_end = mcp_pos + len(self.trigger)
|
|
95
|
+
|
|
96
|
+
# Require a space after /mcp before showing completions
|
|
97
|
+
if mcp_end >= len(text_before_cursor) or text_before_cursor[mcp_end] != " ":
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Extract everything after /mcp (and after the space)
|
|
101
|
+
after_mcp = text_before_cursor[mcp_end + 1 :].strip()
|
|
102
|
+
|
|
103
|
+
# If nothing after /mcp, show all available subcommands
|
|
104
|
+
if not after_mcp:
|
|
105
|
+
for subcommand, description in sorted(self.all_subcommands.items()):
|
|
106
|
+
yield Completion(
|
|
107
|
+
subcommand,
|
|
108
|
+
start_position=0,
|
|
109
|
+
display=subcommand,
|
|
110
|
+
display_meta=description,
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Parse what's been typed after /mcp
|
|
115
|
+
# Split by space but be careful with what we're currently typing
|
|
116
|
+
parts = after_mcp.split()
|
|
117
|
+
|
|
118
|
+
# Priority: Check for server name completion first when appropriate
|
|
119
|
+
# This handles cases like '/mcp start ' where the space indicates ready for server name
|
|
120
|
+
if len(parts) >= 1:
|
|
121
|
+
subcommand = parts[0].lower()
|
|
122
|
+
|
|
123
|
+
# Only complete server names for specific subcommands
|
|
124
|
+
if subcommand in self.server_subcommands:
|
|
125
|
+
# Case 1: Exactly the subcommand followed by a space (ready for server name)
|
|
126
|
+
if len(parts) == 1 and text.endswith(" "):
|
|
127
|
+
partial_server = ""
|
|
128
|
+
start_position = 0
|
|
129
|
+
|
|
130
|
+
server_names = self._get_server_names()
|
|
131
|
+
for server_name in sorted(server_names):
|
|
132
|
+
yield Completion(
|
|
133
|
+
server_name,
|
|
134
|
+
start_position=start_position,
|
|
135
|
+
display=server_name,
|
|
136
|
+
display_meta="MCP Server",
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Case 2: Subcommand + partial server name (require space after subcommand)
|
|
141
|
+
elif len(parts) == 2 and cursor_position > (
|
|
142
|
+
mcp_end + 1 + len(subcommand) + 1
|
|
143
|
+
):
|
|
144
|
+
partial_server = parts[1]
|
|
145
|
+
start_position = -(len(partial_server))
|
|
146
|
+
|
|
147
|
+
server_names = self._get_server_names()
|
|
148
|
+
for server_name in sorted(server_names):
|
|
149
|
+
if server_name.lower().startswith(partial_server.lower()):
|
|
150
|
+
yield Completion(
|
|
151
|
+
server_name,
|
|
152
|
+
start_position=start_position,
|
|
153
|
+
display=server_name,
|
|
154
|
+
display_meta="MCP Server",
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# If we only have one part and haven't returned above, show subcommand completions
|
|
159
|
+
# This includes cases like '/mcp start' where they might want 'start-all'
|
|
160
|
+
# But NOT when there's a space after the subcommand (which indicates they want arguments)
|
|
161
|
+
if len(parts) == 1 and not text.endswith(" "):
|
|
162
|
+
partial_subcommand = parts[0]
|
|
163
|
+
for subcommand, description in sorted(self.all_subcommands.items()):
|
|
164
|
+
if subcommand.startswith(partial_subcommand):
|
|
165
|
+
yield Completion(
|
|
166
|
+
subcommand,
|
|
167
|
+
start_position=-(len(partial_subcommand)),
|
|
168
|
+
display=subcommand,
|
|
169
|
+
display_meta=description,
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# For general subcommands, we don't provide argument completion
|
|
174
|
+
# They may have their own specific completions in the future
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Iterable, Optional
|
|
3
|
+
|
|
4
|
+
from prompt_toolkit import PromptSession
|
|
5
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
6
|
+
from prompt_toolkit.document import Document
|
|
7
|
+
from prompt_toolkit.history import FileHistory
|
|
8
|
+
|
|
9
|
+
from code_puppy.config import get_global_model_name
|
|
10
|
+
from code_puppy.model_factory import ModelFactory
|
|
11
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_model_names():
|
|
15
|
+
"""Load model names from the config that's fetched from the endpoint."""
|
|
16
|
+
models_config = ModelFactory.load_config()
|
|
17
|
+
return list(models_config.keys())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_active_model():
|
|
21
|
+
"""
|
|
22
|
+
Returns the active model from the config using get_model_name().
|
|
23
|
+
This ensures consistency across the codebase by always using the config value.
|
|
24
|
+
"""
|
|
25
|
+
return get_global_model_name()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def set_active_model(model_name: str):
|
|
29
|
+
"""
|
|
30
|
+
Sets the active model name by updating the config (for persistence).
|
|
31
|
+
"""
|
|
32
|
+
set_model_and_reload_agent(model_name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelNameCompleter(Completer):
|
|
36
|
+
"""
|
|
37
|
+
A completer that triggers on '/model' to show available models from models.json.
|
|
38
|
+
Only '/model' (not just '/') will trigger the dropdown.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, trigger: str = "/model"):
|
|
42
|
+
self.trigger = trigger
|
|
43
|
+
self.model_names = load_model_names()
|
|
44
|
+
|
|
45
|
+
def get_completions(
|
|
46
|
+
self, document: Document, complete_event
|
|
47
|
+
) -> Iterable[Completion]:
|
|
48
|
+
text = document.text
|
|
49
|
+
cursor_position = document.cursor_position
|
|
50
|
+
text_before_cursor = text[:cursor_position]
|
|
51
|
+
|
|
52
|
+
# Only trigger if /model is at the very beginning of the line and has a space after it
|
|
53
|
+
stripped_text = text_before_cursor.lstrip()
|
|
54
|
+
if not stripped_text.startswith(self.trigger + " "):
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Find where /model actually starts (after any leading whitespace)
|
|
58
|
+
symbol_pos = text_before_cursor.find(self.trigger)
|
|
59
|
+
text_after_trigger = text_before_cursor[
|
|
60
|
+
symbol_pos + len(self.trigger) + 1 :
|
|
61
|
+
].lstrip()
|
|
62
|
+
start_position = -(len(text_after_trigger))
|
|
63
|
+
|
|
64
|
+
# Filter model names based on what's typed after /model (case-insensitive)
|
|
65
|
+
for model_name in self.model_names:
|
|
66
|
+
if text_after_trigger and not model_name.lower().startswith(
|
|
67
|
+
text_after_trigger.lower()
|
|
68
|
+
):
|
|
69
|
+
continue # Skip models that don't match the typed text
|
|
70
|
+
|
|
71
|
+
meta = (
|
|
72
|
+
"Model (selected)"
|
|
73
|
+
if model_name.lower() == get_active_model().lower()
|
|
74
|
+
else "Model"
|
|
75
|
+
)
|
|
76
|
+
yield Completion(
|
|
77
|
+
model_name,
|
|
78
|
+
start_position=start_position,
|
|
79
|
+
display=model_name,
|
|
80
|
+
display_meta=meta,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _find_matching_model(rest: str, model_names: list[str]) -> Optional[str]:
|
|
85
|
+
"""
|
|
86
|
+
Find the best matching model for the given input.
|
|
87
|
+
|
|
88
|
+
Priority:
|
|
89
|
+
1. Exact match (case-insensitive)
|
|
90
|
+
2. Input starts with a model name (longest/most specific wins)
|
|
91
|
+
3. Model starts with input (prefix/completion match, longest wins)
|
|
92
|
+
"""
|
|
93
|
+
rest_lower = rest.lower()
|
|
94
|
+
|
|
95
|
+
# First check for exact match
|
|
96
|
+
for model in model_names:
|
|
97
|
+
if rest_lower == model.lower():
|
|
98
|
+
return model
|
|
99
|
+
|
|
100
|
+
# Sort by length (longest first) so more specific matches win
|
|
101
|
+
sorted_models = sorted(model_names, key=len, reverse=True)
|
|
102
|
+
|
|
103
|
+
# Check if input starts with a model name (e.g. "gpt-5 tell me a joke")
|
|
104
|
+
for model in sorted_models:
|
|
105
|
+
model_lower = model.lower()
|
|
106
|
+
if rest_lower.startswith(model_lower) and (
|
|
107
|
+
len(rest_lower) == len(model_lower) or rest_lower[len(model_lower)] == " "
|
|
108
|
+
):
|
|
109
|
+
return model
|
|
110
|
+
|
|
111
|
+
# Check for prefix/completion match (input is partial model name)
|
|
112
|
+
for model in sorted_models:
|
|
113
|
+
if model.lower().startswith(rest_lower):
|
|
114
|
+
return model
|
|
115
|
+
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def update_model_in_input(text: str) -> Optional[str]:
|
|
120
|
+
# If input starts with /model or /m and a model name, set model and strip it out
|
|
121
|
+
content = text.strip()
|
|
122
|
+
model_names = load_model_names()
|
|
123
|
+
|
|
124
|
+
# Check for /model command (require space after /model, case-insensitive)
|
|
125
|
+
if content.lower().startswith("/model "):
|
|
126
|
+
# Find the actual /model command (case-insensitive)
|
|
127
|
+
model_cmd = content.split(" ", 1)[0] # Get the command part
|
|
128
|
+
rest = content[len(model_cmd) :].strip() # Remove the actual command
|
|
129
|
+
|
|
130
|
+
# Find the best matching model
|
|
131
|
+
model = _find_matching_model(rest, model_names)
|
|
132
|
+
if model:
|
|
133
|
+
# Found a matching model - now extract it properly
|
|
134
|
+
set_active_model(model)
|
|
135
|
+
|
|
136
|
+
# Find the actual model name in the original text (preserving case)
|
|
137
|
+
# We need to find where the model ends in the original rest string
|
|
138
|
+
model_end_idx = len(model)
|
|
139
|
+
|
|
140
|
+
# Build the full command+model part to remove
|
|
141
|
+
cmd_and_model_pattern = model_cmd + " " + rest[:model_end_idx]
|
|
142
|
+
idx = text.find(cmd_and_model_pattern)
|
|
143
|
+
if idx != -1:
|
|
144
|
+
new_text = (
|
|
145
|
+
text[:idx] + text[idx + len(cmd_and_model_pattern) :]
|
|
146
|
+
).strip()
|
|
147
|
+
return new_text
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# Check for /m command (case-insensitive)
|
|
151
|
+
elif content.lower().startswith("/m ") and not content.lower().startswith(
|
|
152
|
+
"/model "
|
|
153
|
+
):
|
|
154
|
+
# Find the actual /m command (case-insensitive)
|
|
155
|
+
m_cmd = content.split(" ", 1)[0] # Get the command part
|
|
156
|
+
rest = content[len(m_cmd) :].strip() # Remove the actual command
|
|
157
|
+
|
|
158
|
+
# Find the best matching model
|
|
159
|
+
model = _find_matching_model(rest, model_names)
|
|
160
|
+
if model:
|
|
161
|
+
# Found a matching model - now extract it properly
|
|
162
|
+
set_active_model(model)
|
|
163
|
+
|
|
164
|
+
# Find the actual model name in the original text (preserving case)
|
|
165
|
+
# We need to find where the model ends in the original rest string
|
|
166
|
+
model_end_idx = len(model)
|
|
167
|
+
|
|
168
|
+
# Build the full command+model part to remove
|
|
169
|
+
# Handle space variations in the original text
|
|
170
|
+
cmd_and_model_pattern = m_cmd + " " + rest[:model_end_idx]
|
|
171
|
+
idx = text.find(cmd_and_model_pattern)
|
|
172
|
+
if idx != -1:
|
|
173
|
+
new_text = (
|
|
174
|
+
text[:idx] + text[idx + len(cmd_and_model_pattern) :]
|
|
175
|
+
).strip()
|
|
176
|
+
return new_text
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def get_input_with_model_completion(
|
|
183
|
+
prompt_str: str = ">>> ",
|
|
184
|
+
trigger: str = "/model",
|
|
185
|
+
history_file: Optional[str] = None,
|
|
186
|
+
) -> str:
|
|
187
|
+
history = FileHistory(os.path.expanduser(history_file)) if history_file else None
|
|
188
|
+
session = PromptSession(
|
|
189
|
+
completer=ModelNameCompleter(trigger),
|
|
190
|
+
history=history,
|
|
191
|
+
complete_while_typing=True,
|
|
192
|
+
)
|
|
193
|
+
text = await session.prompt_async(prompt_str)
|
|
194
|
+
possibly_stripped = update_model_in_input(text)
|
|
195
|
+
if possibly_stripped is not None:
|
|
196
|
+
return possibly_stripped
|
|
197
|
+
return text
|