Add cc-switch-dev-workflow skill
这个提交包含在:
@@ -0,0 +1,286 @@
|
||||
#!/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())
|
||||
在新工单中引用
屏蔽一个用户