#!/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())