From 51048cdd9f14b5397aa6eb2ac51d474bdddfda03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E9=BA=92?= Date: Wed, 14 Oct 2026 11:50:38 +0800 Subject: [PATCH] feat: add Pi support for agent configuration and MCP registration - Add tests for Pi configuration, credential redaction, and MCP registration/removal - Reuse existing MCP configuration management and file-writing utilities - Preserve compatibility with existing Pi configuration files and ensure endpoint updates take effect --- README.md | 2 +- README.zh.md | 2 +- .../src/commands/config/agent/index.ts | 1 + .../src/commands/config/agent/pi-home.ts | 13 + .../src/commands/config/agent/writers.ts | 2 + .../src/commands/config/agent/writers/pi.ts | 117 +++++++++ .../commands/config/agent/writers/utils.ts | 4 +- .../commands/src/commands/mcp/agent-config.ts | 7 + .../commands/tests/e2e/pi-agent.e2e.test.ts | 143 +++++++++++ .../commands/tests/pi-agent-config.test.ts | 243 ++++++++++++++++++ skills/bailian-cli/SKILL.md | 64 ++--- skills/bailian-cli/reference/config.md | 20 +- skills/bailian-cli/reference/mcp.md | 22 +- 13 files changed, 584 insertions(+), 56 deletions(-) create mode 100644 packages/commands/src/commands/config/agent/pi-home.ts create mode 100644 packages/commands/src/commands/config/agent/writers/pi.ts create mode 100644 packages/commands/tests/e2e/pi-agent.e2e.test.ts create mode 100644 packages/commands/tests/pi-agent-config.test.ts diff --git a/README.md b/README.md index 492cafd60..62ca1235d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ _Built for AI Agents. Every command works as a structured tool call._ - **Model generation** — Full-modality generation across text, image, video, and speech, with editing and reference-based generation - **Asset understanding** — Parse and ask questions about images, documents, audio, and long videos - **App orchestration** — Call Managed Agents, agents, and workflows published on Aliyun Model Studio, wired to knowledge bases, memory, web search, and MCP tools -- **Native MCP setup** — Register Bailian MCP servers in Codex, Claude Code, Cursor, Qoder, Qoder Work, QwenWork (千问办公), Qwen Code, or Gemini CLI with channel attribution preserved +- **Native MCP setup** — Register Bailian MCP servers in Codex, Claude Code, Cursor, Qoder, Qoder Work, QwenWork (千问办公), Qwen Code, Gemini CLI, or Pi with channel attribution preserved - **Training & deployment** — Validate and upload datasets, fine-tune models, deploy dedicated models as endpoints - **Account operations** — Login, UI-based configuration, model marketplace, usage and quota, rate-limit increases, team seat management - **Plan onboarding** — Connect subscription plans such as Token Plan to the CLI and common coding agents in one step diff --git a/README.zh.md b/README.zh.md index 27cb0cfe9..559589a4d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -25,7 +25,7 @@ _专为 AI Agent 打造,每个命令均可作为结构化工具调用。_ - **模型生成** — 文本、图像、视频、语音全模态生成,支持编辑与参考生成 - **素材理解** — 图像、文档、音频、长视频的解析与问答 - **应用编排** — 调用百炼已发布的 Managed Agent、智能体和工作流,接入知识库、记忆库、联网搜索与 MCP 工具 -- **原生 MCP 接入** — 将百炼 MCP 服务注册到 Codex、Claude Code、Cursor、Qoder、Qoder Work、千问办公(QwenWork)、Qwen Code 或 Gemini CLI,并保留渠道归因 +- **原生 MCP 接入** — 将百炼 MCP 服务注册到 Codex、Claude Code、Cursor、Qoder、Qoder Work、千问办公(QwenWork)、Qwen Code、Gemini CLI 或 Pi,并保留渠道归因 - **模型训推** — 数据集校验上传、模型精调、专属模型部署上线 - **账号运维** — 授权登录、界面化配置、模型市场、用量与额度、限流提额、团队席位管理 - **套餐接入** — 支持 Token Plan 等订阅计划一键接到 CLI 和常见 Coding Agent diff --git a/packages/commands/src/commands/config/agent/index.ts b/packages/commands/src/commands/config/agent/index.ts index 581f3cbec..c2e257028 100644 --- a/packages/commands/src/commands/config/agent/index.ts +++ b/packages/commands/src/commands/config/agent/index.ts @@ -132,6 +132,7 @@ export default defineCommand({ model, contextWindow, wireApi, + localize: ctx.localize, }; const summary = agentDef.write(params); diff --git a/packages/commands/src/commands/config/agent/pi-home.ts b/packages/commands/src/commands/config/agent/pi-home.ts new file mode 100644 index 000000000..2b3993f41 --- /dev/null +++ b/packages/commands/src/commands/config/agent/pi-home.ts @@ -0,0 +1,13 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; + +/** Pi's official config directory override, shared by model and MCP writers. */ +export function getPiAgentDir(home: string = homedir()): string { + const configured = process.env.PI_CODING_AGENT_DIR; + if (!configured) return join(home, ".pi", "agent"); + if (configured === "~") return home; + if (configured.startsWith("~/") || configured.startsWith("~\\")) { + return join(home, configured.slice(2)); + } + return configured; +} diff --git a/packages/commands/src/commands/config/agent/writers.ts b/packages/commands/src/commands/config/agent/writers.ts index 68aa95aad..e72a3c3eb 100644 --- a/packages/commands/src/commands/config/agent/writers.ts +++ b/packages/commands/src/commands/config/agent/writers.ts @@ -7,6 +7,7 @@ import opencode from "./writers/opencode.ts"; import openclaw from "./writers/openclaw.ts"; import hermes from "./writers/hermes.ts"; import codex from "./writers/codex.ts"; +import pi from "./writers/pi.ts"; export const AGENTS: Record = { "claude-code": claudeCode, @@ -15,6 +16,7 @@ export const AGENTS: Record = { openclaw, hermes, codex, + pi, }; export const VALID_AGENT_NAMES = Object.keys(AGENTS) as [string, ...string[]]; diff --git a/packages/commands/src/commands/config/agent/writers/pi.ts b/packages/commands/src/commands/config/agent/writers/pi.ts new file mode 100644 index 000000000..d09b78c0c --- /dev/null +++ b/packages/commands/src/commands/config/agent/writers/pi.ts @@ -0,0 +1,117 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { BailianError, ExitCode, type LocalizedText } from "bailian-cli-core"; +import { getPiAgentDir } from "../pi-home.ts"; +import { + backup, + isAnthropicEndpoint, + stripJsonc, + writeJsonAtomic, + type AgentDef, +} from "./utils.ts"; + +const PROVIDER_ID = "bailian-cli"; + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export default { + label: "Pi", + write({ + baseUrl, + apiKey, + model, + localize = (text: LocalizedText) => (typeof text === "string" ? text : text["en-US"]), + }) { + const agentDir = getPiAgentDir(); + const paths = ["models.json", "auth.json", "settings.json"].map((filename) => + join(agentDir, filename), + ); + const originals = paths.map((path) => + existsSync(path) ? readFileSync(path, "utf8") : undefined, + ); + // Read every file before writing any of them. Malformed user config must + // fail closed rather than silently discarding providers or credentials. + const configs = paths.map((path, index) => { + try { + const original = originals[index]; + const content = original?.replace(/^\uFEFF/, ""); + const parsed: unknown = + content === undefined ? {} : JSON.parse(index === 0 ? stripJsonc(content) : content); + if (!isObject(parsed)) throw new Error("root value is not an object"); + return parsed; + } catch { + throw new BailianError( + localize({ + "en-US": `Cannot update Pi because ${path} is invalid; no configuration was changed.`, + "zh-CN": `无法更新 Pi:${path} 格式无效,配置未被修改。`, + }), + ExitCode.GENERAL, + ); + } + }); + const [modelsConfig, authConfig, settingsConfig] = configs; + if (modelsConfig.providers !== undefined && !isObject(modelsConfig.providers)) { + throw new BailianError( + localize({ + "en-US": + "Cannot update Pi because models.json providers is not an object; no configuration was changed.", + "zh-CN": "无法更新 Pi:models.json 中的 providers 必须为对象,配置未被修改。", + }), + ExitCode.GENERAL, + ); + } + const providers = (modelsConfig.providers ?? {}) as Record; + const existing = providers[PROVIDER_ID]; + if ( + existing !== undefined && + (!isObject(existing) || (existing.models !== undefined && !Array.isArray(existing.models))) + ) { + throw new BailianError( + localize({ + "en-US": + "Cannot update Pi because the bailian-cli provider is invalid; no configuration was changed.", + "zh-CN": "无法更新 Pi:bailian-cli provider 格式无效,配置未被修改。", + }), + ExitCode.GENERAL, + ); + } + const provider = (existing ?? {}) as Record; + const models = (provider.models ?? []) as Record[]; + const existingModel = models.find((entry) => entry.id === model); + const updatedModel: Record = { + ...existingModel, + id: model, + name: existingModel?.name ?? model, + }; + // Pi gives model-level connection fields precedence over provider defaults. + // Reconfiguration must use the newly selected endpoint and protocol. + delete updatedModel.baseUrl; + delete updatedModel.api; + provider.models = existingModel + ? models.map((entry) => (entry.id === model ? updatedModel : entry)) + : [...models, updatedModel]; + provider.baseUrl = baseUrl; + provider.api = isAnthropicEndpoint(baseUrl) ? "anthropic-messages" : "openai-completions"; + // Authentication belongs in Pi's private credential store, not model metadata. + delete provider.apiKey; + providers[PROVIDER_ID] = provider; + modelsConfig.providers = providers; + authConfig[PROVIDER_ID] = { type: "api_key", key: apiKey }; + settingsConfig.defaultProvider = PROVIDER_ID; + settingsConfig.defaultModel = model; + + for (const [index, path] of paths.entries()) { + backup(path); + writeJsonAtomic(path, configs[index]); + } + return { + paths, + nextStep: localize({ + "en-US": "Run `pi` to start using Pi with DashScope.", + "zh-CN": "运行 `pi` 开始使用百炼模型。", + }), + }; + }, +} satisfies AgentDef; diff --git a/packages/commands/src/commands/config/agent/writers/utils.ts b/packages/commands/src/commands/config/agent/writers/utils.ts index 32f3d04b1..45e178988 100644 --- a/packages/commands/src/commands/config/agent/writers/utils.ts +++ b/packages/commands/src/commands/config/agent/writers/utils.ts @@ -1,6 +1,6 @@ import { dirname } from "path"; import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, copyFileSync } from "fs"; -import { BailianError, ExitCode } from "bailian-cli-core"; +import { BailianError, ExitCode, type LocalizedText } from "bailian-cli-core"; /** Parameters shared by every agent writer. */ export interface WriteParams { @@ -11,6 +11,8 @@ export interface WriteParams { contextWindow?: number; /** Codex provider wire protocol: "responses" or "chat". */ wireApi?: string; + /** Locale selection for new agent-specific messages. */ + localize?: (text: LocalizedText) => string; } /** What a writer reports back after configuring an agent. */ diff --git a/packages/commands/src/commands/mcp/agent-config.ts b/packages/commands/src/commands/mcp/agent-config.ts index d98793b16..e198da49e 100644 --- a/packages/commands/src/commands/mcp/agent-config.ts +++ b/packages/commands/src/commands/mcp/agent-config.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { BailianError, ExitCode } from "bailian-cli-core"; import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; import yaml from "yaml"; +import { getPiAgentDir } from "../config/agent/pi-home.ts"; import { backup, stripJsonc, @@ -25,6 +26,7 @@ export const MCP_AGENT_IDS = [ "deepseek-harness", "zcode", "workbuddy", + "pi", ] as const; const DSH_MCP_PLUGIN = "@deepseek-ai/dsh-mcp-client"; @@ -345,6 +347,11 @@ function jsonMcpAdapter(options: { } const adapters: Record = { + pi: jsonMcpAdapter({ + path: (home) => join(getPiAgentDir(home), "mcp.json"), + installed: (home) => existsSync(getPiAgentDir(home)), + supports: (transport) => transport === "streamable-http", + }), codex: { path: (home) => join(process.env.CODEX_HOME || join(home, ".codex"), "config.toml"), installed: (home) => diff --git a/packages/commands/tests/e2e/pi-agent.e2e.test.ts b/packages/commands/tests/e2e/pi-agent.e2e.test.ts new file mode 100644 index 000000000..f664f79b3 --- /dev/null +++ b/packages/commands/tests/e2e/pi-agent.e2e.test.ts @@ -0,0 +1,143 @@ +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test"; +import { parseStdoutJson, runCommandE2e, runCommandHelp } from "./helpers.ts"; +import { CONFIG_ROUTES, MCP_ROUTES } from "./topic-routes.ts"; + +const PLACEHOLDER_KEY = "sk-pi-e2e-placeholder-secret"; +let probeRoot: string; +let agentDir: string; +let probeEnv: NodeJS.ProcessEnv; + +beforeEach(() => { + probeRoot = mkdtempSync(join(tmpdir(), "bl-pi-e2e-")); + agentDir = join(probeRoot, "pi-agent"); + probeEnv = { PI_CODING_AGENT_DIR: agentDir, BAILIAN_CONFIG_DIR: join(probeRoot, "bailian") }; +}); + +afterEach(() => rmSync(probeRoot, { recursive: true, force: true })); + +const modelArgs = [ + "config", + "agent", + "--agent", + "pi", + "--region", + "ap-southeast-1", + "--api-key", + PLACEHOLDER_KEY, + "--model", + "qwen3-coder-plus", +]; +const mcpArgs = [ + "mcp", + "connect", + "--agent", + "pi", + "--server", + "TextGenerateImage", + "--transport", + "streamable-http", + "--api-key", + PLACEHOLDER_KEY, + "--output", + "json", +]; + +describe("e2e: Pi Agent integration", () => { + test("config agent and mcp connect/disconnect help expose Pi", async () => { + for (const [routes, args] of [ + [CONFIG_ROUTES, ["config", "agent", "--help"]], + [MCP_ROUTES, ["mcp", "connect", "--help"]], + [MCP_ROUTES, ["mcp", "disconnect", "--help"]], + ] as const) { + const result = await runCommandHelp(routes, [...args]); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stderr).toMatch(/\bpi\b/); + } + }); + + test("model dry-run accepts Pi, redacts its key, and leaves its directory absent", async () => { + const result = await runCommandE2e( + CONFIG_ROUTES, + [...modelArgs, "--dry-run", "--output", "json"], + probeEnv, + ); + expect(result.exitCode, result.stderr).toBe(0); + const plan = parseStdoutJson<{ + agent: string; + base_url: string; + model: string; + api_key: string; + }>(result.stdout); + expect(plan.agent).toBe("pi"); + expect(plan.base_url).toBe( + "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", + ); + expect(plan.model).toBe("qwen3-coder-plus"); + expect(plan.api_key).not.toBe(PLACEHOLDER_KEY); + expect(result.stdout + result.stderr).not.toContain(PLACEHOLDER_KEY); + expect(existsSync(agentDir)).toBe(false); + }); + + test("config agent writes Pi's native settings and private auth without disclosing credentials", async () => { + const result = await runCommandE2e(CONFIG_ROUTES, modelArgs, probeEnv); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout + result.stderr).not.toContain(PLACEHOLDER_KEY); + const settings = JSON.parse(readFileSync(join(agentDir, "settings.json"), "utf8")); + expect(settings).toMatchObject({ + defaultProvider: "bailian-cli", + defaultModel: "qwen3-coder-plus", + }); + const auth = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8")); + expect(auth["bailian-cli"]).toEqual({ type: "api_key", key: PLACEHOLDER_KEY }); + expect(readFileSync(join(agentDir, "models.json"), "utf8")).not.toContain(PLACEHOLDER_KEY); + }); + + test("MCP dry-run reports only non-secret metadata without writing configuration", async () => { + const result = await runCommandE2e(MCP_ROUTES, [...mcpArgs, "--dry-run"], probeEnv); + expect(result.exitCode, result.stderr).toBe(0); + const plan = parseStdoutJson<{ agents: string[]; endpoint: string; header_names: string[] }>( + result.stdout, + ); + expect(plan.agents).toEqual(["pi"]); + expect(plan.endpoint).toBe("https://dashscope.aliyuncs.com/api/v1/mcps/TextGenerateImage/mcp"); + expect(plan.header_names).toContain("Authorization"); + expect(result.stdout + result.stderr).not.toContain(PLACEHOLDER_KEY); + expect(existsSync(agentDir)).toBe(false); + }); + + test("MCP connect is idempotent and disconnect removes the managed entry without disclosing credentials", async () => { + const first = await runCommandE2e(MCP_ROUTES, mcpArgs, probeEnv); + expect(first.exitCode, first.stderr).toBe(0); + expect(first.stdout + first.stderr).not.toContain(PLACEHOLDER_KEY); + expect(parseStdoutJson<{ results: { status: string }[] }>(first.stdout).results[0].status).toBe( + "added", + ); + const repeated = await runCommandE2e(MCP_ROUTES, mcpArgs, probeEnv); + expect(repeated.exitCode, repeated.stderr).toBe(0); + expect( + parseStdoutJson<{ results: { status: string }[] }>(repeated.stdout).results[0].status, + ).toBe("unchanged"); + const disconnected = await runCommandE2e( + MCP_ROUTES, + ["mcp", "disconnect", "--agent", "pi", "--server", "TextGenerateImage", "--output", "json"], + probeEnv, + ); + expect(disconnected.exitCode, disconnected.stderr).toBe(0); + expect( + parseStdoutJson<{ results: { status: string }[] }>(disconnected.stdout).results[0].status, + ).toBe("removed"); + expect(JSON.parse(readFileSync(join(agentDir, "mcp.json"), "utf8")).mcpServers).toEqual({}); + }); + + test("MCP SSE uses the shared adapter rejection before any Pi file is created", async () => { + const sseArgs = mcpArgs.map((argument) => (argument === "streamable-http" ? "sse" : argument)); + const result = await runCommandE2e(MCP_ROUTES, sseArgs, probeEnv); + expect(result.exitCode).toBe(2); + expect(result.stderr).toMatch(/pi.*SSE/i); + expect(result.stdout + result.stderr).not.toContain(PLACEHOLDER_KEY); + expect(existsSync(agentDir)).toBe(false); + }); +}); diff --git a/packages/commands/tests/pi-agent-config.test.ts b/packages/commands/tests/pi-agent-config.test.ts new file mode 100644 index 000000000..f05bd013d --- /dev/null +++ b/packages/commands/tests/pi-agent-config.test.ts @@ -0,0 +1,243 @@ +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vite-plus/test"; +import { AGENTS } from "../src/commands/config/agent/writers.ts"; +import { + connectMcpAgents, + disconnectMcpAgents, + resolveMcpAgentTargets, +} from "../src/commands/mcp/agent-config.ts"; + +let probeRoot: string; +let agentDir: string; +let previousAgentDir: string | undefined; + +beforeEach(() => { + probeRoot = mkdtempSync(join(tmpdir(), "bl-pi-agent-")); + agentDir = join(probeRoot, "pi-agent"); + previousAgentDir = process.env.PI_CODING_AGENT_DIR; + process.env.PI_CODING_AGENT_DIR = agentDir; +}); + +afterEach(() => { + if (previousAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR; + else process.env.PI_CODING_AGENT_DIR = previousAgentDir; + rmSync(probeRoot, { recursive: true, force: true }); +}); + +function configurePi( + model = "qwen3-coder-plus", + baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1", +) { + expect(AGENTS.pi, "Pi must be accepted by config agent").toBeDefined(); + return AGENTS.pi.write({ baseUrl, apiKey: "sk-pi-placeholder", model }); +} + +function readConfig(filename: string): Record { + return JSON.parse(readFileSync(join(agentDir, filename), "utf8")); +} + +function connectPi(transport: "streamable-http" | "sse" = "streamable-http") { + return connectMcpAgents({ + agents: ["pi"], + home: probeRoot, + configDir: join(probeRoot, "bailian"), + cliVersion: "2.2.1", + spec: { + name: "TextGenerateImage", + serverCode: "TextGenerateImage", + transport, + endpoint: "https://dashscope.aliyuncs.com/api/v1/mcps/TextGenerateImage/mcp", + headers: { Authorization: "Bearer sk-pi-placeholder" }, + }, + }); +} + +describe("Pi model configuration", () => { + test("writes a usable provider, private authentication, and startup model to Pi's custom directory", () => { + const summary = configurePi(); + const models = readConfig("models.json"); + expect(models.providers["bailian-cli"]).toMatchObject({ + baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", + api: "openai-completions", + models: [{ id: "qwen3-coder-plus" }], + }); + expect(readFileSync(join(agentDir, "models.json"), "utf8")).not.toContain("sk-pi-placeholder"); + expect(readConfig("auth.json")["bailian-cli"]).toEqual({ + type: "api_key", + key: "sk-pi-placeholder", + }); + expect(readConfig("settings.json")).toMatchObject({ + defaultProvider: "bailian-cli", + defaultModel: "qwen3-coder-plus", + }); + expect(summary.paths).toEqual([ + join(agentDir, "models.json"), + join(agentDir, "auth.json"), + join(agentDir, "settings.json"), + ]); + if (process.platform !== "win32") + expect(statSync(join(agentDir, "auth.json")).mode & 0o777).toBe(0o600); + }); + + test("upserts models without losing other providers, credentials, or settings", () => { + mkdirSync(agentDir, { recursive: true }); + writeFileSync( + join(agentDir, "models.json"), + JSON.stringify({ providers: { other: { baseUrl: "https://example.com/v1" } } }), + ); + writeFileSync( + join(agentDir, "auth.json"), + JSON.stringify({ other: { type: "oauth", access: "placeholder" } }), + ); + writeFileSync(join(agentDir, "settings.json"), JSON.stringify({ theme: "dark" })); + configurePi(); + configurePi( + "qwen3.8-max", + "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", + ); + configurePi( + "qwen3.8-max", + "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1", + ); + const models = readConfig("models.json"); + expect(models.providers.other.baseUrl).toBe("https://example.com/v1"); + expect(models.providers["bailian-cli"].models.map((model: { id: string }) => model.id)).toEqual( + ["qwen3-coder-plus", "qwen3.8-max"], + ); + expect(readConfig("auth.json").other.access).toBe("placeholder"); + expect(readConfig("settings.json")).toMatchObject({ + theme: "dark", + defaultModel: "qwen3.8-max", + }); + }); + + test("uses Anthropic Messages for a compatible Anthropic endpoint", () => { + configurePi("qwen3.8-max", "https://dashscope.aliyuncs.com/apps/anthropic"); + expect(readConfig("models.json").providers["bailian-cli"].api).toBe("anthropic-messages"); + }); + + test("accepts native Pi comments and BOM while preserving other configuration", () => { + mkdirSync(agentDir, { recursive: true }); + writeFileSync( + join(agentDir, "models.json"), + '\uFEFF{ // native Pi accepts comments\n"providers":{"other":{"baseUrl":"https://other.example/v1"}}}', + ); + writeFileSync( + join(agentDir, "auth.json"), + '\uFEFF{"other":{"type":"api_key","key":"placeholder"}}', + ); + writeFileSync(join(agentDir, "settings.json"), '\uFEFF{"theme":"dark"}'); + configurePi(); + expect(readConfig("models.json").providers.other.baseUrl).toBe("https://other.example/v1"); + expect(readConfig("auth.json").other.key).toBe("placeholder"); + expect(readConfig("settings.json").theme).toBe("dark"); + }); + + test("reconfigures a model that previously overrode its provider endpoint and API", () => { + mkdirSync(agentDir, { recursive: true }); + writeFileSync( + join(agentDir, "models.json"), + JSON.stringify({ + providers: { + "bailian-cli": { + baseUrl: "https://old.example/v1", + api: "anthropic-messages", + models: [ + { + id: "qwen3-coder-plus", + name: "Custom name", + contextWindow: 64000, + baseUrl: "https://old-model.example/v1", + api: "anthropic-messages", + }, + ], + }, + }, + }), + ); + configurePi(); + const provider = readConfig("models.json").providers["bailian-cli"]; + expect(provider.baseUrl).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1"); + expect(provider.api).toBe("openai-completions"); + expect(provider.models[0]).toEqual({ + id: "qwen3-coder-plus", + name: "Custom name", + contextWindow: 64000, + }); + }); + + test("invalid existing configuration fails before any configuration is overwritten", () => { + mkdirSync(agentDir, { recursive: true }); + const originalModels = '{"providers":{"keep":{"models":[{"id":"keep-model"}]}}}'; + writeFileSync(join(agentDir, "models.json"), originalModels); + writeFileSync(join(agentDir, "auth.json"), "{invalid"); + expect(() => configurePi()).toThrow(/invalid|Invalid/); + expect(readFileSync(join(agentDir, "models.json"), "utf8")).toBe(originalModels); + expect(readFileSync(join(agentDir, "auth.json"), "utf8")).toBe("{invalid"); + expect(existsSync(join(agentDir, "settings.json"))).toBe(false); + }); +}); + +describe("Pi MCP registration", () => { + test("detects Pi's configured agent directory and completes an idempotent connect/disconnect cycle", () => { + mkdirSync(agentDir, { recursive: true }); + writeFileSync( + join(agentDir, "mcp.json"), + JSON.stringify({ keep: true, mcpServers: { other: { command: "other-server" } } }), + ); + expect(resolveMcpAgentTargets("all", probeRoot)).toContain("pi"); + expect(connectPi()[0]).toMatchObject({ + agent: "pi", + status: "added", + path: join(agentDir, "mcp.json"), + }); + expect(connectPi()[0].status).toBe("unchanged"); + expect(readConfig("mcp.json")).toEqual({ + keep: true, + mcpServers: { + other: { command: "other-server" }, + TextGenerateImage: { + url: "https://dashscope.aliyuncs.com/api/v1/mcps/TextGenerateImage/mcp", + headers: { Authorization: "Bearer sk-pi-placeholder" }, + }, + }, + }); + const result = disconnectMcpAgents({ + agents: ["pi"], + name: "TextGenerateImage", + home: probeRoot, + configDir: join(probeRoot, "bailian"), + }); + expect(result[0].status).toBe("removed"); + expect(readConfig("mcp.json")).toEqual({ + keep: true, + mcpServers: { other: { command: "other-server" } }, + }); + }); + + test("rejects SSE before writing any MCP configuration or manifest", () => { + expect(() => connectPi("sse")).toThrow(/pi.*SSE|SSE.*pi/i); + expect(existsSync(join(agentDir, "mcp.json"))).toBe(false); + expect(existsSync(join(probeRoot, "bailian", "mcp-registrations.json"))).toBe(false); + }); + + test("does not overwrite an unmanaged same-name MCP server", () => { + mkdirSync(agentDir, { recursive: true }); + const original = JSON.stringify({ + mcpServers: { TextGenerateImage: { url: "https://user.example/mcp" } }, + }); + writeFileSync(join(agentDir, "mcp.json"), original); + expect(() => connectPi()).toThrow(/not managed/); + expect(readFileSync(join(agentDir, "mcp.json"), "utf8")).toBe(original); + }); +}); diff --git a/skills/bailian-cli/SKILL.md b/skills/bailian-cli/SKILL.md index 6604a7e2d..1a1c57929 100644 --- a/skills/bailian-cli/SKILL.md +++ b/skills/bailian-cli/SKILL.md @@ -58,38 +58,38 @@ Do not guess flags — use the reference files or `--help`. Use this table only after the decision table in [`bailian-protocol`](../bailian-protocol/SKILL.md#provider-selection-and-consent) has routed the request to `bl` (class 4, or class 2 after the user picks Bailian). Hub-owned intents only — for media / fine-tune / agents.yaml / Sandbox, soft hand-off to the domain skill. -| User intent | Command | Notes | -| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| Explicit Bailian model chat / text execution | `bl text chat` | Default `qwen3.8-max` | -| Bailian agent / workflow | `bl app call` | Needs `--app-id` | -| Find app by name | `bl app list` then `bl app call` | Console auth | -| Bailian app memory CRUD (not host-agent memory) | `bl memory *` | [`reference/memory.md`](reference/memory.md) | -| Bailian knowledge base RAG | `bl knowledge search` / `chat` | API key + agent/workspace IDs | -| Upload a file as a step of a Bailian workflow | `bl file upload` | When you need `oss://` URL explicitly; not for generic hosting | -| Bailian model selection / recommendation | `bl advisor recommend` | Intent → candidate recall → LLM ranking | -| Bailian model catalog / pricing / params | `bl model list` | No auth (public catalog); `--model ` for detail, `--enrich` for input params | -| Find a model by keyword / modality | `bl model search` | Weighted ranking; `--input-modality` / `--output-modality`; offline models hidden by default | -| SDK sample code for calling a model | `bl model code` | `--sdk openai\|dashscope`, `--lang`; text output is the snippet alone, safe to redirect | -| Install / list / update / remove registry skills | `bl skill add` / `list` / `update` / `remove` | Bailian skill registry; see [`reference/skill.md`](reference/skill.md) | -| Bailian MCP marketplace discovery / call | `bl mcp list` / `tools` / `call` | — | -| Register or remove a Bailian MCP in an Agent | `bl mcp connect` / `disconnect` | Codex, Claude Code, Cursor, Qoder, Qoder Work, QwenWork, Qwen Code, Gemini, OpenCode, OpenClaw, DeepSeek Harness, ZCode, WorkBuddy | -| Bailian pipeline workflow (a step in a bl flow) | `bl pipeline run` / `validate` | JSON/YAML workflow definitions | -| Bailian rate limits / quota | `bl quota list` / `check` / `request` | Console auth; class 2 — ask which product first if unnamed | -| Bailian free tier / usage stats | `bl usage free` / `stats` / `freetier` | Console auth; class 2 — ask which product first if unnamed | -| Bailian Token Plan quota usage | `bl usage token-plan` | Console auth; class 2 — ask which product first if unnamed | -| Token Plan harness entitlement quota | `bl token-plan harness-quota` | Console auth; issued quota usage + still-issuing harnesses | -| Bailian Coding Plan quota usage | `bl usage coding-plan` | Console auth; class 2 — ask which product first if unnamed | -| Model monitoring (calls, latency, errors, delivery) | `bl monitor overview` / `models` / `metrics` / `errors` / `delivery *` | Console auth; see [`reference/monitor.md`](reference/monitor.md) | -| Model call logs / traces | `bl log audit *` / `bl log inference *` / `bl log status` / `bl log trace *` | Console auth; audit and inference logs have separate APIs and switches; see [`reference/log.md`](reference/log.md) | -| Model alert rules / templates / history | `bl alert list` / `create` / `update` / `delete` / `enable` / `disable` / `history` / `metrics` / `template *` | Console auth; contacts are managed in the CloudMonitor console; see [`reference/alert.md`](reference/alert.md) | -| Console API (advanced) | `bl console call` | Console auth | -| Bailian workspace listing | `bl workspace list` | Console auth | -| Switch CLI Help / Quick Start language | `bl config set --key language --value zh-CN` | Use `en-US` to switch back; follows the active config profile | -| Image / video / speech / omni / vision | → skill `bailian-gen` | Fallback: `bl image\|video\|speech\|omni\|vision --help` | -| Dataset / fine-tune / deploy | → skill `bailian-finetune` | Fallback: `bl dataset\|finetune\|deploy --help` | -| agents.yaml IaC / managed-agent sessions | → skill `bailian-managed-agent` | Fallback: `bl managed-agent --help`; `apply`/`destroy` need `--yes` after `plan` | -| Bailian Sandbox instance / template lifecycle | → skill `bailian-sandbox` | Fallback: `bl sandbox --help` | -| Web search (model-aware routing) | → skill `bailian-web-search` | Token Plan vs MCP path + fallback; fallback: `bl search web --help` | +| User intent | Command | Notes | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| Explicit Bailian model chat / text execution | `bl text chat` | Default `qwen3.8-max` | +| Bailian agent / workflow | `bl app call` | Needs `--app-id` | +| Find app by name | `bl app list` then `bl app call` | Console auth | +| Bailian app memory CRUD (not host-agent memory) | `bl memory *` | [`reference/memory.md`](reference/memory.md) | +| Bailian knowledge base RAG | `bl knowledge search` / `chat` | API key + agent/workspace IDs | +| Upload a file as a step of a Bailian workflow | `bl file upload` | When you need `oss://` URL explicitly; not for generic hosting | +| Bailian model selection / recommendation | `bl advisor recommend` | Intent → candidate recall → LLM ranking | +| Bailian model catalog / pricing / params | `bl model list` | No auth (public catalog); `--model ` for detail, `--enrich` for input params | +| Find a model by keyword / modality | `bl model search` | Weighted ranking; `--input-modality` / `--output-modality`; offline models hidden by default | +| SDK sample code for calling a model | `bl model code` | `--sdk openai\|dashscope`, `--lang`; text output is the snippet alone, safe to redirect | +| Install / list / update / remove registry skills | `bl skill add` / `list` / `update` / `remove` | Bailian skill registry; see [`reference/skill.md`](reference/skill.md) | +| Bailian MCP marketplace discovery / call | `bl mcp list` / `tools` / `call` | — | +| Register or remove a Bailian MCP in an Agent | `bl mcp connect` / `disconnect` | Codex, Claude Code, Cursor, Qoder, Qoder Work, QwenWork, Qwen Code, Gemini, OpenCode, OpenClaw, DeepSeek Harness, ZCode, WorkBuddy, Pi | +| Bailian pipeline workflow (a step in a bl flow) | `bl pipeline run` / `validate` | JSON/YAML workflow definitions | +| Bailian rate limits / quota | `bl quota list` / `check` / `request` | Console auth; class 2 — ask which product first if unnamed | +| Bailian free tier / usage stats | `bl usage free` / `stats` / `freetier` | Console auth; class 2 — ask which product first if unnamed | +| Bailian Token Plan quota usage | `bl usage token-plan` | Console auth; class 2 — ask which product first if unnamed | +| Token Plan harness entitlement quota | `bl token-plan harness-quota` | Console auth; issued quota usage + still-issuing harnesses | +| Bailian Coding Plan quota usage | `bl usage coding-plan` | Console auth; class 2 — ask which product first if unnamed | +| Model monitoring (calls, latency, errors, delivery) | `bl monitor overview` / `models` / `metrics` / `errors` / `delivery *` | Console auth; see [`reference/monitor.md`](reference/monitor.md) | +| Model call logs / traces | `bl log audit *` / `bl log inference *` / `bl log status` / `bl log trace *` | Console auth; audit and inference logs have separate APIs and switches; see [`reference/log.md`](reference/log.md) | +| Model alert rules / templates / history | `bl alert list` / `create` / `update` / `delete` / `enable` / `disable` / `history` / `metrics` / `template *` | Console auth; contacts are managed in the CloudMonitor console; see [`reference/alert.md`](reference/alert.md) | +| Console API (advanced) | `bl console call` | Console auth | +| Bailian workspace listing | `bl workspace list` | Console auth | +| Switch CLI Help / Quick Start language | `bl config set --key language --value zh-CN` | Use `en-US` to switch back; follows the active config profile | +| Image / video / speech / omni / vision | → skill `bailian-gen` | Fallback: `bl image\|video\|speech\|omni\|vision --help` | +| Dataset / fine-tune / deploy | → skill `bailian-finetune` | Fallback: `bl dataset\|finetune\|deploy --help` | +| agents.yaml IaC / managed-agent sessions | → skill `bailian-managed-agent` | Fallback: `bl managed-agent --help`; `apply`/`destroy` need `--yes` after `plan` | +| Bailian Sandbox instance / template lifecycle | → skill `bailian-sandbox` | Fallback: `bl sandbox --help` | +| Web search (model-aware routing) | → skill `bailian-web-search` | Token Plan vs MCP path + fallback; fallback: `bl search web --help` | Flags, usage, and examples: see hub [`reference/`](reference/index.md) or `bl --help` — do not guess flags. Domain command details live in the owning skill's `reference/`. diff --git a/skills/bailian-cli/reference/config.md b/skills/bailian-cli/reference/config.md index 9e3cc9b83..9d47f4745 100644 --- a/skills/bailian-cli/reference/config.md +++ b/skills/bailian-cli/reference/config.md @@ -29,16 +29,16 @@ Index: [index.md](index.md) #### Flags -| Flag | Type | Required | Description | -| --------------------------------------------------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | -| `--agent ` | string | yes | Target agent: claude-code, qwen-code, opencode, openclaw, hermes, codex | -| `--base-url ` | string | no | API base URL | -| `--region ` | string | no | Model Studio region (e.g. cn-beijing, ap-southeast-1); converted into --base-url. Token Plan only | -| `--api-key ` | string | no | API key | -| `--key ` | string | no | Obfuscated API key from the web console (starts with "o1_"); decoded into --api-key | -| `--model ` | string | yes | Default model name | -| `--context-window ` | number | no | OpenClaw only: model context window in tokens (default: 256000) | -| `--wire-api ` | string | no | Codex only: wire protocol (default: responses). "chat" only works with legacy Codex <= 0.80.0 | +| Flag | Type | Required | Description | +| ------------------------------------------------------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------- | +| `--agent ` | string | yes | Target agent: claude-code, qwen-code, opencode, openclaw, hermes, codex, pi | +| `--base-url ` | string | no | API base URL | +| `--region ` | string | no | Model Studio region (e.g. cn-beijing, ap-southeast-1); converted into --base-url. Token Plan only | +| `--api-key ` | string | no | API key | +| `--key ` | string | no | Obfuscated API key from the web console (starts with "o1_"); decoded into --api-key | +| `--model ` | string | yes | Default model name | +| `--context-window ` | number | no | OpenClaw only: model context window in tokens (default: 256000) | +| `--wire-api ` | string | no | Codex only: wire protocol (default: responses). "chat" only works with legacy Codex <= 0.80.0 | #### Examples diff --git a/skills/bailian-cli/reference/mcp.md b/skills/bailian-cli/reference/mcp.md index 29004f931..7cb2b4350 100644 --- a/skills/bailian-cli/reference/mcp.md +++ b/skills/bailian-cli/reference/mcp.md @@ -63,13 +63,13 @@ bl mcp call --target market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 #### Flags -| Flag | Type | Required | Description | -| -------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--server ` | string | yes | Bailian MCP Server Code, such as TextGenerateImage | -| `--transport ` | string | yes | MCP transport exposed by the server: streamable-http or sse | -| `--agent ` | string | yes | Target Agent: codex, claude-code, cursor, qoder, qoderwork, qwenwork, qwen-code, gemini, opencode, openclaw, deepseek-harness, zcode, workbuddy, all | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--server ` | string | yes | Bailian MCP Server Code, such as TextGenerateImage | +| `--transport ` | string | yes | MCP transport exposed by the server: streamable-http or sse | +| `--agent ` | string | yes | Target Agent: codex, claude-code, cursor, qoder, qoderwork, qwenwork, qwen-code, gemini, opencode, openclaw, deepseek-harness, zcode, workbuddy, pi, all | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | #### Notes @@ -103,10 +103,10 @@ bl mcp connect --server TextGenerateImage --transport streamable-http --agent al #### Flags -| Flag | Type | Required | Description | -| -------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--server ` | string | yes | Bailian MCP Server Code used during connect | -| `--agent ` | string | yes | Target Agent: codex, claude-code, cursor, qoder, qoderwork, qwenwork, qwen-code, gemini, opencode, openclaw, deepseek-harness, zcode, workbuddy, all | +| Flag | Type | Required | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--server ` | string | yes | Bailian MCP Server Code used during connect | +| `--agent ` | string | yes | Target Agent: codex, claude-code, cursor, qoder, qoderwork, qwenwork, qwen-code, gemini, opencode, openclaw, deepseek-harness, zcode, workbuddy, pi, all | #### Notes