diff --git a/packages/cli/src/commands/catalog.ts b/packages/cli/src/commands/catalog.ts index ae48fcc..51cc83b 100644 --- a/packages/cli/src/commands/catalog.ts +++ b/packages/cli/src/commands/catalog.ts @@ -46,6 +46,7 @@ import quotaList from "./quota/list.ts"; import quotaRequest from "./quota/request.ts"; import quotaHistory from "./quota/history.ts"; import quotaCheck from "./quota/check.ts"; +import agentSetup from "./config/agent/index.ts"; /** Command registry map (no dependency on registry.ts — safe for build-time import). */ export const commands: Record = { @@ -94,5 +95,6 @@ export const commands: Record = { "quota request": quotaRequest, "quota history": quotaHistory, "quota check": quotaCheck, + "config agent": agentSetup, update: update, }; diff --git a/packages/cli/src/commands/config/agent/index.ts b/packages/cli/src/commands/config/agent/index.ts new file mode 100644 index 0000000..3a4465c --- /dev/null +++ b/packages/cli/src/commands/config/agent/index.ts @@ -0,0 +1,87 @@ +import { platform } from "os"; +import { + defineCommand, + BailianError, + ExitCode, + type Config, + type GlobalFlags, +} from "bailian-cli-core"; +import { AGENTS, VALID_AGENT_NAMES, type WriteParams } from "./writers.ts"; + +export default defineCommand({ + name: "config agent", + description: "Configure a coding agent to use DashScope API", + skipDefaultApiKeySetup: true, + usage: "bl config agent --agent --base-url --api-key --model ", + options: [ + { + flag: "--agent ", + description: `Target agent: ${VALID_AGENT_NAMES.join(", ")}`, + }, + { flag: "--base-url ", description: "API base URL" }, + { flag: "--api-key ", description: "API key" }, + { flag: "--model ", description: "Default model name" }, + ], + examples: [ + "npx bailian-cli config agent --agent claude-code --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max", + "npx bailian-cli config agent --agent qwen-code --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.6-plus", + "npx bailian-cli config agent --agent opencode --base-url https://dashscope.aliyuncs.com/apps/anthropic/v1 --api-key sk-xxxxx --model qwen3.7-max", + "npx bailian-cli config agent --agent openclaw --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.6-plus", + "npx bailian-cli config agent --agent hermes --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max", + "npx bailian-cli config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.7-max", + ], + async run(_config: Config, flags: GlobalFlags) { + const agent = flags.agent as string | undefined; + const baseUrl = flags.baseUrl as string | undefined; + const apiKey = flags.apiKey as string | undefined; + const model = flags.model as string | undefined; + + if (!agent || !baseUrl || !apiKey || !model) { + throw new BailianError( + "All flags are required: --agent, --base-url, --api-key, --model", + ExitCode.USAGE, + "bl config agent --agent --base-url --api-key --model ", + ); + } + + const agentDef = AGENTS[agent]; + if (!agentDef) { + throw new BailianError( + `Unknown agent "${agent}". Valid agents: ${VALID_AGENT_NAMES.join(", ")}`, + ExitCode.USAGE, + ); + } + + // Hermes does not support native Windows + if (agent === "hermes" && platform() === "win32") { + process.stderr.write( + "Warning: Hermes Agent does not support native Windows. Please use WSL2.\n", + ); + } + + const params: WriteParams = { baseUrl, apiKey, model }; + + if (_config.dryRun) { + process.stdout.write(`[dry-run] Would configure ${agentDef.label} with:\n`); + process.stdout.write(` base-url: ${baseUrl}\n`); + process.stdout.write( + ` api-key: ${apiKey.slice(0, 6)}${"*".repeat(Math.max(0, apiKey.length - 6))}\n`, + ); + process.stdout.write(` model: ${model}\n`); + return; + } + + const summary = agentDef.write(params); + + const isTTY = process.stderr.isTTY; + const green = isTTY ? "\x1b[32m" : ""; + const cyan = isTTY ? "\x1b[36m" : ""; + const reset = isTTY ? "\x1b[0m" : ""; + + process.stderr.write(`\n${green}✔ ${agentDef.label} configured successfully.${reset}\n\n`); + for (const p of summary.paths) { + process.stderr.write(` Written: ${cyan}${p}${reset}\n`); + } + process.stderr.write(`\n ${summary.nextStep}\n\n`); + }, +}); diff --git a/packages/cli/src/commands/config/agent/scripts/pure/agent-setup.ps1 b/packages/cli/src/commands/config/agent/scripts/pure/agent-setup.ps1 new file mode 100644 index 0000000..0d63c26 --- /dev/null +++ b/packages/cli/src/commands/config/agent/scripts/pure/agent-setup.ps1 @@ -0,0 +1,287 @@ +# Bailian — Pure Agent Config (no Node.js / npx required) +# +# Directly writes config files for coding agents to use DashScope API. +# +# Usage (set env vars, then pipe): +# $env:BL_AGENT="claude-code" +# $env:BL_BASE_URL="https://dashscope.aliyuncs.com/apps/anthropic" +# $env:BL_API_KEY="sk-xxxxx" +# $env:BL_MODEL="qwen3.7-max" +# irm https://xxx/agent-setup.ps1 | iex + +$ErrorActionPreference = "Stop" + +function Write-Err { Write-Host "ERROR: $args" -ForegroundColor Red } +function Write-Info { Write-Host "INFO: $args" -ForegroundColor Blue } +function Write-Ok { Write-Host "[OK] $args" -ForegroundColor Green } +function Write-Warn { Write-Host "WARNING: $args" -ForegroundColor Yellow } + +function Test-AnthropicEndpoint([string]$Url) { + return $Url -match '/apps/anthropic' +} + +function Backup-File([string]$Path) { + if (Test-Path $Path) { + $ts = [int][double]::Parse((Get-Date -UFormat %s)) + Copy-Item $Path "$Path.bak.$ts" + } +} + +function Write-SafeFile([string]$Path, [string]$Content) { + $dir = Split-Path $Path -Parent + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } + $tmp = "$Path.tmp" + Set-Content -Path $tmp -Value $Content -Encoding UTF8 -NoNewline + Move-Item -Path $tmp -Destination $Path -Force +} + +# ── Agent writers ───────────────────────────────────────────────────────────── + +function Setup-ClaudeCode([string]$BaseUrl, [string]$ApiKey, [string]$Model, [bool]$DryRun) { + $settings = Join-Path $HOME ".claude" "settings.json" + $onboarding = Join-Path $HOME ".claude.json" + + if ($DryRun) { Write-Info "[dry-run] Would write: $settings, $onboarding"; return } + + # settings.json — merge env if possible + $data = @{} + if (Test-Path $settings) { + try { $data = Get-Content $settings -Raw | ConvertFrom-Json -AsHashtable } catch { $data = @{} } + } + if (-not $data.ContainsKey("env")) { $data["env"] = @{} } + $data["env"]["ANTHROPIC_AUTH_TOKEN"] = $ApiKey + $data["env"]["ANTHROPIC_BASE_URL"] = $BaseUrl + $data["env"]["ANTHROPIC_MODEL"] = $Model + $data["env"]["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = $Model + $data["env"]["ANTHROPIC_DEFAULT_SONNET_MODEL"] = $Model + $data["env"]["ANTHROPIC_DEFAULT_OPUS_MODEL"] = $Model + $data["env"]["CLAUDE_CODE_SUBAGENT_MODEL"] = $Model + + Backup-File $settings + Write-SafeFile $settings ($data | ConvertTo-Json -Depth 10) + + # .claude.json — ensure hasCompletedOnboarding + $ob = @{} + if (Test-Path $onboarding) { + try { $ob = Get-Content $onboarding -Raw | ConvertFrom-Json -AsHashtable } catch { $ob = @{} } + } + $ob["hasCompletedOnboarding"] = $true + + Backup-File $onboarding + Write-SafeFile $onboarding ($ob | ConvertTo-Json -Depth 10) + + Write-Ok "Claude Code configured." + Write-Host " Written: $settings" + Write-Host " Written: $onboarding" + Write-Host "" + Write-Host " Run ``claude`` to start using Claude Code with DashScope." +} + +function Setup-QwenCode([string]$BaseUrl, [string]$ApiKey, [string]$Model, [bool]$DryRun) { + $settings = Join-Path $HOME ".qwen" "settings.json" + + if ($DryRun) { Write-Info "[dry-run] Would write: $settings"; return } + + $data = @{ + env = @{ BAILIAN_API_KEY = $ApiKey } + modelProviders = @{ + openai = @( + @{ + id = $Model + name = "[Bailian] $Model" + baseUrl = $BaseUrl + envKey = "BAILIAN_API_KEY" + } + ) + } + security = @{ auth = @{ selectedType = "openai" } } + model = @{ name = $Model } + '$version' = 3 + } + + Backup-File $settings + Write-SafeFile $settings ($data | ConvertTo-Json -Depth 10) + + Write-Ok "Qwen Code configured." + Write-Host " Written: $settings" + Write-Host "" + Write-Host " Run ``qwen`` to start using Qwen Code with DashScope." +} + +function Setup-OpenCode([string]$BaseUrl, [string]$ApiKey, [string]$Model, [bool]$DryRun) { + $config = Join-Path $HOME ".config" "opencode" "opencode.json" + + $npm = if (Test-AnthropicEndpoint $BaseUrl) { "@ai-sdk/anthropic" } else { "@ai-sdk/openai-compatible" } + + if ($DryRun) { Write-Info "[dry-run] Would write: $config (npm: $npm)"; return } + + $data = @{ + '$schema' = "https://opencode.ai/config.json" + provider = @{ + bailian = @{ + npm = $npm + name = "Alibaba Cloud Model Studio" + options = @{ baseURL = $BaseUrl; apiKey = $ApiKey } + models = @{ $Model = @{ name = $Model } } + } + } + } + + Backup-File $config + Write-SafeFile $config ($data | ConvertTo-Json -Depth 10) + + Write-Ok "OpenCode configured." + Write-Host " Written: $config" + Write-Host "" + Write-Host " Run ``opencode`` then type ``/models`` to select your model." +} + +function Setup-OpenClaw([string]$BaseUrl, [string]$ApiKey, [string]$Model, [bool]$DryRun) { + $config = Join-Path $HOME ".openclaw" "openclaw.json" + + $api = if (Test-AnthropicEndpoint $BaseUrl) { "anthropic-messages" } else { "openai-completions" } + + if ($DryRun) { Write-Info "[dry-run] Would write: $config (api: $api)"; return } + + $data = @{ + models = @{ + mode = "merge" + providers = @{ + bailian = @{ + baseUrl = $BaseUrl + apiKey = $ApiKey + api = $api + models = @( + @{ + id = $Model + name = $Model + reasoning = $false + input = @("text", "image") + contextWindow = 1000000 + maxTokens = 65536 + cost = @{ input = 0; output = 0; cacheRead = 0; cacheWrite = 0 } + } + ) + } + } + } + agents = @{ + defaults = @{ + model = @{ primary = "bailian/$Model" } + } + } + } + + Backup-File $config + Write-SafeFile $config ($data | ConvertTo-Json -Depth 10) + + Write-Ok "OpenClaw configured." + Write-Host " Written: $config" + Write-Host "" + Write-Host " Run ``openclaw`` to start using OpenClaw with DashScope." +} + +function Setup-Hermes([string]$BaseUrl, [string]$ApiKey, [string]$Model, [bool]$DryRun) { + $config = Join-Path $HOME ".hermes" "config.yaml" + + $apiMode = if (Test-AnthropicEndpoint $BaseUrl) { "anthropic_messages" } else { "chat_completions" } + + if ($DryRun) { Write-Info "[dry-run] Would write: $config (api_mode: $apiMode)"; return } + + Write-Warn "Hermes Agent does not support native Windows. This config is for WSL2." + + $yaml = @" +model: + default: $Model + provider: custom + base_url: $BaseUrl + api_mode: $apiMode + api_key: $ApiKey +"@ + + Backup-File $config + Write-SafeFile $config $yaml + + Write-Ok "Hermes Agent configured." + Write-Host " Written: $config" + Write-Host "" + Write-Host " Run ``hermes chat -q `"hello`"`` to verify." +} + +function Setup-Codex([string]$BaseUrl, [string]$ApiKey, [string]$Model, [bool]$DryRun) { + $config = Join-Path $HOME ".codex" "config.toml" + $auth = Join-Path $HOME ".codex" "auth.json" + + if ($DryRun) { Write-Info "[dry-run] Would write: $config, $auth"; return } + + $toml = @" +model_provider = "Model_Studio" +model = "$Model" + +[model_providers.Model_Studio] +name = "Model_Studio" +base_url = "$BaseUrl" +env_key = "OPENAI_API_KEY" +wire_api = "responses" +"@ + + Backup-File $config + Write-SafeFile $config $toml + + Backup-File $auth + Write-SafeFile $auth "{`n `"OPENAI_API_KEY`": `"$ApiKey`"`n}" + + Write-Ok "Codex configured." + Write-Host " Written: $config" + Write-Host " Written: $auth" + Write-Host "" + Write-Host " Run ``codex`` to start using Codex with DashScope." +} + +# ── Read parameters from environment variables ──────────────────────────────── + +$Agent = $env:BL_AGENT +$BaseUrl = $env:BL_BASE_URL +$ApiKey = $env:BL_API_KEY +$Model = $env:BL_MODEL +$DryRun = $env:BL_DRY_RUN -eq "1" + +if (-not $Agent -or -not $BaseUrl -or -not $ApiKey -or -not $Model) { + Write-Err "Missing required environment variables." + Write-Host "" + Write-Host "Set these before running:" + Write-Host ' $env:BL_AGENT = "claude-code"' + Write-Host ' $env:BL_BASE_URL = "https://dashscope.aliyuncs.com/apps/anthropic"' + Write-Host ' $env:BL_API_KEY = "sk-xxxxx"' + Write-Host ' $env:BL_MODEL = "qwen3.7-max"' + Write-Host "" + Write-Host "Then run:" + Write-Host ' irm https://xxx/agent-setup.ps1 | iex' + exit 1 +} + +# ── Dispatch ────────────────────────────────────────────────────────────────── + +Write-Host "" + +switch ($Agent) { + "claude-code" { Setup-ClaudeCode $BaseUrl $ApiKey $Model $DryRun } + "qwen-code" { Setup-QwenCode $BaseUrl $ApiKey $Model $DryRun } + "opencode" { Setup-OpenCode $BaseUrl $ApiKey $Model $DryRun } + "openclaw" { Setup-OpenClaw $BaseUrl $ApiKey $Model $DryRun } + "hermes" { Setup-Hermes $BaseUrl $ApiKey $Model $DryRun } + "codex" { Setup-Codex $BaseUrl $ApiKey $Model $DryRun } + default { + Write-Err "Unknown agent `"$Agent`". Valid agents: claude-code, qwen-code, opencode, openclaw, hermes, codex" + exit 1 + } +} + +Write-Host "" + +# Clean up env vars +Remove-Item Env:BL_AGENT -ErrorAction SilentlyContinue +Remove-Item Env:BL_BASE_URL -ErrorAction SilentlyContinue +Remove-Item Env:BL_API_KEY -ErrorAction SilentlyContinue +Remove-Item Env:BL_MODEL -ErrorAction SilentlyContinue +Remove-Item Env:BL_DRY_RUN -ErrorAction SilentlyContinue diff --git a/packages/cli/src/commands/config/agent/scripts/pure/agent-setup.sh b/packages/cli/src/commands/config/agent/scripts/pure/agent-setup.sh new file mode 100755 index 0000000..eaba55e --- /dev/null +++ b/packages/cli/src/commands/config/agent/scripts/pure/agent-setup.sh @@ -0,0 +1,443 @@ +#!/usr/bin/env bash +# Bailian — Pure Agent Config (no Node.js / npx required) +# +# Directly writes config files for coding agents to use DashScope API. +# +# Usage: +# curl -fsSL /agent-setup.sh | bash -s -- \ +# --agent claude-code \ +# --base-url https://dashscope.aliyuncs.com/apps/anthropic \ +# --api-key sk-xxxxx \ +# --model qwen3.7-max + +set -eo pipefail + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { printf "${BLUE}INFO:${NC} %s\n" "$1"; } +log_success() { printf "${GREEN}✔${NC} %s\n" "$1"; } +log_warning() { printf "${YELLOW}WARNING:${NC} %s\n" "$1" >&2; } +log_error() { printf "${RED}ERROR:${NC} %s\n" "$1" >&2; } + +VALID_AGENTS="claude-code, qwen-code, opencode, openclaw, hermes, codex" + +print_usage() { + echo "" + printf "${CYAN}Bailian — Agent Setup (pure, no Node.js required)${NC}\n" + cat <<'EOF' + +Usage: + curl -fsSL /agent-setup.sh | bash -s -- [OPTIONS] + +Required Options: + --agent Target agent: claude-code, qwen-code, opencode, openclaw, hermes, codex + --base-url API base URL + --api-key API key + --model Default model name + +Optional: + --dry-run Show what would be written without modifying files + -h, --help Show this help message + +Examples: + curl -fsSL /agent-setup.sh | bash -s -- \ + --agent claude-code \ + --base-url https://dashscope.aliyuncs.com/apps/anthropic \ + --api-key sk-xxxxx \ + --model qwen3.7-max +EOF +} + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +is_anthropic_endpoint() { + case "$1" in + */apps/anthropic*) return 0 ;; + *) return 1 ;; + esac +} + +backup_file() { + local f="$1" + [ -f "$f" ] && cp "$f" "${f}.bak.$(date +%s)" +} + +ensure_dir() { + mkdir -p "$1" +} + +write_file() { + local path="$1" content="$2" + ensure_dir "$(dirname "$path")" + local tmp="${path}.tmp" + printf '%s' "$content" > "$tmp" + chmod 600 "$tmp" + mv "$tmp" "$path" +} + +# Minimal JSON merge: read existing file, set a top-level key to a JSON value. +# Uses python3/python if available, otherwise falls back to writing fresh. +json_set_key() { + local file="$1" key="$2" value="$3" + if [ -f "$file" ] && command -v python3 >/dev/null 2>&1; then + python3 -c " +import json, sys +try: + with open('$file') as f: data = json.load(f) +except: data = {} +data['$key'] = json.loads('''$value''') +print(json.dumps(data, indent=2)) +" 2>/dev/null && return 0 + fi + # fallback: cannot merge, caller handles + return 1 +} + +# Read existing JSON file, merge env keys, write back +merge_json_env() { + local file="$1" + shift + # remaining args are key=value pairs + if [ -f "$file" ] && command -v python3 >/dev/null 2>&1; then + local py_pairs="" + while [ $# -gt 0 ]; do + local k="${1%%=*}" v="${1#*=}" + py_pairs="${py_pairs}d.setdefault('env',{})['${k}']='${v}';" + shift + done + python3 -c " +import json +try: + with open('$file') as f: d = json.load(f) +except: d = {} +${py_pairs} +print(json.dumps(d, indent=2)) +" 2>/dev/null && return 0 + fi + return 1 +} + +mask_key() { + local k="$1" + printf '%s' "${k:0:6}" + printf '%*s' $((${#k} - 6)) '' | tr ' ' '*' +} + +# ── Agent writers ───────────────────────────────────────────────────────────── + +setup_claude_code() { + local base_url="$1" api_key="$2" model="$3" dry_run="$4" + local settings="$HOME/.claude/settings.json" + local onboarding="$HOME/.claude.json" + + if [ "$dry_run" = "1" ]; then + log_info "[dry-run] Would write: $settings, $onboarding" + return + fi + + backup_file "$settings" + local merged + if merged=$(merge_json_env "$settings" \ + "ANTHROPIC_AUTH_TOKEN=$api_key" \ + "ANTHROPIC_BASE_URL=$base_url" \ + "ANTHROPIC_MODEL=$model" \ + "ANTHROPIC_DEFAULT_HAIKU_MODEL=$model" \ + "ANTHROPIC_DEFAULT_SONNET_MODEL=$model" \ + "ANTHROPIC_DEFAULT_OPUS_MODEL=$model" \ + "CLAUDE_CODE_SUBAGENT_MODEL=$model"); then + write_file "$settings" "$merged" + else + write_file "$settings" "$(cat </dev/null 2>&1; then + local ob + ob=$(python3 -c " +import json +try: + with open('$onboarding') as f: d = json.load(f) +except: d = {} +d['hasCompletedOnboarding'] = True +print(json.dumps(d, indent=2)) +" 2>/dev/null) && write_file "$onboarding" "$ob" + else + write_file "$onboarding" '{"hasCompletedOnboarding":true}' + fi + + log_success "Claude Code configured." + echo " Written: $settings" + echo " Written: $onboarding" + echo "" + echo " Run \`claude\` to start using Claude Code with DashScope." +} + +setup_qwen_code() { + local base_url="$1" api_key="$2" model="$3" dry_run="$4" + local settings="$HOME/.qwen/settings.json" + + if [ "$dry_run" = "1" ]; then + log_info "[dry-run] Would write: $settings" + return + fi + + backup_file "$settings" + write_file "$settings" "$(cat <$null + if ([int]$nodeMajor -lt 22) { + $nodeVersion = & node -v 2>$null + Write-Err "Node.js $nodeVersion is installed, but version 22+ is required." + Write-Host "" + Write-Host "Upgrade Node.js: https://nodejs.org/" + exit 1 + } +} + +# ── Read parameters from environment variables ──────────────────────────────── + +$Agent = $env:BL_AGENT +$BaseUrl = $env:BL_BASE_URL +$ApiKey = $env:BL_API_KEY +$Model = $env:BL_MODEL +$DryRun = $env:BL_DRY_RUN + +if (-not $Agent -or -not $BaseUrl -or -not $ApiKey -or -not $Model) { + Write-Err "Missing required environment variables." + Write-Host "" + Write-Host "Set these before running:" + Write-Host ' $env:BL_AGENT = "claude-code"' + Write-Host ' $env:BL_BASE_URL = "https://dashscope.aliyuncs.com/apps/anthropic"' + Write-Host ' $env:BL_API_KEY = "sk-xxxxx"' + Write-Host ' $env:BL_MODEL = "qwen3.7-max"' + Write-Host "" + Write-Host "Then run:" + Write-Host ' irm https://xxx/agent-setup.ps1 | iex' + exit 1 +} + +# ── Main ────────────────────────────────────────────────────────────────────── + +Test-Node + +$npxArgs = @("bailian-cli@latest", "config", "agent", + "--agent", $Agent, + "--base-url", $BaseUrl, + "--api-key", $ApiKey, + "--model", $Model) + +if ($DryRun -eq "1") { + $npxArgs += "--dry-run" +} + +Write-Info "Running: npx $($npxArgs -join ' ')" +Write-Host "" + +& npx @npxArgs + +# Clean up env vars +Remove-Item Env:BL_AGENT -ErrorAction SilentlyContinue +Remove-Item Env:BL_BASE_URL -ErrorAction SilentlyContinue +Remove-Item Env:BL_API_KEY -ErrorAction SilentlyContinue +Remove-Item Env:BL_MODEL -ErrorAction SilentlyContinue +Remove-Item Env:BL_DRY_RUN -ErrorAction SilentlyContinue diff --git a/packages/cli/src/commands/config/agent/scripts/via-bailian-cli/agent-setup.sh b/packages/cli/src/commands/config/agent/scripts/via-bailian-cli/agent-setup.sh new file mode 100755 index 0000000..0177b82 --- /dev/null +++ b/packages/cli/src/commands/config/agent/scripts/via-bailian-cli/agent-setup.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Bailian CLI — Agent Setup Script +# +# Configures a coding agent (Claude Code, Qwen Code, OpenCode, OpenClaw, +# Hermes, Codex) to use DashScope API with a single command. +# +# Usage: +# curl -fsSL /agent-setup.sh | bash -s -- \ +# --agent claude-code \ +# --base-url https://dashscope.aliyuncs.com/apps/anthropic \ +# --api-key sk-xxxxx \ +# --model qwen3.7-max + +set -eo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { printf "${BLUE}INFO:${NC} %s\n" "$1"; } +log_success() { printf "${GREEN}✔${NC} %s\n" "$1"; } +log_warning() { printf "${YELLOW}WARNING:${NC} %s\n" "$1" >&2; } +log_error() { printf "${RED}ERROR:${NC} %s\n" "$1" >&2; } + +print_usage() { + echo "" + printf "${CYAN}Bailian CLI — Agent Setup${NC}\n" + cat </agent-setup.sh | bash -s -- [OPTIONS] + +Required Options: + --agent Target agent: claude-code, qwen-code, opencode, openclaw, hermes, codex + --base-url API base URL + --api-key API key + --model Default model name + +Optional: + --dry-run Show what would be written without modifying files + -h, --help Show this help message + +Examples: + curl -fsSL /agent-setup.sh | bash -s -- \\ + --agent claude-code \\ + --base-url https://dashscope.aliyuncs.com/apps/anthropic \\ + --api-key sk-xxxxx \\ + --model qwen3.7-max + +EOF +} + +# ── Check Node.js ───────────────────────────────────────────────────────────── + +check_node() { + if ! command -v node >/dev/null 2>&1; then + log_error "Node.js is not installed." + echo "" + echo "Install Node.js 22+ from one of:" + echo " • https://nodejs.org/" + echo " • brew install node (macOS)" + echo " • apt install nodejs (Debian/Ubuntu)" + echo " • dnf install nodejs (Fedora)" + exit 1 + fi + + local node_major + node_major=$(node -p "Number(process.versions.node.split('.')[0])" 2>/dev/null || echo "0") + + if [ "$node_major" -lt 22 ] 2>/dev/null; then + local node_version + node_version=$(node -v 2>/dev/null || echo "unknown") + log_error "Node.js ${node_version} is installed, but version 22+ is required." + echo "" + echo "Upgrade Node.js: https://nodejs.org/" + exit 1 + fi +} + +# ── Parse args ──────────────────────────────────────────────────────────────── + +if [ $# -eq 0 ]; then + print_usage + exit 1 +fi + +for arg in "$@"; do + case "$arg" in + -h|--help) + print_usage + exit 0 + ;; + esac +done + +# ── Main ────────────────────────────────────────────────────────────────────── + +check_node + +log_info "Running: npx bailian-cli@latest config agent $*" +echo "" + +npx bailian-cli@latest config agent "$@" diff --git a/packages/cli/src/commands/config/agent/writers.ts b/packages/cli/src/commands/config/agent/writers.ts new file mode 100644 index 0000000..3ff0630 --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers.ts @@ -0,0 +1,20 @@ +export type { WriteParams, WriteSummary, AgentDef } from "./writers/utils.ts"; + +import type { AgentDef } from "./writers/utils.ts"; +import claudeCode from "./writers/claude-code.ts"; +import qwenCode from "./writers/qwen-code.ts"; +import opencode from "./writers/opencode.ts"; +import openclaw from "./writers/openclaw.ts"; +import hermes from "./writers/hermes.ts"; +import codex from "./writers/codex.ts"; + +export const AGENTS: Record = { + "claude-code": claudeCode, + "qwen-code": qwenCode, + opencode, + openclaw, + hermes, + codex, +}; + +export const VALID_AGENT_NAMES = Object.keys(AGENTS); diff --git a/packages/cli/src/commands/config/agent/writers/claude-code.ts b/packages/cli/src/commands/config/agent/writers/claude-code.ts new file mode 100644 index 0000000..bf07d68 --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers/claude-code.ts @@ -0,0 +1,36 @@ +import { homedir } from "os"; +import { join } from "path"; +import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts"; + +export default { + label: "Claude Code", + write({ baseUrl, apiKey, model }) { + const settingsPath = join(homedir(), ".claude", "settings.json"); + const onboardingPath = join(homedir(), ".claude.json"); + + // settings.json — merge env + backup(settingsPath); + const settings = readJson(settingsPath); + const env = (settings.env ?? {}) as Record; + env.ANTHROPIC_AUTH_TOKEN = apiKey; + env.ANTHROPIC_BASE_URL = baseUrl; + env.ANTHROPIC_MODEL = model; + env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model; + env.ANTHROPIC_DEFAULT_SONNET_MODEL = model; + env.ANTHROPIC_DEFAULT_OPUS_MODEL = model; + env.CLAUDE_CODE_SUBAGENT_MODEL = model; + settings.env = env; + writeJsonAtomic(settingsPath, settings); + + // .claude.json — ensure hasCompletedOnboarding + backup(onboardingPath); + const onboarding = readJson(onboardingPath); + onboarding.hasCompletedOnboarding = true; + writeJsonAtomic(onboardingPath, onboarding); + + return { + paths: [settingsPath, onboardingPath], + nextStep: "Run `claude` to start using Claude Code with DashScope.", + }; + }, +} satisfies AgentDef; diff --git a/packages/cli/src/commands/config/agent/writers/codex.ts b/packages/cli/src/commands/config/agent/writers/codex.ts new file mode 100644 index 0000000..a84bd63 --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers/codex.ts @@ -0,0 +1,42 @@ +import { homedir } from "os"; +import { join } from "path"; +import { mkdirSync, writeFileSync, renameSync } from "fs"; +import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts"; + +export default { + label: "Codex", + write({ baseUrl, apiKey, model }) { + const configPath = join(homedir(), ".codex", "config.toml"); + + backup(configPath); + + const toml = [ + `model_provider = "Model_Studio"`, + `model = "${model}"`, + ``, + `[model_providers.Model_Studio]`, + `name = "Model_Studio"`, + `base_url = "${baseUrl}"`, + `env_key = "OPENAI_API_KEY"`, + `wire_api = "responses"`, + ``, + ].join("\n"); + + mkdirSync(join(configPath, ".."), { recursive: true }); + const tmp = configPath + ".tmp"; + writeFileSync(tmp, toml, { mode: 0o600 }); + renameSync(tmp, configPath); + + // auth.json — store API key for Codex to read + const authPath = join(homedir(), ".codex", "auth.json"); + backup(authPath); + const auth = readJson(authPath); + auth.OPENAI_API_KEY = apiKey; + writeJsonAtomic(authPath, auth); + + return { + paths: [configPath, authPath], + nextStep: "Run `codex` to start using Codex with DashScope.", + }; + }, +} satisfies AgentDef; diff --git a/packages/cli/src/commands/config/agent/writers/hermes.ts b/packages/cli/src/commands/config/agent/writers/hermes.ts new file mode 100644 index 0000000..d1e2aa9 --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers/hermes.ts @@ -0,0 +1,42 @@ +import { homedir } from "os"; +import { join } from "path"; +import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "fs"; +import yaml from "yaml"; +import { backup, isAnthropicEndpoint, type AgentDef } from "./utils.ts"; + +export default { + label: "Hermes Agent", + write({ baseUrl, apiKey, model }) { + const configPath = join(homedir(), ".hermes", "config.yaml"); + + backup(configPath); + + let config: Record = {}; + if (existsSync(configPath)) { + try { + config = (yaml.parse(readFileSync(configPath, "utf-8")) ?? {}) as Record; + } catch { + config = {}; + } + } + + const apiMode = isAnthropicEndpoint(baseUrl) ? "anthropic_messages" : "chat_completions"; + config.model = { + default: model, + provider: "custom", + base_url: baseUrl, + api_mode: apiMode, + api_key: apiKey, + }; + + mkdirSync(join(configPath, ".."), { recursive: true }); + const tmp = configPath + ".tmp"; + writeFileSync(tmp, yaml.stringify(config), { mode: 0o600 }); + renameSync(tmp, configPath); + + return { + paths: [configPath], + nextStep: 'Run `hermes chat -q "hello"` to verify.', + }; + }, +} satisfies AgentDef; diff --git a/packages/cli/src/commands/config/agent/writers/openclaw.ts b/packages/cli/src/commands/config/agent/writers/openclaw.ts new file mode 100644 index 0000000..533181c --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers/openclaw.ts @@ -0,0 +1,51 @@ +import { homedir } from "os"; +import { join } from "path"; +import { backup, readJson, writeJsonAtomic, isAnthropicEndpoint, type AgentDef } from "./utils.ts"; + +export default { + label: "OpenClaw", + write({ baseUrl, apiKey, model }) { + const configPath = join(homedir(), ".openclaw", "openclaw.json"); + + backup(configPath); + const config = readJson(configPath); + + // models.providers.bailian + const models = (config.models ?? {}) as Record; + models.mode = "merge"; + const providers = (models.providers ?? {}) as Record; + const api = isAnthropicEndpoint(baseUrl) ? "anthropic-messages" : "openai-completions"; + providers.bailian = { + baseUrl, + apiKey, + api, + models: [ + { + id: model, + name: model, + reasoning: false, + input: ["text", "image"], + contextWindow: 1000000, + maxTokens: 65536, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }, + ], + }; + models.providers = providers; + config.models = models; + + // agents.defaults + const agents = (config.agents ?? {}) as Record; + const defaults = (agents.defaults ?? {}) as Record; + defaults.model = { primary: `bailian/${model}` }; + agents.defaults = defaults; + config.agents = agents; + + writeJsonAtomic(configPath, config); + + return { + paths: [configPath], + nextStep: "Run `openclaw` to start using OpenClaw with DashScope.", + }; + }, +} satisfies AgentDef; diff --git a/packages/cli/src/commands/config/agent/writers/opencode.ts b/packages/cli/src/commands/config/agent/writers/opencode.ts new file mode 100644 index 0000000..d4ae556 --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers/opencode.ts @@ -0,0 +1,32 @@ +import { homedir } from "os"; +import { join } from "path"; +import { backup, readJson, writeJsonAtomic, isAnthropicEndpoint, type AgentDef } from "./utils.ts"; + +export default { + label: "OpenCode", + write({ baseUrl, apiKey, model }) { + const configPath = join(homedir(), ".config", "opencode", "opencode.json"); + + backup(configPath); + const config = readJson(configPath); + + if (!config.$schema) config.$schema = "https://opencode.ai/config.json"; + + const provider = (config.provider ?? {}) as Record; + const npm = isAnthropicEndpoint(baseUrl) ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible"; + provider.bailian = { + npm, + name: "Alibaba Cloud Model Studio", + options: { baseURL: baseUrl, apiKey }, + models: { [model]: { name: model } }, + }; + config.provider = provider; + + writeJsonAtomic(configPath, config); + + return { + paths: [configPath], + nextStep: "Run `opencode` then type `/models` to select your model.", + }; + }, +} satisfies AgentDef; diff --git a/packages/cli/src/commands/config/agent/writers/qwen-code.ts b/packages/cli/src/commands/config/agent/writers/qwen-code.ts new file mode 100644 index 0000000..36a6eae --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers/qwen-code.ts @@ -0,0 +1,49 @@ +import { homedir } from "os"; +import { join } from "path"; +import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts"; + +export default { + label: "Qwen Code", + write({ baseUrl, apiKey, model }) { + const settingsPath = join(homedir(), ".qwen", "settings.json"); + + backup(settingsPath); + const settings = readJson(settingsPath); + + // env + const env = (settings.env ?? {}) as Record; + env.BAILIAN_API_KEY = apiKey; + settings.env = env; + + // modelProviders.openai — append or update + const providers = (settings.modelProviders ?? {}) as Record; + const openaiModels = (providers.openai ?? []) as Array>; + const existing = openaiModels.find((m) => m.id === model); + if (existing) { + existing.baseUrl = baseUrl; + existing.envKey = "BAILIAN_API_KEY"; + existing.name = `[Bailian] ${model}`; + } else { + openaiModels.push({ + id: model, + name: `[Bailian] ${model}`, + baseUrl, + envKey: "BAILIAN_API_KEY", + }); + } + providers.openai = openaiModels; + settings.modelProviders = providers; + + // security & model & version + settings.security = { auth: { selectedType: "openai" } }; + settings.model = { name: model }; + settings.$version = 3; + + writeJsonAtomic(settingsPath, settings); + + return { + paths: [settingsPath], + nextStep: "Run `qwen` to start using Qwen Code with DashScope.", + }; + }, +} satisfies AgentDef; diff --git a/packages/cli/src/commands/config/agent/writers/utils.ts b/packages/cli/src/commands/config/agent/writers/utils.ts new file mode 100644 index 0000000..1e57ced --- /dev/null +++ b/packages/cli/src/commands/config/agent/writers/utils.ts @@ -0,0 +1,44 @@ +import { join } from "path"; +import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync } from "fs"; + +export interface WriteParams { + baseUrl: string; + apiKey: string; + model: string; +} + +export interface WriteSummary { + paths: string[]; + nextStep: string; +} + +export interface AgentDef { + label: string; + write(params: WriteParams): WriteSummary; +} + +export function readJson(path: string): Record { + if (!existsSync(path)) return {}; + try { + return JSON.parse(readFileSync(path, "utf-8")) as Record; + } catch { + return {}; + } +} + +export function writeJsonAtomic(path: string, data: unknown): void { + mkdirSync(join(path, ".."), { recursive: true }); + const tmp = path + ".tmp"; + writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 }); + renameSync(tmp, path); +} + +export function backup(path: string): void { + if (!existsSync(path)) return; + const ts = Math.floor(Date.now() / 1000); + copyFileSync(path, `${path}.bak.${ts}`); +} + +export function isAnthropicEndpoint(baseUrl: string): boolean { + return baseUrl.includes("/apps/anthropic"); +} diff --git a/skills/bailian-cli/reference/config.md b/skills/bailian-cli/reference/config.md index 0b7da11..5ee3b9f 100644 --- a/skills/bailian-cli/reference/config.md +++ b/skills/bailian-cli/reference/config.md @@ -9,12 +9,56 @@ Index: [index.md](index.md) | Command | Description | | ------------------------- | ----------------------------------------------------------------------------------- | +| `bl config agent` | Configure a coding agent to use DashScope API | | `bl config export-schema` | Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas | | `bl config set` | Set a config value | | `bl config show` | Display current configuration | ## Command details +### `bl config agent` + +| Field | Value | +| --------------- | --------------------------------------------------------------------------------- | +| **Name** | `config agent` | +| **Description** | Configure a coding agent to use DashScope API | +| **Usage** | `bl config agent --agent --base-url --api-key --model ` | + +#### Options + +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | ----------------------------------------------------------------------- | +| `--agent ` | string | no | Target agent: claude-code, qwen-code, opencode, openclaw, hermes, codex | +| `--base-url ` | string | no | API base URL | +| `--api-key ` | string | no | API key | +| `--model ` | string | no | Default model name | + +#### Examples + +```bash +npx bailian-cli config agent --agent claude-code --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max +``` + +```bash +npx bailian-cli config agent --agent qwen-code --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.6-plus +``` + +```bash +npx bailian-cli config agent --agent opencode --base-url https://dashscope.aliyuncs.com/apps/anthropic/v1 --api-key sk-xxxxx --model qwen3.7-max +``` + +```bash +npx bailian-cli config agent --agent openclaw --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.6-plus +``` + +```bash +npx bailian-cli config agent --agent hermes --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max +``` + +```bash +npx bailian-cli config agent --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.7-max +``` + ### `bl config export-schema` | Field | Value | diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 890f43b..616c33b 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -16,6 +16,7 @@ Use this index for the full quick index and global flags. | `bl auth login` | Authenticate with API key or console browser login (credentials can coexist) | [auth.md](auth.md) | | `bl auth logout` | Clear stored credentials | [auth.md](auth.md) | | `bl auth status` | Show current authentication state | [auth.md](auth.md) | +| `bl config agent` | Configure a coding agent to use DashScope API | [config.md](config.md) | | `bl config export-schema` | Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas | [config.md](config.md) | | `bl config set` | Set a config value | [config.md](config.md) | | `bl config show` | Display current configuration | [config.md](config.md) | @@ -64,7 +65,7 @@ Use this index for the full quick index and global flags. | `advisor` | `recommend` | [advisor.md](advisor.md) | | `app` | `call`, `list` | [app.md](app.md) | | `auth` | `login`, `logout`, `status` | [auth.md](auth.md) | -| `config` | `export-schema`, `set`, `show` | [config.md](config.md) | +| `config` | `agent`, `export-schema`, `set`, `show` | [config.md](config.md) | | `console` | `call` | [console.md](console.md) | | `file` | `upload` | [file.md](file.md) | | `image` | `edit`, `generate` | [image.md](image.md) |