- completion_install: completions_installed() 原只检查 progs[0](myagents), 已存在则短路,导致 xiaohe 等后加 prog 的 completion 从未安装。 改为遍历全部 prog,任一缺失即触发重新安装。 - first_run.py: xiaohe_main() 启动时检测是否有任何可用认证(env var、 credentials、settings.json、已存 keys),无则弹交互式 provider 选择菜单。 - install.sh.in: 移除末尾 xiaohe init + scripts/init.sh wizard,安装脚本 只装软件不初始化配置。
174 lines
5.2 KiB
Python
174 lines
5.2 KiB
Python
"""First-run detection: guide user through provider setup when nothing is configured.
|
|
|
|
Called from ``xiaohe_main()`` on every invocation. When no auth is detected
|
|
(no env vars, no stored keys, no login credentials), prints a menu and
|
|
walks the user through selecting a provider and entering an API key.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import click
|
|
from rich.console import Console
|
|
|
|
from myagents.backends import (
|
|
PROVIDERS,
|
|
THIRD_PARTY_PROVIDER_IDS,
|
|
Provider,
|
|
)
|
|
from myagents.claude_settings import (
|
|
apply_provider,
|
|
get_active_provider,
|
|
load_settings,
|
|
save_settings,
|
|
)
|
|
from myagents.secrets import has_key, set_key
|
|
|
|
stderr_console = Console(stderr=True)
|
|
|
|
|
|
def _has_claude_login() -> bool:
|
|
"""Check for Claude Code OAuth login credentials file."""
|
|
creds = Path.home() / ".claude" / ".credentials.json"
|
|
return creds.is_file()
|
|
|
|
|
|
def is_any_auth_configured() -> bool:
|
|
"""Return True when at least one usable auth method is available."""
|
|
# 1. Official Claude: env var or login credentials file
|
|
if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN"):
|
|
return True
|
|
if _has_claude_login():
|
|
return True
|
|
|
|
# 2. Third-party provider active in Claude Code settings
|
|
if get_active_provider() is not None:
|
|
return True
|
|
|
|
# 3. ANTHROPIC_API_KEY set in ~/.claude/settings.json env block
|
|
env = load_settings().get("env", {})
|
|
if isinstance(env, dict) and env.get("ANTHROPIC_API_KEY"):
|
|
return True
|
|
|
|
# 4. Stored third-party keys (even if not currently active)
|
|
for pid in THIRD_PARTY_PROVIDER_IDS:
|
|
if has_key(PROVIDERS[pid].key_name):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _secure_prompt_key(provider: Provider) -> str:
|
|
"""Prompt for an API key with hidden input."""
|
|
try:
|
|
return click.prompt(
|
|
f"Enter {provider.display_name} API key",
|
|
hide_input=True,
|
|
err=True,
|
|
)
|
|
except click.UsageError:
|
|
try:
|
|
with open("/dev/tty", encoding="utf-8") as tty:
|
|
return tty.readline().rstrip("\n")
|
|
except OSError as exc:
|
|
raise click.ClickException(
|
|
f"Cannot read API key interactively: {exc}"
|
|
) from exc
|
|
|
|
|
|
def _persist_claude_api_key(key: str) -> None:
|
|
"""Write ANTHROPIC_API_KEY to ~/.claude/settings.json env block."""
|
|
settings = load_settings()
|
|
env: dict = {k: v for k, v in settings.get("env", {}).items()
|
|
if isinstance(v, str)}
|
|
env["ANTHROPIC_API_KEY"] = key
|
|
settings["env"] = env
|
|
save_settings(settings)
|
|
|
|
|
|
def _run_setup_wizard() -> None:
|
|
"""Interactive guided setup: choose provider and enter API key."""
|
|
providers = [
|
|
PROVIDERS["claude"],
|
|
PROVIDERS["deepseek"],
|
|
PROVIDERS["kimi"],
|
|
PROVIDERS["kimi-code"],
|
|
]
|
|
|
|
stderr_console.print(
|
|
"\n[bold yellow]No model provider configured yet.[/bold yellow]"
|
|
)
|
|
stderr_console.print(
|
|
"[dim]Claude Code needs authentication. Pick one:[/dim]\n"
|
|
)
|
|
for i, p in enumerate(providers, 1):
|
|
stderr_console.print(f" {i}) {p.display_name}")
|
|
stderr_console.print(
|
|
" [dim]Or press Enter to skip (set up later with:"
|
|
" xiaohe switch provider <name>)[/dim]"
|
|
)
|
|
|
|
try:
|
|
choice = click.prompt(
|
|
"Choose", default="", show_default=False, err=True
|
|
)
|
|
except (click.Abort, EOFError):
|
|
stderr_console.print("[dim]Skipped. Run 'xiaohe switch provider' later.[/dim]")
|
|
return
|
|
|
|
choice = choice.strip()
|
|
if not choice:
|
|
stderr_console.print("[dim]Skipped. Run 'xiaohe switch provider' later.[/dim]")
|
|
return
|
|
|
|
try:
|
|
idx = int(choice) - 1
|
|
if idx < 0 or idx >= len(providers):
|
|
raise ValueError
|
|
except ValueError:
|
|
stderr_console.print(f"[red]Invalid choice: {choice}[/red]")
|
|
return
|
|
|
|
provider = providers[idx]
|
|
stderr_console.print(f"\n[bold]{provider.display_name}[/bold] selected.")
|
|
|
|
if provider.id == "claude":
|
|
key = _secure_prompt_key(provider)
|
|
if not key:
|
|
stderr_console.print("[yellow]No key entered — skipped.[/yellow]")
|
|
return
|
|
_persist_claude_api_key(key)
|
|
stderr_console.print(
|
|
f"[green]{provider.display_name} API key saved to"
|
|
f" ~/.claude/settings.json.[/green]"
|
|
)
|
|
else:
|
|
key = _secure_prompt_key(provider)
|
|
if not key:
|
|
stderr_console.print("[yellow]No key entered — skipped.[/yellow]")
|
|
return
|
|
set_key(provider.key_name, key)
|
|
settings = apply_provider(provider, key=key)
|
|
save_settings(settings)
|
|
stderr_console.print(
|
|
f"[green]Switched provider to {provider.display_name}.[/green]"
|
|
)
|
|
|
|
stderr_console.print()
|
|
|
|
|
|
def ensure_auth_configured() -> None:
|
|
"""Check auth state on ``xiaohe`` startup; run wizard if unconfigured."""
|
|
if is_any_auth_configured():
|
|
return
|
|
try:
|
|
_run_setup_wizard()
|
|
except Exception:
|
|
# Never block xiaohe launch on a setup error.
|
|
stderr_console.print(
|
|
"[yellow]Setup interrupted."
|
|
" Run 'xiaohe switch provider' later to configure.[/yellow]"
|
|
)
|