Skip to content

Commit 12ecac4

Browse files
Merge pull request #107 from modelstudioai/feat/config-agent
feat: add `bl config agent` command for one-click coding agent configuration
2 parents 52f6e26 + ba062c1 commit 12ecac4

19 files changed

Lines changed: 873 additions & 9 deletions

File tree

packages/cli/src/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
configList,
2020
configUse,
2121
configUi,
22+
configAgent,
2223
update,
2324
appCall,
2425
appList,
@@ -115,6 +116,7 @@ export const commands: Record<string, AnyCommand> = {
115116
"config list": configList,
116117
"config use": configUse,
117118
"config ui": configUi,
119+
"config agent": configAgent,
118120
update,
119121
"app call": appCall,
120122
"app list": appList,

packages/commands/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"bailian-cli-runtime": "workspace:*",
4545
"boxen": "catalog:",
4646
"chalk": "catalog:",
47+
"smol-toml": "catalog:",
4748
"yaml": "catalog:"
4849
},
4950
"devDependencies": {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { platform } from "os";
2+
import { defineCommand, detectOutputFormat, maskToken, type FlagsDef } from "bailian-cli-core";
3+
import { emitResult, emitBare } from "bailian-cli-runtime";
4+
import { AGENTS, VALID_AGENT_NAMES, type WriteParams } from "./writers.ts";
5+
6+
const FLAGS = {
7+
agent: {
8+
type: "string",
9+
valueHint: "<name>",
10+
description: `Target agent: ${VALID_AGENT_NAMES.join(", ")}`,
11+
required: true,
12+
choices: VALID_AGENT_NAMES,
13+
},
14+
baseUrl: { type: "string", valueHint: "<url>", description: "API base URL", required: true },
15+
apiKey: { type: "string", valueHint: "<key>", description: "API key", required: true },
16+
model: {
17+
type: "string",
18+
valueHint: "<model>",
19+
description: "Default model name",
20+
required: true,
21+
},
22+
} satisfies FlagsDef;
23+
24+
export default defineCommand({
25+
description: "Configure a coding agent to use DashScope API",
26+
auth: "none",
27+
usageArgs: "--agent <name> --base-url <url> --api-key <key> --model <model>",
28+
flags: FLAGS,
29+
exampleArgs: [
30+
"--agent claude-code --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3-max",
31+
"--agent qwen-code --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus",
32+
"--agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus",
33+
],
34+
async run(ctx) {
35+
const { settings, flags } = ctx;
36+
const agentName = flags.agent;
37+
const { baseUrl, apiKey, model } = flags;
38+
const agentDef = AGENTS[agentName];
39+
const format = detectOutputFormat(settings.output);
40+
41+
// Hermes has no native Windows support.
42+
if (agentName === "hermes" && platform() === "win32") {
43+
process.stderr.write(
44+
"Warning: Hermes Agent does not support native Windows. Please use WSL2.\n",
45+
);
46+
}
47+
48+
if (settings.dryRun) {
49+
emitResult(
50+
{
51+
agent: agentName,
52+
label: agentDef.label,
53+
base_url: baseUrl,
54+
api_key: maskToken(apiKey),
55+
model,
56+
},
57+
format,
58+
);
59+
return;
60+
}
61+
62+
const params: WriteParams = { baseUrl, apiKey, model };
63+
const summary = agentDef.write(params);
64+
65+
if (!settings.quiet) {
66+
emitBare(`${agentDef.label} configured successfully.`);
67+
for (const path of summary.paths) emitBare(` Written: ${path}`);
68+
emitBare(` ${summary.nextStep}`);
69+
}
70+
},
71+
});
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) as [string, ...string[]];
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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. Base URL + auth token connect Claude Code to
12+
// the endpoint; the model tier vars force every tier onto the chosen model.
13+
backup(settingsPath);
14+
const settings = readJson(settingsPath);
15+
const env = (settings.env ?? {}) as Record<string, string>;
16+
env.ANTHROPIC_BASE_URL = baseUrl;
17+
env.ANTHROPIC_AUTH_TOKEN = apiKey;
18+
env.ANTHROPIC_MODEL = model;
19+
env.ANTHROPIC_DEFAULT_HAIKU_MODEL = model;
20+
env.ANTHROPIC_DEFAULT_SONNET_MODEL = model;
21+
env.ANTHROPIC_DEFAULT_OPUS_MODEL = model;
22+
env.CLAUDE_CODE_SUBAGENT_MODEL = model;
23+
settings.env = env;
24+
writeJsonAtomic(settingsPath, settings);
25+
26+
// .claude.json — skip the onboarding prompt on first launch.
27+
backup(onboardingPath);
28+
const onboarding = readJson(onboardingPath);
29+
onboarding.hasCompletedOnboarding = true;
30+
writeJsonAtomic(onboardingPath, onboarding);
31+
32+
return {
33+
paths: [settingsPath, onboardingPath],
34+
nextStep: "Run `claude` to start using Claude Code with DashScope.",
35+
};
36+
},
37+
} satisfies AgentDef;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { existsSync, readFileSync } from "fs";
4+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
5+
import { backup, readJson, writeJsonAtomic, writeTextAtomic, type AgentDef } from "./utils.ts";
6+
7+
const PROVIDER_KEY = "bailian-cli";
8+
9+
export default {
10+
label: "Codex",
11+
write({ baseUrl, apiKey, model }) {
12+
const configPath = join(homedir(), ".codex", "config.toml");
13+
14+
// config.toml — merge into existing config so unrelated settings
15+
// (mcp_servers, approval_policy, other providers, ...) are preserved.
16+
backup(configPath);
17+
let config: Record<string, unknown> = {};
18+
if (existsSync(configPath)) {
19+
try {
20+
config = parseToml(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
21+
} catch {
22+
config = {};
23+
}
24+
}
25+
26+
config.model_provider = PROVIDER_KEY;
27+
config.model = model;
28+
config.model_reasoning_effort = "high";
29+
config.disable_response_storage = true;
30+
31+
const providers = (config.model_providers ?? {}) as Record<string, unknown>;
32+
const existing = (providers[PROVIDER_KEY] ?? {}) as Record<string, unknown>;
33+
providers[PROVIDER_KEY] = {
34+
...existing,
35+
name: PROVIDER_KEY,
36+
base_url: baseUrl,
37+
wire_api: "responses",
38+
requires_openai_auth: true,
39+
};
40+
config.model_providers = providers;
41+
42+
writeTextAtomic(configPath, stringifyToml(config) + "\n");
43+
44+
// auth.json — Codex reads OPENAI_API_KEY from here.
45+
const authPath = join(homedir(), ".codex", "auth.json");
46+
backup(authPath);
47+
const auth = readJson(authPath);
48+
auth.OPENAI_API_KEY = apiKey;
49+
writeJsonAtomic(authPath, auth);
50+
51+
return {
52+
paths: [configPath, authPath],
53+
nextStep: "Run `codex` to start using Codex with DashScope.",
54+
};
55+
},
56+
} satisfies AgentDef;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { existsSync, readFileSync } from "fs";
4+
import yaml from "yaml";
5+
import { backup, writeTextAtomic, isAnthropicEndpoint, type AgentDef } from "./utils.ts";
6+
7+
const PROVIDER_NAME = "bailian-cli";
8+
9+
export default {
10+
label: "Hermes Agent",
11+
write({ baseUrl, apiKey, model }) {
12+
const configPath = join(homedir(), ".hermes", "config.yaml");
13+
14+
backup(configPath);
15+
16+
let config: Record<string, unknown> = {};
17+
if (existsSync(configPath)) {
18+
try {
19+
config = (yaml.parse(readFileSync(configPath, "utf-8")) ?? {}) as Record<string, unknown>;
20+
} catch {
21+
config = {};
22+
}
23+
}
24+
25+
const apiMode = isAnthropicEndpoint(baseUrl) ? "anthropic_messages" : "chat_completions";
26+
const providerEntry = {
27+
name: PROVIDER_NAME,
28+
base_url: baseUrl,
29+
api_key: apiKey,
30+
api_mode: apiMode,
31+
models: [{ id: model, name: model }],
32+
};
33+
34+
// custom_providers — upsert the bailian-cli entry by name.
35+
const providers = Array.isArray(config.custom_providers)
36+
? (config.custom_providers as Array<Record<string, unknown>>)
37+
: [];
38+
const index = providers.findIndex((entry) => entry.name === PROVIDER_NAME);
39+
if (index >= 0) providers[index] = providerEntry;
40+
else providers.push(providerEntry);
41+
config.custom_providers = providers;
42+
43+
// model — select the bailian-cli provider and default model.
44+
config.model = { default: model, provider: PROVIDER_NAME };
45+
46+
writeTextAtomic(configPath, yaml.stringify(config));
47+
48+
return {
49+
paths: [configPath],
50+
nextStep: 'Run `hermes chat -q "hello"` to verify.',
51+
};
52+
},
53+
} satisfies AgentDef;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { backup, readJson, writeJsonAtomic, isAnthropicEndpoint, 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-cli"]
14+
const models = (config.models ?? {}) as Record<string, unknown>;
15+
models.mode = "merge";
16+
const providers = (models.providers ?? {}) as Record<string, unknown>;
17+
const api = isAnthropicEndpoint(baseUrl) ? "anthropic-messages" : "openai-completions";
18+
providers["bailian-cli"] = {
19+
baseUrl,
20+
apiKey,
21+
api,
22+
models: [
23+
{
24+
id: model,
25+
name: model,
26+
contextWindow: 1000000,
27+
cost: { input: 0, output: 0 },
28+
},
29+
],
30+
};
31+
models.providers = providers;
32+
config.models = models;
33+
34+
// agents.defaults
35+
const agents = (config.agents ?? {}) as Record<string, unknown>;
36+
const defaults = (agents.defaults ?? {}) as Record<string, unknown>;
37+
defaults.model = { primary: `bailian-cli/${model}` };
38+
agents.defaults = defaults;
39+
config.agents = agents;
40+
41+
writeJsonAtomic(configPath, config);
42+
43+
return {
44+
paths: [configPath],
45+
nextStep: "Run `openclaw` to start using OpenClaw with DashScope.",
46+
};
47+
},
48+
} satisfies AgentDef;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { homedir } from "os";
2+
import { join } from "path";
3+
import { backup, readJson, writeJsonAtomic, isAnthropicEndpoint, 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+
const npm = isAnthropicEndpoint(baseUrl) ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
17+
provider["bailian-cli"] = {
18+
npm,
19+
name: "Alibaba Cloud Model Studio",
20+
options: { baseURL: baseUrl, apiKey, setCacheKey: true },
21+
models: { [model]: { name: model } },
22+
};
23+
config.provider = provider;
24+
25+
writeJsonAtomic(configPath, config);
26+
27+
return {
28+
paths: [configPath],
29+
nextStep: "Run `opencode` then type `/models` to select your model.",
30+
};
31+
},
32+
} satisfies AgentDef;

0 commit comments

Comments
 (0)