merge: resolve conflicts with origin/main
This commit is contained in:
commit
0897e4350e
100 changed files with 11637 additions and 1337 deletions
|
|
@ -34,8 +34,11 @@ _EXTRA_ENV_KEYS = frozenset({
|
|||
"DISCORD_HOME_CHANNEL", "TELEGRAM_HOME_CHANNEL",
|
||||
"SIGNAL_ACCOUNT", "SIGNAL_HTTP_URL",
|
||||
"SIGNAL_ALLOWED_USERS", "SIGNAL_GROUP_ALLOWED_USERS",
|
||||
"DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_SECRET",
|
||||
"TERMINAL_ENV", "TERMINAL_SSH_KEY", "TERMINAL_SSH_PORT",
|
||||
"WHATSAPP_MODE", "WHATSAPP_ENABLED",
|
||||
"MATTERMOST_HOME_CHANNEL", "MATTERMOST_REPLY_MODE",
|
||||
"MATRIX_PASSWORD", "MATRIX_ENCRYPTION", "MATRIX_HOME_ROOM",
|
||||
})
|
||||
|
||||
import yaml
|
||||
|
|
@ -354,6 +357,11 @@ DEFAULT_CONFIG = {
|
|||
"tirith_path": "tirith",
|
||||
"tirith_timeout": 5,
|
||||
"tirith_fail_open": True,
|
||||
"website_blocklist": {
|
||||
"enabled": False,
|
||||
"domains": [],
|
||||
"shared_files": [],
|
||||
},
|
||||
},
|
||||
|
||||
# Config schema version - bump this when adding new required fields
|
||||
|
|
@ -371,6 +379,7 @@ ENV_VARS_BY_VERSION: Dict[int, List[str]] = {
|
|||
4: ["VOICE_TOOLS_OPENAI_KEY", "ELEVENLABS_API_KEY"],
|
||||
5: ["WHATSAPP_ENABLED", "WHATSAPP_MODE", "WHATSAPP_ALLOWED_USERS",
|
||||
"SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_ALLOWED_USERS"],
|
||||
10: ["TAVILY_API_KEY"],
|
||||
}
|
||||
|
||||
# Required environment variables with metadata for migration prompts.
|
||||
|
|
@ -542,6 +551,14 @@ OPTIONAL_ENV_VARS = {
|
|||
},
|
||||
|
||||
# ── Tool API keys ──
|
||||
"PARALLEL_API_KEY": {
|
||||
"description": "Parallel API key for AI-native web search and extract",
|
||||
"prompt": "Parallel API key",
|
||||
"url": "https://parallel.ai/",
|
||||
"tools": ["web_search", "web_extract"],
|
||||
"password": True,
|
||||
"category": "tool",
|
||||
},
|
||||
"FIRECRAWL_API_KEY": {
|
||||
"description": "Firecrawl API key for web search and scraping",
|
||||
"prompt": "Firecrawl API key",
|
||||
|
|
@ -558,6 +575,14 @@ OPTIONAL_ENV_VARS = {
|
|||
"category": "tool",
|
||||
"advanced": True,
|
||||
},
|
||||
"TAVILY_API_KEY": {
|
||||
"description": "Tavily API key for AI-native web search, extract, and crawl",
|
||||
"prompt": "Tavily API key",
|
||||
"url": "https://app.tavily.com/home",
|
||||
"tools": ["web_search", "web_extract", "web_crawl"],
|
||||
"password": True,
|
||||
"category": "tool",
|
||||
},
|
||||
"BROWSERBASE_API_KEY": {
|
||||
"description": "Browserbase API key for cloud browser (optional — local browser works without this)",
|
||||
"prompt": "Browserbase API key",
|
||||
|
|
@ -686,6 +711,55 @@ OPTIONAL_ENV_VARS = {
|
|||
"password": True,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATTERMOST_URL": {
|
||||
"description": "Mattermost server URL (e.g. https://mm.example.com)",
|
||||
"prompt": "Mattermost server URL",
|
||||
"url": "https://mattermost.com/deploy/",
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATTERMOST_TOKEN": {
|
||||
"description": "Mattermost bot token or personal access token",
|
||||
"prompt": "Mattermost bot token",
|
||||
"url": None,
|
||||
"password": True,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATTERMOST_ALLOWED_USERS": {
|
||||
"description": "Comma-separated Mattermost user IDs allowed to use the bot",
|
||||
"prompt": "Allowed Mattermost user IDs (comma-separated)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATRIX_HOMESERVER": {
|
||||
"description": "Matrix homeserver URL (e.g. https://matrix.example.org)",
|
||||
"prompt": "Matrix homeserver URL",
|
||||
"url": "https://matrix.org/ecosystem/servers/",
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATRIX_ACCESS_TOKEN": {
|
||||
"description": "Matrix access token (preferred over password login)",
|
||||
"prompt": "Matrix access token",
|
||||
"url": None,
|
||||
"password": True,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATRIX_USER_ID": {
|
||||
"description": "Matrix user ID (e.g. @hermes:example.org)",
|
||||
"prompt": "Matrix user ID (@user:server)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATRIX_ALLOWED_USERS": {
|
||||
"description": "Comma-separated Matrix user IDs allowed to use the bot (@user:server format)",
|
||||
"prompt": "Allowed Matrix user IDs (comma-separated)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"GATEWAY_ALLOW_ALL_USERS": {
|
||||
"description": "Allow all users to interact with messaging bots (true/false). Default: false.",
|
||||
"prompt": "Allow all users (true/false)",
|
||||
|
|
@ -1449,7 +1523,9 @@ def show_config():
|
|||
keys = [
|
||||
("OPENROUTER_API_KEY", "OpenRouter"),
|
||||
("VOICE_TOOLS_OPENAI_KEY", "OpenAI (STT/TTS)"),
|
||||
("PARALLEL_API_KEY", "Parallel"),
|
||||
("FIRECRAWL_API_KEY", "Firecrawl"),
|
||||
("TAVILY_API_KEY", "Tavily"),
|
||||
("BROWSERBASE_API_KEY", "Browserbase"),
|
||||
("BROWSER_USE_API_KEY", "Browser Use"),
|
||||
("FAL_KEY", "FAL"),
|
||||
|
|
@ -1598,7 +1674,8 @@ def set_config_value(key: str, value: str):
|
|||
# Check if it's an API key (goes to .env)
|
||||
api_keys = [
|
||||
'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'VOICE_TOOLS_OPENAI_KEY',
|
||||
'FIRECRAWL_API_KEY', 'FIRECRAWL_API_URL', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'BROWSER_USE_API_KEY',
|
||||
'PARALLEL_API_KEY', 'FIRECRAWL_API_KEY', 'FIRECRAWL_API_URL', 'TAVILY_API_KEY',
|
||||
'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID', 'BROWSER_USE_API_KEY',
|
||||
'FAL_KEY', 'TELEGRAM_BOT_TOKEN', 'DISCORD_BOT_TOKEN',
|
||||
'TERMINAL_SSH_HOST', 'TERMINAL_SSH_USER', 'TERMINAL_SSH_KEY',
|
||||
'SUDO_PASSWORD', 'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN',
|
||||
|
|
|
|||
|
|
@ -1001,6 +1001,64 @@ _PLATFORMS = [
|
|||
"help": "Paste your member ID from step 7 above."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "matrix",
|
||||
"label": "Matrix",
|
||||
"emoji": "🔐",
|
||||
"token_var": "MATRIX_ACCESS_TOKEN",
|
||||
"setup_instructions": [
|
||||
"1. Works with any Matrix homeserver (self-hosted Synapse/Conduit/Dendrite or matrix.org)",
|
||||
"2. Create a bot user on your homeserver, or use your own account",
|
||||
"3. Get an access token: Element → Settings → Help & About → Access Token",
|
||||
" Or via API: curl -X POST https://your-server/_matrix/client/v3/login \\",
|
||||
" -d '{\"type\":\"m.login.password\",\"user\":\"@bot:server\",\"password\":\"...\"}'",
|
||||
"4. Alternatively, provide user ID + password and Hermes will log in directly",
|
||||
"5. For E2EE: set MATRIX_ENCRYPTION=true (requires pip install 'matrix-nio[e2e]')",
|
||||
"6. To find your user ID: it's @username:your-server (shown in Element profile)",
|
||||
],
|
||||
"vars": [
|
||||
{"name": "MATRIX_HOMESERVER", "prompt": "Homeserver URL (e.g. https://matrix.example.org)", "password": False,
|
||||
"help": "Your Matrix homeserver URL. Works with any self-hosted instance."},
|
||||
{"name": "MATRIX_ACCESS_TOKEN", "prompt": "Access token (leave empty to use password login instead)", "password": True,
|
||||
"help": "Paste your access token, or leave empty and provide user ID + password below."},
|
||||
{"name": "MATRIX_USER_ID", "prompt": "User ID (@bot:server — required for password login)", "password": False,
|
||||
"help": "Full Matrix user ID, e.g. @hermes:matrix.example.org"},
|
||||
{"name": "MATRIX_ALLOWED_USERS", "prompt": "Allowed user IDs (comma-separated, e.g. @you:server)", "password": False,
|
||||
"is_allowlist": True,
|
||||
"help": "Matrix user IDs who can interact with the bot."},
|
||||
{"name": "MATRIX_HOME_ROOM", "prompt": "Home room ID (for cron/notification delivery, or empty to set later with /set-home)", "password": False,
|
||||
"help": "Room ID (e.g. !abc123:server) for delivering cron results and notifications."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "mattermost",
|
||||
"label": "Mattermost",
|
||||
"emoji": "💬",
|
||||
"token_var": "MATTERMOST_TOKEN",
|
||||
"setup_instructions": [
|
||||
"1. In Mattermost: Integrations → Bot Accounts → Add Bot Account",
|
||||
" (System Console → Integrations → Bot Accounts must be enabled)",
|
||||
"2. Give it a username (e.g. hermes) and copy the bot token",
|
||||
"3. Works with any self-hosted Mattermost instance — enter your server URL",
|
||||
"4. To find your user ID: click your avatar (top-left) → Profile",
|
||||
" Your user ID is displayed there — click it to copy.",
|
||||
" ⚠ This is NOT your username — it's a 26-character alphanumeric ID.",
|
||||
"5. To get a channel ID: click the channel name → View Info → copy the ID",
|
||||
],
|
||||
"vars": [
|
||||
{"name": "MATTERMOST_URL", "prompt": "Server URL (e.g. https://mm.example.com)", "password": False,
|
||||
"help": "Your Mattermost server URL. Works with any self-hosted instance."},
|
||||
{"name": "MATTERMOST_TOKEN", "prompt": "Bot token", "password": True,
|
||||
"help": "Paste the bot token from step 2 above."},
|
||||
{"name": "MATTERMOST_ALLOWED_USERS", "prompt": "Allowed user IDs (comma-separated)", "password": False,
|
||||
"is_allowlist": True,
|
||||
"help": "Your Mattermost user ID from step 4 above."},
|
||||
{"name": "MATTERMOST_HOME_CHANNEL", "prompt": "Home channel ID (for cron/notification delivery, or empty to set later with /set-home)", "password": False,
|
||||
"help": "Channel ID where Hermes delivers cron results and notifications."},
|
||||
{"name": "MATTERMOST_REPLY_MODE", "prompt": "Reply mode — 'off' for flat messages, 'thread' for threaded replies (default: off)", "password": False,
|
||||
"help": "off = flat channel messages, thread = replies nest under your message."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "whatsapp",
|
||||
"label": "WhatsApp",
|
||||
|
|
@ -1013,30 +1071,6 @@ _PLATFORMS = [
|
|||
"emoji": "📡",
|
||||
"token_var": "SIGNAL_HTTP_URL",
|
||||
},
|
||||
{
|
||||
"key": "sms",
|
||||
"label": "SMS (Telnyx)",
|
||||
"emoji": "📱",
|
||||
"token_var": "TELNYX_API_KEY",
|
||||
"setup_instructions": [
|
||||
"1. Create a Telnyx account at https://portal.telnyx.com/",
|
||||
"2. Buy a phone number with SMS capability",
|
||||
"3. Create an API key: API Keys → Create API Key",
|
||||
"4. Set up a Messaging Profile and assign your number to it",
|
||||
"5. Configure the webhook URL: https://your-server/webhooks/telnyx",
|
||||
],
|
||||
"vars": [
|
||||
{"name": "TELNYX_API_KEY", "prompt": "Telnyx API key", "password": True,
|
||||
"help": "Paste the API key from step 3 above."},
|
||||
{"name": "TELNYX_FROM_NUMBERS", "prompt": "From numbers (comma-separated E.164, e.g. +15551234567)", "password": False,
|
||||
"help": "The Telnyx phone number(s) Hermes will send SMS from."},
|
||||
{"name": "SMS_ALLOWED_USERS", "prompt": "Allowed phone numbers (comma-separated E.164)", "password": False,
|
||||
"is_allowlist": True,
|
||||
"help": "Only messages from these phone numbers will be processed."},
|
||||
{"name": "SMS_HOME_CHANNEL", "prompt": "Home channel phone (for cron/notification delivery, or empty)", "password": False,
|
||||
"help": "A phone number where cron job outputs are delivered."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "email",
|
||||
"label": "Email",
|
||||
|
|
@ -1063,6 +1097,51 @@ _PLATFORMS = [
|
|||
"help": "Only emails from these addresses will be processed."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "sms",
|
||||
"label": "SMS (Twilio)",
|
||||
"emoji": "📱",
|
||||
"token_var": "TWILIO_ACCOUNT_SID",
|
||||
"setup_instructions": [
|
||||
"1. Create a Twilio account at https://www.twilio.com/",
|
||||
"2. Get your Account SID and Auth Token from the Twilio Console dashboard",
|
||||
"3. Buy or configure a phone number capable of sending SMS",
|
||||
"4. Set up your webhook URL for inbound SMS:",
|
||||
" Twilio Console → Phone Numbers → Active Numbers → your number",
|
||||
" → Messaging → A MESSAGE COMES IN → Webhook → https://your-server:8080/webhooks/twilio",
|
||||
],
|
||||
"vars": [
|
||||
{"name": "TWILIO_ACCOUNT_SID", "prompt": "Twilio Account SID", "password": False,
|
||||
"help": "Found on the Twilio Console dashboard."},
|
||||
{"name": "TWILIO_AUTH_TOKEN", "prompt": "Twilio Auth Token", "password": True,
|
||||
"help": "Found on the Twilio Console dashboard (click to reveal)."},
|
||||
{"name": "TWILIO_PHONE_NUMBER", "prompt": "Twilio phone number (E.164 format, e.g. +15551234567)", "password": False,
|
||||
"help": "The Twilio phone number to send SMS from."},
|
||||
{"name": "SMS_ALLOWED_USERS", "prompt": "Allowed phone numbers (comma-separated, E.164 format)", "password": False,
|
||||
"is_allowlist": True,
|
||||
"help": "Only messages from these phone numbers will be processed."},
|
||||
{"name": "SMS_HOME_CHANNEL", "prompt": "Home channel phone number (for cron/notification delivery, or empty)", "password": False,
|
||||
"help": "Phone number to deliver cron job results and notifications to."},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "dingtalk",
|
||||
"label": "DingTalk",
|
||||
"emoji": "💬",
|
||||
"token_var": "DINGTALK_CLIENT_ID",
|
||||
"setup_instructions": [
|
||||
"1. Go to https://open-dev.dingtalk.com → Create Application",
|
||||
"2. Under 'Credentials', copy the AppKey (Client ID) and AppSecret (Client Secret)",
|
||||
"3. Enable 'Stream Mode' under the bot settings",
|
||||
"4. Add the bot to a group chat or message it directly",
|
||||
],
|
||||
"vars": [
|
||||
{"name": "DINGTALK_CLIENT_ID", "prompt": "AppKey (Client ID)", "password": False,
|
||||
"help": "The AppKey from your DingTalk application credentials."},
|
||||
{"name": "DINGTALK_CLIENT_SECRET", "prompt": "AppSecret (Client Secret)", "password": True,
|
||||
"help": "The AppSecret from your DingTalk application credentials."},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -1097,6 +1176,16 @@ def _platform_status(platform: dict) -> str:
|
|||
if any([val, pwd, imap, smtp]):
|
||||
return "partially configured"
|
||||
return "not configured"
|
||||
if platform.get("key") == "matrix":
|
||||
homeserver = get_env_value("MATRIX_HOMESERVER")
|
||||
password = get_env_value("MATRIX_PASSWORD")
|
||||
if (val or password) and homeserver:
|
||||
e2ee = get_env_value("MATRIX_ENCRYPTION")
|
||||
suffix = " + E2EE" if e2ee and e2ee.lower() in ("true", "1", "yes") else ""
|
||||
return f"configured{suffix}"
|
||||
if val or password or homeserver:
|
||||
return "partially configured"
|
||||
return "not configured"
|
||||
if val:
|
||||
return "configured"
|
||||
return "not configured"
|
||||
|
|
|
|||
|
|
@ -784,6 +784,7 @@ def cmd_model(args):
|
|||
"opencode-go": "OpenCode Go",
|
||||
"ai-gateway": "AI Gateway",
|
||||
"kilocode": "Kilo Code",
|
||||
"alibaba": "Alibaba Cloud (DashScope)",
|
||||
"custom": "Custom endpoint",
|
||||
}
|
||||
active_label = provider_labels.get(active, active)
|
||||
|
|
@ -807,6 +808,7 @@ def cmd_model(args):
|
|||
("opencode-zen", "OpenCode Zen (35+ curated models, pay-as-you-go)"),
|
||||
("opencode-go", "OpenCode Go (open models, $10/month subscription)"),
|
||||
("ai-gateway", "AI Gateway (Vercel — 200+ models, pay-per-use)"),
|
||||
("alibaba", "Alibaba Cloud / DashScope (Qwen models, Anthropic-compatible)"),
|
||||
]
|
||||
|
||||
# Add user-defined custom providers from config.yaml
|
||||
|
|
@ -875,7 +877,7 @@ def cmd_model(args):
|
|||
_model_flow_anthropic(config, current_model)
|
||||
elif selected_provider == "kimi-coding":
|
||||
_model_flow_kimi(config, current_model)
|
||||
elif selected_provider in ("zai", "minimax", "minimax-cn", "kilocode", "opencode-zen", "opencode-go", "ai-gateway"):
|
||||
elif selected_provider in ("zai", "minimax", "minimax-cn", "kilocode", "opencode-zen", "opencode-go", "ai-gateway", "alibaba"):
|
||||
_model_flow_api_key_provider(config, selected_provider, current_model)
|
||||
|
||||
|
||||
|
|
@ -1994,20 +1996,32 @@ def _update_via_zip(args):
|
|||
print(f"✗ ZIP update failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Reinstall Python dependencies
|
||||
# Reinstall Python dependencies (try .[all] first for optional extras,
|
||||
# fall back to . if extras fail — mirrors the install script behavior)
|
||||
print("→ Updating Python dependencies...")
|
||||
import subprocess
|
||||
uv_bin = shutil.which("uv")
|
||||
if uv_bin:
|
||||
subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", ".", "--quiet"],
|
||||
cwd=PROJECT_ROOT, check=True,
|
||||
env={**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
|
||||
)
|
||||
uv_env = {**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
|
||||
try:
|
||||
subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", ".[all]", "--quiet"],
|
||||
cwd=PROJECT_ROOT, check=True, env=uv_env,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
print(" ⚠ Optional extras failed, installing base dependencies...")
|
||||
subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", ".", "--quiet"],
|
||||
cwd=PROJECT_ROOT, check=True, env=uv_env,
|
||||
)
|
||||
else:
|
||||
venv_pip = PROJECT_ROOT / "venv" / ("Scripts" if sys.platform == "win32" else "bin") / "pip"
|
||||
if venv_pip.exists():
|
||||
subprocess.run([str(venv_pip), "install", "-e", ".", "--quiet"], cwd=PROJECT_ROOT, check=True)
|
||||
pip_cmd = [str(venv_pip)] if venv_pip.exists() else ["pip"]
|
||||
try:
|
||||
subprocess.run(pip_cmd + ["install", "-e", ".[all]", "--quiet"], cwd=PROJECT_ROOT, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print(" ⚠ Optional extras failed, installing base dependencies...")
|
||||
subprocess.run(pip_cmd + ["install", "-e", ".", "--quiet"], cwd=PROJECT_ROOT, check=True)
|
||||
|
||||
# Sync skills
|
||||
try:
|
||||
|
|
@ -2255,21 +2269,31 @@ def cmd_update(args):
|
|||
|
||||
_invalidate_update_cache()
|
||||
|
||||
# Reinstall Python dependencies (prefer uv for speed, fall back to pip)
|
||||
# Reinstall Python dependencies (try .[all] first for optional extras,
|
||||
# fall back to . if extras fail — mirrors the install script behavior)
|
||||
print("→ Updating Python dependencies...")
|
||||
uv_bin = shutil.which("uv")
|
||||
if uv_bin:
|
||||
subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", ".", "--quiet"],
|
||||
cwd=PROJECT_ROOT, check=True,
|
||||
env={**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
|
||||
)
|
||||
uv_env = {**os.environ, "VIRTUAL_ENV": str(PROJECT_ROOT / "venv")}
|
||||
try:
|
||||
subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", ".[all]", "--quiet"],
|
||||
cwd=PROJECT_ROOT, check=True, env=uv_env,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
print(" ⚠ Optional extras failed, installing base dependencies...")
|
||||
subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", ".", "--quiet"],
|
||||
cwd=PROJECT_ROOT, check=True, env=uv_env,
|
||||
)
|
||||
else:
|
||||
venv_pip = PROJECT_ROOT / "venv" / ("Scripts" if sys.platform == "win32" else "bin") / "pip"
|
||||
if venv_pip.exists():
|
||||
subprocess.run([str(venv_pip), "install", "-e", ".", "--quiet"], cwd=PROJECT_ROOT, check=True)
|
||||
else:
|
||||
subprocess.run(["pip", "install", "-e", ".", "--quiet"], cwd=PROJECT_ROOT, check=True)
|
||||
pip_cmd = [str(venv_pip)] if venv_pip.exists() else ["pip"]
|
||||
try:
|
||||
subprocess.run(pip_cmd + ["install", "-e", ".[all]", "--quiet"], cwd=PROJECT_ROOT, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print(" ⚠ Optional extras failed, installing base dependencies...")
|
||||
subprocess.run(pip_cmd + ["install", "-e", ".", "--quiet"], cwd=PROJECT_ROOT, check=True)
|
||||
|
||||
# Check for Node.js deps
|
||||
if (PROJECT_ROOT / "package.json").exists():
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ def provider_model_ids(provider: Optional[str]) -> list[str]:
|
|||
from hermes_cli.auth import fetch_nous_models, resolve_nous_runtime_credentials
|
||||
creds = resolve_nous_runtime_credentials()
|
||||
if creds:
|
||||
live = fetch_nous_models(creds.get("api_key", ""), creds.get("base_url", ""))
|
||||
live = fetch_nous_models(api_key=creds.get("api_key", ""), inference_base_url=creds.get("base_url", ""))
|
||||
if live:
|
||||
return live
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -444,11 +444,11 @@ def _print_setup_summary(config: dict, hermes_home):
|
|||
else:
|
||||
tool_status.append(("Mixture of Agents", False, "OPENROUTER_API_KEY"))
|
||||
|
||||
# Firecrawl (web tools)
|
||||
if get_env_value("FIRECRAWL_API_KEY") or get_env_value("FIRECRAWL_API_URL"):
|
||||
# Web tools (Parallel, Firecrawl, or Tavily)
|
||||
if get_env_value("PARALLEL_API_KEY") or get_env_value("FIRECRAWL_API_KEY") or get_env_value("FIRECRAWL_API_URL") or get_env_value("TAVILY_API_KEY"):
|
||||
tool_status.append(("Web Search & Extract", True, None))
|
||||
else:
|
||||
tool_status.append(("Web Search & Extract", False, "FIRECRAWL_API_KEY"))
|
||||
tool_status.append(("Web Search & Extract", False, "PARALLEL_API_KEY, FIRECRAWL_API_KEY, or TAVILY_API_KEY"))
|
||||
|
||||
# Browser tools (local Chromium or Browserbase cloud)
|
||||
import shutil
|
||||
|
|
@ -738,6 +738,7 @@ def setup_model_provider(config: dict):
|
|||
"Kilo Code (Kilo Gateway API)",
|
||||
"Anthropic (Claude models — API key or Claude Code subscription)",
|
||||
"AI Gateway (Vercel — 200+ models, pay-per-use)",
|
||||
"Alibaba Cloud / DashScope (Qwen models via Anthropic-compatible API)",
|
||||
"OpenCode Zen (35+ curated models, pay-as-you-go)",
|
||||
"OpenCode Go (open models, $10/month subscription)",
|
||||
]
|
||||
|
|
@ -1313,7 +1314,39 @@ def setup_model_provider(config: dict):
|
|||
_update_config_for_provider("ai-gateway", pconfig.inference_base_url, default_model="anthropic/claude-opus-4.6")
|
||||
_set_model_provider(config, "ai-gateway", pconfig.inference_base_url)
|
||||
|
||||
elif provider_idx == 11: # OpenCode Zen
|
||||
elif provider_idx == 11: # Alibaba Cloud / DashScope
|
||||
selected_provider = "alibaba"
|
||||
print()
|
||||
print_header("Alibaba Cloud / DashScope API Key")
|
||||
pconfig = PROVIDER_REGISTRY["alibaba"]
|
||||
print_info(f"Provider: {pconfig.name}")
|
||||
print_info("Get your API key at: https://modelstudio.console.alibabacloud.com/")
|
||||
print()
|
||||
|
||||
existing_key = get_env_value("DASHSCOPE_API_KEY")
|
||||
if existing_key:
|
||||
print_info(f"Current: {existing_key[:8]}... (configured)")
|
||||
if prompt_yes_no("Update API key?", False):
|
||||
new_key = prompt(" DashScope API key", password=True)
|
||||
if new_key:
|
||||
save_env_value("DASHSCOPE_API_KEY", new_key)
|
||||
print_success("DashScope API key updated")
|
||||
else:
|
||||
new_key = prompt(" DashScope API key", password=True)
|
||||
if new_key:
|
||||
save_env_value("DASHSCOPE_API_KEY", new_key)
|
||||
print_success("DashScope API key saved")
|
||||
else:
|
||||
print_warning("Skipped - agent won't work without an API key")
|
||||
|
||||
# Clear custom endpoint vars if switching
|
||||
if existing_custom:
|
||||
save_env_value("OPENAI_BASE_URL", "")
|
||||
save_env_value("OPENAI_API_KEY", "")
|
||||
_update_config_for_provider("alibaba", pconfig.inference_base_url, default_model="qwen3.5-plus")
|
||||
_set_model_provider(config, "alibaba", pconfig.inference_base_url)
|
||||
|
||||
elif provider_idx == 12: # OpenCode Zen
|
||||
selected_provider = "opencode-zen"
|
||||
print()
|
||||
print_header("OpenCode Zen API Key")
|
||||
|
|
@ -1346,7 +1379,7 @@ def setup_model_provider(config: dict):
|
|||
_set_model_provider(config, "opencode-zen", pconfig.inference_base_url)
|
||||
selected_base_url = pconfig.inference_base_url
|
||||
|
||||
elif provider_idx == 12: # OpenCode Go
|
||||
elif provider_idx == 13: # OpenCode Go
|
||||
selected_provider = "opencode-go"
|
||||
print()
|
||||
print_header("OpenCode Go API Key")
|
||||
|
|
@ -1379,7 +1412,7 @@ def setup_model_provider(config: dict):
|
|||
_set_model_provider(config, "opencode-go", pconfig.inference_base_url)
|
||||
selected_base_url = pconfig.inference_base_url
|
||||
|
||||
# else: provider_idx == 13 (Keep current) — only shown when a provider already exists
|
||||
# else: provider_idx == 14 (Keep current) — only shown when a provider already exists
|
||||
# Normalize "keep current" to an explicit provider so downstream logic
|
||||
# doesn't fall back to the generic OpenRouter/static-model path.
|
||||
if selected_provider is None:
|
||||
|
|
@ -2486,6 +2519,119 @@ def setup_gateway(config: dict):
|
|||
" Set SLACK_ALLOW_ALL_USERS=true or GATEWAY_ALLOW_ALL_USERS=true only if you intentionally want open workspace access."
|
||||
)
|
||||
|
||||
# ── Matrix ──
|
||||
existing_matrix = get_env_value("MATRIX_ACCESS_TOKEN") or get_env_value("MATRIX_PASSWORD")
|
||||
if existing_matrix:
|
||||
print_info("Matrix: already configured")
|
||||
if prompt_yes_no("Reconfigure Matrix?", False):
|
||||
existing_matrix = None
|
||||
|
||||
if not existing_matrix and prompt_yes_no("Set up Matrix?", False):
|
||||
print_info("Works with any Matrix homeserver (Synapse, Conduit, Dendrite, or matrix.org).")
|
||||
print_info(" 1. Create a bot user on your homeserver, or use your own account")
|
||||
print_info(" 2. Get an access token from Element, or provide user ID + password")
|
||||
print()
|
||||
homeserver = prompt("Homeserver URL (e.g. https://matrix.example.org)")
|
||||
if homeserver:
|
||||
save_env_value("MATRIX_HOMESERVER", homeserver.rstrip("/"))
|
||||
|
||||
print()
|
||||
print_info("Auth: provide an access token (recommended), or user ID + password.")
|
||||
token = prompt("Access token (leave empty for password login)", password=True)
|
||||
if token:
|
||||
save_env_value("MATRIX_ACCESS_TOKEN", token)
|
||||
user_id = prompt("User ID (@bot:server — optional, will be auto-detected)")
|
||||
if user_id:
|
||||
save_env_value("MATRIX_USER_ID", user_id)
|
||||
print_success("Matrix access token saved")
|
||||
else:
|
||||
user_id = prompt("User ID (@bot:server)")
|
||||
if user_id:
|
||||
save_env_value("MATRIX_USER_ID", user_id)
|
||||
password = prompt("Password", password=True)
|
||||
if password:
|
||||
save_env_value("MATRIX_PASSWORD", password)
|
||||
print_success("Matrix credentials saved")
|
||||
|
||||
if token or get_env_value("MATRIX_PASSWORD"):
|
||||
# E2EE
|
||||
print()
|
||||
if prompt_yes_no("Enable end-to-end encryption (E2EE)?", False):
|
||||
save_env_value("MATRIX_ENCRYPTION", "true")
|
||||
print_success("E2EE enabled")
|
||||
print_info(" Requires: pip install 'matrix-nio[e2e]'")
|
||||
|
||||
# Allowed users
|
||||
print()
|
||||
print_info("🔒 Security: Restrict who can use your bot")
|
||||
print_info(" Matrix user IDs look like @username:server")
|
||||
print()
|
||||
allowed_users = prompt(
|
||||
"Allowed user IDs (comma-separated, leave empty for open access)"
|
||||
)
|
||||
if allowed_users:
|
||||
save_env_value("MATRIX_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||
print_success("Matrix allowlist configured")
|
||||
else:
|
||||
print_info(
|
||||
"⚠️ No allowlist set - anyone who can message the bot can use it!"
|
||||
)
|
||||
|
||||
# Home room
|
||||
print()
|
||||
print_info("📬 Home Room: where Hermes delivers cron job results and notifications.")
|
||||
print_info(" Room IDs look like !abc123:server (shown in Element room settings)")
|
||||
print_info(" You can also set this later by typing /set-home in a Matrix room.")
|
||||
home_room = prompt("Home room ID (leave empty to set later with /set-home)")
|
||||
if home_room:
|
||||
save_env_value("MATRIX_HOME_ROOM", home_room)
|
||||
|
||||
# ── Mattermost ──
|
||||
existing_mattermost = get_env_value("MATTERMOST_TOKEN")
|
||||
if existing_mattermost:
|
||||
print_info("Mattermost: already configured")
|
||||
if prompt_yes_no("Reconfigure Mattermost?", False):
|
||||
existing_mattermost = None
|
||||
|
||||
if not existing_mattermost and prompt_yes_no("Set up Mattermost?", False):
|
||||
print_info("Works with any self-hosted Mattermost instance.")
|
||||
print_info(" 1. In Mattermost: Integrations → Bot Accounts → Add Bot Account")
|
||||
print_info(" 2. Copy the bot token")
|
||||
print()
|
||||
mm_url = prompt("Mattermost server URL (e.g. https://mm.example.com)")
|
||||
if mm_url:
|
||||
save_env_value("MATTERMOST_URL", mm_url.rstrip("/"))
|
||||
token = prompt("Bot token", password=True)
|
||||
if token:
|
||||
save_env_value("MATTERMOST_TOKEN", token)
|
||||
print_success("Mattermost token saved")
|
||||
|
||||
# Allowed users
|
||||
print()
|
||||
print_info("🔒 Security: Restrict who can use your bot")
|
||||
print_info(" To find your user ID: click your avatar → Profile")
|
||||
print_info(" or use the API: GET /api/v4/users/me")
|
||||
print()
|
||||
allowed_users = prompt(
|
||||
"Allowed user IDs (comma-separated, leave empty for open access)"
|
||||
)
|
||||
if allowed_users:
|
||||
save_env_value("MATTERMOST_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||
print_success("Mattermost allowlist configured")
|
||||
else:
|
||||
print_info(
|
||||
"⚠️ No allowlist set - anyone who can message the bot can use it!"
|
||||
)
|
||||
|
||||
# Home channel
|
||||
print()
|
||||
print_info("📬 Home Channel: where Hermes delivers cron job results and notifications.")
|
||||
print_info(" To get a channel ID: click channel name → View Info → copy the ID")
|
||||
print_info(" You can also set this later by typing /set-home in a Mattermost channel.")
|
||||
home_channel = prompt("Home channel ID (leave empty to set later with /set-home)")
|
||||
if home_channel:
|
||||
save_env_value("MATTERMOST_HOME_CHANNEL", home_channel)
|
||||
|
||||
# ── WhatsApp ──
|
||||
existing_whatsapp = get_env_value("WHATSAPP_ENABLED")
|
||||
if not existing_whatsapp and prompt_yes_no("Set up WhatsApp?", False):
|
||||
|
|
@ -2503,6 +2649,9 @@ def setup_gateway(config: dict):
|
|||
get_env_value("TELEGRAM_BOT_TOKEN")
|
||||
or get_env_value("DISCORD_BOT_TOKEN")
|
||||
or get_env_value("SLACK_BOT_TOKEN")
|
||||
or get_env_value("MATTERMOST_TOKEN")
|
||||
or get_env_value("MATRIX_ACCESS_TOKEN")
|
||||
or get_env_value("MATRIX_PASSWORD")
|
||||
or get_env_value("WHATSAPP_ENABLED")
|
||||
)
|
||||
if any_messaging:
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ def show_status(args):
|
|||
"MiniMax": "MINIMAX_API_KEY",
|
||||
"MiniMax-CN": "MINIMAX_CN_API_KEY",
|
||||
"Firecrawl": "FIRECRAWL_API_KEY",
|
||||
"Tavily": "TAVILY_API_KEY",
|
||||
"Browserbase": "BROWSERBASE_API_KEY", # Optional — local browser works without this
|
||||
"FAL": "FAL_KEY",
|
||||
"Tinker": "TINKER_API_KEY",
|
||||
|
|
@ -252,7 +253,7 @@ def show_status(args):
|
|||
"Signal": ("SIGNAL_HTTP_URL", "SIGNAL_HOME_CHANNEL"),
|
||||
"Slack": ("SLACK_BOT_TOKEN", None),
|
||||
"Email": ("EMAIL_ADDRESS", "EMAIL_HOME_ADDRESS"),
|
||||
"SMS": ("TELNYX_API_KEY", "SMS_HOME_CHANNEL"),
|
||||
"SMS": ("TWILIO_ACCOUNT_SID", "SMS_HOME_CHANNEL"),
|
||||
}
|
||||
|
||||
for name, (token_var, home_var) in platforms.items():
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ PLATFORMS = {
|
|||
"whatsapp": {"label": "📱 WhatsApp", "default_toolset": "hermes-whatsapp"},
|
||||
"signal": {"label": "📡 Signal", "default_toolset": "hermes-signal"},
|
||||
"email": {"label": "📧 Email", "default_toolset": "hermes-email"},
|
||||
"dingtalk": {"label": "💬 DingTalk", "default_toolset": "hermes-dingtalk"},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -150,19 +151,37 @@ TOOL_CATEGORIES = {
|
|||
"web": {
|
||||
"name": "Web Search & Extract",
|
||||
"setup_title": "Select Search Provider",
|
||||
"setup_note": "A free DuckDuckGo search skill is also included — skip this if you don't need Firecrawl.",
|
||||
"setup_note": "A free DuckDuckGo search skill is also included — skip this if you don't need a premium provider.",
|
||||
"icon": "🔍",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Firecrawl Cloud",
|
||||
"tag": "Recommended - hosted service",
|
||||
"tag": "Hosted service - search, extract, and crawl",
|
||||
"web_backend": "firecrawl",
|
||||
"env_vars": [
|
||||
{"key": "FIRECRAWL_API_KEY", "prompt": "Firecrawl API key", "url": "https://firecrawl.dev"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Parallel",
|
||||
"tag": "AI-native search and extract",
|
||||
"web_backend": "parallel",
|
||||
"env_vars": [
|
||||
{"key": "PARALLEL_API_KEY", "prompt": "Parallel API key", "url": "https://parallel.ai"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Tavily",
|
||||
"tag": "AI-native search, extract, and crawl",
|
||||
"web_backend": "tavily",
|
||||
"env_vars": [
|
||||
{"key": "TAVILY_API_KEY", "prompt": "Tavily API key", "url": "https://app.tavily.com/home"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Firecrawl Self-Hosted",
|
||||
"tag": "Free - run your own instance",
|
||||
"web_backend": "firecrawl",
|
||||
"env_vars": [
|
||||
{"key": "FIRECRAWL_API_URL", "prompt": "Your Firecrawl instance URL (e.g., http://localhost:3002)"},
|
||||
],
|
||||
|
|
@ -617,6 +636,9 @@ def _is_provider_active(provider: dict, config: dict) -> bool:
|
|||
if "browser_provider" in provider:
|
||||
current = config.get("browser", {}).get("cloud_provider")
|
||||
return provider["browser_provider"] == current
|
||||
if provider.get("web_backend"):
|
||||
current = config.get("web", {}).get("backend")
|
||||
return current == provider["web_backend"]
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -649,6 +671,11 @@ def _configure_provider(provider: dict, config: dict):
|
|||
else:
|
||||
config.get("browser", {}).pop("cloud_provider", None)
|
||||
|
||||
# Set web search backend in config if applicable
|
||||
if provider.get("web_backend"):
|
||||
config.setdefault("web", {})["backend"] = provider["web_backend"]
|
||||
_print_success(f" Web backend set to: {provider['web_backend']}")
|
||||
|
||||
if not env_vars:
|
||||
_print_success(f" {provider['name']} - no configuration needed!")
|
||||
return
|
||||
|
|
@ -832,6 +859,11 @@ def _reconfigure_provider(provider: dict, config: dict):
|
|||
config.get("browser", {}).pop("cloud_provider", None)
|
||||
_print_success(f" Browser set to local mode")
|
||||
|
||||
# Set web search backend in config if applicable
|
||||
if provider.get("web_backend"):
|
||||
config.setdefault("web", {})["backend"] = provider["web_backend"]
|
||||
_print_success(f" Web backend set to: {provider['web_backend']}")
|
||||
|
||||
if not env_vars:
|
||||
_print_success(f" {provider['name']} - no configuration needed!")
|
||||
return
|
||||
|
|
@ -984,12 +1016,19 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
|||
if len(platform_keys) > 1:
|
||||
platform_choices.append("Configure all platforms (global)")
|
||||
platform_choices.append("Reconfigure an existing tool's provider or API key")
|
||||
|
||||
# Show MCP option if any MCP servers are configured
|
||||
_has_mcp = bool(config.get("mcp_servers"))
|
||||
if _has_mcp:
|
||||
platform_choices.append("Configure MCP server tools")
|
||||
|
||||
platform_choices.append("Done")
|
||||
|
||||
# Index offsets for the extra options after per-platform entries
|
||||
_global_idx = len(platform_keys) if len(platform_keys) > 1 else -1
|
||||
_reconfig_idx = len(platform_keys) + (1 if len(platform_keys) > 1 else 0)
|
||||
_done_idx = _reconfig_idx + 1
|
||||
_mcp_idx = (_reconfig_idx + 1) if _has_mcp else -1
|
||||
_done_idx = _reconfig_idx + (2 if _has_mcp else 1)
|
||||
|
||||
while True:
|
||||
idx = _prompt_choice("Select an option:", platform_choices, default=0)
|
||||
|
|
@ -1004,6 +1043,12 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
|||
print()
|
||||
continue
|
||||
|
||||
# "Configure MCP tools" selected
|
||||
if idx == _mcp_idx:
|
||||
_configure_mcp_tools_interactive(config)
|
||||
print()
|
||||
continue
|
||||
|
||||
# "Configure all platforms (global)" selected
|
||||
if idx == _global_idx:
|
||||
# Use the union of all platforms' current tools as the starting state
|
||||
|
|
@ -1090,6 +1135,137 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
|
|||
print()
|
||||
|
||||
|
||||
# ─── MCP Tools Interactive Configuration ─────────────────────────────────────
|
||||
|
||||
|
||||
def _configure_mcp_tools_interactive(config: dict):
|
||||
"""Probe MCP servers for available tools and let user toggle them on/off.
|
||||
|
||||
Connects to each configured MCP server, discovers tools, then shows
|
||||
a per-server curses checklist. Writes changes back as ``tools.exclude``
|
||||
entries in config.yaml.
|
||||
"""
|
||||
from hermes_cli.curses_ui import curses_checklist
|
||||
|
||||
mcp_servers = config.get("mcp_servers") or {}
|
||||
if not mcp_servers:
|
||||
_print_info("No MCP servers configured.")
|
||||
return
|
||||
|
||||
# Count enabled servers
|
||||
enabled_names = [
|
||||
k for k, v in mcp_servers.items()
|
||||
if v.get("enabled", True) not in (False, "false", "0", "no", "off")
|
||||
]
|
||||
if not enabled_names:
|
||||
_print_info("All MCP servers are disabled.")
|
||||
return
|
||||
|
||||
print()
|
||||
print(color(" Discovering tools from MCP servers...", Colors.YELLOW))
|
||||
print(color(f" Connecting to {len(enabled_names)} server(s): {', '.join(enabled_names)}", Colors.DIM))
|
||||
|
||||
try:
|
||||
from tools.mcp_tool import probe_mcp_server_tools
|
||||
server_tools = probe_mcp_server_tools()
|
||||
except Exception as exc:
|
||||
_print_error(f"Failed to probe MCP servers: {exc}")
|
||||
return
|
||||
|
||||
if not server_tools:
|
||||
_print_warning("Could not discover tools from any MCP server.")
|
||||
_print_info("Check that server commands/URLs are correct and dependencies are installed.")
|
||||
return
|
||||
|
||||
# Report discovery results
|
||||
failed = [n for n in enabled_names if n not in server_tools]
|
||||
if failed:
|
||||
for name in failed:
|
||||
_print_warning(f" Could not connect to '{name}'")
|
||||
|
||||
total_tools = sum(len(tools) for tools in server_tools.values())
|
||||
print(color(f" Found {total_tools} tool(s) across {len(server_tools)} server(s)", Colors.GREEN))
|
||||
print()
|
||||
|
||||
any_changes = False
|
||||
|
||||
for server_name, tools in server_tools.items():
|
||||
if not tools:
|
||||
_print_info(f" {server_name}: no tools found")
|
||||
continue
|
||||
|
||||
srv_cfg = mcp_servers.get(server_name, {})
|
||||
tools_cfg = srv_cfg.get("tools") or {}
|
||||
include_list = tools_cfg.get("include") or []
|
||||
exclude_list = tools_cfg.get("exclude") or []
|
||||
|
||||
# Build checklist labels
|
||||
labels = []
|
||||
for tool_name, description in tools:
|
||||
desc_short = description[:70] + "..." if len(description) > 70 else description
|
||||
if desc_short:
|
||||
labels.append(f"{tool_name} ({desc_short})")
|
||||
else:
|
||||
labels.append(tool_name)
|
||||
|
||||
# Determine which tools are currently enabled
|
||||
pre_selected: Set[int] = set()
|
||||
tool_names = [t[0] for t in tools]
|
||||
for i, tool_name in enumerate(tool_names):
|
||||
if include_list:
|
||||
# Include mode: only included tools are selected
|
||||
if tool_name in include_list:
|
||||
pre_selected.add(i)
|
||||
elif exclude_list:
|
||||
# Exclude mode: everything except excluded
|
||||
if tool_name not in exclude_list:
|
||||
pre_selected.add(i)
|
||||
else:
|
||||
# No filter: all enabled
|
||||
pre_selected.add(i)
|
||||
|
||||
chosen = curses_checklist(
|
||||
f"MCP Server: {server_name} ({len(tools)} tools)",
|
||||
labels,
|
||||
pre_selected,
|
||||
cancel_returns=pre_selected,
|
||||
)
|
||||
|
||||
if chosen == pre_selected:
|
||||
_print_info(f" {server_name}: no changes")
|
||||
continue
|
||||
|
||||
# Compute new exclude list based on unchecked tools
|
||||
new_exclude = [tool_names[i] for i in range(len(tool_names)) if i not in chosen]
|
||||
|
||||
# Update config
|
||||
srv_cfg = mcp_servers.setdefault(server_name, {})
|
||||
tools_cfg = srv_cfg.setdefault("tools", {})
|
||||
|
||||
if new_exclude:
|
||||
tools_cfg["exclude"] = new_exclude
|
||||
# Remove include if present — we're switching to exclude mode
|
||||
tools_cfg.pop("include", None)
|
||||
else:
|
||||
# All tools enabled — clear filters
|
||||
tools_cfg.pop("exclude", None)
|
||||
tools_cfg.pop("include", None)
|
||||
|
||||
enabled_count = len(chosen)
|
||||
disabled_count = len(tools) - enabled_count
|
||||
_print_success(
|
||||
f" {server_name}: {enabled_count} enabled, {disabled_count} disabled"
|
||||
)
|
||||
any_changes = True
|
||||
|
||||
if any_changes:
|
||||
save_config(config)
|
||||
print()
|
||||
print(color(" ✓ MCP tool configuration saved", Colors.GREEN))
|
||||
else:
|
||||
print(color(" No changes to MCP tools", Colors.DIM))
|
||||
|
||||
|
||||
# ─── Non-interactive disable/enable ──────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue