287 行
11 KiB
Python
可执行文件
287 行
11 KiB
Python
可执行文件
#!/usr/bin/env python3
|
|
"""Summarize the local CC Switch runtime without exposing raw secrets."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
def safe_json_load(path: Path) -> dict[str, Any]:
|
|
if not path.exists():
|
|
return {}
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
|
|
|
|
def safe_json_parse(raw: str | None) -> dict[str, Any]:
|
|
if not raw:
|
|
return {}
|
|
try:
|
|
parsed = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
return parsed if isinstance(parsed, dict) else {}
|
|
|
|
|
|
def extract_toml_string(config_text: str, key: str) -> str | None:
|
|
match = re.search(rf'{re.escape(key)}\s*=\s*"([^"]+)"', config_text)
|
|
return match.group(1) if match else None
|
|
|
|
|
|
def extract_toml_bool(config_text: str, key: str) -> bool | None:
|
|
match = re.search(rf"{re.escape(key)}\s*=\s*(true|false)", config_text)
|
|
if not match:
|
|
return None
|
|
return match.group(1) == "true"
|
|
|
|
|
|
def extract_host(raw_url: str | None) -> str | None:
|
|
if not raw_url:
|
|
return None
|
|
parsed = urlparse(raw_url)
|
|
return parsed.netloc or raw_url
|
|
|
|
|
|
def count_regex(config_text: str, pattern: str) -> int:
|
|
return len(re.findall(pattern, config_text))
|
|
|
|
|
|
def sqlite_rows(db_path: Path, sql: str) -> list[dict[str, Any]]:
|
|
if not db_path.exists():
|
|
return []
|
|
conn = sqlite3.connect(db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
rows = conn.execute(sql).fetchall()
|
|
return [dict(row) for row in rows]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def summarize_codex_provider(row: dict[str, Any], endpoint_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
|
parsed = safe_json_parse(row.get("settings_config"))
|
|
config_text = parsed.get("config", "")
|
|
endpoint = next(
|
|
(
|
|
item["url"]
|
|
for item in endpoint_rows
|
|
if item["provider_id"] == row["id"] and item["app_type"] == row["app_type"]
|
|
),
|
|
None,
|
|
)
|
|
return {
|
|
"id": row["id"],
|
|
"name": row["name"],
|
|
"endpoint_host": extract_host(endpoint or extract_toml_string(config_text, "base_url")),
|
|
"model": extract_toml_string(config_text, "model"),
|
|
"reasoning_effort": extract_toml_string(config_text, "model_reasoning_effort"),
|
|
"model_provider": extract_toml_string(config_text, "model_provider"),
|
|
"personality": extract_toml_string(config_text, "personality"),
|
|
"multi_agent_enabled": extract_toml_bool(config_text, "multi_agent"),
|
|
"trusted_project_count": count_regex(config_text, r'\[projects\."[^"]+"\]'),
|
|
"embedded_mcp_server_count": count_regex(config_text, r"\[mcp_servers\.[^\]]+\]"),
|
|
"embedded_skill_config_count": count_regex(config_text, r"\[\[skills\.config\]\]"),
|
|
}
|
|
|
|
|
|
def summarize_claude_provider(row: dict[str, Any], endpoint_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
|
parsed = safe_json_parse(row.get("settings_config"))
|
|
env = parsed.get("env", {})
|
|
permissions = parsed.get("permissions", {})
|
|
endpoint = next(
|
|
(
|
|
item["url"]
|
|
for item in endpoint_rows
|
|
if item["provider_id"] == row["id"] and item["app_type"] == row["app_type"]
|
|
),
|
|
None,
|
|
)
|
|
allow_list = permissions.get("allow", []) if isinstance(permissions, dict) else []
|
|
return {
|
|
"id": row["id"],
|
|
"name": row["name"],
|
|
"endpoint_host": extract_host(endpoint or env.get("ANTHROPIC_BASE_URL")),
|
|
"current_model": env.get("ANTHROPIC_MODEL"),
|
|
"default_opus_model": env.get("ANTHROPIC_DEFAULT_OPUS_MODEL"),
|
|
"default_sonnet_model": env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
|
|
"default_haiku_model": env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
|
|
"reasoning_model": env.get("ANTHROPIC_REASONING_MODEL"),
|
|
"allow_count": len(allow_list),
|
|
}
|
|
|
|
|
|
def fallback_flags(rows: list[dict[str, Any]]) -> dict[str, bool]:
|
|
haystack = "\n".join(
|
|
f"{row.get('name', '')} {row.get('settings_config', '')}".lower() for row in rows
|
|
)
|
|
return {
|
|
"glm": "glm" in haystack,
|
|
"kimi": "kimi" in haystack,
|
|
"minimax": "minimax" in haystack,
|
|
}
|
|
|
|
|
|
def collect_summary(root: Path) -> dict[str, Any]:
|
|
settings_path = root / "settings.json"
|
|
db_path = root / "cc-switch.db"
|
|
skills_root = root / "skills"
|
|
|
|
app_settings = safe_json_load(settings_path)
|
|
provider_rows = sqlite_rows(
|
|
db_path,
|
|
"select id,app_type,name,settings_config,is_current,provider_type from providers order by app_type,name;",
|
|
)
|
|
endpoint_rows = sqlite_rows(
|
|
db_path,
|
|
"select provider_id,app_type,url from provider_endpoints order by app_type,provider_id,url;",
|
|
)
|
|
skill_rows = sqlite_rows(
|
|
db_path,
|
|
"select id,name,directory,repo_owner,repo_name,repo_branch,enabled_claude,enabled_codex,enabled_opencode from skills order by name;",
|
|
)
|
|
skill_repo_rows = sqlite_rows(
|
|
db_path,
|
|
"select owner,name,branch,enabled from skill_repos order by owner,name;",
|
|
)
|
|
mcp_rows = sqlite_rows(
|
|
db_path,
|
|
"select id,name,server_config,enabled_claude,enabled_codex from mcp_servers order by name;",
|
|
)
|
|
|
|
current_codex = next((row for row in provider_rows if row["app_type"] == "codex" and row["is_current"] == 1), None)
|
|
current_claude = next((row for row in provider_rows if row["app_type"] == "claude" and row["is_current"] == 1), None)
|
|
|
|
codex_summary = summarize_codex_provider(current_codex, endpoint_rows) if current_codex else None
|
|
claude_summary = summarize_claude_provider(current_claude, endpoint_rows) if current_claude else None
|
|
|
|
fallback = fallback_flags([row for row in provider_rows if row["app_type"] in {"claude", "opencode"}])
|
|
mcp_servers = []
|
|
for row in mcp_rows:
|
|
parsed = safe_json_parse(row.get("server_config"))
|
|
mcp_servers.append(
|
|
{
|
|
"id": row["id"],
|
|
"name": row["name"],
|
|
"type": parsed.get("type", "http" if parsed.get("url") else "unknown"),
|
|
"command": parsed.get("command"),
|
|
"endpoint_host": extract_host(parsed.get("url")),
|
|
"enabled_codex": bool(row.get("enabled_codex")),
|
|
"enabled_claude": bool(row.get("enabled_claude")),
|
|
}
|
|
)
|
|
|
|
local_skill_count = 0
|
|
if skills_root.exists():
|
|
local_skill_count = sum(1 for entry in skills_root.iterdir() if entry.is_dir())
|
|
|
|
return {
|
|
"root": str(root),
|
|
"exists": root.exists(),
|
|
"app_settings": {
|
|
"language": app_settings.get("language"),
|
|
"current_provider_claude": app_settings.get("currentProviderClaude"),
|
|
"current_provider_codex": app_settings.get("currentProviderCodex"),
|
|
"skill_sync_method": app_settings.get("skillSyncMethod"),
|
|
},
|
|
"current_providers": {
|
|
"codex": codex_summary,
|
|
"claude": claude_summary,
|
|
},
|
|
"skills": {
|
|
"database_count": len(skill_rows),
|
|
"local_directory_count": local_skill_count,
|
|
"codex_enabled_count": sum(bool(row["enabled_codex"]) for row in skill_rows),
|
|
"claude_enabled_count": sum(bool(row["enabled_claude"]) for row in skill_rows),
|
|
"opencode_enabled_count": sum(bool(row["enabled_opencode"]) for row in skill_rows),
|
|
"names": [row["name"] for row in skill_rows],
|
|
},
|
|
"skill_repos": [
|
|
{
|
|
"owner": row["owner"],
|
|
"name": row["name"],
|
|
"branch": row["branch"],
|
|
"enabled": bool(row["enabled"]),
|
|
}
|
|
for row in skill_repo_rows
|
|
],
|
|
"mcp_servers": mcp_servers,
|
|
"fallback_providers": fallback,
|
|
"alignment": {
|
|
"codex_primary": bool(
|
|
codex_summary
|
|
and codex_summary.get("model") == "gpt-5.4-pro"
|
|
and codex_summary.get("reasoning_effort") == "xhigh"
|
|
),
|
|
"claude_secondary": bool(
|
|
claude_summary and claude_summary.get("default_opus_model") == "claude-opus-4-6"
|
|
),
|
|
"multi_agent": bool(codex_summary and codex_summary.get("multi_agent_enabled") is True),
|
|
"fallback_pool": all(fallback.values()),
|
|
},
|
|
}
|
|
|
|
|
|
def to_markdown(summary: dict[str, Any]) -> str:
|
|
codex = summary["current_providers"]["codex"] or {}
|
|
claude = summary["current_providers"]["claude"] or {}
|
|
lines = [
|
|
"# CC Switch Runtime Summary",
|
|
"",
|
|
f"- Root: `{summary['root']}`",
|
|
f"- Language: `{summary['app_settings'].get('language')}`",
|
|
f"- Skill sync: `{summary['app_settings'].get('skill_sync_method')}`",
|
|
"",
|
|
"## Current Providers",
|
|
"",
|
|
f"- Codex provider: `{codex.get('name')}`",
|
|
f"- Codex model: `{codex.get('model')}`",
|
|
f"- Codex reasoning: `{codex.get('reasoning_effort')}`",
|
|
f"- Codex base host: `{codex.get('endpoint_host')}`",
|
|
f"- Codex multi-agent: `{codex.get('multi_agent_enabled')}`",
|
|
f"- Claude provider: `{claude.get('name')}`",
|
|
f"- Claude default opus: `{claude.get('default_opus_model')}`",
|
|
f"- Claude base host: `{claude.get('endpoint_host')}`",
|
|
"",
|
|
"## Alignment",
|
|
"",
|
|
f"- Codex primary aligned: `{summary['alignment']['codex_primary']}`",
|
|
f"- Claude secondary aligned: `{summary['alignment']['claude_secondary']}`",
|
|
f"- Multi-agent aligned: `{summary['alignment']['multi_agent']}`",
|
|
f"- Fallback pool aligned: `{summary['alignment']['fallback_pool']}`",
|
|
"",
|
|
"## Inventory",
|
|
"",
|
|
f"- Skills in DB: `{summary['skills']['database_count']}`",
|
|
f"- Skills on disk: `{summary['skills']['local_directory_count']}`",
|
|
f"- Skill repos: `{len(summary['skill_repos'])}`",
|
|
f"- MCP servers: `{len(summary['mcp_servers'])}`",
|
|
f"- Installed skills: {', '.join(summary['skills']['names']) or 'n/a'}",
|
|
]
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Inspect local CC Switch runtime without printing secrets.")
|
|
parser.add_argument("--root", default=str(Path.home() / ".cc-switch"))
|
|
parser.add_argument("--format", choices=("json", "markdown"), default="json")
|
|
args = parser.parse_args()
|
|
|
|
summary = collect_summary(Path(args.root).expanduser())
|
|
if args.format == "markdown":
|
|
print(to_markdown(summary), end="")
|
|
else:
|
|
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|