diff --git a/packages/cli/src/commands/agent/setup.ts b/packages/cli/src/commands/agent/setup.ts new file mode 100644 index 0000000..289dca6 --- /dev/null +++ b/packages/cli/src/commands/agent/setup.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: "agent setup", + description: "Configure a coding agent to use DashScope API", + skipDefaultApiKeySetup: true, + usage: "bl agent setup --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 agent setup --agent claude-code --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max", + "npx bailian-cli agent setup --agent qwen-code --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.6-plus", + "npx bailian-cli agent setup --agent opencode --base-url https://dashscope.aliyuncs.com/apps/anthropic/v1 --api-key sk-xxxxx --model qwen3.7-max", + "npx bailian-cli agent setup --agent openclaw --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.6-plus", + "npx bailian-cli agent setup --agent hermes --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max", + "npx bailian-cli agent setup --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 agent setup --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/agent/writers.ts b/packages/cli/src/commands/agent/writers.ts new file mode 100644 index 0000000..3ff0630 --- /dev/null +++ b/packages/cli/src/commands/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/agent/writers/claude-code.ts b/packages/cli/src/commands/agent/writers/claude-code.ts new file mode 100644 index 0000000..bf07d68 --- /dev/null +++ b/packages/cli/src/commands/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/agent/writers/codex.ts b/packages/cli/src/commands/agent/writers/codex.ts new file mode 100644 index 0000000..a84bd63 --- /dev/null +++ b/packages/cli/src/commands/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/agent/writers/hermes.ts b/packages/cli/src/commands/agent/writers/hermes.ts new file mode 100644 index 0000000..d1e2aa9 --- /dev/null +++ b/packages/cli/src/commands/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/agent/writers/openclaw.ts b/packages/cli/src/commands/agent/writers/openclaw.ts new file mode 100644 index 0000000..533181c --- /dev/null +++ b/packages/cli/src/commands/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/agent/writers/opencode.ts b/packages/cli/src/commands/agent/writers/opencode.ts new file mode 100644 index 0000000..d4ae556 --- /dev/null +++ b/packages/cli/src/commands/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/agent/writers/qwen-code.ts b/packages/cli/src/commands/agent/writers/qwen-code.ts new file mode 100644 index 0000000..36a6eae --- /dev/null +++ b/packages/cli/src/commands/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/agent/writers/utils.ts b/packages/cli/src/commands/agent/writers/utils.ts new file mode 100644 index 0000000..1e57ced --- /dev/null +++ b/packages/cli/src/commands/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/packages/cli/src/commands/catalog.ts b/packages/cli/src/commands/catalog.ts index ae48fcc..a20e921 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 "./agent/setup.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, + "agent setup": agentSetup, update: update, }; diff --git a/skills/bailian-cli/reference/agent.md b/skills/bailian-cli/reference/agent.md new file mode 100644 index 0000000..84e5544 --- /dev/null +++ b/skills/bailian-cli/reference/agent.md @@ -0,0 +1,57 @@ +# `bl agent` commands + +> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Regenerate: `pnpm --filter bailian-cli run generate:reference`. + +Index: [index.md](index.md) + +## Commands in this group + +| Command | Description | +| ---------------- | --------------------------------------------- | +| `bl agent setup` | Configure a coding agent to use DashScope API | + +## Command details + +### `bl agent setup` + +| Field | Value | +| --------------- | -------------------------------------------------------------------------------- | +| **Name** | `agent setup` | +| **Description** | Configure a coding agent to use DashScope API | +| **Usage** | `bl agent setup --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 agent setup --agent claude-code --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max +``` + +```bash +npx bailian-cli agent setup --agent qwen-code --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.6-plus +``` + +```bash +npx bailian-cli agent setup --agent opencode --base-url https://dashscope.aliyuncs.com/apps/anthropic/v1 --api-key sk-xxxxx --model qwen3.7-max +``` + +```bash +npx bailian-cli agent setup --agent openclaw --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.6-plus +``` + +```bash +npx bailian-cli agent setup --agent hermes --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max +``` + +```bash +npx bailian-cli agent setup --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.7-max +``` diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 890f43b..897f8a9 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -11,6 +11,7 @@ Use this index for the full quick index and global flags. | Command | Description | Detail | | -------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- | | `bl advisor recommend` | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) | [advisor.md](advisor.md) | +| `bl agent setup` | Configure a coding agent to use DashScope API | [agent.md](agent.md) | | `bl app call` | Call a Bailian application (agent or workflow) | [app.md](app.md) | | `bl app list` | List Bailian applications | [app.md](app.md) | | `bl auth login` | Authenticate with API key or console browser login (credentials can coexist) | [auth.md](auth.md) | @@ -62,6 +63,7 @@ Use this index for the full quick index and global flags. | Group | Commands | Reference | | ----------- | ---------------------------------------------------------------------------- | ---------------------------- | | `advisor` | `recommend` | [advisor.md](advisor.md) | +| `agent` | `setup` | [agent.md](agent.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) |