Skip to content

Commit e1dea36

Browse files
lishengzxcclaude
andcommitted
feat: add bl agent setup command for one-click agent configuration
Add a new command that configures popular coding agents (Claude Code, Qwen Code, OpenCode, OpenClaw, Hermes, Codex) to use DashScope API with a single copy-paste command from the web console. Supports Linux, macOS, and Windows. Non-destructively merges into existing config files with automatic backup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1ffcbdd commit e1dea36

12 files changed

Lines changed: 457 additions & 0 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { platform } from "os";
2+
import {
3+
defineCommand,
4+
BailianError,
5+
ExitCode,
6+
type Config,
7+
type GlobalFlags,
8+
} from "bailian-cli-core";
9+
import { AGENTS, VALID_AGENT_NAMES, type WriteParams } from "./writers.ts";
10+
11+
export default defineCommand({
12+
name: "agent setup",
13+
description: "Configure a coding agent to use DashScope API",
14+
skipDefaultApiKeySetup: true,
15+
usage: "bl agent setup --agent <name> --base-url <url> --api-key <key> --model <model>",
16+
options: [
17+
{
18+
flag: "--agent <name>",
19+
description: `Target agent: ${VALID_AGENT_NAMES.join(", ")}`,
20+
},
21+
{ flag: "--base-url <url>", description: "API base URL" },
22+
{ flag: "--api-key <key>", description: "API key" },
23+
{ flag: "--model <model>", description: "Default model name" },
24+
],
25+
examples: [
26+
"npx bailian-cli agent setup --agent claude-code --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max",
27+
"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",
28+
"npx bailian-cli agent setup --agent opencode --base-url https://dashscope.aliyuncs.com/apps/anthropic/v1 --api-key sk-xxxxx --model qwen3.7-max",
29+
"npx bailian-cli agent setup --agent openclaw --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.6-plus",
30+
"npx bailian-cli agent setup --agent hermes --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3.7-max",
31+
"npx bailian-cli agent setup --agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3.7-max",
32+
],
33+
async run(_config: Config, flags: GlobalFlags) {
34+
const agent = flags.agent as string | undefined;
35+
const baseUrl = flags.baseUrl as string | undefined;
36+
const apiKey = flags.apiKey as string | undefined;
37+
const model = flags.model as string | undefined;
38+
39+
if (!agent || !baseUrl || !apiKey || !model) {
40+
throw new BailianError(
41+
"All flags are required: --agent, --base-url, --api-key, --model",
42+
ExitCode.USAGE,
43+
"bl agent setup --agent <name> --base-url <url> --api-key <key> --model <model>",
44+
);
45+
}
46+
47+
const agentDef = AGENTS[agent];
48+
if (!agentDef) {
49+
throw new BailianError(
50+
`Unknown agent "${agent}". Valid agents: ${VALID_AGENT_NAMES.join(", ")}`,
51+
ExitCode.USAGE,
52+
);
53+
}
54+
55+
// Hermes does not support native Windows
56+
if (agent === "hermes" && platform() === "win32") {
57+
process.stderr.write(
58+
"Warning: Hermes Agent does not support native Windows. Please use WSL2.\n",
59+
);
60+
}
61+
62+
const params: WriteParams = { baseUrl, apiKey, model };
63+
64+
if (_config.dryRun) {
65+
process.stdout.write(`[dry-run] Would configure ${agentDef.label} with:\n`);
66+
process.stdout.write(` base-url: ${baseUrl}\n`);
67+
process.stdout.write(
68+
` api-key: ${apiKey.slice(0, 6)}${"*".repeat(Math.max(0, apiKey.length - 6))}\n`,
69+
);
70+
process.stdout.write(` model: ${model}\n`);
71+
return;
72+
}
73+
74+
const summary = agentDef.write(params);
75+
76+
const isTTY = process.stderr.isTTY;
77+
const green = isTTY ? "\x1b[32m" : "";
78+
const cyan = isTTY ? "\x1b[36m" : "";
79+
const reset = isTTY ? "\x1b[0m" : "";
80+
81+
process.stderr.write(`\n${green}${agentDef.label} configured successfully.${reset}\n\n`);
82+
for (const p of summary.paths) {
83+
process.stderr.write(` Written: ${cyan}${p}${reset}\n`);
84+
}
85+
process.stderr.write(`\n ${summary.nextStep}\n\n`);
86+
},
87+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export type { WriteParams, WriteSummary, AgentDef } from "./writers/utils.ts";
2+
3+
import type { AgentDef } from "./writers/utils.ts";
4+
import claudeCode from "./writers/claude-code.ts";
5+
import qwenCode from "./writers/qwen-code.ts";
6+
import opencode from "./writers/opencode.ts";
7+
import openclaw from "./writers/openclaw.ts";
8+
import hermes from "./writers/hermes.ts";
9+
import codex from "./writers/codex.ts";
10+
11+
export const AGENTS: Record<string, AgentDef> = {
12+
"claude-code": claudeCode,
13+
"qwen-code": qwenCode,
14+
opencode,
15+
openclaw,
16+
hermes,
17+
codex,
18+
};
19+
20+
export const VALID_AGENT_NAMES = Object.keys(AGENTS);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts";
4+
5+
export default {
6+
label: "Claude Code",
7+
write({ baseUrl, apiKey, model }) {
8+
const settingsPath = join(homedir(), ".claude", "settings.json");
9+
const onboardingPath = join(homedir(), ".claude.json");
10+
11+
// settings.json — merge env
12+
backup(settingsPath);
13+
const settings = readJson(settingsPath);
14+
const env = (settings.env ?? {}) as Record<string, string>;
15+
env.ANTHROPIC_AUTH_TOKEN = apiKey;
16+
env.ANTHROPIC_BASE_URL = baseUrl;
17+
env.ANTHROPIC_MODEL = model;
18+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model;
19+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = model;
20+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = model;
21+
env.CLAUDE_CODE_SUBAGENT_MODEL = model;
22+
settings.env = env;
23+
writeJsonAtomic(settingsPath, settings);
24+
25+
// .claude.json — ensure hasCompletedOnboarding
26+
backup(onboardingPath);
27+
const onboarding = readJson(onboardingPath);
28+
onboarding.hasCompletedOnboarding = true;
29+
writeJsonAtomic(onboardingPath, onboarding);
30+
31+
return {
32+
paths: [settingsPath, onboardingPath],
33+
nextStep: "Run `claude` to start using Claude Code with DashScope.",
34+
};
35+
},
36+
} satisfies AgentDef;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { mkdirSync, writeFileSync, renameSync } from "fs";
4+
import { backup, type AgentDef } from "./utils.ts";
5+
6+
export default {
7+
label: "Codex",
8+
write({ baseUrl, apiKey, model }) {
9+
const configPath = join(homedir(), ".codex", "config.toml");
10+
11+
backup(configPath);
12+
13+
const toml = [
14+
`model_provider = "Model_Studio"`,
15+
`model = "${model}"`,
16+
``,
17+
`[model_providers.Model_Studio]`,
18+
`name = "Model_Studio"`,
19+
`base_url = "${baseUrl}"`,
20+
`env_key = "OPENAI_API_KEY"`,
21+
`wire_api = "responses"`,
22+
``,
23+
].join("\n");
24+
25+
mkdirSync(join(configPath, ".."), { recursive: true });
26+
const tmp = configPath + ".tmp";
27+
writeFileSync(tmp, toml, { mode: 0o600 });
28+
renameSync(tmp, configPath);
29+
30+
// Also hint about OPENAI_API_KEY env var
31+
const shell = process.platform === "win32" ? "powershell" : "shell";
32+
const envHint =
33+
shell === "powershell"
34+
? `Set env: [Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "${apiKey}", "User")`
35+
: `Set env: export OPENAI_API_KEY="${apiKey}"`;
36+
37+
return {
38+
paths: [configPath],
39+
nextStep: `${envHint}\n Then run \`codex\` to start using Codex with DashScope.`,
40+
};
41+
},
42+
} satisfies AgentDef;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
4+
import yaml from "yaml";
5+
import { backup, type AgentDef } from "./utils.ts";
6+
7+
export default {
8+
label: "Hermes Agent",
9+
write({ baseUrl, apiKey, model }) {
10+
const configPath = join(homedir(), ".hermes", "config.yaml");
11+
12+
backup(configPath);
13+
14+
let config: Record<string, unknown> = {};
15+
if (existsSync(configPath)) {
16+
try {
17+
config = (yaml.parse(readFileSync(configPath, "utf-8")) ?? {}) as Record<string, unknown>;
18+
} catch {
19+
config = {};
20+
}
21+
}
22+
23+
config.model = {
24+
default: model,
25+
provider: "custom",
26+
base_url: baseUrl,
27+
api_mode: "anthropic_messages",
28+
api_key: apiKey,
29+
};
30+
31+
mkdirSync(join(configPath, ".."), { recursive: true });
32+
const tmp = configPath + ".tmp";
33+
writeFileSync(tmp, yaml.stringify(config), { mode: 0o600 });
34+
renameSync(tmp, configPath);
35+
36+
return {
37+
paths: [configPath],
38+
nextStep: 'Run `hermes chat -q "hello"` to verify.',
39+
};
40+
},
41+
} satisfies AgentDef;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts";
4+
5+
export default {
6+
label: "OpenClaw",
7+
write({ baseUrl, apiKey, model }) {
8+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
9+
10+
backup(configPath);
11+
const config = readJson(configPath);
12+
13+
// models.providers.bailian
14+
const models = (config.models ?? {}) as Record<string, unknown>;
15+
models.mode = "merge";
16+
const providers = (models.providers ?? {}) as Record<string, unknown>;
17+
providers.bailian = {
18+
baseUrl,
19+
apiKey,
20+
api: "anthropic-messages",
21+
models: [
22+
{
23+
id: model,
24+
name: model,
25+
reasoning: false,
26+
input: ["text", "image"],
27+
contextWindow: 1000000,
28+
maxTokens: 65536,
29+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
30+
},
31+
],
32+
};
33+
models.providers = providers;
34+
config.models = models;
35+
36+
// agents.defaults
37+
const agents = (config.agents ?? {}) as Record<string, unknown>;
38+
const defaults = (agents.defaults ?? {}) as Record<string, unknown>;
39+
defaults.model = { primary: `bailian/${model}` };
40+
agents.defaults = defaults;
41+
config.agents = agents;
42+
43+
writeJsonAtomic(configPath, config);
44+
45+
return {
46+
paths: [configPath],
47+
nextStep: "Run `openclaw` to start using OpenClaw with DashScope.",
48+
};
49+
},
50+
} satisfies AgentDef;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts";
4+
5+
export default {
6+
label: "OpenCode",
7+
write({ baseUrl, apiKey, model }) {
8+
const configPath = join(homedir(), ".config", "opencode", "opencode.json");
9+
10+
backup(configPath);
11+
const config = readJson(configPath);
12+
13+
if (!config.$schema) config.$schema = "https://opencode.ai/config.json";
14+
15+
const provider = (config.provider ?? {}) as Record<string, unknown>;
16+
provider.bailian = {
17+
npm: "@ai-sdk/anthropic",
18+
name: "Alibaba Cloud Model Studio",
19+
options: { baseURL: baseUrl, apiKey },
20+
models: { [model]: { name: model } },
21+
};
22+
config.provider = provider;
23+
24+
writeJsonAtomic(configPath, config);
25+
26+
return {
27+
paths: [configPath],
28+
nextStep: "Run `opencode` then type `/models` to select your model.",
29+
};
30+
},
31+
} satisfies AgentDef;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { backup, readJson, writeJsonAtomic, type AgentDef } from "./utils.ts";
4+
5+
export default {
6+
label: "Qwen Code",
7+
write({ baseUrl, apiKey, model }) {
8+
const settingsPath = join(homedir(), ".qwen", "settings.json");
9+
10+
backup(settingsPath);
11+
const settings = readJson(settingsPath);
12+
13+
// env
14+
const env = (settings.env ?? {}) as Record<string, string>;
15+
env.BAILIAN_API_KEY = apiKey;
16+
settings.env = env;
17+
18+
// modelProviders.openai — append or update
19+
const providers = (settings.modelProviders ?? {}) as Record<string, unknown[]>;
20+
const openaiModels = (providers.openai ?? []) as Array<Record<string, unknown>>;
21+
const existing = openaiModels.find((m) => m.id === model);
22+
if (existing) {
23+
existing.baseUrl = baseUrl;
24+
existing.envKey = "BAILIAN_API_KEY";
25+
existing.name = `[Bailian] ${model}`;
26+
} else {
27+
openaiModels.push({
28+
id: model,
29+
name: `[Bailian] ${model}`,
30+
baseUrl,
31+
envKey: "BAILIAN_API_KEY",
32+
});
33+
}
34+
providers.openai = openaiModels;
35+
settings.modelProviders = providers;
36+
37+
// security & model & version
38+
settings.security = { auth: { selectedType: "openai" } };
39+
settings.model = { name: model };
40+
settings.$version = 3;
41+
42+
writeJsonAtomic(settingsPath, settings);
43+
44+
return {
45+
paths: [settingsPath],
46+
nextStep: "Run `qwen` to start using Qwen Code with DashScope.",
47+
};
48+
},
49+
} satisfies AgentDef;

0 commit comments

Comments
 (0)