Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/cli/src/commands/agent/setup.ts
Original file line number Diff line number Diff line change
@@ -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 <name> --base-url <url> --api-key <key> --model <model>",
options: [
{
flag: "--agent <name>",
description: `Target agent: ${VALID_AGENT_NAMES.join(", ")}`,
},
{ flag: "--base-url <url>", description: "API base URL" },
{ flag: "--api-key <key>", description: "API key" },
{ flag: "--model <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 <name> --base-url <url> --api-key <key> --model <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`);
},
});
20 changes: 20 additions & 0 deletions packages/cli/src/commands/agent/writers.ts
Original file line number Diff line number Diff line change
@@ -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<string, AgentDef> = {
"claude-code": claudeCode,
"qwen-code": qwenCode,
opencode,
openclaw,
hermes,
codex,
};

export const VALID_AGENT_NAMES = Object.keys(AGENTS);
36 changes: 36 additions & 0 deletions packages/cli/src/commands/agent/writers/claude-code.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
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;
42 changes: 42 additions & 0 deletions packages/cli/src/commands/agent/writers/codex.ts
Original file line number Diff line number Diff line change
@@ -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;
42 changes: 42 additions & 0 deletions packages/cli/src/commands/agent/writers/hermes.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {};
if (existsSync(configPath)) {
try {
config = (yaml.parse(readFileSync(configPath, "utf-8")) ?? {}) as Record<string, unknown>;
} 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;
51 changes: 51 additions & 0 deletions packages/cli/src/commands/agent/writers/openclaw.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
models.mode = "merge";
const providers = (models.providers ?? {}) as Record<string, unknown>;
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<string, unknown>;
const defaults = (agents.defaults ?? {}) as Record<string, unknown>;
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;
32 changes: 32 additions & 0 deletions packages/cli/src/commands/agent/writers/opencode.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
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;
49 changes: 49 additions & 0 deletions packages/cli/src/commands/agent/writers/qwen-code.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
env.BAILIAN_API_KEY = apiKey;
settings.env = env;

// modelProviders.openai — append or update
const providers = (settings.modelProviders ?? {}) as Record<string, unknown[]>;
const openaiModels = (providers.openai ?? []) as Array<Record<string, unknown>>;
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;
Loading