diff --git a/myagents/commands/info.py b/myagents/commands/info.py new file mode 100644 index 0000000..b8e195e --- /dev/null +++ b/myagents/commands/info.py @@ -0,0 +1,51 @@ +"""``xiaohe info`` — show current version, agent, and provider.""" + +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + +import click +from rich.console import Console +from rich.table import Table + +console = Console() + + +def _package_version() -> str: + try: + return version("myagents") + except PackageNotFoundError: + return "0.0.0" + + +def _describe_runtime() -> str: + from myagents.commands.upgrade import _current_version + + cur = _current_version() + return cur if cur else "development" + + +@click.command("info") +def info_cmd() -> None: + """Show xiaohe status: version, agent, and provider.""" + from myagents.commands.switch import _resolve_agent + from myagents.claude_settings import describe_active_backend + from myagents.commands.provider import provider_list_table + + table = Table(show_header=False, box=None, padding=(0, 1)) + table.add_column("key", style="dim") + table.add_column("value") + + table.add_row("Version", f"myagents {_package_version()} (runtime: {_describe_runtime()})") + + default_agent = _resolve_agent() + table.add_row("Agent", f"{default_agent} (xiaohe → my{default_agent})") + + table.add_row("Provider", describe_active_backend()) + + console.print(table) + console.print() + provider_list_table() + console.print() + console.print("[dim]Switch with:[/dim]") + console.print(" xiaohe switch version xiaohe switch agent xiaohe switch provider") diff --git a/myagents/commands/backend.py b/myagents/commands/provider.py similarity index 71% rename from myagents/commands/backend.py rename to myagents/commands/provider.py index 892f2f6..0e1c81f 100644 --- a/myagents/commands/backend.py +++ b/myagents/commands/provider.py @@ -1,4 +1,4 @@ -"""``xiaohe backend`` — switch Claude Code's model backend.""" +"""``xiaohe switch provider`` — switch Claude Code's LLM provider.""" from __future__ import annotations @@ -30,7 +30,6 @@ def _secure_prompt_key(provider: Provider) -> str: try: return click.prompt(prompt_text, hide_input=True, err=True) except click.UsageError: - # click.prompt raises when stdin is not a TTY. Fall back to /dev/tty. try: with open("/dev/tty", encoding="utf-8") as tty: # noqa: PTH123 return tty.readline().rstrip("\n") @@ -40,16 +39,10 @@ def _secure_prompt_key(provider: Provider) -> str: ) from exc -@click.group("backend") -def backend_cmd() -> None: - """Switch Claude Code's model backend (DeepSeek, Kimi, Kimi Code, Claude).""" - - -@backend_cmd.command("list") -def backend_list() -> None: - """List built-in backends and the active one.""" +def provider_list_table() -> None: + """Print the provider list table (reusable from info command).""" active = get_active_provider() - console.print("[bold]Built-in backends:[/bold]") + console.print("[bold]Available providers:[/bold]") for provider in list_providers(): marker = "" if active and active.id == provider.id: @@ -65,32 +58,43 @@ def backend_list() -> None: ) -@backend_cmd.command("current") -def backend_current() -> None: - """Show the active backend for this workspace.""" - console.print(f"[bold]Current backend:[/bold] {describe_active_backend()}") +@click.group("provider") +def provider_cmd() -> None: + """Switch Claude Code's LLM provider (DeepSeek, Kimi, Kimi Code, Claude).""" -@backend_cmd.command("use") +@provider_cmd.command("list") +def provider_list() -> None: + """List available providers and the active one.""" + provider_list_table() + + +@provider_cmd.command("current") +def provider_current() -> None: + """Show the active provider.""" + console.print(f"[bold]Active provider:[/bold] {describe_active_backend()}") + + +@provider_cmd.command("use") @click.argument("provider_id") @click.option("--key", help="API key (scripting only; appears in shell history)") -@click.option("--model", help="Override the default model for this provider") +@click.option("--model", help="Override the default model") @click.option("--base-url", help="Override the provider base URL") @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt") -def backend_use( +def provider_use( provider_id: str, key: str | None, model: str | None, base_url: str | None, yes: bool, ) -> None: - """Switch Claude Code to the given backend.""" + """Switch to the given provider.""" try: provider = get_provider(provider_id) except KeyError: known = ", ".join(p.id for p in list_providers()) raise click.ClickException( - f"Unknown backend '{provider_id}'. Choose from: {known}" + f"Unknown provider '{provider_id}'. Choose from: {known}" ) if is_third_party(provider): @@ -98,7 +102,7 @@ def backend_use( if not resolved_key: resolved_key = _secure_prompt_key(provider) if not resolved_key: - raise click.ClickException("API key is required for this backend.") + raise click.ClickException("API key is required for this provider.") else: resolved_key = None @@ -109,7 +113,7 @@ def backend_use( summary += f" (base-url: {base_url})" if not yes and not click.confirm( - f"Set backend to {summary} for this workspace?", + f"Switch LLM provider to {summary}?", default=True, err=True, ): @@ -125,37 +129,37 @@ def backend_use( save_settings(settings) set_setting("backend_provider", provider.id) - console.print(f"[bold green]Backend switched to {summary}.[/bold green]") + console.print(f"[bold green]Switched to {summary}.[/bold green]") if provider.id == "claude": console.print( - "[dim]Cleared third-party backend env vars from ~/.claude/settings.json.[/dim]" + "[dim]Cleared third-party provider env vars from ~/.claude/settings.json.[/dim]" ) -@backend_cmd.command("reset") +@provider_cmd.command("reset") @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt") -def backend_reset(yes: bool) -> None: - """Reset to the official Claude backend.""" +def provider_reset(yes: bool) -> None: + """Reset to the official Claude provider.""" ctx = click.get_current_context() - ctx.invoke(backend_use, provider_id="claude", yes=yes) + ctx.invoke(provider_use, provider_id="claude", yes=yes) -@backend_cmd.group("key") -def backend_key() -> None: +@provider_cmd.group("key") +def provider_key() -> None: """Manage stored API keys.""" -@backend_key.command("set") +@provider_key.command("set") @click.argument("provider_id") @click.option("--key", help="API key (scripting only; appears in shell history)") -def backend_key_set(provider_id: str, key: str | None) -> None: - """Store an API key for a backend without switching to it.""" +def provider_key_set(provider_id: str, key: str | None) -> None: + """Store an API key without switching.""" try: provider = get_provider(provider_id) except KeyError: known = ", ".join(p.id for p in list_providers()) raise click.ClickException( - f"Unknown backend '{provider_id}'. Choose from: {known}" + f"Unknown provider '{provider_id}'. Choose from: {known}" ) if provider.id == "claude": @@ -175,22 +179,17 @@ def backend_key_set(provider_id: str, key: str | None) -> None: ) -@backend_key.command("rm") +@provider_key.command("rm") @click.argument("provider_id") -@click.option( - "--yes", - "-y", - is_flag=True, - help="Skip the confirmation prompt", -) -def backend_key_rm(provider_id: str, yes: bool) -> None: - """Remove the stored API key for a backend.""" +@click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt") +def provider_key_rm(provider_id: str, yes: bool) -> None: + """Remove the stored API key for a provider.""" try: provider = get_provider(provider_id) except KeyError: known = ", ".join(p.id for p in list_providers()) raise click.ClickException( - f"Unknown backend '{provider_id}'. Choose from: {known}" + f"Unknown provider '{provider_id}'. Choose from: {known}" ) if not yes and not click.confirm( @@ -208,4 +207,4 @@ def backend_key_rm(provider_id: str, yes: bool) -> None: if __name__ == "__main__": - backend_cmd() + provider_cmd() diff --git a/myagents/commands/switch.py b/myagents/commands/switch.py index 5e2b55e..4b34a9f 100644 --- a/myagents/commands/switch.py +++ b/myagents/commands/switch.py @@ -1,23 +1,29 @@ -"""``xiaohe switch`` — move ``current`` to another installed runtime version. +"""``xiaohe switch`` — switch version, agent, or provider. -Rollback/roll-forward counterpart to ``xiaohe upgrade``: repoints the -``current`` symlink, reinstalls the CLI tools from that runtime, and re-syncs -the workspace. Run without an argument to list installed versions and pick -one interactively. +Subcommands: + version — switch the active runtime version (rollback/roll-forward) + agent — switch the default agent CLI (claude / kimi / codex / hermes) + provider — switch Claude Code's LLM provider (DeepSeek / Kimi / Kimi Code / Claude) """ +from __future__ import annotations + import click from rich.console import Console -from myagents.commands.sync_workspace import _print_report, sync_workspace -from myagents.commands.upgrade import _current_version, _install_tools, _runtime_root +from myagents.entrypoints import _KNOWN_BACKENDS from myagents.project_root import get_workspace_root +from myagents.settings import get_setting, set_setting stderr_console = Console(stderr=True) console = Console() +# ── version ──────────────────────────────────────────────────────────────── + def _installed_versions() -> list[str]: + from myagents.commands.upgrade import _runtime_root + root = _runtime_root() if not root.is_dir(): return [] @@ -26,11 +32,14 @@ def _installed_versions() -> list[str]: ) -@click.command("switch") +@click.command("version") @click.argument("version", required=False) @click.option("--yes", is_flag=True, help="Skip the confirmation prompt") -def switch_cmd(version: str | None, yes: bool) -> None: +def switch_version(version: str | None, yes: bool) -> None: """Switch the active runtime to another installed version (rollback).""" + from myagents.commands.sync_workspace import _print_report, sync_workspace + from myagents.commands.upgrade import _current_version, _install_tools, _runtime_root + versions = _installed_versions() current = _current_version() if not versions: @@ -82,3 +91,76 @@ def switch_cmd(version: str | None, yes: bool) -> None: f"\n[bold green]Switched: {current} -> {version}[/bold green] " "(open a new terminal to pick up the change)" ) + + +# ── agent ────────────────────────────────────────────────────────────────── + +def agent_list_table() -> None: + """Print the agent list (reusable from info command).""" + default = _resolve_agent() + console.print("[bold]Available agents:[/bold]") + for name in _KNOWN_BACKENDS: + marker = " [green](current)[/green]" if name == default else "" + console.print(f" {name}{marker}") + + +def _resolve_agent() -> str: + import re + + agent = str(get_setting("default_agent", "") or "") + agent = re.sub(r"^my", "", agent.strip().lower()) + return agent if agent in _KNOWN_BACKENDS else "claude" + + +@click.command("agent") +@click.argument("agent", required=False) +def switch_agent(agent: str | None) -> None: + """Switch the default agent CLI.""" + known = list(_KNOWN_BACKENDS) + default = _resolve_agent() + + if not agent: + console.print("[bold]Available agents:[/bold]") + for name in known: + marker = " [green](current)[/green]" if name == default else "" + console.print(f" {name}{marker}") + agent = str(click.prompt("Switch to", err=True)) + + agent = agent.strip().lower() + if agent.startswith("my"): + agent = agent[2:] + if agent not in _KNOWN_BACKENDS: + raise click.ClickException( + f"Unknown agent '{agent}'. Choose from: {', '.join(known)}" + ) + if agent == default: + console.print(f"Already on {agent}.") + return + + set_setting("default_agent", agent) + console.print(f"[bold green]Switched default agent to {agent}.[/bold green]") + console.print(f"[dim]Use 'xiaohe' (or 'my{agent}') to launch.[/dim]") + + +# ── switch group ─────────────────────────────────────────────────────────── + +@click.group("switch", invoke_without_command=True) +@click.pass_context +def switch_cmd(ctx: click.Context) -> None: + """Switch version, agent, or provider. + + Run without arguments to see all options. + """ + if ctx.invoked_subcommand is None: + console.print("[bold]Use one of:[/bold]") + console.print(" xiaohe switch version — switch runtime version") + console.print(" xiaohe switch agent — switch default agent CLI") + console.print(" xiaohe switch provider — switch LLM provider") + console.print() + console.print("[dim]Run 'xiaohe switch --help' for details.[/dim]") + + +switch_cmd.add_command(switch_version) +switch_cmd.add_command(switch_agent) + +# provider_cmd is added in entrypoints.py (lazy import to avoid cycles) diff --git a/myagents/entrypoints.py b/myagents/entrypoints.py index 786f96f..ac0c5c5 100644 --- a/myagents/entrypoints.py +++ b/myagents/entrypoints.py @@ -26,16 +26,19 @@ def default_agent() -> str: def build_xiaohe_cli(): - """The ``xiaohe`` command group: agent forwarding + init/sync/upgrade/switch/uninstall/version.""" - from myagents.commands.backend import backend_cmd + """The ``xiaohe`` command group: agent forwarding + info/init/sync/upgrade/switch/uninstall/version.""" + from myagents.commands.info import info_cmd + from myagents.commands.provider import provider_cmd from myagents.commands.switch import switch_cmd from myagents.commands.sync_workspace import init_cmd, sync_cmd from myagents.commands.uninstall import uninstall_cmd from myagents.commands.upgrade import upgrade_cmd from myagents.commands.version import version_cmd + switch_cmd.add_command(provider_cmd) + xiaohe_cli = build_cli(default_agent(), prog_name="xiaohe", offer_install=True) - xiaohe_cli.add_command(backend_cmd) + xiaohe_cli.add_command(info_cmd) xiaohe_cli.add_command(init_cmd) xiaohe_cli.add_command(sync_cmd) xiaohe_cli.add_command(upgrade_cmd)