refactor(flags): keyed type-inferred flag schema; option→flag rename

Replace the positional `OptionDef[]` array (key/type regex-parsed from
"--x <v>" strings) with a keyed `FlagsDef` record whose `type` drives both
runtime parsing and compile-time flag-type inference (`Flags<typeof DEF>`).
GLOBAL_FLAGS becomes the single source; the hand-kept GlobalFlags interface
(types/flags.ts) is deleted.

- core: SwitchFlag|ValueFlag union, ParsedFlags/Flags inference, defineCommand
  infers F from spec.flags
- runtime: parseFlags dispatches on def.type (switch/string/number/boolean/
  array/choices) with declarative required-flag enforcement
- commands: migrate every flag declaration to the keyed form
- naming: option→flag throughout (OptionDef→FlagDef, OptionsDef→FlagsDef,
  GLOBAL_OPTIONS→GLOBAL_FLAGS, command field options→flags), plus user-facing
  "Options:"→"Flags:" in help and the regenerated skill reference docs

Behavior-preserving aside from the intentional Options→Flags wording:
vp check clean across all packages, 29 parser tests pass, reference regen
byte-identical before the terminology swap.
This commit is contained in:
若麒
2026-06-28 11:19:21 +08:00
parent cbd3c1232c
commit f9bf36c242
82 changed files with 1760 additions and 1502 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Command } from "bailian-cli-core";
import type { AnyCommand } from "bailian-cli-core";
import {
authLogin,
authStatus,
@@ -52,7 +52,7 @@ import {
// 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<string, Command> = {
export const commands: Record<string, AnyCommand> = {
"auth login": authLogin,
"auth status": authStatus,
"auth logout": authLogout,
@@ -1,11 +1,9 @@
import {
analyzeIntent,
buildDocLink,
type Config,
defineCommand,
detectOutputFormat,
type GetModelsOptions,
type GlobalFlags,
getModels,
type IntentProfile,
type PipelineStep,
@@ -217,13 +215,14 @@ export default defineCommand({
"Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking)",
auth: "apiKey",
usageArgs: "--message <text> [flags]",
options: [
{
flag: "--message <text>",
flags: {
message: {
type: "string",
valueHint: "<text>",
description: "Describe your requirements",
required: true,
},
],
},
exampleArgs: [
'--message "I need a visual-understanding chatbot"',
'--message "Build an Agent that auto-generates animations"',
@@ -231,8 +230,8 @@ export default defineCommand({
'--message "Low-cost high-concurrency online customer service" --output json',
'--message "Long document summarization" --dry-run',
],
async run(config: Config, flags: GlobalFlags) {
const userInput = flags.message as string;
async run(config, flags) {
const userInput = flags.message;
const top = 3;
const format = detectOutputFormat(config.output);
+51 -28
View File
@@ -5,8 +5,6 @@ import {
appCompletionEndpoint,
parseSSE,
detectOutputFormat,
type Config,
type GlobalFlags,
type AppCompletionRequest,
type AppStreamChunk,
type AppCompletionResponse,
@@ -17,22 +15,48 @@ export default defineCommand({
description: "Call a Bailian application (agent or workflow)",
auth: "apiKey",
usageArgs: "--app-id <id> --prompt <text> [flags]",
options: [
{ flag: "--app-id <id>", description: "Application ID (required)", required: true },
{ flag: "--prompt <text>", description: "Input prompt text", required: true },
{
flag: "--image <url>",
description: "Image URL(s) to pass to the app (repeatable)",
type: "array",
flags: {
appId: {
type: "string",
valueHint: "<id>",
description: "Application ID (required)",
required: true,
},
{ flag: "--file-id <id>", description: "Pre-uploaded file ID(s) (repeatable)", type: "array" },
{ flag: "--session-id <id>", description: "Session ID for multi-turn conversation" },
{ flag: "--stream", description: "Stream response (default: on in TTY)" },
{ flag: "--pipeline-ids <ids>", description: "Knowledge base pipeline IDs (comma-separated)" },
{ flag: "--memory-id <id>", description: "Memory ID for long-term memory" },
{ flag: "--biz-params <json>", description: "Business parameters JSON (workflow variables)" },
{ flag: "--has-thoughts", description: "Show agent thinking process" },
],
prompt: {
type: "string",
valueHint: "<text>",
description: "Input prompt text",
required: true,
},
image: {
type: "array",
valueHint: "<url>",
description: "Image URL(s) to pass to the app (repeatable)",
},
fileId: {
type: "array",
valueHint: "<id>",
description: "Pre-uploaded file ID(s) (repeatable)",
},
sessionId: {
type: "string",
valueHint: "<id>",
description: "Session ID for multi-turn conversation",
},
stream: { type: "switch", description: "Stream response (default: on in TTY)" },
pipelineIds: {
type: "string",
valueHint: "<ids>",
description: "Knowledge base pipeline IDs (comma-separated)",
},
memoryId: { type: "string", valueHint: "<id>", description: "Memory ID for long-term memory" },
bizParams: {
type: "string",
valueHint: "<json>",
description: "Business parameters JSON (workflow variables)",
},
hasThoughts: { type: "switch", description: "Show agent thinking process" },
},
exampleArgs: [
'--app-id abc123 --prompt "Hello"',
'--app-id abc123 --prompt "Describe this image" --image https://example.com/photo.jpg',
@@ -41,12 +65,11 @@ export default defineCommand({
'--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;
const prompt = flags.prompt as string;
async run(config, flags) {
const appId = flags.appId;
const prompt = flags.prompt;
const shouldStream =
flags.stream === true || (flags.stream === undefined && process.stdout.isTTY);
const shouldStream = flags.stream || process.stdout.isTTY;
const format = detectOutputFormat(config.output);
const body: AppCompletionRequest = {
@@ -57,17 +80,17 @@ export default defineCommand({
};
if (flags.sessionId) {
body.input.session_id = flags.sessionId as string;
body.input.session_id = flags.sessionId;
}
// Pass image URLs via image_list
const imageUrls = flags.image as string[] | undefined;
const imageUrls = flags.image;
if (imageUrls && imageUrls.length > 0) {
body.input.image_list = imageUrls;
}
// Pass pre-uploaded file IDs
const fileIds = flags.fileId as string[] | undefined;
const fileIds = flags.fileId;
if (fileIds && fileIds.length > 0) {
body.input.file_ids = fileIds;
}
@@ -77,7 +100,7 @@ export default defineCommand({
}
if (flags.pipelineIds) {
const ids = (flags.pipelineIds as string)
const ids = flags.pipelineIds
.split(",")
.map((s) => s.trim())
.filter(Boolean);
@@ -85,12 +108,12 @@ export default defineCommand({
}
if (flags.memoryId) {
body.parameters!.memory_id = flags.memoryId as string;
body.parameters!.memory_id = flags.memoryId;
}
if (flags.bizParams) {
try {
body.input.biz_params = JSON.parse(flags.bizParams as string);
body.input.biz_params = JSON.parse(flags.bizParams);
} catch {
process.stderr.write("Error: --biz-params must be valid JSON\n");
process.exit(1);
+20 -24
View File
@@ -3,8 +3,6 @@ import {
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
@@ -14,37 +12,35 @@ export default defineCommand({
description: "List Bailian applications",
auth: "console",
usageArgs: "[flags]",
options: [
{
flag: "--name <name>",
flags: {
name: {
type: "string",
valueHint: "<name>",
description: "Filter by app name (keyword search)",
},
{
flag: "--page <n>",
page: {
type: "number",
valueHint: "<n>",
description: "Page number (default: 1)",
type: "number",
},
{
flag: "--page-size <n>",
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 30)",
type: "number",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
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;
const pageSize = (flags.pageSize as number) || 30;
async run(config, flags) {
const name = flags.name || "";
const pageNo = flags.page || 1;
const pageSize = flags.pageSize || 30;
const format = detectOutputFormat(config.output);
const credential = await resolveConsoleGatewayCredential(config);
+19 -27
View File
@@ -1,10 +1,4 @@
import {
defineCommand,
readConfigFile,
writeConfigFile,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { defineCommand, readConfigFile, writeConfigFile } from "bailian-cli-core";
import { printQuickStart } from "bailian-cli-runtime";
import { emitBare } from "bailian-cli-runtime";
import {
@@ -17,21 +11,22 @@ export default defineCommand({
description: "Authenticate with API key or console browser login (credentials can coexist)",
auth: "none",
usageArgs: "--api-key <key> | --console",
options: [
{ flag: "--api-key <key>", description: "DashScope API key to store" },
{
flag: "--base-url <url>",
flags: {
apiKey: { type: "string", valueHint: "<key>", description: "DashScope API key to store" },
baseUrl: {
type: "string",
valueHint: "<url>",
description: "DashScope API base URL (used with --api-key for validation)",
},
{
flag: "--console",
console: {
type: "switch",
description:
"Sign in via browser; use --console-site to choose domestic (default) or international",
},
],
},
exampleArgs: ["--api-key sk-xxxxx", "--console"],
validate: (f) => (!f.console && !f.apiKey ? "Provide --api-key or --console" : undefined),
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
if (flags.console) {
if (config.dryRun) {
emitBare(
@@ -46,17 +41,16 @@ export default defineCommand({
return;
}
const envKey = process.env.DASHSCOPE_API_KEY;
if (envKey && !flags.apiKey) {
process.stderr.write(`Warning: DASHSCOPE_API_KEY is already set in environment.\n`);
}
// --api-key path; validate() guarantees apiKey on the non-console branch.
if (flags.apiKey) {
const key = flags.apiKey;
const baseUrl = flags.baseUrl || undefined;
const effectiveConfig = baseUrl ? { ...config, baseUrl } : config;
const key = flags.apiKey as string;
const baseUrl = (flags.baseUrl as string) || undefined;
const effectiveConfig = baseUrl ? { ...config, baseUrl } : config;
if (!config.dryRun) {
if (config.dryRun) {
emitBare("Would validate and save API key.");
return;
}
if (baseUrl) {
const existing = readConfigFile() as Record<string, unknown>;
existing.base_url = baseUrl;
@@ -64,8 +58,6 @@ export default defineCommand({
}
await validateAndPersistApiKey(effectiveConfig, key, effectiveConfig.baseUrl);
printQuickStart();
} else {
emitBare("Would validate and save API key.");
}
},
});
@@ -4,8 +4,6 @@ import {
readConfigFile,
writeConfigFile,
getConfigPath,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
@@ -21,16 +19,15 @@ export default defineCommand({
description: "Clear stored credentials",
auth: "none",
usageArgs: "[--console] [--yes] [--dry-run]",
options: [
{
flag: "--console",
flags: {
console: {
type: "switch",
description: "Only clear the console access_token, keep api_key intact",
type: "boolean",
},
{ flag: "--yes", description: "Skip confirmation prompt" },
],
yes: { type: "switch", description: "Skip confirmation prompt" },
},
exampleArgs: ["", "--console", "--dry-run", "--yes"],
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
const file = readConfigFile();
if (flags.console) {
+9 -13
View File
@@ -5,7 +5,6 @@ import {
detectOutputFormat,
maskToken,
type Config,
type GlobalFlags,
type ResolvedCredential,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
@@ -141,20 +140,17 @@ function emitTextStatus(status: AuthStatusPayload, config: Config): void {
export default defineCommand({
description: "Show current authentication state",
auth: "none",
options: [
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
exampleArgs: ["", "--output json"],
flags: {
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
exampleArgs: ["", "--output json"],
async run(config: Config, _flags: GlobalFlags) {
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
async run(config, _flags) {
const format = detectOutputFormat(config.output);
const status = await buildStatus(config);
+9 -10
View File
@@ -5,8 +5,6 @@ import {
readConfigFile,
writeConfigFile,
BailianError,
type Config,
type GlobalFlags,
ExitCode,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
@@ -53,23 +51,24 @@ export default defineCommand({
description: "Set a config value",
auth: "none",
usageArgs: "--key <key> --value <value>",
options: [
{
flag: "--key <key>",
flags: {
key: {
type: "string",
valueHint: "<key>",
description:
"Config key (base_url, output, output_dir, timeout, api_key, access_token, default_*_model, access_key_id, access_key_secret, workspace_id)",
required: true,
},
{ flag: "--value <value>", description: "Value to set", required: true },
],
value: { type: "string", valueHint: "<value>", description: "Value to set", required: true },
},
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;
const value = flags.value as string;
async run(config, flags) {
const key = flags.key;
const value = flags.value;
// Resolve hyphen aliases to underscore keys
const resolvedKey: string = KEY_ALIASES[key] || key;
@@ -4,8 +4,6 @@ import {
getConfigPath,
detectOutputFormat,
maskToken,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
@@ -13,7 +11,7 @@ export default defineCommand({
description: "Display current configuration",
auth: "none",
exampleArgs: ["", "--output json"],
async run(config: Config, _flags: GlobalFlags) {
async run(config, _flags) {
const file = loadConfigFile();
const format = detectOutputFormat(config.output);
+16 -19
View File
@@ -6,8 +6,6 @@ import {
CONSOLE_GATEWAY_NO_TOKEN_MESSAGE,
BailianError,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
@@ -15,35 +13,34 @@ export default defineCommand({
description: "Call a Bailian console API via the CLI gateway",
auth: "console",
usageArgs: "--api <api> --data <json> [flags]",
options: [
{
flag: "--api <api>",
flags: {
api: {
type: "string",
valueHint: "<api>",
description: "API name (e.g. zeldaEasy.broadscope-bailian.memory-library.getLibraries)",
required: true,
},
{
flag: "--data <json>",
data: {
type: "string",
valueHint: "<json>",
description: "Request data as JSON string",
required: true,
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
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;
const dataRaw = flags.data as string;
async run(config, flags) {
const api = flags.api;
const dataRaw = flags.data;
let data: Record<string, unknown>;
try {
+12 -17
View File
@@ -1,38 +1,33 @@
import {
defineCommand,
resolveCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
uploadFile,
} from "bailian-cli-core";
import { defineCommand, resolveCredential, detectOutputFormat, uploadFile } from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
export default defineCommand({
description: "Upload a local file to DashScope temporary storage (48h)",
auth: "apiKey",
usageArgs: "--file <path> --model <model>",
options: [
{
flag: "--file <path>",
flags: {
file: {
type: "string",
valueHint: "<path>",
description: "Local file to upload (image, video, audio)",
required: true,
},
{
flag: "--model <model>",
model: {
type: "string",
valueHint: "<model>",
description: "Target model name (file is bound to this model)",
required: true,
},
],
},
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;
const model = flags.model as string;
async run(config, flags) {
const filePath = flags.file;
const model = flags.model;
const format = detectOutputFormat(config.output);
+51 -35
View File
@@ -3,8 +3,6 @@ import {
requestJson,
imageSyncEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
resolveCredential,
resolveFileUrl,
resolveOutputDir,
@@ -28,38 +26,57 @@ export default defineCommand({
description: "Edit an existing image with text instructions (Qwen-Image)",
auth: "apiKey",
usageArgs: "--image <url> --prompt <text> [flags]",
options: [
{
flag: "--image <url>",
flags: {
image: {
type: "array",
valueHint: "<url>",
description: "Source image URL or local file path (repeatable for multi-image merge)",
required: true,
type: "array",
},
{ flag: "--prompt <text>", description: "Edit instruction text", required: true },
{ flag: "--model <model>", description: "Model ID (default: qwen-image-2.0)" },
{
flag: "--size <W*H>",
prompt: {
type: "string",
valueHint: "<text>",
description: "Edit instruction text",
required: true,
},
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: qwen-image-2.0)",
},
size: {
type: "string",
valueHint: "<W*H>",
description: "Output image size: ratio (3:4, 16:9) or pixels (2048*2048)",
},
{ flag: "--n <count>", description: "Number of images (default: 1, max: 6)", type: "number" },
{ flag: "--seed <n>", description: "Random seed for reproducible results", type: "number" },
{
flag: "--negative-prompt <text>",
n: {
type: "number",
valueHint: "<count>",
description: "Number of images (default: 1, max: 6)",
},
seed: { type: "number", valueHint: "<n>", description: "Random seed for reproducible results" },
negativePrompt: {
type: "string",
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
},
{
flag: "--prompt-extend <bool>",
promptExtend: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE,
type: "boolean",
},
{
flag: "--watermark <bool>",
watermark: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_WATERMARK,
type: "boolean",
},
{ flag: "--out-dir <dir>", description: "Download images to directory" },
{ flag: "--out-prefix <prefix>", description: "Filename prefix (default: edited)" },
],
outDir: { type: "string", valueHint: "<dir>", description: "Download images to directory" },
outPrefix: {
type: "string",
valueHint: "<prefix>",
description: "Filename prefix (default: edited)",
},
},
exampleArgs: [
'--image ./photo.png --prompt "Replace the background with a beach"',
'--image https://example.com/logo.png --prompt "Change color to blue" --n 3',
@@ -67,24 +84,24 @@ export default defineCommand({
'--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) {
async run(config, flags) {
// Normalize --image to string array (supports both single and repeated flags)
let rawImages: string[] = [];
if (Array.isArray(flags.image)) {
rawImages = flags.image as string[];
rawImages = flags.image;
} else if (typeof flags.image === "string") {
rawImages = [flags.image];
}
const prompt = flags.prompt as string;
const prompt = flags.prompt;
const model = (flags.model as string) || config.defaultImageModel || "qwen-image-2.0";
const model = flags.model || config.defaultImageModel || "qwen-image-2.0";
// Auto-upload local files (resolve all images in parallel)
const credential = await resolveCredential(config);
const resolvedImages = await Promise.all(
rawImages.map((img) => resolveFileUrl(img, credential.token, model)),
);
const n = (flags.n as number) ?? 1;
const n = flags.n ?? 1;
const promptExtend = resolveBooleanFlag(flags.promptExtend, true, "prompt-extend");
@@ -92,7 +109,7 @@ export default defineCommand({
const contentItems: Array<{ image?: string; text?: string }> = resolvedImages.map(
(u: string) => ({ image: u }),
);
contentItems.push({ text: prompt! });
contentItems.push({ text: prompt });
const watermark = resolveWatermark(flags.watermark);
@@ -107,12 +124,12 @@ export default defineCommand({
],
},
parameters: {
size: resolveImageSize(flags.size as string | undefined, true),
size: resolveImageSize(flags.size, true),
n,
seed: flags.seed as number | undefined,
seed: flags.seed,
prompt_extend: promptExtend,
watermark,
negative_prompt: (flags.negativePrompt as string) || undefined,
negative_prompt: flags.negativePrompt || undefined,
},
};
@@ -153,12 +170,11 @@ export default defineCommand({
}
const outDir = resolveOutputDir(config, {
flagDir: flags.outDir as string | undefined,
flagDir: flags.outDir,
subDir: flags.outDir ? undefined : "images",
});
const prefix =
(flags.outPrefix as string) || generateFilename("edited", flags?.prompt as string);
const prefix = flags.outPrefix || generateFilename("edited", flags.prompt);
// Parallel download all images
const items =
@@ -6,7 +6,8 @@ import {
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type FlagsDef,
type Flags,
resolveOutputDir,
type DashScopeImageRequest,
type DashScopeImageSyncResponse,
@@ -35,49 +36,66 @@ function isSyncModel(model: string): boolean {
return SYNC_MODEL_PREFIXES.some((p) => model.startsWith(p));
}
const GENERATE_FLAGS = {
prompt: { type: "string", valueHint: "<text>", description: "Image description", required: true },
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: qwen-image-2.0)",
},
size: {
type: "string",
valueHint: "<W*H>",
description: "Image size: ratio (3:4, 16:9, 1:1) or pixels (2048*2048)",
},
n: {
type: "number",
valueHint: "<count>",
description: "Number of images per request (default: 1, max: 6)",
},
seed: {
type: "number",
valueHint: "<n>",
description: "Random seed for reproducible generation",
},
negativePrompt: {
type: "string",
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
},
promptExtend: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
},
watermark: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_WATERMARK,
},
noWait: {
type: "switch",
description: "Return task ID immediately without waiting (async models only)",
},
outDir: { type: "string", valueHint: "<dir>", description: "Download images to directory" },
outPrefix: {
type: "string",
valueHint: "<prefix>",
description: "Filename prefix (default: image)",
},
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 3)",
},
} satisfies FlagsDef;
type GenerateFlags = Flags<typeof GENERATE_FLAGS>;
export default defineCommand({
description: "Generate images (Qwen-Image / wan2.x)",
auth: "apiKey",
usageArgs: "--prompt <text> [flags]",
options: [
{ flag: "--prompt <text>", description: "Image description", required: true },
{ flag: "--model <model>", description: "Model ID (default: qwen-image-2.0)" },
{
flag: "--size <W*H>",
description: "Image size: ratio (3:4, 16:9, 1:1) or pixels (2048*2048)",
},
{
flag: "--n <count>",
description: "Number of images per request (default: 1, max: 6)",
type: "number",
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{
flag: "--negative-prompt <text>",
description: "Negative prompt to exclude unwanted content",
},
{
flag: "--prompt-extend <bool>",
description: BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE,
type: "boolean",
},
{
flag: "--watermark <bool>",
description: BOOL_FLAG_WATERMARK,
type: "boolean",
},
{
flag: "--no-wait",
description: "Return task ID immediately without waiting (async models only)",
},
{ flag: "--out-dir <dir>", description: "Download images to directory" },
{ flag: "--out-prefix <prefix>", description: "Filename prefix (default: image)" },
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 3)",
type: "number",
},
],
flags: GENERATE_FLAGS,
exampleArgs: [
'--prompt "A cat in a spacesuit on Mars"',
'--prompt "Logo design" --n 3 --out-dir ./generated/',
@@ -89,15 +107,15 @@ export default defineCommand({
'--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) {
const prompt = flags.prompt as string;
async run(config, flags) {
const prompt = flags.prompt;
const model = (flags.model as string) || config.defaultImageModel || "qwen-image-2.0";
const model = flags.model || config.defaultImageModel || "qwen-image-2.0";
const useSync = isSyncModel(model);
const defaultSize = useSync ? "1:1" : "1:1";
const sizeInput = (flags.size as string) || defaultSize;
const sizeInput = flags.size || defaultSize;
const size = resolveImageSize(sizeInput, useSync);
const n = (flags.n as number) ?? 1;
const n = flags.n ?? 1;
const concurrent = getConcurrency(flags);
const promptExtend = resolveBooleanFlag(
@@ -111,15 +129,15 @@ export default defineCommand({
const body: DashScopeImageRequest = {
model,
input: {
messages: [{ role: "user", content: [{ text: prompt! }] }],
messages: [{ role: "user", content: [{ text: prompt }] }],
},
parameters: {
size,
n,
seed: flags.seed as number | undefined,
seed: flags.seed,
prompt_extend: promptExtend,
watermark,
negative_prompt: (flags.negativePrompt as string) || undefined,
negative_prompt: flags.negativePrompt || undefined,
},
};
@@ -148,7 +166,7 @@ async function handleSyncMode(
config: Config,
_model: string,
body: DashScopeImageRequest,
flags: GlobalFlags,
flags: GenerateFlags,
format: string,
concurrent: number,
): Promise<void> {
@@ -177,7 +195,7 @@ async function handleAsyncMode(
config: Config,
_model: string,
body: DashScopeImageRequest,
flags: GlobalFlags,
flags: GenerateFlags,
format: string,
concurrent: number,
): Promise<void> {
@@ -198,7 +216,7 @@ async function handleAsyncMode(
}
// Poll all tasks concurrently
const pollInterval = (flags.pollInterval as number) ?? 3;
const pollInterval = flags.pollInterval ?? 3;
const pollPromises = taskIds.map((taskId) => {
const pollUrl = taskEndpoint(config.baseUrl, taskId);
@@ -253,19 +271,19 @@ async function handleAsyncMode(
async function saveImages(
imageUrls: string[],
flags: GlobalFlags,
flags: GenerateFlags,
config: Config,
format: string,
taskId?: string,
taskIds?: string[],
): Promise<void> {
const outDir = resolveOutputDir(config, {
flagDir: flags.outDir as string | undefined,
flagDir: flags.outDir,
subDir: flags.outDir ? undefined : "images",
});
const promptText = (flags.prompt as string) || "";
const prefix = (flags.outPrefix as string) || generateFilename("image", promptText);
const promptText = flags.prompt || "";
const prefix = flags.outPrefix || generateFilename("image", promptText);
// Parallel download all images
const items =
@@ -8,7 +8,8 @@ import {
resolveCredential,
trackingHeaders,
type Config,
type GlobalFlags,
type Flags,
type FlagsDef,
type KnowledgeRetrieveRequest,
type KnowledgeRetrieveResponse,
type DashScopeKnowledgeRetrieveRequest,
@@ -21,55 +22,74 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
const BAILIAN_HOST = "bailian.cn-beijing.aliyuncs.com";
const RETRIEVE_FLAGS = {
indexId: {
type: "string",
valueHint: "<id>",
description: "Knowledge base index ID (required)",
required: true,
},
query: {
type: "string",
valueHint: "<text>",
description: "Search query (required)",
required: true,
},
denseSimilarityTopK: {
type: "number",
valueHint: "<n>",
description: "Dense retrieval top K",
},
sparseSimilarityTopK: {
type: "number",
valueHint: "<n>",
description: "Sparse retrieval top K",
},
rerank: { type: "switch", description: "Enable reranking" },
rerankTopN: { type: "number", valueHint: "<n>", description: "Rerank top N results" },
rerankModel: {
type: "string",
valueHint: "<name>",
description: "Rerank model, e.g. qwen3-rerank-hybrid",
},
rerankMode: {
type: "string",
valueHint: "<mode>",
description: "Rerank mode: qa, similar, or custom",
},
rerankInstruct: {
type: "string",
valueHint: "<text>",
description: "Custom rerank instruction, when mode=custom",
},
topK: {
type: "number",
valueHint: "<n>",
description: "Number of results (deprecated, use --rerank-top-n)",
},
workspaceId: {
type: "string",
valueHint: "<id>",
description: "Bailian workspace ID (only needed for deprecated AK/SK auth)",
},
accessKeyId: {
type: "string",
valueHint: "<key>",
description: "Deprecated: use global --api-key instead",
},
accessKeySecret: {
type: "string",
valueHint: "<key>",
description: "Deprecated: use global --api-key instead",
},
} satisfies FlagsDef;
type RetrieveFlags = Flags<typeof RETRIEVE_FLAGS>;
export default defineCommand({
description: "Retrieve from a Bailian knowledge base",
auth: "apiKey",
usageArgs: "--index-id <id> --query <text> [flags]",
options: [
{ flag: "--index-id <id>", description: "Knowledge base index ID (required)", required: true },
{ flag: "--query <text>", description: "Search query (required)", required: true },
{
flag: "--dense-similarity-top-k <n>",
description: "Dense retrieval top K",
type: "number",
},
{
flag: "--sparse-similarity-top-k <n>",
description: "Sparse retrieval top K",
type: "number",
},
{ flag: "--rerank", description: "Enable reranking" },
{ flag: "--rerank-top-n <n>", description: "Rerank top N results", type: "number" },
{
flag: "--rerank-model <name>",
description: "Rerank model, e.g. qwen3-rerank-hybrid",
},
{
flag: "--rerank-mode <mode>",
description: "Rerank mode: qa, similar, or custom",
},
{
flag: "--rerank-instruct <text>",
description: "Custom rerank instruction, when mode=custom",
},
{
flag: "--top-k <n>",
description: "Number of results (deprecated, use --rerank-top-n)",
type: "number",
},
{
flag: "--workspace-id <id>",
description: "Bailian workspace ID (only needed for deprecated AK/SK auth)",
},
{
flag: "--access-key-id <key>",
description: "Deprecated: use global --api-key instead",
},
{
flag: "--access-key-secret <key>",
description: "Deprecated: use global --api-key instead",
},
],
flags: RETRIEVE_FLAGS,
notes: [
"Authentication: pass `--api-key <key>`. AK/SK auth is deprecated and will be removed in a future version.",
"`--workspace-id` is NOT required when using --api-key.",
@@ -78,9 +98,9 @@ export default defineCommand({
'--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;
const query = flags.query as string;
async run(config, flags) {
const indexId = flags.indexId;
const query = flags.query;
const format = detectOutputFormat(config.output);
@@ -113,7 +133,7 @@ export default defineCommand({
async function runWithApiKey(
config: Config,
flags: GlobalFlags,
flags: RetrieveFlags,
indexId: string,
query: string,
format: OutputFormat,
@@ -130,18 +150,18 @@ async function runWithApiKey(
};
if (flags.denseSimilarityTopK !== undefined)
body.dense_similarity_top_k = flags.denseSimilarityTopK as number;
body.dense_similarity_top_k = flags.denseSimilarityTopK;
if (flags.sparseSimilarityTopK !== undefined)
body.sparse_similarity_top_k = flags.sparseSimilarityTopK as number;
body.sparse_similarity_top_k = flags.sparseSimilarityTopK;
if (flags.rerank) body.enable_reranking = true;
if (flags.rerankTopN !== undefined) body.rerank_top_n = flags.rerankTopN as number;
if (flags.rerankTopN !== undefined) body.rerank_top_n = flags.rerankTopN;
if (flags.rerankModel) {
const rerankEntry: { model_name: string; rerank_mode?: string; rerank_instruct?: string } = {
model_name: flags.rerankModel as string,
model_name: flags.rerankModel,
};
if (flags.rerankMode) rerankEntry.rerank_mode = flags.rerankMode as string;
if (flags.rerankInstruct) rerankEntry.rerank_instruct = flags.rerankInstruct as string;
if (flags.rerankMode) rerankEntry.rerank_mode = flags.rerankMode;
if (flags.rerankInstruct) rerankEntry.rerank_instruct = flags.rerankInstruct;
body.rerank = [rerankEntry];
}
@@ -170,14 +190,14 @@ async function runWithApiKey(
async function runWithAkSk(
config: Config,
flags: GlobalFlags,
flags: RetrieveFlags,
indexId: string,
query: string,
format: OutputFormat,
): Promise<void> {
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
const accessKeyId = flags.accessKeyId || config.accessKeyId;
const accessKeySecret = flags.accessKeySecret || config.accessKeySecret;
const workspaceId = flags.workspaceId || config.workspaceId;
if (!accessKeyId || !accessKeySecret) {
throw new BailianError(
@@ -211,18 +231,17 @@ async function runWithAkSk(
}
if (flags.rerank) body.EnableReranking = true;
if (flags.rerankTopN !== undefined) body.RerankTopN = flags.rerankTopN as number;
if (flags.denseSimilarityTopK !== undefined)
body.DenseSimilarityTopK = flags.denseSimilarityTopK as number;
if (flags.rerankTopN !== undefined) body.RerankTopN = flags.rerankTopN;
if (flags.denseSimilarityTopK !== undefined) body.DenseSimilarityTopK = flags.denseSimilarityTopK;
if (flags.sparseSimilarityTopK !== undefined)
body.SparseSimilarityTopK = flags.sparseSimilarityTopK as number;
body.SparseSimilarityTopK = flags.sparseSimilarityTopK;
if (flags.rerankModel) {
const rerank: { ModelName: string; RerankMode?: string; RerankInstruct?: string } = {
ModelName: flags.rerankModel as string,
ModelName: flags.rerankModel,
};
if (flags.rerankMode) rerank.RerankMode = flags.rerankMode as string;
if (flags.rerankInstruct) rerank.RerankInstruct = flags.rerankInstruct as string;
if (flags.rerankMode) rerank.RerankMode = flags.rerankMode;
if (flags.rerankInstruct) rerank.RerankInstruct = flags.rerankInstruct;
body.Rerank = [rerank];
}
+25 -25
View File
@@ -1,11 +1,4 @@
import {
defineCommand,
McpClient,
bailianMcpUrl,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { defineCommand, McpClient, bailianMcpUrl, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ensureApiKey } from "bailian-cli-runtime";
@@ -32,35 +25,42 @@ export default defineCommand({
description: "Call a tool on an MCP server (tools/call)",
auth: "apiKey",
usageArgs: "--target <server.tool> [--arg k=v ...] [--json '{...}'] [--url <url>]",
options: [
{
flag: "--target <server.tool>",
flags: {
target: {
type: "string",
valueHint: "<server.tool>",
description:
"Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection",
required: true,
},
{
flag: "--arg <kv>",
description: "Tool argument (repeatable). Values parsed as JSON if possible, else string.",
arg: {
type: "array",
valueHint: "<kv>",
description: "Tool argument (repeatable). Values parsed as JSON if possible, else string.",
},
{
flag: "--json <obj>",
json: {
type: "string",
valueHint: "<obj>",
description: "Full arguments object as JSON; merged with --arg (arg wins).",
},
{
flag: "--query <text>",
query: {
type: "string",
valueHint: "<text>",
description: "Shortcut for --arg query=<text> (mirrors many DashScope MCP tools).",
},
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
],
url: {
type: "string",
valueHint: "<url>",
description: "Override the MCP endpoint URL (for non-Bailian servers)",
},
},
exampleArgs: [
'--target market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"',
'--target market-cmapi00073529.FinQuery --json \'{"q":"Guizhou Maotai","limit":5}\'',
"--target market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10",
],
async run(config: Config, flags: GlobalFlags) {
const target = flags.target as string;
async run(config, flags) {
const target = flags.target;
const dot = target.indexOf(".");
if (dot <= 0 || dot === target.length - 1) {
@@ -73,7 +73,7 @@ export default defineCommand({
let toolArgs: Record<string, unknown> = {};
if (flags.json) {
try {
const parsed = JSON.parse(flags.json as string);
const parsed = JSON.parse(flags.json);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
process.stderr.write("Error: --json must decode to an object.\n");
process.exit(1);
@@ -84,10 +84,10 @@ export default defineCommand({
process.exit(1);
}
}
Object.assign(toolArgs, parseArgFlags((flags.arg as string[] | undefined) ?? []));
Object.assign(toolArgs, parseArgFlags(flags.arg ?? []));
if (flags.query !== undefined) toolArgs.query = flags.query;
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, serverCode);
const url = flags.url || bailianMcpUrl(config.baseUrl, serverCode);
const format = detectOutputFormat(config.output);
if (config.dryRun) {
+22 -22
View File
@@ -6,8 +6,6 @@ import {
detectOutputFormat,
BailianError,
ExitCode,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
@@ -28,31 +26,33 @@ export default defineCommand({
description: "List MCP servers activated under your Bailian account",
auth: "console",
usageArgs: "[flags]",
options: [
{ flag: "--name <text>", description: "Filter by server name (substring match)" },
{
flag: "--type <type>",
flags: {
name: {
type: "string",
valueHint: "<text>",
description: "Filter by server name (substring match)",
},
type: {
type: "string",
valueHint: "<type>",
description: "Server type: OFFICIAL | PRIVATE (default: OFFICIAL)",
},
{ flag: "--page <n>", description: "Page number (default: 1)", type: "number" },
{ flag: "--page-size <n>", description: "Results per page (default: 30)", type: "number" },
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
pageSize: { type: "number", valueHint: "<n>", description: "Results per page (default: 30)" },
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: ["", "--name finance", "--output json"],
async run(config: Config, flags: GlobalFlags) {
const serverName = (flags.name as string) || "";
const type = (flags.type as string) || "OFFICIAL";
const pageNo = (flags.page as number) || 1;
const pageSize = (flags.pageSize as number) || 30;
async run(config, flags) {
const serverName = flags.name || "";
const type = flags.type || "OFFICIAL";
const pageNo = flags.page || 1;
const pageSize = flags.pageSize || 30;
const format = detectOutputFormat(config.output);
const data = {
+14 -16
View File
@@ -1,11 +1,4 @@
import {
defineCommand,
McpClient,
bailianMcpUrl,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { defineCommand, McpClient, bailianMcpUrl, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { ensureApiKey } from "bailian-cli-runtime";
@@ -13,23 +6,28 @@ export default defineCommand({
description: "List tools exposed by an MCP server (tools/list)",
auth: "apiKey",
usageArgs: "--server <code> [--url <url>]",
options: [
{
flag: "--server <code>",
flags: {
server: {
type: "string",
valueHint: "<code>",
description: "Server code from `mcp list` (e.g. market-cmapi00073529)",
required: true,
},
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
],
url: {
type: "string",
valueHint: "<url>",
description: "Override the MCP endpoint URL (for non-Bailian servers)",
},
},
exampleArgs: [
"--server market-cmapi00073529",
"--server market-cmapi00073529 --output json",
"--server my-server --url https://example.com/mcp",
],
async run(config: Config, flags: GlobalFlags) {
const code = flags.server as string;
async run(config, flags) {
const code = flags.server;
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, code);
const url = flags.url || bailianMcpUrl(config.baseUrl, code);
const format = detectOutputFormat(config.output);
if (config.dryRun) {
+32 -19
View File
@@ -3,41 +3,54 @@ import {
requestJson,
memoryAddEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type FlagsDef,
type Flags,
type MemoryAddRequest,
type MemoryAddResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const ADD_FLAGS = {
userId: { type: "string", valueHint: "<id>", description: "User ID (required)", required: true },
messages: {
type: "string",
valueHint: "<json>",
description: 'Messages JSON array: [{"role":"user","content":"..."},...]',
},
content: { type: "string", valueHint: "<text>", description: "Custom content text to memorize" },
profileSchema: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID for user profiling",
},
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: "Memory library ID (isolate memory space)",
},
} satisfies FlagsDef;
type AddFlags = Flags<typeof ADD_FLAGS>;
export default defineCommand({
description: "Add memory from messages or custom content",
auth: "apiKey",
usageArgs: "--user-id <id> [--messages <json>] [--content <text>] [flags]",
options: [
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{
flag: "--messages <json>",
description: 'Messages JSON array: [{"role":"user","content":"..."},...]',
},
{ flag: "--content <text>", description: "Custom content text to memorize" },
{ flag: "--profile-schema <id>", description: "Profile schema ID for user profiling" },
{ flag: "--memory-library-id <id>", description: "Memory library ID (isolate memory space)" },
],
flags: ADD_FLAGS,
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',
],
validate: (f) => (!f.messages && !f.content ? "Provide --messages or --content." : undefined),
async run(config: Config, flags: GlobalFlags) {
const userId = flags.userId as string;
validate: (f: AddFlags) =>
!f.messages && !f.content ? "Provide --messages or --content." : undefined,
async run(config, flags) {
const userId = flags.userId;
const body: MemoryAddRequest = { user_id: userId };
if (flags.messages) {
try {
body.messages = JSON.parse(flags.messages as string);
body.messages = JSON.parse(flags.messages);
} catch {
process.stderr.write("Error: --messages must be valid JSON array\n");
process.exit(1);
@@ -45,11 +58,11 @@ export default defineCommand({
}
if (flags.content) {
body.custom_content = flags.content as string;
body.custom_content = flags.content;
}
if (flags.profileSchema) body.profile_schema = flags.profileSchema as string;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
if (flags.profileSchema) body.profile_schema = flags.profileSchema;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(config.output);
+23 -11
View File
@@ -3,8 +3,6 @@ import {
requestJson,
memoryNodeEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
@@ -12,19 +10,33 @@ export default defineCommand({
description: "Delete a memory node",
auth: "apiKey",
usageArgs: "--node-id <id> --user-id <id>",
options: [
{ flag: "--node-id <id>", description: "Memory node ID (required)", required: true },
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{ flag: "--memory-library-id <id>", description: "Memory library ID (non-default library)" },
],
flags: {
nodeId: {
type: "string",
valueHint: "<id>",
description: "Memory node ID (required)",
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: "User ID (required)",
required: true,
},
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: "Memory library ID (non-default library)",
},
},
exampleArgs: ["--node-id node_xxx --user-id user1"],
async run(config: Config, flags: GlobalFlags) {
const nodeId = flags.nodeId as string;
const userId = flags.userId as string;
async run(config, flags) {
const nodeId = flags.nodeId;
const userId = flags.userId;
const format = detectOutputFormat(config.output);
const params = new URLSearchParams({ user_id: userId });
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId as string);
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
const url = `${memoryNodeEndpoint(config.baseUrl, nodeId)}?${params.toString()}`;
if (config.dryRun) {
+20 -13
View File
@@ -3,8 +3,6 @@ import {
requestJson,
memoryListEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type MemoryNodeListResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
@@ -13,22 +11,31 @@ export default defineCommand({
description: "List memory nodes for a user",
auth: "apiKey",
usageArgs: "--user-id <id> [flags]",
options: [
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{ flag: "--page-size <n>", description: "Results per page (default: 10)", type: "number" },
{ flag: "--page <n>", description: "Page number (default: 1)", type: "number" },
{ flag: "--memory-library-id <id>", description: "Memory library ID" },
],
flags: {
userId: {
type: "string",
valueHint: "<id>",
description: "User ID (required)",
required: true,
},
pageSize: {
type: "number",
valueHint: "<n>",
description: "Results per page (default: 10)",
},
page: { type: "number", valueHint: "<n>", description: "Page number (default: 1)" },
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
},
exampleArgs: ["--user-id user1", "--user-id user1 --page-size 20 --page 2"],
async run(config: Config, flags: GlobalFlags) {
const userId = flags.userId as string;
async run(config, flags) {
const userId = flags.userId;
const format = detectOutputFormat(config.output);
const params = new URLSearchParams();
params.set("user_id", userId);
if (flags.pageSize !== undefined) params.set("page_size", String(flags.pageSize as number));
if (flags.page !== undefined) params.set("page_num", String(flags.page as number));
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId as string);
if (flags.pageSize !== undefined) params.set("page_size", String(flags.pageSize));
if (flags.page !== undefined) params.set("page_num", String(flags.page));
if (flags.memoryLibraryId) params.set("memory_library_id", flags.memoryLibraryId);
const url = `${memoryListEndpoint(config.baseUrl)}?${params.toString()}`;
@@ -3,8 +3,6 @@ import {
requestJson,
profileSchemaEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type ProfileSchemaCreateRequest,
type ProfileSchemaCreateResponse,
} from "bailian-cli-core";
@@ -14,21 +12,27 @@ export default defineCommand({
description: "Create a user profile schema for memory profiling",
auth: "apiKey",
usageArgs: "--name <name> --attributes <json> [flags]",
options: [
{ flag: "--name <name>", description: "Schema name (required)", required: true },
{ flag: "--description <text>", description: "Schema description" },
{
flag: "--attributes <json>",
flags: {
name: {
type: "string",
valueHint: "<name>",
description: "Schema name (required)",
required: true,
},
description: { type: "string", valueHint: "<text>", description: "Schema description" },
attributes: {
type: "string",
valueHint: "<json>",
description: 'Attributes JSON array: [{"name":"age","description":"age"}]',
required: true,
},
],
},
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;
const attrStr = flags.attributes as string;
async run(config, flags) {
const name = flags.name;
const attrStr = flags.attributes;
let attributes;
try {
@@ -39,7 +43,7 @@ export default defineCommand({
}
const body: ProfileSchemaCreateRequest = { name, attributes };
if (flags.description) body.description = flags.description as string;
if (flags.description) body.description = flags.description;
const format = detectOutputFormat(config.output);
@@ -3,8 +3,6 @@ import {
requestJson,
userProfileEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type UserProfileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
@@ -13,14 +11,24 @@ export default defineCommand({
description: "Get user profile by schema ID and user ID",
auth: "apiKey",
usageArgs: "--schema-id <id> --user-id <id>",
options: [
{ flag: "--schema-id <id>", description: "Profile schema ID (required)", required: true },
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
],
flags: {
schemaId: {
type: "string",
valueHint: "<id>",
description: "Profile schema ID (required)",
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: "User ID (required)",
required: true,
},
},
exampleArgs: ["--schema-id schema_xxx --user-id user1"],
async run(config: Config, flags: GlobalFlags) {
const schemaId = flags.schemaId as string;
const userId = flags.userId as string;
async run(config, flags) {
const schemaId = flags.schemaId;
const userId = flags.userId;
const format = detectOutputFormat(config.output);
const params = new URLSearchParams({ user_id: userId });
+28 -20
View File
@@ -3,43 +3,51 @@ import {
requestJson,
memorySearchEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type FlagsDef,
type Flags,
type MemorySearchRequest,
type MemorySearchResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
const SEARCH_FLAGS = {
userId: { type: "string", valueHint: "<id>", description: "User ID (required)", required: true },
query: { type: "string", valueHint: "<text>", description: "Search query text" },
messages: {
type: "string",
valueHint: "<json>",
description: "Messages JSON array for context-based search",
},
topK: {
type: "number",
valueHint: "<n>",
description: "Number of results to return (default: 10)",
},
memoryLibraryId: { type: "string", valueHint: "<id>", description: "Memory library ID" },
} satisfies FlagsDef;
type SearchFlags = Flags<typeof SEARCH_FLAGS>;
export default defineCommand({
description: "Search memory nodes by query or messages",
auth: "apiKey",
usageArgs: "--user-id <id> [--query <text>] [flags]",
options: [
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{ flag: "--query <text>", description: "Search query text" },
{ flag: "--messages <json>", description: "Messages JSON array for context-based search" },
{
flag: "--top-k <n>",
description: "Number of results to return (default: 10)",
type: "number",
},
{ flag: "--memory-library-id <id>", description: "Memory library ID" },
],
flags: SEARCH_FLAGS,
exampleArgs: [
'--user-id user1 --query "programming preferences"',
'--user-id user1 --messages \'[{"role":"user","content":"recommend a book"}]\' --top-k 5',
],
validate: (f) => (!f.query && !f.messages ? "Provide --query or --messages." : undefined),
async run(config: Config, flags: GlobalFlags) {
const userId = flags.userId as string;
validate: (f: SearchFlags) =>
!f.query && !f.messages ? "Provide --query or --messages." : undefined,
async run(config, flags) {
const userId = flags.userId;
const body: MemorySearchRequest = { user_id: userId };
if (flags.query) body.query = flags.query as string;
if (flags.query) body.query = flags.query;
if (flags.messages) {
try {
body.messages = JSON.parse(flags.messages as string);
body.messages = JSON.parse(flags.messages);
} catch {
process.stderr.write("Error: --messages must be valid JSON array\n");
process.exit(1);
@@ -51,8 +59,8 @@ export default defineCommand({
body.messages = [{ role: "user", content: body.query }];
}
if (flags.topK !== undefined) body.top_k = flags.topK as number;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
if (flags.topK !== undefined) body.top_k = flags.topK;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(config.output);
+27 -14
View File
@@ -3,8 +3,6 @@ import {
requestJson,
memoryNodeEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type MemoryNodeUpdateRequest,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
@@ -13,27 +11,42 @@ export default defineCommand({
description: "Update a memory node content",
auth: "apiKey",
usageArgs: "--node-id <id> --user-id <id> --content <text>",
options: [
{ flag: "--node-id <id>", description: "Memory node ID (required)", required: true },
{ flag: "--user-id <id>", description: "User ID (required)", required: true },
{
flag: "--content <text>",
flags: {
nodeId: {
type: "string",
valueHint: "<id>",
description: "Memory node ID (required)",
required: true,
},
userId: {
type: "string",
valueHint: "<id>",
description: "User ID (required)",
required: true,
},
content: {
type: "string",
valueHint: "<text>",
description: "New content for the memory node (required)",
required: true,
},
{ flag: "--memory-library-id <id>", description: "Memory library ID (non-default library)" },
],
memoryLibraryId: {
type: "string",
valueHint: "<id>",
description: "Memory library ID (non-default library)",
},
},
exampleArgs: ['--node-id node_xxx --user-id user1 --content "updated memory content"'],
async run(config: Config, flags: GlobalFlags) {
const nodeId = flags.nodeId as string;
const userId = flags.userId as string;
const content = flags.content as string;
async run(config, flags) {
const nodeId = flags.nodeId;
const userId = flags.userId;
const content = flags.content;
const body: MemoryNodeUpdateRequest = {
user_id: userId,
custom_content: content,
};
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId as string;
if (flags.memoryLibraryId) body.memory_library_id = flags.memoryLibraryId;
const format = detectOutputFormat(config.output);
+54 -35
View File
@@ -8,8 +8,6 @@ import {
detectOutputFormat,
BailianError,
ExitCode,
type Config,
type GlobalFlags,
type ChatMessage,
type ChatMessageContent,
type ChatRequest,
@@ -86,36 +84,57 @@ export default defineCommand({
description: "Multimodal chat with text + audio output (Qwen-Omni)",
auth: "apiKey",
usageArgs: "--message <text> [flags]",
options: [
{
flag: "--message <text>",
flags: {
message: {
type: "array",
valueHint: "<text>",
description: "Message text (repeatable, prefix role: to set role)",
required: true,
type: "array",
},
{ flag: "--model <model>", description: "Model ID (default: qwen3.5-omni-plus)" },
{ flag: "--system <text>", description: "System prompt" },
{ flag: "--image <url>", description: "Image URL or local file (repeatable)", type: "array" },
{
flag: "--audio <url>",
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: qwen3.5-omni-plus)",
},
system: { type: "string", valueHint: "<text>", description: "System prompt" },
image: {
type: "array",
valueHint: "<url>",
description: "Image URL or local file (repeatable)",
},
audio: {
type: "array",
valueHint: "<url>",
description: "Audio URL or local file (.wav/.mp3/.amr/.aac/.m4a/.ogg/.3gp/.3gpp)",
type: "array",
},
{
flag: "--video <url>",
video: {
type: "array",
valueHint: "<url>",
description: "Video file URL / local path, or comma-separated frame URLs",
type: "array",
},
{
flag: "--voice <voice>",
voice: {
type: "string",
valueHint: "<voice>",
description: `Output voice (default: Cherry). Options: ${OMNI_VOICES.join(", ")}`,
},
{ flag: "--audio-format <fmt>", description: "Audio output format (default: wav)" },
{ flag: "--audio-out <path>", description: "Save audio to file (default: auto-generate)" },
{ flag: "--text-only", description: "Output text only, no audio generation" },
{ flag: "--max-tokens <n>", description: "Maximum tokens to generate", type: "number" },
{ flag: "--temperature <n>", description: "Sampling temperature (0.0, 2.0]", type: "number" },
],
audioFormat: {
type: "string",
valueHint: "<fmt>",
description: "Audio output format (default: wav)",
},
audioOut: {
type: "string",
valueHint: "<path>",
description: "Save audio to file (default: auto-generate)",
},
textOnly: { type: "switch", description: "Output text only, no audio generation" },
maxTokens: { type: "number", valueHint: "<n>", description: "Maximum tokens to generate" },
temperature: {
type: "number",
valueHint: "<n>",
description: "Sampling temperature (0.0, 2.0]",
},
},
exampleArgs: [
'--message "Hello, who are you?"',
'--message "Describe this image" --image ./photo.jpg',
@@ -126,20 +145,20 @@ export default defineCommand({
'--message "Hello" --text-only --output json',
'--message "Read this passage aloud" --audio-out greeting.wav',
],
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
// --- Parse messages ---
const userMessages = flags.message as string[];
const userMessages = flags.message;
const model = (flags.model as string) || config.defaultOmniModel || "qwen3.5-omni-plus";
const voice = (flags.voice as string) || "Cherry";
const audioFormat = (flags.audioFormat as string) || "wav";
const model = flags.model || config.defaultOmniModel || "qwen3.5-omni-plus";
const voice = flags.voice || "Cherry";
const audioFormat = flags.audioFormat || "wav";
const textOnly = flags.textOnly === true;
const format = detectOutputFormat(config.output);
// --- Build messages array ---
const allMessages: ChatMessage[] = [];
if (flags.system) {
allMessages.push({ role: "system", content: flags.system as string });
allMessages.push({ role: "system", content: flags.system });
}
// Build multimodal content for user messages
@@ -161,9 +180,9 @@ export default defineCommand({
}
// Attach multimodal inputs to the last user message
const rawImageUrls = (flags.image as string[] | undefined) || [];
const rawAudioUrls = (flags.audio as string[] | undefined) || [];
const rawVideoUrls = (flags.video as string[] | undefined) || [];
const rawImageUrls = flags.image || [];
const rawAudioUrls = flags.audio || [];
const rawVideoUrls = flags.video || [];
// Auto-upload local files
const imageUrls: string[] = [];
@@ -258,8 +277,8 @@ export default defineCommand({
body.audio = { voice, format: audioFormat };
}
if (flags.maxTokens !== undefined) body.max_tokens = flags.maxTokens as number;
if (flags.temperature !== undefined) body.temperature = flags.temperature as number;
if (flags.maxTokens !== undefined) body.max_tokens = flags.maxTokens;
if (flags.temperature !== undefined) body.temperature = flags.temperature;
if (config.dryRun) {
emitResult({ request: body }, format);
@@ -322,7 +341,7 @@ export default defineCommand({
const wavHeader = buildWavHeader(pcmBuffer.length);
const wavBuffer = Buffer.concat([wavHeader, pcmBuffer]);
let destPath = flags.audioOut as string | undefined;
let destPath = flags.audioOut;
if (!destPath) {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { join } = await import("path");
+39 -27
View File
@@ -1,32 +1,44 @@
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { defineCommand, type Config, type GlobalFlags } from "bailian-cli-core";
import { defineCommand, type FlagsDef, type Flags } from "bailian-cli-core";
import { emitResult } 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";
const RUN_FLAGS = {
file: {
type: "string",
valueHint: "<path>",
description: "Pipeline definition file (YAML/JSON)",
required: true,
},
input: { type: "string", valueHint: "<json>", description: "Runtime input as inline JSON" },
inputFile: {
type: "string",
valueHint: "<path>",
description: "Runtime input from a JSON file",
},
concurrency: {
type: "number",
valueHint: "<n>",
description: "Max parallel steps (default: 1)",
},
events: { type: "string", valueHint: "<format>", description: "Emit lifecycle events: jsonl" },
timeout: {
type: "number",
valueHint: "<seconds>",
description: "Default step timeout in seconds",
},
} satisfies FlagsDef;
type RunFlags = Flags<typeof RUN_FLAGS>;
export default defineCommand({
description: "Run a pipeline workflow definition",
auth: "none",
usageArgs: "--file <path> [flags]",
options: [
{ flag: "--file <path>", description: "Pipeline definition file (YAML/JSON)", required: true },
{ flag: "--input <json>", description: "Runtime input as inline JSON" },
{ flag: "--input-file <path>", description: "Runtime input from a JSON file" },
{
flag: "--concurrency <n>",
description: "Max parallel steps (default: 1)",
type: "number",
},
{ flag: "--events <format>", description: "Emit lifecycle events: jsonl" },
{
flag: "--timeout <seconds>",
description: "Default step timeout in seconds",
type: "number",
},
],
flags: RUN_FLAGS,
exampleArgs: [
'--file workflow.yaml --input \'{"brief":"hello"}\'',
"--file workflow.json --input-file inputs.json --concurrency 3",
@@ -34,12 +46,12 @@ export default defineCommand({
"--file workflow.json --events jsonl",
"--file workflow.yaml --output json",
],
async run(config: Config, flags: GlobalFlags) {
const file = flags.file as string;
async run(config, flags) {
const file = flags.file;
initPipelineSteps();
const eventsFormat = flags.events as string | undefined;
const eventsFormat = flags.events;
if (eventsFormat !== undefined && eventsFormat !== "jsonl") {
process.stderr.write(
`Error: unsupported --events format: ${eventsFormat}. Supported: jsonl\n`,
@@ -54,10 +66,10 @@ export default defineCommand({
if (eventsFormat === "jsonl") {
for await (const event of streamPipelineEvents(pipeline, runtimeInput, {
concurrency: flags.concurrency as number | undefined,
concurrency: flags.concurrency,
basePath,
dryRun: flags.dryRun,
timeoutSeconds: flags.timeout as number | undefined,
timeoutSeconds: flags.timeout,
})) {
process.stdout.write(JSON.stringify(event) + "\n");
}
@@ -65,10 +77,10 @@ export default defineCommand({
}
const report = await executePipeline(pipeline, runtimeInput, {
concurrency: flags.concurrency as number | undefined,
concurrency: flags.concurrency,
basePath,
dryRun: flags.dryRun,
timeoutSeconds: flags.timeout as number | undefined,
timeoutSeconds: flags.timeout,
onEvent: flags.verbose ? logEvent : undefined,
});
@@ -82,9 +94,9 @@ export default defineCommand({
},
});
async function resolveRuntimeInput(flags: GlobalFlags): Promise<Record<string, unknown>> {
const inputJson = flags.input as string | undefined;
const inputFile = flags.inputFile as string | undefined;
async function resolveRuntimeInput(flags: RunFlags): Promise<Record<string, unknown>> {
const inputJson = flags.input;
const inputFile = flags.inputFile;
if (inputJson && inputFile) {
process.stderr.write("Error: use --input or --input-file, not both\n");
process.exit(2);
@@ -1,5 +1,5 @@
import { resolve } from "node:path";
import { defineCommand, type Config, type GlobalFlags } from "bailian-cli-core";
import { defineCommand } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { initPipelineSteps } from "bailian-cli-runtime";
import { collectPipelineIssues, collectPipelineHints } from "bailian-cli-runtime";
@@ -9,12 +9,17 @@ export default defineCommand({
description: "Validate a pipeline definition without executing",
auth: "none",
usageArgs: "--file <path>",
options: [
{ flag: "--file <path>", description: "Pipeline definition file (YAML/JSON)", required: true },
],
flags: {
file: {
type: "string",
valueHint: "<path>",
description: "Pipeline definition file (YAML/JSON)",
required: true,
},
},
exampleArgs: ["--file workflow.yaml", "--file workflow.json --output json"],
async run(config: Config, flags: GlobalFlags) {
const file = flags.file as string;
async run(config, flags) {
const file = flags.file;
initPipelineSteps();
+15 -17
View File
@@ -5,7 +5,6 @@ import {
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
@@ -238,26 +237,25 @@ export default defineCommand({
description: "Check current usage against rate limits",
auth: "console",
usageArgs: "[--model <model>] [flags]",
options: [
{
flag: "--model <model>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model name(s), comma-separated",
},
{
flag: "--period <minutes>",
period: {
type: "string",
valueHint: "<minutes>",
description: "Query usage for the last N minutes (default: 2)",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: [
"",
"--model qwen3.6-plus",
@@ -265,8 +263,8 @@ export default defineCommand({
"--model qwen3.6-plus,qwen-turbo",
"--output json",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
async run(config, flags) {
const modelFlag = flags.model || undefined;
const rawPeriod = Number(flags.period) || 2;
if (rawPeriod < 1) {
process.stderr.write("Error: --period must be at least 1 minute.\n");
+18 -20
View File
@@ -4,8 +4,6 @@ import {
resolveConsoleGatewayCredential,
detectOutputFormat,
BailianError,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
@@ -94,35 +92,35 @@ export default defineCommand({
description: "View quota change history",
auth: "console",
usageArgs: "[flags]",
options: [
{
flag: "--page <n>",
flags: {
page: {
type: "string",
valueHint: "<n>",
description: "Page number (default: 1)",
},
{
flag: "--page-size <n>",
pageSize: {
type: "string",
valueHint: "<n>",
description: "Page size (default: 10)",
},
{
flag: "--model <model>",
model: {
type: "string",
valueHint: "<model>",
description: "Filter by model name",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: ["", "--page 2", "--page-size 20", "--model qwen-turbo", "--output json"],
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
const page = Number(flags.page) || 1;
const pageSize = Number(flags.pageSize) || 10;
const modelFilter = (flags.model as string) || undefined;
const modelFilter = flags.model || undefined;
const format = detectOutputFormat(config.output);
const requestData = {
+14 -17
View File
@@ -4,7 +4,6 @@ import {
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
@@ -153,26 +152,24 @@ export default defineCommand({
description: "View model RPM/TPM rate limits",
auth: "console",
usageArgs: "[--model <model>] [flags]",
options: [
{
flag: "--model <model>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model name(s), comma-separated",
},
{
flag: "--all",
all: {
type: "switch",
description: "Show all models, not just self-service ones",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: [
"",
"--model qwen3.6-plus",
@@ -180,8 +177,8 @@ export default defineCommand({
"--all",
"--output json",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
async run(config, flags) {
const modelFlag = flags.model || undefined;
const showAll = Boolean(flags.all);
const format = detectOutputFormat(config.output);
+16 -21
View File
@@ -5,7 +5,6 @@ import {
detectOutputFormat,
BailianError,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
@@ -81,39 +80,35 @@ export default defineCommand({
description: "Request a temporary quota increase",
auth: "console",
usageArgs: "--model <model> --tpm <value> [flags]",
options: [
{
flag: "--model <model>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model name (required)",
required: true,
},
{
flag: "--tpm <value>",
tpm: {
type: "string",
valueHint: "<value>",
description: "Target TPM value (required)",
required: true,
},
{
flag: "--yes",
description: "Skip downgrade confirmation",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
yes: { type: "switch", description: "Skip downgrade confirmation" },
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
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;
async run(config, flags) {
const modelName = flags.model;
if (!modelName) {
process.stderr.write("Error: --model is required.\n");
process.exit(1);
+16 -11
View File
@@ -2,22 +2,27 @@ import {
defineCommand,
detectOutputFormat,
mcpWebSearchEndpoint,
type Config,
type GlobalFlags,
McpClient,
type FlagsDef,
} from "bailian-cli-core";
import { createSpinner } from "bailian-cli-runtime";
import { emitResult } from "bailian-cli-runtime";
const WEB_SEARCH_FLAGS = {
query: { type: "string", valueHint: "<text>", description: "Search query text" },
count: {
type: "number",
valueHint: "<n>",
description: "Number of search results (default: 10)",
},
listTools: { type: "switch", description: "List available MCP tools and exit" },
} satisfies FlagsDef;
export default defineCommand({
description: "Search the web using DashScope MCP WebSearch service",
auth: "apiKey",
usageArgs: "--query <text> [flags]",
options: [
{ flag: "--query <text>", description: "Search query text" },
{ flag: "--count <n>", description: "Number of search results (default: 10)", type: "number" },
{ flag: "--list-tools", description: "List available MCP tools and exit" },
],
flags: WEB_SEARCH_FLAGS,
exampleArgs: [
'--query "Alibaba Cloud Bailian latest features"',
'--query "TypeScript 5.9 new features" --count 5',
@@ -25,7 +30,7 @@ export default defineCommand({
"--list-tools",
],
validate: (f) => (!f.listTools && !f.query ? "Missing required flag: --query" : undefined),
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
const mcpUrl = mcpWebSearchEndpoint(config.baseUrl);
const format = detectOutputFormat(config.output);
@@ -45,7 +50,7 @@ export default defineCommand({
}
// --- Search mode ---
const query = flags.query as string;
const query = flags.query;
if (config.dryRun) {
emitResult(
@@ -55,7 +60,7 @@ export default defineCommand({
tool: "bailian_web_search",
arguments: {
query: query!,
count: (flags.count as number) || undefined,
count: flags.count || undefined,
},
},
format,
@@ -76,7 +81,7 @@ export default defineCommand({
// Build tool arguments
const toolArgs: Record<string, unknown> = { query: query! };
if (flags.count) toolArgs.count = flags.count as number;
if (flags.count) toolArgs.count = flags.count;
// Call the search tool
const result = await client.callTool("bailian_web_search", toolArgs);
@@ -5,7 +5,6 @@ import {
ExitCode,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeASRRequest,
type DashScopeASRTaskResult,
type DashScopeAsyncResponse,
@@ -17,39 +16,52 @@ import {
requestJson,
type OutputFormat,
speechRecognizeEndpoint,
type FlagsDef,
type Flags,
} from "bailian-cli-core";
import { poll } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const RECOGNIZE_FLAGS = {
url: {
type: "array",
valueHint: "<url>",
description: "Audio file URL or local file path (repeatable, max 100)",
required: true,
},
model: { type: "string", valueHint: "<model>", description: "Model ID (default: fun-asr)" },
language: { type: "string", valueHint: "<lang>", description: "Language hint (e.g. zh, en, ja)" },
diarization: { type: "switch", description: "Enable automatic speaker diarization" },
speakerCount: {
type: "number",
valueHint: "<n>",
description: "Expected number of speakers (requires --diarization)",
},
vocabularyId: {
type: "string",
valueHint: "<id>",
description: "Hot-word vocabulary ID for improved accuracy",
},
channelId: { type: "number", valueHint: "<n>", description: "Audio channel ID (default: 0)" },
out: {
type: "string",
valueHint: "<path>",
description: "Save full transcription result to JSON file",
},
noWait: { type: "switch", description: "Return task ID immediately without polling" },
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval in seconds (default: 2)",
},
} satisfies FlagsDef;
type RecognizeFlags = Flags<typeof RECOGNIZE_FLAGS>;
export default defineCommand({
description: "Recognize speech from audio files (FunAudio-ASR)",
auth: "apiKey",
usageArgs: "--url <audio-url> [flags]",
options: [
{
flag: "--url <url>",
description: "Audio file URL or local file path (repeatable, max 100)",
required: true,
type: "array",
},
{ flag: "--model <model>", description: "Model ID (default: fun-asr)" },
{ flag: "--language <lang>", description: "Language hint (e.g. zh, en, ja)" },
{ flag: "--diarization", description: "Enable automatic speaker diarization" },
{
flag: "--speaker-count <n>",
description: "Expected number of speakers (requires --diarization)",
type: "number",
},
{ flag: "--vocabulary-id <id>", description: "Hot-word vocabulary ID for improved accuracy" },
{ flag: "--channel-id <n>", description: "Audio channel ID (default: 0)", type: "number" },
{ flag: "--out <path>", description: "Save full transcription result to JSON file" },
{ flag: "--no-wait", description: "Return task ID immediately without polling" },
{
flag: "--poll-interval <seconds>",
description: "Polling interval in seconds (default: 2)",
type: "number",
},
],
flags: RECOGNIZE_FLAGS,
exampleArgs: [
"--url https://example.com/audio.mp3",
"--url https://example.com/a.mp3 --url https://example.com/b.mp3",
@@ -59,17 +71,17 @@ export default defineCommand({
"--url https://example.com/audio.mp3 --out result.json",
"--url https://example.com/audio.mp3 --no-wait --quiet",
],
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
// Normalize --url to string[] (supports both single and repeated flags)
let rawUrls: string[] = [];
if (Array.isArray(flags.url)) {
rawUrls = flags.url as string[];
rawUrls = flags.url;
} else if (typeof flags.url === "string") {
rawUrls = [flags.url];
}
// Strict validation: --speaker-count requires --diarization
const speakerCount = flags.speakerCount as number | undefined;
const speakerCount = flags.speakerCount;
const diarization = flags.diarization === true;
if (speakerCount !== undefined && !diarization) {
throw new BailianError(
@@ -78,7 +90,7 @@ export default defineCommand({
);
}
const model = (flags.model as string) || "fun-asr";
const model = flags.model || "fun-asr";
const format = detectOutputFormat(config.output);
// Auto-upload local files in parallel
@@ -86,9 +98,9 @@ export default defineCommand({
const resolvedUrls = await Promise.all(
rawUrls.map((u) => resolveFileUrl(u, credential.token, model)),
);
const channelId = flags.channelId as number | undefined;
const language = flags.language as string | undefined;
const vocabularyId = flags.vocabularyId as string | undefined;
const channelId = flags.channelId;
const language = flags.language;
const vocabularyId = flags.vocabularyId;
const body: DashScopeASRRequest = {
model,
@@ -125,7 +137,7 @@ async function handleAsyncMode(
config: Config,
url: string,
body: DashScopeASRRequest,
flags: GlobalFlags,
flags: RecognizeFlags,
format: OutputFormat,
fileCount: number,
): Promise<void> {
@@ -146,7 +158,7 @@ async function handleAsyncMode(
}
// Poll until completion
const pollInterval = (flags.pollInterval as number) ?? 2;
const pollInterval = flags.pollInterval ?? 2;
const pollUrl = taskEndpoint(config.baseUrl, taskId);
const result = await poll<DashScopeASRTaskResult>(config, {
@@ -234,7 +246,7 @@ async function handleAsyncMode(
// Save to --out file
if (flags.out) {
const outPath = flags.out as string;
const outPath = flags.out;
const outData = allTransData.length === 1 ? allTransData[0] : allTransData;
writeFileSync(outPath, JSON.stringify(outData, null, 2) + "\n");
if (!config.quiet) {
@@ -5,7 +5,6 @@ import {
ExitCode,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeTTSRequest,
type DashScopeTTSResponse,
type DashScopeTTSStreamChunk,
@@ -17,6 +16,8 @@ import {
resolveOutputDir,
request,
DOCS_HOSTS,
type FlagsDef,
type Flags,
} from "bailian-cli-core";
const COSYVOICE_CLONE_DESIGN_DOC = `${DOCS_HOSTS.cn}/cosyvoice-clone-design-api`;
@@ -139,46 +140,81 @@ function printVoiceList(model: string): void {
process.stdout.write(`\nTotal: ${voices.length} voices\n`);
}
const SYNTHESIZE_FLAGS = {
text: {
type: "string",
valueHint: "<text>",
description: "Text to synthesize into speech (or use --text-file)",
},
textFile: {
type: "string",
valueHint: "<path>",
description: "Read text from a file instead of --text",
},
model: {
type: "string",
valueHint: "<model>",
description:
"Model ID (default: cosyvoice-v3-flash). System voices available for cosyvoice-v3-flash",
},
voice: {
type: "string",
valueHint: "<voice>",
description:
"Voice ID. Use --list-voices to see system voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID",
},
listVoices: {
type: "switch",
description: "List available system voices for the selected model and exit",
},
format: {
type: "string",
valueHint: "<format>",
description: "Audio format: mp3, pcm, wav, opus (default: mp3)",
choices: ["mp3", "pcm", "wav", "opus"] as const,
},
sampleRate: {
type: "string",
valueHint: "<rate>",
description: "Audio sample rate in Hz (e.g. 24000)",
},
volume: { type: "string", valueHint: "<volume>", description: "Volume 0-100 (default: 50)" },
rate: { type: "string", valueHint: "<rate>", description: "Speech rate 0.5-2.0 (default: 1.0)" },
pitch: {
type: "string",
valueHint: "<pitch>",
description: "Pitch multiplier 0.5-2.0 (default: 1.0)",
},
seed: {
type: "string",
valueHint: "<seed>",
description: "Random seed 0-65535 for reproducible synthesis",
},
language: {
type: "string",
valueHint: "<lang>",
description: "Language hint (e.g. zh, en, ja, ko, fr, de)",
},
instruction: {
type: "string",
valueHint: "<text>",
description: 'Natural language instruction to control speech style (e.g. "Use a gentle tone")',
},
enableSsml: { type: "switch", description: "Enable SSML markup parsing in input text" },
out: {
type: "string",
valueHint: "<path>",
description: "Save audio to file (default: auto-generate in temp dir)",
},
stream: { type: "switch", description: "Stream raw PCM audio to stdout (pipe to player)" },
} satisfies FlagsDef;
type SynthesizeFlags = Flags<typeof SYNTHESIZE_FLAGS>;
export default defineCommand({
description: "Synthesize speech from text (CosyVoice TTS)",
auth: "apiKey",
usageArgs: "--text <text> [flags]",
options: [
{ flag: "--text <text>", description: "Text to synthesize into speech (or use --text-file)" },
{ flag: "--text-file <path>", description: "Read text from a file instead of --text" },
{
flag: "--model <model>",
description:
"Model ID (default: cosyvoice-v3-flash). System voices available for cosyvoice-v3-flash",
},
{
flag: "--voice <voice>",
description:
"Voice ID. Use --list-voices to see system voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID",
},
{
flag: "--list-voices",
description: "List available system voices for the selected model and exit",
},
{ flag: "--format <format>", description: "Audio format: mp3, pcm, wav, opus (default: mp3)" },
{ flag: "--sample-rate <rate>", description: "Audio sample rate in Hz (e.g. 24000)" },
{ flag: "--volume <volume>", description: "Volume 0-100 (default: 50)" },
{ flag: "--rate <rate>", description: "Speech rate 0.5-2.0 (default: 1.0)" },
{ flag: "--pitch <pitch>", description: "Pitch multiplier 0.5-2.0 (default: 1.0)" },
{ flag: "--seed <seed>", description: "Random seed 0-65535 for reproducible synthesis" },
{ flag: "--language <lang>", description: "Language hint (e.g. zh, en, ja, ko, fr, de)" },
{
flag: "--instruction <text>",
description:
'Natural language instruction to control speech style (e.g. "Use a gentle tone")',
},
{ flag: "--enable-ssml", description: "Enable SSML markup parsing in input text" },
{
flag: "--out <path>",
description: "Save audio to file (default: auto-generate in temp dir)",
},
{ flag: "--stream", description: "Stream raw PCM audio to stdout (pipe to player)" },
],
flags: SYNTHESIZE_FLAGS,
exampleArgs: [
"--list-voices --model cosyvoice-v3-flash",
'--text "Hello, I am Qwen" --voice <voice_id>',
@@ -197,8 +233,8 @@ export default defineCommand({
if (!f.voice) return "Missing required flag: --voice";
return undefined;
},
async run(config: Config, flags: GlobalFlags) {
const model = (flags.model as string) || config.defaultSpeechModel || "cosyvoice-v3-flash";
async run(config, flags) {
const model = flags.model || config.defaultSpeechModel || "cosyvoice-v3-flash";
// --list-voices: print voice list for the model and exit
if (flags.listVoices) {
@@ -207,9 +243,9 @@ export default defineCommand({
}
// --text / --text-file presence enforced by validate; empty file content → API rejects.
let text = (flags.text as string) || "";
let text = flags.text || "";
if (!text && flags.textFile) {
const filePath = flags.textFile as string;
const filePath = flags.textFile;
try {
text = readFileSync(filePath, "utf-8").trim();
} catch {
@@ -218,9 +254,9 @@ export default defineCommand({
}
const voice = flags.voice as string;
const language = (flags.language as string) || undefined;
const instruction = (flags.instruction as string) || undefined;
const audioFormat = (flags.format as "mp3" | "pcm" | "wav" | "opus") || undefined;
const language = flags.language || undefined;
const instruction = flags.instruction || undefined;
const audioFormat = flags.format || undefined;
const sampleRate = flags.sampleRate !== undefined ? Number(flags.sampleRate) : undefined;
const volume = flags.volume !== undefined ? Number(flags.volume) : undefined;
const rate = flags.rate !== undefined ? Number(flags.rate) : undefined;
@@ -274,7 +310,7 @@ async function handleNonStreamMode(
config: Config,
url: string,
body: DashScopeTTSRequest,
flags: GlobalFlags,
flags: SynthesizeFlags,
format: OutputFormat,
): Promise<void> {
const concurrent = getConcurrency(flags);
@@ -294,7 +330,7 @@ async function handleNonStreamMode(
const destDir = resolveOutputDir(config, { subDir: "speech" });
const items = audioUrls.map((audioUrl, i) => {
let destPath = flags.out as string | undefined;
let destPath = flags.out;
if (destPath && audioUrls.length === 1) {
// Single explicit output path
} else {
@@ -340,7 +376,7 @@ async function handleStreamMode(
config: Config,
url: string,
body: DashScopeTTSRequest,
flags: GlobalFlags,
flags: SynthesizeFlags,
format: OutputFormat,
): Promise<void> {
const res = await request(config, {
@@ -354,7 +390,7 @@ async function handleStreamMode(
},
});
const outPath = flags.out as string | undefined;
const outPath = flags.out;
const writer = outPath ? createWriteStream(outPath) : null;
let lastAudioUrl: string | undefined;
+57 -50
View File
@@ -5,31 +5,73 @@ import {
chatEndpoint,
parseSSE,
detectOutputFormat,
type Config,
type GlobalFlags,
type ChatMessage,
type ChatRequest,
type ChatResponse,
type StreamChunk,
type FlagsDef,
type Flags,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { readFileSync } from "fs";
const CHAT_FLAGS = {
model: { type: "string", valueHint: "<model>", description: "Model ID (default: qwen3.7-max)" },
message: {
type: "array",
valueHint: "<text>",
description: "Message text (repeatable, prefix role: to set role); or use --messages-file",
},
messagesFile: {
type: "string",
valueHint: "<path>",
description: "JSON file with messages array (use - for stdin)",
},
system: { type: "string", valueHint: "<text>", description: "System prompt" },
maxTokens: {
type: "number",
valueHint: "<n>",
description: "Maximum tokens to generate (default: 4096)",
},
temperature: {
type: "number",
valueHint: "<n>",
description: "Sampling temperature (0.0, 2.0]",
},
topP: { type: "number", valueHint: "<n>", description: "Nucleus sampling threshold" },
stream: { type: "switch", description: "Stream response tokens (default: on in TTY)" },
tool: {
type: "array",
valueHint: "<json-or-path>",
description: "Tool definition as JSON or file path (repeatable)",
},
enableThinking: {
type: "switch",
description: "Enable thinking/reasoning mode (for qwen3/qwq models)",
},
thinkingBudget: {
type: "number",
valueHint: "<n>",
description: "Max tokens for thinking (default: 4096)",
},
} satisfies FlagsDef;
type ChatFlags = Flags<typeof CHAT_FLAGS>;
interface ParsedMessages {
system?: string;
messages: ChatMessage[];
}
function parseMessages(flags: GlobalFlags): ParsedMessages {
function parseMessages(flags: ChatFlags): ParsedMessages {
const messages: ChatMessage[] = [];
let system: string | undefined;
if (flags.system) {
system = flags.system as string;
system = flags.system;
}
if (flags.messagesFile) {
const filePath = flags.messagesFile as string;
const filePath = flags.messagesFile;
const raw =
filePath === "-" ? readFileSync("/dev/stdin", "utf-8") : readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw) as Array<{ role: string; content: string }>;
@@ -44,7 +86,7 @@ function parseMessages(flags: GlobalFlags): ParsedMessages {
if (flags.message) {
const validRoles = new Set(["system", "user", "assistant"]);
const msgs = flags.message as string[];
const msgs = flags.message;
for (const m of msgs) {
const colonIdx = m.indexOf(":");
const maybeRole = colonIdx !== -1 ? m.slice(0, colonIdx) : "";
@@ -69,41 +111,7 @@ export default defineCommand({
description: "Send a chat completion (OpenAI compatible, DashScope)",
auth: "apiKey",
usageArgs: "--message <text> [flags]",
options: [
{ flag: "--model <model>", description: "Model ID (default: qwen3.7-max)" },
{
flag: "--message <text>",
description: "Message text (repeatable, prefix role: to set role); or use --messages-file",
type: "array",
},
{
flag: "--messages-file <path>",
description: "JSON file with messages array (use - for stdin)",
},
{ flag: "--system <text>", description: "System prompt" },
{
flag: "--max-tokens <n>",
description: "Maximum tokens to generate (default: 4096)",
type: "number",
},
{ flag: "--temperature <n>", description: "Sampling temperature (0.0, 2.0]", type: "number" },
{ flag: "--top-p <n>", description: "Nucleus sampling threshold", type: "number" },
{ flag: "--stream", description: "Stream response tokens (default: on in TTY)" },
{
flag: "--tool <json-or-path>",
description: "Tool definition as JSON or file path (repeatable)",
type: "array",
},
{
flag: "--enable-thinking",
description: "Enable thinking/reasoning mode (for qwen3/qwq models)",
},
{
flag: "--thinking-budget <n>",
description: "Max tokens for thinking (default: 4096)",
type: "number",
},
],
flags: CHAT_FLAGS,
exampleArgs: [
'--message "What is Qwen?"',
'--model qwen-max --system "You are a coding assistant." --message "Write fizzbuzz in Python"',
@@ -114,12 +122,11 @@ export default defineCommand({
],
validate: (f) =>
!f.message && !f.messagesFile ? "Provide --message or --messages-file." : undefined,
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
const { system, messages } = parseMessages(flags);
const model = (flags.model as string) || config.defaultTextModel || "qwen3.7-max";
const shouldStream =
flags.stream === true || (flags.stream === undefined && process.stdout.isTTY);
const model = flags.model || config.defaultTextModel || "qwen3.7-max";
const shouldStream = flags.stream || process.stdout.isTTY;
const format = detectOutputFormat(config.output);
// Build messages array with system prompt
@@ -132,22 +139,22 @@ export default defineCommand({
const body: ChatRequest = {
model,
messages: allMessages,
max_tokens: (flags.maxTokens as number) ?? 4096,
max_tokens: flags.maxTokens ?? 4096,
stream: shouldStream,
};
if (flags.temperature !== undefined) body.temperature = flags.temperature as number;
if (flags.topP !== undefined) body.top_p = flags.topP as number;
if (flags.temperature !== undefined) body.temperature = flags.temperature;
if (flags.topP !== undefined) body.top_p = flags.topP;
if (flags.enableThinking) {
body.enable_thinking = true;
if (flags.thinkingBudget !== undefined) {
body.thinking_budget = flags.thinkingBudget as number;
body.thinking_budget = flags.thinkingBudget;
}
}
if (flags.tool) {
const tools = (flags.tool as string[]).map((t) => {
const tools = flags.tool.map((t) => {
try {
return JSON.parse(t);
} catch {
+19 -20
View File
@@ -5,7 +5,6 @@ import {
fetchModelList,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
@@ -187,30 +186,30 @@ export default defineCommand({
description: "Query free-tier quota for models (all models if --model is omitted)",
auth: "console",
usageArgs: "[--model <model>[,model2,...]] [flags]",
options: [
{
flag: "--model <model>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model name(s) to query, comma-separated for multiple; omit for all models",
},
{
flag: "--expiring <days>",
expiring: {
type: "string",
valueHint: "<days>",
description: "Only show quotas expiring within N days",
},
{
flag: "--sort <field>",
sort: {
type: "string",
valueHint: "<field>",
description: "Sort by: remaining (ascending), expires (ascending)",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: [
"",
"--model qwen3-max",
@@ -220,11 +219,11 @@ export default defineCommand({
"--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;
async run(config, flags) {
const modelFlag = flags.model || undefined;
const expiringDays = Number(flags.expiring) || 0;
const VALID_SORT_FIELDS = ["remaining", "expires"] as const;
const sortField = (flags.sort as string) || undefined;
const sortField = flags.sort || undefined;
if (sortField && !VALID_SORT_FIELDS.includes(sortField as (typeof VALID_SORT_FIELDS)[number])) {
process.stderr.write(
`Error: invalid --sort value "${sortField}". Must be one of: ${VALID_SORT_FIELDS.join(", ")}\n`,
@@ -5,7 +5,6 @@ import {
fetchModelList,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
@@ -104,34 +103,32 @@ export default defineCommand({
"Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable",
auth: "console",
usageArgs: "<--model <model>[,model2,...] | --all> [--off] [flags]",
options: [
{
flag: "--model <model>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model name(s), comma-separated for multiple",
},
{
flag: "--all",
all: {
type: "switch",
description: "Apply to all free-tier models",
},
{
flag: "--on",
on: {
type: "switch",
description: "Enable auto-stop (default behavior)",
},
{
flag: "--off",
off: {
type: "switch",
description: "Disable auto-stop",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: [
"--model qwen3-max",
"--model qwen3-max,qwen-turbo",
@@ -142,8 +139,8 @@ export default defineCommand({
],
validate: (f) =>
!f.model && !f.all ? "Provide --model <model>[,model2,...] or --all." : undefined,
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
async run(config, flags) {
const modelFlag = flags.model || undefined;
const off = Boolean(flags.off);
const format = detectOutputFormat(config.output);
+23 -23
View File
@@ -4,7 +4,6 @@ import {
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
@@ -288,34 +287,35 @@ export default defineCommand({
description: "Query model usage statistics",
auth: "console",
usageArgs: "[--model <model>] [--days <days>] [flags]",
options: [
{
flag: "--model <model>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model name(s), comma-separated; omit for overview",
},
{
flag: "--days <days>",
days: {
type: "string",
valueHint: "<days>",
description: "Number of days (default: 7)",
},
{
flag: "--type <type>",
type: {
type: "string",
valueHint: "<type>",
description: "Model type: Text, Vision, Multimodal, Audio, Embedding",
},
{
flag: "--workspace-id <id>",
workspaceId: {
type: "string",
valueHint: "<id>",
description: "Workspace ID (env: BAILIAN_WORKSPACE_ID)",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: [
"",
"--days 30",
@@ -325,13 +325,13 @@ export default defineCommand({
"--type Text --days 14",
"--output json",
],
async run(config: Config, flags: GlobalFlags) {
const modelFlag = (flags.model as string) || undefined;
async run(config, flags) {
const modelFlag = flags.model || undefined;
const daysFlag = Number(flags.days) || 7;
const typeFlag = (flags.type as string) || undefined;
const typeFlag = flags.type || undefined;
const format = detectOutputFormat(config.output);
const flagWorkspaceId = (flags.workspaceId as string) || undefined;
const flagWorkspaceId = flags.workspaceId || undefined;
const workspaceId = resolveWorkspaceId(config, flagWorkspaceId);
const endTime = Date.now();
@@ -3,8 +3,6 @@ import {
requestJson,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeTaskResponse,
BailianError,
ExitCode,
@@ -16,18 +14,23 @@ export default defineCommand({
description: "Download a completed video by task ID",
auth: "none",
usageArgs: "--task-id <id> --out <path>",
options: [
{ flag: "--task-id <id>", description: "Task ID to download from", required: true },
{ flag: "--out <path>", description: "Output file path", required: true },
],
flags: {
taskId: {
type: "string",
valueHint: "<id>",
description: "Task ID to download from",
required: true,
},
out: { type: "string", valueHint: "<path>", description: "Output file path", required: true },
},
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;
async run(config, flags) {
const taskId = flags.taskId;
const outPath = flags.out as string;
const outPath = flags.out;
const format = detectOutputFormat(config.output);
+76 -49
View File
@@ -4,8 +4,6 @@ import {
videoGenerateEndpoint,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeVideoEditRequest,
type DashScopeAsyncResponse,
type DashScopeTaskResponse,
@@ -27,81 +25,110 @@ export default defineCommand({
"Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.)",
auth: "apiKey",
usageArgs: "--video <url> --prompt <text> [flags]",
options: [
{ flag: "--model <model>", description: "Model ID (default: happyhorse-1.0-video-edit)" },
{
flag: "--video <url>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: happyhorse-1.0-video-edit)",
},
video: {
type: "string",
valueHint: "<url>",
description: "Input video URL or local file (mp4/mov, 2-10s)",
required: true,
},
{
flag: "--prompt <text>",
prompt: {
type: "string",
valueHint: "<text>",
description: 'Edit instruction (e.g. "Convert the scene to a claymation style")',
},
{ flag: "--ref-image <url>", description: "Reference image URL (up to 4, comma-separated)" },
{
flag: "--negative-prompt <text>",
refImage: {
type: "string",
valueHint: "<url>",
description: "Reference image URL (up to 4, comma-separated)",
},
negativePrompt: {
type: "string",
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
},
{ flag: "--resolution <res>", description: "Resolution: 720P or 1080P (default: 1080P)" },
{ flag: "--ratio <ratio>", description: "Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4)" },
{
flag: "--duration <seconds>",
description: "Output video duration in seconds (2-10)",
resolution: {
type: "string",
valueHint: "<res>",
description: "Resolution: 720P or 1080P (default: 1080P)",
},
ratio: {
type: "string",
valueHint: "<ratio>",
description: "Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4)",
},
duration: {
type: "number",
valueHint: "<seconds>",
description: "Output video duration in seconds (2-10)",
},
{
flag: "--audio-setting <mode>",
audioSetting: {
type: "string",
valueHint: "<mode>",
description: "Audio: auto (default) or origin (keep original)",
choices: ["auto", "origin"] as const,
},
{
flag: "--prompt-extend <bool>",
promptExtend: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
type: "boolean",
},
{
flag: "--watermark <bool>",
watermark: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_WATERMARK,
type: "boolean",
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
{
flag: "--async",
seed: {
type: "number",
valueHint: "<n>",
description: "Random seed for reproducible generation",
},
download: {
type: "string",
valueHint: "<path>",
description: "Save video to file on completion",
},
noWait: { type: "switch", description: "Return task ID immediately without waiting" },
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 15)",
},
async: {
type: "switch",
description: "Return task ID immediately (agent/CI mode, same as --no-wait)",
},
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 15)",
type: "number",
},
],
},
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) {
const videoUrl = flags.video as string;
async run(config, flags) {
const videoUrl = flags.video;
// prompt is optional for video edit per API spec
const prompt = flags.prompt as string | undefined;
const prompt = flags.prompt;
const model = (flags.model as string) || "happyhorse-1.0-video-edit";
const model = flags.model || "happyhorse-1.0-video-edit";
const format = detectOutputFormat(config.output);
// Auto-upload local files
const credential = await resolveCredential(config);
const resolvedVideoUrl = await resolveFileUrl(videoUrl!, credential.token, model);
const resolvedVideoUrl = await resolveFileUrl(videoUrl, credential.token, model);
// --- Build media array ---
const media: DashScopeVideoEditRequest["input"]["media"] = [
{ type: "video", url: resolvedVideoUrl },
];
// Support comma-separated reference images
const refImageArg = flags.refImage as string | undefined;
const refImageArg = flags.refImage;
if (refImageArg) {
const images = refImageArg
.split(",")
@@ -121,17 +148,17 @@ export default defineCommand({
model,
input: {
prompt: prompt || undefined,
negative_prompt: (flags.negativePrompt as string) || undefined,
negative_prompt: flags.negativePrompt || undefined,
media,
},
parameters: {
resolution: (flags.resolution as string) || undefined,
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
audio_setting: (flags.audioSetting as "auto" | "origin") || undefined,
resolution: flags.resolution || undefined,
ratio: flags.ratio || undefined,
duration: flags.duration || undefined,
audio_setting: flags.audioSetting || undefined,
prompt_extend: promptExtend,
watermark,
seed: flags.seed as number | undefined,
seed: flags.seed,
},
};
@@ -164,7 +191,7 @@ export default defineCommand({
// --- Poll until completion ---
// Video editing is compute-intensive; default timeout = 600s (10 min)
const pollInterval = (flags.pollInterval as number) ?? 15;
const pollInterval = flags.pollInterval ?? 15;
const pollUrl = taskEndpoint(config.baseUrl, taskId);
const editTimeout = Math.max(config.timeout, 600);
@@ -190,7 +217,7 @@ export default defineCommand({
// --download: save to file
if (flags.download) {
const destPath = flags.download as string;
const destPath = flags.download;
const { size } = await downloadFile(resultVideoUrl, destPath, { quiet: config.quiet });
if (config.quiet) {
@@ -4,8 +4,6 @@ import {
videoGenerateEndpoint,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeVideoRequest,
type DashScopeAsyncResponse,
type DashScopeTaskResponse,
@@ -28,47 +26,74 @@ export default defineCommand({
"Generate a video from text or image (happyhorse-1.0-t2v / happyhorse-1.0-i2v / wan2.6-t2v)",
auth: "apiKey",
usageArgs: "--prompt <text> [--image <url>] [flags]",
options: [
{
flag: "--model <model>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: happyhorse-1.0-t2v, or happyhorse-1.0-i2v with --image)",
},
{ flag: "--prompt <text>", description: "Video description", required: true },
{ flag: "--image <url>", description: "Input image URL for image-to-video generation" },
{
flag: "--negative-prompt <text>",
prompt: {
type: "string",
valueHint: "<text>",
description: "Video description",
required: true,
},
image: {
type: "string",
valueHint: "<url>",
description: "Input image URL for image-to-video generation",
},
negativePrompt: {
type: "string",
valueHint: "<text>",
description: "Negative prompt to exclude unwanted content",
},
{ flag: "--resolution <res>", description: "Resolution: 720P or 1080P (default: 1080P)" },
{ flag: "--ratio <ratio>", description: "Aspect ratio (e.g. 16:9, 9:16, 1:1)" },
{
flag: "--duration <seconds>",
description: "Video duration in seconds (default: 5)",
resolution: {
type: "string",
valueHint: "<res>",
description: "Resolution: 720P or 1080P (default: 1080P)",
},
ratio: {
type: "string",
valueHint: "<ratio>",
description: "Aspect ratio (e.g. 16:9, 9:16, 1:1)",
},
duration: {
type: "number",
valueHint: "<seconds>",
description: "Video duration in seconds (default: 5)",
},
{
flag: "--prompt-extend <bool>",
promptExtend: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
type: "boolean",
},
{
flag: "--watermark <bool>",
watermark: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_WATERMARK,
type: "boolean",
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
{
flag: "--async",
seed: {
type: "number",
valueHint: "<n>",
description: "Random seed for reproducible generation",
},
download: {
type: "string",
valueHint: "<path>",
description: "Save video to file on completion",
},
noWait: { type: "switch", description: "Return task ID immediately without waiting" },
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 5)",
},
async: {
type: "switch",
description: "Return task ID immediately (agent/CI mode, same as --no-wait)",
},
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 5)",
type: "number",
},
],
},
exampleArgs: [
'--prompt "A person reading a book, static shot"',
'--prompt "Ocean waves at sunset." --download sunset.mp4',
@@ -76,16 +101,16 @@ export default defineCommand({
'--prompt "Mountain landscape" --resolution 720P --duration 5',
'--prompt "A cat playing with a ball" --watermark false',
],
async run(config: Config, flags: GlobalFlags) {
const prompt = flags.prompt as string;
async run(config, flags) {
const prompt = flags.prompt;
const model =
(flags.model as string) ||
flags.model ||
config.defaultVideoModel ||
((flags.image as string) ? "happyhorse-1.0-i2v" : "happyhorse-1.0-t2v");
(flags.image ? "happyhorse-1.0-i2v" : "happyhorse-1.0-t2v");
const format = detectOutputFormat(config.output);
const imageUrl = flags.image as string | undefined;
const imageUrl = flags.image;
// Auto-upload local image file for i2v
let resolvedImageUrl: string | undefined;
@@ -100,20 +125,20 @@ export default defineCommand({
const body: DashScopeVideoRequest = {
model,
input: {
prompt: prompt!,
negative_prompt: (flags.negativePrompt as string) || undefined,
prompt: prompt,
negative_prompt: flags.negativePrompt || undefined,
// i2v models (happyhorse-1.0-i2v) require input.media with type 'first_frame'
...(resolvedImageUrl
? { media: [{ type: "first_frame" as const, url: resolvedImageUrl }] }
: {}),
},
parameters: {
resolution: (flags.resolution as string) || undefined,
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
resolution: flags.resolution || undefined,
ratio: flags.ratio || undefined,
duration: flags.duration || undefined,
prompt_extend: promptExtend,
watermark,
seed: flags.seed as number | undefined,
seed: flags.seed,
},
};
@@ -152,7 +177,7 @@ export default defineCommand({
}
// Poll all tasks concurrently
const pollInterval = (flags.pollInterval as number) ?? 5;
const pollInterval = flags.pollInterval ?? 5;
const pollPromises = taskIds.map((taskId) => {
const pollUrl = taskEndpoint(config.baseUrl, taskId);
@@ -189,7 +214,7 @@ export default defineCommand({
// --download: save to file (first video only for explicit path)
if (flags.download) {
const destPath = flags.download as string;
const destPath = flags.download;
const { size } = await downloadFile(videos[0]!.videoUrl, destPath, { quiet: config.quiet });
if (config.quiet) {
+69 -54
View File
@@ -4,8 +4,6 @@ import {
videoGenerateEndpoint,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeVideoRefRequest,
type DashScopeAsyncResponse,
type DashScopeTaskResponse,
@@ -27,63 +25,80 @@ export default defineCommand({
"Reference-to-video generation (happyhorse-1.0-r2v / wan2.6-r2v): multi-subject, multi-shot with voice",
auth: "apiKey",
usageArgs: "--prompt <text> --image <url>... [--ref-video <url>...] [flags]",
options: [
{ flag: "--model <model>", description: "Model ID (default: happyhorse-1.0-r2v)" },
{
flag: "--prompt <text>",
flags: {
model: {
type: "string",
valueHint: "<model>",
description: "Model ID (default: happyhorse-1.0-r2v)",
},
prompt: {
type: "string",
valueHint: "<text>",
description: "Video description with reference markers (image1, video1, etc.)",
required: true,
},
{
flag: "--image <url>",
image: {
type: "array",
valueHint: "<url>",
description: "Reference image URL or local file (repeatable for multiple subjects)",
type: "array",
},
{
flag: "--ref-video <url>",
refVideo: {
type: "array",
valueHint: "<url>",
description: "Reference video URL or local file (repeatable)",
type: "array",
},
{
flag: "--image-voice <url>",
imageVoice: {
type: "array",
valueHint: "<url>",
description: "Voice URL for corresponding image (pairs by position)",
type: "array",
},
{
flag: "--video-voice <url>",
videoVoice: {
type: "array",
valueHint: "<url>",
description: "Voice URL for corresponding ref-video (pairs by position)",
type: "array",
},
{ flag: "--resolution <res>", description: "Resolution: 720P or 1080P (default: 1080P)" },
{ flag: "--ratio <ratio>", description: "Aspect ratio (16:9, 9:16, 1:1)" },
{
flag: "--duration <seconds>",
description: "Video duration in seconds (default: 5)",
resolution: {
type: "string",
valueHint: "<res>",
description: "Resolution: 720P or 1080P (default: 1080P)",
},
ratio: { type: "string", valueHint: "<ratio>", description: "Aspect ratio (16:9, 9:16, 1:1)" },
duration: {
type: "number",
valueHint: "<seconds>",
description: "Video duration in seconds (default: 5)",
},
{
flag: "--prompt-extend <bool>",
promptExtend: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT,
type: "boolean",
},
{
flag: "--watermark <bool>",
watermark: {
type: "boolean",
valueHint: "<bool>",
description: BOOL_FLAG_WATERMARK,
type: "boolean",
},
{ flag: "--seed <n>", description: "Random seed for reproducible generation", type: "number" },
{ flag: "--download <path>", description: "Save video to file on completion" },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
{
flag: "--async",
seed: {
type: "number",
valueHint: "<n>",
description: "Random seed for reproducible generation",
},
download: {
type: "string",
valueHint: "<path>",
description: "Save video to file on completion",
},
noWait: { type: "switch", description: "Return task ID immediately without waiting" },
pollInterval: {
type: "number",
valueHint: "<seconds>",
description: "Polling interval when waiting (default: 15)",
},
async: {
type: "switch",
description: "Return task ID immediately (agent/CI mode, same as --no-wait)",
},
{
flag: "--poll-interval <seconds>",
description: "Polling interval when waiting (default: 15)",
type: "number",
},
],
},
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',
@@ -95,16 +110,16 @@ export default defineCommand({
!(f.image as string[] | undefined)?.length && !(f.refVideo as string[] | undefined)?.length
? "Provide at least one --image or --ref-video."
: undefined,
async run(config: Config, flags: GlobalFlags) {
const prompt = flags.prompt as string;
async run(config, flags) {
const prompt = flags.prompt;
const images = (flags.image as string[] | undefined) || [];
const refVideos = (flags.refVideo as string[] | undefined) || [];
const images = flags.image || [];
const refVideos = flags.refVideo || [];
const imageVoices = (flags.imageVoice as string[] | undefined) || [];
const videoVoices = (flags.videoVoice as string[] | undefined) || [];
const imageVoices = flags.imageVoice || [];
const videoVoices = flags.videoVoice || [];
const model = (flags.model as string) || "happyhorse-1.0-r2v";
const model = flags.model || "happyhorse-1.0-r2v";
const format = detectOutputFormat(config.output);
// --- Resolve file URLs (auto-upload local files) ---
@@ -152,16 +167,16 @@ export default defineCommand({
const body: DashScopeVideoRefRequest = {
model,
input: {
prompt: prompt!,
prompt: prompt,
media,
},
parameters: {
resolution: (flags.resolution as string) || undefined,
ratio: (flags.ratio as string) || undefined,
duration: (flags.duration as number) || undefined,
resolution: flags.resolution || undefined,
ratio: flags.ratio || undefined,
duration: flags.duration || undefined,
prompt_extend: promptExtend,
watermark,
seed: flags.seed as number | undefined,
seed: flags.seed,
},
};
@@ -195,7 +210,7 @@ export default defineCommand({
}
// --- Poll until completion ---
const pollInterval = (flags.pollInterval as number) ?? 15;
const pollInterval = flags.pollInterval ?? 15;
const pollUrl = taskEndpoint(config.baseUrl, taskId);
const refTimeout = Math.max(config.timeout, 600);
@@ -221,7 +236,7 @@ export default defineCommand({
// --download: save to file
if (flags.download) {
const destPath = flags.download as string;
const destPath = flags.download;
const { size } = await downloadFile(resultVideoUrl, destPath, { quiet: config.quiet });
if (config.quiet) {
@@ -3,8 +3,6 @@ import {
requestJson,
taskEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type DashScopeTaskResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
@@ -13,13 +11,15 @@ export default defineCommand({
description: "Query async task status",
auth: "apiKey",
usageArgs: "--task-id <id>",
options: [{ flag: "--task-id <id>", description: "Async task ID", required: true }],
flags: {
taskId: { type: "string", valueHint: "<id>", description: "Async task ID", required: true },
},
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;
async run(config, flags) {
const taskId = flags.taskId;
const format = detectOutputFormat(config.output);
@@ -3,8 +3,6 @@ import {
requestJson,
chatEndpoint,
detectOutputFormat,
type Config,
type GlobalFlags,
type ChatRequest,
type ChatResponse,
type ChatMessageContent,
@@ -58,16 +56,24 @@ export default defineCommand({
description: "Describe an image or video using Qwen-VL",
auth: "apiKey",
usageArgs: "--image <path-or-url> [--video <url>] [--prompt <text>]",
options: [
{ flag: "--image <path-or-url>", description: "Local image path or URL" },
{
flag: "--video <url>",
description: "Video file URL or local path (mp4/mov/avi/mkv/webm)",
flags: {
image: { type: "string", valueHint: "<path-or-url>", description: "Local image path or URL" },
video: {
type: "array",
valueHint: "<url>",
description: "Video file URL or local path (mp4/mov/avi/mkv/webm)",
},
{ flag: "--prompt <text>", description: "Question about the content (default: auto-detected)" },
{ flag: "--model <model>", description: "Vision model (default: qwen3-vl-plus)" },
],
prompt: {
type: "string",
valueHint: "<text>",
description: "Question about the content (default: auto-detected)",
},
model: {
type: "string",
valueHint: "<model>",
description: "Vision model (default: qwen3-vl-plus)",
},
},
exampleArgs: [
"--image photo.jpg",
'--image https://example.com/photo.jpg --prompt "What breed is this dog?"',
@@ -79,10 +85,10 @@ export default defineCommand({
!f.image && !(f.video as string[] | undefined)?.length
? "Provide --image or --video."
: undefined,
async run(config: Config, flags: GlobalFlags) {
let image = flags.image as string | undefined;
const videoInputs = (flags.video as string[] | undefined) ?? [];
const model = (flags.model as string) || "qwen3-vl-plus";
async run(config, flags) {
let image = flags.image;
const videoInputs = flags.video ?? [];
const model = flags.model || "qwen3-vl-plus";
// Auto-detect: if --image was given a video file, treat it as --video
if (image && isVideoInput(image)) {
@@ -92,7 +98,7 @@ export default defineCommand({
const hasVideo = videoInputs.length > 0;
const defaultPrompt = hasVideo ? "Describe the video." : "Describe the image.";
const prompt = (flags.prompt as string) || defaultPrompt;
const prompt = flags.prompt || defaultPrompt;
const format = detectOutputFormat(config.output);
@@ -3,8 +3,6 @@ import {
callConsoleGateway,
resolveConsoleGatewayCredential,
detectOutputFormat,
type Config,
type GlobalFlags,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { displayWidth, padEnd } from "bailian-cli-runtime";
@@ -79,24 +77,22 @@ export default defineCommand({
description: "List all workspaces",
auth: "console",
usageArgs: "[flags]",
options: [
{
flag: "--list <n>",
flags: {
list: {
type: "string",
valueHint: "<n>",
description: "Limit number of results",
},
{ flag: "--console-region <region>", description: "Console region" },
{
flag: "--console-site <site>",
consoleRegion: { type: "string", valueHint: "<region>", description: "Console region" },
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID",
type: "number",
},
],
consoleSwitchAgent: { type: "number", valueHint: "<uid>", description: "Switch agent UID" },
},
exampleArgs: ["", "--list 5", "--output json"],
async run(config: Config, flags: GlobalFlags) {
async run(config, flags) {
const limit = Number(flags.list) || 0;
const format = detectOutputFormat(config.output);
+1 -1
View File
@@ -4,7 +4,7 @@ import { ensureConfigDir, getConfigPath } from "./paths.ts";
import { detectOutputFormat, type OutputFormat } from "../output/formatter.ts";
import { BailianError } from "../errors/base.ts";
import { ExitCode } from "../errors/codes.ts";
import type { GlobalFlags } from "../types/flags.ts";
import type { GlobalFlags } from "../types/command.ts";
export function readConfigFile(): ConfigFile {
const path = getConfigPath();
+1 -1
View File
@@ -1,5 +1,5 @@
import type { Config } from "../config/schema.ts";
import type { GlobalFlags } from "../types/flags.ts";
import type { GlobalFlags } from "../types/command.ts";
import { BailianError } from "../errors/base.ts";
import { createTrackingEvent } from "./event.ts";
import { localSink, remoteSink } from "./sink.ts";
+123 -99
View File
@@ -1,111 +1,135 @@
import type { Config } from "../config/schema.ts";
import type { GlobalFlags } from "./flags.ts";
/**
* Flag value type, driving the parser.
* - string : `--flag <value>` → string (default).
* - number : `--flag <n>` → coerced + validated finite number.
* - boolean : `--flag true|false` → coerced boolean (a *value* flag).
* - switch : `--flag` → presence = true, takes NO value (`--flag=x` errors).
* - array : repeatable `--flag a --flag b` → string[].
* Omitting `type`: a flag string with a `<…>`/`[…]` placeholder defaults to
* string, otherwise to switch.
*/
export interface OptionDef {
flag: string;
// ── Flag definitions ─────────────────────────────────────────────────────────
// Flags are keyed by camelCase name (the key IS the parsed flag name, e.g.
// `maxTokens` ↔ `--max-tokens`). The flag's type drives both runtime parsing
// and the compile-time flag types inferred via ParsedFlags.
/** A presence flag: `--quiet`. No value; absent → false. */
export interface SwitchFlag {
type: "switch";
description: string;
type?: "string" | "number" | "boolean" | "switch" | "array";
required?: boolean;
}
/** A value flag: `--prompt <text>`, `--n <count>`, `--watermark true|false`. */
export interface ValueFlag {
type: "string" | "number" | "boolean" | "array";
description: string;
valueHint: string;
required?: boolean;
/**
* Restrict to a fixed set of values. The parser rejects anything else and the
* parsed type narrows to the union. Declare with `as const` so the literals
* survive inference: `choices: ["mp3", "wav"] as const`.
*/
choices?: readonly string[];
}
export type FlagDef = SwitchFlag | ValueFlag;
export type FlagsDef = Record<string, FlagDef>;
// ── Type inference: definition → parsed flag types ───────────────────────────
type ParsedValue<F extends FlagDef> = F extends SwitchFlag
? boolean
: F extends { type: "number" }
? number
: F extends { type: "boolean" }
? boolean
: F extends { choices: readonly (infer C extends string)[] }
? F extends { type: "array" }
? C[]
: C
: F extends { type: "array" }
? string[]
: string;
/** Switches and `required` value flags are present; other value flags optional. */
type IsRequired<F extends FlagDef> = F extends SwitchFlag
? true
: F extends { required: true }
? true
: false;
/**
* Credential a command requires. The runtime prepares it before execution.
* - "apiKey" : DashScope API Key. The runtime runs ensureApiKey beforehand,
* except under --dry-run (which only prints the request).
* - "console" : Console Gateway credential (usage / quota / app list / workspace, …).
* - "none" : No credential (config / auth login flow / pipeline validate / update, …).
* Map a FlagsDef to the parsed flags object type. Required flags (switches +
* `required: true`) are required properties; optional value flags are `?`.
*/
export type ParsedFlags<F extends FlagsDef> = {
[K in keyof F as IsRequired<F[K]> extends true ? K : never]: ParsedValue<F[K]>;
} & {
[K in keyof F as IsRequired<F[K]> extends true ? never : K]?: ParsedValue<F[K]>;
};
export type AuthRequirement = "apiKey" | "console" | "none";
export interface Command {
description: string;
/**
* Argument portion of the usage line, WITHOUT the `<bin> <path>` prefix
* (e.g. "--index-id <id> --query <text> [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[];
/**
* Example argument strings, each WITHOUT the `<bin> <path>` prefix
* (e.g. '--index-id idx_xxx --query "..."'). The runtime prepends
* `<bin> <path>` per product when rendering help.
*/
exampleArgs?: string[];
/** Credential this command requires. See {@link AuthRequirement}. */
auth: AuthRequirement;
notes?: string[];
/**
* Cross-flag validation, run after parsing and before execute (one-of, 3-of-N,
* value-conditional, dependency, …). Return an error message → UsageError;
* undefined to pass. Single-flag `required: true` is enforced by the parser —
* use this only for rules spanning multiple flags or depending on a flag's *value*.
*/
validate?: (flags: GlobalFlags) => string | undefined;
execute: (config: Config, flags: GlobalFlags) => Promise<void>;
}
export interface CommandSpec {
description: string;
/** See {@link Command.usageArgs} — argument portion only, no `<bin> <path>` prefix. */
usageArgs?: string;
options?: OptionDef[];
/** See {@link Command.exampleArgs} — argument strings only, no `<bin> <path>` prefix. */
exampleArgs?: string[];
/** Credential this command requires. See {@link AuthRequirement}. */
auth: AuthRequirement;
notes?: string[];
/** Cross-flag validation — see {@link Command.validate}. */
validate?: (flags: GlobalFlags) => string | undefined;
run: (config: Config, flags: GlobalFlags) => Promise<void>;
}
export function defineCommand(spec: CommandSpec): Command {
return {
description: spec.description,
usageArgs: spec.usageArgs,
options: spec.options,
exampleArgs: spec.exampleArgs,
auth: spec.auth,
notes: spec.notes,
validate: spec.validate,
execute: (config, flags) => spec.run(config, flags),
};
}
/** Global flags shared by all commands — drives the parser's type resolution. */
export const GLOBAL_OPTIONS: OptionDef[] = [
{ flag: "--api-key <key>", description: "API key" },
{ flag: "--base-url <url>", description: "API base URL" },
{ flag: "--output <format>", description: "Output format: text, json" },
{ flag: "--timeout <seconds>", description: "Request timeout", type: "number" },
{ flag: "--quiet", description: "Suppress non-essential output", type: "switch" },
{ flag: "--verbose", description: "Print HTTP request/response details", type: "switch" },
{ flag: "--no-color", description: "Disable ANSI colors", type: "switch" },
{ flag: "--dry-run", description: "Dry run mode", type: "switch" },
{ flag: "--non-interactive", description: "Disable interactive prompts", type: "switch" },
{ flag: "--concurrent <n>", description: "Run N parallel requests (default: 1)", type: "number" },
{
flag: "--console-region <region>",
// ── Global flags (single source: derived from GLOBAL_FLAGS) ──────────────────
export const GLOBAL_FLAGS = {
apiKey: { type: "string", valueHint: "<key>", description: "API key" },
baseUrl: { type: "string", valueHint: "<url>", description: "API base URL" },
output: { type: "string", valueHint: "<format>", description: "Output format: text, json" },
timeout: { type: "number", valueHint: "<seconds>", description: "Request timeout" },
concurrent: {
type: "number",
valueHint: "<n>",
description: "Run N parallel requests (default: 1)",
},
quiet: { type: "switch", description: "Suppress non-essential output" },
verbose: { type: "switch", description: "Print HTTP request/response details" },
noColor: { type: "switch", description: "Disable ANSI colors" },
dryRun: { type: "switch", description: "Dry run mode" },
nonInteractive: { type: "switch", description: "Disable interactive prompts" },
yes: { type: "switch", description: "Skip confirmation prompts" },
async: { type: "switch", description: "Return async task id without waiting" },
consoleRegion: {
type: "string",
valueHint: "<region>",
description: "Console gateway region (e.g. cn-beijing, ap-southeast-1)",
},
{ flag: "--console-site <site>", description: "Console site: domestic, international" },
{
flag: "--console-switch-agent <uid>",
description: "Switch agent UID for delegated access",
type: "number",
consoleSite: {
type: "string",
valueHint: "<site>",
description: "Console site: domestic, international",
},
{ flag: "--help", description: "Show help", type: "switch" },
{ flag: "--version", description: "Print version", type: "switch" },
];
consoleSwitchAgent: {
type: "number",
valueHint: "<uid>",
description: "Switch agent UID for delegated access",
},
help: { type: "switch", description: "Show help" },
version: { type: "switch", description: "Print version" },
} satisfies FlagsDef;
export type GlobalFlags = ParsedFlags<typeof GLOBAL_FLAGS>;
/** A command's full flags: global + its own flags, inferred in one pass. */
export type Flags<F extends FlagsDef> = ParsedFlags<typeof GLOBAL_FLAGS & F>;
// ── Command ──────────────────────────────────────────────────────────────────
/**
* A command. Generic over its flags `F` so `run`/`validate` receive precisely
* typed flags (`Flags<F>` = global + own flags). Stored heterogeneously as
* {@link AnyCommand}; the precise typing lives at the `defineCommand` call site.
*/
export interface Command<F extends FlagsDef = FlagsDef> {
description: string;
/** Credential this command requires. See {@link AuthRequirement}. */
auth: AuthRequirement;
/** Usage line arg portion, e.g. "--prompt <text> [flags]". Manually written. */
usageArgs?: string;
/** Example arg strings (without the `<bin> <path>` prefix). */
exampleArgs?: string[];
notes?: string[];
flags?: F;
/**
* Cross-flag validation, after parsing and before run. Return an error message
* → UsageError; undefined to pass. Single-flag `required` is enforced by the
* parser — use this for rules spanning flags or depending on a flag's *value*.
*/
validate?: (flags: Flags<F>) => string | undefined;
run: (config: Config, flags: Flags<F>) => Promise<void>;
}
/** Type-erased command for heterogeneous storage (registry / context). */
export type AnyCommand = Command<any>;
/** Identity wrapper whose only job is to infer `F` from `spec.flags`. */
export function defineCommand<F extends FlagsDef>(spec: Command<F>): Command<F> {
return spec;
}
-17
View File
@@ -1,17 +0,0 @@
export interface GlobalFlags {
apiKey?: string;
baseUrl?: string;
output?: string;
quiet: boolean;
verbose: boolean;
timeout?: number;
noColor: boolean;
yes: boolean;
dryRun: boolean;
nonInteractive: boolean;
async: boolean;
consoleRegion?: string;
consoleSite?: string;
consoleSwitchAgent?: number;
[key: string]: unknown;
}
+10 -3
View File
@@ -1,6 +1,13 @@
export type { Command, CommandSpec, OptionDef } from "./command.ts";
export { defineCommand, GLOBAL_OPTIONS } from "./command.ts";
export type { GlobalFlags } from "./flags.ts";
export type {
Command,
AnyCommand,
FlagDef,
FlagsDef,
ParsedFlags,
Flags,
GlobalFlags,
} from "./command.ts";
export { defineCommand, GLOBAL_FLAGS } from "./command.ts";
export type {
AppCompletionRequest,
AppCompletionResponse,
+2 -2
View File
@@ -1,5 +1,5 @@
import { createCli } from "bailian-cli-runtime";
import type { Command } from "bailian-cli-core";
import type { AnyCommand } from "bailian-cli-core";
import {
authLogin,
authStatus,
@@ -24,7 +24,7 @@ import pkg from "../package.json" with { type: "json" };
// remapped to a flat `rag retrieve` path. Routing is driven entirely by these
// keys, and usage/examples/errors render the path from the key — so the same
// shared command shows `rag retrieve` here and `bl knowledge retrieve` in bl.
const commands: Record<string, Command> = {
const commands: Record<string, AnyCommand> = {
"auth login": authLogin,
"auth status": authStatus,
"auth logout": authLogout,
+47 -102
View File
@@ -1,66 +1,13 @@
import type { GlobalFlags } from "bailian-cli-core";
import type { OptionDef } from "bailian-cli-core";
import type { FlagsDef, ParsedFlags } from "bailian-cli-core";
import { UsageError } from "bailian-cli-core";
function kebabToCamel(str: string): string {
return str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
}
/** Extract camelCase flag name from an OptionDef.flag string, e.g. '--max-tokens <n>' → 'maxTokens' */
function flagKey(def: OptionDef): string | null {
const m = def.flag.match(/^--([a-z][a-z0-9-]*)/i);
return m ? kebabToCamel(m[1]!) : null;
}
interface FlagSchema {
switches: Set<string>;
booleans: Set<string>;
numbers: Set<string>;
arrays: Set<string>;
}
function buildAllowedFlagKeys(options: OptionDef[]): Set<string> {
const keys = new Set<string>();
for (const opt of options) {
const key = flagKey(opt);
if (key) keys.add(key);
}
return keys;
}
/**
* Classify each option by its declared (or inferred) type. Inference: a flag
* with a `<…>`/`[…]` placeholder defaults to string, otherwise to switch.
* Strings are the default bucket and need no set.
*/
function buildSchema(options: OptionDef[]): FlagSchema {
const switches = new Set<string>();
const booleans = new Set<string>();
const numbers = new Set<string>();
const arrays = new Set<string>();
for (const opt of options) {
const key = flagKey(opt);
if (!key) continue;
switch (opt.type) {
case "switch":
switches.add(key);
break;
case "boolean":
booleans.add(key);
break;
case "number":
numbers.add(key);
break;
case "array":
arrays.add(key);
break;
case "string":
break;
default:
if (!opt.flag.includes("<") && !opt.flag.includes("[")) switches.add(key);
}
}
return { switches, booleans, numbers, arrays };
/** maxTokens → max-tokens. For rendering flags in help / error messages. */
export function camelToKebab(str: string): string {
return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
}
export interface ParsePathResult {
@@ -92,22 +39,15 @@ export function parsePath(argv: string[]): ParsePathResult {
/**
* Second pass — parse the flag region into typed values, driven entirely by the
* OptionDef schema. Pure: returns typed flags or throws UsageError — never
* prints/exits. The runtime's error boundary decides rendering.
* keyed FlagsDef (key = camelCase flag name). Pure: returns typed flags or
* throws UsageError — never prints/exits. The error boundary decides rendering.
*/
export function parseFlags(rest: string[], options: OptionDef[]): GlobalFlags {
const allowedKeys = buildAllowedFlagKeys(options);
const schema = buildSchema(options);
export function parseFlags<F extends FlagsDef>(rest: string[], defs: F): ParsedFlags<F> {
const flags: Record<string, unknown> = {};
const seen = new Set<string>();
const flags: GlobalFlags = {
quiet: false,
verbose: false,
noColor: false,
yes: false,
dryRun: false,
nonInteractive: false,
async: false,
};
for (const [key, def] of Object.entries(defs)) {
if (def.type === "switch") flags[key] = false;
}
let i = 0;
while (i < rest.length) {
@@ -121,22 +61,23 @@ export function parseFlags(rest: string[], options: OptionDef[]): GlobalFlags {
}
const eqIdx = arg.indexOf("=");
const key = eqIdx !== -1 ? arg.slice(2, eqIdx) : arg.slice(2);
const rawKey = eqIdx !== -1 ? arg.slice(2, eqIdx) : arg.slice(2);
let value: string | undefined = eqIdx !== -1 ? arg.slice(eqIdx + 1) : undefined;
if (key === "") {
if (rawKey === "") {
throw new UsageError(`Unknown flag "${arg}".`);
}
const camelKey = kebabToCamel(key);
if (!allowedKeys.has(camelKey)) {
throw new UsageError(`Unknown flag "--${key}". Run with --help to see available options.`);
const key = kebabToCamel(rawKey);
const def = defs[key];
if (!def) {
throw new UsageError(`Unknown flag "--${rawKey}". Run with --help to see available options.`);
}
if (schema.switches.has(camelKey)) {
if (def.type === "switch") {
if (value !== undefined) {
throw new UsageError(`Flag --${key} is a switch and takes no value.`);
throw new UsageError(`Flag --${rawKey} is a switch and takes no value.`);
}
(flags as Record<string, unknown>)[camelKey] = true;
flags[key] = true;
i++;
continue;
}
@@ -144,7 +85,7 @@ export function parseFlags(rest: string[], options: OptionDef[]): GlobalFlags {
if (value === undefined) {
const next = rest[i + 1];
if (next === undefined || next.startsWith("--")) {
throw new UsageError(`Flag --${key} requires a value.`);
throw new UsageError(`Flag --${rawKey} requires a value.`);
}
value = next;
i += 2;
@@ -152,45 +93,49 @@ export function parseFlags(rest: string[], options: OptionDef[]): GlobalFlags {
i += 1;
}
if (schema.arrays.has(camelKey)) {
const arr = (flags as Record<string, unknown>)[camelKey] as string[] | undefined;
if (def.choices && !def.choices.includes(value)) {
throw new UsageError(`Flag --${rawKey} must be one of: ${def.choices.join(", ")}.`);
}
if (def.type === "array") {
const arr = flags[key] as string[] | undefined;
if (arr) arr.push(value);
else (flags as Record<string, unknown>)[camelKey] = [value];
else flags[key] = [value];
continue;
}
if (seen.has(camelKey)) {
throw new UsageError(`Flag --${key} given more than once.`);
if (seen.has(key)) {
throw new UsageError(`Flag --${rawKey} given more than once.`);
}
seen.add(camelKey);
seen.add(key);
if (schema.numbers.has(camelKey)) {
if (def.type === "number") {
const n = Number(value);
if (!Number.isFinite(n)) {
throw new UsageError(`Flag --${key} requires a finite number.`);
throw new UsageError(`Flag --${rawKey} requires a finite number.`);
}
(flags as Record<string, unknown>)[camelKey] = n;
} else if (schema.booleans.has(camelKey)) {
flags[key] = n;
} else if (def.type === "boolean") {
const v = value.trim().toLowerCase();
if (v === "true") (flags as Record<string, unknown>)[camelKey] = true;
else if (v === "false") (flags as Record<string, unknown>)[camelKey] = false;
else throw new UsageError(`Flag --${key} requires true or false.`);
if (v === "true") flags[key] = true;
else if (v === "false") flags[key] = false;
else throw new UsageError(`Flag --${rawKey} requires true or false.`);
} else {
(flags as Record<string, unknown>)[camelKey] = value;
flags[key] = value;
}
}
const missing = options.filter((opt) => {
if (!opt.required) return false;
const key = flagKey(opt);
return key !== null && (flags as Record<string, unknown>)[key] === undefined;
});
// Required enforcement — declarative, driven by the schema.
const missing = Object.entries(defs)
.filter(
([key, def]) => def.type !== "switch" && def.required === true && flags[key] === undefined,
)
.map(([key]) => `--${camelToKebab(key)}`);
if (missing.length > 0) {
const names = missing.map((opt) => opt.flag.match(/^(--[a-z][a-z0-9-]*)/i)?.[1] ?? opt.flag);
throw new UsageError(
`Missing required ${names.length > 1 ? "flags" : "flag"}: ${names.join(", ")}`,
`Missing required ${missing.length > 1 ? "flags" : "flag"}: ${missing.join(", ")}`,
);
}
return flags;
return flags as unknown as ParsedFlags<F>;
}
+9 -6
View File
@@ -9,8 +9,8 @@ import {
runCommandStage,
type RunContext,
} from "./middleware.ts";
import type { Command, Config, GlobalFlags } from "bailian-cli-core";
import { GLOBAL_OPTIONS, UsageError, loadConfig, flushTelemetry } from "bailian-cli-core";
import type { AnyCommand, Config, GlobalFlags } from "bailian-cli-core";
import { GLOBAL_FLAGS, UsageError, loadConfig, flushTelemetry } from "bailian-cli-core";
import { setupProxyFromEnv } from "./proxy.ts";
import { handleError } from "./error-handler.ts";
import { printWelcomeBanner, printQuickStart } from "./output/banner.ts";
@@ -36,7 +36,7 @@ export interface Cli {
* its own commands + identity. `run` resolves argv into a {@link Resolution},
* then dispatches it.
*/
export function createCli(commands: Record<string, Command>, opts: CliOptions): Cli {
export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions): Cli {
const registry = new CommandRegistry(commands, opts.binName);
const clientName = opts.clientName ?? opts.binName;
const { binName, version, npmPackage } = opts;
@@ -77,7 +77,7 @@ export function createCli(commands: Record<string, Command>, opts: CliOptions):
let hasKey = false;
try {
const config = buildConfig(parseFlags(argv, GLOBAL_OPTIONS));
const config = buildConfig(parseFlags(argv, GLOBAL_FLAGS));
hasKey = !!(
config.apiKey ||
config.fileApiKey ||
@@ -109,8 +109,11 @@ export function createCli(commands: Record<string, Command>, opts: CliOptions):
case "run": {
try {
// 解析 flag + 跨 flag 校验:任何用法问题都抛 UsageError
const flags = parseFlags(res.rest, [...GLOBAL_OPTIONS, ...(res.command.options ?? [])]);
// 解析 flag + 跨 flag 校验:任何用法问题都抛 UsageError。
const flags = parseFlags(res.rest, {
...GLOBAL_FLAGS,
...res.command.flags,
}) as GlobalFlags;
const invalid = res.command.validate?.(flags);
if (invalid) throw new UsageError(invalid);
+1 -1
View File
@@ -7,7 +7,7 @@ export type { Cli, CliOptions } from "./create-cli.ts";
// Command routing
export { CommandRegistry } from "./registry.ts";
export type { Command, OptionDef, LocateResult } from "./registry.ts";
export type { Command, FlagDef, LocateResult } from "./registry.ts";
export { resolve } from "./resolve.ts";
export type { Resolution } from "./resolve.ts";
export { compose, type RunContext, type Middleware } from "./middleware.ts";
+3 -3
View File
@@ -1,4 +1,4 @@
import type { Command, Config, GlobalFlags } from "bailian-cli-core";
import type { AnyCommand, Config, GlobalFlags } from "bailian-cli-core";
import { resolveCredential, trackCommandExecution } from "bailian-cli-core";
import { ensureApiKey } from "./utils/ensure-key.ts";
import { maybeShowStatusBar } from "./output/status-bar.ts";
@@ -16,7 +16,7 @@ export interface RunContext {
readonly npmPackage: string;
/** The matched command path, e.g. ["speech","recognize"]. */
readonly path: string[];
readonly command: Command;
readonly command: AnyCommand;
config: Config;
flags: GlobalFlags;
}
@@ -80,4 +80,4 @@ export const versionCheckStage: Middleware = async (ctx, next) => {
};
/** Innermost stage: hand control to the command. */
export const runCommandStage: Middleware = (ctx) => ctx.command.execute(ctx.config, ctx.flags);
export const runCommandStage: Middleware = (ctx) => ctx.command.run(ctx.config, ctx.flags);
@@ -9,6 +9,7 @@ const PIPELINE_FLAGS: GlobalFlags = {
yes: false,
dryRun: false,
help: false,
version: false,
async: false,
};
+36 -20
View File
@@ -1,11 +1,20 @@
import type { Command } from "bailian-cli-core";
import type { AnyCommand, FlagDef } from "bailian-cli-core";
import { UsageError } from "bailian-cli-core";
import { GLOBAL_OPTIONS } from "bailian-cli-core";
import { GLOBAL_FLAGS } from "bailian-cli-core";
import { camelToKebab } from "./args.ts";
export type { Command, OptionDef } from "bailian-cli-core";
export type { Command, AnyCommand, FlagDef, FlagsDef } from "bailian-cli-core";
/** "--max-tokens <count>" for a value flag, "--quiet" for a switch. */
function flagDisplay(key: string, def: FlagDef): string {
const flag = `--${camelToKebab(key)}`;
if (def.type === "switch") return flag;
const hint = def.choices ? `<${def.choices.join("|")}>` : def.valueHint;
return `${flag} ${hint}`;
}
interface CommandNode {
command?: Command;
command?: AnyCommand;
children: Map<string, CommandNode>;
}
@@ -18,7 +27,7 @@ interface CommandNode {
* tokens (no positionals); `error` carries the message + hint.
*/
export type LocateResult =
| { kind: "leaf"; command: Command; matched: string[] }
| { kind: "leaf"; command: AnyCommand; matched: string[] }
| { kind: "group"; matched: string[] }
| { kind: "unknown"; error: UsageError };
@@ -27,14 +36,14 @@ export class CommandRegistry {
/** Binary name shown in usage/help/error strings (e.g. "bl", "rag"). */
private readonly cliName: string;
constructor(commands: Record<string, Command>, cliName: string) {
constructor(commands: Record<string, AnyCommand>, cliName: string) {
this.cliName = cliName;
for (const [path, cmd] of Object.entries(commands)) {
this.register(path, cmd);
}
}
private register(path: string, command: Command): void {
private register(path: string, command: AnyCommand): void {
const parts = path.split(" ");
let node = this.root;
for (const part of parts) {
@@ -46,8 +55,8 @@ export class CommandRegistry {
node.command = command;
}
getAllCommands(): Command[] {
const commands: Command[] = [];
getAllCommands(): AnyCommand[] {
const commands: AnyCommand[] = [];
const traverse = (node: CommandNode) => {
if (node.command) commands.push(node.command);
for (const child of node.children.values()) {
@@ -153,10 +162,12 @@ export class CommandRegistry {
}
private buildGlobalFlagLines(a: (s: string) => string, d: (s: string) => string): string {
const maxLen = Math.max(...GLOBAL_OPTIONS.map((o) => o.flag.length));
return GLOBAL_OPTIONS.map((o) => ` ${a(o.flag.padEnd(maxLen + 2))} ${d(o.description)}`).join(
"\n",
);
const lines = Object.entries(GLOBAL_FLAGS).map(([k, def]) => ({
flag: flagDisplay(k, def),
desc: def.description,
}));
const maxLen = Math.max(...lines.map((l) => l.flag.length));
return lines.map((l) => ` ${a(l.flag.padEnd(maxLen + 2))} ${d(l.desc)}`).join("\n");
}
// Color helpers — no-ops when output is not a TTY
@@ -256,12 +267,12 @@ ${b("Global Flags:")}
${globalFlagLines}
${b("Getting Help:")}
${d("Add --help after any command to see its full list of options, defaults,")}
${d("Add --help after any command to see its full list of flags, defaults,")}
${d("and usage examples. For example:")} ${this.cliName} ${this.helpExample()} --help
`);
}
private printCommandHelp(cmd: Command, commandPath: string[], out: NodeJS.WriteStream): void {
private printCommandHelp(cmd: AnyCommand, 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);
@@ -272,11 +283,16 @@ ${b("Getting Help:")}
out.write(`\n${cmd.description}\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`);
for (const opt of cmd.options) {
out.write(` ${a(opt.flag.padEnd(maxLen + 2))} ${d(opt.description)}\n`);
const flagEntries = Object.entries(cmd.flags ?? {}) as [string, FlagDef][];
if (flagEntries.length > 0) {
const lines = flagEntries.map(([k, def]) => ({
flag: flagDisplay(k, def),
desc: def.description,
}));
const maxLen = Math.max(...lines.map((l) => l.flag.length));
out.write(`\n${b("Flags:")}\n`);
for (const l of lines) {
out.write(` ${a(l.flag.padEnd(maxLen + 2))} ${d(l.desc)}\n`);
}
}
if (cmd.notes && cmd.notes.length > 0) {
+2 -2
View File
@@ -1,4 +1,4 @@
import type { Command } from "bailian-cli-core";
import type { AnyCommand } from "bailian-cli-core";
import { type UsageError } from "bailian-cli-core";
import type { CommandRegistry } from "./registry.ts";
import { parsePath } from "./args.ts";
@@ -15,7 +15,7 @@ import { parsePath } from "./args.ts";
export type Resolution =
| { kind: "version" }
| { kind: "help"; path: string[] }
| { kind: "run"; path: string[]; command: Command; rest: string[] }
| { kind: "run"; path: string[]; command: AnyCommand; rest: string[] }
| { kind: "usageError"; error: UsageError };
/**
+10 -10
View File
@@ -1,16 +1,16 @@
import { expect, test } from "vite-plus/test";
import { ExitCode, GLOBAL_OPTIONS, type OptionDef } from "bailian-cli-core";
import { ExitCode, GLOBAL_FLAGS, type FlagsDef } from "bailian-cli-core";
import { parsePath, parseFlags } from "../src/args.ts";
const IMAGE_GENERATE_OPTIONS: OptionDef[] = [
{ flag: "--prompt <text>", description: "Image description", required: true },
{ flag: "--model <model>", description: "Model ID" },
{ flag: "--image <url>", description: "Image URL (repeatable)", type: "array" },
{ flag: "--n <count>", description: "Number of images", type: "number" },
{ flag: "--watermark <bool>", description: "Watermark", type: "boolean" },
{ flag: "--no-wait", description: "Return immediately", type: "switch" },
];
const OPTS = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
const IMAGE_GENERATE_FLAGS = {
prompt: { type: "string", valueHint: "<text>", description: "Image description", required: true },
model: { type: "string", valueHint: "<model>", description: "Model ID" },
image: { type: "array", valueHint: "<url>", description: "Image URL (repeatable)" },
n: { type: "number", valueHint: "<count>", description: "Number of images" },
watermark: { type: "boolean", valueHint: "<bool>", description: "Watermark" },
noWait: { type: "switch", description: "Return immediately" },
} satisfies FlagsDef;
const OPTS = { ...GLOBAL_FLAGS, ...IMAGE_GENERATE_FLAGS };
// ---- parsePath: routing only (command path first, then flags) ----
+1 -1
View File
@@ -21,7 +21,7 @@ Index: [index.md](index.md)
| **Description** | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) |
| **Usage** | `bl advisor recommend --message <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | -------------------------- |
+14 -14
View File
@@ -22,20 +22,20 @@ Index: [index.md](index.md)
| **Description** | Call a Bailian application (agent or workflow) |
| **Usage** | `bl app call --app-id <id> --prompt <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ---------------------- | ------- | -------- | --------------------------------------------- |
| `--app-id <id>` | string | yes | Application ID (required) |
| `--prompt <text>` | string | yes | Input prompt text |
| `--image <url>` | array | no | Image URL(s) to pass to the app (repeatable) |
| `--file-id <id>` | array | no | Pre-uploaded file ID(s) (repeatable) |
| `--session-id <id>` | string | no | Session ID for multi-turn conversation |
| `--stream` | boolean | no | Stream response (default: on in TTY) |
| `--pipeline-ids <ids>` | string | no | Knowledge base pipeline IDs (comma-separated) |
| `--memory-id <id>` | string | no | Memory ID for long-term memory |
| `--biz-params <json>` | string | no | Business parameters JSON (workflow variables) |
| `--has-thoughts` | boolean | no | Show agent thinking process |
| Flag | Type | Required | Description |
| ---------------------- | ------ | -------- | --------------------------------------------- |
| `--app-id <id>` | string | yes | Application ID (required) |
| `--prompt <text>` | string | yes | Input prompt text |
| `--image <url>` | array | no | Image URL(s) to pass to the app (repeatable) |
| `--file-id <id>` | array | no | Pre-uploaded file ID(s) (repeatable) |
| `--session-id <id>` | string | no | Session ID for multi-turn conversation |
| `--stream` | switch | no | Stream response (default: on in TTY) |
| `--pipeline-ids <ids>` | string | no | Knowledge base pipeline IDs (comma-separated) |
| `--memory-id <id>` | string | no | Memory ID for long-term memory |
| `--biz-params <json>` | string | no | Business parameters JSON (workflow variables) |
| `--has-thoughts` | switch | no | Show agent thinking process |
#### Examples
@@ -71,7 +71,7 @@ bl app call --app-id abc123 --prompt "Start" --biz-params '{"key":"value"}'
| **Description** | List Bailian applications |
| **Usage** | `bl app list [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------- |
+12 -12
View File
@@ -23,13 +23,13 @@ Index: [index.md](index.md)
| **Description** | Authenticate with API key or console browser login (credentials can coexist) |
| **Usage** | `bl auth login --api-key <key> \| --console` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------- | -------- | ------------------------------------------------------------------------------------- |
| `--api-key <key>` | string | no | DashScope API key to store |
| `--base-url <url>` | string | no | DashScope API base URL (used with --api-key for validation) |
| `--console` | boolean | no | Sign in via browser; use --console-site to choose domestic (default) or international |
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------- |
| `--api-key <key>` | string | no | DashScope API key to store |
| `--base-url <url>` | string | no | DashScope API base URL (used with --api-key for validation) |
| `--console` | switch | no | Sign in via browser; use --console-site to choose domestic (default) or international |
#### Examples
@@ -49,12 +49,12 @@ bl auth login --console
| **Description** | Clear stored credentials |
| **Usage** | `bl auth logout [--console] [--yes] [--dry-run]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ----------- | ------- | -------- | -------------------------------------------------------- |
| `--console` | boolean | no | Only clear the console access_token, keep api_key intact |
| `--yes` | boolean | no | Skip confirmation prompt |
| Flag | Type | Required | Description |
| ----------- | ------ | -------- | -------------------------------------------------------- |
| `--console` | switch | no | Only clear the console access_token, keep api_key intact |
| `--yes` | switch | no | Skip confirmation prompt |
#### Examples
@@ -82,7 +82,7 @@ bl auth logout --yes
| **Description** | Show current authentication state |
| **Usage** | `bl auth status` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------- |
+3 -3
View File
@@ -22,7 +22,7 @@ Index: [index.md](index.md)
| **Description** | Set a config value |
| **Usage** | `bl config set --key <key> --value <value>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -51,9 +51,9 @@ bl config set --key base_url --value https://dashscope.aliyuncs.com
| **Description** | Display current configuration |
| **Usage** | `bl config show` |
#### Options
#### Flags
_No command-specific options._
_No command-specific flags._
#### Examples
+1 -1
View File
@@ -21,7 +21,7 @@ Index: [index.md](index.md)
| **Description** | Call a Bailian console API via the CLI gateway |
| **Usage** | `bl console call --api <api> --data <json> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------------------------------------ |
+1 -1
View File
@@ -21,7 +21,7 @@ Index: [index.md](index.md)
| **Description** | Upload a local file to DashScope temporary storage (48h) |
| **Usage** | `bl file upload --file <path> --model <model>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ----------------- | ------ | -------- | ----------------------------------------------- |
+3 -3
View File
@@ -22,7 +22,7 @@ Index: [index.md](index.md)
| **Description** | Edit an existing image with text instructions (Qwen-Image) |
| **Usage** | `bl image edit --image <url> --prompt <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------- | -------- | ----------------------------------------------------------------------- |
@@ -68,7 +68,7 @@ bl image edit --image ./photo.png --prompt "Replace the background with a beach"
| **Description** | Generate images (Qwen-Image / wan2.x) |
| **Usage** | `bl image generate --prompt <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
@@ -80,7 +80,7 @@ bl image edit --image ./photo.png --prompt "Replace the background with a beach"
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag: true for qwen-image sync; parameter omitted on async models (API default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--no-wait` | boolean | no | Return task ID immediately without waiting (async models only) |
| `--no-wait` | switch | no | Return task ID immediately without waiting (async models only) |
| `--out-dir <dir>` | string | no | Download images to directory |
| `--out-prefix <prefix>` | string | no | Filename prefix (default: image) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 3) |
+4 -2
View File
@@ -84,7 +84,7 @@ Use this index for the full quick index and global flags.
## Global flags
Available on every command (in addition to command-specific options):
Available on every command (in addition to command-specific flags):
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | -------------------------------------------------------- |
@@ -92,12 +92,14 @@ Available on every command (in addition to command-specific options):
| `--base-url <url>` | string | no | API base URL |
| `--output <format>` | string | no | Output format: text, json |
| `--timeout <seconds>` | number | no | Request timeout |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--quiet` | switch | no | Suppress non-essential output |
| `--verbose` | switch | no | Print HTTP request/response details |
| `--no-color` | switch | no | Disable ANSI colors |
| `--dry-run` | switch | no | Dry run mode |
| `--non-interactive` | switch | no | Disable interactive prompts |
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
| `--yes` | switch | no | Skip confirmation prompts |
| `--async` | switch | no | Return async task id without waiting |
| `--console-region <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
+16 -16
View File
@@ -21,23 +21,23 @@ Index: [index.md](index.md)
| **Description** | Retrieve from a Bailian knowledge base |
| **Usage** | `bl knowledge retrieve --index-id <id> --query <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------- | ------- | -------- | ------------------------------------------------------------ |
| `--index-id <id>` | string | yes | Knowledge base index ID (required) |
| `--query <text>` | string | yes | Search query (required) |
| `--dense-similarity-top-k <n>` | number | no | Dense retrieval top K |
| `--sparse-similarity-top-k <n>` | number | no | Sparse retrieval top K |
| `--rerank` | boolean | no | Enable reranking |
| `--rerank-top-n <n>` | number | no | Rerank top N results |
| `--rerank-model <name>` | string | no | Rerank model, e.g. qwen3-rerank-hybrid |
| `--rerank-mode <mode>` | string | no | Rerank mode: qa, similar, or custom |
| `--rerank-instruct <text>` | string | no | Custom rerank instruction, when mode=custom |
| `--top-k <n>` | number | no | Number of results (deprecated, use --rerank-top-n) |
| `--workspace-id <id>` | string | no | Bailian workspace ID (only needed for deprecated AK/SK auth) |
| `--access-key-id <key>` | string | no | Deprecated: use global --api-key instead |
| `--access-key-secret <key>` | string | no | Deprecated: use global --api-key instead |
| Flag | Type | Required | Description |
| ------------------------------- | ------ | -------- | ------------------------------------------------------------ |
| `--index-id <id>` | string | yes | Knowledge base index ID (required) |
| `--query <text>` | string | yes | Search query (required) |
| `--dense-similarity-top-k <n>` | number | no | Dense retrieval top K |
| `--sparse-similarity-top-k <n>` | number | no | Sparse retrieval top K |
| `--rerank` | switch | no | Enable reranking |
| `--rerank-top-n <n>` | number | no | Rerank top N results |
| `--rerank-model <name>` | string | no | Rerank model, e.g. qwen3-rerank-hybrid |
| `--rerank-mode <mode>` | string | no | Rerank mode: qa, similar, or custom |
| `--rerank-instruct <text>` | string | no | Custom rerank instruction, when mode=custom |
| `--top-k <n>` | number | no | Number of results (deprecated, use --rerank-top-n) |
| `--workspace-id <id>` | string | no | Bailian workspace ID (only needed for deprecated AK/SK auth) |
| `--access-key-id <key>` | string | no | Deprecated: use global --api-key instead |
| `--access-key-secret <key>` | string | no | Deprecated: use global --api-key instead |
#### Notes
+3 -3
View File
@@ -23,7 +23,7 @@ Index: [index.md](index.md)
| **Description** | Call a tool on an MCP server (tools/call) |
| **Usage** | `bl mcp call --target <server.tool> [--arg k=v ...] [--json '{...}'] [--url <url>]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ---------------------------------------------------------------------------------------- |
@@ -55,7 +55,7 @@ bl mcp call --target market-cmapi00073529.SmartFundSelection --arg riskLevel=R3
| **Description** | List MCP servers activated under your Bailian account |
| **Usage** | `bl mcp list [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------- |
@@ -89,7 +89,7 @@ bl mcp list --output json
| **Description** | List tools exposed by an MCP server (tools/list) |
| **Usage** | `bl mcp tools --server <code> [--url <url>]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ----------------- | ------ | -------- | ------------------------------------------------------- |
+7 -7
View File
@@ -27,7 +27,7 @@ Index: [index.md](index.md)
| **Description** | Add memory from messages or custom content |
| **Usage** | `bl memory add --user-id <id> [--messages <json>] [--content <text>] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ---------------------------------------------------------- |
@@ -59,7 +59,7 @@ bl memory add --user-id user1 --content "Lives in Beijing" --profile-schema sche
| **Description** | Delete a memory node |
| **Usage** | `bl memory delete --node-id <id> --user-id <id>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | --------------------------------------- |
@@ -81,7 +81,7 @@ bl memory delete --node-id node_xxx --user-id user1
| **Description** | List memory nodes for a user |
| **Usage** | `bl memory list --user-id <id> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------ |
@@ -108,7 +108,7 @@ bl memory list --user-id user1 --page-size 20 --page 2
| **Description** | Create a user profile schema for memory profiling |
| **Usage** | `bl memory profile create --name <name> --attributes <json> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ---------------------- | ------ | -------- | ----------------------------------------------------------- |
@@ -130,7 +130,7 @@ bl memory profile create --name "user_basic" --attributes '[{"name":"age","descr
| **Description** | Get user profile by schema ID and user ID |
| **Usage** | `bl memory profile get --schema-id <id> --user-id <id>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------ | ------ | -------- | ---------------------------- |
@@ -151,7 +151,7 @@ bl memory profile get --schema-id schema_xxx --user-id user1
| **Description** | Search memory nodes by query or messages |
| **Usage** | `bl memory search --user-id <id> [--query <text>] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | -------------------------------------------- |
@@ -179,7 +179,7 @@ bl memory search --user-id user1 --messages '[{"role":"user","content":"recommen
| **Description** | Update a memory node content |
| **Usage** | `bl memory update --node-id <id> --user-id <id> --content <text>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| -------------------------- | ------ | -------- | ------------------------------------------ |
+15 -15
View File
@@ -21,22 +21,22 @@ Index: [index.md](index.md)
| **Description** | Multimodal chat with text + audio output (Qwen-Omni) |
| **Usage** | `bl omni --message <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ---------------------- | ------- | -------- | ------------------------------------------------------------------------------------ |
| `--message <text>` | array | yes | Message text (repeatable, prefix role: to set role) |
| `--model <model>` | string | no | Model ID (default: qwen3.5-omni-plus) |
| `--system <text>` | string | no | System prompt |
| `--image <url>` | array | no | Image URL or local file (repeatable) |
| `--audio <url>` | array | no | Audio URL or local file (.wav/.mp3/.amr/.aac/.m4a/.ogg/.3gp/.3gpp) |
| `--video <url>` | array | no | Video file URL / local path, or comma-separated frame URLs |
| `--voice <voice>` | string | no | Output voice (default: Cherry). Options: Chelsie, Cherry, Ethan, Serena, Sunny, Tina |
| `--audio-format <fmt>` | string | no | Audio output format (default: wav) |
| `--audio-out <path>` | string | no | Save audio to file (default: auto-generate) |
| `--text-only` | boolean | no | Output text only, no audio generation |
| `--max-tokens <n>` | number | no | Maximum tokens to generate |
| `--temperature <n>` | number | no | Sampling temperature (0.0, 2.0] |
| Flag | Type | Required | Description |
| ---------------------- | ------ | -------- | ------------------------------------------------------------------------------------ |
| `--message <text>` | array | yes | Message text (repeatable, prefix role: to set role) |
| `--model <model>` | string | no | Model ID (default: qwen3.5-omni-plus) |
| `--system <text>` | string | no | System prompt |
| `--image <url>` | array | no | Image URL or local file (repeatable) |
| `--audio <url>` | array | no | Audio URL or local file (.wav/.mp3/.amr/.aac/.m4a/.ogg/.3gp/.3gpp) |
| `--video <url>` | array | no | Video file URL / local path, or comma-separated frame URLs |
| `--voice <voice>` | string | no | Output voice (default: Cherry). Options: Chelsie, Cherry, Ethan, Serena, Sunny, Tina |
| `--audio-format <fmt>` | string | no | Audio output format (default: wav) |
| `--audio-out <path>` | string | no | Save audio to file (default: auto-generate) |
| `--text-only` | switch | no | Output text only, no audio generation |
| `--max-tokens <n>` | number | no | Maximum tokens to generate |
| `--temperature <n>` | number | no | Sampling temperature (0.0, 2.0] |
#### Examples
+2 -2
View File
@@ -22,7 +22,7 @@ Index: [index.md](index.md)
| **Description** | Run a pipeline workflow definition |
| **Usage** | `bl pipeline run --file <path> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| --------------------- | ------ | -------- | ------------------------------------ |
@@ -63,7 +63,7 @@ bl pipeline run --file workflow.yaml --output json
| **Description** | Validate a pipeline definition without executing |
| **Usage** | `bl pipeline validate --file <path>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| --------------- | ------ | -------- | ------------------------------------ |
+19 -19
View File
@@ -24,7 +24,7 @@ Index: [index.md](index.md)
| **Description** | Check current usage against rate limits |
| **Usage** | `bl quota check [--model <model>] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ----------------------------------------------- |
@@ -64,7 +64,7 @@ bl quota check --output json
| **Description** | View quota change history |
| **Usage** | `bl quota history [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------- |
@@ -105,15 +105,15 @@ bl quota history --output json
| **Description** | View model RPM/TPM rate limits |
| **Usage** | `bl quota list [--model <model>] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------- | -------- | ------------------------------------------- |
| `--model <model>` | string | no | Model name(s), comma-separated |
| `--all` | boolean | no | Show all models, not just self-service ones |
| `--console-region <region>` | string | no | Console region |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID |
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------- |
| `--model <model>` | string | no | Model name(s), comma-separated |
| `--all` | switch | no | Show all models, not just self-service ones |
| `--console-region <region>` | string | no | Console region |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID |
#### Examples
@@ -145,16 +145,16 @@ bl quota list --output json
| **Description** | Request a temporary quota increase |
| **Usage** | `bl quota request --model <model> --tpm <value> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------- | -------- | ------------------------------------- |
| `--model <model>` | string | yes | Model name (required) |
| `--tpm <value>` | string | yes | Target TPM value (required) |
| `--yes` | boolean | no | Skip downgrade confirmation |
| `--console-region <region>` | string | no | Console region |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID |
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------- |
| `--model <model>` | string | yes | Model name (required) |
| `--tpm <value>` | string | yes | Target TPM value (required) |
| `--yes` | switch | no | Skip downgrade confirmation |
| `--console-region <region>` | string | no | Console region |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID |
#### Examples
+6 -6
View File
@@ -21,13 +21,13 @@ Index: [index.md](index.md)
| **Description** | Search the web using DashScope MCP WebSearch service |
| **Usage** | `bl search web --query <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ---------------- | ------- | -------- | -------------------------------------- |
| `--query <text>` | string | no | Search query text |
| `--count <n>` | number | no | Number of search results (default: 10) |
| `--list-tools` | boolean | no | List available MCP tools and exit |
| Flag | Type | Required | Description |
| ---------------- | ------ | -------- | -------------------------------------- |
| `--query <text>` | string | no | Search query text |
| `--count <n>` | number | no | Number of search results (default: 10) |
| `--list-tools` | switch | no | List available MCP tools and exit |
#### Examples
+32 -32
View File
@@ -22,20 +22,20 @@ Index: [index.md](index.md)
| **Description** | Recognize speech from audio files (FunAudio-ASR) |
| **Usage** | `bl speech recognize --url <audio-url> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | ------------------------------------------------------- |
| `--url <url>` | array | yes | Audio file URL or local file path (repeatable, max 100) |
| `--model <model>` | string | no | Model ID (default: fun-asr) |
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja) |
| `--diarization` | boolean | no | Enable automatic speaker diarization |
| `--speaker-count <n>` | number | no | Expected number of speakers (requires --diarization) |
| `--vocabulary-id <id>` | string | no | Hot-word vocabulary ID for improved accuracy |
| `--channel-id <n>` | number | no | Audio channel ID (default: 0) |
| `--out <path>` | string | no | Save full transcription result to JSON file |
| `--no-wait` | boolean | no | Return task ID immediately without polling |
| `--poll-interval <seconds>` | number | no | Polling interval in seconds (default: 2) |
| Flag | Type | Required | Description |
| --------------------------- | ------ | -------- | ------------------------------------------------------- |
| `--url <url>` | array | yes | Audio file URL or local file path (repeatable, max 100) |
| `--model <model>` | string | no | Model ID (default: fun-asr) |
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja) |
| `--diarization` | switch | no | Enable automatic speaker diarization |
| `--speaker-count <n>` | number | no | Expected number of speakers (requires --diarization) |
| `--vocabulary-id <id>` | string | no | Hot-word vocabulary ID for improved accuracy |
| `--channel-id <n>` | number | no | Audio channel ID (default: 0) |
| `--out <path>` | string | no | Save full transcription result to JSON file |
| `--no-wait` | switch | no | Return task ID immediately without polling |
| `--poll-interval <seconds>` | number | no | Polling interval in seconds (default: 2) |
#### Examples
@@ -75,26 +75,26 @@ bl speech recognize --url https://example.com/audio.mp3 --no-wait --quiet
| **Description** | Synthesize speech from text (CosyVoice TTS) |
| **Usage** | `bl speech synthesize --text <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ---------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--text <text>` | string | no | Text to synthesize into speech (or use --text-file) |
| `--text-file <path>` | string | no | Read text from a file instead of --text |
| `--model <model>` | string | no | Model ID (default: cosyvoice-v3-flash). System voices available for cosyvoice-v3-flash |
| `--voice <voice>` | string | no | Voice ID. Use --list-voices to see system voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID |
| `--list-voices` | boolean | no | List available system voices for the selected model and exit |
| `--format <format>` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3) |
| `--sample-rate <rate>` | string | no | Audio sample rate in Hz (e.g. 24000) |
| `--volume <volume>` | string | no | Volume 0-100 (default: 50) |
| `--rate <rate>` | string | no | Speech rate 0.5-2.0 (default: 1.0) |
| `--pitch <pitch>` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) |
| `--seed <seed>` | string | no | Random seed 0-65535 for reproducible synthesis |
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) |
| `--instruction <text>` | string | no | Natural language instruction to control speech style (e.g. "Use a gentle tone") |
| `--enable-ssml` | boolean | no | Enable SSML markup parsing in input text |
| `--out <path>` | string | no | Save audio to file (default: auto-generate in temp dir) |
| `--stream` | boolean | no | Stream raw PCM audio to stdout (pipe to player) |
| Flag | Type | Required | Description |
| -------------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `--text <text>` | string | no | Text to synthesize into speech (or use --text-file) |
| `--text-file <path>` | string | no | Read text from a file instead of --text |
| `--model <model>` | string | no | Model ID (default: cosyvoice-v3-flash). System voices available for cosyvoice-v3-flash |
| `--voice <voice>` | string | no | Voice ID. Use --list-voices to see system voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID |
| `--list-voices` | switch | no | List available system voices for the selected model and exit |
| `--format <mp3\|pcm\|wav\|opus>` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3) |
| `--sample-rate <rate>` | string | no | Audio sample rate in Hz (e.g. 24000) |
| `--volume <volume>` | string | no | Volume 0-100 (default: 50) |
| `--rate <rate>` | string | no | Speech rate 0.5-2.0 (default: 1.0) |
| `--pitch <pitch>` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) |
| `--seed <seed>` | string | no | Random seed 0-65535 for reproducible synthesis |
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) |
| `--instruction <text>` | string | no | Natural language instruction to control speech style (e.g. "Use a gentle tone") |
| `--enable-ssml` | switch | no | Enable SSML markup parsing in input text |
| `--out <path>` | string | no | Save audio to file (default: auto-generate in temp dir) |
| `--stream` | switch | no | Stream raw PCM audio to stdout (pipe to player) |
#### Examples
+14 -14
View File
@@ -21,21 +21,21 @@ Index: [index.md](index.md)
| **Description** | Send a chat completion (OpenAI compatible, DashScope) |
| **Usage** | `bl text chat --message <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------ | ------- | -------- | --------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: qwen3.7-max) |
| `--message <text>` | array | no | Message text (repeatable, prefix role: to set role); or use --messages-file |
| `--messages-file <path>` | string | no | JSON file with messages array (use - for stdin) |
| `--system <text>` | string | no | System prompt |
| `--max-tokens <n>` | number | no | Maximum tokens to generate (default: 4096) |
| `--temperature <n>` | number | no | Sampling temperature (0.0, 2.0] |
| `--top-p <n>` | number | no | Nucleus sampling threshold |
| `--stream` | boolean | no | Stream response tokens (default: on in TTY) |
| `--tool <json-or-path>` | array | no | Tool definition as JSON or file path (repeatable) |
| `--enable-thinking` | boolean | no | Enable thinking/reasoning mode (for qwen3/qwq models) |
| `--thinking-budget <n>` | number | no | Max tokens for thinking (default: 4096) |
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | --------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: qwen3.7-max) |
| `--message <text>` | array | no | Message text (repeatable, prefix role: to set role); or use --messages-file |
| `--messages-file <path>` | string | no | JSON file with messages array (use - for stdin) |
| `--system <text>` | string | no | System prompt |
| `--max-tokens <n>` | number | no | Maximum tokens to generate (default: 4096) |
| `--temperature <n>` | number | no | Sampling temperature (0.0, 2.0] |
| `--top-p <n>` | number | no | Nucleus sampling threshold |
| `--stream` | switch | no | Stream response tokens (default: on in TTY) |
| `--tool <json-or-path>` | array | no | Tool definition as JSON or file path (repeatable) |
| `--enable-thinking` | switch | no | Enable thinking/reasoning mode (for qwen3/qwq models) |
| `--thinking-budget <n>` | number | no | Max tokens for thinking (default: 4096) |
#### Examples
+2 -2
View File
@@ -21,9 +21,9 @@ Index: [index.md](index.md)
| **Description** | Update the CLI to the latest version |
| **Usage** | `bl update` |
#### Options
#### Flags
_No command-specific options._
_No command-specific flags._
#### Examples
+12 -12
View File
@@ -23,7 +23,7 @@ Index: [index.md](index.md)
| **Description** | Query free-tier quota for models (all models if --model is omitted) |
| **Usage** | `bl usage free [--model <model>[,model2,...]] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------------------------------------- |
@@ -72,17 +72,17 @@ bl usage free --model qwen3-max --console-region cn-beijing
| **Description** | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable |
| **Usage** | `bl usage freetier <--model <model>[,model2,...] \| --all> [--off] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------- | -------- | ------------------------------------------- |
| `--model <model>` | string | no | Model name(s), comma-separated for multiple |
| `--all` | boolean | no | Apply to all free-tier models |
| `--on` | boolean | no | Enable auto-stop (default behavior) |
| `--off` | boolean | no | Disable auto-stop |
| `--console-region <region>` | string | no | Console region |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID |
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------- |
| `--model <model>` | string | no | Model name(s), comma-separated for multiple |
| `--all` | switch | no | Apply to all free-tier models |
| `--on` | switch | no | Enable auto-stop (default behavior) |
| `--off` | switch | no | Disable auto-stop |
| `--console-region <region>` | string | no | Console region |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID |
#### Examples
@@ -118,7 +118,7 @@ bl usage freetier --off --all
| **Description** | Query model usage statistics |
| **Usage** | `bl usage stats [--model <model>] [--days <days>] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------------------------ |
+27 -27
View File
@@ -25,7 +25,7 @@ Index: [index.md](index.md)
| **Description** | Download a completed video by task ID |
| **Usage** | `bl video download --task-id <id> --out <path>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ---------------- | ------ | -------- | ------------------------ |
@@ -50,26 +50,26 @@ bl video download --task-id 3b256896-xxxx --out video.mp4 --quiet
| **Description** | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) |
| **Usage** | `bl video edit --video <url> --prompt <text> [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: happyhorse-1.0-video-edit) |
| `--video <url>` | string | yes | Input video URL or local file (mp4/mov, 2-10s) |
| `--prompt <text>` | string | no | Edit instruction (e.g. "Convert the scene to a claymation style") |
| `--ref-image <url>` | string | no | Reference image URL (up to 4, comma-separated) |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4) |
| `--duration <seconds>` | number | no | Output video duration in seconds (2-10) |
| `--audio-setting <mode>` | string | no | Audio: auto (default) or origin (keep original) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--no-wait` | boolean | no | Return task ID immediately without waiting |
| `--async` | boolean | no | Return task ID immediately (agent/CI mode, same as --no-wait) |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 15) |
| Flag | Type | Required | Description |
| -------------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
| `--model <model>` | string | no | Model ID (default: happyhorse-1.0-video-edit) |
| `--video <url>` | string | yes | Input video URL or local file (mp4/mov, 2-10s) |
| `--prompt <text>` | string | no | Edit instruction (e.g. "Convert the scene to a claymation style") |
| `--ref-image <url>` | string | no | Reference image URL (up to 4, comma-separated) |
| `--negative-prompt <text>` | string | no | Negative prompt to exclude unwanted content |
| `--resolution <res>` | string | no | Resolution: 720P or 1080P (default: 1080P) |
| `--ratio <ratio>` | string | no | Aspect ratio (16:9, 9:16, 1:1, 4:3, 3:4) |
| `--duration <seconds>` | number | no | Output video duration in seconds (2-10) |
| `--audio-setting <auto\|origin>` | string | no | Audio: auto (default) or origin (keep original) |
| `--prompt-extend <bool>` | boolean | no | Enable prompt extend (true/false). Omit flag to omit the parameter (DashScope default). |
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--no-wait` | switch | no | Return task ID immediately without waiting |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 15) |
| `--async` | switch | no | Return task ID immediately (agent/CI mode, same as --no-wait) |
#### Examples
@@ -97,7 +97,7 @@ bl video edit --video https://example.com/input.mp4 --prompt "Put clothes on the
| **Description** | Generate a video from text or image (happyhorse-1.0-t2v / happyhorse-1.0-i2v / wan2.6-t2v) |
| **Usage** | `bl video generate --prompt <text> [--image <url>] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
@@ -112,9 +112,9 @@ bl video edit --video https://example.com/input.mp4 --prompt "Put clothes on the
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--no-wait` | boolean | no | Return task ID immediately without waiting |
| `--async` | boolean | no | Return task ID immediately (agent/CI mode, same as --no-wait) |
| `--no-wait` | switch | no | Return task ID immediately without waiting |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 5) |
| `--async` | switch | no | Return task ID immediately (agent/CI mode, same as --no-wait) |
#### Examples
@@ -146,7 +146,7 @@ bl video generate --prompt "A cat playing with a ball" --watermark false
| **Description** | Reference-to-video generation (happyhorse-1.0-r2v / wan2.6-r2v): multi-subject, multi-shot with voice |
| **Usage** | `bl video ref --prompt <text> --image <url>... [--ref-video <url>...] [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| --------------------------- | ------- | -------- | --------------------------------------------------------------------------------------- |
@@ -163,9 +163,9 @@ bl video generate --prompt "A cat playing with a ball" --watermark false
| `--watermark <bool>` | boolean | no | Enable watermark (true/false). Omit flag to use CLI default (true). |
| `--seed <n>` | number | no | Random seed for reproducible generation |
| `--download <path>` | string | no | Save video to file on completion |
| `--no-wait` | boolean | no | Return task ID immediately without waiting |
| `--async` | boolean | no | Return task ID immediately (agent/CI mode, same as --no-wait) |
| `--no-wait` | switch | no | Return task ID immediately without waiting |
| `--poll-interval <seconds>` | number | no | Polling interval when waiting (default: 15) |
| `--async` | switch | no | Return task ID immediately (agent/CI mode, same as --no-wait) |
#### Examples
@@ -197,7 +197,7 @@ bl video ref --prompt "Image 1 drinks water" --image person.jpg --watermark fals
| **Description** | Query async task status |
| **Usage** | `bl video task get --task-id <id>` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ---------------- | ------ | -------- | ------------- |
+1 -1
View File
@@ -21,7 +21,7 @@ Index: [index.md](index.md)
| **Description** | Describe an image or video using Qwen-VL |
| **Usage** | `bl vision describe --image <path-or-url> [--video <url>] [--prompt <text>]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ----------------------- | ------ | -------- | --------------------------------------------------- |
+1 -1
View File
@@ -21,7 +21,7 @@ Index: [index.md](index.md)
| **Description** | List all workspaces |
| **Usage** | `bl workspace list [flags]` |
#### Options
#### Flags
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ------------------------------------- |
+37 -20
View File
@@ -11,7 +11,12 @@
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 {
GLOBAL_FLAGS,
type AnyCommand,
type FlagDef,
type FlagsDef,
} from "../packages/core/dist/index.mjs";
import { commands } from "../packages/cli/src/commands.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -30,17 +35,29 @@ function topLevel(path: string): string {
return path.split(" ")[0]!;
}
function optionType(opt: OptionDef): string {
if (opt.type) return opt.type;
if (!opt.flag.includes("<") && !opt.flag.includes("[")) return "boolean";
return "string";
/** maxTokens → max-tokens. Flags are keyed by camelCase flag name. */
function camelToKebab(str: string): string {
return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
}
function formatOptionsTable(options: OptionDef[] | undefined): string {
if (!options?.length) return "_No command-specific options._\n";
const rows = options.map((o) => {
const req = o.required ? "yes" : "no";
return `| \`${escCell(o.flag)}\` | ${escCell(optionType(o))} | ${req} | ${escCell(o.description)} |`;
/** "--max-tokens <count>" for a value flag, "--quiet" for a switch. */
function flagDisplay(key: string, def: FlagDef): string {
const flag = `--${camelToKebab(key)}`;
if (def.type === "switch") return flag;
const hint = def.choices ? `<${def.choices.join("|")}>` : def.valueHint;
return `${flag} ${hint}`;
}
function flagType(def: FlagDef): string {
return def.type;
}
function formatFlagsTable(flags: FlagsDef | undefined): string {
const entries = Object.entries(flags ?? {});
if (!entries.length) return "_No command-specific flags._\n";
const rows = entries.map(([key, def]) => {
const req = def.type !== "switch" && def.required ? "yes" : "no";
return `| \`${escCell(flagDisplay(key, def))}\` | ${escCell(flagType(def))} | ${req} | ${escCell(def.description)} |`;
});
return [
"| Flag | Type | Required | Description |",
@@ -65,7 +82,7 @@ function formatNotes(notes: string[] | undefined): string {
return notes.map((n) => `- ${n}`).join("\n") + "\n";
}
function commandSection(path: string, cmd: Command): string {
function commandSection(path: string, cmd: AnyCommand): string {
const lines: string[] = [];
lines.push(`### \`bl ${path}\``, "");
lines.push(`| Field | Value |`, `| --- | --- |`);
@@ -76,8 +93,8 @@ function commandSection(path: string, cmd: Command): string {
lines.push(`| **Usage** | \`${escCell(usage)}\` |`);
lines.push("");
lines.push("#### Options", "");
lines.push(formatOptionsTable(cmd.options));
lines.push("#### Flags", "");
lines.push(formatFlagsTable(cmd.flags));
if (cmd.notes?.length) {
lines.push("#### Notes", "");
@@ -90,8 +107,8 @@ function commandSection(path: string, cmd: Command): string {
return lines.join("\n");
}
function groupByTopLevel(entries: [string, Command][]): Map<string, [string, Command][]> {
const groups = new Map<string, [string, Command][]>();
function groupByTopLevel(entries: [string, AnyCommand][]): Map<string, [string, AnyCommand][]> {
const groups = new Map<string, [string, AnyCommand][]>();
for (const entry of entries) {
const key = topLevel(entry[0]);
const list = groups.get(key) ?? [];
@@ -104,7 +121,7 @@ function groupByTopLevel(entries: [string, Command][]): Map<string, [string, Com
return groups;
}
function buildGroupFile(group: string, groupEntries: [string, Command][]): string {
function buildGroupFile(group: string, groupEntries: [string, AnyCommand][]): string {
const lines: string[] = [
`# \`bl ${group}\` commands`,
"",
@@ -131,8 +148,8 @@ function buildGroupFile(group: string, groupEntries: [string, Command][]): strin
}
function buildIndex(
entries: [string, Command][],
groups: Map<string, [string, Command][]>,
entries: [string, AnyCommand][],
groups: Map<string, [string, AnyCommand][]>,
): string {
const lines: string[] = [
"# bailian-cli (`bl`) command reference",
@@ -168,9 +185,9 @@ function buildIndex(
"",
"## Global flags",
"",
"Available on every command (in addition to command-specific options):",
"Available on every command (in addition to command-specific flags):",
"",
formatOptionsTable(GLOBAL_OPTIONS),
formatFlagsTable(GLOBAL_FLAGS),
"",
"## Notes",
"",