diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bd91e89..c45f8b4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,6 +3,13 @@ name: Publish on: workflow_dispatch: inputs: + package: + description: "Which package set to publish" + required: true + type: choice + options: + - bailian-cli + - knowledge-studio-cli mode: description: "Publish mode" required: true @@ -16,13 +23,13 @@ on: type: string concurrency: - group: publish-${{ inputs.mode }}-${{ inputs.channel }} + group: publish-${{ inputs.package }}-${{ inputs.mode }}-${{ inputs.channel }} cancel-in-progress: false jobs: publish-stable: if: inputs.mode == 'stable' - name: publish stable to npm + tag + name: publish stable (${{ inputs.package }}) to npm + tag runs-on: ubuntu-latest environment: production # Required Reviewers gate permissions: @@ -51,11 +58,11 @@ jobs: - run: pnpm install --frozen-lockfile - name: publish-stable - run: node tools/release/publish-stable.mjs + run: node tools/release/publish-stable.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }} publish-channel: if: inputs.mode == 'channel' - name: publish beta to npm + name: publish channel (${{ inputs.package }}) to npm runs-on: ubuntu-latest permissions: contents: read # no tag, no Release; just publish @@ -83,4 +90,4 @@ jobs: - run: pnpm install --frozen-lockfile - name: publish-channel - run: node tools/release/publish-channel.mjs --channel "${{ inputs.channel }}" + run: node tools/release/publish-channel.mjs ${{ inputs.package == 'knowledge-studio-cli' && '--knowledge' || '' }} --channel "${{ inputs.channel }}" diff --git a/.gitignore b/.gitignore index 004fdde..f1c88cb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ node_modules dist dist-ssr tools/generated +.node-version *.local diff --git a/package.json b/package.json index e7d8112..7e5acf0 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "ready": "vp check && vp run -r test && vp run -r build", "prepare": "vp config", "check": "vp check", - "sync:skill-assets": "pnpm --filter bailian-cli-core run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version", + "sync:skill-assets": "pnpm --filter \"bailian-cli^...\" run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version", "dev": "pnpm -F bailian-cli-core dev", "bl": "pnpm -F bailian-cli dev", + "kscli": "pnpm -F knowledge-studio-cli dev", "test": "vp test", "release:check": "node tools/release/check.mjs", "wiki:crawl": "node tools/wiki-crawler/index.mjs", diff --git a/packages/cli/package.json b/packages/cli/package.json index 2b06a22..92bcb2c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -33,6 +33,10 @@ "./package.json": "./package.json" }, "publishConfig": { + "exports": { + ".": "./dist/bailian.mjs", + "./package.json": "./package.json" + }, "registry": "https://registry.npmjs.org/" }, "scripts": { @@ -44,32 +48,23 @@ "check": "vp check" }, "dependencies": { + "bailian-cli-commands": "workspace:*", "bailian-cli-core": "workspace:*", - "boxen": "catalog:", - "chalk": "catalog:", - "undici": "catalog:" + "bailian-cli-runtime": "workspace:*" }, "devDependencies": { "@clack/prompts": "^0.7.0", "@types/node": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260328.1", "ajv": "catalog:", + "boxen": "catalog:", + "chalk": "catalog:", "typescript": "^6.0.2", - "vite-plus": "catalog:", + "undici": "catalog:", + "vite-plus": "0.1.22", "yaml": "catalog:" }, "engines": { "node": ">=22.12.0" - }, - "inlinedDependencies": { - "@clack/core": "0.3.5", - "@clack/prompts": "0.7.0", - "ajv": "8.20.0", - "fast-deep-equal": "3.1.3", - "fast-uri": "3.1.2", - "json-schema-traverse": "1.0.0", - "picocolors": "1.1.1", - "sisteransi": "1.0.5", - "yaml": "2.8.3" } } diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts new file mode 100644 index 0000000..3779aff --- /dev/null +++ b/packages/cli/src/commands.ts @@ -0,0 +1,105 @@ +import type { Command } from "bailian-cli-core"; +import { + authLogin, + authStatus, + authLogout, + textChat, + textOmni, + imageGenerate, + imageEdit, + videoGenerate, + videoEdit, + videoRef, + videoTaskGet, + videoDownload, + visionDescribe, + configShow, + configSet, + update, + appCall, + appList, + memoryAdd, + memorySearch, + memoryList, + memoryUpdate, + memoryDelete, + memoryProfileCreate, + memoryProfileGet, + knowledgeRetrieve, + knowledgeSearch, + knowledgeChat, + mcpCall, + mcpList, + mcpTools, + searchWeb, + speechSynthesize, + speechRecognize, + fileUpload, + consoleCall, + usageFree, + usageFreetier, + usageStats, + pipelineRun, + pipelineValidate, + advisorRecommend, + workspaceList, + quotaList, + quotaRequest, + quotaHistory, + quotaCheck, +} from "bailian-cli-commands"; + +// Full bailian-cli product: every command, exposed under the `bl` binary. +// The command paths below are this product's decision — the command library +// ships no presets, so the map is spelled out here. Kept in its own module +// (no side effects) so tools like generate-reference.ts can import it without +// starting the CLI. +export const commands: Record = { + "auth login": authLogin, + "auth status": authStatus, + "auth logout": authLogout, + "text chat": textChat, + omni: textOmni, + "image generate": imageGenerate, + "image edit": imageEdit, + "video generate": videoGenerate, + "video edit": videoEdit, + "video ref": videoRef, + "video task get": videoTaskGet, + "video download": videoDownload, + "vision describe": visionDescribe, + "config show": configShow, + "config set": configSet, + update, + "app call": appCall, + "app list": appList, + "memory add": memoryAdd, + "memory search": memorySearch, + "memory list": memoryList, + "memory update": memoryUpdate, + "memory delete": memoryDelete, + "memory profile create": memoryProfileCreate, + "memory profile get": memoryProfileGet, + "knowledge retrieve": knowledgeRetrieve, + "knowledge search": knowledgeSearch, + "knowledge chat": knowledgeChat, + "mcp call": mcpCall, + "mcp list": mcpList, + "mcp tools": mcpTools, + "search web": searchWeb, + "speech synthesize": speechSynthesize, + "speech recognize": speechRecognize, + "file upload": fileUpload, + "console call": consoleCall, + "usage free": usageFree, + "usage freetier": usageFreetier, + "usage stats": usageStats, + "pipeline run": pipelineRun, + "pipeline validate": pipelineValidate, + "advisor recommend": advisorRecommend, + "workspace list": workspaceList, + "quota list": quotaList, + "quota request": quotaRequest, + "quota history": quotaHistory, + "quota check": quotaCheck, +}; diff --git a/packages/cli/src/commands/catalog.ts b/packages/cli/src/commands/catalog.ts deleted file mode 100644 index ae48fcc..0000000 --- a/packages/cli/src/commands/catalog.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Command } from "bailian-cli-core"; - -import authLogin from "./auth/login.ts"; -import authStatus from "./auth/status.ts"; -import authLogout from "./auth/logout.ts"; -import textChat from "./text/chat.ts"; -import textOmni from "./omni/chat.ts"; -import imageGenerate from "./image/generate.ts"; -import imageEdit from "./image/edit.ts"; -import videoGenerate from "./video/generate.ts"; -import videoEdit from "./video/edit.ts"; -import videoRef from "./video/ref.ts"; -import videoTaskGet from "./video/task-get.ts"; -import videoDownload from "./video/download.ts"; -import visionDescribe from "./vision/describe.ts"; -import configShow from "./config/show.ts"; -import configSet from "./config/set.ts"; -import configExportSchema from "./config/export-schema.ts"; -import update from "./update.ts"; -import appCall from "./app/call.ts"; -import appList from "./app/list.ts"; -import memoryAdd from "./memory/add.ts"; -import memorySearch from "./memory/search.ts"; -import memoryList from "./memory/list.ts"; -import memoryUpdate from "./memory/update.ts"; -import memoryDelete from "./memory/delete.ts"; -import memoryProfileCreate from "./memory/profile-create.ts"; -import memoryProfileGet from "./memory/profile-get.ts"; -import knowledgeRetrieve from "./knowledge/retrieve.ts"; -import mcpCall from "./mcp/call.ts"; -import mcpList from "./mcp/list.ts"; -import mcpTools from "./mcp/tools.ts"; -import searchWeb from "./search/web.ts"; -import speechSynthesize from "./speech/synthesize.ts"; -import speechRecognize from "./speech/recognize.ts"; -import fileUpload from "./file/upload.ts"; -import consoleCall from "./console/call.ts"; -import usageFree from "./usage/free.ts"; -import usageFreetier from "./usage/freetier.ts"; -import usageStats from "./usage/stats.ts"; -import pipelineRun from "./pipeline/run.ts"; -import pipelineValidate from "./pipeline/validate.ts"; -import advisorRecommend from "./advisor/recommend.ts"; -import workspaceList from "./workspace/list.ts"; -import quotaList from "./quota/list.ts"; -import quotaRequest from "./quota/request.ts"; -import quotaHistory from "./quota/history.ts"; -import quotaCheck from "./quota/check.ts"; - -/** Command registry map (no dependency on registry.ts — safe for build-time import). */ -export const commands: Record = { - "auth login": authLogin, - "auth status": authStatus, - "auth logout": authLogout, - "text chat": textChat, - omni: textOmni, - "image generate": imageGenerate, - "image edit": imageEdit, - "video generate": videoGenerate, - "video edit": videoEdit, - "video ref": videoRef, - "video task get": videoTaskGet, - "video download": videoDownload, - "vision describe": visionDescribe, - "app call": appCall, - "app list": appList, - "memory add": memoryAdd, - "memory search": memorySearch, - "memory list": memoryList, - "memory update": memoryUpdate, - "memory delete": memoryDelete, - "memory profile create": memoryProfileCreate, - "memory profile get": memoryProfileGet, - "knowledge retrieve": knowledgeRetrieve, - "mcp list": mcpList, - "mcp tools": mcpTools, - "mcp call": mcpCall, - "search web": searchWeb, - "speech synthesize": speechSynthesize, - "speech recognize": speechRecognize, - "file upload": fileUpload, - "console call": consoleCall, - "usage free": usageFree, - "usage freetier": usageFreetier, - "usage stats": usageStats, - "pipeline run": pipelineRun, - "pipeline validate": pipelineValidate, - "config show": configShow, - "config set": configSet, - "config export-schema": configExportSchema, - "advisor recommend": advisorRecommend, - "workspace list": workspaceList, - "quota list": quotaList, - "quota request": quotaRequest, - "quota history": quotaHistory, - "quota check": quotaCheck, - update: update, -}; diff --git a/packages/cli/src/commands/config/export-schema.ts b/packages/cli/src/commands/config/export-schema.ts deleted file mode 100644 index 70824c3..0000000 --- a/packages/cli/src/commands/config/export-schema.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { defineCommand, generateToolSchema } from "bailian-cli-core"; -import type { Config } from "bailian-cli-core"; -import type { GlobalFlags } from "bailian-cli-core"; -import { BailianError } from "bailian-cli-core"; -import { ExitCode } from "bailian-cli-core"; - -/** - * Commands that are infrastructure/auth-related and not suitable as Agent tools. - */ -const SKIP_PREFIXES = ["auth ", "config ", "update"]; - -export default defineCommand({ - name: "config export-schema", - description: - "Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas", - skipDefaultApiKeySetup: true, - usage: 'bl config export-schema [--command ""]', - options: [ - { - flag: "--command ", - description: 'Export schema for a specific command only (e.g. "image generate")', - }, - ], - examples: ["bl config export-schema", 'bl config export-schema --command "video generate"'], - async run(config: Config, flags: GlobalFlags) { - const { commands } = await import("../catalog.ts"); - const targetCommand = flags.command as string | undefined; - - if (targetCommand) { - const command = commands[targetCommand]; - if (!command) { - throw new BailianError(`Command "${targetCommand}" not found.`, ExitCode.USAGE); - } - const schema = generateToolSchema(command); - process.stdout.write(JSON.stringify(schema, null, 2) + "\n"); - return; - } - - // Export all suitable commands - const allCommands = Object.values(commands); - const schemas = allCommands - .filter((c) => !SKIP_PREFIXES.some((p) => c.name.startsWith(p))) - .map((c) => generateToolSchema(c)); - - process.stdout.write(JSON.stringify(schemas, null, 2) + "\n"); - }, -}); diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts deleted file mode 100644 index ac06d96..0000000 --- a/packages/cli/src/commands/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { commands } from "./catalog.ts"; diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 279f65d..1e32e7f 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -1,151 +1,10 @@ -import { scanCommandPath, parseFlags } from "./args.ts"; -import { registry } from "./registry.ts"; -import { - GLOBAL_OPTIONS, - loadConfig, - resolveCredential, - trackCommandExecution, - flushTelemetry, -} from "bailian-cli-core"; -import { ensureApiKey } from "./utils/ensure-key.ts"; -import { setupProxyFromEnv } from "./proxy.ts"; -import { handleError } from "./error-handler.ts"; -import { checkForUpdate, getPendingUpdateNotification } from "./utils/update-checker.ts"; -import { maybeShowStatusBar } from "./output/status-bar.ts"; -import { printWelcomeBanner, printQuickStart } from "./output/banner.ts"; -import { CLI_VERSION } from "./version.ts"; -import { - printCurrentCommandHelp, - registerCommandHelpPrinter, - setExecutingCommandPath, -} from "./utils/command-help.ts"; - -// 必须在任何 fetch 发起前安装(含 update-checker / telemetry) -try { - setupProxyFromEnv(); -} catch (err) { - handleError(err); -} - -registerCommandHelpPrinter((commandPath, out) => { - registry.printHelp(commandPath, out); -}); - -// 优雅处理 Ctrl+C -// 退出前尝试 best-effort 刷出埋点,让去抖队列中 / 在途的 fetch 请求有机会 -// 落网络;flush 与较短超时 race,保证 SIGINT 仍然响应及时。 -process.on("SIGINT", () => { - process.stderr.write("\nInterrupted. Exiting.\n"); - void flushTelemetry(500).finally(() => process.exit(130)); -}); - -// 优雅处理 stdout EPIPE(例如管道到提前退出的 `mpv`) -process.stdout.on("error", (e: NodeJS.ErrnoException) => { - if (e.code === "EPIPE") process.exit(0); - else throw e; -}); - -async function main() { - let argv = process.argv.slice(2); - if (argv[0] === "--") argv = argv.slice(1); - - if (argv.includes("--version") || argv.includes("-v")) { - process.stdout.write(`bl ${CLI_VERSION}\n`); - process.exit(0); - } - - const commandPath = scanCommandPath(argv, GLOBAL_OPTIONS); - - if (argv.includes("--help") || argv.includes("-h")) { - registry.printHelp(commandPath, process.stderr); - process.exit(0); - } - - // 未传任何命令:展示帮助信息与登录引导 - if (commandPath.length === 0) { - registry.printHelp([], process.stderr); - - const flags = parseFlags(argv, GLOBAL_OPTIONS); - const config = loadConfig(flags); - config.clientName = "bailian-cli"; - config.clientVersion = CLI_VERSION; - - const hasKey = !!( - config.apiKey || - config.fileApiKey || - config.fileAccessToken || - config.accessTokenEnv - ); - if (hasKey) printQuickStart(); - else printWelcomeBanner(); - process.exit(0); - } - - // 组路径(例如 `bl speech` 未接子命令):展示帮助后干净退出 - if (registry.isGroupPath(commandPath)) { - registry.printHelp(commandPath, process.stderr); - process.exit(0); - } - - const { command, extra } = registry.resolve(commandPath); - const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...(command.options ?? [])]); - - if (extra.length > 0) (flags as Record)._positional = extra; - - const config = loadConfig(flags); - config.clientName = "bailian-cli"; - config.clientVersion = CLI_VERSION; - - // 默认执行 ensureApiKey;自行处理鉴权或仅需 Console/AK-SK 等的命令在 defineCommand 上设 skipDefaultApiKeySetup - if (!command.skipDefaultApiKeySetup) { - await ensureApiKey(config); - try { - const credential = await resolveCredential(config); - maybeShowStatusBar(config, credential.token, credential); - } catch { - /* 没有凭证,不展示状态栏 */ - } - } - - const updateCheckPromise = checkForUpdate(CLI_VERSION).catch(() => {}); - - setExecutingCommandPath(commandPath); - - if ( - commandPath[0] === "auth" && - commandPath[1] === "login" && - !flags.console && - !String((flags.apiKey as string | undefined) ?? "").trim() && - !String(config.apiKey ?? "").trim() && - !process.env.DASHSCOPE_API_KEY?.trim() - ) { - printCurrentCommandHelp(process.stderr); - process.exit(0); - } - - await trackCommandExecution(config, commandPath, flags, () => command.execute(config, flags)); - - await updateCheckPromise; - const isUpdateCommand = commandPath.length === 1 && commandPath[0] === "update"; - const newVersion = getPendingUpdateNotification(); - if (newVersion && !config.quiet && !isUpdateCommand) { - const isTTY = process.stderr.isTTY; - const yellow = isTTY ? "\x1b[33m" : ""; - const cyan = isTTY ? "\x1b[36m" : ""; - const reset = isTTY ? "\x1b[0m" : ""; - process.stderr.write(`\n ${yellow}Update available: ${CLI_VERSION} → ${newVersion}${reset}\n`); - process.stderr.write(` Run ${cyan}bl update${reset} to upgrade\n\n`); - } - - // 进程退出前尽力等待在途的埋点完成。 - // 使用较短超时兜底,避免慢网拖慢用户感知。 - await flushTelemetry(1000); -} - -main().catch((err) => { - // 在 handleError() 调用 process.exit() 之前刷出在途埋点。 - // 命令抛出的错误已被 trackCommandExecution 的 finally 块记录, - // 但底层 tracker 有 ~500ms 的发送去抖。不主动 flush 的话, - // 错误事件会随进程退出丢掉。 - void flushTelemetry(1000).finally(() => handleError(err)); -}); +import { createCli } from "bailian-cli-runtime"; +import { commands } from "./commands.ts"; +import pkg from "../package.json" with { type: "json" }; + +createCli(commands, { + binName: "bl", + version: pkg.version, + clientName: "bailian-cli", + npmPackage: "bailian-cli", +}).run(); diff --git a/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts b/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts new file mode 100644 index 0000000..27813c4 --- /dev/null +++ b/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts @@ -0,0 +1,211 @@ +import { tmpdir } from "os"; +import { describe, expect, test } from "vite-plus/test"; +import { parseStdoutJson, runCli } from "./helpers.ts"; + +interface ContentPart { + type: string; + text?: string; + image_url?: { url: string }; +} + +interface DryRunBody { + endpoint?: string; + request?: { + input?: { + messages?: Array<{ role: string; content: string | ContentPart[] }>; + }; + parameters?: { + agent_options?: { + agent_id?: string; + }; + }; + stream?: boolean; + }; +} + +describe("e2e: knowledge chat", () => { + test("knowledge chat --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["knowledge", "chat", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--message/i); + expect(stderr).toMatch(/--agent-id/i); + expect(stderr).toMatch(/--workspace-id/i); + }); + + test("缺少 --message 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "chat", + "--agent-id", + "aid_test", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--message|Usage:/i); + }); + + test("缺少 --agent-id 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "chat", + "--message", + "Hello", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--agent-id|Usage:/i); + }); + + test("缺少 --workspace-id 时非零退出并提示", async () => { + const { stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--message", + "Hello", + "--agent-id", + "aid_test", + "--non-interactive", + "--output", + "json", + ], + { + DASHSCOPE_API_KEY: "sk-fake", + BAILIAN_WORKSPACE_ID: undefined, + BAILIAN_CONFIG_DIR: tmpdir(), + }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/workspace.*required/i); + }); + + test("--dry-run 输出 endpoint 和 request body", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--dry-run", + "--message", + "什么是RAG", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.endpoint).toMatch(/ws_test\.cn-beijing\.maas\.aliyuncs\.com/); + expect(data.endpoint).toMatch(/api\/v2\/apps\/knowledge\/chat/); + expect(data.request?.input?.messages?.[0]?.role).toBe("user"); + expect(data.request?.input?.messages?.[0]?.content).toBe("什么是RAG"); + expect(data.request?.parameters?.agent_options?.agent_id).toBe("aid_test"); + }); + + test("--dry-run 多轮消息解析 role:content 前缀", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--dry-run", + "--message", + "user:什么是RAG", + "--message", + "assistant:RAG是检索增强生成", + "--message", + "它怎么工作", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + const msgs = data.request?.input?.messages ?? []; + expect(msgs).toHaveLength(3); + expect(msgs[0]?.role).toBe("user"); + expect(msgs[0]?.content).toBe("什么是RAG"); + expect(msgs[1]?.role).toBe("assistant"); + expect(msgs[1]?.content).toBe("RAG是检索增强生成"); + expect(msgs[2]?.role).toBe("user"); + expect(msgs[2]?.content).toBe("它怎么工作"); + }); + + test("--dry-run + --image 输出多模态 content 数组", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--dry-run", + "--message", + "描述这张图", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--image", + "https://example.com/img.jpg", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + const lastMsg = data.request?.input?.messages?.[0]; + expect(lastMsg?.role).toBe("user"); + expect(Array.isArray(lastMsg?.content)).toBe(true); + const parts = lastMsg?.content as ContentPart[]; + expect(parts[0]).toEqual({ type: "text", text: "描述这张图" }); + expect(parts[1]).toEqual({ + type: "image_url", + image_url: { url: "https://example.com/img.jpg" }, + }); + }); + + test("--dry-run + --image 无 --message 自动创建空 user message", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "chat", + "--dry-run", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--image", + "https://example.com/a.png", + "--image", + "https://example.com/b.png", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + const lastMsg = data.request?.input?.messages?.[0]; + expect(lastMsg?.role).toBe("user"); + const parts = lastMsg?.content as ContentPart[]; + expect(parts[0]).toEqual({ type: "text", text: "" }); + expect(parts[1]).toEqual({ + type: "image_url", + image_url: { url: "https://example.com/a.png" }, + }); + expect(parts[2]).toEqual({ + type: "image_url", + image_url: { url: "https://example.com/b.png" }, + }); + }); +}); diff --git a/packages/cli/tests/e2e/knowledge-search.e2e.test.ts b/packages/cli/tests/e2e/knowledge-search.e2e.test.ts new file mode 100644 index 0000000..e611af5 --- /dev/null +++ b/packages/cli/tests/e2e/knowledge-search.e2e.test.ts @@ -0,0 +1,180 @@ +import { tmpdir } from "os"; +import { describe, expect, test } from "vite-plus/test"; +import { parseStdoutJson, runCli } from "./helpers.ts"; + +interface DryRunBody { + endpoint?: string; + request?: { + query?: string; + agent_id?: string; + images?: string[]; + query_history?: Array<{ role: string; content: string }>; + }; +} + +describe("e2e: knowledge search", () => { + test("knowledge search --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["knowledge", "search", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--query/i); + expect(stderr).toMatch(/--agent-id/i); + expect(stderr).toMatch(/--workspace-id/i); + expect(stderr).toMatch(/--image/i); + expect(stderr).toMatch(/--query-history/i); + }); + + test("缺少 --query 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "search", + "--agent-id", + "aid_test", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--query|Usage:/i); + }); + + test("缺少 --agent-id 时打印帮助并退出 (0)", async () => { + const { stderr, exitCode } = await runCli([ + "knowledge", + "search", + "--query", + "test", + "--non-interactive", + ]); + expect(exitCode).toBe(0); + expect(stderr).toMatch(/--agent-id|Usage:/i); + }); + + test("缺少 --workspace-id 时非零退出并提示", async () => { + const { stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--query", + "test", + "--agent-id", + "aid_test", + "--non-interactive", + "--output", + "json", + ], + { + DASHSCOPE_API_KEY: "sk-fake", + BAILIAN_WORKSPACE_ID: undefined, + BAILIAN_CONFIG_DIR: tmpdir(), + }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/workspace.*required/i); + }); + + test("--dry-run 输出 endpoint 和 request body", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "什么是RAG", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.endpoint).toMatch(/ws_test\.cn-beijing\.maas\.aliyuncs\.com/); + expect(data.endpoint).toMatch(/api\/v1\/indices\/knowledge\/search/); + expect(data.request?.query).toBe("什么是RAG"); + expect(data.request?.agent_id).toBe("aid_test"); + }); + + test("--dry-run + --image 输出 images", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "test", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--image", + "https://example.com/a.jpg", + "--image", + "https://example.com/b.jpg", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.request?.images).toEqual([ + "https://example.com/a.jpg", + "https://example.com/b.jpg", + ]); + }); + + test("--dry-run + --query-history 输出用户对话历史", async () => { + const { stdout, stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "它怎么工作", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--query-history", + '[{"role":"user","content":"什么是RAG"},{"role":"assistant","content":"RAG是检索增强生成"}]', + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.request?.query_history).toEqual([ + { role: "user", content: "什么是RAG" }, + { role: "assistant", content: "RAG是检索增强生成" }, + ]); + }); + + test("--dry-run + --query-history 无效 JSON 非零退出", async () => { + const { stderr, exitCode } = await runCli( + [ + "knowledge", + "search", + "--dry-run", + "--query", + "test", + "--agent-id", + "aid_test", + "--workspace-id", + "ws_test", + "--query-history", + "not-valid-json", + "--non-interactive", + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-fake-for-dryrun" }, + ); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/query-history.*valid JSON/i); + }); +}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index ff4adab..5910788 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -5,6 +5,7 @@ "moduleDetection": "force", "module": "nodenext", "moduleResolution": "nodenext", + "customConditions": ["@bailian-cli/source"], "resolveJsonModule": true, "types": ["node"], "strict": true, diff --git a/packages/commands/.gitignore b/packages/commands/.gitignore new file mode 100644 index 0000000..7535211 --- /dev/null +++ b/packages/commands/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/packages/commands/package.json b/packages/commands/package.json new file mode 100644 index 0000000..cda2dfe --- /dev/null +++ b/packages/commands/package.json @@ -0,0 +1,58 @@ +{ + "name": "bailian-cli-commands", + "version": "1.4.0", + "description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.", + "homepage": "https://bailian.console.aliyun.com/cli", + "bugs": { + "url": "https://github.com/modelstudioai/cli/issues" + }, + "license": "Apache-2.0", + "author": "Aliyun Model Studio", + "repository": { + "type": "git", + "url": "git+https://github.com/modelstudioai/cli.git", + "directory": "packages/commands" + }, + "files": [ + "dist" + ], + "type": "module", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "@bailian-cli/source": "./src/index.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "build": "vp pack", + "dev": "vp pack --watch", + "test": "vp test", + "check": "vp check" + }, + "dependencies": { + "bailian-cli-core": "workspace:*", + "bailian-cli-runtime": "workspace:*", + "boxen": "catalog:", + "chalk": "catalog:", + "yaml": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "@typescript/native-preview": "7.0.0-dev.20260328.1", + "typescript": "^6.0.2", + "vite-plus": "0.1.22" + }, + "engines": { + "node": ">=22.12.0" + } +} diff --git a/packages/cli/src/commands/advisor/recommend.ts b/packages/commands/src/commands/advisor/recommend.ts similarity index 92% rename from packages/cli/src/commands/advisor/recommend.ts rename to packages/commands/src/commands/advisor/recommend.ts index 121c15a..81c6ddd 100644 --- a/packages/cli/src/commands/advisor/recommend.ts +++ b/packages/commands/src/commands/advisor/recommend.ts @@ -17,9 +17,9 @@ import { } from "bailian-cli-core"; import boxen from "boxen"; import chalk, { Chalk, type ChalkInstance } from "chalk"; -import { emitBare, emitResult } from "../../output/output.ts"; -import { createSpinner } from "../../output/progress.ts"; -import { failIfMissing, promptText } from "../../output/prompt.ts"; +import { emitBare, emitResult } from "bailian-cli-runtime"; +import { createSpinner } from "bailian-cli-runtime"; +import { failIfMissing, promptText, cmdUsage } from "bailian-cli-runtime"; function formatContextWindow(tokens: number): string { if (tokens >= 1_000_000) @@ -215,10 +215,9 @@ function isEmptyResult(result: RecommendResult): boolean { } export default defineCommand({ - name: "advisor recommend", description: "Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking)", - usage: "bl advisor recommend [flags]", + usageArgs: " [flags]", options: [ { flag: "--message ", @@ -233,13 +232,13 @@ export default defineCommand({ description: "Output format: text (default in TTY), json, yaml", }, ], - examples: [ - 'bl advisor recommend --message "I need a visual-understanding chatbot"', - 'bl advisor recommend --message "Build an Agent that auto-generates animations"', - 'bl advisor recommend --message "Legal contract review, high precision required"', - 'bl advisor recommend --message "Low-cost high-concurrency online customer service" --output json', - 'bl advisor recommend --message "Long document summarization" --dry-run', - "bl advisor recommend # Interactive input", + exampleArgs: [ + '--message "I need a visual-understanding chatbot"', + '--message "Build an Agent that auto-generates animations"', + '--message "Legal contract review, high precision required"', + '--message "Low-cost high-concurrency online customer service" --output json', + '--message "Long document summarization" --dry-run', + " # Interactive input", ], async run(config: Config, flags: GlobalFlags) { const positional = ((flags as Record)._positional as string[]) ?? []; @@ -254,7 +253,7 @@ export default defineCommand({ } userInput = hint; } else { - failIfMissing("message", 'bl advisor recommend "your requirement"'); + failIfMissing("message", cmdUsage(config, '"your requirement"')); } } diff --git a/packages/cli/src/commands/app/call.ts b/packages/commands/src/commands/app/call.ts similarity index 85% rename from packages/cli/src/commands/app/call.ts rename to packages/commands/src/commands/app/call.ts index 689db78..d784a65 100644 --- a/packages/cli/src/commands/app/call.ts +++ b/packages/commands/src/commands/app/call.ts @@ -11,13 +11,12 @@ import { type AppStreamChunk, type AppCompletionResponse, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "app call", description: "Call a Bailian application (agent or workflow)", - usage: "bl app call --app-id --prompt [flags]", + usageArgs: "--app-id --prompt [flags]", options: [ { flag: "--app-id ", description: "Application ID (required)", required: true }, { flag: "--prompt ", description: "Input prompt text", required: true }, @@ -34,20 +33,20 @@ export default defineCommand({ { flag: "--biz-params ", description: "Business parameters JSON (workflow variables)" }, { flag: "--has-thoughts", description: "Show agent thinking process" }, ], - examples: [ - 'bl app call --app-id abc123 --prompt "Hello"', - 'bl app call --app-id abc123 --prompt "Describe this image" --image https://example.com/photo.jpg', - 'bl app call --app-id abc123 --prompt "Analyze the image" --image img1.jpg --image img2.jpg', - 'bl app call --app-id abc123 --prompt "Continue" --session-id sess_xxx --stream', - 'bl app call --app-id abc123 --prompt "Search for materials" --pipeline-ids pipe1,pipe2', - 'bl app call --app-id abc123 --prompt "Start" --biz-params \'{"key":"value"}\'', + exampleArgs: [ + '--app-id abc123 --prompt "Hello"', + '--app-id abc123 --prompt "Describe this image" --image https://example.com/photo.jpg', + '--app-id abc123 --prompt "Analyze the image" --image img1.jpg --image img2.jpg', + '--app-id abc123 --prompt "Continue" --session-id sess_xxx --stream', + '--app-id abc123 --prompt "Search for materials" --pipeline-ids pipe1,pipe2', + '--app-id abc123 --prompt "Start" --biz-params \'{"key":"value"}\'', ], async run(config: Config, flags: GlobalFlags) { const appId = flags.appId as string; - if (!appId) failIfMissing("app-id", "bl app call --app-id --prompt "); + if (!appId) failIfMissing("app-id", cmdUsage(config, "--app-id --prompt ")); const prompt = flags.prompt as string; - if (!prompt) failIfMissing("prompt", "bl app call --app-id --prompt "); + if (!prompt) failIfMissing("prompt", cmdUsage(config, "--app-id --prompt ")); const shouldStream = flags.stream === true || (flags.stream === undefined && process.stdout.isTTY); diff --git a/packages/cli/src/commands/app/list.ts b/packages/commands/src/commands/app/list.ts similarity index 89% rename from packages/cli/src/commands/app/list.ts rename to packages/commands/src/commands/app/list.ts index 7a0d28b..ca98664 100644 --- a/packages/cli/src/commands/app/list.ts +++ b/packages/commands/src/commands/app/list.ts @@ -6,15 +6,14 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; +import { emitResult } from "bailian-cli-runtime"; const APP_LIST_API = "zeldaEasy.broadscope-bailian.app-control.list"; export default defineCommand({ - name: "app list", description: "List Bailian applications", skipDefaultApiKeySetup: true, - usage: "bl app list [flags]", + usageArgs: "[flags]", options: [ { flag: "--name ", @@ -41,12 +40,7 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl app list", - "bl app list --name customer service", - "bl app list --page 2 --page-size 10", - "bl app list --output json", - ], + exampleArgs: ["", "--name customer service", "--page 2 --page-size 10", "--output json"], async run(config: Config, flags: GlobalFlags) { const name = (flags.name as string) || ""; const pageNo = (flags.page as number) || 1; diff --git a/packages/cli/src/commands/auth/login-console.ts b/packages/commands/src/commands/auth/login-console.ts similarity index 100% rename from packages/cli/src/commands/auth/login-console.ts rename to packages/commands/src/commands/auth/login-console.ts diff --git a/packages/cli/src/commands/auth/login.ts b/packages/commands/src/commands/auth/login.ts similarity index 86% rename from packages/cli/src/commands/auth/login.ts rename to packages/commands/src/commands/auth/login.ts index 50cf832..3901369 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/commands/src/commands/auth/login.ts @@ -7,10 +7,10 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { printQuickStart } from "../../output/banner.ts"; -import { emitBare } from "../../output/output.ts"; -import { promptConfirm } from "../../output/prompt.ts"; -import { printCurrentCommandHelp } from "../../utils/command-help.ts"; +import { printQuickStart } from "bailian-cli-runtime"; +import { emitBare } from "bailian-cli-runtime"; +import { promptConfirm } from "bailian-cli-runtime"; +import { printCurrentCommandHelp } from "bailian-cli-runtime"; import { resolveConsoleOrigin, runConsoleLogin, @@ -18,10 +18,9 @@ import { } from "./login-console.ts"; export default defineCommand({ - name: "auth login", description: "Authenticate with API key or console browser login (credentials can coexist)", skipDefaultApiKeySetup: true, - usage: "bl auth login --api-key | bl auth login --console", + usageArgs: "--api-key | --console", options: [ { flag: "--api-key ", description: "DashScope API key to store" }, { @@ -34,7 +33,7 @@ export default defineCommand({ "Sign in via browser; use --console-site to choose domestic (default) or international", }, ], - examples: ["bl auth login --api-key sk-xxxxx", "bl auth login --console"], + exampleArgs: ["--api-key sk-xxxxx", "--console"], async run(config: Config, flags: GlobalFlags) { if (flags.console) { if (config.dryRun) { diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/commands/src/commands/auth/logout.ts similarity index 88% rename from packages/cli/src/commands/auth/logout.ts rename to packages/commands/src/commands/auth/logout.ts index 5dbc34d..f05646a 100644 --- a/packages/cli/src/commands/auth/logout.ts +++ b/packages/commands/src/commands/auth/logout.ts @@ -7,7 +7,7 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitBare } from "../../output/output.ts"; +import { emitBare } from "bailian-cli-runtime"; async function clearConsoleToken(): Promise { const file = readConfigFile() as Record; @@ -18,10 +18,9 @@ async function clearConsoleToken(): Promise { } export default defineCommand({ - name: "auth logout", description: "Clear stored credentials", skipDefaultApiKeySetup: true, - usage: "bl auth logout [--console] [--yes] [--dry-run]", + usageArgs: "[--console] [--yes] [--dry-run]", options: [ { flag: "--console", @@ -30,12 +29,7 @@ export default defineCommand({ }, { flag: "--yes", description: "Skip confirmation prompt" }, ], - examples: [ - "bl auth logout", - "bl auth logout --console", - "bl auth logout --dry-run", - "bl auth logout --yes", - ], + exampleArgs: ["", "--console", "--dry-run", "--yes"], async run(config: Config, flags: GlobalFlags) { const file = readConfigFile(); diff --git a/packages/cli/src/commands/auth/status.ts b/packages/commands/src/commands/auth/status.ts similarity index 89% rename from packages/cli/src/commands/auth/status.ts rename to packages/commands/src/commands/auth/status.ts index 6078037..aacf4ea 100644 --- a/packages/cli/src/commands/auth/status.ts +++ b/packages/commands/src/commands/auth/status.ts @@ -8,8 +8,8 @@ import { type GlobalFlags, type ResolvedCredential, } from "bailian-cli-core"; -import { emitResult, emitBare } from "../../output/output.ts"; -import { API_KEY_PAGE } from "../../urls.ts"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { API_KEY_PAGE } from "bailian-cli-runtime"; interface StoredCredential { configured: boolean; @@ -108,7 +108,7 @@ function hasAnyAuth(status: AuthStatusPayload): boolean { ); } -function emitTextStatus(status: AuthStatusPayload): void { +function emitTextStatus(status: AuthStatusPayload, config: Config): void { emitBare("Authentication Status:"); emitBare(" Stored credentials (can coexist):"); if (status.api_key.configured) { @@ -134,14 +134,12 @@ function emitTextStatus(status: AuthStatusPayload): void { ` Console gateway: ${status.console_gateway_commands.method} (${status.console_gateway_commands.source}) ${status.console_gateway_commands.masked}`, ); } else { - emitBare(" Console gateway: unavailable (run bl auth login --console)"); + emitBare(` Console gateway: unavailable (run ${config.binName} auth login --console)`); } } export default defineCommand({ - name: "auth status", description: "Show current authentication state", - usage: "bl auth status", options: [ { flag: "--console-region ", description: "Console region" }, { @@ -154,7 +152,7 @@ export default defineCommand({ type: "number", }, ], - examples: ["bl auth status", "bl auth status --output json"], + exampleArgs: ["", "--output json"], async run(config: Config, _flags: GlobalFlags) { const format = detectOutputFormat(config.output); const status = await buildStatus(config); @@ -164,8 +162,8 @@ export default defineCommand({ authenticated: false, message: "Not authenticated.", hint: [ - "DashScope API: bl auth login --api-key or DASHSCOPE_API_KEY", - "Console gateway: bl auth login --console or DASHSCOPE_ACCESS_TOKEN", + `DashScope API: ${config.binName} auth login --api-key or DASHSCOPE_API_KEY`, + `Console gateway: ${config.binName} auth login --console or DASHSCOPE_ACCESS_TOKEN`, `Get API Key: ${API_KEY_PAGE}`, ].join("\n"), ...status, @@ -179,6 +177,6 @@ export default defineCommand({ return; } - emitTextStatus(status); + emitTextStatus(status, config); }, }); diff --git a/packages/cli/src/commands/config/set.ts b/packages/commands/src/commands/config/set.ts similarity index 90% rename from packages/cli/src/commands/config/set.ts rename to packages/commands/src/commands/config/set.ts index d06099e..748594a 100644 --- a/packages/cli/src/commands/config/set.ts +++ b/packages/commands/src/commands/config/set.ts @@ -9,7 +9,7 @@ import { type GlobalFlags, ExitCode, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; +import { emitResult, cmdUsage } from "bailian-cli-runtime"; const VALID_KEYS = [ "base_url", @@ -50,10 +50,9 @@ const KEY_ALIASES: Record = { }; export default defineCommand({ - name: "config set", description: "Set a config value", skipDefaultApiKeySetup: true, - usage: "bl config set --key --value ", + usageArgs: "--key --value ", options: [ { flag: "--key ", @@ -62,10 +61,10 @@ export default defineCommand({ }, { flag: "--value ", description: "Value to set" }, ], - examples: [ - "bl config set --key output --value json", - "bl config set --key timeout --value 600", - "bl config set --key base_url --value https://dashscope.aliyuncs.com", + exampleArgs: [ + "--key output --value json", + "--key timeout --value 600", + "--key base_url --value https://dashscope.aliyuncs.com", ], async run(config: Config, flags: GlobalFlags) { const key = flags.key as string | undefined; @@ -75,7 +74,7 @@ export default defineCommand({ throw new BailianError( "--key and --value are required.", ExitCode.USAGE, - "bl config set --key --value ", + cmdUsage(config, "--key --value "), ); } diff --git a/packages/cli/src/commands/config/show.ts b/packages/commands/src/commands/config/show.ts similarity index 86% rename from packages/cli/src/commands/config/show.ts rename to packages/commands/src/commands/config/show.ts index bb36f0e..7b76687 100644 --- a/packages/cli/src/commands/config/show.ts +++ b/packages/commands/src/commands/config/show.ts @@ -7,14 +7,12 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; +import { emitResult } from "bailian-cli-runtime"; export default defineCommand({ - name: "config show", description: "Display current configuration", skipDefaultApiKeySetup: true, - usage: "bl config show", - examples: ["bl config show", "bl config show --output json"], + exampleArgs: ["", "--output json"], async run(config: Config, _flags: GlobalFlags) { const file = loadConfigFile(); const format = detectOutputFormat(config.output); diff --git a/packages/cli/src/commands/console/call.ts b/packages/commands/src/commands/console/call.ts similarity index 76% rename from packages/cli/src/commands/console/call.ts rename to packages/commands/src/commands/console/call.ts index f929ae9..525377b 100644 --- a/packages/cli/src/commands/console/call.ts +++ b/packages/commands/src/commands/console/call.ts @@ -9,14 +9,13 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult } from "bailian-cli-runtime"; export default defineCommand({ - name: "console call", description: "Call a Bailian console API via the CLI gateway", skipDefaultApiKeySetup: true, - usage: "bl console call --api --data [flags]", + usageArgs: "--api --data [flags]", options: [ { flag: "--api ", @@ -39,16 +38,16 @@ export default defineCommand({ type: "number", }, ], - examples: [ - `bl console call --api zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`, - `bl console call --api some.api.name --data '{"key":"value"}' --console-region cn-beijing`, + exampleArgs: [ + `--api zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota --data '{"queryFreeTierQuotaRequest":{"models":["qwen3-max"]}}'`, + `--api some.api.name --data '{"key":"value"}' --console-region cn-beijing`, ], async run(config: Config, flags: GlobalFlags) { const api = flags.api as string; - if (!api) failIfMissing("api", "bl console call --api --data "); + if (!api) failIfMissing("api", cmdUsage(config, "--api --data ")); const dataRaw = flags.data as string; - if (!dataRaw) failIfMissing("data", "bl console call --api --data "); + if (!dataRaw) failIfMissing("data", cmdUsage(config, "--api --data ")); let data: Record; try { diff --git a/packages/cli/src/commands/file/upload.ts b/packages/commands/src/commands/file/upload.ts similarity index 70% rename from packages/cli/src/commands/file/upload.ts rename to packages/commands/src/commands/file/upload.ts index 9d6a1cc..f0a1713 100644 --- a/packages/cli/src/commands/file/upload.ts +++ b/packages/commands/src/commands/file/upload.ts @@ -6,13 +6,12 @@ import { type GlobalFlags, uploadFile, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "file upload", description: "Upload a local file to DashScope temporary storage (48h)", - usage: "bl file upload --file --model ", + usageArgs: "--file --model ", options: [ { flag: "--file ", @@ -25,21 +24,21 @@ export default defineCommand({ required: true, }, ], - examples: [ - "bl file upload --file photo.jpg --model qwen3-vl-plus", - "bl file upload --file video.mp4 --model wan2.1-t2v-plus", - "bl file upload --file audio.wav --model qwen3-asr-flash", - "bl file upload --file cat.png --model qwen-image-2.0", + exampleArgs: [ + "--file photo.jpg --model qwen3-vl-plus", + "--file video.mp4 --model wan2.1-t2v-plus", + "--file audio.wav --model qwen3-asr-flash", + "--file cat.png --model qwen-image-2.0", ], async run(config: Config, flags: GlobalFlags) { const filePath = flags.file as string | undefined; if (!filePath) { - failIfMissing("file", "bl file upload --file --model "); + failIfMissing("file", cmdUsage(config, "--file --model ")); } const model = flags.model as string | undefined; if (!model) { - failIfMissing("model", "bl file upload --file --model "); + failIfMissing("model", cmdUsage(config, "--file --model ")); } const format = detectOutputFormat(config.output); diff --git a/packages/cli/src/commands/image/edit.ts b/packages/commands/src/commands/image/edit.ts similarity index 83% rename from packages/cli/src/commands/image/edit.ts rename to packages/commands/src/commands/image/edit.ts index e6b8b3a..e6c302c 100644 --- a/packages/cli/src/commands/image/edit.ts +++ b/packages/commands/src/commands/image/edit.ts @@ -18,21 +18,17 @@ import { resolveBooleanFlag, resolveWatermark, } from "bailian-cli-core"; -import { downloadFile } from "../../utils/download.ts"; -import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/concurrent.ts"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; -import { resolveImageSize } from "../../utils/image-size.ts"; +import { downloadFile } from "bailian-cli-runtime"; +import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { resolveImageSize } from "bailian-cli-runtime"; import { join } from "path"; -import { - BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, - BOOL_FLAG_WATERMARK, -} from "../../utils/flag-descriptions.ts"; +import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime"; export default defineCommand({ - name: "image edit", description: "Edit an existing image with text instructions (Qwen-Image)", - usage: "bl image edit --image --prompt [flags]", + usageArgs: "--image --prompt [flags]", options: [ { flag: "--image ", @@ -63,12 +59,12 @@ export default defineCommand({ { flag: "--out-dir ", description: "Download images to directory" }, { flag: "--out-prefix ", description: "Filename prefix (default: edited)" }, ], - examples: [ - 'bl image edit --image ./photo.png --prompt "Replace the background with a beach"', - 'bl image edit --image https://example.com/logo.png --prompt "Change color to blue" --n 3', - 'bl image edit --image ./a.png --image ./b.png --prompt "Merge two images into one collage"', - 'bl image edit --image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro', - 'bl image edit --image ./photo.png --prompt "Replace the background with a beach" --watermark false', + exampleArgs: [ + '--image ./photo.png --prompt "Replace the background with a beach"', + '--image https://example.com/logo.png --prompt "Change color to blue" --n 3', + '--image ./a.png --image ./b.png --prompt "Merge two images into one collage"', + '--image https://example.com/photo.png --prompt "Remove the person" --model qwen-image-2.0-pro', + '--image ./photo.png --prompt "Replace the background with a beach" --watermark false', ], async run(config: Config, flags: GlobalFlags) { // Normalize --image to string array (supports both single and repeated flags) @@ -79,7 +75,7 @@ export default defineCommand({ rawImages = [flags.image]; } if (rawImages.length === 0) { - failIfMissing("image", "bl image edit --image --prompt "); + failIfMissing("image", cmdUsage(config, "--image --prompt ")); } let prompt = flags.prompt as string | undefined; @@ -94,7 +90,7 @@ export default defineCommand({ } prompt = hint; } else { - failIfMissing("prompt", "bl image edit --image --prompt "); + failIfMissing("prompt", cmdUsage(config, "--image --prompt ")); } } diff --git a/packages/cli/src/commands/image/generate.ts b/packages/commands/src/commands/image/generate.ts similarity index 86% rename from packages/cli/src/commands/image/generate.ts rename to packages/commands/src/commands/image/generate.ts index 3bf1be9..000afd3 100644 --- a/packages/cli/src/commands/image/generate.ts +++ b/packages/commands/src/commands/image/generate.ts @@ -20,16 +20,13 @@ import { resolveBooleanFlag, resolveWatermark, } from "bailian-cli-core"; -import { poll } from "../../utils/polling.ts"; -import { downloadFile } from "../../utils/download.ts"; -import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/concurrent.ts"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; -import { resolveImageSize } from "../../utils/image-size.ts"; -import { - BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, - BOOL_FLAG_WATERMARK, -} from "../../utils/flag-descriptions.ts"; +import { poll } from "bailian-cli-runtime"; +import { downloadFile } from "bailian-cli-runtime"; +import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { resolveImageSize } from "bailian-cli-runtime"; +import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime"; import { join } from "path"; @@ -41,9 +38,8 @@ function isSyncModel(model: string): boolean { } export default defineCommand({ - name: "image generate", description: "Generate images (Qwen-Image / wan2.x)", - usage: "bl image generate --prompt [flags]", + usageArgs: "--prompt [flags]", options: [ { flag: "--prompt ", description: "Image description", required: true }, { flag: "--model ", description: "Model ID (default: qwen-image-2.0)" }, @@ -81,16 +77,16 @@ export default defineCommand({ type: "number", }, ], - examples: [ - 'bl image generate --prompt "A cat in a spacesuit on Mars"', - 'bl image generate --prompt "Logo design" --n 3 --out-dir ./generated/', - 'bl image generate --prompt "Mountain landscape" --size 2688*1536', - 'bl image generate --prompt "A castle" --seed 42 --prompt-extend false', - 'bl image generate --prompt "Logo" --watermark false', - 'bl image generate --prompt "An alien in the space" --watermark false', - 'bl image generate --prompt "sunset" --model wan2.6-t2i --no-wait --quiet', - 'bl image generate --prompt "Pro quality" --model qwen-image-2.0-pro', - 'bl image generate --prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel', + exampleArgs: [ + '--prompt "A cat in a spacesuit on Mars"', + '--prompt "Logo design" --n 3 --out-dir ./generated/', + '--prompt "Mountain landscape" --size 2688*1536', + '--prompt "A castle" --seed 42 --prompt-extend false', + '--prompt "Logo" --watermark false', + '--prompt "An alien in the space" --watermark false', + '--prompt "sunset" --model wan2.6-t2i --no-wait --quiet', + '--prompt "Pro quality" --model qwen-image-2.0-pro', + '--prompt "Product shots" --n 2 --concurrent 3 # 6 images in parallel', ], async run(config: Config, flags: GlobalFlags) { let prompt = (flags.prompt ?? (flags._positional as string[] | undefined)?.[0]) as @@ -108,7 +104,7 @@ export default defineCommand({ } prompt = hint; } else { - failIfMissing("prompt", "bl image generate --prompt "); + failIfMissing("prompt", cmdUsage(config, "--prompt ")); } } diff --git a/packages/commands/src/commands/knowledge/chat.ts b/packages/commands/src/commands/knowledge/chat.ts new file mode 100644 index 0000000..f52f64a --- /dev/null +++ b/packages/commands/src/commands/knowledge/chat.ts @@ -0,0 +1,336 @@ +import { + defineCommand, + request, + knowledgeChatEndpoint, + parseSSE, + detectOutputFormat, + BailianError, + ExitCode, + isInteractive, + type Config, + type GlobalFlags, + type KnowledgeChatContentPart, + type KnowledgeChatMessage, + type KnowledgeChatRequest, + type KnowledgeChatStreamChunk, +} from "bailian-cli-core"; +import { failIfMissing, cmdUsage, emitResult, emitBare, promptText } from "bailian-cli-runtime"; + +/** + * Parse --message flags into KnowledgeChatMessage[]. + * Supports: + * 1. Simple text: "hello" → {role:"user", content:"hello"} + * 2. Role prefix: "user:hello" / "assistant:hi" → {role, content} + * 3. JSON object: '{"role":"user","content":[...]}' → structured message (advanced) + */ +function parseMessages(flags: GlobalFlags): KnowledgeChatMessage[] { + const messages: KnowledgeChatMessage[] = []; + if (flags.message) { + const validRoles = new Set(["user", "assistant"]); + const msgs = flags.message as string[]; + for (const m of msgs) { + // Try JSON object first (advanced usage) + if (m.startsWith("{")) { + try { + const parsed = JSON.parse(m) as { role?: string; content?: unknown }; + if (parsed.role && validRoles.has(parsed.role) && parsed.content !== undefined) { + messages.push(parsed as KnowledgeChatMessage); + continue; + } + } catch { + // Not valid JSON, fall through to simple parsing + } + } + + // Simple role:content or plain text + const colonIdx = m.indexOf(":"); + const maybeRole = colonIdx !== -1 ? m.slice(0, colonIdx) : ""; + + if (validRoles.has(maybeRole)) { + messages.push({ role: maybeRole as "user" | "assistant", content: m.slice(colonIdx + 1) }); + } else { + messages.push({ role: "user", content: m }); + } + } + } + return messages; +} + +/** Check if any message content already contains image_url parts */ +function hasEmbeddedImages(messages: KnowledgeChatMessage[]): boolean { + for (const msg of messages) { + if (Array.isArray(msg.content)) { + if (msg.content.some((p) => p.type === "image_url")) return true; + } + } + return false; +} + +/** Attach --image URLs to the last user message's content (as multimodal array) */ +function attachImagesToLastUserMessage( + messages: KnowledgeChatMessage[], + imageUrls: string[], +): void { + // Find last user message index + let lastUserIdx = -1; + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i]!.role === "user") { + lastUserIdx = i; + break; + } + } + + // If no user message exists, append an empty one + if (lastUserIdx === -1) { + messages.push({ role: "user", content: "" }); + lastUserIdx = messages.length - 1; + } + + const target = messages[lastUserIdx]!; + const contentParts: KnowledgeChatContentPart[] = []; + + // Preserve existing text content (always include a text part, even if empty) + if (typeof target.content === "string") { + contentParts.push({ type: "text", text: target.content }); + } else { + // Already an array, extend it + contentParts.push(...target.content); + } + + // Append image parts + for (const url of imageUrls) { + contentParts.push({ type: "image_url", image_url: { url } }); + } + + target.content = contentParts; +} + +/** SSE step_change → human-friendly progress label (TTY only) */ +const STEP_LABELS: Record = { + tool_calling: "🔍 Retrieving...", + plan_start: "🤔 Planning...", + generation_start: "✍️ Generating...", +}; + +export default defineCommand({ + description: "Chat with a Bailian knowledge base (RAG Q&A with streaming)", + usageArgs: "--message --agent-id [flags]", + options: [ + { + flag: "--message ", + description: + "Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format", + required: true, + type: "array", + }, + { + flag: "--agent-id ", + description: "Q&A service ID (find in console knowledge Q&A page)", + required: true, + }, + { + flag: "--workspace-id ", + description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)", + }, + { + flag: "--image ", + description: + "Image URL (repeatable). Attached to the last user message as multimodal content", + type: "array", + }, + ], + notes: [ + "Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times.", + "Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.", + "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `.", + 'Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history.', + ], + exampleArgs: [ + '--message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx', + '--message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx', + '--message "Describe these images" --image https://example.com/a.png --image https://example.com/b.png --agent-id aid-xxx --workspace-id ws-xxx', + ], + async run(config: Config, flags: GlobalFlags) { + let messages = parseMessages(flags); + + const imageUrls = flags.image as string[] | undefined; + const hasImages = imageUrls && imageUrls.length > 0; + + if (messages.length === 0) { + if (hasImages) { + // --image without --message: create an empty user message to hold images + messages = [{ role: "user", content: "" }]; + } else if (isInteractive({ nonInteractive: config.nonInteractive })) { + const hint = await promptText({ message: "Enter your message:" }); + if (!hint) { + process.stderr.write("Chat cancelled.\n"); + process.exit(1); + } + messages = [{ role: "user", content: hint }]; + } else { + failIfMissing("message", cmdUsage(config, "--message --agent-id ")); + } + } + + const agentId = flags.agentId as string; + if (!agentId) failIfMissing("agent-id", cmdUsage(config, "--message --agent-id ")); + + const workspaceId = (flags.workspaceId as string) || config.workspaceId; + if (!workspaceId) { + throw new BailianError( + "Workspace ID is required.", + ExitCode.USAGE, + "Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: kscli config set workspace_id ", + ); + } + + const format = detectOutputFormat(config.output); + // API only supports SSE; streamOutput controls whether to print tokens in real-time + const streamOutput = format === "text" && !!process.stdout.isTTY; + + // Attach --image URLs to messages (multimodal content array) + if (hasImages) { + if (hasEmbeddedImages(messages)) { + throw new BailianError( + "Cannot use --image when messages already contain embedded image_url content parts. Use one approach or the other.", + ExitCode.USAGE, + ); + } + attachImagesToLastUserMessage(messages, imageUrls!); + } + + const body: KnowledgeChatRequest = { + input: { + messages, + }, + parameters: { + agent_options: { + agent_id: agentId, + }, + }, + stream: true, + }; + + const url = knowledgeChatEndpoint(workspaceId); + + if (config.dryRun) { + emitResult({ endpoint: url, request: body }, format); + return; + } + + const res = await request(config, { + url, + method: "POST", + body, + stream: true, + }); + + if (streamOutput) { + let textContent = ""; + const dim = config.noColor ? "" : "\x1b[2m"; + const reset = config.noColor ? "" : "\x1b[0m"; + const verbose = config.verbose; + + for await (const event of parseSSE(res)) { + if (event.data === "[DONE]") break; + + if (event.event === "error") { + let errMsg = "Chat API error"; + let errCode: string | undefined; + try { + const err = JSON.parse(event.data); + errMsg = err.message || errMsg; + errCode = err.code; + } catch { + /* use defaults */ + } + throw new BailianError( + errMsg, + ExitCode.GENERAL, + errCode ? `API error: ${errCode}` : undefined, + ); + } + + try { + const chunk = JSON.parse(event.data) as KnowledgeChatStreamChunk; + + for (const choice of chunk.output?.choices ?? []) { + const msg = choice.message; + + // Progress indicator (TTY text mode) + if (msg.extra?.step_change) { + const label = STEP_LABELS[msg.extra.step_change]; + if (label) { + process.stdout.write(`${dim}${label}${reset}\n`); + } + } + + // Verbose: dump all events to stderr + if (verbose && msg.extra?.step_change) { + process.stderr.write( + `${dim}[event] step_change=${msg.extra.step_change} step=${msg.extra?.step ?? ""} group=${msg.extra?.group ?? ""}${reset}\n`, + ); + } + + // Extract generated content + if (msg.content) { + textContent += msg.content; + process.stdout.write(msg.content); + } + + if (choice.finish_reason === "stop") break; + } + } catch { + // Skip unparseable chunks + } + } + + process.stdout.write("\n"); + } else { + // Buffered output: collect all chunks then emit + let textContent = ""; + let requestId = ""; + + for await (const event of parseSSE(res)) { + if (event.data === "[DONE]") break; + + if (event.event === "error") { + let errMsg = "Chat API error"; + let errCode: string | undefined; + try { + const err = JSON.parse(event.data); + errMsg = err.message || errMsg; + errCode = err.code; + } catch { + /* use defaults */ + } + throw new BailianError( + errMsg, + ExitCode.GENERAL, + errCode ? `API error: ${errCode}` : undefined, + ); + } + + try { + const chunk = JSON.parse(event.data) as KnowledgeChatStreamChunk; + if (chunk.request_id) requestId = chunk.request_id; + + for (const choice of chunk.output?.choices ?? []) { + if (choice.message?.content) { + textContent += choice.message.content; + } + if (choice.finish_reason === "stop") break; + } + } catch { + // Skip unparseable chunks + } + } + + if (config.quiet || format === "text") { + emitBare(textContent); + } else { + emitResult({ answer: textContent, request_id: requestId }, format); + } + } + }, +}); diff --git a/packages/cli/src/commands/knowledge/retrieve.ts b/packages/commands/src/commands/knowledge/retrieve.ts similarity index 91% rename from packages/cli/src/commands/knowledge/retrieve.ts rename to packages/commands/src/commands/knowledge/retrieve.ts index 8e7d136..769737d 100644 --- a/packages/cli/src/commands/knowledge/retrieve.ts +++ b/packages/commands/src/commands/knowledge/retrieve.ts @@ -17,16 +17,15 @@ import { BailianError, ExitCode, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com"; export default defineCommand({ - name: "knowledge retrieve", - description: "Retrieve from a Bailian knowledge base", + description: "Retrieve from a Bailian knowledge base (deprecated, use `search` instead)", skipDefaultApiKeySetup: true, - usage: "bl knowledge retrieve --index-id --query [flags]", + usageArgs: "--index-id --query [flags]", options: [ { flag: "--index-id ", description: "Knowledge base index ID (required)", required: true }, { flag: "--query ", description: "Search query (required)", required: true }, @@ -76,16 +75,16 @@ export default defineCommand({ "Authentication: pass `--api-key `. AK/SK auth is deprecated and will be removed in a future version.", "`--workspace-id` is NOT required when using --api-key.", ], - examples: [ - 'bl knowledge retrieve --index-id idx_xxx --query "How to use Alibaba Cloud Bailian"', - 'bl knowledge retrieve --api-key $DASHSCOPE_API_KEY --index-id idx_xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid', + exampleArgs: [ + '--index-id idx_xxx --query "How to use Alibaba Cloud Bailian"', + '--api-key $DASHSCOPE_API_KEY --index-id idx_xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid', ], async run(config: Config, flags: GlobalFlags) { const indexId = flags.indexId as string; - if (!indexId) failIfMissing("index-id", "bl knowledge retrieve --index-id --query "); + if (!indexId) failIfMissing("index-id", cmdUsage(config, "--index-id --query ")); const query = flags.query as string; - if (!query) failIfMissing("query", "bl knowledge retrieve --index-id --query "); + if (!query) failIfMissing("query", cmdUsage(config, "--index-id --query ")); const format = detectOutputFormat(config.output); @@ -196,7 +195,7 @@ async function runWithAkSk( if (!workspaceId) { throw new BailianError( "Knowledge retrieve requires a workspace ID.\n" + - "Set via: --workspace-id flag, or env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id ", + `Set via: --workspace-id flag, or env: BAILIAN_WORKSPACE_ID, or config: ${config.binName} config set workspace_id `, ExitCode.USAGE, ); } diff --git a/packages/commands/src/commands/knowledge/search.ts b/packages/commands/src/commands/knowledge/search.ts new file mode 100644 index 0000000..e869455 --- /dev/null +++ b/packages/commands/src/commands/knowledge/search.ts @@ -0,0 +1,139 @@ +import { + defineCommand, + requestJson, + knowledgeSearchEndpoint, + detectOutputFormat, + BailianError, + ExitCode, + isInteractive, + type Config, + type GlobalFlags, + type KnowledgeSearchRequest, + type KnowledgeSearchResponse, +} from "bailian-cli-core"; +import { failIfMissing, cmdUsage, emitResult, emitBare, promptText } from "bailian-cli-runtime"; + +export default defineCommand({ + description: "Search a Bailian knowledge base (RAG semantic retrieval)", + usageArgs: "--query --agent-id [flags]", + options: [ + { + flag: "--query ", + description: "Search query text (required, cannot be empty)", + required: true, + }, + { + flag: "--agent-id ", + description: "Retrieval service ID (find in console knowledge retrieval page)", + required: true, + }, + { + flag: "--workspace-id ", + description: "Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID)", + }, + { + flag: "--image ", + description: "Image URL for multimodal retrieval (repeatable)", + type: "array", + }, + { + flag: "--query-history ", + description: + 'User conversation history JSON for context understanding and query rewriting. Format: \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]\'', + }, + ], + notes: [ + "Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required.", + "Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page.", + "`--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `.", + "`--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance.", + ], + exampleArgs: [ + '--query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx', + '--api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg', + '--query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history \'[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]\'', + ], + async run(config: Config, flags: GlobalFlags) { + let query = flags.query as string | undefined; + if (!query) { + if (isInteractive({ nonInteractive: config.nonInteractive })) { + const hint = await promptText({ message: "Enter your search query:" }); + if (!hint) { + process.stderr.write("Search cancelled.\n"); + process.exit(1); + } + query = hint; + } else { + failIfMissing("query", cmdUsage(config, "--query --agent-id ")); + } + } + + const agentId = flags.agentId as string; + if (!agentId) failIfMissing("agent-id", cmdUsage(config, "--query --agent-id ")); + + const workspaceId = (flags.workspaceId as string) || config.workspaceId; + if (!workspaceId) { + throw new BailianError( + "Workspace ID is required.", + ExitCode.USAGE, + "Pass --workspace-id, set BAILIAN_WORKSPACE_ID env, or configure: kscli config set workspace_id ", + ); + } + + const format = detectOutputFormat(config.output); + + const body: KnowledgeSearchRequest = { + query: query!, + agent_id: agentId, + }; + + const imageUrls = flags.image as string[] | undefined; + if (imageUrls && imageUrls.length > 0) { + body.images = imageUrls; + } + + // Parse query_history JSON for multi-turn context + if (flags.queryHistory) { + try { + body.query_history = JSON.parse(flags.queryHistory as string) as Array<{ + role: "user" | "assistant"; + content: string; + }>; + } catch { + throw new BailianError( + '--query-history must be valid JSON. Example: --query-history \'[{"role":"user","content":"What is RAG"}]\'', + ExitCode.USAGE, + ); + } + } + + const url = knowledgeSearchEndpoint(workspaceId); + + if (config.dryRun) { + emitResult({ endpoint: url, request: body }, format); + return; + } + + const response = await requestJson(config, { + url, + method: "POST", + body, + }); + + const nodes = response.data?.nodes || []; + if (config.quiet || format === "text") { + if (nodes.length === 0) { + emitBare("No results found."); + } else { + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]!; + emitBare(`[${i + 1}] (score: ${node.score.toFixed(4)})`); + emitBare(node.text); + emitBare(""); + } + } + } else { + emitResult(response, format); + } + }, +}); diff --git a/packages/cli/src/commands/mcp/call.ts b/packages/commands/src/commands/mcp/call.ts similarity index 83% rename from packages/cli/src/commands/mcp/call.ts rename to packages/commands/src/commands/mcp/call.ts index 3f6eeb5..43b36f8 100644 --- a/packages/cli/src/commands/mcp/call.ts +++ b/packages/commands/src/commands/mcp/call.ts @@ -6,9 +6,9 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult } from "../../output/output.ts"; -import { ensureApiKey } from "../../utils/ensure-key.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult } from "bailian-cli-runtime"; +import { ensureApiKey } from "bailian-cli-runtime"; function parseArgFlags(raw: string[]): Record { const out: Record = {}; @@ -30,10 +30,9 @@ function parseArgFlags(raw: string[]): Record { } export default defineCommand({ - name: "mcp call", description: "Call a tool on an MCP server (tools/call)", skipDefaultApiKeySetup: true, - usage: "bl mcp call . [--arg k=v ...] [--json '{...}'] [--url ]", + usageArgs: ". [--arg k=v ...] [--json '{...}'] [--url ]", options: [ { flag: ".", @@ -56,16 +55,16 @@ export default defineCommand({ }, { flag: "--url ", description: "Override the MCP endpoint URL (for non-Bailian servers)" }, ], - examples: [ - 'bl mcp call market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"', - 'bl mcp call market-cmapi00073529.FinQuery --json \'{"q":"Guizhou Maotai","limit":5}\'', - "bl mcp call market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10", + exampleArgs: [ + 'market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"', + 'market-cmapi00073529.FinQuery --json \'{"q":"Guizhou Maotai","limit":5}\'', + "market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10", ], async run(config: Config, flags: GlobalFlags) { const positional = ((flags as Record)._positional as string[] | undefined) ?? []; const target = positional[0]; - if (!target) failIfMissing(".", "bl mcp call ."); + if (!target) failIfMissing(".", cmdUsage(config, ".")); const dot = target!.indexOf("."); if (dot <= 0 || dot === target!.length - 1) { diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/commands/src/commands/mcp/list.ts similarity index 92% rename from packages/cli/src/commands/mcp/list.ts rename to packages/commands/src/commands/mcp/list.ts index 3df644b..e93fa03 100644 --- a/packages/cli/src/commands/mcp/list.ts +++ b/packages/commands/src/commands/mcp/list.ts @@ -9,7 +9,7 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; +import { emitResult } from "bailian-cli-runtime"; const MCP_LIST_API = "zeldaEasy.broadscope-bailian.mcp-server.PageList"; @@ -25,10 +25,9 @@ interface ServerSummary { } export default defineCommand({ - name: "mcp list", description: "List MCP servers activated under your Bailian account", skipDefaultApiKeySetup: true, - usage: "bl mcp list [flags]", + usageArgs: "[flags]", options: [ { flag: "--name ", description: "Filter by server name (substring match)" }, { @@ -48,7 +47,7 @@ export default defineCommand({ type: "number", }, ], - examples: ["bl mcp list", "bl mcp list --name finance", "bl mcp list --output json"], + exampleArgs: ["", "--name finance", "--output json"], async run(config: Config, flags: GlobalFlags) { const serverName = (flags.name as string) || ""; const type = (flags.type as string) || "OFFICIAL"; @@ -85,7 +84,7 @@ export default defineCommand({ const msg = (dataField.errorMsg as string | undefined) ?? code; const hint = code === "BailianGateway.Login.NotLogined" - ? "Run `bl auth login --console` to refresh your console session." + ? `Run \`${config.binName} auth login --console\` to refresh your console session.` : undefined; throw new BailianError(`Console gateway: ${msg}`, ExitCode.AUTH, hint); } diff --git a/packages/cli/src/commands/mcp/tools.ts b/packages/commands/src/commands/mcp/tools.ts similarity index 66% rename from packages/cli/src/commands/mcp/tools.ts rename to packages/commands/src/commands/mcp/tools.ts index 5c4754e..d30aa99 100644 --- a/packages/cli/src/commands/mcp/tools.ts +++ b/packages/commands/src/commands/mcp/tools.ts @@ -6,33 +6,32 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult } from "../../output/output.ts"; -import { ensureApiKey } from "../../utils/ensure-key.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult } from "bailian-cli-runtime"; +import { ensureApiKey } from "bailian-cli-runtime"; export default defineCommand({ - name: "mcp tools", description: "List tools exposed by an MCP server (tools/list)", skipDefaultApiKeySetup: true, - usage: "bl mcp tools [--url ]", + usageArgs: " [--url ]", options: [ { flag: "", - description: "Server code from `bl mcp list` (e.g. market-cmapi00073529)", + description: "Server code from `mcp list` (e.g. market-cmapi00073529)", required: true, }, { flag: "--url ", description: "Override the MCP endpoint URL (for non-Bailian servers)" }, ], - examples: [ - "bl mcp tools market-cmapi00073529", - "bl mcp tools market-cmapi00073529 --output json", - "bl mcp tools my-server --url https://example.com/mcp", + exampleArgs: [ + "market-cmapi00073529", + "market-cmapi00073529 --output json", + "my-server --url https://example.com/mcp", ], async run(config: Config, flags: GlobalFlags) { const positional = ((flags as Record)._positional as string[] | undefined) ?? []; const code = positional[0]; - if (!code) failIfMissing("server-code", "bl mcp tools "); + if (!code) failIfMissing("server-code", cmdUsage(config, "")); const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, code!); const format = detectOutputFormat(config.output); diff --git a/packages/cli/src/commands/memory/add.ts b/packages/commands/src/commands/memory/add.ts similarity index 78% rename from packages/cli/src/commands/memory/add.ts rename to packages/commands/src/commands/memory/add.ts index 1604918..830e7c5 100644 --- a/packages/cli/src/commands/memory/add.ts +++ b/packages/commands/src/commands/memory/add.ts @@ -8,13 +8,12 @@ import { type MemoryAddRequest, type MemoryAddResponse, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "memory add", description: "Add memory from messages or custom content", - usage: "bl memory add --user-id [--messages ] [--content ] [flags]", + usageArgs: "--user-id [--messages ] [--content ] [flags]", options: [ { flag: "--user-id ", description: "User ID (required)", required: true }, { @@ -25,14 +24,14 @@ export default defineCommand({ { flag: "--profile-schema ", description: "Profile schema ID for user profiling" }, { flag: "--memory-library-id ", description: "Memory library ID (isolate memory space)" }, ], - examples: [ - 'bl memory add --user-id user1 --content "The user likes Python programming"', - 'bl memory add --user-id user1 --messages \'[{"role":"user","content":"I like traveling"}]\'', - 'bl memory add --user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx', + exampleArgs: [ + '--user-id user1 --content "The user likes Python programming"', + '--user-id user1 --messages \'[{"role":"user","content":"I like traveling"}]\'', + '--user-id user1 --content "Lives in Beijing" --profile-schema schema_xxx', ], async run(config: Config, flags: GlobalFlags) { const userId = flags.userId as string; - if (!userId) failIfMissing("user-id", "bl memory add --user-id "); + if (!userId) failIfMissing("user-id", cmdUsage(config, "--user-id ")); const body: MemoryAddRequest = { user_id: userId }; diff --git a/packages/cli/src/commands/memory/delete.ts b/packages/commands/src/commands/memory/delete.ts similarity index 74% rename from packages/cli/src/commands/memory/delete.ts rename to packages/commands/src/commands/memory/delete.ts index eb04163..d359c46 100644 --- a/packages/cli/src/commands/memory/delete.ts +++ b/packages/commands/src/commands/memory/delete.ts @@ -6,25 +6,24 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "memory delete", description: "Delete a memory node", - usage: "bl memory delete --node-id --user-id ", + usageArgs: "--node-id --user-id ", options: [ { flag: "--node-id ", description: "Memory node ID (required)", required: true }, { flag: "--user-id ", description: "User ID (required)", required: true }, { flag: "--memory-library-id ", description: "Memory library ID (non-default library)" }, ], - examples: ["bl memory delete --node-id node_xxx --user-id user1"], + exampleArgs: ["--node-id node_xxx --user-id user1"], async run(config: Config, flags: GlobalFlags) { const nodeId = flags.nodeId as string; - if (!nodeId) failIfMissing("node-id", "bl memory delete --node-id --user-id "); + if (!nodeId) failIfMissing("node-id", cmdUsage(config, "--node-id --user-id ")); const userId = flags.userId as string; - if (!userId) failIfMissing("user-id", "bl memory delete --node-id --user-id "); + if (!userId) failIfMissing("user-id", cmdUsage(config, "--node-id --user-id ")); const format = detectOutputFormat(config.output); const params = new URLSearchParams({ user_id: userId }); diff --git a/packages/cli/src/commands/memory/list.ts b/packages/commands/src/commands/memory/list.ts similarity index 83% rename from packages/cli/src/commands/memory/list.ts rename to packages/commands/src/commands/memory/list.ts index d01f013..77ce5b9 100644 --- a/packages/cli/src/commands/memory/list.ts +++ b/packages/commands/src/commands/memory/list.ts @@ -7,26 +7,22 @@ import { type GlobalFlags, type MemoryNodeListResponse, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "memory list", description: "List memory nodes for a user", - usage: "bl memory list --user-id [flags]", + usageArgs: "--user-id [flags]", options: [ { flag: "--user-id ", description: "User ID (required)", required: true }, { flag: "--page-size ", description: "Results per page (default: 10)", type: "number" }, { flag: "--page ", description: "Page number (default: 1)", type: "number" }, { flag: "--memory-library-id ", description: "Memory library ID" }, ], - examples: [ - "bl memory list --user-id user1", - "bl memory list --user-id user1 --page-size 20 --page 2", - ], + exampleArgs: ["--user-id user1", "--user-id user1 --page-size 20 --page 2"], async run(config: Config, flags: GlobalFlags) { const userId = flags.userId as string; - if (!userId) failIfMissing("user-id", "bl memory list --user-id "); + if (!userId) failIfMissing("user-id", cmdUsage(config, "--user-id ")); const format = detectOutputFormat(config.output); const params = new URLSearchParams(); diff --git a/packages/cli/src/commands/memory/profile-create.ts b/packages/commands/src/commands/memory/profile-create.ts similarity index 74% rename from packages/cli/src/commands/memory/profile-create.ts rename to packages/commands/src/commands/memory/profile-create.ts index 3b2b1be..5f61de3 100644 --- a/packages/cli/src/commands/memory/profile-create.ts +++ b/packages/commands/src/commands/memory/profile-create.ts @@ -8,13 +8,12 @@ import { type ProfileSchemaCreateRequest, type ProfileSchemaCreateResponse, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "memory profile create", description: "Create a user profile schema for memory profiling", - usage: "bl memory profile create --name --attributes [flags]", + usageArgs: "--name --attributes [flags]", options: [ { flag: "--name ", description: "Schema name (required)", required: true }, { flag: "--description ", description: "Schema description" }, @@ -24,16 +23,16 @@ export default defineCommand({ required: true, }, ], - examples: [ - 'bl memory profile create --name "user_basic" --attributes \'[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]\'', + exampleArgs: [ + '--name "user_basic" --attributes \'[{"name":"age","description":"age"},{"name":"hobby","description":"hobby"}]\'', ], async run(config: Config, flags: GlobalFlags) { const name = flags.name as string; - if (!name) failIfMissing("name", "bl memory profile create --name --attributes "); + if (!name) failIfMissing("name", cmdUsage(config, "--name --attributes ")); const attrStr = flags.attributes as string; if (!attrStr) - failIfMissing("attributes", "bl memory profile create --name --attributes "); + failIfMissing("attributes", cmdUsage(config, "--name --attributes ")); let attributes; try { diff --git a/packages/cli/src/commands/memory/profile-get.ts b/packages/commands/src/commands/memory/profile-get.ts similarity index 73% rename from packages/cli/src/commands/memory/profile-get.ts rename to packages/commands/src/commands/memory/profile-get.ts index 33b51e1..9ce1092 100644 --- a/packages/cli/src/commands/memory/profile-get.ts +++ b/packages/commands/src/commands/memory/profile-get.ts @@ -7,25 +7,23 @@ import { type GlobalFlags, type UserProfileResponse, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "memory profile get", description: "Get user profile by schema ID and user ID", - usage: "bl memory profile get --schema-id --user-id ", + usageArgs: "--schema-id --user-id ", options: [ { flag: "--schema-id ", description: "Profile schema ID (required)", required: true }, { flag: "--user-id ", description: "User ID (required)", required: true }, ], - examples: ["bl memory profile get --schema-id schema_xxx --user-id user1"], + exampleArgs: ["--schema-id schema_xxx --user-id user1"], async run(config: Config, flags: GlobalFlags) { const schemaId = flags.schemaId as string; - if (!schemaId) - failIfMissing("schema-id", "bl memory profile get --schema-id --user-id "); + if (!schemaId) failIfMissing("schema-id", cmdUsage(config, "--schema-id --user-id ")); const userId = flags.userId as string; - if (!userId) failIfMissing("user-id", "bl memory profile get --schema-id --user-id "); + if (!userId) failIfMissing("user-id", cmdUsage(config, "--schema-id --user-id ")); const format = detectOutputFormat(config.output); const params = new URLSearchParams({ user_id: userId }); diff --git a/packages/cli/src/commands/memory/search.ts b/packages/commands/src/commands/memory/search.ts similarity index 83% rename from packages/cli/src/commands/memory/search.ts rename to packages/commands/src/commands/memory/search.ts index 21926d2..d7d4522 100644 --- a/packages/cli/src/commands/memory/search.ts +++ b/packages/commands/src/commands/memory/search.ts @@ -8,13 +8,12 @@ import { type MemorySearchRequest, type MemorySearchResponse, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "memory search", description: "Search memory nodes by query or messages", - usage: "bl memory search --user-id [--query ] [flags]", + usageArgs: "--user-id [--query ] [flags]", options: [ { flag: "--user-id ", description: "User ID (required)", required: true }, { flag: "--query ", description: "Search query text" }, @@ -26,13 +25,13 @@ export default defineCommand({ }, { flag: "--memory-library-id ", description: "Memory library ID" }, ], - examples: [ - 'bl memory search --user-id user1 --query "programming preferences"', - 'bl memory search --user-id user1 --messages \'[{"role":"user","content":"recommend a book"}]\' --top-k 5', + exampleArgs: [ + '--user-id user1 --query "programming preferences"', + '--user-id user1 --messages \'[{"role":"user","content":"recommend a book"}]\' --top-k 5', ], async run(config: Config, flags: GlobalFlags) { const userId = flags.userId as string; - if (!userId) failIfMissing("user-id", "bl memory search --user-id "); + if (!userId) failIfMissing("user-id", cmdUsage(config, "--user-id ")); const body: MemorySearchRequest = { user_id: userId }; diff --git a/packages/cli/src/commands/memory/update.ts b/packages/commands/src/commands/memory/update.ts similarity index 72% rename from packages/cli/src/commands/memory/update.ts rename to packages/commands/src/commands/memory/update.ts index e3e6586..431327b 100644 --- a/packages/cli/src/commands/memory/update.ts +++ b/packages/commands/src/commands/memory/update.ts @@ -7,13 +7,12 @@ import { type GlobalFlags, type MemoryNodeUpdateRequest, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "memory update", description: "Update a memory node content", - usage: "bl memory update --node-id --user-id --content ", + usageArgs: "--node-id --user-id --content ", options: [ { flag: "--node-id ", description: "Memory node ID (required)", required: true }, { flag: "--user-id ", description: "User ID (required)", required: true }, @@ -24,21 +23,19 @@ export default defineCommand({ }, { flag: "--memory-library-id ", description: "Memory library ID (non-default library)" }, ], - examples: [ - 'bl memory update --node-id node_xxx --user-id user1 --content "updated memory content"', - ], + exampleArgs: ['--node-id node_xxx --user-id user1 --content "updated memory content"'], async run(config: Config, flags: GlobalFlags) { const nodeId = flags.nodeId as string; if (!nodeId) - failIfMissing("node-id", "bl memory update --node-id --user-id --content "); + failIfMissing("node-id", cmdUsage(config, "--node-id --user-id --content ")); const userId = flags.userId as string; if (!userId) - failIfMissing("user-id", "bl memory update --node-id --user-id --content "); + failIfMissing("user-id", cmdUsage(config, "--node-id --user-id --content ")); const content = flags.content as string; if (!content) - failIfMissing("content", "bl memory update --node-id --user-id --content "); + failIfMissing("content", cmdUsage(config, "--node-id --user-id --content ")); const body: MemoryNodeUpdateRequest = { user_id: userId, diff --git a/packages/cli/src/commands/omni/chat.ts b/packages/commands/src/commands/omni/chat.ts similarity index 93% rename from packages/cli/src/commands/omni/chat.ts rename to packages/commands/src/commands/omni/chat.ts index cf71ff9..5ff15b0 100644 --- a/packages/cli/src/commands/omni/chat.ts +++ b/packages/commands/src/commands/omni/chat.ts @@ -17,8 +17,8 @@ import { isInteractive, resolveFileUrl, } from "bailian-cli-core"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult } from "../../output/output.ts"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult } from "bailian-cli-runtime"; import { resolveOutputDir, resolveCredential } from "bailian-cli-core"; const OMNI_VOICES = ["Chelsie", "Cherry", "Ethan", "Serena", "Sunny", "Tina"]; @@ -85,9 +85,8 @@ function buildWavHeader(dataLength: number): Buffer { } export default defineCommand({ - name: "omni", description: "Multimodal chat with text + audio output (Qwen-Omni)", - usage: "bl omni --message [flags]", + usageArgs: "--message [flags]", options: [ { flag: "--message ", @@ -118,15 +117,15 @@ export default defineCommand({ { flag: "--max-tokens ", description: "Maximum tokens to generate", type: "number" }, { flag: "--temperature ", description: "Sampling temperature (0.0, 2.0]", type: "number" }, ], - examples: [ - 'bl omni --message "Hello, who are you?"', - 'bl omni --message "Describe this image" --image ./photo.jpg', - 'bl omni --message "What is this audio saying?" --audio https://example.com/audio.wav', - 'bl omni --message "Summarize this video" --video https://example.com/video.mp4', - 'bl omni --message "What is this video about?" --video ./local-video.mp4 --text-only', - 'bl omni --message "Answer in Sichuan dialect: How\'s the weather today?" --voice Sunny', - 'bl omni --message "Hello" --text-only --output json', - 'bl omni --message "Read this passage aloud" --audio-out greeting.wav', + exampleArgs: [ + '--message "Hello, who are you?"', + '--message "Describe this image" --image ./photo.jpg', + '--message "What is this audio saying?" --audio https://example.com/audio.wav', + '--message "Summarize this video" --video https://example.com/video.mp4', + '--message "What is this video about?" --video ./local-video.mp4 --text-only', + '--message "Answer in Sichuan dialect: How\'s the weather today?" --voice Sunny', + '--message "Hello" --text-only --output json', + '--message "Read this passage aloud" --audio-out greeting.wav', ], async run(config: Config, flags: GlobalFlags) { // --- Parse messages --- @@ -144,7 +143,7 @@ export default defineCommand({ } userMessages = [hint]; } else { - failIfMissing("message", "bl text omni --message "); + failIfMissing("message", cmdUsage(config, "--message ")); } } diff --git a/packages/cli/src/commands/pipeline/load-file.ts b/packages/commands/src/commands/pipeline/load-file.ts similarity index 91% rename from packages/cli/src/commands/pipeline/load-file.ts rename to packages/commands/src/commands/pipeline/load-file.ts index a6a61e2..b7a24c3 100644 --- a/packages/cli/src/commands/pipeline/load-file.ts +++ b/packages/commands/src/commands/pipeline/load-file.ts @@ -1,6 +1,6 @@ import { readFile } from "node:fs/promises"; import { extname } from "node:path"; -import type { PipelineDefinition } from "../../pipeline/types.ts"; +import type { PipelineDefinition } from "bailian-cli-runtime"; export async function loadPipelineFile(filePath: string): Promise { const raw = await readFile(filePath, "utf-8").catch((err: Error) => { diff --git a/packages/cli/src/commands/pipeline/run.ts b/packages/commands/src/commands/pipeline/run.ts similarity index 85% rename from packages/cli/src/commands/pipeline/run.ts rename to packages/commands/src/commands/pipeline/run.ts index ee7c780..c3dd3d0 100644 --- a/packages/cli/src/commands/pipeline/run.ts +++ b/packages/commands/src/commands/pipeline/run.ts @@ -1,17 +1,16 @@ import { readFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { defineCommand, type Config, type GlobalFlags } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { initPipelineSteps } from "../../pipeline/init.ts"; -import { executePipeline, streamPipelineEvents } from "../../pipeline/executor.ts"; -import type { PipelineLifecycleEvent } from "../../pipeline/types.ts"; +import { emitResult, cmdUsage } from "bailian-cli-runtime"; +import { initPipelineSteps } from "bailian-cli-runtime"; +import { executePipeline, streamPipelineEvents } from "bailian-cli-runtime"; +import type { PipelineLifecycleEvent } from "bailian-cli-runtime"; import { loadPipelineFile } from "./load-file.ts"; export default defineCommand({ - name: "pipeline run", description: "Run a pipeline workflow definition", skipDefaultApiKeySetup: true, - usage: "bl pipeline run [flags]", + usageArgs: " [flags]", options: [ { flag: "--input ", description: "Runtime input as inline JSON" }, { flag: "--input-file ", description: "Runtime input from a JSON file" }, @@ -27,17 +26,19 @@ export default defineCommand({ type: "number", }, ], - examples: [ - 'bl pipeline run workflow.yaml --input \'{"brief":"hello"}\'', - "bl pipeline run workflow.json --input-file inputs.json --concurrency 3", - "bl pipeline run workflow.yaml --dry-run", - "bl pipeline run workflow.json --events jsonl", - "bl pipeline run workflow.yaml --output json", + exampleArgs: [ + 'workflow.yaml --input \'{"brief":"hello"}\'', + "workflow.json --input-file inputs.json --concurrency 3", + "workflow.yaml --dry-run", + "workflow.json --events jsonl", + "workflow.yaml --output json", ], async run(config: Config, flags: GlobalFlags) { const file = ((flags._positional as string[] | undefined) ?? [])[0] as string | undefined; if (!file) { - process.stderr.write("Error: pipeline file is required\nUsage: bl pipeline run \n"); + process.stderr.write( + `Error: pipeline file is required\nUsage: ${cmdUsage(config, "")}\n`, + ); process.exit(2); } diff --git a/packages/cli/src/commands/pipeline/validate.ts b/packages/commands/src/commands/pipeline/validate.ts similarity index 74% rename from packages/cli/src/commands/pipeline/validate.ts rename to packages/commands/src/commands/pipeline/validate.ts index c32ddda..27cd2b3 100644 --- a/packages/cli/src/commands/pipeline/validate.ts +++ b/packages/commands/src/commands/pipeline/validate.ts @@ -1,25 +1,21 @@ import { resolve } from "node:path"; import { defineCommand, type Config, type GlobalFlags } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { initPipelineSteps } from "../../pipeline/init.ts"; -import { collectPipelineIssues, collectPipelineHints } from "../../pipeline/validation.ts"; +import { emitResult, cmdUsage } from "bailian-cli-runtime"; +import { initPipelineSteps } from "bailian-cli-runtime"; +import { collectPipelineIssues, collectPipelineHints } from "bailian-cli-runtime"; import { loadPipelineFile } from "./load-file.ts"; export default defineCommand({ - name: "pipeline validate", description: "Validate a pipeline definition without executing", skipDefaultApiKeySetup: true, - usage: "bl pipeline validate ", + usageArgs: "", options: [], - examples: [ - "bl pipeline validate workflow.yaml", - "bl pipeline validate workflow.json --output json", - ], + exampleArgs: ["workflow.yaml", "workflow.json --output json"], async run(config: Config, flags: GlobalFlags) { const file = ((flags._positional as string[] | undefined) ?? [])[0] as string | undefined; if (!file) { process.stderr.write( - "Error: pipeline file is required\nUsage: bl pipeline validate \n", + `Error: pipeline file is required\nUsage: ${cmdUsage(config, "")}\n`, ); process.exit(2); } diff --git a/packages/cli/src/commands/quota/check.ts b/packages/commands/src/commands/quota/check.ts similarity index 96% rename from packages/cli/src/commands/quota/check.ts rename to packages/commands/src/commands/quota/check.ts index fdc8e5d..817caa6 100644 --- a/packages/cli/src/commands/quota/check.ts +++ b/packages/commands/src/commands/quota/check.ts @@ -7,8 +7,8 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { displayWidth, padEnd } from "../../output/cjk-width.ts"; +import { emitResult } from "bailian-cli-runtime"; +import { displayWidth, padEnd } from "bailian-cli-runtime"; const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels"; const MONITOR_API = "zeldaEasy.bailian-telemetry.monitor.getMonitorData"; @@ -235,10 +235,9 @@ function printTable(rows: CheckRow[], noColor: boolean): void { } export default defineCommand({ - name: "quota check", description: "Check current usage against rate limits", skipDefaultApiKeySetup: true, - usage: "bl quota check [--model ] [flags]", + usageArgs: "[--model ] [flags]", options: [ { flag: "--model ", @@ -259,12 +258,12 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl quota check", - "bl quota check --model qwen3.6-plus", - "bl quota check --period 5", - "bl quota check --model qwen3.6-plus,qwen-turbo", - "bl quota check --output json", + exampleArgs: [ + "", + "--model qwen3.6-plus", + "--period 5", + "--model qwen3.6-plus,qwen-turbo", + "--output json", ], async run(config: Config, flags: GlobalFlags) { const modelFlag = (flags.model as string) || undefined; diff --git a/packages/cli/src/commands/quota/history.ts b/packages/commands/src/commands/quota/history.ts similarity index 91% rename from packages/cli/src/commands/quota/history.ts rename to packages/commands/src/commands/quota/history.ts index f3035db..0a4e0f9 100644 --- a/packages/cli/src/commands/quota/history.ts +++ b/packages/commands/src/commands/quota/history.ts @@ -7,8 +7,8 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { displayWidth, padEnd } from "../../output/cjk-width.ts"; +import { emitResult } from "bailian-cli-runtime"; +import { displayWidth, padEnd } from "bailian-cli-runtime"; const HISTORY_API = "zeldaEasy.broadscope-platform.modelInstance.listModelLimitApplications"; @@ -91,10 +91,9 @@ function printTable(records: LimitApplicationItem[], noColor: boolean, total: nu } export default defineCommand({ - name: "quota history", description: "View quota change history", skipDefaultApiKeySetup: true, - usage: "bl quota history [flags]", + usageArgs: "[flags]", options: [ { flag: "--page ", @@ -119,13 +118,7 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl quota history", - "bl quota history --page 2", - "bl quota history --page-size 20", - "bl quota history --model qwen-turbo", - "bl quota history --output json", - ], + exampleArgs: ["", "--page 2", "--page-size 20", "--model qwen-turbo", "--output json"], async run(config: Config, flags: GlobalFlags) { const page = Number(flags.page) || 1; const pageSize = Number(flags.pageSize) || 10; @@ -152,7 +145,7 @@ export default defineCommand({ } catch (err) { if (err instanceof BailianError && err.message.includes("NotLogined")) { process.stderr.write( - "Error: session expired. Run `bl auth login --console` to re-authenticate.\n", + `Error: session expired. Run \`${config.binName} auth login --console\` to re-authenticate.\n`, ); process.exit(1); } diff --git a/packages/cli/src/commands/quota/list.ts b/packages/commands/src/commands/quota/list.ts similarity index 94% rename from packages/cli/src/commands/quota/list.ts rename to packages/commands/src/commands/quota/list.ts index e0eb7c4..7129fac 100644 --- a/packages/cli/src/commands/quota/list.ts +++ b/packages/commands/src/commands/quota/list.ts @@ -6,8 +6,8 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { displayWidth, padEnd } from "../../output/cjk-width.ts"; +import { emitResult } from "bailian-cli-runtime"; +import { displayWidth, padEnd } from "bailian-cli-runtime"; const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels"; @@ -150,10 +150,9 @@ function printTable(models: ModelWithQpm[], noColor: boolean): void { } export default defineCommand({ - name: "quota list", description: "View model RPM/TPM rate limits", skipDefaultApiKeySetup: true, - usage: "bl quota list [--model ] [flags]", + usageArgs: "[--model ] [flags]", options: [ { flag: "--model ", @@ -174,12 +173,12 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl quota list", - "bl quota list --model qwen3.6-plus", - "bl quota list --model qwen3.6-plus,qwen-turbo", - "bl quota list --all", - "bl quota list --output json", + exampleArgs: [ + "", + "--model qwen3.6-plus", + "--model qwen3.6-plus,qwen-turbo", + "--all", + "--output json", ], async run(config: Config, flags: GlobalFlags) { const modelFlag = (flags.model as string) || undefined; diff --git a/packages/cli/src/commands/quota/request.ts b/packages/commands/src/commands/quota/request.ts similarity index 92% rename from packages/cli/src/commands/quota/request.ts rename to packages/commands/src/commands/quota/request.ts index ae1c42f..b5b024a 100644 --- a/packages/cli/src/commands/quota/request.ts +++ b/packages/commands/src/commands/quota/request.ts @@ -7,7 +7,7 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; +import { emitResult } from "bailian-cli-runtime"; const MODEL_LIST_API = "zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels"; const UPDATE_LIMITS_API = "zeldaEasy.broadscope-platform.modelInstance.updateFoundationModelLimits"; @@ -78,10 +78,9 @@ async function fetchModelQpmInfo( } export default defineCommand({ - name: "quota request", description: "Request a temporary quota increase", skipDefaultApiKeySetup: true, - usage: "bl quota request --model --tpm [flags]", + usageArgs: "--model --tpm [flags]", options: [ { flag: "--model ", @@ -108,10 +107,10 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl quota request --model qwen-turbo --tpm 100000", - "bl quota request --model qwen3.6-plus --tpm 8000000 --yes", - "bl quota request --model qwen-turbo --tpm 100000 --output json", + exampleArgs: [ + "--model qwen-turbo --tpm 100000", + "--model qwen3.6-plus --tpm 8000000 --yes", + "--model qwen-turbo --tpm 100000 --output json", ], async run(config: Config, flags: GlobalFlags) { const modelName = flags.model as string; @@ -147,7 +146,9 @@ export default defineCommand({ process.stderr.write( `Error: model "${modelName}" not found or does not support self-service quota increase.\n`, ); - process.stderr.write("Hint: run `bl quota list` to view available models.\n"); + process.stderr.write( + `Hint: run \`${config.binName} quota list\` to view available models.\n`, + ); process.exit(1); } @@ -186,7 +187,7 @@ export default defineCommand({ } catch (err) { if (err instanceof BailianError && err.message.includes("NotLogined")) { process.stderr.write( - "Error: session expired. Run `bl auth login --console` to re-authenticate.\n", + `Error: session expired. Run \`${config.binName} auth login --console\` to re-authenticate.\n`, ); process.exit(1); } diff --git a/packages/cli/src/commands/search/web.ts b/packages/commands/src/commands/search/web.ts similarity index 87% rename from packages/cli/src/commands/search/web.ts rename to packages/commands/src/commands/search/web.ts index 8efadc7..a7d9594 100644 --- a/packages/cli/src/commands/search/web.ts +++ b/packages/commands/src/commands/search/web.ts @@ -7,24 +7,23 @@ import { isInteractive, McpClient, } from "bailian-cli-core"; -import { createSpinner } from "../../output/progress.ts"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult } from "../../output/output.ts"; +import { createSpinner } from "bailian-cli-runtime"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult } from "bailian-cli-runtime"; export default defineCommand({ - name: "search web", description: "Search the web using DashScope MCP WebSearch service", - usage: "bl search web --query [flags]", + usageArgs: "--query [flags]", options: [ { flag: "--query ", description: "Search query text", required: true }, { flag: "--count ", description: "Number of search results (default: 10)", type: "number" }, { flag: "--list-tools", description: "List available MCP tools and exit" }, ], - examples: [ - 'bl search web --query "Alibaba Cloud Bailian latest features"', - 'bl search web --query "TypeScript 5.9 new features" --count 5', - 'bl search web --query "Today\'s news"', - "bl search web --list-tools", + exampleArgs: [ + '--query "Alibaba Cloud Bailian latest features"', + '--query "TypeScript 5.9 new features" --count 5', + '--query "Today\'s news"', + "--list-tools", ], async run(config: Config, flags: GlobalFlags) { const mcpUrl = mcpWebSearchEndpoint(config.baseUrl); @@ -56,7 +55,7 @@ export default defineCommand({ } query = hint; } else { - failIfMissing("query", "bl search web --query "); + failIfMissing("query", cmdUsage(config, "--query ")); } } diff --git a/packages/cli/src/commands/speech/recognize.ts b/packages/commands/src/commands/speech/recognize.ts similarity index 89% rename from packages/cli/src/commands/speech/recognize.ts rename to packages/commands/src/commands/speech/recognize.ts index 05e8874..c26d702 100644 --- a/packages/cli/src/commands/speech/recognize.ts +++ b/packages/commands/src/commands/speech/recognize.ts @@ -18,14 +18,13 @@ import { type OutputFormat, speechRecognizeEndpoint, } from "bailian-cli-core"; -import { poll } from "../../utils/polling.ts"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { poll } from "bailian-cli-runtime"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "speech recognize", description: "Recognize speech from audio files (FunAudio-ASR)", - usage: "bl speech recognize --url [flags]", + usageArgs: "--url [flags]", options: [ { flag: "--url ", @@ -51,14 +50,14 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl speech recognize --url https://example.com/audio.mp3", - "bl speech recognize --url https://example.com/a.mp3 --url https://example.com/b.mp3", - "bl speech recognize --url https://example.com/meeting.wav --diarization --speaker-count 3", - "bl speech recognize --url https://example.com/audio.mp3 --language zh", - "bl speech recognize --url https://example.com/audio.mp3 --vocabulary-id vocab-abc123", - "bl speech recognize --url https://example.com/audio.mp3 --out result.json", - "bl speech recognize --url https://example.com/audio.mp3 --no-wait --quiet", + exampleArgs: [ + "--url https://example.com/audio.mp3", + "--url https://example.com/a.mp3 --url https://example.com/b.mp3", + "--url https://example.com/meeting.wav --diarization --speaker-count 3", + "--url https://example.com/audio.mp3 --language zh", + "--url https://example.com/audio.mp3 --vocabulary-id vocab-abc123", + "--url https://example.com/audio.mp3 --out result.json", + "--url https://example.com/audio.mp3 --no-wait --quiet", ], async run(config: Config, flags: GlobalFlags) { // Normalize --url to string[] (supports both single and repeated flags) @@ -69,7 +68,7 @@ export default defineCommand({ rawUrls = [flags.url]; } if (rawUrls.length === 0) { - failIfMissing("url", "bl speech recognize --url "); + failIfMissing("url", cmdUsage(config, "--url ")); } // Strict validation: --speaker-count requires --diarization diff --git a/packages/cli/src/commands/speech/synthesize.ts b/packages/commands/src/commands/speech/synthesize.ts similarity index 93% rename from packages/cli/src/commands/speech/synthesize.ts rename to packages/commands/src/commands/speech/synthesize.ts index 23d9874..2df8c8f 100644 --- a/packages/cli/src/commands/speech/synthesize.ts +++ b/packages/commands/src/commands/speech/synthesize.ts @@ -21,10 +21,10 @@ import { } from "bailian-cli-core"; const COSYVOICE_CLONE_DESIGN_DOC = `${DOCS_HOSTS.cn}/cosyvoice-clone-design-api`; -import { downloadFile } from "../../utils/download.ts"; -import { runConcurrent, downloadParallel, getConcurrency } from "../../utils/concurrent.ts"; -import { promptText, promptSelect, failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { downloadFile } from "bailian-cli-runtime"; +import { runConcurrent, downloadParallel, getConcurrency } from "bailian-cli-runtime"; +import { promptText, promptSelect, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; interface VoiceEntry { voice: string; @@ -142,9 +142,8 @@ function printVoiceList(model: string): void { } export default defineCommand({ - name: "speech synthesize", description: "Synthesize speech from text (CosyVoice TTS)", - usage: "bl speech synthesize --text [flags]", + usageArgs: "--text [flags]", options: [ { flag: "--text ", description: "Text to synthesize into speech", required: true }, { flag: "--text-file ", description: "Read text from a file instead of --text" }, @@ -181,17 +180,17 @@ export default defineCommand({ }, { flag: "--stream", description: "Stream raw PCM audio to stdout (pipe to player)" }, ], - examples: [ - "bl speech synthesize --list-voices --model cosyvoice-v3-flash", - 'bl speech synthesize --text "Hello, I am Qwen" --voice ', - 'bl speech synthesize --text "Hello world" --voice --language en', - "bl speech synthesize --text-file script.txt --out speech.wav --voice ", - 'bl speech synthesize --text "Today is a good day" --voice --instruction "Use a gentle tone"', - 'bl speech synthesize --text "Hello" --voice --format wav --sample-rate 24000', + exampleArgs: [ + "--list-voices --model cosyvoice-v3-flash", + '--text "Hello, I am Qwen" --voice ', + '--text "Hello world" --voice --language en', + "--text-file script.txt --out speech.wav --voice ", + '--text "Today is a good day" --voice --instruction "Use a gentle tone"', + '--text "Hello" --voice --format wav --sample-rate 24000', "# Stream to audio player (macOS)", - 'bl speech synthesize --text "Hello" --voice --stream | afplay -', + '--text "Hello" --voice --stream | afplay -', "# Pipe to ffplay", - 'bl speech synthesize --text "Hello" --voice --stream | ffplay -nodisp -autoexit -f s16le -ar 24000 -ac 1 -', + '--text "Hello" --voice --stream | ffplay -nodisp -autoexit -f s16le -ar 24000 -ac 1 -', ], async run(config: Config, flags: GlobalFlags) { const model = (flags.model as string) || config.defaultSpeechModel || "cosyvoice-v3-flash"; @@ -223,7 +222,7 @@ export default defineCommand({ } text = hint; } else { - failIfMissing("text", "bl speech synthesize --text "); + failIfMissing("text", cmdUsage(config, "--text ")); } } @@ -264,7 +263,7 @@ export default defineCommand({ const modelVoices = MODEL_VOICES[model]; if (modelVoices && modelVoices.length > 0) { throw new BailianError( - `--voice is required.\nRun the following to see available voices:\n bl speech synthesize --list-voices --model ${model}`, + `--voice is required.\nRun the following to see available voices:\n ${cmdUsage(config, `--list-voices --model ${model}`)}`, ExitCode.USAGE, ); } else { diff --git a/packages/cli/src/commands/text/chat.ts b/packages/commands/src/commands/text/chat.ts similarity index 90% rename from packages/cli/src/commands/text/chat.ts rename to packages/commands/src/commands/text/chat.ts index 9741c6d..78a03df 100644 --- a/packages/cli/src/commands/text/chat.ts +++ b/packages/commands/src/commands/text/chat.ts @@ -13,8 +13,8 @@ import { type StreamChunk, isInteractive, } from "bailian-cli-core"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; import { readFileSync } from "fs"; interface ParsedMessages { @@ -68,9 +68,8 @@ function parseMessages(flags: GlobalFlags): ParsedMessages { } export default defineCommand({ - name: "text chat", description: "Send a chat completion (OpenAI compatible, DashScope)", - usage: "bl text chat --message [flags]", + usageArgs: "--message [flags]", options: [ { flag: "--model ", description: "Model ID (default: qwen3.7-max)" }, { @@ -107,13 +106,13 @@ export default defineCommand({ type: "number", }, ], - examples: [ - 'bl text chat --message "What is Qwen?"', - 'bl text chat --model qwen-max --system "You are a coding assistant." --message "Write fizzbuzz in Python"', - 'bl text chat --message "Hello" --message "assistant:Hi!" --message "How are you?"', - "cat conversation.json | bl text chat --messages-file - --stream", - 'bl text chat --message "Hello" --output json', - 'bl text chat --model qwq-plus --message "Solve 1+1" --enable-thinking', + exampleArgs: [ + '--message "What is Qwen?"', + '--model qwen-max --system "You are a coding assistant." --message "Write fizzbuzz in Python"', + '--message "Hello" --message "assistant:Hi!" --message "How are you?"', + "--messages-file - --stream", + '--message "Hello" --output json', + '--model qwq-plus --message "Solve 1+1" --enable-thinking', ], async run(config: Config, flags: GlobalFlags) { const { system, messages: parsedMessages } = parseMessages(flags); @@ -130,7 +129,7 @@ export default defineCommand({ } messages = [{ role: "user", content: hint }]; } else { - failIfMissing("message", "bl text chat --message "); + failIfMissing("message", cmdUsage(config, "--message ")); } } diff --git a/packages/cli/src/commands/update.ts b/packages/commands/src/commands/update.ts similarity index 65% rename from packages/cli/src/commands/update.ts rename to packages/commands/src/commands/update.ts index 353c477..c8a8a27 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/commands/src/commands/update.ts @@ -2,15 +2,14 @@ import { execSync } from "child_process"; import { writeFileSync } from "fs"; import { join } from "path"; import { defineCommand, getConfigDir } from "bailian-cli-core"; -import { CLI_VERSION } from "../version.ts"; -import { NPM_PACKAGE, fetchLatestVersion } from "../utils/update-checker.ts"; +import { fetchLatestVersion } from "bailian-cli-runtime"; const SKILL_SOURCE = "modelstudioai/cli"; const SKILL_INSTALL_CMD = `npx skills add ${SKILL_SOURCE} --all -g -y`; -/** Build the install command */ -function detectInstallCommand(): { cmd: string; label: string } { - return { cmd: `npm install -g ${NPM_PACKAGE}@latest`, label: "npm" }; +/** Build the install command for the given npm package. */ +function detectInstallCommand(npmPackage: string): { cmd: string; label: string } { + return { cmd: `npm install -g ${npmPackage}@latest`, label: "npm" }; } function updateAgentSkill(colors: { green: string; yellow: string; reset: string }): void { @@ -29,25 +28,26 @@ function updateAgentSkill(colors: { green: string; yellow: string; reset: string } export default defineCommand({ - name: "update", - description: "Update bl to the latest version", + description: "Update the CLI to the latest version", skipDefaultApiKeySetup: true, - usage: "bl update", - examples: ["bl update"], - async run() { + exampleArgs: [""], + async run(config) { + const npmPackage = config.npmPackage!; + const binName = config.binName!; + const currentVersion = config.clientVersion!; const isTTY = process.stderr.isTTY; const green = isTTY ? "\x1b[32m" : ""; const yellow = isTTY ? "\x1b[33m" : ""; const reset = isTTY ? "\x1b[0m" : ""; - process.stderr.write(`Current version: ${yellow}${CLI_VERSION}${reset}\n`); + process.stderr.write(`Current version: ${yellow}${currentVersion}${reset}\n`); // Check latest version first process.stderr.write("Checking for updates...\n"); - const latest = await fetchLatestVersion(5000); + const latest = await fetchLatestVersion(5000, npmPackage); - if (latest && latest === CLI_VERSION) { - process.stderr.write(`${green}\u2713 Already up to date (${CLI_VERSION}).${reset}\n`); + if (latest && latest === currentVersion) { + process.stderr.write(`${green}\u2713 Already up to date (${currentVersion}).${reset}\n`); updateAgentSkill({ green, yellow, reset }); return; } @@ -56,18 +56,18 @@ export default defineCommand({ process.stderr.write(`Latest version: ${green}${latest}${reset}\n\n`); } - const { cmd, label } = detectInstallCommand(); - process.stderr.write(`Updating ${NPM_PACKAGE} via ${label}...\n\n`); + const { cmd, label } = detectInstallCommand(npmPackage); + process.stderr.write(`Updating ${npmPackage} via ${label}...\n\n`); try { execSync(cmd, { stdio: "inherit" }); // Verify the installed version after update try { - const rawVer = execSync("bl --version 2>/dev/null", { encoding: "utf-8" }).trim(); - // bl --version outputs "bl X.Y.Z" — extract just the version number - const newVer = rawVer.replace(/^bl\s+/, ""); + const rawVer = execSync(`${binName} --version 2>/dev/null`, { encoding: "utf-8" }).trim(); + // ` --version` outputs " X.Y.Z" — extract just the version number + const newVer = rawVer.replace(new RegExp(`^${binName}\\s+`), ""); process.stderr.write( - `\n${green}\u2713 Update complete: ${CLI_VERSION} \u2192 ${newVer}${reset}\n`, + `\n${green}\u2713 Update complete: ${currentVersion} \u2192 ${newVer}${reset}\n`, ); // Update the cached state so the post-run notification doesn't fire try { diff --git a/packages/cli/src/commands/usage/free.ts b/packages/commands/src/commands/usage/free.ts similarity index 95% rename from packages/cli/src/commands/usage/free.ts rename to packages/commands/src/commands/usage/free.ts index b0d63b5..b6361af 100644 --- a/packages/cli/src/commands/usage/free.ts +++ b/packages/commands/src/commands/usage/free.ts @@ -7,8 +7,8 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { displayWidth, padEnd } from "../../output/cjk-width.ts"; +import { emitResult } from "bailian-cli-runtime"; +import { displayWidth, padEnd } from "bailian-cli-runtime"; const FREE_TIER_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierQuota"; const FREE_TIER_ONLY_STATUS_API = "zeldaEasy.broadscope-bailian.freeTrial.queryFreeTierOnlyStatus"; @@ -184,10 +184,9 @@ async function fetchAllModels(config: Config, token: string): Promise[,model2,...]] [flags]", + usageArgs: "[--model [,model2,...]] [flags]", options: [ { flag: "--model ", @@ -212,14 +211,14 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl usage free", - "bl usage free --model qwen3-max", - "bl usage free --model qwen3-max,qwen-turbo", - "bl usage free --expiring 30", - "bl usage free --sort remaining", - "bl usage free --model qwen-turbo --output json", - "bl usage free --model qwen3-max --console-region cn-beijing", + exampleArgs: [ + "", + "--model qwen3-max", + "--model qwen3-max,qwen-turbo", + "--expiring 30", + "--sort remaining", + "--model qwen-turbo --output json", + "--model qwen3-max --console-region cn-beijing", ], async run(config: Config, flags: GlobalFlags) { const modelFlag = (flags.model as string) || undefined; diff --git a/packages/cli/src/commands/usage/freetier.ts b/packages/commands/src/commands/usage/freetier.ts similarity index 94% rename from packages/cli/src/commands/usage/freetier.ts rename to packages/commands/src/commands/usage/freetier.ts index 95ddb65..6171513 100644 --- a/packages/cli/src/commands/usage/freetier.ts +++ b/packages/commands/src/commands/usage/freetier.ts @@ -7,7 +7,7 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; +import { emitResult } from "bailian-cli-runtime"; const ACTIVATE_API = "zeldaEasy.broadscope-bailian.freeTrial.batchActivateFreeTierOnly"; const DEACTIVATE_API = "zeldaEasy.broadscope-bailian.freeTrial.batchDeactivateFreeTierOnly"; @@ -100,11 +100,10 @@ async function fetchAllModelNames(config: Config, token: string): Promise[,model2,...] | --all> [--off] [flags]", + usageArgs: "<--model [,model2,...] | --all> [--off] [flags]", options: [ { flag: "--model ", @@ -133,13 +132,13 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl usage freetier --model qwen3-max", - "bl usage freetier --model qwen3-max,qwen-turbo", - "bl usage freetier --all", - "bl usage freetier --on --model qwen3-max", - "bl usage freetier --off --model qwen3-max", - "bl usage freetier --off --all", + exampleArgs: [ + "--model qwen3-max", + "--model qwen3-max,qwen-turbo", + "--all", + "--on --model qwen3-max", + "--off --model qwen3-max", + "--off --all", ], async run(config: Config, flags: GlobalFlags) { const modelFlag = (flags.model as string) || undefined; diff --git a/packages/cli/src/commands/usage/stats.ts b/packages/commands/src/commands/usage/stats.ts similarity index 94% rename from packages/cli/src/commands/usage/stats.ts rename to packages/commands/src/commands/usage/stats.ts index db3e9fa..f33d4a6 100644 --- a/packages/cli/src/commands/usage/stats.ts +++ b/packages/commands/src/commands/usage/stats.ts @@ -6,8 +6,8 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { displayWidth, padEnd } from "../../output/cjk-width.ts"; +import { emitResult } from "bailian-cli-runtime"; +import { displayWidth, padEnd } from "bailian-cli-runtime"; const OVERVIEW_API = "zeldaEasy.bailian-telemetry.model.getModelUsageStatistic"; const LIST_API = "zeldaEasy.bailian-telemetry.model.listModelUsageStatisticData"; @@ -101,9 +101,11 @@ function resolveWorkspaceId(config: Config, flagWorkspaceId?: string): string { if (config.workspaceId) return config.workspaceId; process.stderr.write( - "Error: workspace-id is required. Set via --workspace-id, BAILIAN_WORKSPACE_ID, or `bl config set workspace_id `.\n", + `Error: workspace-id is required. Set via --workspace-id, BAILIAN_WORKSPACE_ID, or \`${config.binName} config set workspace_id \`.\n`, + ); + process.stderr.write( + `Hint: run \`${config.binName} workspace list\` to view available workspaces.\n`, ); - process.stderr.write("Hint: run `bl workspace list` to view available workspaces.\n"); process.exit(1); } @@ -283,10 +285,9 @@ function printModelTable( } export default defineCommand({ - name: "usage stats", description: "Query model usage statistics", skipDefaultApiKeySetup: true, - usage: "bl usage stats [--model ] [--days ] [flags]", + usageArgs: "[--model ] [--days ] [flags]", options: [ { flag: "--model ", @@ -315,14 +316,14 @@ export default defineCommand({ type: "number", }, ], - examples: [ - "bl usage stats", - "bl usage stats --days 30", - "bl usage stats --model qwen-turbo", - "bl usage stats --model qwen-turbo --days 7", - "bl usage stats --model qwen3.6-plus,deepseek-v4-pro", - "bl usage stats --type Text --days 14", - "bl usage stats --output json", + exampleArgs: [ + "", + "--days 30", + "--model qwen-turbo", + "--model qwen-turbo --days 7", + "--model qwen3.6-plus,deepseek-v4-pro", + "--type Text --days 14", + "--output json", ], async run(config: Config, flags: GlobalFlags) { const modelFlag = (flags.model as string) || undefined; diff --git a/packages/cli/src/commands/video/download.ts b/packages/commands/src/commands/video/download.ts similarity index 73% rename from packages/cli/src/commands/video/download.ts rename to packages/commands/src/commands/video/download.ts index 9d3aa67..5e99020 100644 --- a/packages/cli/src/commands/video/download.ts +++ b/packages/commands/src/commands/video/download.ts @@ -9,28 +9,27 @@ import { BailianError, ExitCode, } from "bailian-cli-core"; -import { downloadFile, formatBytes } from "../../utils/download.ts"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { downloadFile, formatBytes } from "bailian-cli-runtime"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "video download", description: "Download a completed video by task ID", - usage: "bl video download --task-id --out ", + usageArgs: "--task-id --out ", options: [ { flag: "--task-id ", description: "Task ID to download from" }, { flag: "--out ", description: "Output file path" }, ], - examples: [ - "bl video download --task-id 3b256896-xxxx --out video.mp4", - "bl video download --task-id 3b256896-xxxx --out video.mp4 --quiet", + exampleArgs: [ + "--task-id 3b256896-xxxx --out video.mp4", + "--task-id 3b256896-xxxx --out video.mp4 --quiet", ], async run(config: Config, flags: GlobalFlags) { const taskId = flags.taskId as string | undefined; - if (!taskId) failIfMissing("task-id", "bl video download --task-id --out "); + if (!taskId) failIfMissing("task-id", cmdUsage(config, "--task-id --out ")); const outPath = flags.out as string | undefined; - if (!outPath) failIfMissing("out", "bl video download --task-id --out video.mp4"); + if (!outPath) failIfMissing("out", cmdUsage(config, "--task-id --out video.mp4")); const format = detectOutputFormat(config.output); diff --git a/packages/cli/src/commands/video/edit.ts b/packages/commands/src/commands/video/edit.ts similarity index 87% rename from packages/cli/src/commands/video/edit.ts rename to packages/commands/src/commands/video/edit.ts index 0ff6c80..cb2156b 100644 --- a/packages/cli/src/commands/video/edit.ts +++ b/packages/commands/src/commands/video/edit.ts @@ -18,20 +18,16 @@ import { resolveBooleanFlag, resolveWatermark, } from "bailian-cli-core"; -import { poll } from "../../utils/polling.ts"; -import { downloadFile, formatBytes } from "../../utils/download.ts"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; -import { - BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, - BOOL_FLAG_WATERMARK, -} from "../../utils/flag-descriptions.ts"; +import { poll } from "bailian-cli-runtime"; +import { downloadFile, formatBytes } from "bailian-cli-runtime"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime"; export default defineCommand({ - name: "video edit", description: "Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.)", - usage: "bl video edit --video --prompt [flags]", + usageArgs: "--video --prompt [flags]", options: [ { flag: "--model ", description: "Model ID (default: happyhorse-1.0-video-edit)" }, { @@ -80,11 +76,11 @@ export default defineCommand({ type: "number", }, ], - examples: [ - 'bl video edit --video https://example.com/input.mp4 --prompt "Convert the entire scene to claymation style"', - 'bl video edit --video https://example.com/input.mp4 --prompt "Replace the outfit with the style shown in the image" --ref-image https://example.com/clothes.png', - 'bl video edit --video https://example.com/input.mp4 --prompt "Convert to anime style" --resolution 720P --download output.mp4', - 'bl video edit --video https://example.com/input.mp4 --prompt "Put clothes on the kitten in the video" --watermark false', + exampleArgs: [ + '--video https://example.com/input.mp4 --prompt "Convert the entire scene to claymation style"', + '--video https://example.com/input.mp4 --prompt "Replace the outfit with the style shown in the image" --ref-image https://example.com/clothes.png', + '--video https://example.com/input.mp4 --prompt "Convert to anime style" --resolution 720P --download output.mp4', + '--video https://example.com/input.mp4 --prompt "Put clothes on the kitten in the video" --watermark false', ], async run(config: Config, flags: GlobalFlags) { // --- Validate video URL --- @@ -98,7 +94,7 @@ export default defineCommand({ } videoUrl = hint; } else { - failIfMissing("video", "bl video edit --video --prompt "); + failIfMissing("video", cmdUsage(config, "--video --prompt ")); } } diff --git a/packages/cli/src/commands/video/generate.ts b/packages/commands/src/commands/video/generate.ts similarity index 87% rename from packages/cli/src/commands/video/generate.ts rename to packages/commands/src/commands/video/generate.ts index 4888dee..320988a 100644 --- a/packages/cli/src/commands/video/generate.ts +++ b/packages/commands/src/commands/video/generate.ts @@ -18,21 +18,17 @@ import { resolveBooleanFlag, resolveWatermark, } from "bailian-cli-core"; -import { poll } from "../../utils/polling.ts"; -import { downloadFile, formatBytes } from "../../utils/download.ts"; -import { runConcurrent, getConcurrency } from "../../utils/concurrent.ts"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; -import { - BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, - BOOL_FLAG_WATERMARK, -} from "../../utils/flag-descriptions.ts"; +import { poll } from "bailian-cli-runtime"; +import { downloadFile, formatBytes } from "bailian-cli-runtime"; +import { runConcurrent, getConcurrency } from "bailian-cli-runtime"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime"; export default defineCommand({ - name: "video generate", description: "Generate a video from text or image (happyhorse-1.0-t2v / happyhorse-1.0-i2v / wan2.6-t2v)", - usage: "bl video generate --prompt [--image ] [flags]", + usageArgs: "--prompt [--image ] [flags]", options: [ { flag: "--model ", @@ -72,12 +68,12 @@ export default defineCommand({ type: "number", }, ], - examples: [ - 'bl video generate --prompt "A person reading a book, static shot"', - 'bl video generate --prompt "Ocean waves at sunset." --download sunset.mp4', - 'bl video generate --image https://example.com/cat.png --prompt "Make the cat in the scene move"', - 'bl video generate --prompt "Mountain landscape" --resolution 720P --duration 5', - 'bl video generate --prompt "A cat playing with a ball" --watermark false', + exampleArgs: [ + '--prompt "A person reading a book, static shot"', + '--prompt "Ocean waves at sunset." --download sunset.mp4', + '--image https://example.com/cat.png --prompt "Make the cat in the scene move"', + '--prompt "Mountain landscape" --resolution 720P --duration 5', + '--prompt "A cat playing with a ball" --watermark false', ], async run(config: Config, flags: GlobalFlags) { let prompt = flags.prompt as string | undefined; @@ -91,7 +87,7 @@ export default defineCommand({ } prompt = hint; } else { - failIfMissing("prompt", "bl video generate --prompt "); + failIfMissing("prompt", cmdUsage(config, "--prompt ")); } } diff --git a/packages/cli/src/commands/video/ref.ts b/packages/commands/src/commands/video/ref.ts similarity index 87% rename from packages/cli/src/commands/video/ref.ts rename to packages/commands/src/commands/video/ref.ts index 616691c..1f2cd34 100644 --- a/packages/cli/src/commands/video/ref.ts +++ b/packages/commands/src/commands/video/ref.ts @@ -18,20 +18,16 @@ import { resolveBooleanFlag, resolveWatermark, } from "bailian-cli-core"; -import { poll } from "../../utils/polling.ts"; -import { downloadFile, formatBytes } from "../../utils/download.ts"; -import { promptText, failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; -import { - BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, - BOOL_FLAG_WATERMARK, -} from "../../utils/flag-descriptions.ts"; +import { poll } from "bailian-cli-runtime"; +import { downloadFile, formatBytes } from "bailian-cli-runtime"; +import { promptText, failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; +import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime"; export default defineCommand({ - name: "video ref", description: "Reference-to-video generation (happyhorse-1.0-r2v / wan2.6-r2v): multi-subject, multi-shot with voice", - usage: "bl video ref --prompt --image ... [--ref-video ...] [flags]", + usageArgs: "--prompt --image ... [--ref-video ...] [flags]", options: [ { flag: "--model ", description: "Model ID (default: happyhorse-1.0-r2v)" }, { @@ -87,12 +83,12 @@ export default defineCommand({ type: "number", }, ], - examples: [ - 'bl video ref --prompt "Image1 running on the grass" --image person.jpg', - 'bl video ref --prompt "Video 1 plays guitar, Image 1 walks over" --ref-video scene.mp4 --image person.jpg', - 'bl video ref --prompt "Image 1 speaks" --image person.jpg --image-voice voice.mp3 --resolution 1080P', - 'bl video ref --prompt "Image 1 and Image 2 have a conversation" --image a.jpg --image b.jpg --image-voice va.mp3 --image-voice vb.mp3', - 'bl video ref --prompt "Image 1 drinks water" --image person.jpg --watermark false', + exampleArgs: [ + '--prompt "Image1 running on the grass" --image person.jpg', + '--prompt "Video 1 plays guitar, Image 1 walks over" --ref-video scene.mp4 --image person.jpg', + '--prompt "Image 1 speaks" --image person.jpg --image-voice voice.mp3 --resolution 1080P', + '--prompt "Image 1 and Image 2 have a conversation" --image a.jpg --image b.jpg --image-voice va.mp3 --image-voice vb.mp3', + '--prompt "Image 1 drinks water" --image person.jpg --watermark false', ], async run(config: Config, flags: GlobalFlags) { // --- Validate prompt --- @@ -108,7 +104,7 @@ export default defineCommand({ } prompt = hint; } else { - failIfMissing("prompt", "bl video ref --prompt --image "); + failIfMissing("prompt", cmdUsage(config, "--prompt --image ")); } } @@ -119,7 +115,7 @@ export default defineCommand({ throw new BailianError( "At least one --image or --ref-video is required.", ExitCode.USAGE, - 'bl video ref --prompt "description" --image person.jpg', + cmdUsage(config, '--prompt "description" --image person.jpg'), ); } diff --git a/packages/cli/src/commands/video/task-get.ts b/packages/commands/src/commands/video/task-get.ts similarity index 72% rename from packages/cli/src/commands/video/task-get.ts rename to packages/commands/src/commands/video/task-get.ts index 65652a4..a214889 100644 --- a/packages/cli/src/commands/video/task-get.ts +++ b/packages/commands/src/commands/video/task-get.ts @@ -7,21 +7,20 @@ import { type GlobalFlags, type DashScopeTaskResponse, } from "bailian-cli-core"; -import { failIfMissing } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { failIfMissing, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; export default defineCommand({ - name: "video task get", description: "Query async task status", - usage: "bl video task get --task-id ", + usageArgs: "--task-id ", options: [{ flag: "--task-id ", description: "Async task ID" }], - examples: [ - "bl video task get --task-id 3b256896-3e70-xxxx-xxxx-xxxxxxxxxxxx", - "bl video task get --task-id 3b256896-3e70-xxxx --output json", + exampleArgs: [ + "--task-id 3b256896-3e70-xxxx-xxxx-xxxxxxxxxxxx", + "--task-id 3b256896-3e70-xxxx --output json", ], async run(config: Config, flags: GlobalFlags) { const taskId = flags.taskId as string | undefined; - if (!taskId) failIfMissing("task-id", "bl video task get --task-id "); + if (!taskId) failIfMissing("task-id", cmdUsage(config, "--task-id ")); const format = detectOutputFormat(config.output); diff --git a/packages/cli/src/commands/vision/describe.ts b/packages/commands/src/commands/vision/describe.ts similarity index 88% rename from packages/cli/src/commands/vision/describe.ts rename to packages/commands/src/commands/vision/describe.ts index b0ba310..abe04f2 100644 --- a/packages/cli/src/commands/vision/describe.ts +++ b/packages/commands/src/commands/vision/describe.ts @@ -15,8 +15,8 @@ import { ExitCode, isLocalFile, } from "bailian-cli-core"; -import { promptText } from "../../output/prompt.ts"; -import { emitResult, emitBare } from "../../output/output.ts"; +import { promptText, cmdUsage } from "bailian-cli-runtime"; +import { emitResult, emitBare } from "bailian-cli-runtime"; import { readFileSync, existsSync } from "fs"; import { extname } from "path"; @@ -57,9 +57,8 @@ async function toImageUrl(image: string): Promise { } export default defineCommand({ - name: "vision describe", description: "Describe an image or video using Qwen-VL", - usage: "bl vision describe --image [--video ] [--prompt ]", + usageArgs: "--image [--video ] [--prompt ]", options: [ { flag: "--image ", description: "Local image path or URL" }, { @@ -70,12 +69,12 @@ export default defineCommand({ { flag: "--prompt ", description: "Question about the content (default: auto-detected)" }, { flag: "--model ", description: "Vision model (default: qwen3-vl-plus)" }, ], - examples: [ - "bl vision describe --image photo.jpg", - 'bl vision describe --image https://example.com/photo.jpg --prompt "What breed is this dog?"', - 'bl vision describe --video https://example.com/video.mp4 --prompt "Summarize the video content"', - "bl vision describe --video ./local-video.mp4", - 'bl vision describe --image photo.png --prompt "Extract the text" --model qwen-vl-plus', + exampleArgs: [ + "--image photo.jpg", + '--image https://example.com/photo.jpg --prompt "What breed is this dog?"', + '--video https://example.com/video.mp4 --prompt "Summarize the video content"', + "--video ./local-video.mp4", + '--image photo.png --prompt "Extract the text" --model qwen-vl-plus', ], async run(config: Config, flags: GlobalFlags) { let image = (flags.image ?? (flags._positional as string[] | undefined)?.[0]) as @@ -113,7 +112,7 @@ export default defineCommand({ throw new BailianError( "Missing required argument --image or --video.", ExitCode.USAGE, - "bl vision describe --image \nbl vision describe --video ", + `${cmdUsage(config, "--image ")}\n${cmdUsage(config, "--video ")}`, ); } } diff --git a/packages/cli/src/commands/workspace/list.ts b/packages/commands/src/commands/workspace/list.ts similarity index 93% rename from packages/cli/src/commands/workspace/list.ts rename to packages/commands/src/commands/workspace/list.ts index 8df978d..eff573e 100644 --- a/packages/cli/src/commands/workspace/list.ts +++ b/packages/commands/src/commands/workspace/list.ts @@ -6,8 +6,8 @@ import { type Config, type GlobalFlags, } from "bailian-cli-core"; -import { emitResult } from "../../output/output.ts"; -import { displayWidth, padEnd } from "../../output/cjk-width.ts"; +import { emitResult } from "bailian-cli-runtime"; +import { displayWidth, padEnd } from "bailian-cli-runtime"; const LIST_WORKSPACES_API = "zeldaEasy.bailian-dash-workspace.space.listWorkspaces"; @@ -76,10 +76,9 @@ function printTable(workspaces: WorkspaceInfo[], noColor: boolean): void { } export default defineCommand({ - name: "workspace list", description: "List all workspaces", skipDefaultApiKeySetup: true, - usage: "bl workspace list [flags]", + usageArgs: "[flags]", options: [ { flag: "--list ", @@ -96,7 +95,7 @@ export default defineCommand({ type: "number", }, ], - examples: ["bl workspace list", "bl workspace list --list 5", "bl workspace list --output json"], + exampleArgs: ["", "--list 5", "--output json"], async run(config: Config, flags: GlobalFlags) { const limit = Number(flags.list) || 0; const format = detectOutputFormat(config.output); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts new file mode 100644 index 0000000..050e3ee --- /dev/null +++ b/packages/commands/src/index.ts @@ -0,0 +1,52 @@ +// Command library for bailian-cli products. Exposes individual command +// implementations only — no path presets or capability groups. Each product +// entrypoint (bl / rag / …) builds its own `{ "": command }` map, so the +// command paths a product exposes are a product decision, not baked in here. + +export { default as authLogin } from "./commands/auth/login.ts"; +export { default as authStatus } from "./commands/auth/status.ts"; +export { default as authLogout } from "./commands/auth/logout.ts"; +export { default as textChat } from "./commands/text/chat.ts"; +export { default as textOmni } from "./commands/omni/chat.ts"; +export { default as imageGenerate } from "./commands/image/generate.ts"; +export { default as imageEdit } from "./commands/image/edit.ts"; +export { default as videoGenerate } from "./commands/video/generate.ts"; +export { default as videoEdit } from "./commands/video/edit.ts"; +export { default as videoRef } from "./commands/video/ref.ts"; +export { default as videoTaskGet } from "./commands/video/task-get.ts"; +export { default as videoDownload } from "./commands/video/download.ts"; +export { default as visionDescribe } from "./commands/vision/describe.ts"; +export { default as configShow } from "./commands/config/show.ts"; +export { default as configSet } from "./commands/config/set.ts"; +export { default as update } from "./commands/update.ts"; +export { default as appCall } from "./commands/app/call.ts"; +export { default as appList } from "./commands/app/list.ts"; +export { default as memoryAdd } from "./commands/memory/add.ts"; +export { default as memorySearch } from "./commands/memory/search.ts"; +export { default as memoryList } from "./commands/memory/list.ts"; +export { default as memoryUpdate } from "./commands/memory/update.ts"; +export { default as memoryDelete } from "./commands/memory/delete.ts"; +export { default as memoryProfileCreate } from "./commands/memory/profile-create.ts"; +export { default as memoryProfileGet } from "./commands/memory/profile-get.ts"; +export { default as knowledgeRetrieve } from "./commands/knowledge/retrieve.ts"; +export { default as knowledgeSearch } from "./commands/knowledge/search.ts"; +export { default as knowledgeChat } from "./commands/knowledge/chat.ts"; +export { default as mcpCall } from "./commands/mcp/call.ts"; +export { default as mcpList } from "./commands/mcp/list.ts"; +export { default as mcpTools } from "./commands/mcp/tools.ts"; +export { default as searchWeb } from "./commands/search/web.ts"; +export { default as speechSynthesize } from "./commands/speech/synthesize.ts"; +export { default as speechRecognize } from "./commands/speech/recognize.ts"; +export { default as fileUpload } from "./commands/file/upload.ts"; +export { default as consoleCall } from "./commands/console/call.ts"; +export { default as usageFree } from "./commands/usage/free.ts"; +export { default as usageFreetier } from "./commands/usage/freetier.ts"; +export { default as usageStats } from "./commands/usage/stats.ts"; +export { default as pipelineRun } from "./commands/pipeline/run.ts"; +export { default as pipelineValidate } from "./commands/pipeline/validate.ts"; +export { default as advisorRecommend } from "./commands/advisor/recommend.ts"; +export { default as workspaceList } from "./commands/workspace/list.ts"; +export { default as quotaList } from "./commands/quota/list.ts"; +export { default as quotaRequest } from "./commands/quota/request.ts"; +export { default as quotaHistory } from "./commands/quota/history.ts"; +export { default as quotaCheck } from "./commands/quota/check.ts"; diff --git a/packages/commands/tsconfig.json b/packages/commands/tsconfig.json new file mode 100644 index 0000000..5910788 --- /dev/null +++ b/packages/commands/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "nodenext", + "moduleResolution": "nodenext", + "customConditions": ["@bailian-cli/source"], + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/packages/commands/vite.config.ts b/packages/commands/vite.config.ts new file mode 100644 index 0000000..1c26ed4 --- /dev/null +++ b/packages/commands/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + minify: true, + dts: { + tsgo: true, + }, + exports: { + devExports: "@bailian-cli/source", + }, + }, + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + }, + fmt: {}, +}); diff --git a/packages/core/package.json b/packages/core/package.json index 3b64c1c..ab48262 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,11 +19,18 @@ "type": "module", "types": "./dist/index.d.mts", "exports": { - ".": "./dist/index.mjs", + ".": { + "@bailian-cli/source": "./src/index.ts", + "default": "./dist/index.mjs" + }, "./package.json": "./package.json" }, "publishConfig": { "access": "public", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, "registry": "https://registry.npmjs.org/" }, "scripts": { diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index 7cb4ab2..f020b2e 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -79,6 +79,18 @@ export function knowledgeRetrieveEndpoint(baseUrl: string): string { return `${baseUrl}/api/v1/indices/rag/index/retrieve`; } +// ---- Knowledge Search (新版 RAG 检索, workspace-based host) ---- + +export function knowledgeSearchEndpoint(workspaceId: string): string { + return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search`; +} + +// ---- Knowledge Chat (新版 RAG 问答, workspace-based host) ---- + +export function knowledgeChatEndpoint(workspaceId: string): string { + return `https://${workspaceId}.cn-beijing.maas.aliyuncs.com/api/v2/apps/knowledge/chat`; +} + // ---- MCP Services (Streamable HTTP) ---- export function mcpWebSearchEndpoint(baseUrl: string): string { diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 22be23e..d44942b 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -5,7 +5,9 @@ export { chatEndpoint, imageEndpoint, imageSyncEndpoint, + knowledgeChatEndpoint, knowledgeRetrieveEndpoint, + knowledgeSearchEndpoint, memoryAddEndpoint, memoryListEndpoint, memoryNodeEndpoint, diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index 331da0b..b25bd1e 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -100,6 +100,10 @@ export function parseConfigFile(raw: unknown): ConfigFile { export interface Config { clientName?: string; clientVersion?: string; + /** Product binary name (e.g. "bl", "rag"), injected by createCli for command-facing output. */ + binName?: string; + /** npm package name for self-update (e.g. "bailian-cli", "bailian-cli-rag"), injected by createCli. */ + npmPackage?: string; apiKey?: string; /** `DASHSCOPE_ACCESS_TOKEN` env (explicit override). */ accessTokenEnv?: string; diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index 87f0782..d7a570c 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -417,6 +417,91 @@ export interface DashScopeKnowledgeRetrieveResponse { }; } +// ---- Knowledge Search (新版 RAG 检索 API, agent_id-based) ---- + +export interface KnowledgeSearchRequest { + query: string; + agent_id: string; + images?: string[]; + query_history?: Array<{ role: "user" | "assistant"; content: string }>; +} + +export interface KnowledgeSearchResponse { + code: string; + status_code: number; + request_id: string; + data: { + total: number; + cost_time: number; + nodes: Array<{ + score: number; + text: string; + metadata: { + content?: string; + title?: string; + doc_id?: string; + doc_name?: string; + doc_url?: string; + pipeline_id?: string; + workspace_id?: string; + page_number?: number; + image_url?: string; + _knowledge_type?: string; + _citation_index?: number; + _score?: number; + }; + }>; + }; +} + +// ---- Knowledge Chat (新版 RAG 问答 SSE API, agent_id-based) ---- + +export type KnowledgeChatContentPart = + | { type: "text"; text: string } + | { type: "image_url"; image_url: { url: string } }; + +export interface KnowledgeChatMessage { + role: "user" | "assistant"; + content: string | KnowledgeChatContentPart[]; +} + +export interface KnowledgeChatRequest { + input: { + messages: KnowledgeChatMessage[]; + }; + parameters: { + agent_options: { + agent_id: string; + user?: { + user_id?: string; + workspace_id?: string; + }; + }; + }; + stream: boolean; +} + +export interface KnowledgeChatStreamChunk { + output: { + choices: Array<{ + message: { + role: string; + content: string; + tool_calls?: unknown[]; + extra?: { + group?: string; + step_change?: string; + step?: string; + }; + }; + finish_reason: string; + }>; + }; + code: string; + message: string; + request_id: string; +} + // ---- Speech Synthesis / TTS (DashScope) ---- export interface DashScopeTTSRequest { diff --git a/packages/core/src/types/command.ts b/packages/core/src/types/command.ts index 343d58c..f2043da 100644 --- a/packages/core/src/types/command.ts +++ b/packages/core/src/types/command.ts @@ -9,22 +9,33 @@ export interface OptionDef { } export interface Command { - name: string; description: string; - usage?: string; + /** + * Argument portion of the usage line, WITHOUT the ` ` prefix + * (e.g. "--index-id --query [flags]"). The runtime prepends the + * product binary name and the command's actual path when rendering help, so + * the same command renders correctly under any product (bl / rag / …). + */ + usageArgs?: string; options?: OptionDef[]; - examples?: string[]; + /** + * Example argument strings, each WITHOUT the ` ` prefix + * (e.g. '--index-id idx_xxx --query "..."'). The runtime prepends + * ` ` per product when rendering help. + */ + exampleArgs?: string[]; skipDefaultApiKeySetup?: boolean; notes?: string[]; execute: (config: Config, flags: GlobalFlags) => Promise; } export interface CommandSpec { - name: string; description: string; - usage?: string; + /** See {@link Command.usageArgs} — argument portion only, no ` ` prefix. */ + usageArgs?: string; options?: OptionDef[]; - examples?: string[]; + /** See {@link Command.exampleArgs} — argument strings only, no ` ` prefix. */ + exampleArgs?: string[]; skipDefaultApiKeySetup?: boolean; notes?: string[]; run: (config: Config, flags: GlobalFlags) => Promise; @@ -32,11 +43,10 @@ export interface CommandSpec { export function defineCommand(spec: CommandSpec): Command { return { - name: spec.name, description: spec.description, - usage: spec.usage, + usageArgs: spec.usageArgs, options: spec.options, - examples: spec.examples, + exampleArgs: spec.exampleArgs, skipDefaultApiKeySetup: spec.skipDefaultApiKeySetup, notes: spec.notes, execute: (config, flags) => spec.run(config, flags), diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index fa1e40e..fd01b48 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -23,8 +23,14 @@ export type { DashScopeVideoEditRequest, DashScopeVideoRefRequest, DashScopeVideoRequest, + KnowledgeChatContentPart, + KnowledgeChatMessage, + KnowledgeChatRequest, + KnowledgeChatStreamChunk, KnowledgeRetrieveRequest, KnowledgeRetrieveResponse, + KnowledgeSearchRequest, + KnowledgeSearchResponse, MemoryAddRequest, MemoryAddResponse, MemoryMessage, diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 56baedc..25b9f30 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,6 +1,5 @@ export { generateFilename } from "./filename.ts"; export { resolveOutputDir } from "./output-dir.ts"; -export { generateToolSchema } from "./schema.ts"; export { maskToken } from "./token.ts"; export { isInteractive } from "./env.ts"; export { isCI } from "./env.ts"; diff --git a/packages/core/src/utils/schema.ts b/packages/core/src/utils/schema.ts deleted file mode 100644 index a368fc5..0000000 --- a/packages/core/src/utils/schema.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { Command } from "../types/command.ts"; - -/** - * Parse a CLI flag string (e.g. "--prompt ", "--stream") into - * a parameter name and inferred type. - */ -function parseFlag(flag: string): { - name: string; - kebabName: string; - inferredType: string; - isArray: boolean; -} { - // e.g. "--prompt " -> "prompt" - const match = flag.match(/^--([a-zA-Z0-9-]+)/); - const kebabName = match ? match[1]! : ""; - // camelCase to match internal API conventions - const name = kebabName.replace(/-([a-zA-Z0-9])/g, (_, c: string) => c.toUpperCase()); - - let inferredType = "string"; - let isArray = false; - - if (!flag.includes("<") && !flag.includes("[")) { - // No parameter value — typically a boolean flag like --stream - inferredType = "boolean"; - } else if ( - flag.includes("") || - flag.includes("") || - flag.includes("") || - flag.includes("") - ) { - inferredType = "number"; - } - - if (flag.toLowerCase().includes("repeatable")) { - isArray = true; - } - - return { name, kebabName, inferredType, isArray }; -} - -export function generateToolSchema(cmd: Command): Record { - const toolName = `bailian_${cmd.name.replace(/ /g, "_")}`; - - const schema: Record = { - name: toolName, - description: cmd.description, - input_schema: { - type: "object", - properties: {} as Record, - required: [] as string[], - }, - }; - - if (cmd.options) { - for (const opt of cmd.options) { - const { name, inferredType, isArray } = parseFlag(opt.flag); - if (!name) continue; - - // Explicit type from OptionDef takes precedence; fall back to inference - const explicitType = opt.type; - const effectiveType = isArray ? "array" : (explicitType ?? inferredType); - - const propSchema: Record = { description: opt.description }; - - if (effectiveType === "array") { - propSchema.type = "array"; - propSchema.items = { type: "string" }; - } else { - propSchema.type = effectiveType; - } - - const inputSchema = schema.input_schema as Record; - (inputSchema.properties as Record)[name] = propSchema; - - if (opt.required) { - (inputSchema.required as string[]).push(name); - } - } - } - - return schema; -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index ff4adab..5910788 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -5,6 +5,7 @@ "moduleDetection": "force", "module": "nodenext", "moduleResolution": "nodenext", + "customConditions": ["@bailian-cli/source"], "resolveJsonModule": true, "types": ["node"], "strict": true, diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index bffbbc1..1c26ed4 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -2,10 +2,13 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ pack: { + minify: true, dts: { tsgo: true, }, - exports: true, + exports: { + devExports: "@bailian-cli/source", + }, }, lint: { options: { diff --git a/packages/kscli/.gitignore b/packages/kscli/.gitignore new file mode 100644 index 0000000..7535211 --- /dev/null +++ b/packages/kscli/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/packages/kscli/LICENSE b/packages/kscli/LICENSE new file mode 100644 index 0000000..9eb125c --- /dev/null +++ b/packages/kscli/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Aliyun Model Studio (DashScope) AI Platform + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/kscli/README.md b/packages/kscli/README.md new file mode 100644 index 0000000..bf44712 --- /dev/null +++ b/packages/kscli/README.md @@ -0,0 +1,100 @@ +
+ +# Knowledge Studio CLI + +**Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.** + +[![npm version](https://img.shields.io/npm/v/knowledge-studio-cli?color=0969da&label=npm)](https://www.npmjs.com/package/knowledge-studio-cli) +[![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen)](https://nodejs.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](https://www.typescriptlang.org) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE) + +[Knowledge Studio Console](https://rag.console.aliyun.com/) · [中文文档](README.zh.md) · [API Documentation](https://help.aliyun.com/zh/model-studio/) + +
+ +## What is this? + +`kscli` is a standalone CLI for **knowledge-base retrieval** on Aliyun Model Studio (DashScope), purpose-built for RAG (Retrieval-Augmented Generation) workflows. + +## Installation + +```bash +npm install -g knowledge-studio-cli +``` + +> Requires Node.js >= 22.12. + +## Quick Start + +```bash +# Search a knowledge base +kscli search \ + --query "What is Model Studio?" \ + --agent-id \ + --workspace-id + +# Chat with a knowledge base +kscli chat \ + --message "What is RAG?" \ + --agent-id \ + --workspace-id +``` + +## Commands + +| Command | Description | +| :------------ | :------------------------------------------------ | +| `search` | Semantic search across knowledge bases (RAG) | +| `chat` | Knowledge-base Q&A with streaming (RAG) | +| `retrieve` | Query a knowledge base (deprecated, use `search`) | +| `config show` | Display current configuration | +| `config set` | Set a configuration value | +| `update` | Self-update to the latest version | + +## Authentication + +A DashScope API Key is recommended. Get yours from the [DashScope Console](https://bailian.console.aliyun.com/?tab=app#/api-key). + +```bash +# Option 1: Environment variable +export DASHSCOPE_API_KEY=sk-xxxxx + +# Option 2: Persist to config (~/.bailian/config.json) +kscli config set --key api_key --value sk-xxxxx + +# Option 3: Per-command flag +kscli search --api-key sk-xxxxx --query "..." --agent-id --workspace-id +``` + +## Configuration + +```bash +# View current config +kscli config show + +# Set defaults +kscli config set --key base_url --value https://dashscope-us.aliyuncs.com +kscli config set --key timeout --value 600 + +# Self-update +kscli update +``` + +Config file location: `~/.bailian/config.json` + +## Links + +| Resource | URL | +| :----------------------- | :--------------------------------------------------- | +| Knowledge Studio Console | https://rag.console.aliyun.com/ | +| DashScope API Docs | https://help.aliyun.com/zh/model-studio/ | +| Get API Key | https://bailian.console.aliyun.com/?tab=app#/api-key | + +## Contributing + +Bug reports, feature requests, and PRs are welcome. See [CONTRIBUTING.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.md) for developer setup and contribution workflow. + +## License + +[Apache 2.0](LICENSE) diff --git a/packages/kscli/README.zh.md b/packages/kscli/README.zh.md new file mode 100644 index 0000000..5c1334a --- /dev/null +++ b/packages/kscli/README.zh.md @@ -0,0 +1,100 @@ +
+ +# Knowledge Studio CLI + +**阿里云 Model Studio 轻量级 RAG 命令行工具 — 专注知识库检索。** + +[![npm version](https://img.shields.io/npm/v/knowledge-studio-cli?color=0969da&label=npm)](https://www.npmjs.com/package/knowledge-studio-cli) +[![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen)](https://nodejs.org) +[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](https://www.typescriptlang.org) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE) + +[Knowledge Studio 控制台](https://rag.console.aliyun.com/) · [English](README.md) · [API 文档](https://help.aliyun.com/zh/model-studio/) + +
+ +## 这是什么? + +`kscli` 是阿里云 Model Studio (DashScope) 平台的**知识库检索**专用命令行工具,专为 RAG(检索增强生成)场景打造。 + +## 安装 + +```bash +npm install -g knowledge-studio-cli +``` + +> 需要 Node.js >= 22.12。 + +## 快速开始 + +```bash +# 检索知识库 +kscli search \ + --query "什么是 Model Studio?" \ + --agent-id \ + --workspace-id + +# 知识库问答 +kscli chat \ + --message "什么是RAG?" \ + --agent-id \ + --workspace-id +``` + +## 命令列表 + +| 命令 | 说明 | +| :------------ | :------------------------------------ | +| `search` | 知识库语义检索(RAG) | +| `chat` | 知识库问答(流式输出) | +| `retrieve` | 查询知识库(已弃用,请使用 `search`) | +| `config show` | 显示当前配置 | +| `config set` | 设置配置项 | +| `update` | 自更新到最新版本 | + +## 认证方式 + +推荐使用 DashScope API Key 进行认证。前往 [DashScope 控制台](https://bailian.console.aliyun.com/?tab=app#/api-key) 获取。 + +```bash +# 方式一:环境变量 +export DASHSCOPE_API_KEY=sk-xxxxx + +# 方式二:登录命令(持久化到 ~/.bailian/config.json) +kscli config set --key api_key --value sk-xxxxx + +# 方式三:命令行参数 +kscli search --api-key sk-xxxxx --query "..." --agent-id --workspace-id +``` + +## 配置 + +```bash +# 查看当前配置 +kscli config show + +# 设置默认值 +kscli config set --key base_url --value https://dashscope-us.aliyuncs.com +kscli config set --key timeout --value 600 + +# 自更新 +kscli update +``` + +配置文件位置:`~/.bailian/config.json` + +## 相关链接 + +| 资源 | 地址 | +| :---------------------- | :--------------------------------------------------- | +| Knowledge Studio 控制台 | https://rag.console.aliyun.com/ | +| DashScope API 文档 | https://help.aliyun.com/zh/model-studio/ | +| 获取 API Key | https://bailian.console.aliyun.com/?tab=app#/api-key | + +## 参与贡献 + +欢迎提 Issue、Feature Request 和 PR。开发环境搭建与贡献流程请见 [CONTRIBUTING.zh.md](https://github.com/modelstudioai/cli/blob/main/CONTRIBUTING.zh.md)。 + +## 许可证 + +[Apache 2.0](LICENSE) diff --git a/packages/kscli/package.json b/packages/kscli/package.json new file mode 100644 index 0000000..3cc5ce6 --- /dev/null +++ b/packages/kscli/package.json @@ -0,0 +1,70 @@ +{ + "name": "knowledge-studio-cli", + "version": "1.4.0", + "description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.", + "keywords": [ + "alibaba-cloud", + "aliyun", + "bailian", + "dashscope", + "knowledge-base", + "model-studio", + "rag", + "retrieval" + ], + "homepage": "https://rag.console.aliyun.com/", + "bugs": { + "url": "https://github.com/modelstudioai/cli/issues" + }, + "license": "Apache-2.0", + "author": "Aliyun Model Studio", + "repository": { + "type": "git", + "url": "git+https://github.com/modelstudioai/cli.git", + "directory": "packages/kscli" + }, + "bin": { + "kscli": "dist/kscli.mjs" + }, + "files": [ + "dist" + ], + "type": "module", + "exports": { + ".": "./dist/kscli.mjs", + "./package.json": "./package.json" + }, + "publishConfig": { + "exports": { + ".": "./dist/kscli.mjs", + "./package.json": "./package.json" + }, + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "build": "vp pack", + "dev": "node src/main.ts", + "test": "vp test", + "check": "vp check" + }, + "dependencies": { + "bailian-cli-commands": "workspace:*", + "bailian-cli-core": "workspace:*", + "bailian-cli-runtime": "workspace:*" + }, + "devDependencies": { + "@clack/prompts": "^0.7.0", + "@types/node": "catalog:", + "@typescript/native-preview": "7.0.0-dev.20260328.1", + "ajv": "catalog:", + "boxen": "catalog:", + "chalk": "catalog:", + "typescript": "^6.0.2", + "undici": "catalog:", + "vite-plus": "0.1.22", + "yaml": "catalog:" + }, + "engines": { + "node": ">=22.12.0" + } +} diff --git a/packages/kscli/src/main.ts b/packages/kscli/src/main.ts new file mode 100644 index 0000000..c99134b --- /dev/null +++ b/packages/kscli/src/main.ts @@ -0,0 +1,27 @@ +import { createCli } from "bailian-cli-runtime"; +import type { Command } from "bailian-cli-core"; +import { + configShow, + configSet, + update, + knowledgeRetrieve, + knowledgeSearch, + knowledgeChat, +} from "bailian-cli-commands"; +import pkg from "../package.json" with { type: "json" }; + +const commands: Record = { + "config show": configShow, + "config set": configSet, + update, + retrieve: knowledgeRetrieve, + search: knowledgeSearch, + chat: knowledgeChat, +}; + +void createCli(commands, { + binName: "kscli", + version: pkg.version, + clientName: "knowledge-studio-cli", + npmPackage: "knowledge-studio-cli", +}).run(); diff --git a/packages/kscli/tests/e2e/chat.e2e.test.ts b/packages/kscli/tests/e2e/chat.e2e.test.ts new file mode 100644 index 0000000..0286ab2 --- /dev/null +++ b/packages/kscli/tests/e2e/chat.e2e.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, test } from "vite-plus/test"; +import { isChatE2EReady, parseStdoutJson, runKscli } from "./helpers.ts"; + +// ---- Types ---- + +interface ChatJsonResult { + answer: string; + request_id: string; +} + +// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ---- + +describe.skipIf(!isChatE2EReady())("e2e: kscli chat (live)", () => { + const agentId = process.env.BAILIAN_E2E_CHAT_AGENT_ID!; + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + + test("chat (JSON mode) returns answer", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是大模型?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + expect(data.request_id).toBeTruthy(); + }); + + test("chat (text mode) returns plain text", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是RAG?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + expect(stdout.trim().length).toBeGreaterThan(0); + }); + + test("chat (stream, JSON mode) collects and returns answer", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是检索增强生成?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + expect(data.request_id).toBeTruthy(); + }); + + test("chat (stream, text mode) outputs streaming text", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "什么是向量检索?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + // Streaming text mode: output should contain some text content + expect(stdout.trim().length).toBeGreaterThan(0); + }); + + test("chat with multi-turn messages returns context-aware answer", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "chat", + "--message", + "user:什么是大模型", + "--message", + "assistant:大模型是大规模语言模型,具有强大的理解和生成能力", + "--message", + "它有哪些应用场景?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + }); + + test("chat with invalid agent_id fails gracefully", async () => { + const { stderr, exitCode } = await runKscli([ + "chat", + "--message", + "test", + "--agent-id", + "aid-invalid-not-exist", + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode).not.toBe(0); + expect(stderr).toBeTruthy(); + }); +}); diff --git a/packages/kscli/tests/e2e/global-setup.ts b/packages/kscli/tests/e2e/global-setup.ts new file mode 100644 index 0000000..fc93de0 --- /dev/null +++ b/packages/kscli/tests/e2e/global-setup.ts @@ -0,0 +1,9 @@ +import { loadRootEnv } from "./helpers.ts"; + +/** + * Vitest globalSetup: load monorepo root `.env` into `process.env` before tests run. + */ +export default function vitestGlobalSetup(): () => void { + loadRootEnv(); + return () => {}; +} diff --git a/packages/kscli/tests/e2e/helpers.ts b/packages/kscli/tests/e2e/helpers.ts new file mode 100644 index 0000000..c575061 --- /dev/null +++ b/packages/kscli/tests/e2e/helpers.ts @@ -0,0 +1,129 @@ +import { execFile } from "child_process"; +import { existsSync, mkdtempSync, readFileSync } from "fs"; +import { tmpdir } from "os"; +import { promisify } from "util"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { parseEnv } from "util"; + +const execFileAsync = promisify(execFile); + +/** `packages/kscli` 根目录(含 `src/main.ts`) */ +export const kscliPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + +const mainTs = join(kscliPackageRoot, "src", "main.ts"); + +/** Monorepo 根(含根 `package.json` 和 `.env`) */ +export function monorepoRoot(): string { + return join(kscliPackageRoot, "..", ".."); +} + +// ---- E2E gating helpers ---- + +// ---- .env loader (cached) ---- + +let _rootEnvCache: Record | null = null; + +/** 读取 monorepo 根目录 `.env` 并缓存(.env 值优先于 shell 环境变量) */ +function getRootEnv(): Record { + if (_rootEnvCache !== null) return _rootEnvCache; + const rootEnvPath = join(monorepoRoot(), ".env"); + _rootEnvCache = existsSync(rootEnvPath) ? parseEnv(readFileSync(rootEnvPath, "utf8")) : {}; + return _rootEnvCache; +} + +/** 从 .env 或 process.env 获取值(.env 优先) */ +function envVar(key: string): string | undefined { + return getRootEnv()[key] ?? process.env[key]; +} + +// ---- E2E gating helpers ---- + +/** 显式开启后才跑真实网络 E2E */ +export function isBailianE2EEnabled(): boolean { + return envVar("BAILIAN_E2E") === "1"; +} + +/** 是否有 DashScope API Key 可用 */ +export function isDashScopeE2EReady(): boolean { + if (!isBailianE2EEnabled()) return false; + return !!envVar("DASHSCOPE_API_KEY")?.trim(); +} + +/** 知识检索 E2E 就绪:E2E 开启 + API Key + search agent ID + workspace ID */ +export function isSearchE2EReady(): boolean { + if (!isDashScopeE2EReady()) return false; + return ( + !!envVar("BAILIAN_E2E_SEARCH_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim() + ); +} + +/** 知识问答 E2E 就绪:E2E 开启 + API Key + chat agent ID + workspace ID */ +export function isChatE2EReady(): boolean { + if (!isDashScopeE2EReady()) return false; + return !!envVar("BAILIAN_E2E_CHAT_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim(); +} + +// ---- CLI runner ---- + +export interface RunCliResult { + stdout: string; + stderr: string; + exitCode: number; +} + +/** + * 子进程执行 kscli(等价于 `node packages/kscli/src/main.ts ...`)。 + */ +export async function runKscli( + args: string[], + envOverrides: NodeJS.ProcessEnv = {}, +): Promise { + try { + const { stdout, stderr } = await execFileAsync("node", [mainTs, ...args], { + cwd: kscliPackageRoot, + encoding: "utf8", + maxBuffer: 32 * 1024 * 1024, + env: { + ...process.env, + // .env values override shell env vars (ensures correct API key is used) + ...getRootEnv(), + // Unique clean config dir per run — prevents stale config.json from previous tests + BAILIAN_CONFIG_DIR: mkdtempSync(join(tmpdir(), "kscli-test-")), + NODE_NO_WARNINGS: "1", + DO_NOT_TRACK: "1", + ...envOverrides, + }, + }); + return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 }; + } catch (err: unknown) { + const e = err as { + stdout?: string; + stderr?: string; + code?: number; + }; + return { + stdout: e.stdout ?? "", + stderr: e.stderr ?? "", + exitCode: typeof e.code === "number" ? e.code : 1, + }; + } +} + +export function parseStdoutJson(stdout: string): T { + const t = stdout.trim(); + return JSON.parse(t) as T; +} + +// ---- Global setup: load root .env ---- + +/** + * Vitest globalSetup:加载 monorepo 根目录 `.env` 合并到 `process.env`。 + */ +export function loadRootEnv(): void { + const rootEnv = join(monorepoRoot(), ".env"); + if (existsSync(rootEnv)) { + const parsed = parseEnv(readFileSync(rootEnv, "utf8")); + Object.assign(process.env, parsed); + } +} diff --git a/packages/kscli/tests/e2e/search.e2e.test.ts b/packages/kscli/tests/e2e/search.e2e.test.ts new file mode 100644 index 0000000..77518c6 --- /dev/null +++ b/packages/kscli/tests/e2e/search.e2e.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, test } from "vite-plus/test"; +import { isSearchE2EReady, parseStdoutJson, runKscli } from "./helpers.ts"; + +// ---- Types ---- + +interface SearchResponse { + code: string; + status_code: number; + request_id: string; + data: { + total: number; + cost_time: number; + nodes: Array<{ + score: number; + text: string; + metadata: { + content?: string; + title?: string; + doc_id?: string; + doc_name?: string; + doc_url?: string; + pipeline_id?: string; + workspace_id?: string; + page_number?: number; + image_url?: string; + _knowledge_type?: string; + _citation_index?: number; + _score?: number; + }; + }>; + }; +} + +// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ---- + +describe.skipIf(!isSearchE2EReady())("e2e: kscli search (live)", () => { + const agentId = process.env.BAILIAN_E2E_SEARCH_AGENT_ID!; + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + + test("search returns results in JSON mode", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "search", + "--query", + "什么是大模型", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(data.request_id).toBeTruthy(); + expect(data.data.total).toBeGreaterThan(0); + expect(data.data.nodes.length).toBeGreaterThan(0); + + const firstNode = data.data.nodes[0]!; + expect(typeof firstNode.score).toBe("number"); + expect(firstNode.score).toBeGreaterThanOrEqual(0); + expect(typeof firstNode.text).toBe("string"); + expect(firstNode.text.length).toBeGreaterThan(0); + }); + + test("search returns results in text mode", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "search", + "--query", + "RAG", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + // Text mode: [1] (score: 0.xxxx) followed by text content + expect(stdout).toMatch(/\[1\].*score/); + }); + + test("search with --query-history returns results", async () => { + const { stdout, stderr, exitCode } = await runKscli([ + "search", + "--query", + "它怎么工作", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--query-history", + '[{"role":"user","content":"什么是大模型"},{"role":"assistant","content":"大模型是大规模语言模型"}]', + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(data.data.nodes.length).toBeGreaterThan(0); + }); + + test("search with invalid agent_id fails gracefully", async () => { + const { stderr, exitCode } = await runKscli([ + "search", + "--query", + "test", + "--agent-id", + "aid-invalid-not-exist", + "--workspace-id", + workspaceId, + "--non-interactive", + "--output", + "json", + ]); + + expect(exitCode).not.toBe(0); + expect(stderr).toBeTruthy(); + }); +}); diff --git a/packages/kscli/tsconfig.json b/packages/kscli/tsconfig.json new file mode 100644 index 0000000..5910788 --- /dev/null +++ b/packages/kscli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "nodenext", + "moduleResolution": "nodenext", + "customConditions": ["@bailian-cli/source"], + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/packages/kscli/vite.config.ts b/packages/kscli/vite.config.ts new file mode 100644 index 0000000..3bfbf90 --- /dev/null +++ b/packages/kscli/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + test: { + globalSetup: "./tests/e2e/global-setup.ts", + testTimeout: 60_000, + hookTimeout: 60_000, + }, + pack: { + entry: { + kscli: "src/main.ts", + }, + hash: false, + minify: true, + platform: "node", + banner: "#!/usr/bin/env node\n", + outputOptions: { + codeSplitting: false, + }, + dts: { + tsgo: true, + }, + exports: true, + }, + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + }, + fmt: {}, +}); diff --git a/packages/runtime/.gitignore b/packages/runtime/.gitignore new file mode 100644 index 0000000..7535211 --- /dev/null +++ b/packages/runtime/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +*.log +.DS_Store diff --git a/packages/runtime/package.json b/packages/runtime/package.json new file mode 100644 index 0000000..246f52b --- /dev/null +++ b/packages/runtime/package.json @@ -0,0 +1,70 @@ +{ + "name": "bailian-cli-runtime", + "version": "1.4.0", + "description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.", + "homepage": "https://bailian.console.aliyun.com/cli", + "bugs": { + "url": "https://github.com/modelstudioai/cli/issues" + }, + "license": "Apache-2.0", + "author": "Aliyun Model Studio", + "repository": { + "type": "git", + "url": "git+https://github.com/modelstudioai/cli.git", + "directory": "packages/runtime" + }, + "files": [ + "dist" + ], + "type": "module", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "@bailian-cli/source": "./src/index.ts", + "default": "./dist/index.mjs" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "build": "vp pack", + "dev": "vp pack --watch", + "test": "vp test", + "check": "vp check" + }, + "dependencies": { + "bailian-cli-core": "workspace:*", + "boxen": "catalog:", + "chalk": "catalog:", + "undici": "catalog:" + }, + "devDependencies": { + "@clack/prompts": "^0.7.0", + "@types/node": "catalog:", + "@typescript/native-preview": "7.0.0-dev.20260328.1", + "ajv": "catalog:", + "typescript": "^6.0.2", + "vite-plus": "0.1.22", + "yaml": "catalog:" + }, + "engines": { + "node": ">=22.12.0" + }, + "inlinedDependencies": { + "@clack/core": "0.3.5", + "@clack/prompts": "0.7.0", + "ajv": "8.20.0", + "fast-deep-equal": "3.1.3", + "fast-uri": "3.1.2", + "json-schema-traverse": "1.0.0", + "picocolors": "1.1.1", + "sisteransi": "1.0.5" + } +} diff --git a/packages/cli/src/args.ts b/packages/runtime/src/args.ts similarity index 100% rename from packages/cli/src/args.ts rename to packages/runtime/src/args.ts diff --git a/packages/runtime/src/create-cli.ts b/packages/runtime/src/create-cli.ts new file mode 100644 index 0000000..1ca4531 --- /dev/null +++ b/packages/runtime/src/create-cli.ts @@ -0,0 +1,173 @@ +import { scanCommandPath, parseFlags } from "./args.ts"; +import { CommandRegistry } from "./registry.ts"; +import type { Command } from "bailian-cli-core"; +import { + GLOBAL_OPTIONS, + loadConfig, + resolveCredential, + trackCommandExecution, + flushTelemetry, +} from "bailian-cli-core"; +import { ensureApiKey } from "./utils/ensure-key.ts"; +import { setupProxyFromEnv } from "./proxy.ts"; +import { handleError } from "./error-handler.ts"; +import { checkForUpdate, getPendingUpdateNotification } from "./utils/update-checker.ts"; +import { maybeShowStatusBar } from "./output/status-bar.ts"; +import { printWelcomeBanner, printQuickStart } from "./output/banner.ts"; +import { registerCommandHelpPrinter, setExecutingCommandPath } from "./utils/command-help.ts"; + +/** Per-product identity injected by each CLI entrypoint (bl / rag / …). */ +export interface CliOptions { + /** Binary name shown in help/usage/version output (e.g. "bl", "rag"). */ + binName: string; + /** Product version for `--version` output, telemetry and update checks. */ + version: string; + /** Telemetry client name (e.g. "bailian-cli", "rag-cli"). Defaults to `binName`. */ + clientName?: string; + /** npm package name for self-update (e.g. "bailian-cli", "bailian-cli-rag"). */ + npmPackage: string; +} + +export interface Cli { + run(argv?: string[]): Promise; +} + +/** + * Build a CLI from an injected command set. The runtime is agnostic to *which* + * commands exist — each product (bailian-cli, rag-cli, …) passes its own map and + * identity. No module-level singleton: the registry is scoped to this instance. + */ +export function createCli(commands: Record, opts: CliOptions): Cli { + const registry = new CommandRegistry(commands, opts.binName); + const clientName = opts.clientName ?? opts.binName; + const npmPackage = opts.npmPackage; + const version = opts.version; + + // 必须在任何 fetch 发起前安装(含 update-checker / telemetry) + try { + setupProxyFromEnv(); + } catch (err) { + handleError(err, opts.binName); + } + + registerCommandHelpPrinter((commandPath, out) => { + registry.printHelp(commandPath, out); + }); + + // 优雅处理 Ctrl+C + // 退出前尝试 best-effort 刷出埋点,让去抖队列中 / 在途的 fetch 请求有机会 + // 落网络;flush 与较短超时 race,保证 SIGINT 仍然响应及时。 + process.on("SIGINT", () => { + process.stderr.write("\nInterrupted. Exiting.\n"); + void flushTelemetry(500).finally(() => process.exit(130)); + }); + + // 优雅处理 stdout EPIPE(例如管道到提前退出的 `mpv`) + process.stdout.on("error", (e: NodeJS.ErrnoException) => { + if (e.code === "EPIPE") process.exit(0); + else throw e; + }); + + async function main(): Promise { + let argv = process.argv.slice(2); + if (argv[0] === "--") argv = argv.slice(1); + + if (argv.includes("--version") || argv.includes("-v")) { + process.stdout.write(`${opts.binName} ${version}\n`); + process.exit(0); + } + + const commandPath = scanCommandPath(argv, GLOBAL_OPTIONS); + + if (argv.includes("--help") || argv.includes("-h")) { + registry.printHelp(commandPath, process.stderr); + process.exit(0); + } + + // 未传任何命令:展示帮助信息与登录引导 + if (commandPath.length === 0) { + registry.printHelp([], process.stderr); + + const flags = parseFlags(argv, GLOBAL_OPTIONS); + const config = loadConfig(flags); + config.clientName = clientName; + config.clientVersion = version; + config.binName = opts.binName; + config.npmPackage = npmPackage; + + const hasKey = !!( + config.apiKey || + config.fileApiKey || + config.fileAccessToken || + config.accessTokenEnv + ); + if (hasKey) printQuickStart(); + else printWelcomeBanner(opts.binName); + process.exit(0); + } + + // 组路径(例如 `bl speech` 未接子命令):展示帮助后干净退出 + if (registry.isGroupPath(commandPath)) { + registry.printHelp(commandPath, process.stderr); + process.exit(0); + } + + const { command, extra } = registry.resolve(commandPath); + const flags = parseFlags(argv, [...GLOBAL_OPTIONS, ...(command.options ?? [])]); + + if (extra.length > 0) (flags as Record)._positional = extra; + + const config = loadConfig(flags); + config.clientName = clientName; + config.clientVersion = version; + config.binName = opts.binName; + config.npmPackage = npmPackage; + + // 默认执行 ensureApiKey;自行处理鉴权或仅需 Console/AK-SK 等的命令在 defineCommand 上设 skipDefaultApiKeySetup + if (!command.skipDefaultApiKeySetup) { + await ensureApiKey(config); + try { + const credential = await resolveCredential(config); + maybeShowStatusBar(config, credential.token, credential); + } catch { + /* 没有凭证,不展示状态栏 */ + } + } + + const updateCheckPromise = checkForUpdate(version, npmPackage).catch(() => {}); + + setExecutingCommandPath(commandPath); + + await trackCommandExecution(config, commandPath, flags, () => command.execute(config, flags)); + + await updateCheckPromise; + const isUpdateCommand = commandPath.length === 1 && commandPath[0] === "update"; + const newVersion = getPendingUpdateNotification(); + if (newVersion && !config.quiet && !isUpdateCommand) { + const isTTY = process.stderr.isTTY; + const yellow = isTTY ? "\x1b[33m" : ""; + const cyan = isTTY ? "\x1b[36m" : ""; + const reset = isTTY ? "\x1b[0m" : ""; + process.stderr.write(`\n ${yellow}Update available: ${version} → ${newVersion}${reset}\n`); + process.stderr.write(` Run ${cyan}${opts.binName} update${reset} to upgrade\n\n`); + } + + // 进程退出前尽力等待在途的埋点完成。 + // 使用较短超时兜底,避免慢网拖慢用户感知。 + await flushTelemetry(1000); + } + + return { + run() { + return main().catch((err) => { + // 在 handleError() 调用 process.exit() 之前刷出在途埋点。 + // 命令抛出的错误已被 trackCommandExecution 的 finally 块记录, + // 但底层 tracker 有 ~500ms 的发送去抖。不主动 flush 的话, + // 错误事件会随进程退出丢掉。 + return flushTelemetry(1000).finally(() => + handleError(err, opts.binName), + ) as unknown as void; + }); + }, + }; +} diff --git a/packages/cli/src/error-handler.ts b/packages/runtime/src/error-handler.ts similarity index 85% rename from packages/cli/src/error-handler.ts rename to packages/runtime/src/error-handler.ts index 93572b7..c362372 100644 --- a/packages/cli/src/error-handler.ts +++ b/packages/runtime/src/error-handler.ts @@ -9,9 +9,13 @@ import { API_KEY_PAGE } from "./urls.ts"; const LABEL_WIDTH = 13; +/** Binary name used in error hints; set by handleError() so its helpers can read it. */ +let binName: string; + /** Short reminder; full resolution order matches `loadConfig` in bailian-cli-core. */ -const BASE_URL_HINT = - "If the DashScope host is wrong, check baseUrl (--base-url, bl config show, or DASHSCOPE_BASE_URL)."; +function baseUrlHint(): string { + return `If the DashScope host is wrong, check baseUrl (--base-url, ${binName} config show, or DASHSCOPE_BASE_URL).`; +} function pad(label: string): string { return label.padEnd(LABEL_WIDTH); @@ -35,8 +39,8 @@ function enhanceHint(err: BailianError): string | undefined { return [ err.hint, "", - "bl auth login --api-key ", - "bl auth status", + `${binName} auth login --api-key `, + `${binName} auth status`, `Get API Key: ${API_KEY_PAGE}`, ] .filter((s): s is string => s !== undefined) @@ -83,23 +87,23 @@ function pickNetworkHint(code: string | undefined): string { switch (code) { case "ENOTFOUND": case "EAI_AGAIN": - return `DNS resolution failed. Check DNS / network. ${BASE_URL_HINT}`; + return `DNS resolution failed. Check DNS / network. ${baseUrlHint()}`; case "ECONNREFUSED": return "Connection refused. Check the target host/port and proxy settings."; case "ECONNRESET": return ( "Connection reset by peer. Retry, or check proxy / firewall.\n" + - "If you are behind a VPN or corporate proxy, route bl through it:\n" + + `If you are behind a VPN or corporate proxy, route ${binName} through it:\n` + "export HTTPS_PROXY=http://127.0.0.1:" ); case "ETIMEDOUT": - return `Connection timed out. Check your network. ${BASE_URL_HINT}`; + return `Connection timed out. Check your network. ${baseUrlHint()}`; case "CERT_HAS_EXPIRED": case "UNABLE_TO_VERIFY_LEAF_SIGNATURE": case "DEPTH_ZERO_SELF_SIGNED_CERT": return "TLS certificate error. Check system clock and CA bundle."; default: - return `Check network and proxy (HTTP_PROXY / HTTPS_PROXY). ${BASE_URL_HINT}`; + return `Check network and proxy (HTTP_PROXY / HTTPS_PROXY). ${baseUrlHint()}`; } } @@ -151,7 +155,9 @@ function writeBailianErrorText(err: BailianError): void { process.stderr.write(`${pad("Exit code:")}${err.exitCode}\n`); } -export function handleError(err: unknown): never { +export function handleError(err: unknown, cliName: string): never { + binName = cliName; + if (err instanceof BailianError) { const format = detectErrorOutputFormat(); @@ -173,20 +179,20 @@ export function handleError(err: unknown): never { "Request timed out.", ExitCode.TIMEOUT, "Try increasing --timeout (e.g. --timeout 60).\n" + - `${BASE_URL_HINT}\n` + - "Run: bl auth status — to check credentials.", + `${baseUrlHint()}\n` + + `Run: ${binName} auth status — to check credentials.`, { cause: err }, ); - return handleError(timeout); + return handleError(timeout, binName); } if (err instanceof TypeError && err.message === "fetch failed") { - return handleError(fromFetchFailed(err)); + return handleError(fromFetchFailed(err), binName); } const ecode = (err as NodeJS.ErrnoException).code; if (typeof ecode === "string" && ecode.startsWith("E")) { - return handleError(fromFsError(err as NodeJS.ErrnoException)); + return handleError(fromFsError(err as NodeJS.ErrnoException), binName); } process.stderr.write(`\n${pad("Error:")}${err.message}\n`); diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts new file mode 100644 index 0000000..790df67 --- /dev/null +++ b/packages/runtime/src/index.ts @@ -0,0 +1,67 @@ +// Runtime framework for bailian-cli products. Agnostic to which commands exist: +// each entrypoint (bl / rag / …) injects its own command set via createCli(). + +// Entrypoint factory + identity +export { createCli } from "./create-cli.ts"; +export type { Cli, CliOptions } from "./create-cli.ts"; + +// Command routing +export { CommandRegistry } from "./registry.ts"; +export type { Command, OptionDef } from "./registry.ts"; + +// Arg parsing +export { scanCommandPath, parseFlags } from "./args.ts"; + +// Process-level setup / error handling +export { setupProxyFromEnv } from "./proxy.ts"; +export { handleError } from "./error-handler.ts"; +export { CLI_VERSION } from "./version.ts"; + +// Console URLs referenced by commands (e.g. auth/status, banner) +export { BAILIAN_CONSOLE_ROOT, BAILIAN_CONSOLE, API_KEY_PAGE } from "./urls.ts"; + +// Output facilities consumed by commands +export { emitResult, emitBare } from "./output/output.ts"; +export { + promptText, + promptSelect, + promptConfirm, + failIfMissing, + cmdUsage, +} from "./output/prompt.ts"; +export { createSpinner, createProgressBar } from "./output/progress.ts"; +export { printWelcomeBanner, printQuickStart } from "./output/banner.ts"; +export { maybeShowStatusBar } from "./output/status-bar.ts"; +export { displayWidth, padEnd } from "./output/cjk-width.ts"; + +// Utility facilities consumed by commands +export { poll } from "./utils/polling.ts"; +export { downloadFile, formatBytes } from "./utils/download.ts"; +export { runConcurrent, getConcurrency, downloadParallel } from "./utils/concurrent.ts"; +export { resolveImageSize } from "./utils/image-size.ts"; +export { ensureApiKey } from "./utils/ensure-key.ts"; +export { + printCurrentCommandHelp, + setExecutingCommandPath, + getExecutingCommandPath, + registerCommandHelpPrinter, +} from "./utils/command-help.ts"; +export { + checkForUpdate, + getPendingUpdateNotification, + fetchLatestVersion, + NPM_PACKAGE, + NPM_REGISTRY, +} from "./utils/update-checker.ts"; +export { + BOOL_FLAG_WATERMARK, + BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, + BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, + BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, +} from "./utils/flag-descriptions.ts"; + +// Pipeline subsystem consumed by pipeline commands +export { initPipelineSteps } from "./pipeline/init.ts"; +export { executePipeline, streamPipelineEvents } from "./pipeline/executor.ts"; +export { collectPipelineIssues, collectPipelineHints } from "./pipeline/validation.ts"; +export type { PipelineDefinition, PipelineLifecycleEvent } from "./pipeline/types.ts"; diff --git a/packages/cli/src/output/banner.ts b/packages/runtime/src/output/banner.ts similarity index 88% rename from packages/cli/src/output/banner.ts rename to packages/runtime/src/output/banner.ts index 422ec70..82d9143 100644 --- a/packages/cli/src/output/banner.ts +++ b/packages/runtime/src/output/banner.ts @@ -16,12 +16,12 @@ function colors() { }; } -export function printWelcomeBanner(): void { +export function printWelcomeBanner(cliName: string): void { const { purple, reset } = colors(); process.stderr.write(`\n Welcome to ${purple}Bailian${reset} CLI!\n\n`); process.stderr.write(" Get started in 2 steps:\n"); process.stderr.write(` 1. Get your API Key: ${API_KEY_PAGE}\n`); - process.stderr.write(" 2. Login: bl auth login --api-key \n\n"); + process.stderr.write(` 2. Login: ${cliName} auth login --api-key \n\n`); } export function printQuickStart(): void { diff --git a/packages/cli/src/output/cjk-width.ts b/packages/runtime/src/output/cjk-width.ts similarity index 100% rename from packages/cli/src/output/cjk-width.ts rename to packages/runtime/src/output/cjk-width.ts diff --git a/packages/cli/src/output/output.ts b/packages/runtime/src/output/output.ts similarity index 100% rename from packages/cli/src/output/output.ts rename to packages/runtime/src/output/output.ts diff --git a/packages/cli/src/output/progress.ts b/packages/runtime/src/output/progress.ts similarity index 100% rename from packages/cli/src/output/progress.ts rename to packages/runtime/src/output/progress.ts diff --git a/packages/cli/src/output/prompt.ts b/packages/runtime/src/output/prompt.ts similarity index 85% rename from packages/cli/src/output/prompt.ts rename to packages/runtime/src/output/prompt.ts index 909e29c..bc1d825 100644 --- a/packages/cli/src/output/prompt.ts +++ b/packages/runtime/src/output/prompt.ts @@ -10,9 +10,20 @@ * case explicitly. */ -import { BailianError, ExitCode, isInteractive } from "bailian-cli-core"; +import { BailianError, ExitCode, isInteractive, type Config } from "bailian-cli-core"; import { printCurrentCommandHelp, getExecutingCommandPath } from "../utils/command-help.ts"; +/** + * Build a command-usage string for the running command: ` `. + * Both the product binary name and the command path come from the runtime, so + * callers never hardcode "bl" or their own path — the same code renders as + * `bl knowledge retrieve …` under bl and `rag retrieve …` under rag. + */ +export function cmdUsage(config: Config, args = ""): string { + const parts = [config.binName, ...getExecutingCommandPath()].filter(Boolean); + return args ? `${parts.join(" ")} ${args}` : parts.join(" "); +} + // Dynamic import to avoid loading @clack/prompts in non-interactive envs unnecessarily // (though for CLI tools the startup cost is usually acceptable) diff --git a/packages/cli/src/output/status-bar.ts b/packages/runtime/src/output/status-bar.ts similarity index 100% rename from packages/cli/src/output/status-bar.ts rename to packages/runtime/src/output/status-bar.ts diff --git a/packages/cli/src/pipeline/bl-config.ts b/packages/runtime/src/pipeline/bl-config.ts similarity index 100% rename from packages/cli/src/pipeline/bl-config.ts rename to packages/runtime/src/pipeline/bl-config.ts diff --git a/packages/cli/src/pipeline/dispatcher.ts b/packages/runtime/src/pipeline/dispatcher.ts similarity index 100% rename from packages/cli/src/pipeline/dispatcher.ts rename to packages/runtime/src/pipeline/dispatcher.ts diff --git a/packages/cli/src/pipeline/errors.ts b/packages/runtime/src/pipeline/errors.ts similarity index 100% rename from packages/cli/src/pipeline/errors.ts rename to packages/runtime/src/pipeline/errors.ts diff --git a/packages/cli/src/pipeline/executor.ts b/packages/runtime/src/pipeline/executor.ts similarity index 100% rename from packages/cli/src/pipeline/executor.ts rename to packages/runtime/src/pipeline/executor.ts diff --git a/packages/cli/src/pipeline/expressions.ts b/packages/runtime/src/pipeline/expressions.ts similarity index 100% rename from packages/cli/src/pipeline/expressions.ts rename to packages/runtime/src/pipeline/expressions.ts diff --git a/packages/cli/src/pipeline/init.ts b/packages/runtime/src/pipeline/init.ts similarity index 100% rename from packages/cli/src/pipeline/init.ts rename to packages/runtime/src/pipeline/init.ts diff --git a/packages/cli/src/pipeline/scheduler.ts b/packages/runtime/src/pipeline/scheduler.ts similarity index 100% rename from packages/cli/src/pipeline/scheduler.ts rename to packages/runtime/src/pipeline/scheduler.ts diff --git a/packages/cli/src/pipeline/schema.ts b/packages/runtime/src/pipeline/schema.ts similarity index 100% rename from packages/cli/src/pipeline/schema.ts rename to packages/runtime/src/pipeline/schema.ts diff --git a/packages/cli/src/pipeline/steps/bl-api.ts b/packages/runtime/src/pipeline/steps/bl-api.ts similarity index 100% rename from packages/cli/src/pipeline/steps/bl-api.ts rename to packages/runtime/src/pipeline/steps/bl-api.ts diff --git a/packages/cli/src/pipeline/steps/bl-steps.ts b/packages/runtime/src/pipeline/steps/bl-steps.ts similarity index 100% rename from packages/cli/src/pipeline/steps/bl-steps.ts rename to packages/runtime/src/pipeline/steps/bl-steps.ts diff --git a/packages/cli/src/pipeline/steps/logic.ts b/packages/runtime/src/pipeline/steps/logic.ts similarity index 100% rename from packages/cli/src/pipeline/steps/logic.ts rename to packages/runtime/src/pipeline/steps/logic.ts diff --git a/packages/cli/src/pipeline/steps/script-js.ts b/packages/runtime/src/pipeline/steps/script-js.ts similarity index 100% rename from packages/cli/src/pipeline/steps/script-js.ts rename to packages/runtime/src/pipeline/steps/script-js.ts diff --git a/packages/cli/src/pipeline/types.ts b/packages/runtime/src/pipeline/types.ts similarity index 100% rename from packages/cli/src/pipeline/types.ts rename to packages/runtime/src/pipeline/types.ts diff --git a/packages/cli/src/pipeline/utils.ts b/packages/runtime/src/pipeline/utils.ts similarity index 100% rename from packages/cli/src/pipeline/utils.ts rename to packages/runtime/src/pipeline/utils.ts diff --git a/packages/cli/src/pipeline/validation.ts b/packages/runtime/src/pipeline/validation.ts similarity index 100% rename from packages/cli/src/pipeline/validation.ts rename to packages/runtime/src/pipeline/validation.ts diff --git a/packages/cli/src/proxy.ts b/packages/runtime/src/proxy.ts similarity index 100% rename from packages/cli/src/proxy.ts rename to packages/runtime/src/proxy.ts diff --git a/packages/cli/src/registry.ts b/packages/runtime/src/registry.ts similarity index 80% rename from packages/cli/src/registry.ts rename to packages/runtime/src/registry.ts index f1e7cef..ef23fe8 100644 --- a/packages/cli/src/registry.ts +++ b/packages/runtime/src/registry.ts @@ -2,7 +2,6 @@ import type { Command } from "bailian-cli-core"; import { BailianError } from "bailian-cli-core"; import { ExitCode } from "bailian-cli-core"; import { GLOBAL_OPTIONS } from "bailian-cli-core"; -import { commands } from "./commands/catalog.ts"; export type { Command, OptionDef } from "bailian-cli-core"; @@ -11,10 +10,13 @@ interface CommandNode { children: Map; } -class CommandRegistry { +export class CommandRegistry { private root: CommandNode = { children: new Map() }; + /** Binary name shown in usage/help/error strings (e.g. "bl", "rag"). */ + private readonly cliName: string; - constructor(commands: Record) { + constructor(commands: Record, cliName: string) { + this.cliName = cliName; for (const [path, cmd] of Object.entries(commands)) { this.register(path, cmd); } @@ -44,6 +46,19 @@ class CommandRegistry { return commands; } + /** First registered command path, for the "Getting Help" example (e.g. "knowledge retrieve"). */ + private helpExample(): string { + const walk = (node: CommandNode, path: string[]): string | null => { + for (const [name, child] of node.children) { + if (child.command) return [...path, name].join(" "); + const deeper = walk(child, [...path, name]); + if (deeper) return deeper; + } + return null; + }; + return walk(this.root, []) ?? " "; + } + isGroupPath(commandPath: string[]): boolean { let node = this.root; for (const part of commandPath) { @@ -87,16 +102,16 @@ class CommandRegistry { }) .join("\n"); throw new BailianError( - `Unknown command: bl ${commandPath.join(" ")}\n\nAvailable commands:\n${subcommands}`, + `Unknown command: ${this.cliName} ${commandPath.join(" ")}\n\nAvailable commands:\n${subcommands}`, ExitCode.USAGE, - `bl ${matched.join(" ")} --help`, + `${this.cliName} ${matched.join(" ")} --help`, ); } throw new BailianError( - `Unknown command: bl ${commandPath.join(" ")}`, + `Unknown command: ${this.cliName} ${commandPath.join(" ")}`, ExitCode.USAGE, - "bl --help", + `${this.cliName} --help`, ); } @@ -150,13 +165,13 @@ class CommandRegistry { } if (node.command) { - this.printCommandHelp(node.command, out); + this.printCommandHelp(node.command, commandPath, out); return; } // Group help (e.g. `bl auth --help`) const prefix = commandPath.join(" "); - out.write(`\n${this.bold("Usage:", out)} bl ${prefix} [flags]\n\n`); + out.write(`\n${this.bold("Usage:", out)} ${this.cliName} ${prefix} [flags]\n\n`); out.write(`${this.bold("Commands:", out)}\n`); this.printChildren(node, prefix, out); if (prefix === "pipeline") { @@ -180,8 +195,8 @@ ${d(' message: "Who are you?"')} ${d(' system: "You are a concise assistant."')} ${b("Try it:")} -${d(" bl pipeline validate workflow.yaml")} -${d(" bl pipeline run workflow.yaml --dry-run --output json")} +${d(` ${this.cliName} pipeline validate workflow.yaml`)} +${d(` ${this.cliName} pipeline run workflow.yaml --dry-run --output json`)} `); } @@ -215,7 +230,7 @@ ${d(" bl pipeline run workflow.yaml --dry-run --output json")} const globalFlagLines = this.buildGlobalFlagLines(a, d); out.write(` -${b("Usage:")} bl [flags] +${b("Usage:")} ${this.cliName} [flags] ${b("Commands:")} ${commandLines} @@ -225,17 +240,21 @@ ${globalFlagLines} ${b("Getting Help:")} ${d("Add --help after any command to see its full list of options, defaults,")} - ${d("and usage examples. For example:")} bl text chat --help + ${d("and usage examples. For example:")} ${this.cliName} ${this.helpExample()} --help `); } - private printCommandHelp(cmd: Command, out: NodeJS.WriteStream): void { + private printCommandHelp(cmd: Command, commandPath: string[], out: NodeJS.WriteStream): void { const b = (s: string) => this.bold(s, out); const a = (s: string) => this.accent(s, out); const d = (s: string) => this.dim(s, out); + // ` ` prefix is rendered here, not stored in the command, so the + // same command shows the right invocation under any product (bl / rag / …). + const prefix = [this.cliName, ...commandPath].join(" "); + out.write(`\n${cmd.description}\n`); - if (cmd.usage) out.write(`${b("Usage:")} ${cmd.usage}\n`); + out.write(`${b("Usage:")} ${prefix}${cmd.usageArgs ? ` ${cmd.usageArgs}` : ""}\n`); if (cmd.options && cmd.options.length > 0) { const maxLen = Math.max(...cmd.options.map((o) => o.flag.length)); out.write(`\n${b("Options:")}\n`); @@ -249,16 +268,16 @@ ${b("Getting Help:")} out.write(` ${note}\n`); } } - if (cmd.examples && cmd.examples.length > 0) { + if (cmd.exampleArgs && cmd.exampleArgs.length > 0) { out.write(`\n${b("Examples:")}\n`); - for (const ex of cmd.examples) { - out.write(` ${d(ex)}\n`); + for (const ex of cmd.exampleArgs) { + out.write(` ${d(ex ? `${prefix} ${ex}` : prefix)}\n`); } } out.write( `\n${d("Global flags (--api-key, --output, --quiet, etc.) are always available.")}\n`, ); - out.write(`${d("Run")} bl --help ${d("for the full list.")}\n`); + out.write(`${d("Run")} ${this.cliName} --help ${d("for the full list.")}\n`); } private printChildren(node: CommandNode, prefix: string, out: NodeJS.WriteStream): void { @@ -277,5 +296,3 @@ ${b("Getting Help:")} } } } - -export const registry = new CommandRegistry(commands); diff --git a/packages/cli/src/urls.ts b/packages/runtime/src/urls.ts similarity index 100% rename from packages/cli/src/urls.ts rename to packages/runtime/src/urls.ts diff --git a/packages/cli/src/utils/command-help.ts b/packages/runtime/src/utils/command-help.ts similarity index 100% rename from packages/cli/src/utils/command-help.ts rename to packages/runtime/src/utils/command-help.ts diff --git a/packages/cli/src/utils/concurrent.ts b/packages/runtime/src/utils/concurrent.ts similarity index 100% rename from packages/cli/src/utils/concurrent.ts rename to packages/runtime/src/utils/concurrent.ts diff --git a/packages/cli/src/utils/download.ts b/packages/runtime/src/utils/download.ts similarity index 100% rename from packages/cli/src/utils/download.ts rename to packages/runtime/src/utils/download.ts diff --git a/packages/cli/src/utils/ensure-key.ts b/packages/runtime/src/utils/ensure-key.ts similarity index 100% rename from packages/cli/src/utils/ensure-key.ts rename to packages/runtime/src/utils/ensure-key.ts diff --git a/packages/cli/src/utils/flag-descriptions.ts b/packages/runtime/src/utils/flag-descriptions.ts similarity index 100% rename from packages/cli/src/utils/flag-descriptions.ts rename to packages/runtime/src/utils/flag-descriptions.ts diff --git a/packages/cli/src/utils/image-size.ts b/packages/runtime/src/utils/image-size.ts similarity index 100% rename from packages/cli/src/utils/image-size.ts rename to packages/runtime/src/utils/image-size.ts diff --git a/packages/cli/src/utils/polling.ts b/packages/runtime/src/utils/polling.ts similarity index 100% rename from packages/cli/src/utils/polling.ts rename to packages/runtime/src/utils/polling.ts diff --git a/packages/cli/src/utils/update-checker.ts b/packages/runtime/src/utils/update-checker.ts similarity index 86% rename from packages/cli/src/utils/update-checker.ts rename to packages/runtime/src/utils/update-checker.ts index 0d194c4..bf3f111 100644 --- a/packages/cli/src/utils/update-checker.ts +++ b/packages/runtime/src/utils/update-checker.ts @@ -3,6 +3,7 @@ import { readFileSync, writeFileSync } from "fs"; import { getConfigDir, trackingHeaders } from "bailian-cli-core"; export const NPM_REGISTRY = "https://registry.npmjs.org"; +/** Default npm package; products override per-call via the `npmPackage` argument. */ export const NPM_PACKAGE = "bailian-cli"; const STATE_FILE = () => join(getConfigDir(), "update-state.json"); @@ -47,9 +48,10 @@ function writeState(state: UpdateState): void { export async function fetchLatestVersion( timeoutMs: number = FETCH_TIMEOUT_MS, + npmPackage: string = NPM_PACKAGE, ): Promise { try { - const encoded = NPM_PACKAGE.replace("/", "%2f"); + const encoded = npmPackage.replace("/", "%2f"); const res = await fetch(`${NPM_REGISTRY}/${encoded}/latest`, { headers: { Accept: "application/json", @@ -71,7 +73,10 @@ export function getPendingUpdateNotification(): string | null { return pendingNotification; } -export async function checkForUpdate(currentVersion: string): Promise { +export async function checkForUpdate( + currentVersion: string, + npmPackage: string = NPM_PACKAGE, +): Promise { // Skip in CI / non-TTY environments if (process.env.CI || !process.stderr.isTTY) return; @@ -86,7 +91,7 @@ export async function checkForUpdate(currentVersion: string): Promise { return; } - const latest = await fetchLatestVersion(); + const latest = await fetchLatestVersion(FETCH_TIMEOUT_MS, npmPackage); if (!latest) return; writeState({ lastChecked: now, latestVersion: latest }); diff --git a/packages/cli/src/version.ts b/packages/runtime/src/version.ts similarity index 100% rename from packages/cli/src/version.ts rename to packages/runtime/src/version.ts diff --git a/packages/runtime/tests/args.test.ts b/packages/runtime/tests/args.test.ts new file mode 100644 index 0000000..b32d208 --- /dev/null +++ b/packages/runtime/tests/args.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from "vite-plus/test"; +import { ExitCode, GLOBAL_OPTIONS } from "bailian-cli-core"; +import { parseFlags } from "../src/args.ts"; +import { BOOL_FLAG_WATERMARK } from "../src/utils/flag-descriptions.ts"; + +const IMAGE_GENERATE_OPTIONS = [ + { flag: "--prompt ", description: "Image description", required: true }, + { flag: "--model ", description: "Model ID" }, + { flag: "--watermark ", description: BOOL_FLAG_WATERMARK }, + { flag: "--no-wait", description: "Return task ID immediately without waiting" }, +]; + +test("parseFlags rejects unknown long flags", () => { + expect(() => + parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]), + ).toThrowError( + expect.objectContaining({ + name: "BailianError", + exitCode: ExitCode.USAGE, + message: expect.stringContaining('Unknown flag "--xxxx"'), + }), + ); +}); + +test("parseFlags rejects unknown flags with = syntax", () => { + expect(() => + parseFlags( + ["--prompt=cat", "--unknown-flag=yes"], + [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS], + ), + ).toThrow(/Unknown flag "--unknown-flag"/); +}); + +test("parseFlags accepts defined command and global flags", () => { + const flags = parseFlags( + ["--quiet", "--prompt", "cat", "--watermark", "false"], + [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS], + ); + expect(flags.quiet).toBe(true); + expect(flags.prompt).toBe("cat"); + expect(flags.watermark).toBe("false"); +}); + +test("parseFlags rejects value flag when next token is another flag", () => { + const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]; + for (const argv of [ + ["--watermark", "--prompt", "cat"], + ["--watermark", "-h"], + ["--prompt", "cat", "--watermark", "--model", "qwen-image-2.0"], + ]) { + expect(() => parseFlags(argv, opts)).toThrowError( + expect.objectContaining({ + name: "BailianError", + exitCode: ExitCode.USAGE, + message: expect.stringContaining("Flag --watermark requires a value"), + }), + ); + } +}); + +test("parseFlags rejects trailing value flag without value", () => { + expect(() => + parseFlags(["--prompt", "cat", "--watermark"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]), + ).toThrowError( + expect.objectContaining({ + message: expect.stringContaining("Flag --watermark requires a value"), + }), + ); +}); + +test("parseFlags allows boolean flags without values adjacent to other flags", () => { + const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]; + const flags = parseFlags( + ["--quiet", "--dry-run", "--no-wait", "--prompt", "cat", "--watermark", "false"], + opts, + ); + expect(flags.quiet).toBe(true); + expect(flags.dryRun).toBe(true); + expect(flags.noWait).toBe(true); + expect(flags.prompt).toBe("cat"); + expect(flags.watermark).toBe("false"); +}); + +test("parseFlags does not treat the next flag as a boolean flag value", () => { + const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]; + expect(() => parseFlags(["--dry-run", "--prompt"], opts)).toThrowError( + expect.objectContaining({ + message: expect.stringContaining("Flag --prompt requires a value"), + }), + ); + // --dry-run is boolean: no value check; parsing continues to --prompt. + const flags = parseFlags(["--dry-run", "--prompt", "cat"], opts); + expect(flags.dryRun).toBe(true); + expect(flags.prompt).toBe("cat"); +}); diff --git a/packages/runtime/tests/pipeline.test.ts b/packages/runtime/tests/pipeline.test.ts new file mode 100644 index 0000000..f6853c0 --- /dev/null +++ b/packages/runtime/tests/pipeline.test.ts @@ -0,0 +1,109 @@ +import { expect, test } from "vite-plus/test"; +import { createStepDispatcher } from "../src/pipeline/dispatcher.ts"; +import { executePipeline } from "../src/pipeline/executor.ts"; +import { collectPipelineIssues } from "../src/pipeline/validation.ts"; +import { getByJsonPointer } from "../src/pipeline/schema.ts"; +import { normalizeConcurrency } from "../src/pipeline/scheduler.ts"; +import { WORKFLOW_VERSION, type PipelineDefinition } from "../src/pipeline/types.ts"; + +test("cli package skeleton", () => { + expect(true).toBe(true); +}); + +test("pipeline execution can use an isolated step dispatcher", async () => { + const dispatcher = createStepDispatcher(); + dispatcher.registerStep("test/echo", (input, ctx) => ({ + data: { input, hasSignal: !!ctx.signal }, + })); + + const controller = new AbortController(); + const pipeline: PipelineDefinition = { + version: WORKFLOW_VERSION, + steps: [{ id: "echo", type: "test/echo", input: { message: "hello" } }], + }; + + const report = await executePipeline( + pipeline, + {}, + { + stepDispatcher: dispatcher, + signal: controller.signal, + }, + ); + + expect(report.status).toBe("succeeded"); + expect(report.steps[0]?.output?.data).toEqual({ + input: { message: "hello" }, + hasSignal: true, + }); +}); + +test("dry-run never executes $js expressions (preview must not run code)", async () => { + const dispatcher = createStepDispatcher(); + dispatcher.registerStep("test/echo", (input) => ({ data: input })); + const flag = "__bailian_dryrun_should_not_run__"; + delete (globalThis as Record)[flag]; + + const pipeline: PipelineDefinition = { + version: WORKFLOW_VERSION, + steps: [ + { + id: "s1", + type: "test/echo", + input: { probe: { $js: `(globalThis[${JSON.stringify(flag)}] = true), 1` } }, + }, + ], + }; + + const report = await executePipeline(pipeline, {}, { stepDispatcher: dispatcher, dryRun: true }); + expect(report.status).toBe("planned"); + expect((globalThis as Record)[flag]).toBeUndefined(); +}); + +test("script/js rejects non-literal code sourced from another step ($from)", () => { + const dispatcher = createStepDispatcher(); + dispatcher.registerStep("test/echo", (input) => ({ data: input })); + dispatcher.registerStep("script/js", () => ({ data: {} })); + + const pipeline: PipelineDefinition = { + version: WORKFLOW_VERSION, + steps: [ + { id: "gen", type: "test/echo", input: { message: "x" } }, + { + id: "run", + type: "script/js", + input: { code: { $from: "gen", path: "/data/message" } as never }, + }, + ], + }; + + const issues = collectPipelineIssues(pipeline, dispatcher); + expect(issues.some((issue) => issue.includes('literal string "code"'))).toBe(true); +}); + +test("script/js accepts a literal string code", () => { + const dispatcher = createStepDispatcher(); + dispatcher.registerStep("script/js", () => ({ data: {} })); + + const pipeline: PipelineDefinition = { + version: WORKFLOW_VERSION, + steps: [{ id: "run", type: "script/js", input: { code: "return 1" } }], + }; + + expect(collectPipelineIssues(pipeline, dispatcher)).toEqual([]); +}); + +test("getByJsonPointer refuses prototype keys and inherited properties", () => { + const obj = { a: { b: 1 } }; + expect(getByJsonPointer(obj, "/a/b")).toBe(1); + expect(getByJsonPointer(obj, "/__proto__")).toBeUndefined(); + expect(getByJsonPointer(obj, "/constructor")).toBeUndefined(); + expect(getByJsonPointer(obj, "/a/constructor/constructor")).toBeUndefined(); + expect(getByJsonPointer(obj, "/toString")).toBeUndefined(); +}); + +test("normalizeConcurrency clamps to a safe maximum", () => { + expect(normalizeConcurrency(undefined)).toBe(1); + expect(normalizeConcurrency(4)).toBe(4); + expect(normalizeConcurrency(100000)).toBe(64); +}); diff --git a/packages/runtime/tests/proxy.test.ts b/packages/runtime/tests/proxy.test.ts new file mode 100644 index 0000000..1986459 --- /dev/null +++ b/packages/runtime/tests/proxy.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "vite-plus/test"; +import { readProxyEnv } from "../src/proxy.ts"; + +test("readProxyEnv: 未设置任何代理变量时全部为 undefined", () => { + expect(readProxyEnv({})).toEqual({ + httpProxy: undefined, + httpsProxy: undefined, + noProxy: undefined, + }); +}); + +test("readProxyEnv: 空白值视为未设置", () => { + expect(readProxyEnv({ HTTPS_PROXY: "", HTTP_PROXY: " ", NO_PROXY: "" })).toEqual({ + httpProxy: undefined, + httpsProxy: undefined, + noProxy: undefined, + }); +}); + +test("readProxyEnv: 大小写变量均可识别,小写优先", () => { + expect(readProxyEnv({ HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe("http://upper:1"); + expect(readProxyEnv({ https_proxy: "http://lower:1" }).httpsProxy).toBe("http://lower:1"); + expect( + readProxyEnv({ https_proxy: "http://lower:1", HTTPS_PROXY: "http://upper:1" }).httpsProxy, + ).toBe("http://lower:1"); +}); + +test("readProxyEnv: 空字符串小写变量不屏蔽已设置的大写变量", () => { + expect(readProxyEnv({ https_proxy: "", HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe( + "http://upper:1", + ); + expect(readProxyEnv({ http_proxy: "", HTTP_PROXY: "http://upper:2" }).httpProxy).toBe( + "http://upper:2", + ); +}); + +test("readProxyEnv: NO_PROXY 独立读取", () => { + const r = readProxyEnv({ NO_PROXY: "*.aliyuncs.com" }); + expect(r.noProxy).toBe("*.aliyuncs.com"); + expect(r.httpProxy).toBeUndefined(); + expect(r.httpsProxy).toBeUndefined(); +}); diff --git a/packages/runtime/tsconfig.json b/packages/runtime/tsconfig.json new file mode 100644 index 0000000..5910788 --- /dev/null +++ b/packages/runtime/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "nodenext", + "moduleResolution": "nodenext", + "customConditions": ["@bailian-cli/source"], + "resolveJsonModule": true, + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "declaration": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + } +} diff --git a/packages/runtime/vite.config.ts b/packages/runtime/vite.config.ts new file mode 100644 index 0000000..1c26ed4 --- /dev/null +++ b/packages/runtime/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + minify: true, + dts: { + tsgo: true, + }, + exports: { + devExports: "@bailian-cli/source", + }, + }, + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + }, + fmt: {}, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55a0fcb..a316881 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,18 +42,108 @@ importers: packages/cli: dependencies: + bailian-cli-commands: + specifier: workspace:* + version: link:../commands bailian-cli-core: specifier: workspace:* version: link:../core + bailian-cli-runtime: + specifier: workspace:* + version: link:../runtime + devDependencies: + '@clack/prompts': + specifier: ^0.7.0 + version: 0.7.0 + '@types/node': + specifier: 'catalog:' + version: 24.12.2 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260328.1 + version: 7.0.0-dev.20260328.1 + ajv: + specifier: 'catalog:' + version: 8.20.0 boxen: specifier: 'catalog:' version: 8.0.1 chalk: specifier: 'catalog:' version: 5.6.2 + typescript: + specifier: ^6.0.2 + version: 6.0.3 undici: specifier: 'catalog:' version: 8.4.1 + vite-plus: + specifier: 0.1.22 + version: 0.1.22(@types/node@24.12.2)(jiti@2.6.1)(typescript@6.0.3)(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(yaml@2.8.3) + yaml: + specifier: 'catalog:' + version: 2.8.3 + + packages/commands: + dependencies: + bailian-cli-core: + specifier: workspace:* + version: link:../core + bailian-cli-runtime: + specifier: workspace:* + version: link:../runtime + boxen: + specifier: 'catalog:' + version: 8.0.1 + chalk: + specifier: 'catalog:' + version: 5.6.2 + yaml: + specifier: 'catalog:' + version: 2.8.3 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.12.2 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260328.1 + version: 7.0.0-dev.20260328.1 + typescript: + specifier: ^6.0.2 + version: 6.0.3 + vite-plus: + specifier: 0.1.22 + version: 0.1.22(@types/node@24.12.2)(jiti@2.6.1)(typescript@6.0.3)(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(yaml@2.8.3) + + packages/core: + dependencies: + yaml: + specifier: ^2.8.3 + version: 2.8.3 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.12.2 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260328.1 + version: 7.0.0-dev.20260328.1 + typescript: + specifier: ^6.0.2 + version: 6.0.3 + vite-plus: + specifier: 'catalog:' + version: 0.1.22(@types/node@24.12.2)(jiti@2.6.1)(typescript@6.0.3)(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(yaml@2.8.3) + + packages/kscli: + dependencies: + bailian-cli-commands: + specifier: workspace:* + version: link:../commands + bailian-cli-core: + specifier: workspace:* + version: link:../core + bailian-cli-runtime: + specifier: workspace:* + version: link:../runtime devDependencies: '@clack/prompts': specifier: ^0.7.0 @@ -67,34 +157,61 @@ importers: ajv: specifier: 'catalog:' version: 8.20.0 + boxen: + specifier: 'catalog:' + version: 8.0.1 + chalk: + specifier: 'catalog:' + version: 5.6.2 typescript: specifier: ^6.0.2 version: 6.0.3 - vite-plus: + undici: specifier: 'catalog:' + version: 8.4.1 + vite-plus: + specifier: 0.1.22 version: 0.1.22(@types/node@24.12.2)(jiti@2.6.1)(typescript@6.0.3)(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(yaml@2.8.3) yaml: specifier: 'catalog:' version: 2.8.3 - packages/core: + packages/runtime: dependencies: - yaml: - specifier: ^2.8.3 - version: 2.8.3 + bailian-cli-core: + specifier: workspace:* + version: link:../core + boxen: + specifier: 'catalog:' + version: 8.0.1 + chalk: + specifier: 'catalog:' + version: 5.6.2 + undici: + specifier: 'catalog:' + version: 8.4.1 devDependencies: + '@clack/prompts': + specifier: ^0.7.0 + version: 0.7.0 '@types/node': specifier: 'catalog:' version: 24.12.2 '@typescript/native-preview': specifier: 7.0.0-dev.20260328.1 version: 7.0.0-dev.20260328.1 + ajv: + specifier: 'catalog:' + version: 8.20.0 typescript: specifier: ^6.0.2 version: 6.0.3 vite-plus: - specifier: 'catalog:' + specifier: 0.1.22 version: 0.1.22(@types/node@24.12.2)(jiti@2.6.1)(typescript@6.0.3)(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(yaml@2.8.3) + yaml: + specifier: 'catalog:' + version: 2.8.3 packages: diff --git a/skills/bailian-cli/reference/advisor.md b/skills/bailian-cli/reference/advisor.md index c03387e..306957f 100644 --- a/skills/bailian-cli/reference/advisor.md +++ b/skills/bailian-cli/reference/advisor.md @@ -1,6 +1,6 @@ # `bl advisor` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) @@ -52,5 +52,5 @@ bl advisor recommend --message "Long document summarization" --dry-run ``` ```bash -bl advisor recommend # Interactive input +bl advisor recommend # Interactive input ``` diff --git a/skills/bailian-cli/reference/app.md b/skills/bailian-cli/reference/app.md index 7076857..47ef1af 100644 --- a/skills/bailian-cli/reference/app.md +++ b/skills/bailian-cli/reference/app.md @@ -1,6 +1,6 @@ # `bl app` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/auth.md b/skills/bailian-cli/reference/auth.md index eaa1006..e4c9957 100644 --- a/skills/bailian-cli/reference/auth.md +++ b/skills/bailian-cli/reference/auth.md @@ -1,6 +1,6 @@ # `bl auth` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) @@ -21,7 +21,7 @@ Index: [index.md](index.md) | --------------- | ---------------------------------------------------------------------------- | | **Name** | `auth login` | | **Description** | Authenticate with API key or console browser login (credentials can coexist) | -| **Usage** | `bl auth login --api-key \| bl auth login --console` | +| **Usage** | `bl auth login --api-key \| --console` | #### Options diff --git a/skills/bailian-cli/reference/config.md b/skills/bailian-cli/reference/config.md index 0b7da11..ae92f90 100644 --- a/skills/bailian-cli/reference/config.md +++ b/skills/bailian-cli/reference/config.md @@ -1,44 +1,19 @@ # `bl config` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.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 config export-schema` | Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas | -| `bl config set` | Set a config value | -| `bl config show` | Display current configuration | +| Command | Description | +| ---------------- | ----------------------------- | +| `bl config set` | Set a config value | +| `bl config show` | Display current configuration | ## Command details -### `bl config export-schema` - -| Field | Value | -| --------------- | ----------------------------------------------------------------------------------- | -| **Name** | `config export-schema` | -| **Description** | Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas | -| **Usage** | `bl config export-schema [--command ""]` | - -#### Options - -| Flag | Type | Required | Description | -| ------------------ | ------ | -------- | ----------------------------------------------------------------- | -| `--command ` | string | no | Export schema for a specific command only (e.g. "image generate") | - -#### Examples - -```bash -bl config export-schema -``` - -```bash -bl config export-schema --command "video generate" -``` - ### `bl config set` | Field | Value | diff --git a/skills/bailian-cli/reference/console.md b/skills/bailian-cli/reference/console.md index 26e70d5..ee61c38 100644 --- a/skills/bailian-cli/reference/console.md +++ b/skills/bailian-cli/reference/console.md @@ -1,6 +1,6 @@ # `bl console` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/file.md b/skills/bailian-cli/reference/file.md index ef892da..060e257 100644 --- a/skills/bailian-cli/reference/file.md +++ b/skills/bailian-cli/reference/file.md @@ -1,6 +1,6 @@ # `bl file` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/image.md b/skills/bailian-cli/reference/image.md index 66e9df9..d9e9490 100644 --- a/skills/bailian-cli/reference/image.md +++ b/skills/bailian-cli/reference/image.md @@ -1,6 +1,6 @@ # `bl image` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 890f43b..d5bcf1b 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -1,6 +1,6 @@ # bailian-cli (`bl`) command reference -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Command **details** are in sibling `.md` files in this directory. @@ -16,14 +16,15 @@ Use this index for the full quick index and global flags. | `bl auth login` | Authenticate with API key or console browser login (credentials can coexist) | [auth.md](auth.md) | | `bl auth logout` | Clear stored credentials | [auth.md](auth.md) | | `bl auth status` | Show current authentication state | [auth.md](auth.md) | -| `bl config export-schema` | Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas | [config.md](config.md) | | `bl config set` | Set a config value | [config.md](config.md) | | `bl config show` | Display current configuration | [config.md](config.md) | | `bl console call` | Call a Bailian console API via the CLI gateway | [console.md](console.md) | | `bl file upload` | Upload a local file to DashScope temporary storage (48h) | [file.md](file.md) | | `bl image edit` | Edit an existing image with text instructions (Qwen-Image) | [image.md](image.md) | | `bl image generate` | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) | -| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base | [knowledge.md](knowledge.md) | +| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) | [knowledge.md](knowledge.md) | +| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | [knowledge.md](knowledge.md) | +| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) | [knowledge.md](knowledge.md) | | `bl mcp call` | Call a tool on an MCP server (tools/call) | [mcp.md](mcp.md) | | `bl mcp list` | List MCP servers activated under your Bailian account | [mcp.md](mcp.md) | | `bl mcp tools` | List tools exposed by an MCP server (tools/list) | [mcp.md](mcp.md) | @@ -45,7 +46,7 @@ Use this index for the full quick index and global flags. | `bl speech recognize` | Recognize speech from audio files (FunAudio-ASR) | [speech.md](speech.md) | | `bl speech synthesize` | Synthesize speech from text (CosyVoice TTS) | [speech.md](speech.md) | | `bl text chat` | Send a chat completion (OpenAI compatible, DashScope) | [text.md](text.md) | -| `bl update` | Update bl to the latest version | [update.md](update.md) | +| `bl update` | Update the CLI to the latest version | [update.md](update.md) | | `bl usage free` | Query free-tier quota for models (all models if --model is omitted) | [usage.md](usage.md) | | `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) | | `bl usage stats` | Query model usage statistics | [usage.md](usage.md) | @@ -64,11 +65,11 @@ Use this index for the full quick index and global flags. | `advisor` | `recommend` | [advisor.md](advisor.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) | +| `config` | `set`, `show` | [config.md](config.md) | | `console` | `call` | [console.md](console.md) | | `file` | `upload` | [file.md](file.md) | | `image` | `edit`, `generate` | [image.md](image.md) | -| `knowledge` | `retrieve` | [knowledge.md](knowledge.md) | +| `knowledge` | `chat`, `retrieve`, `search` | [knowledge.md](knowledge.md) | | `mcp` | `call`, `list`, `tools` | [mcp.md](mcp.md) | | `memory` | `add`, `delete`, `list`, `profile create`, `profile get`, `search`, `update` | [memory.md](memory.md) | | `omni` | `(root)` | [omni.md](omni.md) | diff --git a/skills/bailian-cli/reference/knowledge.md b/skills/bailian-cli/reference/knowledge.md index fe97738..94c2935 100644 --- a/skills/bailian-cli/reference/knowledge.md +++ b/skills/bailian-cli/reference/knowledge.md @@ -1,25 +1,65 @@ # `bl knowledge` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.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 knowledge retrieve` | Retrieve from a Bailian knowledge base | +| Command | Description | +| ----------------------- | ------------------------------------------------------------------------- | +| `bl knowledge chat` | Chat with a Bailian knowledge base (RAG Q&A with streaming) | +| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | +| `bl knowledge search` | Search a Bailian knowledge base (RAG semantic retrieval) | ## Command details +### `bl knowledge chat` + +| Field | Value | +| --------------- | ------------------------------------------------------------ | +| **Name** | `knowledge chat` | +| **Description** | Chat with a Bailian knowledge base (RAG Q&A with streaming) | +| **Usage** | `bl knowledge chat --message --agent-id [flags]` | + +#### Options + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `--message ` | array | yes | Message text (repeatable). Supports role:content prefix to set role (e.g. user:hello), defaults to user. Follows OpenAI message format | +| `--agent-id ` | string | yes | Q&A service ID (find in console knowledge Q&A page) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--image ` | array | no | Image URL (repeatable). Attached to the last user message as multimodal content | + +#### Notes + +- Response is returned as SSE stream events. Event lifecycle: tool_calling → tool_return → plan_start → planning → plan_end → generation_start → generating → generation_end. tool_calling → tool_return may loop multiple times. +- Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page. +- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `. +- Multi-turn: use --message "user:..." and --message "assistant:..." to pass conversation history. + +#### Examples + +```bash +bl knowledge chat --message "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx +``` + +```bash +bl knowledge chat --message "user:What is RAG?" --message "assistant:RAG is..." --message "How does it work?" --agent-id aid-xxx --workspace-id ws-xxx +``` + +```bash +bl knowledge chat --message "Describe these images" --image https://example.com/a.png --image https://example.com/b.png --agent-id aid-xxx --workspace-id ws-xxx +``` + ### `bl knowledge retrieve` -| Field | Value | -| --------------- | -------------------------------------------------------------- | -| **Name** | `knowledge retrieve` | -| **Description** | Retrieve from a Bailian knowledge base | -| **Usage** | `bl knowledge retrieve --index-id --query [flags]` | +| Field | Value | +| --------------- | ------------------------------------------------------------------------- | +| **Name** | `knowledge retrieve` | +| **Description** | Retrieve from a Bailian knowledge base (deprecated, use `search` instead) | +| **Usage** | `bl knowledge retrieve --index-id --query [flags]` | #### Options @@ -53,3 +93,42 @@ bl knowledge retrieve --index-id idx_xxx --query "How to use Alibaba Cloud Baili ```bash bl knowledge retrieve --api-key $DASHSCOPE_API_KEY --index-id idx_xxx --query "RAG retrieval" --rerank --rerank-model qwen3-rerank-hybrid ``` + +### `bl knowledge search` + +| Field | Value | +| --------------- | ------------------------------------------------------------ | +| **Name** | `knowledge search` | +| **Description** | Search a Bailian knowledge base (RAG semantic retrieval) | +| **Usage** | `bl knowledge search --query --agent-id [flags]` | + +#### Options + +| Flag | Type | Required | Description | +| ------------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--query ` | string | yes | Search query text (required, cannot be empty) | +| `--agent-id ` | string | yes | Retrieval service ID (find in console knowledge retrieval page) | +| `--workspace-id ` | string | no | Workspace ID for API endpoint URL (or set BAILIAN_WORKSPACE_ID) | +| `--image ` | array | no | Image URL for multimodal retrieval (repeatable) | +| `--query-history ` | string | no | User conversation history JSON for context understanding and query rewriting. Format: '[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is..."}]' | + +#### Notes + +- Retrieval scope and strategy (multi-index weighting, routing, reranking, etc.) are driven by the agent_id service config. Only query and agent_id are required. +- Auth: uses DashScope API Key (Bearer token). Get yours from the console API Key page. +- `--workspace-id` can be set via BAILIAN_WORKSPACE_ID env or `kscli config set workspace_id `. +- `--query-history` passes prior conversation turns; the server rewrites the query based on context to improve retrieval relevance. + +#### Examples + +```bash +bl knowledge search --query "What is RAG?" --agent-id aid-xxx --workspace-id ws-xxx +``` + +```bash +bl knowledge search --api-key $DASHSCOPE_API_KEY --query "test search" --agent-id aid-xxx --workspace-id ws-xxx --image https://example.com/img.jpg +``` + +```bash +bl knowledge search --query "How does it work" --agent-id aid-xxx --workspace-id ws-xxx --query-history '[{"role":"user","content":"What is RAG"},{"role":"assistant","content":"RAG is retrieval-augmented generation"}]' +``` diff --git a/skills/bailian-cli/reference/mcp.md b/skills/bailian-cli/reference/mcp.md index 20450c4..e879e36 100644 --- a/skills/bailian-cli/reference/mcp.md +++ b/skills/bailian-cli/reference/mcp.md @@ -1,6 +1,6 @@ # `bl mcp` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) @@ -91,10 +91,10 @@ bl mcp list --output json #### Options -| Flag | Type | Required | Description | -| --------------- | ------ | -------- | ---------------------------------------------------------- | -| `` | string | yes | Server code from `bl mcp list` (e.g. market-cmapi00073529) | -| `--url ` | string | no | Override the MCP endpoint URL (for non-Bailian servers) | +| Flag | Type | Required | Description | +| --------------- | ------ | -------- | ------------------------------------------------------- | +| `` | string | yes | Server code from `mcp list` (e.g. market-cmapi00073529) | +| `--url ` | string | no | Override the MCP endpoint URL (for non-Bailian servers) | #### Examples diff --git a/skills/bailian-cli/reference/memory.md b/skills/bailian-cli/reference/memory.md index 5929e77..6d7e561 100644 --- a/skills/bailian-cli/reference/memory.md +++ b/skills/bailian-cli/reference/memory.md @@ -1,6 +1,6 @@ # `bl memory` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/omni.md b/skills/bailian-cli/reference/omni.md index 5e12aa8..d9aed23 100644 --- a/skills/bailian-cli/reference/omni.md +++ b/skills/bailian-cli/reference/omni.md @@ -1,6 +1,6 @@ # `bl omni` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/pipeline.md b/skills/bailian-cli/reference/pipeline.md index 1040b7a..b81b52a 100644 --- a/skills/bailian-cli/reference/pipeline.md +++ b/skills/bailian-cli/reference/pipeline.md @@ -1,6 +1,6 @@ # `bl pipeline` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/quota.md b/skills/bailian-cli/reference/quota.md index 236f248..807e472 100644 --- a/skills/bailian-cli/reference/quota.md +++ b/skills/bailian-cli/reference/quota.md @@ -1,6 +1,6 @@ # `bl quota` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/search.md b/skills/bailian-cli/reference/search.md index 3926dbf..caf031d 100644 --- a/skills/bailian-cli/reference/search.md +++ b/skills/bailian-cli/reference/search.md @@ -1,6 +1,6 @@ # `bl search` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/speech.md b/skills/bailian-cli/reference/speech.md index f4c5dbc..cf5bbe1 100644 --- a/skills/bailian-cli/reference/speech.md +++ b/skills/bailian-cli/reference/speech.md @@ -1,6 +1,6 @@ # `bl speech` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) @@ -123,7 +123,7 @@ bl speech synthesize --text "Hello" --voice --format wav --sample-rat ``` ```bash -# Stream to audio player (macOS) +bl speech synthesize # Stream to audio player (macOS) ``` ```bash @@ -131,7 +131,7 @@ bl speech synthesize --text "Hello" --voice --stream | afplay - ``` ```bash -# Pipe to ffplay +bl speech synthesize # Pipe to ffplay ``` ```bash diff --git a/skills/bailian-cli/reference/text.md b/skills/bailian-cli/reference/text.md index a1dedfa..cf04650 100644 --- a/skills/bailian-cli/reference/text.md +++ b/skills/bailian-cli/reference/text.md @@ -1,6 +1,6 @@ # `bl text` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) @@ -52,7 +52,7 @@ bl text chat --message "Hello" --message "assistant:Hi!" --message "How are you? ``` ```bash -cat conversation.json | bl text chat --messages-file - --stream +bl text chat --messages-file - --stream ``` ```bash diff --git a/skills/bailian-cli/reference/update.md b/skills/bailian-cli/reference/update.md index aeafe97..f47effb 100644 --- a/skills/bailian-cli/reference/update.md +++ b/skills/bailian-cli/reference/update.md @@ -1,25 +1,25 @@ # `bl update` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.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 update` | Update bl to the latest version | +| Command | Description | +| ----------- | ------------------------------------ | +| `bl update` | Update the CLI to the latest version | ## Command details ### `bl update` -| Field | Value | -| --------------- | ------------------------------- | -| **Name** | `update` | -| **Description** | Update bl to the latest version | -| **Usage** | `bl update` | +| Field | Value | +| --------------- | ------------------------------------ | +| **Name** | `update` | +| **Description** | Update the CLI to the latest version | +| **Usage** | `bl update` | #### Options diff --git a/skills/bailian-cli/reference/usage.md b/skills/bailian-cli/reference/usage.md index 6ae89c9..b7f9369 100644 --- a/skills/bailian-cli/reference/usage.md +++ b/skills/bailian-cli/reference/usage.md @@ -1,6 +1,6 @@ # `bl usage` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/video.md b/skills/bailian-cli/reference/video.md index 9979c88..54b088e 100644 --- a/skills/bailian-cli/reference/video.md +++ b/skills/bailian-cli/reference/video.md @@ -1,6 +1,6 @@ # `bl video` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/vision.md b/skills/bailian-cli/reference/vision.md index 61b22ad..6373bfb 100644 --- a/skills/bailian-cli/reference/vision.md +++ b/skills/bailian-cli/reference/vision.md @@ -1,6 +1,6 @@ # `bl vision` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/skills/bailian-cli/reference/workspace.md b/skills/bailian-cli/reference/workspace.md index 5cceddc..27fc6b8 100644 --- a/skills/bailian-cli/reference/workspace.md +++ b/skills/bailian-cli/reference/workspace.md @@ -1,6 +1,6 @@ # `bl workspace` commands -> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand. +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. > Regenerate: `pnpm --filter bailian-cli run generate:reference`. Index: [index.md](index.md) diff --git a/tools/generate-reference.ts b/tools/generate-reference.ts index c193035..675a4b2 100644 --- a/tools/generate-reference.ts +++ b/tools/generate-reference.ts @@ -1,5 +1,5 @@ /** - * Generator: reads `packages/cli/src/commands/catalog.ts` and writes: + * Generator: reads the bl product command map (`packages/cli/src/commands.ts`) and writes: * - `skills/bailian-cli/reference/index.md` — quick index, global flags, notes * - `skills/bailian-cli/reference/.md` — per top-level command group details * @@ -12,14 +12,14 @@ import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { GLOBAL_OPTIONS, type Command, type OptionDef } from "../packages/core/dist/index.mjs"; -import { commands } from "../packages/cli/src/commands/catalog.ts"; +import { commands } from "../packages/cli/src/commands.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); const REF_DIR = join(__dirname, "../skills/bailian-cli/reference"); const INDEX_PATH = join(REF_DIR, "index.md"); const GENERATED_BANNER = - "> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand.\n" + + "> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand.\n" + "> Regenerate: `pnpm --filter bailian-cli run generate:reference`."; function escCell(s: string): string { @@ -50,9 +50,14 @@ function formatOptionsTable(options: OptionDef[] | undefined): string { ].join("\n"); } -function formatExamples(examples: string[] | undefined): string { - if (!examples?.length) return "_No examples._\n"; - return examples.map((ex) => ["```bash", ex, "```"].join("\n")).join("\n\n") + "\n"; +function formatExamples(path: string, exampleArgs: string[] | undefined): string { + if (!exampleArgs?.length) return "_No examples._\n"; + // Commands store argument-only examples; prepend `bl ` for the reference. + return ( + exampleArgs + .map((ex) => ["```bash", `bl ${path}${ex ? ` ${ex}` : ""}`, "```"].join("\n")) + .join("\n\n") + "\n" + ); } function formatNotes(notes: string[] | undefined): string { @@ -64,11 +69,11 @@ function commandSection(path: string, cmd: Command): string { const lines: string[] = []; lines.push(`### \`bl ${path}\``, ""); lines.push(`| Field | Value |`, `| --- | --- |`); - lines.push(`| **Name** | \`${escCell(cmd.name)}\` |`); + lines.push(`| **Name** | \`${escCell(path)}\` |`); lines.push(`| **Description** | ${escCell(cmd.description)} |`); - if (cmd.usage) { - lines.push(`| **Usage** | \`${escCell(cmd.usage)}\` |`); - } + // Commands store argument-only usage; the `bl ` prefix is added here. + const usage = `bl ${path}${cmd.usageArgs ? ` ${cmd.usageArgs}` : ""}`; + lines.push(`| **Usage** | \`${escCell(usage)}\` |`); lines.push(""); lines.push("#### Options", ""); @@ -80,7 +85,7 @@ function commandSection(path: string, cmd: Command): string { } lines.push("#### Examples", ""); - lines.push(formatExamples(cmd.examples)); + lines.push(formatExamples(path, cmd.exampleArgs)); return lines.join("\n"); } diff --git a/tools/release/check.mjs b/tools/release/check.mjs index aad327f..cd5686d 100644 --- a/tools/release/check.mjs +++ b/tools/release/check.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from "url"; import { packAndScan } from "./lib/pack-scan.mjs"; import { run } from "./lib/proc.mjs"; import { assertReadmeSync, loadAndValidatePackages } from "./lib/validate.mjs"; +import { ALL_PACKAGES, PACKAGES } from "./lib/packages.mjs"; function log(msg = "") { process.stdout.write(`${msg}\n`); @@ -17,25 +18,29 @@ function step(msg) { * Pure-validation pipeline. Reusable from publish-stable / publish-channel. * Returns { coreJson, cliJson } for callers that need the parsed package.jsons. * - * @param {{ channel?: boolean }} [options] + * @param {{ channel?: boolean, knowledge?: boolean }} [options] * @param {boolean} [options.channel] — When true (publish-channel): regenerate * `reference/` and assert it matches git, but do not sync `SKILL.md` from the * temporary beta `package.json` version (repo skill stays aligned with stable). + * @param {boolean} [options.knowledge] — When true: also build and validate + * knowledge-studio-cli alongside the base packages. */ export async function runCheck(options = {}) { const channel = options.channel === true; + const knowledge = options.knowledge === true; + const packages = knowledge ? ALL_PACKAGES : PACKAGES; step("pnpm install --frozen-lockfile"); run("pnpm", ["install", "--frozen-lockfile"]); step("metadata: README sync, version consistency, workspace:* dep"); assertReadmeSync(); - const { coreJson, cliJson } = loadAndValidatePackages(); + const { coreJson, cliJson } = loadAndValidatePackages({ packages }); log(`bailian-cli-core@${coreJson.version}`); log(`bailian-cli@${cliJson.version}`); - step("build bailian-cli-core"); - run("pnpm", ["--filter", "bailian-cli-core", "run", "build"]); + step("build bailian-cli dependencies (core, commands, runtime)"); + run("pnpm", ["--filter", "bailian-cli^...", "run", "build"]); step( channel @@ -63,8 +68,13 @@ export async function runCheck(options = {}) { step("build bailian-cli"); run("pnpm", ["--filter", "bailian-cli", "run", "build"]); + if (knowledge) { + step("build knowledge-studio-cli"); + run("pnpm", ["--filter", "knowledge-studio-cli", "run", "build"]); + } + step("pack + scan (publint, gitleaks)"); - packAndScan({ log }); + packAndScan({ log, packages }); log("\nrelease check passed."); return { coreJson, cliJson }; diff --git a/tools/release/lib/pack-scan.mjs b/tools/release/lib/pack-scan.mjs index 5825d7b..b7b0d8e 100644 --- a/tools/release/lib/pack-scan.mjs +++ b/tools/release/lib/pack-scan.mjs @@ -13,10 +13,11 @@ function extractTarball(tarball, tempDir, key) { return extractDir; } -export function packAndScan({ log }) { +export function packAndScan({ log, packages }) { + const pkgs = packages ?? PACKAGES; const tempDir = mkdtempSync(join(tmpdir(), "bailian-release-")); try { - for (const pkg of PACKAGES) { + for (const pkg of pkgs) { const json = readPackageJson(pkg); log(`packing ${pkg.name}@${json.version}`); const tarball = pnpmPack(pkg, tempDir, json); diff --git a/tools/release/lib/packages.mjs b/tools/release/lib/packages.mjs index 6416ea1..aa48e37 100644 --- a/tools/release/lib/packages.mjs +++ b/tools/release/lib/packages.mjs @@ -6,9 +6,16 @@ export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..") export const PACKAGES = [ { key: "core", dir: "packages/core", name: "bailian-cli-core" }, + { key: "runtime", dir: "packages/runtime", name: "bailian-cli-runtime" }, + { key: "commands", dir: "packages/commands", name: "bailian-cli-commands" }, { key: "cli", dir: "packages/cli", name: "bailian-cli" }, ]; +// knowledge-studio-cli shares the same library deps as bailian-cli. +// Published via publish.yml with package=knowledge-studio-cli (passes --knowledge flag). +export const KSCLI_PACKAGE = { key: "kscli", dir: "packages/kscli", name: "knowledge-studio-cli" }; +export const ALL_PACKAGES = [...PACKAGES, KSCLI_PACKAGE]; + export function readJson(path) { return JSON.parse(readFileSync(path, "utf-8")); } diff --git a/tools/release/lib/validate.mjs b/tools/release/lib/validate.mjs index 27b51ae..bb0fe92 100644 --- a/tools/release/lib/validate.mjs +++ b/tools/release/lib/validate.mjs @@ -18,9 +18,11 @@ export function assertReadmeSync() { } } -export function loadAndValidatePackages() { +export function loadAndValidatePackages({ packages } = {}) { + const pkgs = packages ?? PACKAGES; + const internalNames = new Set(pkgs.map((p) => p.name)); const jsonByKey = new Map(); - for (const pkg of PACKAGES) { + for (const pkg of pkgs) { const json = readPackageJson(pkg); if (json.name !== pkg.name) { throw new Error(`${pkg.dir} name must be ${pkg.name}, got ${json.name}`); @@ -30,18 +32,21 @@ export function loadAndValidatePackages() { const coreJson = jsonByKey.get("core"); const cliJson = jsonByKey.get("cli"); + const version = coreJson.version; - if (cliJson.version !== coreJson.version) { - throw new Error( - `core and cli versions must match, got ${coreJson.version} and ${cliJson.version}.`, - ); - } - - const cliCoreDep = cliJson.dependencies?.["bailian-cli-core"]; - if (cliCoreDep !== "workspace:*") { - throw new Error( - `packages/cli source dependency on bailian-cli-core must be "workspace:*", got ${cliCoreDep}.`, - ); + for (const pkg of pkgs) { + const json = jsonByKey.get(pkg.key); + if (json.version !== version) { + throw new Error( + `all package versions must match ${version} (bailian-cli-core), ` + + `but ${pkg.name} is ${json.version}.`, + ); + } + for (const [dep, range] of Object.entries(json.dependencies ?? {})) { + if (internalNames.has(dep) && range !== "workspace:*") { + throw new Error(`${pkg.name} dependency on ${dep} must be "workspace:*", got ${range}.`); + } + } } return { coreJson, cliJson }; diff --git a/tools/release/publish-channel.mjs b/tools/release/publish-channel.mjs index 7253d7e..9c24f49 100644 --- a/tools/release/publish-channel.mjs +++ b/tools/release/publish-channel.mjs @@ -6,7 +6,8 @@ import { runCheck } from "./check.mjs"; import { headSha7, utcDateStamp } from "./lib/git.mjs"; import { npmViewExists, pnpmPublish } from "./lib/npm.mjs"; import { - findPackage, + ALL_PACKAGES, + PACKAGES, packageJsonPath, readPackageJson, writePackageJson, @@ -25,11 +26,14 @@ const { values } = parseArgs({ options: { channel: { type: "string" }, "dry-run": { type: "boolean", default: false }, + knowledge: { type: "boolean", default: false }, }, allowPositionals: false, }); const channel = values.channel; const dryRun = values["dry-run"]; +const knowledge = values.knowledge; +const packages = knowledge ? ALL_PACKAGES : PACKAGES; assertChannel(channel); if (!dryRun && !process.env.CI) { @@ -37,16 +41,15 @@ if (!dryRun && !process.env.CI) { process.exit(1); } -const core = findPackage("core"); -const cli = findPackage("cli"); -const corePath = packageJsonPath(core); -const cliPath = packageJsonPath(cli); -const coreOriginal = readFileSync(corePath, "utf-8"); -const cliOriginal = readFileSync(cliPath, "utf-8"); +// Snapshot every package.json so the temporary version bump is reverted in +// `finally`, even when the release fails midway. +const originals = packages.map((pkg) => { + const path = packageJsonPath(pkg); + return { pkg, path, content: readFileSync(path, "utf-8") }; +}); function restoreOriginals() { - writeFileSync(corePath, coreOriginal); - writeFileSync(cliPath, cliOriginal); + for (const { path, content } of originals) writeFileSync(path, content); } try { @@ -57,32 +60,29 @@ try { log(`channel=${channel} version=${betaVersion}`); step("temporarily bump package.json (not committed)"); - const coreJson = readPackageJson(core); - const cliJson = readPackageJson(cli); - coreJson.version = betaVersion; - cliJson.version = betaVersion; - writePackageJson(core, coreJson); - writePackageJson(cli, cliJson); - // pnpm pack resolves `workspace:*` to the in-tree version, so CLI tarball - // will depend on bailian-cli-core@ after this bump. + for (const pkg of packages) { + const json = readPackageJson(pkg); + json.version = betaVersion; + writePackageJson(pkg, json); + } - await runCheck({ channel: true }); + await runCheck({ channel: true, knowledge }); step(`idempotency: check ${betaVersion} against registry`); - const corePublished = npmViewExists(core.name, betaVersion); - const cliPublished = npmViewExists(cli.name, betaVersion); - log(`${core.name}@${betaVersion}: ${corePublished ? "already published" : "to publish"}`); - log(`${cli.name}@${betaVersion}: ${cliPublished ? "already published" : "to publish"}`); - if (corePublished && cliPublished) { - log("\nboth packages already published; nothing to do."); + const published = new Map(); + for (const pkg of packages) { + const exists = npmViewExists(pkg.name, betaVersion); + published.set(pkg.key, exists); + log(`${pkg.name}@${betaVersion}: ${exists ? "already published" : "to publish"}`); + } + if (packages.every((pkg) => published.get(pkg.key))) { + log("\nall packages already published; nothing to do."); } else { - if (!corePublished) { - step(`publish ${core.name}@${betaVersion} (tag=${channel}, provenance)`); - pnpmPublish(core, { tag: channel, provenance: true, dryRun }); - } - if (!cliPublished) { - step(`publish ${cli.name}@${betaVersion} (tag=${channel}, provenance)`); - pnpmPublish(cli, { tag: channel, provenance: true, dryRun }); + // Publish in dependency order. + for (const pkg of packages) { + if (published.get(pkg.key)) continue; + step(`publish ${pkg.name}@${betaVersion} (tag=${channel}, provenance)`); + pnpmPublish(pkg, { tag: channel, provenance: true, dryRun }); } } diff --git a/tools/release/publish-stable.mjs b/tools/release/publish-stable.mjs index 4891276..13c6359 100644 --- a/tools/release/publish-stable.mjs +++ b/tools/release/publish-stable.mjs @@ -4,7 +4,7 @@ import { parseArgs } from "util"; import { runCheck } from "./check.mjs"; import { createTag, currentBranch, isWorkingTreeClean, pushTag, tagExists } from "./lib/git.mjs"; import { npmViewExists, pnpmPublish } from "./lib/npm.mjs"; -import { findPackage } from "./lib/packages.mjs"; +import { ALL_PACKAGES, findPackage, PACKAGES } from "./lib/packages.mjs"; function log(msg = "") { process.stdout.write(`${msg}\n`); @@ -17,10 +17,13 @@ function step(msg) { const { values } = parseArgs({ options: { "dry-run": { type: "boolean", default: false }, + knowledge: { type: "boolean", default: false }, }, allowPositionals: false, }); const dryRun = values["dry-run"]; +const knowledge = values.knowledge; +const packages = knowledge ? ALL_PACKAGES : PACKAGES; try { if (!dryRun && !process.env.CI) { @@ -40,28 +43,26 @@ try { log("[dry-run] skipping working-tree + branch preflight"); } - const { coreJson } = await runCheck(); - const version = coreJson.version; // === cliJson.version, asserted by runCheck + const { coreJson } = await runCheck({ knowledge }); + const version = coreJson.version; // all packages share this, asserted by runCheck step(`idempotency: check ${version} against registry`); - const core = findPackage("core"); - const cli = findPackage("cli"); - const corePublished = npmViewExists(core.name, version); - const cliPublished = npmViewExists(cli.name, version); - log(`${core.name}@${version}: ${corePublished ? "already published" : "to publish"}`); - log(`${cli.name}@${version}: ${cliPublished ? "already published" : "to publish"}`); - if (corePublished && cliPublished) { - log("\nboth packages already published; nothing to do."); + const published = new Map(); + for (const pkg of packages) { + const exists = npmViewExists(pkg.name, version); + published.set(pkg.key, exists); + log(`${pkg.name}@${version}: ${exists ? "already published" : "to publish"}`); + } + if (packages.every((pkg) => published.get(pkg.key))) { + log("\nall packages already published; nothing to do."); process.exit(0); } - if (!corePublished) { - step(`publish ${core.name}@${version} (tag=latest, provenance)`); - pnpmPublish(core, { tag: "latest", provenance: true, dryRun }); - } - if (!cliPublished) { - step(`publish ${cli.name}@${version} (tag=latest, provenance)`); - pnpmPublish(cli, { tag: "latest", provenance: true, dryRun }); + // Publish in dependency order. + for (const pkg of packages) { + if (published.get(pkg.key)) continue; + step(`publish ${pkg.name}@${version} (tag=latest, provenance)`); + pnpmPublish(pkg, { tag: "latest", provenance: true, dryRun }); } if (dryRun) { diff --git a/tsconfig.json b/tsconfig.json index c785f30..44c1642 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "noEmit": true, "module": "nodenext", "moduleResolution": "nodenext", + "customConditions": ["@bailian-cli/source"], "allowImportingTsExtensions": true, "esModuleInterop": true }