Skill: mkuiwu/ai-config-admin (version 1.1.0)

--- name: ai-config-admin description: >- Manage AI configuration for OpenClaw, OpenCode, Codex CLI, and Claude Code. Use when the user wants to add/remove models or providers, switch default or agent models, replace or update OpenClaw/OpenCode/Codex/Claude config files, or convert natural-language / irregular JSON / TOML input into the exact config changes supported by the bundled scripts. Triggers include: openclaw.json, opencode.json, config.toml, auth.json, settings.json, Claude Code, Codex CLI, provider, model, default model, agent model, OPENAI_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL. --- # AI Config Admin Identify which target the user wants to modify first: OpenClaw, OpenCode, Codex CLI, or Claude Code. Then use the bundled script for that target. ## Required rules - Follow this skill and the existing script capabilities exactly. - Do not manually edit config files to bypass script limitations. - If the request is outside current script support, say so clearly and explain what is missing. - Before every write, create a backup in the same directory as the original file. - Perform backup and write through the bundled script, not manual editing. - Do not echo secrets such as `apiKey`, `OPENAI_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, tokens, or refresh tokens in replies. ## Target files - OpenClaw: `~/.openclaw/openclaw.json` - OpenCode: `~/.config/opencode/opencode.json` - Codex CLI: `~/.codex/config.toml`, `~/.codex/auth.json` - Claude Code: `~/.claude/settings.json` ## Routing - OpenClaw requests: `openclaw.json`, default model, agent model, add/remove provider, add/remove model, memory search. - OpenCode requests: `opencode.json`, OpenCode config, replace full JSON. - Codex CLI requests: `config.toml`, `auth.json`, Codex CLI, `model_provider`, `review_model`, `wire_api`, `requires_openai_auth`. - Claude Code requests: `settings.json`, Claude Code, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_MODEL`, [REDACTED_SECRET], `CLAUDE_CODE_ATTRIBUTION_HEADER`. ## Input handling - Accept natural language, semi-structured JSON, or mixed descriptions. - Convert user intent into explicit script parameters or complete JSON/TOML text before invoking scripts. - Ask follow-up questions only when critical parameters cannot be inferred. ### OpenClaw - Infer supported arguments from user intent. - When fields are missing, prefer filling from the current config when safe. - Use `add-model` to add a model to an existing provider, or to create/update a provider when the user also supplies the required provider fields. - For a brand-new provider, `add-model` needs enough provider config to make it valid: at minimum `--base-url` and `--api`; use `--api-key` and `--auth-header` when appropriate. - Use `add-openai-model` only for OpenAI-compatible provider setup flows that require explicit `apiKey` and `api` handling in one step. ### OpenCode - If the user provides complete JSON, replace the file directly. - Do not merge partial JSON. ### Codex CLI - `config.toml`: infer structured parameters and call the script. - `auth.json`: write only when the user provides a complete JSON payload; default to full replacement. - Do not delete unknown TOML sections such as `projects`, `notice`, or `tui`. - If existing `auth.json` contains login-state fields, replace it only when the user explicitly asks for full replacement. ### Claude Code - Full `settings.json` input may be replaced directly. - If the user provides only an `{"env": {...}}` fragment: - If `ANTHROPIC_BASE_URL` is present, replace the Claude-related env key set. - Otherwise, update only the explicitly provided Claude-related env keys. - Do not invent missing model fields. - Do not modify non-Claude env keys. ## Commands Use `{baseDir}` as the skill root. ### OpenClaw ```bash python3 {baseDir}/scripts/openclaw_config.py --help python3 {baseDir}/scripts/openclaw_config.py --file ~/.openclaw/openclaw.json summary python3 {baseDir}/scripts/openclaw_config.py --file ~/.openclaw/openclaw.json add-model \ --provider-id minimax-cn \ --model-id MiniMax-M2.7 \ --name 'MiniMax M2.7' \ --context-window 200000 \ --max-tokens 8192 \ --set-default ``` Supported operations: - `summary` - `add-model [--provider-id ...] [--model-id ...] [--name ...] [--context-window ...] [--max-tokens ...] [--reasoning true|false] [--allowlist] [--set-default] [--input text|image ...] [--base-url ...] [--api-key ...] [--api ...] [--auth-header true|false]` - `add-openai-model ...` - `set-openai-provider [--base-url ...] [--api-key ...]` - `remove-model <provider/model>` - `set-memory-search on|off` - `set-default-model <modelId>` - `set-agent-model <agentId> <modelId>` - `remove-provider <providerId>` ### OpenCode ```bash python3 {baseDir}/scripts/opencode_config.py --file ~/.config/opencode/opencode.json summary python3 {baseDir}/scripts/opencode_config.py --file ~/.config/opencode/opencode.json replace-from-stdin <<'EOF' {...full JSON...} EOF ``` Supported operations: - `summary` - `replace-from-stdin` ### Codex CLI ```bash python3 {baseDir}/scripts/codex_config.py --help python3 {baseDir}/scripts/codex_config.py --config-file ~/.codex/config.toml --auth-file ~/.codex/auth.json summary python3 {baseDir}/scripts/codex_config.py --config-file ~/.codex/config.toml replace-config-from-stdin <<'EOF' model_provider = "OpenAI" model = "gpt-5.4" EOF python3 {baseDir}/scripts/codex_config.py --auth-file ~/.codex/auth.json replace-auth-from-stdin <<'EOF' {"OPENAI_API_KEY":"sk-..."} EOF ``` Supported operations: - `summary` - `replace-config-from-stdin` - `replace-auth-from-stdin` - `set-openai-provider [--model-provider ...] [--model ...] [--review-model ...] [--reasoning-effort ...] [--disable-response-storage true|false] [--network-access ...] [--windows-wsl-setup-acknowledged true|false] [--model-context-window ...] [--model-auto-compact-token-limit ...] [--provider-name ...] [--base-url ...] [--wire-api ...] [--requires-openai-auth true|false]` ### Claude Code ```bash python3 {baseDir}/scripts/claude_config.py --help python3 {baseDir}/scripts/claude_config.py --file ~/.claude/settings.json summary python3 {baseDir}/scripts/claude_config.py --file ~/.claude/settings.json set-env \ --anthropic-auth-token 'sk-...' \ --attribution-header '0' python3 {baseDir}/scripts/claude_config.py --file ~/.claude/settings.json replace-env \ --anthropic-base-url 'https://example.invalid' \ --anthropic-auth-token 'sk-...' \ --attribution-header '0' python3 {baseDir}/scripts/claude_config.py --file ~/.claude/settings.json replace-from-stdin <<'EOF' {"env":{"ANTHROPIC_BASE_URL":"https://example.invalid"}} EOF ``` Supported operations: - `summary` - `replace-from-stdin` - `set-env [--anthropic-auth-token ...] [--anthropic-base-url ...] [--anthropic-default-haiku-model ...] [--anthropic-default-opus-model ...] [--anthropic-default-sonnet-model ...] [--anthropic-model ...] [--api-timeout-ms ...] [--disable-nonessential-traffic ...] [--attribution-header ...]` - `replace-env [--anthropic-auth-token ...] [--anthropic-base-url ...] [--anthropic-default-haiku-model ...] [--anthropic-default-opus-model ...] [--anthropic-default-sonnet-model ...] [--anthropic-model ...] [--api-timeout-ms ...] [--disable-nonessential-traffic ...] [--attribution-header ...]` ## Defaults and safeguards - Do not write `headers.User-Agent` unless explicitly needed. - Do not switch the default model unless the user asks. - Do not change `memorySearch` unless the user asks. - Stop if the script reports validation or dependency errors. - For OpenCode, default to full replacement when complete JSON is provided. - For Claude Code, if `ANTHROPIC_BASE_URL` is present, rebuild the Claude-related env set instead of carrying forward stale provider-specific env/model keys.

path: _meta.json
content: ownerId: kn7abr2hy41e7dnqz76krtdfss82mskz
slug: ai-config-admin
version: 1.0.1
publishedAt: 1773841441041

path: scripts/claude_config.py
content: #!/usr/bin/env python3
import argparse
from datetime import datetime
import json
import shutil
import sys
from pathlib import Path


HOME = Path.home()
DEFAULT_CLAUDE_PATH = HOME / ".claude" / "settings.json"
CLAUDE_ENV_KEYS = [
    "ANTHROPIC_AUTH_TOKEN",
    "ANTHROPIC_BASE_URL",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL",
    "ANTHROPIC_DEFAULT_OPUS_MODEL",
    "ANTHROPIC_DEFAULT_SONNET_MODEL",
    "ANTHROPIC_MODEL",
    "API_TIMEOUT_MS",
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
    "CLAUDE_CODE_ATTRIBUTION_HEADER",
]


def load_settings(path: Path) -> dict:
    with path.open("r", encoding="utf-8") as f:
        data = json.load(f)
    if not isinstance(data, dict):
        raise SystemExit("json root must be an object")
    return data


def load_json_text(text: str) -> dict:
    try:
        data = json.loads(text)
    except json.JSONDecodeError as exc:
        raise SystemExit(f"invalid json: {exc}") from exc
    if not isinstance(data, dict):
        raise SystemExit("json root must be an object")
    return data


def backup_file(path: Path) -> Path | None:
    if not path.exists():
        return None
    stamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
    backup_path = path.with_name(f"{path.name}.{stamp}.bak")
    shutil.copy2(path, backup_path)
    return backup_path


def save_settings(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    backup_file(path)
    with path.open("w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
        f.write("\n")


def ensure_env(settings: dict) -> dict:
    env = settings.setdefault("env", {})
    if not isinstance(env, dict):
        raise SystemExit("settings.env must be an object")
    return env


def summarize(settings: dict | None) -> dict:
    env = settings.get("env", {}) if isinstance(settings, dict) else {}
    if not isinstance(env, dict):
        env = {}
    return {
        "settingsExists": settings is not None,
        "hasEnv": isinstance(settings.get("env"), dict) if isinstance(settings, dict) else False,
        "envKeys": sorted(env.keys()),
        "anthropicBaseUrl": env.get("ANTHROPIC_BASE_URL"),
        "anthropicModel": env.get("ANTHROPIC_MODEL"),
        "hasAnthropicAuthToken": bool(env.get("ANTHROPIC_AUTH_TOKEN")),
        "claudeCodeDisableNonessentialTraffic": env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
        "claudeCodeAttributionHeader": env.get("CLAUDE_CODE_ATTRIBUTION_HEADER"),
    }


def collect_env_updates(args: argparse.Namespace) -> dict[str, str]:
    updates = {}
    mapping = {
        "ANTHROPIC_AUTH_TOKEN": args.anthropic_auth_token,
        "ANTHROPIC_BASE_URL": args.anthropic_base_url,
        "ANTHROPIC_DEFAULT_HAIKU_MODEL": args.anthropic_default_haiku_model,
        "ANTHROPIC_DEFAULT_OPUS_MODEL": args.anthropic_default_opus_model,
        "ANTHROPIC_DEFAULT_SONNET_MODEL": args.anthropic_default_sonnet_model,
        "ANTHROPIC_MODEL": args.anthropic_model,
        "API_TIMEOUT_MS": args.api_timeout_ms,
        "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": args.disable_nonessential_traffic,
        "CLAUDE_CODE_ATTRIBUTION_HEADER": args.attribution_header,
    }
    for key, value in mapping.items():
        if value is not None:
            updates[key] = value
    if not updates:
        raise SystemExit("pass at least one env flag")
    return updates


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser()
    parser.add_argument("--file")
    sub = parser.add_subparsers(dest="cmd", required=True)

    sub.add_parser("summary")
    sub.add_parser("replace-from-stdin")

    for name in ("set-env", "replace-env"):
        env_parser = sub.add_parser(name)
        env_parser.add_argument("--anthropic-auth-token")
        env_parser.add_argument("--anthropic-base-url")
        env_parser.add_argument("--anthropic-default-haiku-model")
        env_parser.add_argument("--anthropic-default-opus-model")
        env_parser.add_argument("--anthropic-default-sonnet-model")
        env_parser.add_argument("--anthropic-model")
        env_parser.add_argument("--api-timeout-ms")
        env_parser.add_argument("--disable-nonessential-traffic")
        env_parser.add_argument("--attribution-header")

    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    path = Path(args.file).expanduser() if args.file else DEFAULT_CLAUDE_PATH

    if args.cmd == "replace-from-stdin":
        data = load_json_text(sys.stdin.read())
        save_settings(path, data)
        return 0

    settings = load_settings(path) if path.exists() else {}

    if args.cmd == "summary":
        print(json.dumps(summarize(settings), ensure_ascii=False, indent=2))
        return 0

    if args.cmd == "set-env":
        env = ensure_env(settings)
        env.update(collect_env_updates(args))
        save_settings(path, settings)
        return 0

    if args.cmd == "replace-env":
        env = ensure_env(settings)
        for key in CLAUDE_ENV_KEYS:
            env.pop(key, None)
        env.update(collect_env_updates(args))
        save_settings(path, settings)
        return 0

    return 1


if __name__ == "__main__":
    sys.exit(main())


path: scripts/codex_config.py
content: #!/usr/bin/env python3
import argparse
from datetime import datetime
import json
import re
import shutil
import sys
import tomllib
from pathlib import Path


HOME = Path.home()
DEFAULT_CONFIG_PATH = HOME / ".codex" / "config.toml"
DEFAULT_AUTH_PATH = HOME / ".codex" / "auth.json"

SECTION_RE = re.compile(r"^\s*\[(.+)\]\s*(?:#.*)?$")
KEY_RE = re.compile(r"^(\s*)([A-Za-z0-9_.-]+)\s*=")


def load_toml(path: Path) -> dict:
    with path.open("rb") as f:
        return tomllib.load(f)


def load_json(path: Path) -> dict:
    with path.open("r", encoding="utf-8") as f:
        data = json.load(f)
    if not isinstance(data, dict):
        raise SystemExit("json root must be an object")
    return data


def load_toml_text(text: str) -> dict:
    try:
        data = tomllib.loads(text)
    except tomllib.TOMLDecodeError as exc:
        raise SystemExit(f"invalid toml: {exc}") from exc
    if not isinstance(data, dict):
        raise SystemExit("toml root must be a table")
    return data


def load_json_text(text: str) -> dict:
    try:
        data = json.loads(text)
    except json.JSONDecodeError as exc:
        raise SystemExit(f"invalid json: {exc}") from exc
    if not isinstance(data, dict):
        raise SystemExit("json root must be an object")
    return data


def backup_file(path: Path) -> Path | None:
    if not path.exists():
        return None
    stamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
    backup_path = path.with_name(f"{path.name}.{stamp}.bak")
    shutil.copy2(path, backup_path)
    return backup_path


def write_text(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    backup_file(path)
    with path.open("w", encoding="utf-8") as f:
        f.write(text)


def summarize(config_data: dict | None, auth_data: dict | None) -> dict:
    providers = config_data.get("model_providers", {}) if isinstance(config_data, dict) else {}
    provider_summary = {}
    for provider_id, provider in sorted(providers.items()):
        if not isinstance(provider, dict):
            continue
        provider_summary[provider_id] = {
            "name": provider.get("name"),
            "base_url": provider.get("base_url"),
            "wire_api": provider.get("wire_api"),
            "requires_openai_auth": provider.get("requires_openai_auth"),
        }
    return {
        "configExists": config_data is not None,
        "authExists": auth_data is not None,
        "modelProvider": config_data.get("model_provider") if isinstance(config_data, dict) else None,
        "model": config_data.get("model") if isinstance(config_data, dict) else None,
        "reviewModel": config_data.get("review_model") if isinstance(config_data, dict) else None,
        "modelReasoningEffort": config_data.get("model_reasoning_effort") if isinstance(config_data, dict) else None,
        "disableResponseStorage": config_data.get("disable_response_storage") if isinstance(config_data, dict) else None,
        "networkAccess": config_data.get("network_access") if isinstance(config_data, dict) else None,
        "modelContextWindow": config_data.get("model_context_window") if isinstance(config_data, dict) else None,
        "modelAutoCompactTokenLimit": config_data.get("model_auto_compact_token_limit") if isinstance(config_data, dict) else None,
        "providers": provider_summary,
        "authMode": auth_data.get("auth_mode") if isinstance(auth_data, dict) else None,
        "hasOpenAIKey": bool(auth_data.get("OPENAI_API_KEY")) if isinstance(auth_data, dict) else False,
        "hasTokens": bool(auth_data.get("tokens")) if isinstance(auth_data, dict) else False,
    }


def format_toml_value(value) -> str:
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, int):
        return str(value)
    if isinstance(value, str):
        return json.dumps(value, ensure_ascii=False)
    raise SystemExit(f"unsupported value type: {type(value).__name__}")


def split_lines(text: str) -> list[str]:
    lines = text.splitlines(keepends=True)
    if not lines:
        return []
    return lines


def current_section(line: str) -> str | None:
    match = SECTION_RE.match(line)
    if not match:
        return None
    return match.group(1).strip()


def replace_assignment(lines: list[str], section: str | None, key: str, rendered_value: str) -> list[str]:
    active_section = None
    section_start = None
    section_end = None

    for idx, line in enumerate(lines):
        header = current_section(line)
        if header is not None:
            if section is not None and header == section and section_start is None:
                section_start = idx
            elif section is not None and section_start is not None and section_end is None:
                section_end = idx
            active_section = header
        match = KEY_RE.match(line)
        if not match or match.group(2) != key:
            continue
        if section is None and active_section is None:
            lines[idx] = f"{match.group(1)}{key} = {rendered_value}\n"
            return lines
        if section is not None and active_section == section:
            lines[idx] = f"{match.group(1)}{key} = {rendered_value}\n"
            return lines

    if section is None:
        insert_at = next((idx for idx, line in enumerate(lines) if current_section(line) is not None), len(lines))
        lines.insert(insert_at, f"{key} = {rendered_value}\n")
        return lines

    if section_start is None:
        if lines and not lines[-1].endswith("\n"):
            lines[-1] = lines[-1] + "\n"
        if lines and lines[-1].strip():
            lines.append("\n")
        lines.append(f"[{section}]\n")
        lines.append(f"{key} = {rendered_value}\n")
        return lines

    if section_end is None:
        section_end = len(lines)
    lines.insert(section_end, f"{key} = {rendered_value}\n")
    return lines


def update_config_text(text: str, updates: dict[str, object], provider_updates: dict[str, object]) -> str:
    if text.strip():
        load_toml_text(text)
    lines = split_lines(text)

    top_level_order = list(updates.items())
    for key, value in reversed(top_level_order):
        if value is None:
            continue
        lines = replace_assignment(lines, None, key, format_toml_value(value))

    for key, value in provider_updates.items():
        if value is None:
            continue
        lines = replace_assignment(lines, "model_providers.OpenAI", key, format_toml_value(value))

    rendered = "".join(lines)
    if not rendered.endswith("\n"):
        rendered += "\n"
    load_toml_text(rendered)
    return rendered


def bool_arg(value: str) -> bool:
    normalized = value.strip().lower()
    if normalized in {"true", "1", "yes", "on"}:
        return True
    if normalized in {"false", "0", "no", "off"}:
        return False
    raise argparse.ArgumentTypeError("expected true or false")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--config-file", default=str(DEFAULT_CONFIG_PATH))
    parser.add_argument("--auth-file", default=str(DEFAULT_AUTH_PATH))
    sub = parser.add_subparsers(dest="cmd", required=True)

    sub.add_parser("summary")
    sub.add_parser("replace-config-from-stdin")
    sub.add_parser("replace-auth-from-stdin")

    set_provider = sub.add_parser("set-openai-provider")
    set_provider.add_argument("--model-provider")
    set_provider.add_argument("--model")
    set_provider.add_argument("--review-model")
    set_provider.add_argument("--reasoning-effort")
    set_provider.add_argument("--disable-response-storage", type=bool_arg)
    set_provider.add_argument("--network-access")
    set_provider.add_argument("--windows-wsl-setup-acknowledged", type=bool_arg)
    set_provider.add_argument("--model-context-window", type=int)
    set_provider.add_argument("--model-auto-compact-token-limit", type=int)
    set_provider.add_argument("--provider-name")
    set_provider.add_argument("--base-url")
    set_provider.add_argument("--wire-api")
    set_provider.add_argument("--requires-openai-auth", type=bool_arg)

    args = parser.parse_args()

    config_path = Path(args.config_file).expanduser()
    auth_path = Path(args.auth_file).expanduser()

    if args.cmd == "replace-config-from-stdin":
        text = sys.stdin.read()
        load_toml_text(text)
        write_text(config_path, text if text.endswith("\n") else f"{text}\n")
        return 0

    if args.cmd == "replace-auth-from-stdin":
        data = load_json_text(sys.stdin.read())
        write_text(auth_path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
        return 0

    config_data = load_toml(config_path) if config_path.exists() else None
    auth_data = load_json(auth_path) if auth_path.exists() else None

    if args.cmd == "summary":
        print(json.dumps(summarize(config_data, auth_data), ensure_ascii=False, indent=2))
        return 0

    if args.cmd == "set-openai-provider":
        existing_text = config_path.read_text("utf-8") if config_path.exists() else ""
        updated_text = update_config_text(
            existing_text,
            updates={
                "model_provider": args.model_provider,
                "model": args.model,
                "review_model": args.review_model,
                "model_reasoning_effort": args.reasoning_effort,
                "disable_response_storage": args.disable_response_storage,
                "network_access": args.network_access,
                "windows_wsl_setup_acknowledged": args.windows_wsl_setup_acknowledged,
                "model_context_window": args.model_context_window,
                "model_auto_compact_token_limit": args.model_auto_compact_token_limit,
            },
            provider_updates={
                "name": args.provider_name,
                "base_url": args.base_url,
                "wire_api": args.wire_api,
                "requires_openai_auth": args.requires_openai_auth,
            },
        )
        write_text(config_path, updated_text)
        return 0

    return 1


if __name__ == "__main__":
    sys.exit(main())


path: scripts/opencode_config.py
content: #!/usr/bin/env python3
import argparse
from datetime import datetime
import json
import shutil
import sys
from pathlib import Path


HOME = Path.home()
DEFAULT_OPENCODE_PATH = HOME / ".config" / "opencode" / "opencode.json"


def load_config(path: Path) -> dict:
    with path.open("r", encoding="utf-8") as f:
        return json.load(f)


def backup_config(path: Path) -> Path | None:
    if not path.exists():
        return None
    stamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
    backup_path = path.with_name(f"{path.name}.{stamp}.bak")
    shutil.copy2(path, backup_path)
    return backup_path


def save_config(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    backup_config(path)
    with path.open("w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
        f.write("\n")


def load_json_text(text: str) -> dict:
    try:
        data = json.loads(text)
    except json.JSONDecodeError as exc:
        raise SystemExit(f"invalid json: {exc}") from exc
    if not isinstance(data, dict):
        raise SystemExit("json root must be an object")
    return data


def summarize(data: dict) -> dict:
    providers = data.get("provider", {})
    agent = data.get("agent", {})
    provider_models = {}
    for provider_id, provider in sorted(providers.items()):
        if not isinstance(provider, dict):
            continue
        models = provider.get("models", {})
        if isinstance(models, dict):
            provider_models[provider_id] = sorted(models.keys())
    return {
        "providers": sorted(provider_models.keys()),
        "providerModels": provider_models,
        "agentKeys": sorted(
            key for key, value in agent.items() if isinstance(value, dict)
        ),
        "schema": data.get("$schema"),
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--file")
    sub = parser.add_subparsers(dest="cmd", required=True)

    sub.add_parser("summary")
    sub.add_parser("replace-from-stdin")

    args = parser.parse_args()
    path = Path(args.file).expanduser() if args.file else DEFAULT_OPENCODE_PATH

    if args.cmd == "replace-from-stdin":
        data = load_json_text(sys.stdin.read())
        save_config(path, data)
        return 0

    data = load_config(path)

    if args.cmd == "summary":
        print(json.dumps(summarize(data), ensure_ascii=False, indent=2))
        return 0

    return 1


if __name__ == "__main__":
    sys.exit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)