bailian-cli-core 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/dist/index.d.mts +900 -0
- package/dist/index.mjs +924 -0
- package/package.json +36 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { basename, join } from "path";
|
|
4
|
+
import { createHash, createHmac, randomUUID } from "crypto";
|
|
5
|
+
//#region src/errors/codes.ts
|
|
6
|
+
const ExitCode = {
|
|
7
|
+
SUCCESS: 0,
|
|
8
|
+
GENERAL: 1,
|
|
9
|
+
USAGE: 2,
|
|
10
|
+
AUTH: 3,
|
|
11
|
+
QUOTA: 4,
|
|
12
|
+
TIMEOUT: 5,
|
|
13
|
+
NETWORK: 6,
|
|
14
|
+
CONTENT_FILTER: 10
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/errors/base.ts
|
|
18
|
+
/**
|
|
19
|
+
* Base error class for the Bailian SDK.
|
|
20
|
+
*
|
|
21
|
+
* Carries an `exitCode` (intended for CLI consumers to translate into a
|
|
22
|
+
* process exit code) and an optional `hint` describing how to recover.
|
|
23
|
+
* SDK consumers may ignore `exitCode` and treat instances as ordinary errors.
|
|
24
|
+
*/
|
|
25
|
+
var BailianError = class extends Error {
|
|
26
|
+
exitCode;
|
|
27
|
+
hint;
|
|
28
|
+
constructor(message, exitCode = ExitCode.GENERAL, hint) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "BailianError";
|
|
31
|
+
this.exitCode = exitCode;
|
|
32
|
+
this.hint = hint;
|
|
33
|
+
}
|
|
34
|
+
toJSON() {
|
|
35
|
+
return { error: {
|
|
36
|
+
code: this.exitCode,
|
|
37
|
+
message: this.message,
|
|
38
|
+
...this.hint ? { hint: this.hint } : {}
|
|
39
|
+
} };
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/errors/api.ts
|
|
44
|
+
function mapApiError(status, body, _url) {
|
|
45
|
+
const apiMsg = body.error?.message || body.message || `HTTP ${status}`;
|
|
46
|
+
const apiCode = body.error?.type || body.code;
|
|
47
|
+
if (status === 401 || status === 403 || apiCode === "InvalidApiKey" || apiCode === "Unauthorized") return new BailianError(`API key rejected (HTTP ${status}). ${apiMsg}`, ExitCode.AUTH, "Verify your API key is valid and not expired.");
|
|
48
|
+
if (status === 429 || apiCode === "Throttling" || apiCode === "Throttling.RateQuota" || apiCode === "Throttling.AllocationQuota") return new BailianError(`Rate limit or quota exceeded. ${apiMsg}`, ExitCode.QUOTA, "Wait a moment and retry, or check your account quota.");
|
|
49
|
+
if (status === 408 || status === 504) return new BailianError(`Request timed out (HTTP ${status}).`, ExitCode.TIMEOUT, "Try increasing the request timeout or retry later.");
|
|
50
|
+
if (apiCode === "InvalidParameter" || apiCode === "BadRequest") return new BailianError(`Invalid parameter: ${apiMsg}`, ExitCode.USAGE);
|
|
51
|
+
if (apiCode === "ModelNotFound" || apiCode === "AccessDenied") return new BailianError(`Model access denied or not found: ${apiMsg}`, ExitCode.AUTH, "Verify the model name and that your account has access to it.");
|
|
52
|
+
if (apiCode === "DataInspectionFailed") return new BailianError(`Content flagged by safety filter: ${apiMsg}`, ExitCode.CONTENT_FILTER);
|
|
53
|
+
return new BailianError(`API error: ${apiMsg} (HTTP ${status}${apiCode ? `, code: ${apiCode}` : ""})`, ExitCode.GENERAL);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/config/schema.ts
|
|
57
|
+
const REGIONS = {
|
|
58
|
+
cn: "https://dashscope.aliyuncs.com",
|
|
59
|
+
us: "https://dashscope-us.aliyuncs.com",
|
|
60
|
+
intl: "https://dashscope-intl.aliyuncs.com"
|
|
61
|
+
};
|
|
62
|
+
const DOCS_HOSTS = {
|
|
63
|
+
cn: "https://help.aliyun.com/zh/model-studio",
|
|
64
|
+
us: "https://help.aliyun.com/zh/model-studio",
|
|
65
|
+
intl: "https://help.aliyun.com/zh/model-studio"
|
|
66
|
+
};
|
|
67
|
+
const BAILIAN_HOST = "https://bailian.cn-beijing.aliyuncs.com";
|
|
68
|
+
const VALID_REGIONS = new Set([
|
|
69
|
+
"cn",
|
|
70
|
+
"us",
|
|
71
|
+
"intl"
|
|
72
|
+
]);
|
|
73
|
+
const VALID_OUTPUTS = new Set(["text", "json"]);
|
|
74
|
+
function parseConfigFile(raw) {
|
|
75
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
76
|
+
const obj = raw;
|
|
77
|
+
const out = {};
|
|
78
|
+
if (typeof obj.api_key === "string") out.api_key = obj.api_key;
|
|
79
|
+
if (typeof obj.region === "string" && VALID_REGIONS.has(obj.region)) out.region = obj.region;
|
|
80
|
+
if (typeof obj.base_url === "string" && obj.base_url.startsWith("http")) out.base_url = obj.base_url;
|
|
81
|
+
if (typeof obj.output === "string" && VALID_OUTPUTS.has(obj.output)) out.output = obj.output;
|
|
82
|
+
if (typeof obj.output_dir === "string" && obj.output_dir.length > 0) out.output_dir = obj.output_dir;
|
|
83
|
+
if (typeof obj.timeout === "number" && obj.timeout > 0) out.timeout = obj.timeout;
|
|
84
|
+
if (typeof obj.default_text_model === "string" && obj.default_text_model.length > 0) out.default_text_model = obj.default_text_model;
|
|
85
|
+
if (typeof obj.default_video_model === "string" && obj.default_video_model.length > 0) out.default_video_model = obj.default_video_model;
|
|
86
|
+
if (typeof obj.default_image_model === "string" && obj.default_image_model.length > 0) out.default_image_model = obj.default_image_model;
|
|
87
|
+
if (typeof obj.default_speech_model === "string" && obj.default_speech_model.length > 0) out.default_speech_model = obj.default_speech_model;
|
|
88
|
+
if (typeof obj.default_omni_model === "string" && obj.default_omni_model.length > 0) out.default_omni_model = obj.default_omni_model;
|
|
89
|
+
if (typeof obj.access_key_id === "string" && obj.access_key_id.length > 0) out.access_key_id = obj.access_key_id;
|
|
90
|
+
if (typeof obj.access_key_secret === "string" && obj.access_key_secret.length > 0) out.access_key_secret = obj.access_key_secret;
|
|
91
|
+
if (typeof obj.workspace_id === "string" && obj.workspace_id.length > 0) out.workspace_id = obj.workspace_id;
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/config/paths.ts
|
|
96
|
+
const CONFIG_DIR_NAME = ".bailian";
|
|
97
|
+
function getConfigDir() {
|
|
98
|
+
if (process.env.BAILIAN_CONFIG_DIR) return process.env.BAILIAN_CONFIG_DIR;
|
|
99
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
100
|
+
}
|
|
101
|
+
function getConfigPath() {
|
|
102
|
+
return join(getConfigDir(), "config.json");
|
|
103
|
+
}
|
|
104
|
+
function getCredentialsPath() {
|
|
105
|
+
return join(getConfigDir(), "credentials.json");
|
|
106
|
+
}
|
|
107
|
+
async function ensureConfigDir() {
|
|
108
|
+
const dir = getConfigDir();
|
|
109
|
+
await (await import("fs/promises")).mkdir(dir, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
mode: 448
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/output/text.ts
|
|
116
|
+
function formatText(data) {
|
|
117
|
+
if (data === null || data === void 0) return "";
|
|
118
|
+
if (typeof data === "string") return data;
|
|
119
|
+
if (typeof data === "number" || typeof data === "boolean") return String(data);
|
|
120
|
+
if (Array.isArray(data)) {
|
|
121
|
+
if (data.length === 0) return "(empty)";
|
|
122
|
+
if (typeof data[0] === "object" && data[0] !== null) return formatTable(data);
|
|
123
|
+
return data.map(String).join("\n");
|
|
124
|
+
}
|
|
125
|
+
if (typeof data === "object") return formatKeyValue(data);
|
|
126
|
+
return String(data);
|
|
127
|
+
}
|
|
128
|
+
function formatKeyValue(obj, indent = 0) {
|
|
129
|
+
const prefix = " ".repeat(indent);
|
|
130
|
+
const lines = [];
|
|
131
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
132
|
+
if (value === null || value === void 0) continue;
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
lines.push(`${prefix}${key}:`);
|
|
135
|
+
for (const item of value) if (typeof item === "object" && item !== null) lines.push(`${prefix} - ${formatKeyValue(item, indent + 4).trimStart()}`);
|
|
136
|
+
else lines.push(`${prefix} - ${String(item)}`);
|
|
137
|
+
} else if (typeof value === "object") {
|
|
138
|
+
lines.push(`${prefix}${key}:`);
|
|
139
|
+
lines.push(formatKeyValue(value, indent + 2));
|
|
140
|
+
} else lines.push(`${prefix}${key}: ${String(value)}`);
|
|
141
|
+
}
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
function formatTable(rows) {
|
|
145
|
+
if (rows.length === 0) return "(empty)";
|
|
146
|
+
const keys = Object.keys(rows[0]);
|
|
147
|
+
const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => String(r[k] ?? "").length)));
|
|
148
|
+
return [
|
|
149
|
+
keys.map((k, i) => k.toUpperCase().padEnd(widths[i])).join(" "),
|
|
150
|
+
widths.map((w) => "-".repeat(w)).join(" "),
|
|
151
|
+
...rows.map((r) => keys.map((k, i) => String(r[k] ?? "").padEnd(widths[i])).join(" "))
|
|
152
|
+
].join("\n");
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/output/json.ts
|
|
156
|
+
function formatJson(data) {
|
|
157
|
+
return JSON.stringify(data, null, 2);
|
|
158
|
+
}
|
|
159
|
+
function formatErrorJson(code, message, hint) {
|
|
160
|
+
return JSON.stringify({ error: {
|
|
161
|
+
code,
|
|
162
|
+
message,
|
|
163
|
+
...hint ? { hint } : {}
|
|
164
|
+
} }, null, 2);
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/output/formatter.ts
|
|
168
|
+
function detectOutputFormat(flagValue) {
|
|
169
|
+
if (flagValue === "json" || flagValue === "text") return flagValue;
|
|
170
|
+
if (!process.stdout.isTTY) return "json";
|
|
171
|
+
return "text";
|
|
172
|
+
}
|
|
173
|
+
function formatOutput(data, format) {
|
|
174
|
+
switch (format) {
|
|
175
|
+
case "json": return formatJson(data);
|
|
176
|
+
case "text": return formatText(data);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/config/loader.ts
|
|
181
|
+
function readConfigFile() {
|
|
182
|
+
const path = getConfigPath();
|
|
183
|
+
if (!existsSync(path)) return {};
|
|
184
|
+
try {
|
|
185
|
+
return parseConfigFile(JSON.parse(readFileSync(path, "utf-8")));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const e = err;
|
|
188
|
+
if (e instanceof SyntaxError || e.message.includes("JSON")) console.warn("Warning: config file is corrupted; using defaults.");
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function writeConfigFile(data) {
|
|
193
|
+
await ensureConfigDir();
|
|
194
|
+
const path = getConfigPath();
|
|
195
|
+
const tmp = path + ".tmp";
|
|
196
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
197
|
+
renameSync(tmp, path);
|
|
198
|
+
}
|
|
199
|
+
function loadConfig(flags) {
|
|
200
|
+
const file = readConfigFile();
|
|
201
|
+
const apiKey = flags.apiKey || void 0;
|
|
202
|
+
const fileApiKey = file.api_key;
|
|
203
|
+
const explicitRegion = flags.region || process.env.DASHSCOPE_REGION || void 0;
|
|
204
|
+
const cachedRegion = file.region;
|
|
205
|
+
const region = explicitRegion || cachedRegion || "cn";
|
|
206
|
+
const baseUrl = flags.baseUrl || process.env.DASHSCOPE_BASE_URL || file.base_url || REGIONS[region] || REGIONS.cn;
|
|
207
|
+
const output = detectOutputFormat(flags.output || process.env.DASHSCOPE_OUTPUT || file.output);
|
|
208
|
+
const envTimeout = process.env.DASHSCOPE_TIMEOUT ? Number(process.env.DASHSCOPE_TIMEOUT) : void 0;
|
|
209
|
+
const validEnvTimeout = envTimeout !== void 0 && Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0;
|
|
210
|
+
const timeout = flags.timeout ?? validEnvTimeout ?? file.timeout ?? 300;
|
|
211
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new BailianError("Timeout must be a positive finite number.", ExitCode.USAGE);
|
|
212
|
+
return {
|
|
213
|
+
apiKey,
|
|
214
|
+
fileApiKey,
|
|
215
|
+
fileRegion: file.region,
|
|
216
|
+
configPath: getConfigPath(),
|
|
217
|
+
region,
|
|
218
|
+
baseUrl,
|
|
219
|
+
output,
|
|
220
|
+
outputDir: file.output_dir || void 0,
|
|
221
|
+
timeout,
|
|
222
|
+
defaultTextModel: file.default_text_model,
|
|
223
|
+
defaultVideoModel: file.default_video_model,
|
|
224
|
+
defaultImageModel: file.default_image_model,
|
|
225
|
+
defaultSpeechModel: file.default_speech_model,
|
|
226
|
+
defaultOmniModel: file.default_omni_model,
|
|
227
|
+
accessKeyId: process.env.ALIBABA_CLOUD_ACCESS_KEY_ID || file.access_key_id || void 0,
|
|
228
|
+
accessKeySecret: process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET || file.access_key_secret || void 0,
|
|
229
|
+
workspaceId: process.env.BAILIAN_WORKSPACE_ID || file.workspace_id || void 0,
|
|
230
|
+
verbose: flags.verbose || process.env.DASHSCOPE_VERBOSE === "1",
|
|
231
|
+
quiet: flags.quiet || false,
|
|
232
|
+
noColor: flags.noColor || process.env.NO_COLOR !== void 0 || !process.stdout.isTTY,
|
|
233
|
+
yes: flags.yes || false,
|
|
234
|
+
dryRun: flags.dryRun || false,
|
|
235
|
+
nonInteractive: flags.nonInteractive || false,
|
|
236
|
+
async: flags.async || false
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/auth/credentials.ts
|
|
241
|
+
function loadApiKeyFromConfig() {
|
|
242
|
+
const path = getConfigPath();
|
|
243
|
+
if (!existsSync(path)) return null;
|
|
244
|
+
try {
|
|
245
|
+
const raw = readFileSync(path, "utf-8");
|
|
246
|
+
const data = JSON.parse(raw);
|
|
247
|
+
if (typeof data.api_key === "string" && data.api_key.length > 0) return data.api_key;
|
|
248
|
+
return null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function saveApiKeyToConfig(apiKey) {
|
|
254
|
+
await ensureConfigDir();
|
|
255
|
+
const path = getConfigPath();
|
|
256
|
+
let existing = {};
|
|
257
|
+
try {
|
|
258
|
+
existing = JSON.parse(readFileSync(path, "utf-8"));
|
|
259
|
+
} catch {}
|
|
260
|
+
existing.api_key = apiKey;
|
|
261
|
+
const tmp = path + ".tmp";
|
|
262
|
+
writeFileSync(tmp, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
|
|
263
|
+
renameSync(tmp, path);
|
|
264
|
+
}
|
|
265
|
+
async function clearApiKey() {
|
|
266
|
+
const path = getConfigPath();
|
|
267
|
+
if (!existsSync(path)) return;
|
|
268
|
+
try {
|
|
269
|
+
const existing = JSON.parse(readFileSync(path, "utf-8"));
|
|
270
|
+
delete existing.api_key;
|
|
271
|
+
const tmp = path + ".tmp";
|
|
272
|
+
writeFileSync(tmp, JSON.stringify(existing, null, 2) + "\n", { mode: 384 });
|
|
273
|
+
renameSync(tmp, path);
|
|
274
|
+
} catch {}
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/auth/resolver.ts
|
|
278
|
+
async function resolveCredential(config) {
|
|
279
|
+
if (config.apiKey) return {
|
|
280
|
+
token: config.apiKey,
|
|
281
|
+
method: "api-key",
|
|
282
|
+
source: "flag"
|
|
283
|
+
};
|
|
284
|
+
if (config.fileApiKey) return {
|
|
285
|
+
token: config.fileApiKey,
|
|
286
|
+
method: "api-key",
|
|
287
|
+
source: "config.json"
|
|
288
|
+
};
|
|
289
|
+
if (process.env.DASHSCOPE_API_KEY) return {
|
|
290
|
+
token: process.env.DASHSCOPE_API_KEY,
|
|
291
|
+
method: "api-key",
|
|
292
|
+
source: "DASHSCOPE_API_KEY"
|
|
293
|
+
};
|
|
294
|
+
throw new BailianError("No credentials found.", ExitCode.AUTH, "Set DASHSCOPE_API_KEY environment variable, pass --api-key, or configure a key.");
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/client/ak-sign.ts
|
|
298
|
+
/**
|
|
299
|
+
* Alibaba Cloud V3 Signature (ROA style) for Bailian Cloud API.
|
|
300
|
+
*
|
|
301
|
+
* Used by Knowledge Base Retrieve API which requires AK/SK authentication
|
|
302
|
+
* instead of Bearer token.
|
|
303
|
+
*
|
|
304
|
+
* Reference: https://help.aliyun.com/document_detail/2712195.html
|
|
305
|
+
*/
|
|
306
|
+
function signRequest(cfg) {
|
|
307
|
+
const method = cfg.method ?? "POST";
|
|
308
|
+
const dateISO = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
309
|
+
const nonce = randomUUID();
|
|
310
|
+
const hashedBody = sha256Hex(cfg.body);
|
|
311
|
+
const headers = {
|
|
312
|
+
host: cfg.host,
|
|
313
|
+
"x-acs-action": cfg.action,
|
|
314
|
+
"x-acs-version": cfg.version,
|
|
315
|
+
"x-acs-date": dateISO,
|
|
316
|
+
"x-acs-signature-nonce": nonce,
|
|
317
|
+
"x-acs-content-sha256": hashedBody,
|
|
318
|
+
"content-type": "application/json"
|
|
319
|
+
};
|
|
320
|
+
const signedHeaderKeys = Object.keys(headers).filter((k) => k === "host" || k === "content-type" || k.startsWith("x-acs-")).sort();
|
|
321
|
+
const canonicalHeaders = signedHeaderKeys.map((k) => `${k}:${headers[k]}`).join("\n") + "\n";
|
|
322
|
+
const signedHeadersStr = signedHeaderKeys.join(";");
|
|
323
|
+
const canonicalRequest = [
|
|
324
|
+
method,
|
|
325
|
+
cfg.pathname,
|
|
326
|
+
"",
|
|
327
|
+
canonicalHeaders,
|
|
328
|
+
signedHeadersStr,
|
|
329
|
+
hashedBody
|
|
330
|
+
].join("\n");
|
|
331
|
+
const algorithm = "ACS3-HMAC-SHA256";
|
|
332
|
+
const stringToSign = `${algorithm}\n${sha256Hex(canonicalRequest)}`;
|
|
333
|
+
const signature = hmacSHA256Hex(cfg.accessKeySecret, stringToSign);
|
|
334
|
+
headers["authorization"] = `${algorithm} Credential=${cfg.accessKeyId},SignedHeaders=${signedHeadersStr},Signature=${signature}`;
|
|
335
|
+
return headers;
|
|
336
|
+
}
|
|
337
|
+
function sha256Hex(data) {
|
|
338
|
+
return createHash("sha256").update(data, "utf8").digest("hex");
|
|
339
|
+
}
|
|
340
|
+
function hmacSHA256Hex(key, data) {
|
|
341
|
+
return createHmac("sha256", key).update(data, "utf8").digest("hex");
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/client/endpoints.ts
|
|
345
|
+
function chatEndpoint(baseUrl) {
|
|
346
|
+
return `${baseUrl}/compatible-mode/v1/chat/completions`;
|
|
347
|
+
}
|
|
348
|
+
function imageEndpoint(baseUrl) {
|
|
349
|
+
return `${baseUrl}/api/v1/services/aigc/image-generation/generation`;
|
|
350
|
+
}
|
|
351
|
+
function imageSyncEndpoint(baseUrl) {
|
|
352
|
+
return `${baseUrl}/api/v1/services/aigc/multimodal-generation/generation`;
|
|
353
|
+
}
|
|
354
|
+
function videoGenerateEndpoint(baseUrl) {
|
|
355
|
+
return `${baseUrl}/api/v1/services/aigc/video-generation/video-synthesis`;
|
|
356
|
+
}
|
|
357
|
+
function taskEndpoint(baseUrl, taskId) {
|
|
358
|
+
return `${baseUrl}/api/v1/tasks/${taskId}`;
|
|
359
|
+
}
|
|
360
|
+
function appCompletionEndpoint(baseUrl, appId) {
|
|
361
|
+
return `${baseUrl}/api/v1/apps/${appId}/completion`;
|
|
362
|
+
}
|
|
363
|
+
function memoryAddEndpoint(baseUrl) {
|
|
364
|
+
return `${baseUrl}/api/v2/apps/memory/add`;
|
|
365
|
+
}
|
|
366
|
+
function memorySearchEndpoint(baseUrl) {
|
|
367
|
+
return `${baseUrl}/api/v2/apps/memory/memory_nodes/search`;
|
|
368
|
+
}
|
|
369
|
+
function memoryListEndpoint(baseUrl) {
|
|
370
|
+
return `${baseUrl}/api/v2/apps/memory/memory_nodes`;
|
|
371
|
+
}
|
|
372
|
+
function memoryNodeEndpoint(baseUrl, nodeId) {
|
|
373
|
+
return `${baseUrl}/api/v2/apps/memory/memory_nodes/${nodeId}`;
|
|
374
|
+
}
|
|
375
|
+
function speechSynthesizeEndpoint(baseUrl) {
|
|
376
|
+
return `${baseUrl}/api/v1/services/audio/tts/SpeechSynthesizer`;
|
|
377
|
+
}
|
|
378
|
+
function speechRecognizeEndpoint(baseUrl) {
|
|
379
|
+
return `${baseUrl}/api/v1/services/audio/asr/transcription`;
|
|
380
|
+
}
|
|
381
|
+
function profileSchemaEndpoint(baseUrl) {
|
|
382
|
+
return `${baseUrl}/api/v2/apps/memory/profile_schemas`;
|
|
383
|
+
}
|
|
384
|
+
function userProfileEndpoint(baseUrl, schemaId) {
|
|
385
|
+
return `${baseUrl}/api/v2/apps/memory/profile_schemas/${schemaId}/profiles`;
|
|
386
|
+
}
|
|
387
|
+
function mcpWebSearchEndpoint(baseUrl) {
|
|
388
|
+
return `${baseUrl}/api/v1/mcps/WebSearch/mcp`;
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/client/headers.ts
|
|
392
|
+
/**
|
|
393
|
+
* Shared HTTP request headers for all outgoing requests.
|
|
394
|
+
*
|
|
395
|
+
* Centralises the `x-dashscope-source-config` header so every fetch call
|
|
396
|
+
* (both via the central http client and the bypass paths) uses the
|
|
397
|
+
* same values from a single source of truth.
|
|
398
|
+
*/
|
|
399
|
+
const CHANNEL = "bailian-cli";
|
|
400
|
+
const TAGS = {
|
|
401
|
+
t1: "public",
|
|
402
|
+
t2: ""
|
|
403
|
+
};
|
|
404
|
+
const SOURCE_CONFIG = JSON.stringify({
|
|
405
|
+
channel: CHANNEL,
|
|
406
|
+
tags: TAGS
|
|
407
|
+
});
|
|
408
|
+
/** Standard tracking headers required on every outbound request. */
|
|
409
|
+
function trackingHeaders() {
|
|
410
|
+
return { "x-dashscope-source-config": SOURCE_CONFIG };
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/client/http.ts
|
|
414
|
+
/**
|
|
415
|
+
* Bailian requires `X-DashScope-OssResourceResolve: enable` on any request whose body
|
|
416
|
+
* references an `oss://` URL (returned by the upload API). Detected automatically here
|
|
417
|
+
* so callers don't need to track it manually.
|
|
418
|
+
*/
|
|
419
|
+
function bodyReferencesOssUrl(body) {
|
|
420
|
+
if (body == null || typeof body !== "object") return false;
|
|
421
|
+
if (body instanceof FormData) return false;
|
|
422
|
+
return JSON.stringify(body).includes("oss://");
|
|
423
|
+
}
|
|
424
|
+
async function request(config, opts) {
|
|
425
|
+
const isFormData = typeof FormData !== "undefined" && opts.body instanceof FormData;
|
|
426
|
+
const headers = {
|
|
427
|
+
"User-Agent": `${config.clientName ?? "bailian-cli-core"}/${config.clientVersion ?? "0.0.0-dev"}`,
|
|
428
|
+
...trackingHeaders(),
|
|
429
|
+
...opts.headers
|
|
430
|
+
};
|
|
431
|
+
if (!isFormData && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
432
|
+
if (opts.async) headers["X-DashScope-Async"] = "enable";
|
|
433
|
+
if (bodyReferencesOssUrl(opts.body)) headers["X-DashScope-OssResourceResolve"] = "enable";
|
|
434
|
+
if (!opts.noAuth) {
|
|
435
|
+
const credential = await resolveCredential(config);
|
|
436
|
+
headers["Authorization"] = `Bearer ${credential.token}`;
|
|
437
|
+
if (config.verbose) {
|
|
438
|
+
console.error(`> ${opts.method ?? "GET"} ${opts.url}`);
|
|
439
|
+
console.error(`> Auth: ${credential.token.slice(0, 8)}...`);
|
|
440
|
+
console.error(`> x-dashscope-source-config: ${SOURCE_CONFIG}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const timeoutMs = (opts.timeout ?? config.timeout) * 1e3;
|
|
444
|
+
const res = await fetch(opts.url, {
|
|
445
|
+
method: opts.method ?? "GET",
|
|
446
|
+
headers,
|
|
447
|
+
body: opts.body ? isFormData ? opts.body : JSON.stringify(opts.body) : void 0,
|
|
448
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
449
|
+
});
|
|
450
|
+
if (config.verbose) {
|
|
451
|
+
console.error(`< ${res.status} ${res.statusText}`);
|
|
452
|
+
const reqId = res.headers.get("x-request-id");
|
|
453
|
+
if (reqId) console.error(`request_id: ${reqId}`);
|
|
454
|
+
}
|
|
455
|
+
if (!res.ok) {
|
|
456
|
+
let body = {};
|
|
457
|
+
try {
|
|
458
|
+
body = await res.json();
|
|
459
|
+
} catch {}
|
|
460
|
+
throw mapApiError(res.status, body, opts.url);
|
|
461
|
+
}
|
|
462
|
+
return res;
|
|
463
|
+
}
|
|
464
|
+
async function requestJson(config, opts) {
|
|
465
|
+
const res = await request(config, opts);
|
|
466
|
+
let data;
|
|
467
|
+
try {
|
|
468
|
+
data = await res.json();
|
|
469
|
+
} catch {
|
|
470
|
+
throw new BailianError(`API returned non-JSON response (${res.headers.get("content-type") || "unknown type"}). Server may be experiencing issues.`, ExitCode.GENERAL);
|
|
471
|
+
}
|
|
472
|
+
if (data.code && typeof data.code === "string" && data.code !== "200" && data.code !== "Success") throw mapApiError(200, { error: {
|
|
473
|
+
message: data.message,
|
|
474
|
+
type: data.code
|
|
475
|
+
} }, opts.url);
|
|
476
|
+
return data;
|
|
477
|
+
}
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/client/mcp.ts
|
|
480
|
+
var McpClient = class {
|
|
481
|
+
baseUrl;
|
|
482
|
+
sessionId;
|
|
483
|
+
nextId = 1;
|
|
484
|
+
config;
|
|
485
|
+
authToken;
|
|
486
|
+
constructor(config, baseUrl) {
|
|
487
|
+
this.config = config;
|
|
488
|
+
this.baseUrl = baseUrl;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Initialize the MCP session. Must be called before any other method.
|
|
492
|
+
*/
|
|
493
|
+
async initialize() {
|
|
494
|
+
const credential = await resolveCredential(this.config);
|
|
495
|
+
this.authToken = credential.token;
|
|
496
|
+
const result = await this.rpc("initialize", {
|
|
497
|
+
protocolVersion: "2025-03-26",
|
|
498
|
+
capabilities: {},
|
|
499
|
+
clientInfo: {
|
|
500
|
+
name: this.config.clientName ?? "bailian-cli-core",
|
|
501
|
+
version: this.config.clientVersion ?? "0.0.0-dev"
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
if (this.config.verbose) {
|
|
505
|
+
console.error(`[MCP] Session initialized: ${this.sessionId ?? "no session"}`);
|
|
506
|
+
console.error(`[MCP] Server: ${JSON.stringify(result)}`);
|
|
507
|
+
}
|
|
508
|
+
await this.notify("notifications/initialized");
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* List available tools from the MCP server.
|
|
512
|
+
*/
|
|
513
|
+
async listTools() {
|
|
514
|
+
return (await this.rpc("tools/list")).tools || [];
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Call a tool on the MCP server.
|
|
518
|
+
*/
|
|
519
|
+
async callTool(name, args) {
|
|
520
|
+
return await this.rpc("tools/call", {
|
|
521
|
+
name,
|
|
522
|
+
arguments: args
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async rpc(method, params) {
|
|
526
|
+
const body = {
|
|
527
|
+
jsonrpc: "2.0",
|
|
528
|
+
id: this.nextId++,
|
|
529
|
+
method,
|
|
530
|
+
...params ? { params } : {}
|
|
531
|
+
};
|
|
532
|
+
const data = await (await this.send(body)).json();
|
|
533
|
+
if (data.error) throw new BailianError(`MCP error (${data.error.code}): ${data.error.message}`, ExitCode.GENERAL);
|
|
534
|
+
return data.result;
|
|
535
|
+
}
|
|
536
|
+
async notify(method, params) {
|
|
537
|
+
const body = {
|
|
538
|
+
jsonrpc: "2.0",
|
|
539
|
+
method,
|
|
540
|
+
...params ? { params } : {}
|
|
541
|
+
};
|
|
542
|
+
await this.send(body);
|
|
543
|
+
}
|
|
544
|
+
async send(body) {
|
|
545
|
+
const headers = {
|
|
546
|
+
"Content-Type": "application/json",
|
|
547
|
+
Accept: "application/json, text/event-stream",
|
|
548
|
+
"User-Agent": `${this.config.clientName ?? "bailian-cli-core"}/${this.config.clientVersion ?? "0.0.0-dev"}`,
|
|
549
|
+
...trackingHeaders()
|
|
550
|
+
};
|
|
551
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
552
|
+
if (this.sessionId) headers["Mcp-Session-Id"] = this.sessionId;
|
|
553
|
+
if (this.config.verbose) {
|
|
554
|
+
console.error(`> POST ${this.baseUrl}`);
|
|
555
|
+
console.error(`> Method: ${body.method}`);
|
|
556
|
+
}
|
|
557
|
+
const timeoutMs = this.config.timeout * 1e3;
|
|
558
|
+
const res = await fetch(this.baseUrl, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers,
|
|
561
|
+
body: JSON.stringify(body),
|
|
562
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
563
|
+
});
|
|
564
|
+
if (this.config.verbose) console.error(`< ${res.status} ${res.statusText}`);
|
|
565
|
+
const sid = res.headers.get("Mcp-Session-Id") || res.headers.get("mcp-session-id");
|
|
566
|
+
if (sid) this.sessionId = sid;
|
|
567
|
+
if (!res.ok) {
|
|
568
|
+
let errMsg = `MCP request failed: ${res.status} ${res.statusText}`;
|
|
569
|
+
try {
|
|
570
|
+
const errBody = await res.text();
|
|
571
|
+
if (errBody) errMsg += ` - ${errBody.slice(0, 500)}`;
|
|
572
|
+
} catch {}
|
|
573
|
+
throw new BailianError(errMsg, ExitCode.GENERAL);
|
|
574
|
+
}
|
|
575
|
+
return res;
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/client/stream.ts
|
|
580
|
+
async function* parseSSE(response) {
|
|
581
|
+
const reader = response.body?.getReader();
|
|
582
|
+
if (!reader) return;
|
|
583
|
+
const decoder = new TextDecoder();
|
|
584
|
+
let buffer = "";
|
|
585
|
+
try {
|
|
586
|
+
while (true) {
|
|
587
|
+
const { done, value } = await reader.read();
|
|
588
|
+
if (done) break;
|
|
589
|
+
buffer += decoder.decode(value, { stream: true });
|
|
590
|
+
const lines = buffer.split("\n");
|
|
591
|
+
buffer = lines.pop() || "";
|
|
592
|
+
let event = {};
|
|
593
|
+
for (const line of lines) {
|
|
594
|
+
if (line === "") {
|
|
595
|
+
if (event.data !== void 0) yield {
|
|
596
|
+
data: event.data,
|
|
597
|
+
event: event.event,
|
|
598
|
+
id: event.id
|
|
599
|
+
};
|
|
600
|
+
event = {};
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (line.startsWith(":")) continue;
|
|
604
|
+
const colonIndex = line.indexOf(":");
|
|
605
|
+
if (colonIndex === -1) continue;
|
|
606
|
+
const field = line.slice(0, colonIndex);
|
|
607
|
+
const value = line.slice(colonIndex + 1).trimStart();
|
|
608
|
+
switch (field) {
|
|
609
|
+
case "data":
|
|
610
|
+
event.data = event.data !== void 0 ? `${event.data}\n${value}` : value;
|
|
611
|
+
break;
|
|
612
|
+
case "event":
|
|
613
|
+
event.event = value;
|
|
614
|
+
break;
|
|
615
|
+
case "id":
|
|
616
|
+
event.id = value;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (buffer.trim() && buffer.includes("data:")) {
|
|
622
|
+
const colonIndex = buffer.indexOf(":");
|
|
623
|
+
if (colonIndex !== -1) yield { data: buffer.slice(colonIndex + 1).trimStart() };
|
|
624
|
+
}
|
|
625
|
+
} finally {
|
|
626
|
+
reader.releaseLock();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/files/upload.ts
|
|
631
|
+
/**
|
|
632
|
+
* Upload local files to DashScope temporary OSS storage.
|
|
633
|
+
*
|
|
634
|
+
* Returns an `oss://` prefixed URL valid for 48 hours.
|
|
635
|
+
* When using this URL in API calls, the request MUST include:
|
|
636
|
+
* X-DashScope-OssResourceResolve: enable
|
|
637
|
+
*/
|
|
638
|
+
const UPLOAD_API = `${REGIONS.cn}/api/v1/uploads`;
|
|
639
|
+
/**
|
|
640
|
+
* Step 1: Fetch the upload policy (presigned credentials) from DashScope.
|
|
641
|
+
*/
|
|
642
|
+
async function getUploadPolicy(apiKey, model) {
|
|
643
|
+
const url = `${UPLOAD_API}?action=getPolicy&model=${encodeURIComponent(model)}`;
|
|
644
|
+
const res = await fetch(url, {
|
|
645
|
+
headers: {
|
|
646
|
+
Authorization: `Bearer ${apiKey}`,
|
|
647
|
+
"Content-Type": "application/json",
|
|
648
|
+
...trackingHeaders()
|
|
649
|
+
},
|
|
650
|
+
signal: AbortSignal.timeout(15e3)
|
|
651
|
+
});
|
|
652
|
+
if (!res.ok) {
|
|
653
|
+
const text = await res.text().catch(() => "");
|
|
654
|
+
throw new BailianError(`Failed to get upload policy (HTTP ${res.status}): ${text}`, ExitCode.GENERAL);
|
|
655
|
+
}
|
|
656
|
+
return (await res.json()).data;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Step 2: Upload the file to OSS using the policy.
|
|
660
|
+
*/
|
|
661
|
+
async function uploadToOSS(policy, filePath) {
|
|
662
|
+
const fileName = basename(filePath);
|
|
663
|
+
const key = `${policy.upload_dir}/${fileName}`;
|
|
664
|
+
const fileData = readFileSync(filePath);
|
|
665
|
+
const form = new FormData();
|
|
666
|
+
form.append("OSSAccessKeyId", policy.oss_access_key_id);
|
|
667
|
+
form.append("Signature", policy.signature);
|
|
668
|
+
form.append("policy", policy.policy);
|
|
669
|
+
form.append("x-oss-object-acl", policy.x_oss_object_acl);
|
|
670
|
+
form.append("x-oss-forbid-overwrite", policy.x_oss_forbid_overwrite);
|
|
671
|
+
form.append("key", key);
|
|
672
|
+
form.append("success_action_status", "200");
|
|
673
|
+
form.append("file", new Blob([fileData]), fileName);
|
|
674
|
+
const res = await fetch(policy.upload_host, {
|
|
675
|
+
method: "POST",
|
|
676
|
+
headers: { ...trackingHeaders() },
|
|
677
|
+
body: form,
|
|
678
|
+
signal: AbortSignal.timeout(12e4)
|
|
679
|
+
});
|
|
680
|
+
if (!res.ok) {
|
|
681
|
+
const text = await res.text().catch(() => "");
|
|
682
|
+
throw new BailianError(`Failed to upload file to OSS (HTTP ${res.status}): ${text}`, ExitCode.GENERAL);
|
|
683
|
+
}
|
|
684
|
+
return `oss://${key}`;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Upload a local file to DashScope temporary storage and return the oss:// URL.
|
|
688
|
+
* The URL is valid for 48 hours.
|
|
689
|
+
*/
|
|
690
|
+
async function uploadFile(opts) {
|
|
691
|
+
const { apiKey, model, filePath } = opts;
|
|
692
|
+
if (!existsSync(filePath)) throw new BailianError(`File not found: ${filePath}`, ExitCode.USAGE);
|
|
693
|
+
if (!statSync(filePath).isFile()) throw new BailianError(`Not a file: ${filePath}`, ExitCode.USAGE);
|
|
694
|
+
return uploadToOSS(await getUploadPolicy(apiKey, model), filePath);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Check if a string looks like a local file path (not a URL).
|
|
698
|
+
*/
|
|
699
|
+
function isLocalFile(input) {
|
|
700
|
+
if (input.startsWith("http://") || input.startsWith("https://")) return false;
|
|
701
|
+
if (input.startsWith("oss://")) return false;
|
|
702
|
+
if (input.startsWith("data:")) return false;
|
|
703
|
+
return existsSync(input);
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Resolve a file argument: if it's a local path, upload it and return the oss:// URL.
|
|
707
|
+
* If it's already a URL, return as-is.
|
|
708
|
+
*/
|
|
709
|
+
async function resolveFileUrl(input, apiKey, model) {
|
|
710
|
+
if (!isLocalFile(input)) return input;
|
|
711
|
+
return uploadFile({
|
|
712
|
+
apiKey,
|
|
713
|
+
model,
|
|
714
|
+
filePath: input
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/types/command.ts
|
|
719
|
+
function defineCommand(spec) {
|
|
720
|
+
return {
|
|
721
|
+
name: spec.name,
|
|
722
|
+
description: spec.description,
|
|
723
|
+
usage: spec.usage,
|
|
724
|
+
options: spec.options,
|
|
725
|
+
examples: spec.examples,
|
|
726
|
+
apiDocs: spec.apiDocs,
|
|
727
|
+
execute: spec.run
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/** Global flags shared by all commands — drives the parser's type resolution. */
|
|
731
|
+
const GLOBAL_OPTIONS = [
|
|
732
|
+
{
|
|
733
|
+
flag: "--api-key <key>",
|
|
734
|
+
description: "API key"
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
flag: "--region <region>",
|
|
738
|
+
description: "API region: cn (default), us, intl"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
flag: "--base-url <url>",
|
|
742
|
+
description: "API base URL"
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
flag: "--output <format>",
|
|
746
|
+
description: "Output format: text, json"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
flag: "--timeout <seconds>",
|
|
750
|
+
description: "Request timeout",
|
|
751
|
+
type: "number"
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
flag: "--quiet",
|
|
755
|
+
description: "Suppress non-essential output"
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
flag: "--verbose",
|
|
759
|
+
description: "Print HTTP request/response details"
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
flag: "--no-color",
|
|
763
|
+
description: "Disable ANSI colors"
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
flag: "--dry-run",
|
|
767
|
+
description: "Dry run mode"
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
flag: "--non-interactive",
|
|
771
|
+
description: "Disable interactive prompts"
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
flag: "--concurrent <n>",
|
|
775
|
+
description: "Run N parallel requests (default: 1)",
|
|
776
|
+
type: "number"
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
flag: "--help",
|
|
780
|
+
description: "Show help"
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
flag: "--version",
|
|
784
|
+
description: "Print version"
|
|
785
|
+
}
|
|
786
|
+
];
|
|
787
|
+
//#endregion
|
|
788
|
+
//#region src/utils/filename.ts
|
|
789
|
+
/**
|
|
790
|
+
* 生成文件名前缀
|
|
791
|
+
* @param prefix prompt的前10个字符
|
|
792
|
+
* @param suffix timestamp
|
|
793
|
+
* @returns
|
|
794
|
+
*/
|
|
795
|
+
function sanitizeFilenamePart(input, fallback) {
|
|
796
|
+
return input.normalize("NFKC").replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "") || fallback;
|
|
797
|
+
}
|
|
798
|
+
function generateFilename(prefix, prompt) {
|
|
799
|
+
return `${sanitizeFilenamePart(prefix || "image", "image")}_${sanitizeFilenamePart((prompt || "").substring(0, 20), "untitled")}_${Date.now()}`;
|
|
800
|
+
}
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/utils/output-dir.ts
|
|
803
|
+
const DEFAULT_OUTPUT_DIR = () => join(homedir(), "bailian-output");
|
|
804
|
+
/**
|
|
805
|
+
* Resolve the output directory for generated files.
|
|
806
|
+
*
|
|
807
|
+
* Priority:
|
|
808
|
+
* 1. User-specified dir (e.g. --out-dir flag)
|
|
809
|
+
* 2. Config file output_dir
|
|
810
|
+
* 3. Default: ~/bailian-output/
|
|
811
|
+
*
|
|
812
|
+
* Optionally appends a subdirectory (e.g. 'images', 'videos', 'speech').
|
|
813
|
+
* Creates the directory if it doesn't exist.
|
|
814
|
+
*/
|
|
815
|
+
function resolveOutputDir(config, options) {
|
|
816
|
+
const base = options?.flagDir || config.outputDir || DEFAULT_OUTPUT_DIR();
|
|
817
|
+
const dir = options?.subDir ? join(base, options.subDir) : base;
|
|
818
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
819
|
+
return dir;
|
|
820
|
+
}
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/utils/schema.ts
|
|
823
|
+
/**
|
|
824
|
+
* Parse a CLI flag string (e.g. "--prompt <text>", "--stream") into
|
|
825
|
+
* a parameter name and inferred type.
|
|
826
|
+
*/
|
|
827
|
+
function parseFlag(flag) {
|
|
828
|
+
const match = flag.match(/^--([a-zA-Z0-9-]+)/);
|
|
829
|
+
const kebabName = match ? match[1] : "";
|
|
830
|
+
const name = kebabName.replace(/-([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
|
|
831
|
+
let inferredType = "string";
|
|
832
|
+
let isArray = false;
|
|
833
|
+
if (!flag.includes("<") && !flag.includes("[")) inferredType = "boolean";
|
|
834
|
+
else if (flag.includes("<n>") || flag.includes("<hz>") || flag.includes("<bps>") || flag.includes("<count>")) inferredType = "number";
|
|
835
|
+
if (flag.toLowerCase().includes("repeatable")) isArray = true;
|
|
836
|
+
return {
|
|
837
|
+
name,
|
|
838
|
+
kebabName,
|
|
839
|
+
inferredType,
|
|
840
|
+
isArray
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
function generateToolSchema(cmd) {
|
|
844
|
+
const schema = {
|
|
845
|
+
name: `bailian_${cmd.name.replace(/ /g, "_")}`,
|
|
846
|
+
description: cmd.description,
|
|
847
|
+
input_schema: {
|
|
848
|
+
type: "object",
|
|
849
|
+
properties: {},
|
|
850
|
+
required: []
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
if (cmd.options) for (const opt of cmd.options) {
|
|
854
|
+
const { name, inferredType, isArray } = parseFlag(opt.flag);
|
|
855
|
+
if (!name) continue;
|
|
856
|
+
const explicitType = opt.type;
|
|
857
|
+
const effectiveType = isArray ? "array" : explicitType ?? inferredType;
|
|
858
|
+
const propSchema = { description: opt.description };
|
|
859
|
+
if (effectiveType === "array") {
|
|
860
|
+
propSchema.type = "array";
|
|
861
|
+
propSchema.items = { type: "string" };
|
|
862
|
+
} else propSchema.type = effectiveType;
|
|
863
|
+
const inputSchema = schema.input_schema;
|
|
864
|
+
inputSchema.properties[name] = propSchema;
|
|
865
|
+
if (opt.required) inputSchema.required.push(name);
|
|
866
|
+
}
|
|
867
|
+
return schema;
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/utils/token.ts
|
|
871
|
+
function maskToken(token) {
|
|
872
|
+
return token.length > 8 ? `${token.slice(0, 4)}...${token.slice(-4)}` : "***";
|
|
873
|
+
}
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/utils/env.ts
|
|
876
|
+
/**
|
|
877
|
+
* Environment detection utilities for bailian-cli.
|
|
878
|
+
*
|
|
879
|
+
* Used to determine whether the CLI is running in an interactive terminal
|
|
880
|
+
* (human user) or in a non-interactive environment (CI, agent, pipe, etc.),
|
|
881
|
+
* so commands can adjust their behavior accordingly.
|
|
882
|
+
*/
|
|
883
|
+
/**
|
|
884
|
+
* Detects whether the current environment is interactive.
|
|
885
|
+
*
|
|
886
|
+
* Returns false when:
|
|
887
|
+
* - stdout or stdin is not a TTY
|
|
888
|
+
* - The --non-interactive flag was explicitly set
|
|
889
|
+
* - The process is running in a known CI environment (CI env var present)
|
|
890
|
+
*
|
|
891
|
+
* Returns true when stdout and stdin are both TTYs and --non-interactive
|
|
892
|
+
* was not passed.
|
|
893
|
+
*/
|
|
894
|
+
function isInteractive(options) {
|
|
895
|
+
if (options?.nonInteractive === true) return false;
|
|
896
|
+
if (process.env.CI) return false;
|
|
897
|
+
return process.stdout.isTTY === true && process.stdin.isTTY === true;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Detects whether the current process is running in a CI environment.
|
|
901
|
+
*/
|
|
902
|
+
function isCI() {
|
|
903
|
+
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.JENKINS_URL || process.env.TRAVIS || process.env.CIRCLECI);
|
|
904
|
+
}
|
|
905
|
+
//#endregion
|
|
906
|
+
//#region src/utils/object.ts
|
|
907
|
+
/**
|
|
908
|
+
* Generic object-cleaning utilities.
|
|
909
|
+
*/
|
|
910
|
+
/**
|
|
911
|
+
* Remove all keys whose value is `undefined` from a plain object (in-place).
|
|
912
|
+
* Returns the same reference for chaining convenience.
|
|
913
|
+
*
|
|
914
|
+
* ```ts
|
|
915
|
+
* const params = { a: 1, b: undefined };
|
|
916
|
+
* stripUndefined(params); // { a: 1 }
|
|
917
|
+
* ```
|
|
918
|
+
*/
|
|
919
|
+
function stripUndefined(obj) {
|
|
920
|
+
for (const key of Object.keys(obj)) if (obj[key] === void 0) delete obj[key];
|
|
921
|
+
return obj;
|
|
922
|
+
}
|
|
923
|
+
//#endregion
|
|
924
|
+
export { BAILIAN_HOST, BailianError, CHANNEL, DOCS_HOSTS, ExitCode, GLOBAL_OPTIONS, McpClient, REGIONS, SOURCE_CONFIG, TAGS, appCompletionEndpoint, chatEndpoint, clearApiKey, defineCommand, detectOutputFormat, ensureConfigDir, formatErrorJson, formatJson, formatKeyValue, formatOutput, formatTable, formatText, generateFilename, generateToolSchema, getConfigDir, getConfigPath, getCredentialsPath, imageEndpoint, imageSyncEndpoint, isCI, isInteractive, isLocalFile, loadApiKeyFromConfig, loadConfig, mapApiError, maskToken, mcpWebSearchEndpoint, memoryAddEndpoint, memoryListEndpoint, memoryNodeEndpoint, memorySearchEndpoint, parseConfigFile, parseSSE, profileSchemaEndpoint, readConfigFile, request, requestJson, resolveCredential, resolveFileUrl, resolveOutputDir, saveApiKeyToConfig, signRequest, speechRecognizeEndpoint, speechSynthesizeEndpoint, stripUndefined, taskEndpoint, trackingHeaders, uploadFile, userProfileEndpoint, videoGenerateEndpoint, writeConfigFile };
|