Skip to content

Commit 9fb388b

Browse files
committed
Merge branch 'main' of github.com:modelstudioai/cli into feat/multi-channel-install
2 parents 45d4688 + ed81178 commit 9fb388b

26 files changed

Lines changed: 5719 additions & 217 deletions

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
66

77
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
88

9+
## [1.13.0] - 2026-07-30
10+
11+
### Added
12+
13+
- **`bl config ui` Skills / MCP / Agents / Assets inventory** — browse installed skills, MCP servers, coding agents, and generated assets in the local Web UI with click-to-open detail drawers:
14+
- Skills: render `SKILL.md` as Markdown (GFM tables supported), show local vs remote origin badges, and install a skill by uploading a `.zip` archive into any supported agent's skills root.
15+
- MCP: view and edit JSON configuration with secret masking and mask-preserving writes; create, update, and delete MCP entries across Claude Code, Qwen Code, OpenCode, Cursor, Windsurf, Gemini, Qoder Work, OpenClaw, and Claude Desktop.
16+
- Agents: quick-launch coding agents directly from the UI (gated on the CLI binary being on PATH).
17+
- Assets: categorized, time-sorted browser with preview, open-locally, and delete.
18+
- **Model catalog suggestion chips** — per-category model names surfaced as click-to-fill chips under each `default_*_model` field in the config UI.
19+
- **Profiles tile grid** — profiles displayed as a tile grid with an add-tile and a design-consistent new-profile modal.
20+
21+
### Changed
22+
23+
- Config UI layout: collapsible grouped sidebar with icons and persistent state, responsive breakpoint, wider main area, sticky view headers, and right-side drawers for editing.
24+
25+
### Fixed
26+
27+
- Symlinked skill directories are now correctly identified as an installed source.
28+
- Config file detection now supports environment-variable-based paths and legacy configuration schemes.
29+
930
## [1.12.0] - 2026-07-28
1031

1132
### Added

CHANGELOG.zh.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@
66

77
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
88

9+
## [1.13.0] - 2026-07-30
10+
11+
### 新增
12+
13+
- **`bl config ui` 技能 / MCP / 代理 / 资产清单** — 在本地 Web UI 中浏览已安装的技能、MCP 服务器、编码代理和生成的资产,点击打开右侧详情抽屉:
14+
- 技能:将 `SKILL.md` 渲染为 Markdown(支持 GFM 表格),展示本地/远程来源徽章,支持上传 `.zip` 压缩包将技能安装到任意受支持代理的技能目录。
15+
- MCP:查看和编辑 JSON 配置,支持密钥掩码与掩码保真写回;支持在 Claude Code、Qwen Code、OpenCode、Cursor、Windsurf、Gemini、Qoder Work、OpenClaw 和 Claude Desktop 中创建、更新、删除 MCP 条目。
16+
- 代理:从 UI 一键启动编码代理(需对应 CLI 二进制在 PATH 中)。
17+
- 资产:按类别分组、按时间排序的浏览器,支持预览、本地打开和删除。
18+
- **模型目录建议芯片** — 在配置 UI 的每个 `default_*_model` 字段下方展示按类别分组的模型名称,点击即可填入。
19+
- **Profile 磁贴网格** — 配置文件以磁贴网格展示,新增添加磁贴和设计一致的新建 Profile 弹窗。
20+
21+
### 变更
22+
23+
- 配置 UI 布局:可折叠分组侧边栏(带图标和持久化状态)、响应式断点、更宽的主区域、吸顶视图标题、右侧抽屉式编辑。
24+
25+
### 修复
26+
27+
- 修复软链接技能目录未被正确识别为已安装来源的问题。
28+
- 配置文件检测现支持基于环境变量的路径和旧版配置方案。
29+
930
## [1.12.0] - 2026-07-28
1031

1132
### 新增

docs/agents/config-profile-change.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
- `config list` 标识所有 Profile 与当前激活项。
4747
- `config show``auth status` 只输出本次最终选择的 `config``config_file`,不重复携带激活状态。
4848
- `config ui` 从持久化元数据读取激活项,提供显式激活操作,并在删除激活项后刷新为 `default`
49-
- `config ui` 保存时只替换 UI 管理的字段;Profile 中未展示但仍属于 `ConfigFile` 的合法字段必须保留,不能因打开并保存 UI 而丢失。
49+
- `config ui` 展示并可编辑完整 `ConfigFile`(含 `console_*``telemetry`),保存时按类型(数字/布尔/枚举)归一化写回;`config set` 仍只暴露较窄的 `VALID_KEYS`。UI 未管理的顶层元数据(如 `active_config`)不进入 Profile block,仍由写盘逻辑单独保留。
50+
- `config ui` 只读展示本地 agent 生态:Skills 跨全部 agent skill 目录(`~/.agents/skills` 及各 agent 的 `skills/`,含软链接)按 id 聚合并标注安装来源;MCP、Agents 从各 agent 本地配置读取。
51+
- `config ui` 提供 Assets 资产管理:扫描 `output_dir`(默认 `~/bailian-output`)下的 `images/videos/speech/omni` 分类及根目录散落文件,按分类与生成时间(mtime)标记,支持按分类筛选、内联预览(图/视频/音频)与删除单个文件;文件读取与删除均通过限定在输出目录内的路径校验(防目录穿越)。
5052
- 同步 E2E topic routes、Skill setup 和自动生成 reference。
5153

5254
## 6. 最小测试矩阵
@@ -62,7 +64,8 @@
6264
`--config default` 成功后切回 `default`
6365
- Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。
6466
- `config list/show/use/ui``auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
65-
- `config ui` 覆盖保存时保留未管理字段,并继续允许空值清除 UI 管理字段。
67+
- `config ui` 覆盖保存时保留顶层元数据(如 `active_config`),继续允许空值清除字段,并覆盖 `console_*`/`telemetry` 的类型归一化与枚举校验。
68+
- Assets:`listAssets` 覆盖分类归类、时间倒序、目录缺失返回空;`resolveAssetPath` 覆盖目录穿越拦截;`contentType` 覆盖常见扩展名映射。
6669

6770
## 7. 完成检查
6871

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli",
3-
"version": "1.12.0",
3+
"version": "1.13.0",
44
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
55
"keywords": [
66
"agent",

packages/commands/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli-commands",
3-
"version": "1.12.0",
3+
"version": "1.13.0",
44
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
55
"homepage": "https://bailian.console.aliyun.com/cli",
66
"bugs": {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { maskToken, type AuthStore, type Identity, type Settings } from "bailian-cli-core";
2+
import { runConsoleLogin, resolveConsoleOrigin } from "./login-console.ts";
3+
4+
/** Read-only auth snapshot the config UI account widget renders. bl stores no
5+
* user profile (name/avatar), so this exposes only which credential domains
6+
* resolve, the console region/site, and a masked token. */
7+
export interface AuthUiStatus {
8+
authenticated: boolean;
9+
methods: { apiKey: boolean; console: boolean; openapi: boolean };
10+
primary: "console" | "apiKey" | "openapi" | null;
11+
region?: string;
12+
site?: "domestic" | "international";
13+
masked?: string;
14+
}
15+
16+
/**
17+
* The auth capability surface the config UI is allowed to use. All `authStore`
18+
* access is kept inside this module (commands/auth/**), which the lint boundary
19+
* permits; commands/config/** consumes only this opaque bridge and never
20+
* touches `authStore` directly.
21+
*/
22+
export interface AuthUiBridge {
23+
status(): AuthUiStatus;
24+
/** Start browser-based console login (fire-and-forget; UI polls status). */
25+
startConsoleLogin(): void;
26+
/** Clear all stored credentials. Returns whether anything changed. */
27+
logout(): Promise<boolean>;
28+
}
29+
30+
/** Build the bridge from a command context (identity/settings/authStore). */
31+
export function makeAuthUiBridge(ctx: {
32+
identity: Identity;
33+
settings: Settings;
34+
authStore: AuthStore;
35+
}): AuthUiBridge {
36+
const { identity, settings, authStore } = ctx;
37+
return {
38+
status() {
39+
const a = authStore.describe();
40+
const methods = { apiKey: !!a.apiKey, console: !!a.console, openapi: !!a.openapi };
41+
let masked: string | undefined;
42+
if (a.console) masked = maskToken(a.console.token);
43+
else if (a.apiKey) masked = maskToken(a.apiKey.token);
44+
else if (a.openapi) masked = maskToken(a.openapi.accessKeyId);
45+
const primary = a.console ? "console" : a.apiKey ? "apiKey" : a.openapi ? "openapi" : null;
46+
return {
47+
authenticated: methods.apiKey || methods.console || methods.openapi,
48+
methods,
49+
primary,
50+
region: a.console?.region,
51+
site: a.console?.site,
52+
masked,
53+
};
54+
},
55+
startConsoleLogin() {
56+
const origin = resolveConsoleOrigin(authStore.describe().console?.site);
57+
// Mirror the CLI (`bl auth login --console`): request an api_key from the
58+
// console only when one isn't already stored, so a first console login in
59+
// the config UI also provisions the model api_key (not just access_token).
60+
const hasApiKey = !!authStore.stored().apiKey;
61+
// runConsoleLogin opens the browser and runs its own callback server
62+
// (up to 15 min). We don't await it — the config UI polls the status
63+
// endpoint to detect completion. Errors are logged, not surfaced.
64+
void runConsoleLogin(
65+
origin,
66+
{ identity, settings, authStore },
67+
{
68+
needApiKey: !hasApiKey,
69+
},
70+
).catch((err: unknown) => {
71+
const msg = err instanceof Error ? err.message : String(err);
72+
process.stderr.write(`console login failed: ${msg}\n`);
73+
});
74+
},
75+
logout() {
76+
return authStore.logout("all");
77+
},
78+
};
79+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Best-effort local launcher for coding-agent CLIs surfaced in the config UI.
3+
*
4+
* The command for each agent is taken from a fixed allowlist keyed by the
5+
* agent id, so no user-controlled string is ever executed. Every child process
6+
* is spawned via `execFile` (array args, no shell) to avoid injection.
7+
*/
8+
import { execFile } from "node:child_process";
9+
10+
/** Fixed allowlist: agent id -> launch binary. Keys match `AGENT_PROBES` ids. */
11+
export const AGENT_COMMANDS: Record<string, string> = {
12+
"claude-code": "claude",
13+
"qwen-code": "qwen",
14+
opencode: "opencode",
15+
openclaw: "openclaw",
16+
hermes: "hermes",
17+
codex: "codex",
18+
};
19+
20+
/** The launch binary for a known agent id, or undefined when unknown. */
21+
export function agentCommand(id: string): string | undefined {
22+
return Object.prototype.hasOwnProperty.call(AGENT_COMMANDS, id) ? AGENT_COMMANDS[id] : undefined;
23+
}
24+
25+
/**
26+
* Per-agent argv that passes an initial task prompt while keeping the agent
27+
* interactive in the terminal. Only verified contracts are listed; an agent
28+
* absent here cannot be dispatched a prompt (its bare launch still works).
29+
* - qwen-code: `qwen -i "<prompt>"` (execute prompt, stay interactive)
30+
* - claude-code: `claude "<prompt>"` (positional initial prompt)
31+
* - codex: `codex "<prompt>"` (positional initial prompt)
32+
*/
33+
const AGENT_PROMPT_ARGV: Record<string, (prompt: string) => string[]> = {
34+
"qwen-code": (p) => ["-i", p],
35+
"claude-code": (p) => [p],
36+
codex: (p) => [p],
37+
};
38+
39+
/** Whether a known agent supports being dispatched an initial task prompt. */
40+
export function agentSupportsPrompt(id: string): boolean {
41+
return Object.prototype.hasOwnProperty.call(AGENT_PROMPT_ARGV, id);
42+
}
43+
44+
/** Resolve whether a binary is reachable on PATH (via `which`/`where`). */
45+
function onPath(bin: string): Promise<boolean> {
46+
const cmd = process.platform === "win32" ? "where" : "which";
47+
return new Promise((resolve) => {
48+
execFile(cmd, [bin], { windowsHide: true }, (err) => resolve(!err));
49+
});
50+
}
51+
52+
/**
53+
* Whether a known agent can actually be quick-launched right now: its id maps to
54+
* a launch binary and that binary is reachable on PATH. Unknown ids resolve to
55+
* false. Used to gate the UI's Quick launch button so "Connected" agents whose
56+
* CLI is not installed do not offer a launch that would immediately fail.
57+
*/
58+
export function agentLaunchable(id: string): Promise<boolean> {
59+
const command = agentCommand(id);
60+
if (!command) return Promise.resolve(false);
61+
return onPath(command);
62+
}
63+
64+
/** Single-quote a path for a POSIX shell command line. */
65+
function shQuote(p: string): string {
66+
return `'${p.replace(/'/g, "'\\''")}'`;
67+
}
68+
69+
/** Open a new OS terminal window that cd's into `cwd` and runs `command`. */
70+
function spawnTerminal(command: string, cwd: string): Promise<void> {
71+
const platform = process.platform;
72+
return new Promise((resolve, reject) => {
73+
if (platform === "darwin") {
74+
const inner = `cd ${shQuote(cwd)} && ${command}`;
75+
const escaped = inner.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
76+
const args = [
77+
"-e",
78+
`tell application "Terminal" to do script "${escaped}"`,
79+
"-e",
80+
'tell application "Terminal" to activate',
81+
];
82+
execFile("osascript", args, { windowsHide: true }, (err) => (err ? reject(err) : resolve()));
83+
return;
84+
}
85+
if (platform === "win32") {
86+
const args = ["/c", "start", "", "cmd", "/k", `cd /d ${cwd} && ${command}`];
87+
execFile("cmd", args, { windowsHide: true }, (err) => (err ? reject(err) : resolve()));
88+
return;
89+
}
90+
// Linux / other: best-effort via the distro's default terminal emulator.
91+
const inner = `cd ${shQuote(cwd)} && ${command}; exec $SHELL`;
92+
execFile("x-terminal-emulator", ["-e", "bash", "-lc", inner], { windowsHide: true }, (err) =>
93+
err ? reject(new Error("No supported terminal emulator was found")) : resolve(),
94+
);
95+
});
96+
}
97+
98+
export interface LaunchResult {
99+
launched: boolean;
100+
command: string;
101+
}
102+
103+
/**
104+
* Launch a known coding agent's local CLI in a new terminal window. When
105+
* `prompt` is provided, it is passed as a single quoted argument using the
106+
* agent's verified prompt contract so the agent starts with that task.
107+
* Rejects when the id is unknown, the binary is missing from PATH, the agent
108+
* does not support prompt dispatch, or the platform terminal could not open.
109+
*/
110+
export async function launchAgent(
111+
id: string,
112+
cwd: string = process.cwd(),
113+
prompt?: string,
114+
): Promise<LaunchResult> {
115+
const command = agentCommand(id);
116+
if (!command) throw new Error(`Unknown agent: ${id}`);
117+
if (!(await onPath(command))) {
118+
throw new Error(`\`${command}\` was not found on your PATH — install ${id} first.`);
119+
}
120+
let fullCommand = command;
121+
const task = (prompt ?? "").trim();
122+
if (task) {
123+
const build = AGENT_PROMPT_ARGV[id];
124+
if (!build) throw new Error(`${id} does not support dispatching a task prompt.`);
125+
// shQuote keeps the whole prompt as one shell argument (no injection); the
126+
// platform terminal layer escapes the resulting command line separately.
127+
fullCommand = [command, ...build(task).map(shQuote)].join(" ");
128+
}
129+
await spawnTerminal(fullCommand, cwd);
130+
return { launched: true, command: fullCommand };
131+
}

0 commit comments

Comments
 (0)