Compare commits

..

29 Commits

Author SHA1 Message Date
chenanran555 4f41eaabc2 chore(release): prepare 1.20.0 2026-09-03 10:47:23 +08:00
chenanran555 e1634c40ed chore(deps): upgrade @openagentpack/sdk to 0.5.0 2026-09-02 22:26:33 +08:00
chenanran555 4c5688ad4a feat(managed-agent)!: restrict CLI to Bailian provider
Remove multi-provider CLI flags and reject non-Bailian provider configs while preserving SDK support.
2026-09-02 19:23:40 +08:00
chenanran555 176961dc56 feat(managed-agent): hide capabilities command from public CLI 2026-09-02 15:33:50 +08:00
chenanran555 9d19558645 feat(managed-agent): enforce high-risk confirmation for runtime operations 2026-09-02 15:07:22 +08:00
chenanran555 a9200a3f85 Merge remote-tracking branch 'origin/main' into feat/cma-command
# Conflicts:
#	packages/commands/src/commands/managed-agent/session-delete.ts
#	packages/commands/tests/e2e/managed-agent.e2e.test.ts
#	skills/bailian-managed-agent/SKILL.md
#	skills/bailian-managed-agent/reference/managed-agent.md
2026-09-02 14:51:49 +08:00
chenanran555 83d7982ecb feat(managed-agent): support creating agents with remote and local skills 2026-09-02 14:36:22 +08:00
Gong Shiqi d33cb71cc7 Merge pull request #180 from modelstudioai/feat/runtime-risk-confirmation
feat(cli): add unified confirmation for high-risk operations
2026-09-02 14:31:18 +08:00
若麒 737f8db359 chore(release): prepare 1.19.0 2026-09-01 20:21:21 +08:00
若麒 a402026cce Merge remote-tracking branch 'origin/main' into feat/runtime-risk-confirmation 2026-09-01 19:49:04 +08:00
若麒 da6e1313e5 feat(commands): migrate delete commands to runtime risk confirmation 2026-09-01 19:25:50 +08:00
若麒 2d2ec1c628 Merge remote-tracking branch 'origin/feat/quota-delete-split' into feat/runtime-risk-confirmation 2026-09-01 19:16:40 +08:00
Gong Shiqi a78ed7ffe8 Merge pull request #187 from modelstudioai/release/1.18.2
chore(release): prepare 1.18.2
2026-09-01 19:02:29 +08:00
若麒 5a1dfa5227 chore(release): prepare 1.18.2 2026-09-01 18:56:28 +08:00
duck-bubi-ya b5dec926b7 Merge pull request #185 from modelstudioai/fix/skill-init-issue
fix(skill): retry registry fetches and tolerate blocked backup cleanup
2026-09-01 17:37:22 +08:00
若麒 719c0b68bb Merge remote-tracking branch 'origin/main' into feat/runtime-risk-confirmation 2026-09-01 17:06:41 +08:00
故璃 9e6f5ca9cb fix(skill): retry registry fetches and tolerate blocked backup cleanup
- bl skill init: raise index timeout 10s→30s and retry transient network
  failures (3 attempts); advisor sync silent channel stays fail-fast
- make post-swap backup deletion best-effort so host safe-delete guards
  cannot fail a completed install (postinstall.js mirror included)
- add unit coverage for guard-blocked cleanup and registry retry policy
2026-09-01 16:37:12 +08:00
chenanran555 67ae182ec3 feat(managed-agent): require confirmation for removals 2026-08-31 11:06:43 +08:00
若麒 676b6c2ece test(runtime): update auth fallback fixture for confirmation context 2026-08-28 17:48:07 +08:00
若麒 4f59ca5118 Merge branch 'main' into feat/runtime-risk-confirmation 2026-08-28 17:42:57 +08:00
若麒 0872ff6a20 feat(cli): expose high-risk confirmation guidance in help and skills 2026-08-28 17:19:24 +08:00
若麒 afb547e0c8 refactor(commands): migrate remaining confirmations to runtime gate 2026-08-28 14:35:17 +08:00
chenanran555 449eede237 fix(managed-agent): upgrade OpenAgentPack SDK to 0.4.0 beta 2026-08-27 20:47:51 +08:00
若麒 8906108744 refactor(knowledge): migrate high-risk commands to runtime confirmation 2026-08-27 19:42:51 +08:00
chenanran555 2fbb6efe67 Merge remote-tracking branch 'origin/main' into feat/cma-command 2026-08-27 17:27:55 +08:00
chenanran555 47e9bcc634 feat(managed-agent): add scoped resource create commands 2026-08-27 17:01:27 +08:00
若麒 00bcee36a6 feat(runtime): add unified confirmation gate for high-risk commands 2026-08-27 15:54:23 +08:00
chenanran555 4c7ef0c7a5 feat(managed-agent): add scoped agent create command
Add `bl managed-agent agent create` with automatic YAML key generation,
atomic config updates, preview-by-default behavior, and create-only scoped
apply that ignores unrelated resource drift.
2026-08-27 11:44:00 +08:00
chenanran555 69c1fb9f08 feat(managed-agent): 增加托管 Agent 全面管理命令集及功能实现
- 扩展 CLI 命令,新增托管 Agent 相关全部操作命令包括能力、Agent、环境、技能、保管库、部署、会话及文件管理
- 实现 Agent 列表、详情、版本、搜索等功能
- 实现环境列表、详情及搜索功能
- 实现部署列表、详情、搜索、运行、暂停与恢复功能
- 实现会话的搜索、更新、归档及事件操作功能
- 实现文件上传、列表、下载、删除及搜索功能
- 添加托管 Agent API 操作能力展示命令
- 统一异常处理及输出格式支持,支持 JSON 与表格展示
- 完善分页、查询参数及权限验证逻辑,支持按需筛选与翻页
- 新增输入输出文件支持与控制台保护机制,提升命令行体验和稳定性
2026-08-24 20:15:09 +08:00
166 changed files with 10758 additions and 1510 deletions
+43
View File
@@ -6,6 +6,49 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.20.0] - 2026-09-03
> Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations.
### Added
- **Managed Agent API commands** — added direct list, get, search, version, upload, download, run, pause, archive, event, and diagnostic operations for Agents, Environments, Skills, Vaults, Deployments, Sessions, and Files.
- **Scoped YAML-backed resource creation** — `agent create`, `environment create`, `skill create`, `vault create`, `vault credential create`, and `deployment create` update `agents.yaml` and apply only the target resource without unrelated drift blocking the operation.
- **Agent Skill attachment** — Agent creation supports existing custom or official Skill IDs as well as local Skill directories and ZIP archives.
### Changed
- **Bailian-only Managed Agent CLI** — `bl managed-agent` now targets the Bailian provider exclusively; provider-selection flags were removed and configurations containing other providers are rejected.
- **Runtime mutation confirmation** — Deployment run/pause/unpause, Session archive/delete, and File delete operations require explicit high-risk confirmation.
### Fixed
- **Managed Agent error reporting** — Apply and scoped-create failures preserve the underlying provider diagnostic instead of ending with only `Apply failed.`.
### Security
- Credentials are resolved in memory and removed from the process environment; Vault credential declarations reference environment variables without persisting plaintext secrets.
## [1.19.0] - 2026-09-01
### Added
- **`bl quota delete`** — clears all custom QPM/TPM rate limits for a model.
### Changed
- **High-risk operation confirmation** — high-risk commands show risk details in `--help` and Skill command references. Without `--yes`, the high-risk operation is not executed; JSON output returns exit code `7` with `error.type: "requires_confirmation"`. After confirmation, re-run with `--yes`; `--dry-run` does not require confirmation.
## [1.18.2] - 2026-09-01
### Changed
- **Confirmation before deleting or clearing resources** — `bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
### Fixed
- **Skill installation reliability** — `bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
## [1.18.1] - 2026-08-28
### Removed
+43
View File
@@ -6,6 +6,49 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.20.0] - 2026-09-03
> Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。
### 新增
- **Managed Agent API 命令** —— 新增 Agent、Environment、Skill、Vault、Deployment、Session 和 File 的列表、详情、搜索、版本、上传、下载、运行、暂停、归档、事件及诊断等操作。
- **基于 YAML 的单资源创建** —— `agent create``environment create``skill create``vault create``vault credential create``deployment create` 会更新 `agents.yaml`,并且只 Apply 目标资源,不受无关资源 Drift 阻塞。
- **Agent Skill 挂载** —— 创建 Agent 时支持引用已有的自定义或官方 Skill ID也支持本地 Skill 目录和 ZIP 文件。
### 变更
- **Managed Agent CLI 限定为百炼 Provider** —— `bl managed-agent` 现在只面向百炼,移除 Provider 选择参数,并拒绝包含其他 Provider 的配置。
- **运行时变更增加确认** —— Deployment 运行/暂停/恢复、Session 归档/删除以及 File 删除操作需要显式进行高风险确认。
### 修复
- **Managed Agent 错误输出** —— Apply 和单资源创建失败时会保留底层 Provider 的具体诊断,不再只显示 `Apply failed.`
### 安全
- 凭证仅在内存中解析并从进程环境清除Vault Credential 声明通过环境变量引用 Secret不会持久化明文。
## [1.19.0] - 2026-09-01
### 新增
- **`bl quota delete`** — 清除指定模型的全部自定义 QPM/TPM 限流配置。
### 变更
- **高风险操作确认** — 高风险命令会在 `--help` 和 Skill 命令参考中展示风险说明。未传入 `--yes`高风险操作不会执行JSON 输出会返回退出码 `7``error.type: "requires_confirmation"`。确认后可添加 `--yes` 重新执行;`--dry-run` 无需确认。
## [1.18.2] - 2026-09-01
### 变更
- **删除与清除操作增加确认** —— `bl finetune delete``bl deploy delete``bl dataset delete``bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`
### 修复
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
## [1.18.1] - 2026-08-28
### 已移除
+7 -5
View File
@@ -62,17 +62,19 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
`bl managed-agent *` 按调用链分两层:
- **离线命令** — `init``validate``state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
- **联网命令** — `plan``apply``destroy``state import``skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁 —— 无论目标 provider 是谁authStage `resolveApiKey(sources)` 解析 bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层 `assertProviderCredentials` 再对 agents.yaml 里**全部已声明 provider** 的空 key 拦截并给 provider 专属 hint。例外:`plan --no-refresh` / `plan --dry-run``credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state不查 provider key其中 `--dry-run` 连登录也不要求authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
- **联网命令** — `plan``apply``destroy``state import``skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁authStage 经 `resolveApiKey(sources)` 解析 Bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层再断言 Bailian key 非空。例外:`plan --no-refresh` / `plan --dry-run``credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state不查 provider key其中 `--dry-run` 连登录也不要求authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
`bl managed-agent` 是 Bailian-only 产品入口:命令不暴露 `--provider``init` 只生成 `providers.bailian`,所有远端调用固定传 `provider: "bailian"``resolveAgentProjectConfig` 在创建 SDK runtime 前通过 `assertBailianOnlyProviders` 拒绝包含非 Bailian Provider 的手写配置;共享 `@openagentpack/sdk` 仍可保留多 Provider 能力。
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK管道五步:
1. `prepareProviderEnv()``bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
2. `resolveProjectConfig`插值发生:bailian 插值拿到占位空串claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免离线命令下空 key 在 SDK zod 层报 "received null")
1. `prepareProviderEnv()`调用 SDK 的凭证 bootstrap,再把凭证类 env(`CREDENTIAL_ENV_KEYS`,含兼容别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值阶段能够完成并由 CLI 输出明确的 Bailian-only 配置错误
2. `resolveProjectConfig`完成插值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`避免空 key 在 SDK zod 层提前报 "received null"
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
5. `assertProviderCredentials(providers)` — 任一已声明 provider 的 `api_key` 为空 CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
5. `assertBailianOnlyProviders(providers)` — 拒绝非 Bailian Provider随后 `assertProviderCredentials(providers)` 在 Bailian `api_key` 为空时给出 CLI 权威 `AUTH` 错误和登录 hint离线命令传 `credentials: "none"` 跳过 key 断言,但仍执行 Bailian-only 配置校验
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
禁止命令层直接 `readConfigFile` 裸读凭证Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
## 必查清单
+7
View File
@@ -85,6 +85,13 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
4. **真实集成**:放在 skip 块**末尾**
高风险命令额外要求:
- `--help` 展示 runtime 注入的 `--yes`
-`--yes` 返回 exit code 7 和 JSON `type: "requires_confirmation"`
- `--dry-run` 无需 `--yes`,且必须证明在任何远端请求或本地写入之前返回
- runtime 的离线 high-risk fixture 必须覆盖带 `--yes` 确实进入 `run()`,并断言 `yes` 不进入 command 自有 flags
## Journey 层(用户旅程全链路)
- **定位**:命令 E2E 验单命令契约journey 验“用户带着目标跨命令走通回路”,结构性断言不在 journey 重复
+2
View File
@@ -71,7 +71,9 @@ packages/commands/src/index.ts
- `usageArgs`(不含 bin/path 前缀)
- `exampleArgs`(不含 bin/path 前缀)
- `validate`(跨 flag 校验)
- 高风险命令必须声明 `risk: { level: "high", message: <双语文案> }`;`--yes` 由 runtime 注入,命令不得自行声明
- 普通业务命令的 `run(ctx)` 只读 `ctx.flags` / `ctx.settings` / `ctx.client`
- 声明 `risk``run(ctx)` 必须在任何远端请求或本地写入之前处理 `ctx.settings.dryRun` 并返回预览;runtime 只负责确认闸门,不替命令实现 dry-run
- `commands/auth/**` 可用 `ctx.authStore`,`commands/config/**` 可用 `ctx.configStore`;不要把这些持久化能力扩散到普通业务命令
- `commands/plugin/**` 可用 `ctx.commandPacks`;产品 policy 由 runtime 绑定,命令不要自行 import 产品入口
- [ ] 用户可见 Help 文案在命令文件中就近提供 `en-US` / `zh-CN`:命令 `description`、flag `description``notes` 和包含自然语言的 `exampleArgs`;纯命令语法示例可保留为字符串,服务端错误不翻译
+3
View File
@@ -40,6 +40,8 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
- [ ] **整包装齐**:安装/升级文案主推 `bl skill init`;业务 skill **不**声明 `companions`
- [ ] **协议读取**CRITICAL / references 可链 `../bailian-protocol/…`;若读不到 → 停止执行 `bl`,提示 `bl skill init`
- [ ] **高风险确认**:统一由 `bailian-protocol` 定义reference / leaf help 以 `risk: high` 明示风险,业务 skill 不得引导 Agent 自动补 `--yes`。遇到 exit code 7 / `requires_confirmation` 时停止执行并请求确认;目标或范围变化后重新确认
- [ ] **正常控制流**`requires_confirmation` 不是 CLI bug`assets/issue-reporting.md` 必须将 exit code 7 保持在 EXCLUDE 范围
- [ ] **软 hand-off**:兄弟业务 skill **只写 skill 名**;已安装则 Read未安装则 `bl … --help` 或提示整包安装;**不要**把 `../bailian-gen/…` 等写成执行前提
- [ ] **Hub vs 领域**`bailian-cli` 的「When to use which command」只列 hub 拥有的意图;媒体 / 精调 / managed-agent 各留 hand-off 行,**不抄**领域默认模型与子命令明细
- [ ] **渐进披露**SKILL 写意图路由与领域硬规则flags / usage / examples 以 `reference/``bl <command> --help` 为准,表后保留「勿猜 flag」指向句
@@ -55,6 +57,7 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts``GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表hub 最多加一行 hand-off
- [ ]`pnpm run sync:skill-assets`(或 commit 走 pre-commit提交生成的 `reference/` 与 version 同步结果
- [ ] 高风险命令生成的 reference 必须包含 `Risk` / `Risk message` 和简短 Agent safety 提示;带 `--yes` 的示例必须标注只能在确认后执行,不要手改生成物
- [ ] 默认模型若写在领域路由表(如 `bailian-gen`):与命令 default / [model-add-remove.md](model-add-remove.md) 一并核对
## 完成后自查
+19 -17
View File
@@ -17,11 +17,12 @@
│ ├─ ~/.bailian/telemetry.jsonl
│ └─ AEM(pid=bailian-cli-node, event name=命令路径)
└─ authStage
apiKey → DashScope / 模型域
├─ console → Bailian Console Gateway
├─ openapi → 阿里云 OpenAPI
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
└─ confirmationStage
versionCheckStage → authStage
├─ apiKey → DashScope / 模型域
├─ console → Bailian Console Gateway
├─ openapi → 阿里云 OpenAPI
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
```
### 1. 三套鉴权与埋点标识
@@ -77,7 +78,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
### 3. 全命令 AEM 客户端埋点
`packages/runtime/src/middleware.ts``telemetryStage` 包裹 `authStage` 与命令执行,因此成功、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`
`packages/runtime/src/middleware.ts``telemetryStage` 包裹确认闸门、`authStage` 与命令执行,因此成功、确认未通过、业务失败、网络失败和鉴权失败都会形成一次命令事件。事件名是空格连接的命令路径,例如 `text chat`确认闸门仍位于版本检查、鉴权和业务执行之前,不会因为埋点而放行高风险操作。
以下情况不会形成命令事件,因为没有进入 middleware 的 `run`
@@ -92,7 +93,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
- `command``timestamp``durationMs``success`
- `cliVersion``nodeVersion``os`
- `authMethod`
- 失败时的 `errorMessage``httpStatus``requestId`
- 失败时的 `errorMessage``exitCode``httpStatus``requestId`
- 安全 allowlist 过滤后的 `params`
参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts``PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。
@@ -108,16 +109,16 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
AEM 映射:
| AEM 字段 | 内容 |
| ---------- | ----------------------------------------- |
| event name | 命令路径 |
| `et` | `EXP` |
| `ext` | 除 `command``params` 外的结构化事件字段 |
| `c1` | allowlist 参数 |
| `c2` | `success` / `failure` |
| `c3` | HTTP status |
| `c4` | 错误文案,最多 500 字符 |
| `c5` | request ID |
| AEM 字段 | 内容 |
| ---------- | ------------------------------------------------------------------ |
| event name | 命令路径 |
| `et` | `EXP` |
| `ext` | 除 `command``params` 外的结构化事件字段,包含失败时的 `exitCode` |
| `c1` | allowlist 参数 |
| `c2` | `success` / `failure` |
| `c3` | HTTP status |
| `c4` | 错误文案,最多 500 字符 |
| `c5` | request ID |
远端发送是 best-effort不得阻塞命令或改变退出码。正常退出最多等待 1 秒SIGINT 最多等待 500 ms。
@@ -144,6 +145,7 @@ AEM 映射:
- [ ] 更新 `TrackingEvent``createTrackingEvent()``buildRemoteAemOptions()` 的字段映射
- [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径
- [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码
- [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1``c5`
- [ ] 检查 `DO_NOT_TRACK=1``telemetry: false` 两个关闭入口
- [ ] 错误字段不得额外拼接 token、请求体、prompt 或本地路径
+1 -1
View File
@@ -213,7 +213,7 @@ bl knowledge chunk delete --index-id <id> --chunk-id <id> [flags]
| ----------------- | ------ | ---- | ------------------------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--chunk-id <id>` | array | 是 | Chunk ID可重复每批最多 10 个,超出自动分批) |
| `--yes` | switch | 否 | 跳过确认提示 |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+4 -4
View File
@@ -232,10 +232,10 @@ bl knowledge category delete --category-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ------------ |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ---------------------- |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+6 -6
View File
@@ -190,11 +190,11 @@ bl knowledge doc delete --index-id <id> --doc-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ----------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID可重复 |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID可重复 |
| `--yes` | switch | 否 | 显式确认高风险操作 |
**输出**
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
# 删除单个文档
bl knowledge doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
# 批量删除,跳过确认
# 用户明确确认后批量删除
bl knowledge doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
```
+4 -4
View File
@@ -120,10 +120,10 @@ bl knowledge file delete --file-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | --------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | ---------------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+5 -5
View File
@@ -254,10 +254,10 @@ bl knowledge delete --index-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ---------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
- **不可逆操作**:知识库及所有索引内容被永久删除。
- 数据中心中的源文件不受影响,仅删除知识库索引。
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求
**示例**
+1 -1
View File
@@ -135,7 +135,7 @@
### 危险操作确认
涉及删除的命令(`kb delete``doc delete``chunk delete``file delete``category delete``service delete``service deploy`在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本
涉及删除的命令(`kb delete``doc delete``chunk delete``file delete``category delete``service delete``service deploy`属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行
### Dry-run 模式
+9 -9
View File
@@ -270,11 +270,11 @@ bl knowledge service deploy --agent-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------------- | ------ | ---- | ---------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------------- | ------ | ---- | ---------------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
@@ -319,10 +319,10 @@ bl knowledge service delete --agent-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | --------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ---------------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+1 -1
View File
@@ -213,7 +213,7 @@ kscli chunk delete --index-id <id> --chunk-id <id> [flags]
| ----------------- | ------ | ---- | ------------------------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--chunk-id <id>` | array | 是 | Chunk ID可重复每批最多 10 个,超出自动分批) |
| `--yes` | switch | 否 | 跳过确认提示 |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+4 -4
View File
@@ -232,10 +232,10 @@ kscli category delete --category-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ------------ |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ---------------------- |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+6 -6
View File
@@ -190,11 +190,11 @@ kscli doc delete --index-id <id> --doc-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ----------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID可重复 |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID可重复 |
| `--yes` | switch | 否 | 显式确认高风险操作 |
**输出**
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
# 删除单个文档
kscli doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
# 批量删除,跳过确认
# 用户明确确认后批量删除
kscli doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
```
+4 -4
View File
@@ -120,10 +120,10 @@ kscli file delete --file-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | --------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | ---------------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+5 -5
View File
@@ -254,10 +254,10 @@ kscli kb delete --index-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ---------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
- **不可逆操作**:知识库及所有索引内容被永久删除。
- 数据中心中的源文件不受影响,仅删除知识库索引。
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求
**示例**
+1 -1
View File
@@ -152,7 +152,7 @@ kscli --help
### 危险操作确认
涉及删除的命令(`kb delete``doc delete``chunk delete``file delete``category delete``service delete`)以及 `service deploy` 在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本
涉及删除的命令(`kb delete``doc delete``chunk delete``file delete``category delete``service delete`)以及 `service deploy` 属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行
### Dry-run 模式
+9 -9
View File
@@ -270,11 +270,11 @@ kscli service deploy --agent-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------------- | ------ | ---- | ---------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------------- | ------ | ---- | ---------------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
@@ -319,10 +319,10 @@ kscli service delete --agent-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | --------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--yes` | switch | 否 | 跳过确认提示 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ---------------------- |
| `--agent-id <id>` | string | 是 | 服务agentID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
**输出**
+2 -2
View File
@@ -116,9 +116,9 @@
**Flags**`--index-id` 必填;`--doc-id` array 必填(可重复);`--yes`
**实现方案**`doc-delete.ts`确认摘要含 index_id + doc_id 列表≤5 个全列,超出显示前 5 + 总数);输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
**实现方案**`doc-delete.ts`命令在 `risk` 对象中同时声明 `level: "high"` 和双语 `message`,由 runtime 在 `run()` 前统一确认;输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
**测试方案**help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 非 TTY`--yes` exitCode 2 / live 配合 upload 清理链。
**测试方案**help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 无 `--yes` 返回 exitCode 7 + `requires_confirmation` / live 配合 upload 清理链。
## doc tag — 批量更新文档标签
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.18.1",
"version": "1.20.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+12 -2
View File
@@ -181,7 +181,13 @@ function atomicSwap(tmpDir, catalogDir) {
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
throw err;
}
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
// Best-effort cleanup (symmetric with core skills/extract.ts): the swap already
// succeeded, so a backup deletion failure must not fail the pre-download
try {
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
} catch {
/* keep the backup on disk rather than report a completed swap as failed */
}
}
async function main() {
@@ -214,7 +220,11 @@ async function main() {
}
atomicSwap(tmpDir, catalogDir);
} catch (err) {
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
try {
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
} catch {
/* cleanup must not mask the original error */
}
throw err;
}
+85
View File
@@ -152,6 +152,48 @@ import {
managedAgentSessionSend,
managedAgentSessionEvents,
managedAgentSkillList,
managedAgentAgentCreate,
managedAgentEnvironmentCreate,
managedAgentSkillCreate,
managedAgentVaultCreate,
managedAgentVaultCredentialCreate,
managedAgentDeploymentCreate,
managedAgentAgentList,
managedAgentAgentGet,
managedAgentAgentSearch,
managedAgentAgentVersions,
managedAgentEnvironmentList,
managedAgentEnvironmentGet,
managedAgentEnvironmentSearch,
managedAgentSkillGet,
managedAgentSkillSearch,
managedAgentSkillVersions,
managedAgentSkillDownload,
managedAgentVaultList,
managedAgentVaultGet,
managedAgentVaultSearch,
managedAgentDeploymentList,
managedAgentDeploymentGet,
managedAgentDeploymentSearch,
managedAgentDeploymentRunsList,
managedAgentDeploymentRunsGet,
managedAgentDeploymentRun,
managedAgentDeploymentPause,
managedAgentDeploymentUnpause,
managedAgentSessionSearch,
managedAgentSessionUpdate,
managedAgentSessionArchive,
managedAgentSessionEventList,
managedAgentSessionEventSend,
managedAgentSessionEventStream,
managedAgentSessionDebug,
managedAgentSessionExport,
managedAgentFileUpload,
managedAgentFileList,
managedAgentFileGet,
managedAgentFileSearch,
managedAgentFileDownload,
managedAgentFileDelete,
} from "bailian-cli-commands";
// Full bailian-cli product: every command, exposed under the `bl` binary.
@@ -314,6 +356,49 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent session send": managedAgentSessionSend,
"managed-agent session events": managedAgentSessionEvents,
"managed-agent skill-list": managedAgentSkillList,
"managed-agent agent create": managedAgentAgentCreate,
"managed-agent agent list": managedAgentAgentList,
"managed-agent agent get": managedAgentAgentGet,
"managed-agent agent search": managedAgentAgentSearch,
"managed-agent agent versions": managedAgentAgentVersions,
"managed-agent environment create": managedAgentEnvironmentCreate,
"managed-agent environment list": managedAgentEnvironmentList,
"managed-agent environment get": managedAgentEnvironmentGet,
"managed-agent environment search": managedAgentEnvironmentSearch,
"managed-agent skill create": managedAgentSkillCreate,
"managed-agent skill list": managedAgentSkillList,
"managed-agent skill get": managedAgentSkillGet,
"managed-agent skill search": managedAgentSkillSearch,
"managed-agent skill versions": managedAgentSkillVersions,
"managed-agent skill download": managedAgentSkillDownload,
"managed-agent vault create": managedAgentVaultCreate,
"managed-agent vault credential create": managedAgentVaultCredentialCreate,
"managed-agent vault list": managedAgentVaultList,
"managed-agent vault get": managedAgentVaultGet,
"managed-agent vault search": managedAgentVaultSearch,
"managed-agent deployment create": managedAgentDeploymentCreate,
"managed-agent deployment list": managedAgentDeploymentList,
"managed-agent deployment get": managedAgentDeploymentGet,
"managed-agent deployment search": managedAgentDeploymentSearch,
"managed-agent deployment runs list": managedAgentDeploymentRunsList,
"managed-agent deployment runs get": managedAgentDeploymentRunsGet,
"managed-agent deployment run": managedAgentDeploymentRun,
"managed-agent deployment pause": managedAgentDeploymentPause,
"managed-agent deployment unpause": managedAgentDeploymentUnpause,
"managed-agent session search": managedAgentSessionSearch,
"managed-agent session update": managedAgentSessionUpdate,
"managed-agent session archive": managedAgentSessionArchive,
"managed-agent session event list": managedAgentSessionEventList,
"managed-agent session event send": managedAgentSessionEventSend,
"managed-agent session event stream": managedAgentSessionEventStream,
"managed-agent session debug": managedAgentSessionDebug,
"managed-agent session export": managedAgentSessionExport,
"managed-agent file upload": managedAgentFileUpload,
"managed-agent file list": managedAgentFileList,
"managed-agent file get": managedAgentFileGet,
"managed-agent file search": managedAgentFileSearch,
"managed-agent file download": managedAgentFileDownload,
"managed-agent file delete": managedAgentFileDelete,
};
/**
@@ -65,6 +65,7 @@ describe("e2e: Command Pack", () => {
expect(linkedJson.linked.commands).toEqual([
"agent credential",
"agent credential-denied",
"agent dangerous",
"agent fail",
"agent output",
"agent ping",
@@ -170,6 +171,34 @@ describe("e2e: Command Pack", () => {
expect(failed.stderr).toContain("Use agent fail only in tests.");
});
test("high-risk 命令由 runtime 统一确认并支持安全 dry-run", async () => {
const dangerousHelp = await runCli(["agent", "dangerous", "--help"], env());
expect(dangerousHelp.exitCode, dangerousHelp.stderr).toBe(0);
expect(dangerousHelp.stderr).toContain("--yes");
const unconfirmed = await runCli(["agent", "dangerous", "--output", "json"], env());
expect(unconfirmed.exitCode).toBe(7);
expect(JSON.parse(unconfirmed.stderr)).toMatchObject({
error: { code: 7, type: "requires_confirmation" },
});
const confirmed = await runCli(["agent", "dangerous", "--yes", "--output", "json"], env());
expect(confirmed.exitCode, confirmed.stderr).toBe(0);
expect(parseStdoutJson(confirmed.stdout)).toEqual({
executed: true,
dry_run: false,
command_flags: [],
});
const preview = await runCli(["agent", "dangerous", "--dry-run", "--output", "json"], env());
expect(preview.exitCode, preview.stderr).toBe(0);
expect(parseStdoutJson(preview.stdout)).toEqual({
executed: false,
dry_run: true,
command_flags: [],
});
});
test("plugin list 输出加载状态", async () => {
const result = await runCli(["plugin", "list", "--output", "json"], env());
expect(result.exitCode, result.stderr).toBe(0);
@@ -183,6 +212,7 @@ describe("e2e: Command Pack", () => {
commands: [
"agent credential",
"agent credential-denied",
"agent dangerous",
"agent fail",
"agent output",
"agent ping",
+21
View File
@@ -19,6 +19,26 @@ const ping = {
},
};
const dangerous = {
description: "Exercise runtime confirmation for a high-risk Command Pack command",
auth: "none",
risk: {
level: "high",
message: {
"en-US": "This fixture represents a high-risk operation.",
"zh-CN": "该测试命令代表高风险操作。",
},
},
async run(ctx) {
const dryRun = ctx.settings.dryRun;
ctx.output.result({
executed: !dryRun,
dry_run: dryRun,
command_flags: Object.keys(ctx.flags),
});
},
};
const credential = {
description: "Read an API key through the Command Pack host adapter",
auth: "apiKey",
@@ -57,6 +77,7 @@ const fail = {
export default {
"agent credential": credential,
"agent credential-denied": credentialDenied,
"agent dangerous": dangerous,
"agent fail": fail,
"agent output": output,
"agent ping": ping,
@@ -0,0 +1,69 @@
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, test } from "vite-plus/test";
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
const skillsRoot = join(repositoryRoot, "skills");
const scopedCreateCommands = new Set([
"bl managed-agent agent create",
"bl managed-agent deployment create",
"bl managed-agent environment create",
"bl managed-agent skill create",
"bl managed-agent vault create",
"bl managed-agent vault credential create",
]);
test("generated references distinguish runtime high-risk confirmation from scoped create execution", () => {
let highRiskCommandCount = 0;
const seenScopedCreateCommands = new Set<string>();
for (const skillDirectory of readdirSync(skillsRoot, { withFileTypes: true })) {
if (!skillDirectory.isDirectory()) continue;
const referenceDirectory = join(skillsRoot, skillDirectory.name, "reference");
let referenceFiles: string[];
try {
referenceFiles = readdirSync(referenceDirectory).filter(
(fileName) => fileName.endsWith(".md") && fileName !== "index.md",
);
} catch {
continue;
}
for (const referenceFile of referenceFiles) {
const markdown = readFileSync(join(referenceDirectory, referenceFile), "utf8");
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
for (const commandSection of commandSections) {
const commandName = commandSection.match(/^### `([^`]+)`/m)?.[1];
const hasConfirmationFlag = commandSection.includes("`--yes`");
const hasHighRiskMetadata = /\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/.test(commandSection);
if (!hasHighRiskMetadata) {
if (!hasConfirmationFlag) continue;
expect(commandName).toBeDefined();
expect(scopedCreateCommands.has(commandName ?? "")).toBe(true);
expect(commandSection).toMatch(/Without --yes, .*preview/i);
seenScopedCreateCommands.add(commandName ?? "");
continue;
}
highRiskCommandCount += 1;
expect(hasConfirmationFlag).toBe(true);
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
expect(commandSection).toMatch(/type=.*requires_confirmation/);
const agentSafetyLine = commandSection
.split("\n")
.find((line) => line.startsWith("> **Agent safety:**"));
expect(agentSafetyLine).toBeDefined();
expect(agentSafetyLine).toMatch(/never add `--yes` automatically/i);
expect(agentSafetyLine).toMatch(/explicit user confirmation/i);
expect(agentSafetyLine).not.toContain("`--dry-run`");
}
}
}
expect(highRiskCommandCount).toBeGreaterThan(0);
expect([...seenScopedCreateCommands].sort()).toEqual([...scopedCreateCommands].sort());
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.18.1",
"version": "1.20.0",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
@@ -40,7 +40,7 @@
"check": "vp check"
},
"dependencies": {
"@openagentpack/sdk": "0.3.2",
"@openagentpack/sdk": "0.5.0",
"bailian-cli-core": "workspace:*",
"bailian-cli-runtime": "workspace:*",
"boxen": "catalog:",
@@ -1,5 +1,5 @@
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
fileId: {
@@ -8,28 +8,25 @@ const DELETE_FLAGS = {
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID必填" },
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
auth: "apiKey",
usageArgs: "--file-id <id> [--yes]",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the specified dataset file and cannot be undone.",
"zh-CN": "该操作会永久删除指定的数据集文件,且无法撤销。",
},
},
usageArgs: "--file-id <id>",
flags: DELETE_FLAGS,
exampleArgs: [
"--file-id file-id-xxx",
"--file-id file-id-xxx --dry-run",
"--file-id file-id-xxx --yes",
],
notes: [
{
"en-US": "Irreversible — the dataset file is permanently removed.",
"zh-CN": "该操作不可撤销——数据集文件将被永久删除。",
},
],
async run(ctx) {
const { settings, flags } = ctx;
const fileId = flags.fileId;
@@ -39,11 +36,6 @@ export default defineCommand({
return;
}
await confirmDangerousAction(
`Delete dataset file ${fileId}.\nThe file is permanently removed. This cannot be undone.`,
flags.yes ?? false,
);
const response = await deleteDataset(ctx.client, fileId);
if (settings.quiet) {
@@ -6,7 +6,7 @@ import {
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
deployedModel: {
@@ -25,10 +25,6 @@ const DELETE_FLAGS = {
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
},
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
} satisfies FlagsDef;
/**
@@ -44,14 +40,15 @@ export default defineCommand({
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED",
},
auth: "apiKey",
usageArgs: "--deployed-model <id> [--skip-precheck] [--yes]",
flags: DELETE_FLAGS,
notes: [
{
"en-US": "Irreversible — the deployment is permanently destroyed.",
"zh-CN": "该操作不可撤销——部署将被永久销毁。",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the specified model deployment and cannot be undone.",
"zh-CN": "该操作会永久删除指定的模型部署,且无法撤销。",
},
],
},
usageArgs: "--deployed-model <id> [--skip-precheck]",
flags: DELETE_FLAGS,
exampleArgs: [
"--deployed-model dep-...",
"--deployed-model dep-... --dry-run",
@@ -87,11 +84,6 @@ export default defineCommand({
}
}
await confirmDangerousAction(
`Delete deployment ${deployedModel}.\nThe deployment is permanently destroyed. This cannot be undone.`,
flags.yes ?? false,
);
const response = await deleteDeployment(ctx.client, deployedModel);
if (settings.quiet) {
@@ -1,5 +1,5 @@
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
const DELETE_FLAGS = {
jobId: {
@@ -8,23 +8,22 @@ const DELETE_FLAGS = {
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID必填" },
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
auth: "apiKey",
usageArgs: "--job-id <id> [--yes]",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the specified fine-tune job record and cannot be undone.",
"zh-CN": "该操作会永久删除指定的微调任务记录,且无法撤销。",
},
},
usageArgs: "--job-id <id>",
flags: DELETE_FLAGS,
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
notes: [
{
"en-US": "Irreversible — the job record is permanently removed.",
"zh-CN": "该操作不可撤销——任务记录将被永久删除。",
},
{
"en-US":
"Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.",
@@ -40,11 +39,6 @@ export default defineCommand({
return;
}
await confirmDangerousAction(
`Delete fine-tune job record ${jobId}.\nThe job record is permanently removed. This cannot be undone.`,
flags.yes ?? false,
);
const response = await deleteFineTune(ctx.client, jobId);
if (settings.quiet) {
@@ -6,7 +6,7 @@ import {
type FlagsDef,
type RagConnectorResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CATEGORY_DELETE_FLAGS = {
@@ -16,16 +16,19 @@ const CATEGORY_DELETE_FLAGS = {
description: { "en-US": "Category ID to delete", "zh-CN": "要删除的类目 ID" },
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Delete a data-center category", "zh-CN": "删除数据中心类目" },
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This deletes the selected data-center category and cannot be undone.",
"zh-CN": "该操作会删除所选数据中心类目,且无法撤销。",
},
},
usageArgs: "--category-id <id> [flags]",
flags: CATEGORY_DELETE_FLAGS,
notes: [
@@ -49,11 +52,6 @@ export default defineCommand({
return;
}
await confirmDangerousAction(
`Delete category ${flags.categoryId}\nThis cannot be undone.`,
flags.yes ?? false,
);
const response = await ctx.client.requestJson<
RagConnectorResponse<Record<string, unknown> | undefined>
>({
@@ -7,7 +7,7 @@ import {
type FlagsDef,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CHUNK_DELETE_FLAGS = {
@@ -26,10 +26,6 @@ const CHUNK_DELETE_FLAGS = {
},
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
@@ -48,6 +44,13 @@ export default defineCommand({
"zh-CN": "从知识库中删除 Chunk不可撤销",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the selected chunks and cannot be undone.",
"zh-CN": "该操作会永久删除所选 Chunk且无法撤销。",
},
},
usageArgs: "--index-id <id> --chunk-id <id> [flags]",
flags: CHUNK_DELETE_FLAGS,
notes: [
@@ -81,11 +84,6 @@ export default defineCommand({
return;
}
await confirmDangerousAction(
`Delete ${flags.chunkId.length} chunk(s) from knowledge base ${flags.indexId} in ${batches.length} batch(es).\nChunks are permanently removed. This cannot be undone.`,
flags.yes ?? false,
);
// Sequential batches; any batch failure aborts, listing already-deleted batches in the error
let deletedCount = 0;
for (const batchIds of batches) {
@@ -6,7 +6,7 @@ import {
type FlagsDef,
type RagDeleteFileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const DOC_DELETE_FLAGS = {
@@ -25,28 +25,22 @@ const DOC_DELETE_FLAGS = {
},
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary: list all doc_ids up to 5, otherwise show the first 5 + total count */
function buildDeleteSummary(indexId: string, docIds: string[]): string {
const listed =
docIds.length <= 5
? docIds.join("\n ")
: `${docIds.slice(0, 5).join("\n ")}\n ... (${docIds.length} documents total)`;
return `Delete ${docIds.length} document(s) from knowledge base ${indexId}:\n ${listed}\nDocuments and all their chunks are permanently removed from the index. This cannot be undone.`;
}
export default defineCommand({
description: {
"en-US": "Delete documents and their chunks from a knowledge base",
"zh-CN": "从知识库中删除文档及其 Chunk",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the selected documents and all of their chunks.",
"zh-CN": "该操作会永久删除所选文档及其全部 Chunk且无法撤销。",
},
},
usageArgs: "--index-id <id> --doc-id <id> [flags]",
flags: DOC_DELETE_FLAGS,
notes: [
@@ -72,7 +66,7 @@ export default defineCommand({
},
],
exampleArgs: [
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx",
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx --dry-run",
"--index-id idx-xxx --doc-id file-a --doc-id file-b --yes",
],
async run(ctx) {
@@ -89,11 +83,6 @@ export default defineCommand({
return;
}
await confirmDangerousAction(
buildDeleteSummary(flags.indexId, flags.docId),
flags.yes ?? false,
);
const response = await ctx.client.requestJson<RagDeleteFileResponse>({
path: endpoint,
method: "POST",
@@ -3,12 +3,10 @@ import {
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type Client,
type FlagsDef,
type RagConnectorResponse,
type RagDescribeFileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const FILE_DELETE_FLAGS = {
@@ -21,39 +19,23 @@ const FILE_DELETE_FLAGS = {
},
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary lookup (file name/size); failure degrades to id-only */
async function buildDeleteSummary(
client: Client,
workspaceId: string,
fileId: string,
): Promise<string> {
let infoPart = "";
try {
const detail = await client.requestJson<RagDescribeFileResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.describeFile),
method: "POST",
body: { fileId },
});
if (detail.data?.fileName) infoPart = ` name: ${detail.data.fileName}`;
} catch {
// Degrade gracefully: a failed lookup does not block confirmation
}
return `Delete data-center file ${fileId}${infoPart}\nPERMANENT: if the file is referenced by knowledge bases, their document indexes break too. This differs from removing a document from one knowledge base.`;
}
export default defineCommand({
description: {
"en-US": "Permanently delete a file from the data center",
"zh-CN": "从数据中心永久删除文件",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This permanently deletes the data-center file. Knowledge-base document indexes that reference it may become invalid.",
"zh-CN": "该操作会永久删除数据中心文件;引用该文件的知识库文档索引可能失效。",
},
},
usageArgs: "--file-id <id> [flags]",
flags: FILE_DELETE_FLAGS,
notes: [
@@ -82,11 +64,6 @@ export default defineCommand({
return;
}
const summary = flags.yes
? ""
: await buildDeleteSummary(ctx.client, workspaceId, flags.fileId);
await confirmDangerousAction(summary, flags.yes ?? false);
const response = await ctx.client.requestJson<
RagConnectorResponse<Record<string, unknown> | undefined>
>({
@@ -4,12 +4,10 @@ import {
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagIndexFilesResponse,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
import { fetchIndexDetail } from "./kb-info.ts";
const KB_DELETE_FLAGS = {
indexId: {
@@ -18,50 +16,23 @@ const KB_DELETE_FLAGS = {
description: { "en-US": "Knowledge base ID", "zh-CN": "知识库 ID" },
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary lookup: name + document count; any lookup failure degrades to id-only (never blocks deletion) */
async function buildDeleteSummary(
ctx: { client: Parameters<typeof fetchIndexDetail>[0] },
workspaceId: string,
indexId: string,
): Promise<string> {
let namePart = "";
let docCountPart = "";
try {
const detail = await fetchIndexDetail(ctx.client, workspaceId, indexId);
namePart = ` name: ${detail.name}`;
} catch {
// Degrade gracefully: a missing name does not block confirmation
}
try {
const filesUrl = new URL(ragEndpoint(workspaceId, RAG_PATHS.indexFiles));
filesUrl.searchParams.set("index_id", indexId);
filesUrl.searchParams.set("page_num", "1");
filesUrl.searchParams.set("page_size", "1");
const files = await ctx.client.requestJson<RagIndexFilesResponse>({
path: filesUrl.toString(),
method: "GET",
});
const totalCount = files.data?.total_count;
if (typeof totalCount === "number") docCountPart = ` documents: ${totalCount}`;
} catch {
// Same graceful degradation as above
}
return `Delete knowledge base ${indexId}${namePart}${docCountPart}\nThis permanently removes the knowledge base with all documents and chunks. It cannot be undone.`;
}
export default defineCommand({
description: {
"en-US": "Delete a knowledge base with all its documents and chunks",
"zh-CN": "删除知识库及其所有文档和 Chunk",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This permanently deletes the knowledge base and all of its documents and chunks. Data-center files are not deleted.",
"zh-CN": "该操作会永久删除知识库及其全部文档和 Chunk但不会删除数据中心文件。",
},
},
usageArgs: "--index-id <id> [flags]",
flags: KB_DELETE_FLAGS,
notes: [
@@ -90,11 +61,6 @@ export default defineCommand({
return;
}
const summary = flags.yes
? "" // --yes bypasses the prompt, so skip the summary lookups
: await buildDeleteSummary(ctx, workspaceId, flags.indexId);
await confirmDangerousAction(summary, flags.yes ?? false);
const response = await ctx.client.requestJson<RagMutationResponse>({
path: endpoint,
method: "POST",
@@ -3,12 +3,10 @@ import {
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type Client,
type FlagsDef,
type RagAgentGetResponse,
type RagAgentMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const SERVICE_DELETE_FLAGS = {
@@ -18,48 +16,23 @@ const SERVICE_DELETE_FLAGS = {
description: { "en-US": "Service (agent) ID", "zh-CN": "服务AgentID" },
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary lookup (name/status); failure degrades to id-only */
async function buildDeleteSummary(
client: Client,
workspaceId: string,
agentId: string,
): Promise<string> {
let infoPart = "";
let liveWarning = "";
try {
const detail = await client.requestJson<RagAgentGetResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
method: "POST",
body: { agent_id: agentId },
});
const name = detail.data?.agent_name;
const status = detail.data?.agent_status;
if (name) infoPart += ` name: ${name}`;
if (status) {
infoPart += ` status: ${status}`;
if (status === "deployed" || status === "edited") {
liveWarning = "\nWARNING: this service is LIVE — deleting it breaks existing callers.";
}
}
} catch {
// Degrade gracefully: a failed lookup does not block confirmation
}
return `Delete service ${agentId}${infoPart}${liveWarning}\nDeletion cannot be undone; the agent_id can no longer be used for search or chat calls.`;
}
export default defineCommand({
description: {
"en-US": "Delete a retrieval / Q&A service (soft delete, idempotent)",
"zh-CN": "删除检索/问答服务(软删除,幂等)",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This deletes the service and makes its agent ID unavailable for search and chat calls. The operation cannot be undone.",
"zh-CN": "该操作会删除服务,使其 Agent ID 无法再用于搜索和对话调用,且无法撤销。",
},
},
usageArgs: "--agent-id <id> [flags]",
flags: SERVICE_DELETE_FLAGS,
notes: [
@@ -91,11 +64,6 @@ export default defineCommand({
return;
}
const summary = flags.yes
? ""
: await buildDeleteSummary(ctx.client, workspaceId, flags.agentId);
await confirmDangerousAction(summary, flags.yes ?? false);
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
path: endpoint,
method: "POST",
@@ -3,12 +3,10 @@ import {
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type Client,
type FlagsDef,
type RagAgentGetResponse,
type RagAgentMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const SERVICE_DEPLOY_FLAGS = {
@@ -26,49 +24,23 @@ const SERVICE_DEPLOY_FLAGS = {
"zh-CN": "新发布版本的描述",
},
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
/** Confirmation summary lookup (name/status); warns that deploying an edited draft overwrites live behavior; failure degrades to id-only */
async function buildDeploySummary(
client: Client,
workspaceId: string,
agentId: string,
): Promise<string> {
let infoPart = "";
let editedWarning = "";
try {
const detail = await client.requestJson<RagAgentGetResponse>({
path: ragEndpoint(workspaceId, RAG_PATHS.agentGet),
method: "POST",
body: { agent_id: agentId },
});
const name = detail.data?.agent_name;
const status = detail.data?.agent_status;
if (name) infoPart += ` name: ${name}`;
if (status) {
infoPart += ` status: ${status}`;
if (status === "edited") {
editedWarning =
"\nWARNING: a published version is live — deploying replaces its behavior with the current draft.";
}
}
} catch {
// Degrade gracefully: a failed lookup does not block confirmation
}
return `Deploy service ${agentId}${infoPart}${editedWarning}\nPublishing changes what live callers get from this service.`;
}
export default defineCommand({
description: {
"en-US": "Publish the beta draft of a service as a new version",
"zh-CN": "将服务的 beta 草稿发布为新版本",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This publishes the current draft as a new version and changes the behavior seen by live callers.",
"zh-CN": "该操作会将当前草稿发布为新版本,并改变线上调用方使用的服务行为。",
},
},
usageArgs: "--agent-id <id> [flags]",
flags: SERVICE_DEPLOY_FLAGS,
notes: [
@@ -109,11 +81,6 @@ export default defineCommand({
return;
}
const summary = flags.yes
? ""
: await buildDeploySummary(ctx.client, workspaceId, flags.agentId);
await confirmDangerousAction(summary, flags.yes ?? false);
const response = await ctx.client.requestJson<RagAgentMutationResponse>({
path: endpoint,
method: "POST",
@@ -0,0 +1,196 @@
import type { FlagsDef } from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import type { PagedResult } from "./pagination.ts";
export const API_TARGET_FLAGS = {
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
} satisfies FlagsDef;
export const CURSOR_FLAGS = {
limit: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Page size (1-100)",
"zh-CN": "单页数量1-100",
},
},
page: {
type: "string",
valueHint: "<cursor>",
description: {
"en-US": "Opaque page cursor returned by a previous request",
"zh-CN": "上一次请求返回的不透明分页 Cursor",
},
},
all: {
type: "switch",
description: {
"en-US": "Fetch all pages by following opaque cursors",
"zh-CN": "跟随不透明 Cursor 获取全部分页",
},
},
} satisfies FlagsDef;
export const SEARCH_FLAGS = {
query: {
type: "string",
valueHint: "<text>",
required: true,
description: {
"en-US": "Case-insensitive text to find in IDs, names, and descriptions",
"zh-CN": "在 ID、名称和描述中进行不区分大小写的文本搜索",
},
},
pageLimit: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Maximum pages to scan for client-side search (default: 10)",
"zh-CN": "客户端搜索最多扫描的页数默认10",
},
},
} satisfies FlagsDef;
export const INCLUDE_ARCHIVED_FLAG = {
includeArchived: {
type: "switch",
description: {
"en-US": "Include archived resources",
"zh-CN": "包含已归档资源",
},
},
} satisfies FlagsDef;
export function validateLimitAndPageLimit(flags: {
limit?: number;
pageLimit?: number;
}): string | undefined {
if (
flags.limit !== undefined &&
(!Number.isInteger(flags.limit) || flags.limit < 1 || flags.limit > 100)
) {
return "--limit must be an integer between 1 and 100.";
}
if (
flags.pageLimit !== undefined &&
(!Number.isInteger(flags.pageLimit) || flags.pageLimit < 1 || flags.pageLimit > 100)
) {
return "--page-limit must be an integer between 1 and 100.";
}
return undefined;
}
export function splitCommaSeparated(value?: string): string[] | undefined {
if (!value) return undefined;
const values = value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
return values.length > 0 ? values : undefined;
}
export function matchesQuery(query: string, ...values: unknown[]): boolean {
const normalized = query.trim().toLocaleLowerCase();
if (!normalized) return true;
return values.some((value) => {
if (value === undefined || value === null) return false;
const text = typeof value === "string" ? value : JSON.stringify(value);
return text.toLocaleLowerCase().includes(normalized);
});
}
export interface SearchResult<T> extends PagedResult<T> {
scannedPages: number;
truncated: boolean;
}
export async function searchCursorPages<T>(
fetchPage: (page?: string) => Promise<PagedResult<T>>,
matches: (item: T) => boolean,
pageLimit = 10,
): Promise<SearchResult<T>> {
const items: T[] = [];
let page: string | undefined;
let hasMore = false;
let nextPage: string | undefined;
let scannedPages = 0;
do {
const result = await fetchPage(page);
scannedPages += 1;
items.push(...result.items.filter(matches));
hasMore = result.hasMore;
nextPage = result.nextPage;
page = result.nextPage;
} while (hasMore && page && scannedPages < pageLimit);
return {
items,
hasMore,
nextPage,
scannedPages,
truncated: Boolean(hasMore && nextPage),
};
}
export function emitCollection<T>(options: {
format: "json" | "text";
key: string;
items: T[];
headers: string[];
rows: string[][];
hasMore?: boolean;
nextPage?: string;
truncated?: boolean;
scannedPages?: number;
emptyMessage?: string;
}): void {
const {
format,
key,
items,
headers,
rows,
hasMore = false,
nextPage,
truncated,
scannedPages,
emptyMessage = "No resources found.",
} = options;
if (format === "json") {
emitResult(
{
[key]: items,
has_more: hasMore,
next_page: nextPage,
...(truncated === undefined ? {} : { truncated }),
...(scannedPages === undefined ? {} : { scanned_pages: scannedPages }),
},
format,
);
return;
}
if (items.length === 0) {
emitBare(emptyMessage);
return;
}
for (const line of formatTable(headers, rows)) emitBare(line);
emitBare(`\nTotal: ${items.length}`);
if (truncated) emitBare("Search stopped at --page-limit; more pages remain.");
else if (hasMore)
emitBare(`More results are available.${nextPage ? ` Next page: ${nextPage}` : ""}`);
}
export function displayValue(value: unknown, maxLength = 40): string {
if (value === undefined || value === null || value === "") return "-";
const text = typeof value === "string" ? value : JSON.stringify(value);
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
}
@@ -3,8 +3,8 @@ import {
type LoadedProjectConfig,
type ProjectRuntimeContext,
resolveProjectConfig,
UserError,
} from "@openagentpack/sdk";
import { BailianError, ExitCode } from "bailian-cli-core";
import {
assertProviderCredentials,
type CredentialHost,
@@ -20,12 +20,14 @@ export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts";
/**
* Whether this run requires provider keys:
* - "all" (default) — online command: every provider declared in agents.yaml
* must have a non-empty key after injection
* - "all" (default) — online command: the Bailian provider must have a
* non-empty key after injection
* - "none" — offline command (local config/state only), skip the check
*/
export type CredentialScope = "all" | "none";
export const BAILIAN_PROVIDER = "bailian";
interface AgentConfigOptions {
resolveEnv?: boolean;
projectName?: string;
@@ -42,7 +44,8 @@ interface AgentConfigOptions {
* 3. override the bailian block with the CLI auth chain's credential (in-memory)
* 4. scrub all credential vars from process.env (real values now live only in
* the config object → provider adapters, never the environment)
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
* 5. enforce Bailian-only config and fail with a CLI-authoritative AUTH error
* if its key is empty
* (offline commands pass `credentials: "none"` to skip the check)
*/
export async function resolveAgentProjectConfig(
@@ -55,6 +58,7 @@ export async function resolveAgentProjectConfig(
normalizeInterpolatedProviderBlocks(resolved.config.providers);
injectProviderCredentials(resolved.config.providers, host);
scrubCredentialEnv();
assertBailianOnlyProviders(resolved.config.providers);
if ((options.credentials ?? "all") !== "none") {
assertProviderCredentials(resolved.config.providers);
}
@@ -91,15 +95,23 @@ export async function buildAgentRuntime(
return { ...ctx, configPath };
}
/** Ensure a user-supplied --provider value is actually configured in agents.yaml. */
export function assertProviderConfigured(
ctx: ProjectRuntimeContext,
provider: string | undefined,
): void {
if (!provider || provider === "all") return;
if (ctx.providers.has(provider)) return;
const available = Array.from(ctx.providers.keys()).join(", ") || "none";
throw new UserError(
`Provider '${provider}' is not configured. Available providers: ${available}.`,
/** Enforce the bl product boundary while the shared SDK remains multi-provider capable. */
export function assertBailianOnlyProviders(providers: Record<string, unknown>): void {
const configuredProviders = Object.keys(providers);
const unsupportedProviders = configuredProviders.filter(
(provider) => provider !== BAILIAN_PROVIDER,
);
if (unsupportedProviders.length > 0) {
const names = unsupportedProviders.join(", ");
throw new BailianError(
`bl managed-agent only supports provider '${BAILIAN_PROVIDER}'; remove unsupported providers: ${names}. / bl managed-agent 仅支持 Provider '${BAILIAN_PROVIDER}';请移除不支持的 Provider${names}`,
ExitCode.USAGE,
);
}
if (!(BAILIAN_PROVIDER in providers)) {
throw new BailianError(
`bl managed-agent requires a '${BAILIAN_PROVIDER}' provider configuration. / bl managed-agent 需要配置 Provider '${BAILIAN_PROVIDER}'。`,
ExitCode.USAGE,
);
}
}
@@ -34,6 +34,12 @@ const CREDENTIAL_HINTS: Record<string, string> = {
qoder: "Set QODER_PAT (or QODER_API_KEY) in your shell or .env.",
};
const BAILIAN_ONLY_NOTE = {
"en-US":
"bl managed-agent supports the Bailian provider only; configurations containing other providers are rejected.",
"zh-CN": "bl managed-agent 仅支持百炼 Provider包含其他 Provider 的配置会被拒绝。",
} as const;
/** The slice of CommandContext the credential pipeline needs: authStage-resolved client + settings. */
export interface CredentialHost {
client: Client;
@@ -43,24 +49,18 @@ export interface CredentialHost {
/**
* Shared `--help` note documenting where agent commands get provider
* credentials. Bailian goes through bl's own auth chain (commands declare
* `auth: "apiKey"`); other providers come from env. Either way the resolved
* credential is injected into the SDK in-memory and scrubbed from the
* environment. Attach to every command that loads agents.yaml. `bl` prefix is
* safe: agent commands ship on `bl` only.
* `auth: "apiKey"`). The resolved credential is injected into the SDK in-memory
* and scrubbed from the environment. Attach to every command that loads
* agents.yaml. `bl` prefix is safe: agent commands ship on `bl` only.
*/
export const CREDENTIALS_NOTE = [
BAILIAN_ONLY_NOTE,
{
"en-US":
"Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).",
"zh-CN":
"百炼凭证来自 bl 鉴权链:--api-key > DASHSCOPE_API_KEY > `bl auth login`(当前激活的配置 Profile。",
},
{
"en-US":
"Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.",
"zh-CN":
"其他 Provider 读取 agents.yaml 中引用的环境变量(例如 ${ANTHROPIC_API_KEY}),包括 .env 和 ~/.agents/config.json。",
},
{
"en-US":
"Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.",
@@ -73,6 +73,7 @@ export const CREDENTIALS_NOTE = [
* agents.yaml / local state only, so no login or provider key is required.
*/
export const OFFLINE_NOTE = [
BAILIAN_ONLY_NOTE,
{
"en-US": "Runs fully offline against local files: no login or provider credentials required.",
"zh-CN": "完全离线处理本地文件:无需登录或提供 Provider 凭证。",
@@ -166,15 +167,16 @@ export function normalizeInterpolatedProviderBlocks(providers: Record<string, un
}
/**
* After injection, fail with a CLI-authoritative AUTH error if any configured
* provider's `api_key` resolved empty (missing env var, or no bl login for
* bailian). Replaces the SDK's raw `Environment variable '...' is not set` /
* zod config error with a clean message plus a provider-specific hint. Validates
* every declared provider, so a project is only runnable once all its providers'
* keys are available; offline commands skip the check entirely.
* After injection, fail with a CLI-authoritative AUTH error if Bailian's
* `api_key` resolved empty. Replaces the SDK's raw config error with a clean
* message and hint. Offline commands skip the check entirely.
*/
export function assertProviderCredentials(providers: Record<string, unknown>): void {
export function assertProviderCredentials(
providers: Record<string, unknown>,
targetProviders?: readonly string[],
): void {
for (const [name, raw] of Object.entries(providers)) {
if (targetProviders && !targetProviders.includes(name)) continue;
if (!raw || typeof raw !== "object") continue;
const block = raw as Record<string, unknown>;
if (!("api_key" in block)) continue;
@@ -12,6 +12,12 @@ interface SdkApiErrorLike extends Error {
responseBody: string;
}
interface AgentDiagnosticLike {
severity: string;
code?: string;
message: string;
}
function isSdkApiError(error: Error): error is SdkApiErrorLike {
const candidate = error as Partial<SdkApiErrorLike>;
return typeof candidate.statusCode === "number" && typeof candidate.responseBody === "string";
@@ -45,6 +51,42 @@ function isSdkPollingTimeout(error: UserError): boolean {
return /did not complete within the timeout/i.test(error.message);
}
/**
* Keep the final CLI error useful for hosts that retain stderr's terminal
* BailianError but discard the diagnostics emitted before it. The complete
* diagnostic collection remains on stdout/stderr; the terminal error carries
* the first actionable reason and signals when more errors are available.
*/
export function formatAgentDiagnosticFailure(
diagnostics: readonly AgentDiagnosticLike[],
fallback: string,
): string {
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
const firstError = errors[0];
if (!firstError) return fallback;
const codePrefix = firstError.code?.trim() ? `[${firstError.code}] ` : "";
const remaining = errors.length - 1;
const remainingSuffix = remaining > 0 ? ` (+${remaining} more errors)` : "";
return `${codePrefix}${firstError.message}${remainingSuffix}`;
}
/** Add a recovery hint without replacing the original error or its metadata. */
export function retainAgentError(error: unknown, hint: string, fallback: string): BailianError {
if (error instanceof BailianError) {
return new BailianError(error.message, error.exitCode, hint, {
api: error.api,
rawResponse: error.rawResponse,
cause: error.cause,
});
}
if (error instanceof Error) {
return new BailianError(error.message, ExitCode.GENERAL, hint, { cause: error });
}
const message = typeof error === "string" && error.trim() ? error : fallback;
return new BailianError(message, ExitCode.GENERAL, hint);
}
/**
* Run an SDK-backed operation, translating SDK error types into BailianError so
* bl's error handler produces the right exit code and hint formatting.
@@ -6,7 +6,7 @@ import {
type StateScope,
} from "@openagentpack/sdk";
function createStateScope(configPath: string, projectName?: string): StateScope {
export function createFileStateScope(configPath: string, projectName?: string): StateScope {
const resolved = resolve(configPath);
return { projectId: projectName ?? basename(dirname(resolved)) };
}
@@ -19,6 +19,6 @@ export async function loadFileState(
): Promise<IStateManager> {
const resolved = resolve(configPath);
const backend = new LocalFileStateBackend({ configPath: resolved, statePath });
const path = backend.getStatePath(createStateScope(resolved, projectName));
const path = backend.getStatePath(createFileStateScope(resolved, projectName));
return StateManager.load(path);
}
@@ -0,0 +1,68 @@
import { randomUUID } from "node:crypto";
import { link, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { basename, dirname, resolve } from "node:path";
import { BailianError, ExitCode } from "bailian-cli-core";
export async function readInputFile(path: string): Promise<Uint8Array> {
return new Uint8Array(await readFile(resolve(path)));
}
export async function readJsonInput(argument: string): Promise<unknown> {
const source = argument.startsWith("@")
? await readFile(resolve(argument.slice(1)), "utf8")
: argument;
try {
return JSON.parse(source) as unknown;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new BailianError(`Invalid JSON input: ${message}`, ExitCode.USAGE);
}
}
export async function writeOutputFile(
outputPath: string,
content: Uint8Array,
force = false,
): Promise<string> {
const destination = resolve(outputPath);
const parent = dirname(destination);
await mkdir(parent, { recursive: true });
const temporary = `${destination}.${randomUUID()}.tmp`;
await writeFile(temporary, content, { flag: "wx" });
try {
if (force) {
await rename(temporary, destination);
} else {
try {
await link(temporary, destination);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "EEXIST") {
throw new BailianError(
`Output file already exists: ${destination}`,
ExitCode.USAGE,
"Choose another --output-file path or re-run with --force.",
);
}
throw error;
}
await unlink(temporary);
}
return destination;
} catch (error) {
await unlink(temporary).catch(() => undefined);
throw error;
}
}
export function inferMimeType(path: string): string {
const filename = basename(path).toLocaleLowerCase();
if (filename.endsWith(".json")) return "application/json";
if (filename.endsWith(".md")) return "text/markdown";
if (filename.endsWith(".txt")) return "text/plain";
if (filename.endsWith(".pdf")) return "application/pdf";
if (filename.endsWith(".png")) return "image/png";
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) return "image/jpeg";
if (filename.endsWith(".zip")) return "application/zip";
return "application/octet-stream";
}
@@ -8,8 +8,9 @@ export interface PagedResult<T> {
export async function fetchAllPages<T>(
fetchPage: (page?: string) => Promise<PagedResult<T>>,
all?: boolean,
initialPage?: string,
): Promise<PagedResult<T>> {
const first = await fetchPage();
const first = await fetchPage(initialPage);
const items = [...first.items];
let hasMore = first.hasMore;
let nextPage = first.nextPage;
@@ -0,0 +1,406 @@
import { randomUUID } from "node:crypto";
import { readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
import { basename, dirname, resolve } from "node:path";
import {
type BackendRuntimeInput,
type IStateManager,
LocalFileStateBackend,
planProjectWithStateBackend,
type ResolvedProjectConfig,
type ResourceAddress,
type ResourceSyncRun,
resolveProjectConfigFromObject,
syncProjectResourcesWithStateBackend,
} from "@openagentpack/sdk";
import { BailianError, type CommandContext, detectOutputFormat, ExitCode } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { parseDocument } from "yaml";
import { formatResourceLabel } from "./address-utils.ts";
import { resolveAgentProjectConfig } from "./config-loader.ts";
import { assertProviderCredentials } from "./credentials.ts";
import { withStdoutProtected } from "./console-capture.ts";
import { retainAgentError, withAgentErrors } from "./errors.ts";
import { createFileStateScope } from "./file-state-manager.ts";
import { installSdkTransport } from "./transport.ts";
export type ScopedCreateHost = Pick<CommandContext, "client" | "identity" | "settings">;
export type ScopedCreateGroup = "agents" | "environments" | "skills" | "vaults" | "deployments";
export interface LoadedScopedCreateProject {
configPath: string;
projectName: string;
config: ResolvedProjectConfig;
source: string;
provider: string;
stateBackend: LocalFileStateBackend;
stateScope: ReturnType<typeof createFileStateScope>;
}
export interface ResourceKeySelection {
key: string;
reusedPending: boolean;
}
export const SCOPED_CREATE_NOTE = [
{
"en-US":
"Without --yes, this command only previews. --dry-run is fully offline. The scoped flow checks only the target resource and its transitive dependencies; unrelated resources are not refreshed or drift-checked.",
"zh-CN":
"不带 --yes 时仅预览;--dry-run 完全离线。定向流程只检查目标资源及其传递依赖,不刷新或检测无关资源的 Drift。",
},
];
export interface ScopedTopLevelCreateInput {
host: ScopedCreateHost;
project: LoadedScopedCreateProject;
group: Exclude<ScopedCreateGroup, "agents">;
resourceType: "environment" | "skill" | "vault" | "deployment";
displayName: string;
rawDeclaration: Record<string, unknown>;
resolvedDeclaration: Record<string, unknown>;
existingDeclarations: Record<string, Record<string, unknown>>;
effectiveName: (key: string, declaration: Record<string, unknown>) => string;
fallbackKey: string;
yes: boolean;
}
function canonicalJson(value: unknown): string {
const normalize = (candidate: unknown): unknown => {
if (Array.isArray(candidate)) return candidate.map(normalize);
if (!candidate || typeof candidate !== "object") return candidate;
return Object.fromEntries(
Object.entries(candidate as Record<string, unknown>)
.filter(([, entry]) => entry !== undefined)
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
.map(([key, entry]) => [key, normalize(entry)]),
);
};
return JSON.stringify(normalize(value));
}
export function normalizeResourceKey(displayName: string, fallback = "resource"): string {
const normalized = displayName
.normalize("NFKC")
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, "-")
.replace(/^-+|-+$/g, "");
return normalized || fallback;
}
export function selectResourceKey(options: {
displayName: string;
provider: string;
resourceTypes: ResourceAddress["type"][];
declarations: Record<string, Record<string, unknown>>;
candidate: Record<string, unknown>;
effectiveName: (key: string, declaration: Record<string, unknown>) => string;
fallbackKey?: string;
state: IStateManager;
}): ResourceKeySelection {
const tracked = new Set(
options.state
.listResources()
.filter(
(resource) =>
resource.address.provider === options.provider &&
options.resourceTypes.includes(resource.address.type),
)
.map((resource) => resource.address.name),
);
const candidateJson = canonicalJson(options.candidate);
for (const [key, declaration] of Object.entries(options.declarations)) {
if (options.effectiveName(key, declaration) !== options.displayName || tracked.has(key))
continue;
if (canonicalJson(declaration) === candidateJson) return { key, reusedPending: true };
}
const baseKey = normalizeResourceKey(options.displayName, options.fallbackKey);
if (!(baseKey in options.declarations)) return { key: baseKey, reusedPending: false };
let suffix = 2;
while (`${baseKey}-${suffix}` in options.declarations) suffix += 1;
return { key: `${baseKey}-${suffix}`, reusedPending: false };
}
export async function loadScopedCreateProject(
host: ScopedCreateHost,
file: string,
): Promise<LoadedScopedCreateProject> {
const sourceBeforeLoad = await readFile(resolve(file), "utf8").catch((error) => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
throw new BailianError(
`Config file not found: ${file}`,
ExitCode.USAGE,
"Run `bl managed-agent init` first.",
);
}
throw error;
});
const loaded = await withAgentErrors(() =>
resolveAgentProjectConfig(host, file, { credentials: "none" }),
);
const source = await readFile(loaded.configPath, "utf8");
if (source !== sourceBeforeLoad) {
throw new BailianError(
`${file} changed while it was being loaded.`,
ExitCode.GENERAL,
"Re-run the command against the latest file.",
);
}
const provider = "bailian";
if (!host.settings.dryRun) assertProviderCredentials(loaded.config.providers, [provider]);
installSdkTransport(host);
const stateBackend = new LocalFileStateBackend({ configPath: loaded.configPath });
return {
...loaded,
source,
provider,
stateBackend,
stateScope: createFileStateScope(loaded.configPath, loaded.projectName),
};
}
export async function resolveCandidateDeclaration(options: {
project: LoadedScopedCreateProject;
group: ScopedCreateGroup;
rawDeclaration: Record<string, unknown>;
}): Promise<Record<string, unknown>> {
const temporaryKey = "__bailian_cli_create_candidate__";
const rawConfig = structuredClone(options.project.config) as unknown as Record<string, unknown>;
const declarations = (rawConfig[options.group] ?? {}) as Record<string, unknown>;
rawConfig[options.group] = { ...declarations, [temporaryKey]: options.rawDeclaration };
const resolved = await withAgentErrors(() =>
resolveProjectConfigFromObject(rawConfig, {
projectName: options.project.projectName,
basePath: dirname(options.project.configPath),
}),
);
const resolvedGroup = resolved.config[options.group] as
| Record<string, Record<string, unknown>>
| undefined;
return resolvedGroup?.[temporaryKey] ?? options.rawDeclaration;
}
export async function runScopedTopLevelCreate(input: ScopedTopLevelCreateInput): Promise<void> {
const { project, host } = input;
const format = detectOutputFormat(host.settings.output);
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
selectResourceKey({
displayName: input.displayName,
provider: project.provider,
resourceTypes: [input.resourceType],
declarations: input.existingDeclarations,
candidate: input.resolvedDeclaration,
effectiveName: input.effectiveName,
fallbackKey: input.fallbackKey,
state,
}),
);
const resourceKey = keySelection.key;
const candidateConfig = structuredClone(project.config) as unknown as Record<string, unknown>;
const existingGroup = (candidateConfig[input.group] ?? {}) as Record<string, unknown>;
candidateConfig[input.group] = { ...existingGroup, [resourceKey]: input.resolvedDeclaration };
candidateConfig._resolved = true;
const document = parseDocument(project.source);
if (document.errors.length > 0) {
throw new BailianError(
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
ExitCode.USAGE,
);
}
document.setIn([input.group, resourceKey], input.rawDeclaration);
const nextSource = document.toString();
const root: ResourceAddress = {
type: input.resourceType,
name: resourceKey,
provider: project.provider,
};
const backendInput: BackendRuntimeInput = {
projectName: project.projectName,
config: candidateConfig as unknown as ResolvedProjectConfig,
configPath: project.configPath,
providers: { [project.provider]: project.config.providers[project.provider] },
stateBackend: project.stateBackend,
stateScope: project.stateScope,
};
if (host.settings.dryRun || !input.yes) {
const planned = await withAgentErrors(() =>
withStdoutProtected(() =>
planProjectWithStateBackend(backendInput, {
provider: project.provider,
scope: { roots: [root] },
mode: "create-only",
refresh: !host.settings.dryRun,
quiet: format === "json",
}),
),
);
const readyToCreate = !planned.plan.diagnostics.some(
(diagnostic) => diagnostic.severity === "error",
);
const result = {
resource: {
type: input.resourceType,
key: resourceKey,
name: input.displayName,
provider: project.provider,
},
config_file: project.configPath,
yaml_written: false,
reused_pending: keySelection.reusedPending,
requires_confirmation: !host.settings.dryRun,
ready_to_create: readyToCreate,
actions: planned.plan.actions,
diagnostics: planned.plan.diagnostics,
};
if (format === "json") emitResult(result, format);
else {
emitBare(`Generated YAML key: ${resourceKey}`);
renderPlan(planned.plan.actions, planned.plan.diagnostics);
emitBare(
host.settings.dryRun
? "Dry run: YAML, State, and remote resources were not changed."
: "Preview only: re-run with --yes to write YAML and create this resource.",
);
}
return;
}
await replaceConfigAtomically(project.configPath, project.source, nextSource);
let run: ResourceSyncRun;
try {
run = await withAgentErrors(() =>
withStdoutProtected(() =>
syncProjectResourcesWithStateBackend(backendInput, {
provider: project.provider,
scope: { roots: [root] },
mode: "create-only",
refresh: true,
quiet: format === "json",
policy: "block",
}),
),
);
} catch (error) {
throw retainAgentError(
error,
"The YAML declaration was kept. Fix the related dependency or provider error, then re-run the same create command.",
`Scoped ${input.resourceType} create failed.`,
);
}
const remoteId = await project.stateBackend.read(
project.stateScope,
(state) => state.getResource(root)?.remote_id,
);
const results = run.execution?.results ?? [];
const failed = results.find((result) => result.status === "failed");
const result = {
resource: {
type: input.resourceType,
key: resourceKey,
name: input.displayName,
provider: project.provider,
remote_id: remoteId,
},
config_file: project.configPath,
yaml_written: true,
reused_pending: keySelection.reusedPending,
actions: run.planned.plan.actions,
diagnostics: run.planned.plan.diagnostics,
results,
error: failed?.error,
};
if (format === "json") emitResult(result, format);
else {
emitBare(`Wrote ${project.configPath} with ${input.resourceType} key '${resourceKey}'.`);
if (!failed) emitBare(`Created ${input.resourceType} '${input.displayName}'.`);
}
if (failed) {
throw new BailianError(
failed.error ?? `Scoped ${input.resourceType} create failed.`,
ExitCode.GENERAL,
"The YAML declaration was kept. Fix the provider error, then re-run the same create command.",
);
}
}
export async function replaceConfigAtomically(
configPath: string,
expectedSource: string,
nextSource: string,
): Promise<void> {
const destination = resolve(configPath);
const currentSource = await readFile(destination, "utf8");
if (currentSource !== expectedSource) {
throw new BailianError(
`${configPath} changed while resource create was being prepared.`,
ExitCode.GENERAL,
"Review the latest YAML and re-run the command; no file was overwritten.",
);
}
const currentStat = await stat(destination);
const temporary = resolve(dirname(destination), `.${basename(destination)}.${randomUUID()}.tmp`);
await writeFile(temporary, nextSource, { flag: "wx", mode: currentStat.mode });
try {
await rename(temporary, destination);
} catch (error) {
await unlink(temporary).catch(() => undefined);
throw error;
}
}
export function parseMetadata(values: string[] | undefined): Record<string, string> | undefined {
if (!values?.length) return undefined;
const metadata: Record<string, string> = {};
for (const entry of values) {
const separator = entry.indexOf("=");
if (separator <= 0) {
throw new BailianError(
`Invalid metadata '${entry}'.`,
ExitCode.USAGE,
"Use --metadata key=value.",
);
}
const key = entry.slice(0, separator).trim();
if (!key) throw new BailianError("Metadata key must not be empty.", ExitCode.USAGE);
metadata[key] = entry.slice(separator + 1);
}
return metadata;
}
export async function parseJsonInputs(
values: string[] | undefined,
label: string,
): Promise<unknown[]> {
const parsed: unknown[] = [];
for (const value of values ?? []) {
const raw = value.startsWith("@") ? await readFile(resolve(value.slice(1)), "utf8") : value;
let decoded: unknown;
try {
decoded = JSON.parse(raw);
} catch (error) {
throw new BailianError(
`Invalid ${label} JSON: ${error instanceof Error ? error.message : String(error)}`,
ExitCode.USAGE,
);
}
if (Array.isArray(decoded)) parsed.push(...decoded);
else parsed.push(decoded);
}
return parsed;
}
function renderPlan(
actions: Array<{ action: string; address: ResourceAddress }>,
diagnostics: Array<{ severity: string; code: string; message: string }>,
): void {
for (const diagnostic of diagnostics) {
emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
}
for (const action of actions.filter((entry) => entry.action !== "no-op")) {
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitBare(` ${icon} ${formatResourceLabel(action.address)}`);
}
}
@@ -0,0 +1,52 @@
import type { CloudAgent } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
INCLUDE_ARCHIVED_FLAG,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const AGENT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const AGENT_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const AGENT_GET_FLAGS = {
...API_TARGET_FLAGS,
agentId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Agent ID", "zh-CN": "Agent ID" },
},
agentVersion: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Specific agent version", "zh-CN": "指定 Agent 版本" },
},
} as const;
export const AGENT_VERSIONS_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
agentId: AGENT_GET_FLAGS.agentId,
};
export function agentRows(agents: CloudAgent[]): string[][] {
return agents.map((agent) => [
agent.id,
displayValue(agent.name),
displayValue(agent.version),
displayValue(agent.type),
displayValue(agent.updated_at),
]);
}
@@ -0,0 +1,585 @@
import { stat } from "node:fs/promises";
import { dirname, extname, relative, sep } from "node:path";
import {
type BackendRuntimeInput,
buildAgentDecl,
type IStateManager,
inspectSkillSource,
planProjectWithStateBackend,
type ResolvedProjectConfig,
type ResourceAddress,
type ResourceSyncRun,
type SkillDecl,
syncProjectResourcesWithStateBackend,
} from "@openagentpack/sdk";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { parseDocument } from "yaml";
import { formatResourceLabel } from "../_engine/address-utils.ts";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { retainAgentError, withAgentErrors } from "../_engine/errors.ts";
import {
type LoadedScopedCreateProject,
loadScopedCreateProject,
normalizeResourceKey,
replaceConfigAtomically,
resolveCandidateDeclaration,
selectResourceKey,
} from "../_engine/scoped-create.ts";
export { replaceConfigAtomically } from "../_engine/scoped-create.ts";
const CREATE_FLAGS = {
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Remote Agent display name; the YAML key is generated automatically",
"zh-CN": "远端 Agent 显示名称YAML key 将自动生成",
},
},
model: {
type: "string",
valueHint: "<model>",
required: true,
description: { "en-US": "Model ID", "zh-CN": "模型 ID" },
},
instructions: {
type: "string",
valueHint: "<text|path>",
required: true,
description: {
"en-US": "Inline instructions or a ./, ../, or absolute file path",
"zh-CN": "内联指令,或以 ./、../、/ 开头的文件路径",
},
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Agent description", "zh-CN": "Agent 描述" },
},
skill: {
type: "array",
valueHint: "<id>",
description: {
"en-US": "Existing remote Skill ID (repeatable)",
"zh-CN": "已存在的远端 Skill ID可重复",
},
},
skillDir: {
type: "array",
valueHint: "<path>",
description: {
"en-US":
"Local Skill directory or ZIP to declare, upload, and attach through the same scoped create (repeatable)",
"zh-CN": "通过同一次定向创建声明、上传并绑定的本地 Skill 目录或 ZIP可重复",
},
},
type: {
type: "string",
valueHint: "<custom|official>",
choices: ["custom", "official"],
description: {
"en-US": "Type applied to every --skill value (default: custom)",
"zh-CN": "应用于所有 --skill 的类型默认custom",
},
},
tool: {
type: "array",
valueHint: "<name>",
description: {
"en-US": "Builtin tool name (repeatable)",
"zh-CN": "内置工具名称(可重复)",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and run the scoped remote create",
"zh-CN": "写入 YAML 并执行定向远端创建",
},
},
} satisfies FlagsDef;
type BuiltAgentDecl = ReturnType<typeof buildAgentDecl>["agent"];
type AgentSkillType = "custom" | "official";
type AgentSkillDecl = NonNullable<BuiltAgentDecl["skills"]>[number];
interface AgentKeySelection {
key: string;
reusedPending: boolean;
}
interface LocalSkillKeySelection {
key: string;
needsCreate: boolean;
reusedPending: boolean;
reusedTracked: boolean;
}
interface PreparedLocalSkill extends LocalSkillKeySelection {
name: string;
source: string;
rawDeclaration: Record<string, unknown>;
resolvedDeclaration: SkillDecl;
}
export function normalizeAgentKey(displayName: string): string {
return normalizeResourceKey(displayName, "agent");
}
export function selectAgentKey(options: {
displayName: string;
provider: string;
agents: Record<string, BuiltAgentDecl>;
candidate: BuiltAgentDecl;
state: IStateManager;
}): AgentKeySelection {
return selectResourceKey({
displayName: options.displayName,
provider: options.provider,
resourceTypes: ["agent", "template"],
declarations: options.agents as unknown as Record<string, Record<string, unknown>>,
candidate: options.candidate as unknown as Record<string, unknown>,
effectiveName: (key, declaration) =>
typeof declaration.name === "string" ? declaration.name : key,
fallbackKey: "agent",
state: options.state,
});
}
function agentSkillType(value: string | undefined): AgentSkillType {
if (value === undefined || value === "custom") return "custom";
if (value === "official") return "official";
throw new BailianError("--type must be either custom or official.", ExitCode.USAGE);
}
export function buildAgentSkillRefs(
skillIds: string[] | undefined,
type: AgentSkillType = "custom",
): NonNullable<BuiltAgentDecl["skills"]> {
const seen = new Set<string>();
const refs: NonNullable<BuiltAgentDecl["skills"]> = [];
for (const rawId of skillIds ?? []) {
const skillId = rawId.trim();
if (!skillId) {
throw new BailianError("--skill values must not be empty.", ExitCode.USAGE);
}
if (seen.has(skillId)) continue;
seen.add(skillId);
refs.push({ type, skill_id: skillId });
}
return refs;
}
function canonicalJson(value: unknown): string {
const normalize = (candidate: unknown): unknown => {
if (Array.isArray(candidate)) return candidate.map(normalize);
if (!candidate || typeof candidate !== "object") return candidate;
return Object.fromEntries(
Object.entries(candidate as Record<string, unknown>)
.filter(([, entry]) => entry !== undefined)
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
.map(([key, entry]) => [key, normalize(entry)]),
);
};
return JSON.stringify(normalize(value));
}
export function selectLocalSkillKey(options: {
displayName: string;
provider: string;
declarations: Record<string, Record<string, unknown>>;
candidate: Record<string, unknown>;
state: IStateManager;
}): LocalSkillKeySelection {
const trackedKeys = new Set(
options.state
.listResources()
.filter(
(resource) =>
resource.address.provider === options.provider && resource.address.type === "skill",
)
.map((resource) => resource.address.name),
);
const sameName = Object.entries(options.declarations).filter(([key, declaration]) => {
const effectiveName = typeof declaration.name === "string" ? declaration.name : key;
return effectiveName === options.displayName;
});
const matchingTracked = sameName.find(
([key, declaration]) =>
trackedKeys.has(key) && canonicalJson(declaration) === canonicalJson(options.candidate),
);
if (matchingTracked) {
return {
key: matchingTracked[0],
needsCreate: false,
reusedPending: false,
reusedTracked: true,
};
}
const differentTracked = sameName.find(([key]) => trackedKeys.has(key));
if (differentTracked) {
throw new BailianError(
`Skill '${options.displayName}' is already tracked with a different declaration.`,
ExitCode.USAGE,
"Modify its existing YAML declaration and run full `bl managed-agent apply`, or bind its remote ID with --skill.",
);
}
const selected = selectResourceKey({
displayName: options.displayName,
provider: options.provider,
resourceTypes: ["skill"],
declarations: options.declarations,
candidate: options.candidate,
effectiveName: (key, declaration) =>
typeof declaration.name === "string" ? declaration.name : key,
fallbackKey: "skill",
state: options.state,
});
return {
...selected,
needsCreate: true,
reusedTracked: false,
};
}
async function prepareLocalSkillSources(
project: LoadedScopedCreateProject,
sources: string[] | undefined,
): Promise<PreparedLocalSkill[]> {
const declarations = {
...((project.config.skills ?? {}) as unknown as Record<string, Record<string, unknown>>),
};
const preparedByPath = new Map<string, PreparedLocalSkill>();
const preparedByKey = new Map<string, PreparedLocalSkill>();
for (const rawSource of sources ?? []) {
const requestedSource = rawSource.trim();
if (!requestedSource) {
throw new BailianError("--skill-dir values must not be empty.", ExitCode.USAGE);
}
const inspected = await withAgentErrors(() =>
inspectSkillSource(requestedSource, { basePath: process.cwd() }),
);
const sourceStat = await stat(inspected.sourcePath);
const supportedSource =
sourceStat.isDirectory() ||
(sourceStat.isFile() && extname(inspected.sourcePath).toLowerCase() === ".zip");
if (!supportedSource) {
throw new BailianError(
`--skill-dir requires a Skill directory or .zip file: ${requestedSource}`,
ExitCode.USAGE,
);
}
if (preparedByPath.has(inspected.sourcePath)) continue;
const source =
relative(dirname(project.configPath), inspected.sourcePath).split(sep).join("/") || ".";
const rawDeclaration: Record<string, unknown> = {
name: inspected.name,
source,
origin: "custom",
provider: project.provider,
};
const resolvedDeclaration = (await resolveCandidateDeclaration({
project,
group: "skills",
rawDeclaration,
})) as unknown as SkillDecl;
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
selectLocalSkillKey({
displayName: inspected.name,
provider: project.provider,
declarations,
candidate: resolvedDeclaration as unknown as Record<string, unknown>,
state,
}),
);
const existingPrepared = preparedByKey.get(keySelection.key);
if (existingPrepared) {
preparedByPath.set(inspected.sourcePath, existingPrepared);
continue;
}
const prepared: PreparedLocalSkill = {
...keySelection,
name: inspected.name,
source,
rawDeclaration,
resolvedDeclaration,
};
declarations[keySelection.key] = resolvedDeclaration as unknown as Record<string, unknown>;
preparedByPath.set(inspected.sourcePath, prepared);
preparedByKey.set(keySelection.key, prepared);
}
return [...preparedByKey.values()];
}
export default defineCommand({
description: {
"en-US": "Declare and create one Managed Agent through an isolated YAML apply",
"zh-CN": "通过隔离的 YAML Apply 声明并创建一个托管 Agent",
},
auth: "apiKey",
usageArgs:
"--name <name> --model <model> --instructions <text|path> [--description <text>] [--skill <id>...] [--type custom|official] [--skill-dir <path>...] [--tool <name>...] [--file <path>] [--yes]",
flags: CREATE_FLAGS,
exampleArgs: [
'--name assistant --model qwen3.8-max --instructions "You are helpful."',
"--name assistant --model qwen3.8-max --instructions ./prompts/assistant.md --skill skill_abc --yes",
"--name slides --model qwen3.8-max --instructions ./prompts/slides.md --skill skill_pptx --type official --yes",
"--name reviewer --model qwen3.8-max --instructions ./prompts/reviewer.md --skill-dir ./skills/code-review --yes",
],
notes: [
...CREDENTIALS_NOTE,
{
"en-US":
"Without --yes, previews the generated YAML key and scoped plan. --dry-run stays offline. Unrelated resources are not refreshed or drift-checked.",
"zh-CN":
"不带 --yes 时预览自动生成的 YAML key 和定向计划;--dry-run 完全离线。无关资源不会刷新或检测 Drift。",
},
{
"en-US":
"--skill writes an external Skill reference directly into the Agent declaration. --type defaults to custom; use --type official for platform Skills. These Skills are not managed through the top-level skills map.",
"zh-CN":
"--skill 会把外部 Skill 引用直接写入 Agent 声明。--type 默认为 custom平台 Skill 使用 --type official。这些 Skill 不通过顶层 skills 资源管理。",
},
{
"en-US":
"--skill-dir accepts a local Skill directory or ZIP, writes it as a top-level custom Skill declaration, and writes its generated YAML key into the Agent skills list. Skill and Agent are created together in dependency order. --type applies only to --skill IDs.",
"zh-CN":
"--skill-dir 接收本地 Skill 目录或 ZIP将其写为顶层 custom Skill 声明,并把生成的 YAML key 写入 Agent 的 skills 列表Skill 与 Agent 按依赖顺序一并创建。--type 只作用于 --skill ID。",
},
],
validate: (flags) => {
if (!flags.name.trim()) return "--name must not be empty.";
if (!flags.model.trim()) return "--model must not be empty.";
if (!flags.instructions.trim()) return "--instructions must not be empty.";
if (flags.type && !flags.skill?.length) return "--type requires at least one --skill.";
if (flags.skill?.some((skillId) => !skillId.trim())) {
return "--skill values must not be empty.";
}
if (flags.skillDir?.some((source) => !source.trim())) {
return "--skill-dir values must not be empty.";
}
return undefined;
},
async run(ctx) {
const { flags, settings } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const project = await loadScopedCreateProject(ctx, file);
const provider = project.provider;
const externalSkillRefs = buildAgentSkillRefs(flags.skill, agentSkillType(flags.type));
const localSkills = await prepareLocalSkillSources(project, flags.skillDir);
const skillRefs: AgentSkillDecl[] = [
...externalSkillRefs,
...localSkills.map((skill) => skill.key),
];
const rawAgent = buildAgentDecl(undefined, {
name: flags.name.trim(),
description: flags.description,
model: flags.model,
instructions: flags.instructions,
provider,
builtinTools: flags.tool,
}).agent;
rawAgent.skills = skillRefs;
const candidateConfig = structuredClone(project.config);
candidateConfig.skills = { ...candidateConfig.skills };
for (const skill of localSkills) {
candidateConfig.skills[skill.key] = skill.resolvedDeclaration;
}
candidateConfig._resolved = true;
const candidateAgent = (await resolveCandidateDeclaration({
project: { ...project, config: candidateConfig },
group: "agents",
rawDeclaration: rawAgent as unknown as Record<string, unknown>,
})) as unknown as BuiltAgentDecl;
const keySelection = await project.stateBackend.read(project.stateScope, (state) =>
selectAgentKey({
displayName: flags.name.trim(),
provider,
agents: (project.config.agents ?? {}) as Record<string, BuiltAgentDecl>,
candidate: candidateAgent,
state,
}),
);
const agentKey = keySelection.key;
candidateConfig.agents = { ...candidateConfig.agents, [agentKey]: candidateAgent };
candidateConfig._resolved = true;
const document = parseDocument(project.source);
if (document.errors.length > 0) {
throw new BailianError(
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
ExitCode.USAGE,
);
}
for (const skill of localSkills) {
document.setIn(["skills", skill.key], skill.rawDeclaration);
}
document.setIn(["agents", agentKey], rawAgent);
const nextSource = document.toString();
const agentRoot: ResourceAddress = { type: "agent", name: agentKey, provider };
const roots: ResourceAddress[] = [
...localSkills
.filter((skill) => skill.needsCreate)
.map((skill) => ({ type: "skill" as const, name: skill.key, provider })),
agentRoot,
];
const backendInput: BackendRuntimeInput = {
projectName: project.projectName,
config: candidateConfig as ResolvedProjectConfig,
configPath: project.configPath,
providers: { [provider]: candidateConfig.providers[provider] },
stateBackend: project.stateBackend,
stateScope: project.stateScope,
};
if (settings.dryRun || !flags.yes) {
const planned = await withAgentErrors(() =>
withStdoutProtected(() =>
planProjectWithStateBackend(backendInput, {
provider,
scope: { roots },
refresh: !settings.dryRun,
quiet: format === "json",
mode: "create-only",
}),
),
);
const readyToCreate = !planned.plan.diagnostics.some(
(diagnostic) => diagnostic.severity === "error",
);
const result = {
agent: { key: agentKey, name: flags.name.trim(), provider, skills: skillRefs },
local_skills: localSkills.map((skill) => ({
key: skill.key,
name: skill.name,
source: skill.source,
reused_pending: skill.reusedPending,
reused_tracked: skill.reusedTracked,
})),
config_file: project.configPath,
yaml_written: false,
reused_pending: keySelection.reusedPending,
requires_confirmation: !settings.dryRun,
ready_to_create: readyToCreate,
actions: planned.plan.actions,
diagnostics: planned.plan.diagnostics,
};
if (format === "json") {
emitResult(result, format);
} else {
for (const skill of localSkills) {
emitBare(`Generated Skill YAML key: ${skill.key}`);
}
emitBare(`Generated Agent YAML key: ${agentKey}`);
for (const diagnostic of planned.plan.diagnostics) {
emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
}
for (const action of planned.plan.actions.filter((entry) => entry.action !== "no-op")) {
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitBare(` ${icon} ${formatResourceLabel(action.address)}`);
}
emitBare(
settings.dryRun
? "Dry run: YAML, State, and remote resources were not changed."
: "Preview only: re-run with --yes to write YAML and create this Agent.",
);
}
return;
}
await replaceConfigAtomically(project.configPath, project.source, nextSource);
let run: ResourceSyncRun;
try {
run = await withAgentErrors(() =>
withStdoutProtected(() =>
syncProjectResourcesWithStateBackend(backendInput, {
provider,
scope: { roots },
refresh: true,
quiet: format === "json",
mode: "create-only",
policy: "block",
}),
),
);
} catch (error) {
throw retainAgentError(
error,
"The YAML declarations were kept. Fix the related dependency or provider error, then re-run the same create command.",
"Scoped Agent and Skill create failed.",
);
}
const remoteResources = await project.stateBackend.read(project.stateScope, (state) => ({
agentId: state.getResource(agentRoot)?.remote_id,
skillIds: Object.fromEntries(
localSkills.map((skill) => [
skill.key,
state.getResource({ type: "skill", name: skill.key, provider })?.remote_id,
]),
),
}));
const results = run.execution?.results ?? [];
const failed = results.find((entry) => entry.status === "failed");
const status = failed ? "failed" : "completed";
const result = {
agent: {
key: agentKey,
name: flags.name.trim(),
provider,
remote_id: remoteResources.agentId,
skills: skillRefs,
},
local_skills: localSkills.map((skill) => ({
key: skill.key,
name: skill.name,
source: skill.source,
remote_id: remoteResources.skillIds[skill.key],
reused_pending: skill.reusedPending,
reused_tracked: skill.reusedTracked,
})),
config_file: project.configPath,
yaml_written: true,
reused_pending: keySelection.reusedPending,
status,
actions: run.planned.plan.actions,
diagnostics: run.planned.plan.diagnostics,
results,
error: failed?.error,
};
if (format === "json") emitResult(result, format);
else {
emitBare(
`Wrote ${project.configPath} with Agent key '${agentKey}'${localSkills.length ? ` and ${localSkills.length} local Skill declaration(s)` : ""}.`,
);
if (status === "completed") emitBare(`Created Agent '${flags.name.trim()}'.`);
else emitBare(`Scoped create ${status}: ${failed?.error ?? "unknown error"}`);
}
if (failed) {
throw new BailianError(
failed.error ?? "Scoped Agent create failed.",
ExitCode.GENERAL,
"The YAML declarations were kept. Fix the related dependency or provider error, then re-run the same create command.",
);
}
},
});
@@ -0,0 +1,45 @@
import { getRemoteAgent } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { AGENT_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Get a Managed Agent", "zh-CN": "获取托管 Agent 详情" },
auth: "apiKey",
usageArgs: "--agent-id <id> [--agent-version <n>] [--file <path>]",
flags: AGENT_GET_FLAGS,
exampleArgs: ["--agent-id agent_abc", "--agent-id agent_abc --agent-version 3 --output json"],
notes: CREDENTIALS_NOTE,
validate: (flags) =>
flags.agentVersion !== undefined &&
(!Number.isInteger(flags.agentVersion) || flags.agentVersion < 1)
? "--agent-version must be a positive integer."
: undefined,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const agent = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteAgent(runtime, ctx.flags.agentId, {
provider: "bailian",
version: ctx.flags.agentVersion,
});
}),
);
if (format === "json") {
emitResult(agent, format);
return;
}
emitBare(`ID: ${agent.id}`);
emitBare(`Name: ${displayValue(agent.name)}`);
emitBare(`Description: ${displayValue(agent.description, 120)}`);
emitBare(`Version: ${displayValue(agent.version)}`);
emitBare(`Type: ${displayValue(agent.type)}`);
emitBare(`Created: ${displayValue(agent.created_at)}`);
emitBare(`Updated: ${displayValue(agent.updated_at)}`);
},
});
@@ -0,0 +1,53 @@
import { listRemoteAgents } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { AGENT_LIST_FLAGS, agentRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "List Managed Agents", "zh-CN": "列出托管 Agent" },
auth: "apiKey",
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived] [--file <path>]",
flags: AGENT_LIST_FLAGS,
exampleArgs: ["", "--limit 50", "--all --include-archived --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteAgents(runtime, {
provider: "bailian",
limit: ctx.flags.limit,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "agents",
items: result.items,
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
rows: agentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No agents found.",
});
},
});
@@ -0,0 +1,59 @@
import { listRemoteAgents } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import {
emitCollection,
matchesQuery,
searchCursorPages,
validateLimitAndPageLimit,
} from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { AGENT_SEARCH_FLAGS, agentRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Search Managed Agents", "zh-CN": "搜索托管 Agent" },
auth: "apiKey",
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
flags: AGENT_SEARCH_FLAGS,
exampleArgs: ["--query assistant", "--query code --page-limit 20 --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listRemoteAgents(runtime, {
provider: "bailian",
limit: ctx.flags.limit ?? 100,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
(agent) => matchesQuery(ctx.flags.query, agent.id, agent.name, agent.description),
ctx.flags.pageLimit,
);
}),
);
emitCollection({
format,
key: "agents",
items: result.items,
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
rows: agentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching agents found.",
});
},
});
@@ -0,0 +1,52 @@
import { listRemoteAgentVersions } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { AGENT_VERSIONS_FLAGS, agentRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "List Managed Agent versions", "zh-CN": "列出托管 Agent 版本" },
auth: "apiKey",
usageArgs: "--agent-id <id> [--limit <n>] [--page <cursor>] [--all]",
flags: AGENT_VERSIONS_FLAGS,
exampleArgs: ["--agent-id agent_abc", "--agent-id agent_abc --all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteAgentVersions(runtime, ctx.flags.agentId, {
provider: "bailian",
limit: ctx.flags.limit,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "versions",
items: result.items,
headers: ["ID", "NAME", "VERSION", "TYPE", "UPDATED"],
rows: agentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No agent versions found.",
});
},
});
@@ -8,13 +8,9 @@ import {
import { emitBare, emitResult } from "bailian-cli-runtime";
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
import { formatResourceLabel } from "./_engine/address-utils.ts";
import {
assertProviderConfigured,
buildAgentRuntime,
CREDENTIALS_NOTE,
} from "./_engine/config-loader.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
const APPLY_FLAGS = {
@@ -26,21 +22,6 @@ const APPLY_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Target provider (default: all configured)",
"zh-CN": "目标 Provider默认全部已配置项",
},
},
yes: {
type: "switch",
description: {
"en-US": "Confirm and apply without an interactive prompt (required to mutate)",
"zh-CN": "无需交互提示直接确认并应用(执行变更时必填)",
},
},
noRefresh: {
type: "switch",
description: {
@@ -64,9 +45,17 @@ export default defineCommand({
"zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源",
},
auth: "apiKey",
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
risk: {
level: "high",
message: {
"en-US":
"This applies the current plan and may create, update, or delete remote managed Agent resources.",
"zh-CN": "该操作会应用当前计划,可能创建、更新或删除远端托管 Agent 资源。",
},
},
usageArgs: "[--file <path>] [--concurrency <n>]",
flags: APPLY_FLAGS,
exampleArgs: ["--yes", "--provider bailian --yes"],
exampleArgs: ["--yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
@@ -77,7 +66,7 @@ export default defineCommand({
emitResult(
{
would_apply: {
provider: flags.provider ?? "all",
provider: "bailian",
refresh: !flags.noRefresh,
concurrency: flags.concurrency,
},
@@ -92,9 +81,8 @@ export default defineCommand({
const planned = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, {
provider: flags.provider,
provider: "bailian",
refresh: !flags.noRefresh,
quiet: true,
onFeedback: renderAgentFeedback,
@@ -109,11 +97,20 @@ export default defineCommand({
if (format === "json") process.stderr.write(`${line}\n`);
else emitBare(line);
};
if (plan.diagnostics.some((diag) => diag.severity === "error")) {
for (const diag of plan.diagnostics) {
if (diag.severity === "error") emitProgress(`[error] ${diag.code}: ${diag.message}`);
const errorDiagnostics = plan.diagnostics.filter(
(diagnostic) => diagnostic.severity === "error",
);
if (errorDiagnostics.length > 0) {
for (const diagnostic of errorDiagnostics) {
emitProgress(`[error] ${diagnostic.code}: ${diagnostic.message}`);
}
throw new BailianError("Cannot apply: resolve the errors above first.", ExitCode.GENERAL);
throw new BailianError(
formatAgentDiagnosticFailure(
errorDiagnostics,
"Cannot apply: resolve the errors above first.",
),
ExitCode.GENERAL,
);
}
const actionable = plan.actions.filter((action) => action.action !== "no-op");
@@ -124,23 +121,11 @@ export default defineCommand({
return;
}
const creates = actionable.filter((action) => action.action === "create").length;
const updates = actionable.filter((action) => action.action === "update").length;
const deletes = planned.destructiveActions;
for (const action of actionable) {
const icon = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitProgress(` ${icon} ${formatResourceLabel(action.address)}`);
}
if (!flags.yes) {
throw new BailianError(
`Refusing to apply ${actionable.length} change(s) (${creates} create, ${updates} update, ${deletes.length} destroy) without confirmation.`,
ExitCode.USAGE,
"Review with `bl managed-agent plan`, then re-run with --yes to apply.",
);
}
const result = await withAgentErrors(() =>
withStdoutProtected(() =>
executePlannedProject(planned, {
@@ -152,7 +137,8 @@ export default defineCommand({
);
const succeeded = result.results.filter((entry) => entry.status === "success").length;
const failed = result.results.filter((entry) => entry.status === "failed").length;
const failedResults = result.results.filter((entry) => entry.status === "failed");
const failed = failedResults.length;
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
if (format === "json") {
@@ -161,6 +147,9 @@ export default defineCommand({
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
}
if (failed > 0) throw new BailianError("Apply failed.", ExitCode.GENERAL);
if (failed > 0) {
const firstFailure = failedResults.find((entry) => entry.error);
throw new BailianError(firstFailure?.error ?? "Apply failed.", ExitCode.GENERAL);
}
},
});
@@ -0,0 +1,71 @@
import { setRemoteDeploymentPaused } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import {
DEPLOYMENT_ACTION_TARGET_FLAGS,
resolveDeploymentTarget,
validateDeploymentActionTarget,
} from "./_shared.ts";
export function createSetDeploymentPausedCommand(paused: boolean) {
return defineCommand({
description: paused
? { "en-US": "Pause a Managed Agent deployment", "zh-CN": "暂停托管 Agent Deployment" }
: { "en-US": "Unpause a Managed Agent deployment", "zh-CN": "恢复托管 Agent Deployment" },
auth: "apiKey",
risk: {
level: "high",
message: paused
? {
"en-US":
"This pauses the specified Managed Agent deployment and stops its scheduled executions until resumed.",
"zh-CN": "该操作会暂停指定的托管 Agent Deployment并停止其定时执行直至恢复。",
}
: {
"en-US":
"This resumes the specified Managed Agent deployment and may restart scheduled executions and related usage.",
"zh-CN":
"该操作会恢复指定的托管 Agent Deployment可能重新开始定时执行并产生相关用量。",
},
},
usageArgs: "(--deployment <name> | --deployment-id <id>)",
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc --yes`],
notes: CREDENTIALS_NOTE,
validate: validateDeploymentActionTarget,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{
[paused ? "would_pause_deployment" : "would_unpause_deployment"]:
ctx.flags.deploymentId ?? ctx.flags.deployment,
target_kind: ctx.flags.deploymentId ? "id" : "state_name",
},
format,
);
return;
}
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags);
return {
deploymentId,
deployment: await setRemoteDeploymentPaused(runtime, deploymentId, paused, {
provider: "bailian",
}),
};
}),
);
if (format === "json") {
emitResult({ deployment_id: result.deploymentId, deployment: result.deployment }, format);
} else {
emitBare(`Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.`);
}
},
});
}
@@ -0,0 +1,144 @@
import type { DeploymentInfo, DeploymentRunInfo, ProjectRuntimeContext } from "@openagentpack/sdk";
import { UserError } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
INCLUDE_ARCHIVED_FLAG,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const DEPLOYMENT_ID_FLAG = {
deploymentId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Deployment ID", "zh-CN": "Deployment ID" },
},
} as const;
const DEPLOYMENT_LIST_FILTER_FLAGS = {
agentId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Filter by agent ID", "zh-CN": "按 Agent ID 筛选" },
},
status: {
type: "string",
valueHint: "<status>",
choices: ["active", "paused"] as const,
description: { "en-US": "Filter by deployment status", "zh-CN": "按 Deployment 状态筛选" },
},
...INCLUDE_ARCHIVED_FLAG,
createdAtGte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or after this timestamp",
"zh-CN": "创建时间不早于该时间戳",
},
},
createdAtLte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or before this timestamp",
"zh-CN": "创建时间不晚于该时间戳",
},
},
} as const;
export const DEPLOYMENT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
...DEPLOYMENT_LIST_FILTER_FLAGS,
};
export const DEPLOYMENT_GET_FLAGS = { ...API_TARGET_FLAGS, ...DEPLOYMENT_ID_FLAG };
export const DEPLOYMENT_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
query: SEARCH_FLAGS.query,
...DEPLOYMENT_LIST_FILTER_FLAGS,
};
export const DEPLOYMENT_RUNS_LIST_FLAGS = {
...API_TARGET_FLAGS,
...DEPLOYMENT_ID_FLAG,
...CURSOR_FLAGS,
};
export const DEPLOYMENT_RUNS_GET_FLAGS = {
...API_TARGET_FLAGS,
runId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Deployment run ID", "zh-CN": "Deployment Run ID" },
},
} as const;
export const DEPLOYMENT_ACTION_TARGET_FLAGS = {
...API_TARGET_FLAGS,
deployment: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Logical deployment name in agents.yaml/state",
"zh-CN": "agents.yaml/State 中的逻辑 Deployment 名称",
},
},
deploymentId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Direct deployment ID", "zh-CN": "直接指定 Deployment ID" },
},
} as const;
export function validateDeploymentActionTarget(flags: {
deployment?: string;
deploymentId?: string;
}): string | undefined {
if (Boolean(flags.deployment) === Boolean(flags.deploymentId)) {
return "Provide exactly one of --deployment or --deployment-id.";
}
return undefined;
}
export function deploymentRows(deployments: DeploymentInfo[]): string[][] {
return deployments.map((deployment) => [
displayValue(deployment.id),
deployment.status,
displayValue(deployment.schedule?.expression),
displayValue(deployment.paused_reason?.type),
]);
}
export function deploymentRunRows(runs: DeploymentRunInfo[]): string[][] {
return runs.map((run) => [
run.id,
displayValue(run.deployment_id),
displayValue(run.session_id),
displayValue(run.status),
displayValue(run.created_at),
]);
}
export async function resolveDeploymentTarget(
runtime: ProjectRuntimeContext,
options: { deployment?: string; deploymentId?: string },
): Promise<string> {
if (options.deploymentId) return options.deploymentId;
const state = runtime.state.getResource({
provider: "bailian",
type: "deployment",
name: options.deployment!,
});
if (!state?.remote_id) {
throw new UserError(
`Deployment '${options.deployment}' is not tracked in state. Use --deployment-id or run managed-agent apply/import first.`,
);
}
return state.remote_id;
}
@@ -0,0 +1,284 @@
import type { DeploymentDecl } from "@openagentpack/sdk";
import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import {
loadScopedCreateProject,
parseJsonInputs,
parseMetadata,
resolveCandidateDeclaration,
runScopedTopLevelCreate,
SCOPED_CREATE_NOTE,
} from "../_engine/scoped-create.ts";
const FLAGS = {
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Remote Deployment display name; the YAML key is generated automatically",
"zh-CN": "远端 Deployment 显示名称YAML key 将自动生成",
},
},
agent: {
type: "string",
valueHint: "<yaml-key>",
required: true,
description: {
"en-US": "Existing Agent key from agents.yaml",
"zh-CN": "agents.yaml 中已有的 Agent key",
},
},
agentVersion: {
type: "number",
valueHint: "<number>",
description: { "en-US": "Agent version", "zh-CN": "Agent 版本" },
},
environment: {
type: "string",
valueHint: "<yaml-key>",
description: {
"en-US": "Existing Environment key from agents.yaml",
"zh-CN": "agents.yaml 中已有的 Environment key",
},
},
vault: {
type: "array",
valueHint: "<yaml-key>",
description: {
"en-US": "Existing Vault key from agents.yaml (repeatable)",
"zh-CN": "agents.yaml 中已有的 Vault key可重复",
},
},
message: {
type: "array",
valueHint: "<text>",
description: {
"en-US": "Initial user message (repeatable)",
"zh-CN": "初始用户消息(可重复)",
},
},
event: {
type: "array",
valueHint: "<json|@path>",
description: {
"en-US": "Initial user.message or system.message JSON (repeatable)",
"zh-CN": "初始 user.message 或 system.message JSON可重复",
},
},
resource: {
type: "array",
valueHint: "<json|@path>",
description: {
"en-US": "File Resource JSON with source or file_id (repeatable)",
"zh-CN": "包含 source 或 file_id 的 File Resource JSON可重复",
},
},
schedule: {
type: "string",
valueHint: "<cron>",
description: { "en-US": "Five-field cron expression", "zh-CN": "五段式 Cron 表达式" },
},
timezone: {
type: "string",
valueHint: "<timezone>",
description: { "en-US": "IANA schedule timezone", "zh-CN": "Schedule 的 IANA 时区" },
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Deployment description", "zh-CN": "Deployment 描述" },
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Metadata entry (repeatable)",
"zh-CN": "Metadata 条目(可重复)",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and run the scoped remote create",
"zh-CN": "写入 YAML 并执行定向远端创建",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Declare and create one Managed Agent Deployment through a scoped YAML apply",
"zh-CN": "通过定向 YAML Apply 声明并创建一个托管 Agent Deployment",
},
auth: "apiKey",
usageArgs:
"--name <name> --agent <yaml-key> (--message <text>... | --event <json|@path>...) [--agent-version <number>] [--environment <yaml-key>] [--vault <yaml-key>...] [--resource <json|@path>...] [--schedule <cron> --timezone <timezone>] [--description <text>] [--metadata <key=value>...] [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: [
{
"en-US": '--name Daily --agent assistant --message "Generate the report"',
"zh-CN": '--name Daily --agent assistant --message "生成报告"',
},
{
"en-US":
'--name Daily --agent assistant --event \'{"type":"system.message","content":"Be concise"}\' --yes',
"zh-CN":
'--name Daily --agent assistant --event \'{"type":"system.message","content":"保持简洁"}\' --yes',
},
],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US":
"Initial Events must contain 1-50 user.message/system.message entries. --resource accepts only File Resources in this release.",
"zh-CN":
"Initial Events 必须包含 150 条 user.message/system.message本期 --resource 仅接受 File Resource。",
},
{
"en-US":
"Use either repeatable --message values or repeatable --event values; the two input forms cannot be mixed.",
"zh-CN": "重复使用 --message 或重复使用 --event两种输入形式不能混用。",
},
],
validate: (flags) => {
if (!flags.name.trim()) return "--name must not be empty.";
if (!flags.agent.trim()) return "--agent must not be empty.";
if (flags.message?.length && flags.event?.length) {
return "--message and --event cannot be used together.";
}
if (!flags.message?.length && !flags.event?.length)
return "Pass at least one --message or --event.";
if (Boolean(flags.schedule) !== Boolean(flags.timezone)) {
return "--schedule and --timezone must be provided together.";
}
if (
flags.agentVersion !== undefined &&
(!Number.isInteger(flags.agentVersion) || flags.agentVersion < 1)
) {
return "--agent-version must be a positive integer.";
}
return undefined;
},
async run(ctx) {
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
const initialEvents = [
...(ctx.flags.message ?? []).map((content) => ({ type: "user.message" as const, content })),
...validateEvents(await parseJsonInputs(ctx.flags.event, "event")),
];
if (initialEvents.length < 1 || initialEvents.length > 50) {
throw new BailianError("Initial Events must contain 1-50 entries.", ExitCode.USAGE);
}
const resources = validateResources(await parseJsonInputs(ctx.flags.resource, "resource"));
if (ctx.flags.schedule && ctx.flags.schedule.trim().split(/\s+/).length !== 5) {
throw new BailianError("--schedule must be a five-field cron expression.", ExitCode.USAGE);
}
if (ctx.flags.timezone) validateTimezone(ctx.flags.timezone);
const rawDeclaration: DeploymentDecl = {
name: ctx.flags.name.trim(),
agent: ctx.flags.agent,
agent_version: ctx.flags.agentVersion,
environment: ctx.flags.environment,
vaults: ctx.flags.vault,
resources,
initial_events: initialEvents,
schedule:
ctx.flags.schedule && ctx.flags.timezone
? { expression: ctx.flags.schedule, timezone: ctx.flags.timezone }
: undefined,
description: ctx.flags.description,
provider: project.provider,
metadata: parseMetadata(ctx.flags.metadata),
};
const resolvedDeclaration = await resolveCandidateDeclaration({
project,
group: "deployments",
rawDeclaration: rawDeclaration as unknown as Record<string, unknown>,
});
await runScopedTopLevelCreate({
host: ctx,
project,
group: "deployments",
resourceType: "deployment",
displayName: ctx.flags.name.trim(),
rawDeclaration: rawDeclaration as unknown as Record<string, unknown>,
resolvedDeclaration,
existingDeclarations: (project.config.deployments ?? {}) as unknown as Record<
string,
Record<string, unknown>
>,
effectiveName: (key, declaration) =>
typeof declaration.name === "string" ? declaration.name : key,
fallbackKey: "deployment",
yes: ctx.flags.yes,
});
},
});
function validateEvents(
values: unknown[],
): Array<{ type: "user.message" | "system.message"; content: string }> {
return values.map((value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new BailianError("Each --event must be a JSON object.", ExitCode.USAGE);
}
const event = value as Record<string, unknown>;
if (event.type !== "user.message" && event.type !== "system.message") {
throw new BailianError(
"--event type must be user.message or system.message.",
ExitCode.USAGE,
);
}
if (typeof event.content !== "string" || !event.content.trim()) {
throw new BailianError("--event content must be a non-empty string.", ExitCode.USAGE);
}
return { type: event.type, content: event.content };
});
}
function validateResources(values: unknown[]): NonNullable<DeploymentDecl["resources"]> {
return values.map((value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new BailianError("Each --resource must be a JSON object.", ExitCode.USAGE);
}
const resource = value as Record<string, unknown>;
if (resource.type !== "file") {
throw new BailianError("--resource currently accepts only type=file.", ExitCode.USAGE);
}
const source = typeof resource.source === "string" ? resource.source : undefined;
const fileId = typeof resource.file_id === "string" ? resource.file_id : undefined;
if (Boolean(source) === Boolean(fileId)) {
throw new BailianError(
"A File Resource must provide exactly one of source or file_id.",
ExitCode.USAGE,
);
}
if (resource.mount_path !== undefined && typeof resource.mount_path !== "string") {
throw new BailianError("File Resource mount_path must be a string.", ExitCode.USAGE);
}
return {
type: "file" as const,
source,
file_id: fileId,
mount_path: resource.mount_path as string | undefined,
};
});
}
function validateTimezone(timezone: string): void {
try {
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
} catch {
throw new BailianError(`Invalid IANA timezone: ${timezone}`, ExitCode.USAGE);
}
}
@@ -0,0 +1,38 @@
import { getRemoteDeployment } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { DEPLOYMENT_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Get a Managed Agent deployment",
"zh-CN": "获取托管 Agent Deployment 详情",
},
auth: "apiKey",
usageArgs: "--deployment-id <id>",
flags: DEPLOYMENT_GET_FLAGS,
exampleArgs: ["--deployment-id dep_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const deployment = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteDeployment(runtime, ctx.flags.deploymentId, {
provider: "bailian",
});
}),
);
if (format === "json") emitResult(deployment, format);
else {
emitBare(`ID: ${displayValue(deployment.id)}`);
emitBare(`Status: ${deployment.status}`);
emitBare(`Schedule:${displayValue(deployment.schedule?.expression)}`);
emitBare(`Paused: ${displayValue(deployment.paused_reason)}`);
}
},
});
@@ -0,0 +1,57 @@
import { listRemoteDeployments } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { DEPLOYMENT_LIST_FLAGS, deploymentRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "List Managed Agent deployments", "zh-CN": "列出托管 Agent Deployment" },
auth: "apiKey",
usageArgs: "[--agent-id <id>] [--status active|paused] [--limit <n>] [--page <cursor>] [--all]",
flags: DEPLOYMENT_LIST_FLAGS,
exampleArgs: ["", "--status active --all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteDeployments(runtime, {
provider: "bailian",
agent_id: ctx.flags.agentId,
status: ctx.flags.status,
include_archived: ctx.flags.includeArchived,
created_at_gte: ctx.flags.createdAtGte,
created_at_lte: ctx.flags.createdAtLte,
limit: ctx.flags.limit,
page,
});
return {
items: response.deployments,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "deployments",
items: result.items,
headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"],
rows: deploymentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No deployments found.",
});
},
});
@@ -0,0 +1,3 @@
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
export default createSetDeploymentPausedCommand(true);
@@ -0,0 +1,58 @@
import { runRemoteDeployment } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import {
DEPLOYMENT_ACTION_TARGET_FLAGS,
resolveDeploymentTarget,
validateDeploymentActionTarget,
} from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Run a Managed Agent deployment now",
"zh-CN": "立即运行托管 Agent Deployment",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This immediately starts a run for the specified Managed Agent deployment and may incur usage or trigger configured actions.",
"zh-CN": "该操作会立即运行指定的托管 Agent Deployment可能产生用量或触发已配置的动作。",
},
},
usageArgs: "(--deployment <name> | --deployment-id <id>)",
flags: DEPLOYMENT_ACTION_TARGET_FLAGS,
exampleArgs: ["--deployment daily-report --dry-run", "--deployment-id dep_abc --yes"],
notes: CREDENTIALS_NOTE,
validate: validateDeploymentActionTarget,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{
would_run_deployment: ctx.flags.deploymentId ?? ctx.flags.deployment,
target_kind: ctx.flags.deploymentId ? "id" : "state_name",
},
format,
);
return;
}
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
const deploymentId = await resolveDeploymentTarget(runtime, ctx.flags);
return {
deploymentId,
run: await runRemoteDeployment(runtime, deploymentId, { provider: "bailian" }),
};
}),
);
if (format === "json")
emitResult({ deployment_id: result.deploymentId, ...result.run }, format);
else emitBare(`Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}`);
},
});
@@ -0,0 +1,38 @@
import { getRemoteDeploymentRun } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
import { withStdoutProtected } from "../../_engine/console-capture.ts";
import { withAgentErrors } from "../../_engine/errors.ts";
import { DEPLOYMENT_RUNS_GET_FLAGS } from "../_shared.ts";
export default defineCommand({
description: {
"en-US": "Get a Managed Agent deployment run",
"zh-CN": "获取托管 Agent Deployment Run 详情",
},
auth: "apiKey",
usageArgs: "--run-id <id>",
flags: DEPLOYMENT_RUNS_GET_FLAGS,
exampleArgs: ["--run-id run_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const run = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteDeploymentRun(runtime, ctx.flags.runId, { provider: "bailian" });
}),
);
if (format === "json") emitResult(run, format);
else {
emitBare(`ID: ${run.id}`);
emitBare(`Deployment: ${displayValue(run.deployment_id)}`);
emitBare(`Session: ${displayValue(run.session_id)}`);
emitBare(`Status: ${displayValue(run.status)}`);
emitBare(`Created: ${displayValue(run.created_at)}`);
emitBare(`Error: ${displayValue(run.error)}`);
}
},
});
@@ -0,0 +1,55 @@
import { listRemoteDeploymentRuns } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
import { withStdoutProtected } from "../../_engine/console-capture.ts";
import { withAgentErrors } from "../../_engine/errors.ts";
import { fetchAllPages } from "../../_engine/pagination.ts";
import { DEPLOYMENT_RUNS_LIST_FLAGS, deploymentRunRows } from "../_shared.ts";
export default defineCommand({
description: {
"en-US": "List runs for a Managed Agent deployment",
"zh-CN": "列出托管 Agent Deployment Run",
},
auth: "apiKey",
usageArgs: "--deployment-id <id> [--limit <n>] [--page <cursor>] [--all]",
flags: DEPLOYMENT_RUNS_LIST_FLAGS,
exampleArgs: ["--deployment-id dep_abc", "--deployment-id dep_abc --all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteDeploymentRuns(runtime, ctx.flags.deploymentId, {
provider: "bailian",
limit: ctx.flags.limit,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "runs",
items: result.items,
headers: ["ID", "DEPLOYMENT", "SESSION", "STATUS", "CREATED"],
rows: deploymentRunRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No deployment runs found.",
});
},
});
@@ -0,0 +1,67 @@
import { listRemoteDeployments } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { DEPLOYMENT_SEARCH_FLAGS, deploymentRows } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Search Managed Agent deployments",
"zh-CN": "搜索托管 Agent Deployment",
},
auth: "apiKey",
usageArgs: "--query <text> [--limit <n>] [--page <cursor>] [--all]",
flags: DEPLOYMENT_SEARCH_FLAGS,
exampleArgs: ["--query report", "--query nightly --all --output json"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US": "Deployment search maps --query to the provider's server-side keyword parameter.",
"zh-CN": "Deployment 搜索会把 --query 映射为 Provider 服务端 keyword 参数。",
},
],
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteDeployments(runtime, {
provider: "bailian",
keyword: ctx.flags.query,
agent_id: ctx.flags.agentId,
status: ctx.flags.status,
include_archived: ctx.flags.includeArchived,
created_at_gte: ctx.flags.createdAtGte,
created_at_lte: ctx.flags.createdAtLte,
limit: ctx.flags.limit,
page,
});
return {
items: response.deployments,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "deployments",
items: result.items,
headers: ["ID", "STATUS", "SCHEDULE", "PAUSED REASON"],
rows: deploymentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No matching deployments found.",
});
},
});
@@ -0,0 +1,3 @@
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
export default createSetDeploymentPausedCommand(false);
@@ -21,13 +21,6 @@ const DESTROY_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Confirm and destroy without an interactive prompt (required)",
"zh-CN": "无需交互提示直接确认并销毁(必填)",
},
},
cascade: {
type: "switch",
description: {
@@ -43,7 +36,15 @@ export default defineCommand({
"zh-CN": "销毁 State 中跟踪的全部托管 Agent 资源",
},
auth: "apiKey",
usageArgs: "[--file <path>] [--yes] [--cascade]",
risk: {
level: "high",
message: {
"en-US":
"This deletes every managed Agent resource tracked in state; --cascade may also delete dependent resources.",
"zh-CN": "该操作会删除 State 中跟踪的全部托管 Agent 资源;--cascade 还可能删除依赖资源。",
},
},
usageArgs: "[--file <path>] [--cascade]",
flags: DESTROY_FLAGS,
exampleArgs: ["--yes", "--yes --cascade"],
notes: CREDENTIALS_NOTE,
@@ -67,7 +68,16 @@ export default defineCommand({
const planned = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return planDestroyProjectContext(runtime);
const planned = planDestroyProjectContext(runtime);
return {
...planned,
resources: planned.resources.filter(
(resource) => resource.address.provider === "bailian",
),
defaultMemoryStores: (planned.defaultMemoryStores ?? []).filter(
(memoryStore) => memoryStore.provider === "bailian",
),
};
}),
);
@@ -86,14 +96,6 @@ export default defineCommand({
else emitBare(line);
}
if (!flags.yes) {
throw new BailianError(
`Refusing to destroy ${resources.length} resource(s) without confirmation.`,
ExitCode.USAGE,
"Re-run with --yes to destroy (add --cascade to remove dependents).",
);
}
const result = await withAgentErrors(() =>
withStdoutProtected(() =>
destroyPlannedProjectResources(planned, {
@@ -0,0 +1,41 @@
import type { CloudEnvironment } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
INCLUDE_ARCHIVED_FLAG,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const ENVIRONMENT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const ENVIRONMENT_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const ENVIRONMENT_GET_FLAGS = {
...API_TARGET_FLAGS,
environmentId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Environment ID", "zh-CN": "Environment ID" },
},
} as const;
export function environmentRows(environments: CloudEnvironment[]): string[][] {
return environments.map((environment) => [
environment.id,
displayValue(environment.name),
displayValue(environment.scope),
displayValue(environment.version),
displayValue(environment.updated_at),
]);
}
@@ -0,0 +1,141 @@
import { defineCommand, type FlagsDef } from "bailian-cli-core";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import {
loadScopedCreateProject,
parseMetadata,
resolveCandidateDeclaration,
runScopedTopLevelCreate,
SCOPED_CREATE_NOTE,
} from "../_engine/scoped-create.ts";
const FLAGS = {
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Remote Environment display name; the YAML key is generated automatically",
"zh-CN": "远端 Environment 显示名称YAML key 将自动生成",
},
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Environment description", "zh-CN": "Environment 描述" },
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Metadata entry (repeatable)",
"zh-CN": "Metadata 条目(可重复)",
},
},
apt: packageFlag("APT"),
pip: packageFlag("pip"),
npm: packageFlag("npm"),
cargo: packageFlag("Cargo"),
gem: packageFlag("Ruby gem"),
go: packageFlag("Go"),
file: fileFlag(),
yes: yesFlag(),
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Declare and create one Managed Agent Environment through a scoped YAML apply",
"zh-CN": "通过定向 YAML Apply 声明并创建一个托管 Agent Environment",
},
auth: "apiKey",
usageArgs:
"--name <name> [--description <text>] [--metadata <key=value>...] [--apt <package>...] [--pip <package>...] [--npm <package>...] [--cargo <package>...] [--gem <package>...] [--go <package>...] [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: [
"--name Development",
"--name Development --pip pandas --npm typescript --metadata owner=platform --yes",
],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US":
"Creates a cloud Environment with unrestricted networking. Without --yes, only previews the generated YAML key and scoped plan.",
"zh-CN":
"创建使用 unrestricted 网络的 cloud Environment。不带 --yes 时仅预览自动生成的 YAML key 和定向计划。",
},
],
validate: (flags) => (!flags.name.trim() ? "--name must not be empty." : undefined),
async run(ctx) {
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
const packages = Object.fromEntries(
["apt", "pip", "npm", "cargo", "gem", "go"]
.map((manager) => [manager, ctx.flags[manager as keyof typeof ctx.flags]])
.filter((entry) => Array.isArray(entry[1]) && entry[1].length > 0),
);
const rawDeclaration: Record<string, unknown> = {
name: ctx.flags.name.trim(),
description: ctx.flags.description,
provider: project.provider,
config: {
type: "cloud",
networking: { type: "unrestricted" },
...(Object.keys(packages).length > 0 ? { packages } : {}),
},
metadata: parseMetadata(ctx.flags.metadata),
};
const resolvedDeclaration = await resolveCandidateDeclaration({
project,
group: "environments",
rawDeclaration,
});
await runScopedTopLevelCreate({
host: ctx,
project,
group: "environments",
resourceType: "environment",
displayName: ctx.flags.name.trim(),
rawDeclaration,
resolvedDeclaration,
existingDeclarations: (project.config.environments ?? {}) as unknown as Record<
string,
Record<string, unknown>
>,
effectiveName: (key, declaration) =>
typeof declaration.name === "string" ? declaration.name : key,
fallbackKey: "environment",
yes: ctx.flags.yes,
});
},
});
function packageFlag(manager: string) {
return {
type: "array" as const,
valueHint: "<package>",
description: {
"en-US": `${manager} package (repeatable)`,
"zh-CN": `${manager} 软件包(可重复)`,
},
};
}
function fileFlag() {
return {
type: "string" as const,
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
};
}
function yesFlag() {
return {
type: "switch" as const,
description: {
"en-US": "Write YAML and run the scoped remote create",
"zh-CN": "写入 YAML 并执行定向远端创建",
},
};
}
@@ -0,0 +1,41 @@
import { getRemoteEnvironment } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { ENVIRONMENT_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Get a Managed Agent environment",
"zh-CN": "获取托管 Agent 环境详情",
},
auth: "apiKey",
usageArgs: "--environment-id <id>",
flags: ENVIRONMENT_GET_FLAGS,
exampleArgs: ["--environment-id env_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const environment = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteEnvironment(runtime, ctx.flags.environmentId, {
provider: "bailian",
});
}),
);
if (format === "json") {
emitResult(environment, format);
return;
}
emitBare(`ID: ${environment.id}`);
emitBare(`Name: ${displayValue(environment.name)}`);
emitBare(`Description: ${displayValue(environment.description, 120)}`);
emitBare(`Scope: ${displayValue(environment.scope)}`);
emitBare(`Version: ${displayValue(environment.version)}`);
emitBare(`Updated: ${displayValue(environment.updated_at)}`);
},
});
@@ -0,0 +1,53 @@
import { listRemoteEnvironments } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { ENVIRONMENT_LIST_FLAGS, environmentRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "List Managed Agent environments", "zh-CN": "列出托管 Agent 环境" },
auth: "apiKey",
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]",
flags: ENVIRONMENT_LIST_FLAGS,
exampleArgs: ["", "--all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteEnvironments(runtime, {
provider: "bailian",
limit: ctx.flags.limit,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "environments",
items: result.items,
headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"],
rows: environmentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No environments found.",
});
},
});
@@ -0,0 +1,68 @@
import { listRemoteEnvironments } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import {
emitCollection,
matchesQuery,
searchCursorPages,
validateLimitAndPageLimit,
} from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { ENVIRONMENT_SEARCH_FLAGS, environmentRows } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Search Managed Agent environments",
"zh-CN": "搜索托管 Agent 环境",
},
auth: "apiKey",
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
flags: ENVIRONMENT_SEARCH_FLAGS,
exampleArgs: ["--query sandbox", "--query production --page-limit 20 --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listRemoteEnvironments(runtime, {
provider: "bailian",
limit: ctx.flags.limit ?? 100,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
(environment) =>
matchesQuery(
ctx.flags.query,
environment.id,
environment.name,
environment.description,
),
ctx.flags.pageLimit,
);
}),
);
emitCollection({
format,
key: "environments",
items: result.items,
headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"],
rows: environmentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching environments found.",
});
},
});
@@ -0,0 +1,316 @@
import { basename } from "node:path";
import type { ProviderFileInfo } from "@openagentpack/sdk";
import {
deleteFile,
downloadRemoteFile,
getFileInfo,
listRemoteFiles,
uploadFile,
} from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
emitCollection,
matchesQuery,
SEARCH_FLAGS,
searchCursorPages,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { inferMimeType, readInputFile, writeOutputFile } from "./_engine/output-file.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const FILE_ID_FLAG = {
fileId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Remote file ID", "zh-CN": "远端 File ID" },
},
} as const;
const SCOPE_ID_FLAG = {
scopeId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Filter by scope ID", "zh-CN": "按 Scope ID 筛选" },
},
} as const;
const UPLOAD_FLAGS = {
...API_TARGET_FLAGS,
path: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Local file path", "zh-CN": "本地文件路径" },
},
filename: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" },
},
mimeType: {
type: "string",
valueHint: "<type>",
description: { "en-US": "MIME type override", "zh-CN": "覆盖 MIME 类型" },
},
} as const;
const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SCOPE_ID_FLAG };
const SEARCH_RESOURCE_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...SCOPE_ID_FLAG,
};
const GET_FLAGS = { ...API_TARGET_FLAGS, ...FILE_ID_FLAG };
const DOWNLOAD_FLAGS = {
...GET_FLAGS,
outputFile: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Destination path", "zh-CN": "目标路径" },
},
force: {
type: "switch",
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
},
} as const;
const DELETE_FLAGS = {
...GET_FLAGS,
} as const;
function fileRows(files: ProviderFileInfo[]): string[][] {
return files.map((file) => [
file.id,
displayValue(file.filename),
displayValue(file.status),
String(file.size_bytes),
displayValue(file.scope?.id),
displayValue(file.created_at),
]);
}
export const managedAgentFileUpload = defineCommand({
description: { "en-US": "Upload a Managed Agent file", "zh-CN": "上传托管 Agent 文件" },
auth: "apiKey",
usageArgs: "--path <path> [--filename <name>] [--mime-type <type>]",
flags: UPLOAD_FLAGS,
exampleArgs: ["--path ./report.pdf"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{
would_upload_file: ctx.flags.path,
filename: ctx.flags.filename ?? basename(ctx.flags.path),
mime_type: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path),
},
format,
);
return;
}
const content = await readInputFile(ctx.flags.path);
const file = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return uploadFile(runtime, content, ctx.flags.filename ?? basename(ctx.flags.path), {
provider: "bailian",
mimeType: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path),
});
}),
);
if (format === "json") emitResult(file, format);
else emitBare(`File uploaded: ${file.id} (${file.filename})`);
},
});
export const managedAgentFileList = defineCommand({
description: { "en-US": "List Managed Agent files", "zh-CN": "列出托管 Agent 文件" },
auth: "apiKey",
usageArgs: "[--scope-id <id>] [--limit <n>] [--page <cursor>] [--all]",
flags: LIST_FLAGS,
exampleArgs: ["", "--scope-id sess_abc --all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteFiles(runtime, {
provider: "bailian",
scope_id: ctx.flags.scopeId,
limit: ctx.flags.limit,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "files",
items: result.items,
headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"],
rows: fileRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No files found.",
});
},
});
export const managedAgentFileGet = defineCommand({
description: { "en-US": "Get Managed Agent file metadata", "zh-CN": "获取托管 Agent 文件元数据" },
auth: "apiKey",
usageArgs: "--file-id <id>",
flags: GET_FLAGS,
exampleArgs: ["--file-id file_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const file = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getFileInfo(runtime, ctx.flags.fileId, { provider: "bailian" });
}),
);
if (format === "json") emitResult(file, format);
else {
emitBare(`ID: ${file.id}`);
emitBare(`Filename: ${file.filename}`);
emitBare(`MIME: ${file.mime_type}`);
emitBare(`Bytes: ${file.size_bytes}`);
emitBare(`Status: ${displayValue(file.status)}`);
emitBare(`Scope: ${displayValue(file.scope?.id)}`);
}
},
});
export const managedAgentFileSearch = defineCommand({
description: { "en-US": "Search Managed Agent files", "zh-CN": "搜索托管 Agent 文件" },
auth: "apiKey",
usageArgs: "--query <text> [--scope-id <id>] [--limit <n>] [--page-limit <n>]",
flags: SEARCH_RESOURCE_FLAGS,
exampleArgs: ["--query report", "--query pdf --scope-id sess_abc --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listRemoteFiles(runtime, {
provider: "bailian",
scope_id: ctx.flags.scopeId,
limit: ctx.flags.limit ?? 100,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
(file) => matchesQuery(ctx.flags.query, file.id, file.filename, file.mime_type),
ctx.flags.pageLimit,
);
}),
);
emitCollection({
format,
key: "files",
items: result.items,
headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"],
rows: fileRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching files found.",
});
},
});
export const managedAgentFileDownload = defineCommand({
description: {
"en-US": "Download Managed Agent file content",
"zh-CN": "下载托管 Agent 文件内容",
},
auth: "apiKey",
usageArgs: "--file-id <id> --output-file <path> [--force]",
flags: DOWNLOAD_FLAGS,
exampleArgs: ["--file-id file_abc --output-file ./artifact.pdf"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{ would_download_file: ctx.flags.fileId, output_file: ctx.flags.outputFile },
format,
);
return;
}
const content = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return downloadRemoteFile(runtime, ctx.flags.fileId, { provider: "bailian" });
}),
);
const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force);
if (format === "json")
emitResult({ downloaded: ctx.flags.fileId, output_file: outputFile }, format);
else emitBare(`File downloaded to ${outputFile}`);
},
});
export const managedAgentFileDelete = defineCommand({
description: { "en-US": "Delete a Managed Agent file", "zh-CN": "删除托管 Agent 文件" },
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the specified remote Managed Agent file.",
"zh-CN": "该操作会永久删除指定的远端托管 Agent 文件。",
},
},
usageArgs: "--file-id <id>",
flags: DELETE_FLAGS,
exampleArgs: ["--file-id file_abc --dry-run", "--file-id file_abc --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult({ would_delete_file: ctx.flags.fileId }, format);
return;
}
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
await deleteFile(runtime, ctx.flags.fileId, { provider: "bailian" });
}),
);
if (format === "json") emitResult({ deleted: ctx.flags.fileId }, format);
else emitBare(`File ${ctx.flags.fileId} deleted.`);
},
});
@@ -15,45 +15,17 @@ agents.state.json
.env
`;
const PROVIDERS = ["bailian", "claude", "qoder", "ark", "all"] as const;
const PROVIDER_BLOCKS: Record<string, string> = {
bailian: ` bailian:\n # bl auth login --api-key <key> sets DASHSCOPE_API_KEY; --base-url <url> sets BAILIAN_BASE_URL\n api_key: \${DASHSCOPE_API_KEY}\n base_url: \${BAILIAN_BASE_URL}`,
claude: ` claude:\n api_key: \${ANTHROPIC_API_KEY}`,
qoder: ` qoder:\n api_key: \${QODER_PAT}\n gateway: "https://api.qoder.com/api/v1/cloud"`,
ark: ` ark:\n api_key: \${ARK_API_KEY}`,
};
const SINGLE_MODEL: Record<string, string> = {
bailian: ` model: qwen3.8-max`,
claude: ` model: claude-sonnet-4-6`,
qoder: ` model: ultimate`,
ark: ` model: doubao-seed-2-1-pro-260628`,
};
function buildTemplate(options: { provider: string; agentName: string }): string {
const providerBlock =
options.provider === "all"
? `${PROVIDER_BLOCKS.bailian}\n${PROVIDER_BLOCKS.claude}\n${PROVIDER_BLOCKS.qoder}\n${PROVIDER_BLOCKS.ark}`
: PROVIDER_BLOCKS[options.provider]!;
const modelBlock =
options.provider === "all"
? ` model:\n bailian: qwen3.8-max\n claude: claude-sonnet-4-6\n qoder: ultimate\n ark: doubao-seed-2-1-pro-260628`
: SINGLE_MODEL[options.provider]!;
const toolBlock =
options.provider === "bailian"
? "[bash, read, glob, grep]"
: "[read, glob, grep, web_search, web_fetch]";
function buildTemplate(options: { agentName: string }): string {
return `version: "1"
providers:
${providerBlock}
bailian:
# bl auth login --api-key <key> sets DASHSCOPE_API_KEY; --base-url <url> sets BAILIAN_BASE_URL
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: ${options.provider === "all" ? "all" : options.provider}
provider: bailian
environments:
dev:
@@ -65,25 +37,16 @@ environments:
agents:
${options.agentName}:
description: "General-purpose assistant"
${modelBlock}
model: qwen3.8-max
instructions: |
You are a helpful assistant.
environment: dev
tools:
builtin: ${toolBlock}
builtin: [bash, read, glob, grep]
`;
}
const INIT_FLAGS = {
provider: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Provider: bailian, claude, qoder, ark, all (default: bailian)",
"zh-CN": "Providerbailian、claude、qoder、ark、all默认bailian",
},
choices: PROVIDERS,
},
agentName: {
type: "string",
valueHint: "<name>",
@@ -112,13 +75,12 @@ export default defineCommand({
"zh-CN": "创建新的 agents.yaml 模板",
},
auth: "none",
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]",
usageArgs: "[--agent-name <name>] [--file <path>] [--force]",
flags: INIT_FLAGS,
exampleArgs: ["", "--provider bailian --agent-name assistant", "--provider all"],
exampleArgs: ["", "--agent-name assistant"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const provider = flags.provider ?? "bailian";
const agentName = flags.agentName ?? "assistant";
const file = flags.file ?? "agents.yaml";
@@ -141,7 +103,7 @@ export default defineCommand({
emitResult(
{
would_create: file,
provider,
provider: "bailian",
agent: agentName,
would_update_gitignore: wouldUpdateGitignore,
},
@@ -150,7 +112,7 @@ export default defineCommand({
return;
}
const template = buildTemplate({ provider, agentName });
const template = buildTemplate({ agentName });
await writeFile(file, template, "utf8");
if (existsSync(gitignorePath)) {
@@ -163,14 +125,12 @@ export default defineCommand({
}
if (format === "json") {
emitResult({ created: file, provider, agent: agentName }, format);
emitResult({ created: file, provider: "bailian", agent: agentName }, format);
} else {
emitBare(`Created ${file}`);
if (provider === "bailian" || provider === "all") {
emitBare(
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
);
}
emitBare(
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
);
emitBare("Next: edit agents.yaml, then run `bl managed-agent plan`.");
}
},
@@ -8,13 +8,9 @@ import {
import { emitBare, emitResult } from "bailian-cli-runtime";
import { planProjectContext } from "@openagentpack/sdk";
import { formatResourceLabel } from "./_engine/address-utils.ts";
import {
assertProviderConfigured,
buildAgentRuntime,
CREDENTIALS_NOTE,
} from "./_engine/config-loader.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
const PLAN_FLAGS = {
@@ -26,14 +22,6 @@ const PLAN_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Target provider (default: all configured)",
"zh-CN": "目标 Provider默认全部已配置项",
},
},
noRefresh: {
type: "switch",
description: {
@@ -56,9 +44,9 @@ export default defineCommand({
"zh-CN": "显示将应用到 Agent 基础设施的变更",
},
auth: "apiKey",
usageArgs: "[--file <path>] [--provider <name>] [--no-refresh] [--refresh-only]",
usageArgs: "[--file <path>] [--no-refresh] [--refresh-only]",
flags: PLAN_FLAGS,
exampleArgs: ["", "--provider bailian", "--no-refresh"],
exampleArgs: ["", "--no-refresh"],
notes: [
...CREDENTIALS_NOTE,
{
@@ -83,9 +71,8 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file, {
credentials: offline ? "none" : "all",
});
assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, {
provider: flags.provider,
provider: "bailian",
refresh: !offline,
quiet: format === "json",
onFeedback: format === "json" ? undefined : renderAgentFeedback,
@@ -95,17 +82,18 @@ export default defineCommand({
const plan = planned.plan;
const hasErrors = plan.diagnostics.some((diag) => diag.severity === "error");
const failureMessage = formatAgentDiagnosticFailure(plan.diagnostics, "Plan contains errors.");
if (format === "json") {
emitResult(plan, format);
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL);
return;
}
for (const diag of plan.diagnostics) {
emitBare(`[${diag.severity}] ${diag.code}: ${diag.message}`);
}
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL);
const creates = plan.actions.filter((action) => action.action === "create");
const updates = plan.actions.filter((action) => action.action === "update");
@@ -49,14 +49,6 @@ const SESSION_CREATE_FLAGS = {
valueHint: "<title>",
description: { "en-US": "Session title", "zh-CN": "Session 标题" },
},
provider: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Target provider (multi-provider agents)",
"zh-CN": "目标 Provider多 Provider Agent",
},
},
} satisfies FlagsDef;
export default defineCommand({
@@ -83,7 +75,7 @@ export default defineCommand({
{
would_create_session: {
agent: flags.agent ?? "auto",
provider: flags.provider ?? "auto",
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memory_stores: parseMemoryStores(flags.memoryStores),
@@ -101,7 +93,7 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file);
return createSessionForAgent(runtime, {
agent: flags.agent,
provider: flags.provider,
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memoryStores: parseMemoryStores(flags.memoryStores),
@@ -0,0 +1,348 @@
import {
getManagedAgentProviderCapabilities,
getSession,
listRemoteFiles,
listSessionEvents,
type ProjectRuntimeContext,
type ProviderFileInfo,
type ProviderSessionEvent,
type ProviderSessionInfo,
} from "@openagentpack/sdk";
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { API_TARGET_FLAGS } from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { writeOutputFile } from "./_engine/output-file.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const SESSION_ID_FLAG = {
sessionId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
},
} as const;
const DEBUG_FLAGS = { ...API_TARGET_FLAGS, ...SESSION_ID_FLAG };
const EXPORT_FLAGS = {
...DEBUG_FLAGS,
outputFile: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Destination ZIP path", "zh-CN": "目标 ZIP 路径" },
},
force: {
type: "switch",
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
},
} as const;
interface SessionDiagnosticBundle {
session: ProviderSessionInfo;
events: unknown[];
files: ProviderFileInfo[];
capabilities: ReturnType<typeof getManagedAgentProviderCapabilities>;
errors: Array<{ component: "events" | "files"; message: string }>;
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
async function collectSessionDiagnostics(
runtime: ProjectRuntimeContext,
sessionId: string,
provider?: string,
): Promise<SessionDiagnosticBundle> {
// Session lookup is the identity anchor. If it fails, the aggregate is not meaningful.
const session = await getSession(runtime, sessionId, provider);
const providerName =
provider ??
(runtime.providers.size === 1 ? Array.from(runtime.providers.keys())[0]! : "bailian");
const errors: SessionDiagnosticBundle["errors"] = [];
let events: ProviderSessionEvent[] = [];
let files: ProviderFileInfo[] = [];
try {
const result = await fetchAllPages(async (page) => {
const response = await listSessionEvents(runtime, sessionId, {
provider,
limit: 100,
page_token: page,
order: "asc",
});
return { items: response.events, hasMore: response.has_more, nextPage: response.next_page };
}, true);
events = result.items;
} catch (error) {
errors.push({ component: "events", message: errorMessage(error) });
}
try {
const result = await fetchAllPages(async (page) => {
const response = await listRemoteFiles(runtime, {
provider,
scope_id: sessionId,
limit: 100,
page,
});
return { items: response.data, hasMore: response.has_more, nextPage: response.next_page };
}, true);
files = result.items;
} catch (error) {
errors.push({ component: "files", message: errorMessage(error) });
}
return {
session,
events: sanitizeSessionEvents(events),
files,
capabilities: getManagedAgentProviderCapabilities(providerName),
errors,
};
}
const SENSITIVE_MARKER = String.raw`(?:api[_-]?key|access[_-]?key|private[_-]?key|secret|token|authorization|credential|password|signature|signed[_-]?url)`;
const SENSITIVE_KEY = new RegExp(`${SENSITIVE_MARKER}|tool[_-]?input`, "i");
const SENSITIVE_URL_PARAMETER = new RegExp(
`([?&][^=&#\\s]*(?:${SENSITIVE_MARKER})[^=&#\\s]*=)[^&#\\s"'<>),}\\]]*`,
"gi",
);
const SENSITIVE_DOUBLE_QUOTED_VALUE = new RegExp(
`("[^"]*(?:${SENSITIVE_MARKER})[^"]*"\\s*:\\s*")[^"]*(")`,
"gi",
);
const SENSITIVE_SINGLE_QUOTED_VALUE = new RegExp(
`('[^']*(?:${SENSITIVE_MARKER})[^']*'\\s*:\\s*')[^']*(')`,
"gi",
);
const SENSITIVE_ASSIGNMENT = new RegExp(
`(\\b[^\\s=:,;]*(?:${SENSITIVE_MARKER})[^\\s=:,;]*\\s*=\\s*)(?!\\[REDACTED\\])(?:"[^"]*"|'[^']*'|[^\\s,;&]+)`,
"gi",
);
const AUTHORIZATION_VALUE = /\b(authorization\s*:\s*)(?:bearer\s+)?[^\s,;]+/gi;
const BEARER_TOKEN = /\b(bearer\s+)[A-Za-z0-9._~+/=-]+/gi;
const API_KEY_LITERAL = /\bsk-[A-Za-z0-9_-]{8,}\b/g;
function redactSensitiveText(value: string): string {
return value
.replace(SENSITIVE_URL_PARAMETER, "$1[REDACTED]")
.replace(SENSITIVE_DOUBLE_QUOTED_VALUE, "$1[REDACTED]$2")
.replace(SENSITIVE_SINGLE_QUOTED_VALUE, "$1[REDACTED]$2")
.replace(SENSITIVE_ASSIGNMENT, "$1[REDACTED]")
.replace(AUTHORIZATION_VALUE, "$1[REDACTED]")
.replace(BEARER_TOKEN, "$1[REDACTED]")
.replace(API_KEY_LITERAL, "[REDACTED]");
}
export function redactSensitiveValues(value: unknown): unknown {
if (Array.isArray(value)) return value.map(redactSensitiveValues);
if (typeof value === "string") return redactSensitiveText(value);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [
key,
SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactSensitiveValues(entry),
]),
);
}
function jsonBytes(value: unknown): Uint8Array {
return new TextEncoder().encode(`${JSON.stringify(redactSensitiveValues(value), null, 2)}\n`);
}
// Minimal ZIP writer using stored (uncompressed) entries. This avoids shelling out
// and keeps export deterministic across supported Node runtimes.
function crc32(bytes: Uint8Array): number {
let checksum = 0xffffffff;
for (const byte of bytes) {
checksum ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
checksum = (checksum >>> 1) ^ (checksum & 1 ? 0xedb88320 : 0);
}
}
return (checksum ^ 0xffffffff) >>> 0;
}
function concatBytes(parts: Uint8Array[]): Uint8Array {
const total = parts.reduce((sum, part) => sum + part.length, 0);
const output = new Uint8Array(total);
let offset = 0;
for (const part of parts) {
output.set(part, offset);
offset += part.length;
}
return output;
}
function littleEndian(values: Array<[number, number]>): Uint8Array {
const size = values.reduce((sum, [, bytes]) => sum + bytes, 0);
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
let offset = 0;
for (const [value, bytes] of values) {
if (bytes === 2) view.setUint16(offset, value, true);
else view.setUint32(offset, value, true);
offset += bytes;
}
return new Uint8Array(buffer);
}
export function createZip(entries: Array<{ name: string; content: Uint8Array }>): Uint8Array {
const encoder = new TextEncoder();
const localParts: Uint8Array[] = [];
const centralParts: Uint8Array[] = [];
let localOffset = 0;
for (const entry of entries) {
const name = encoder.encode(entry.name);
const checksum = crc32(entry.content);
const localHeader = littleEndian([
[0x04034b50, 4],
[20, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 2],
[checksum, 4],
[entry.content.length, 4],
[entry.content.length, 4],
[name.length, 2],
[0, 2],
]);
const local = concatBytes([localHeader, name, entry.content]);
localParts.push(local);
const centralHeader = littleEndian([
[0x02014b50, 4],
[20, 2],
[20, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 2],
[checksum, 4],
[entry.content.length, 4],
[entry.content.length, 4],
[name.length, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 4],
[localOffset, 4],
]);
centralParts.push(concatBytes([centralHeader, name]));
localOffset += local.length;
}
const central = concatBytes(centralParts);
const end = littleEndian([
[0x06054b50, 4],
[0, 2],
[0, 2],
[entries.length, 2],
[entries.length, 2],
[central.length, 4],
[localOffset, 4],
[0, 2],
]);
return concatBytes([...localParts, central, end]);
}
export const managedAgentSessionDebug = defineCommand({
description: { "en-US": "Aggregate session diagnostics", "zh-CN": "聚合 Session 诊断信息" },
auth: "apiKey",
usageArgs: "--session-id <id>",
flags: DEBUG_FLAGS,
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --output json"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const bundle = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian");
}),
);
const redacted = redactSensitiveValues(bundle);
if (format === "json") {
emitResult(redacted, format);
return;
}
emitBare(`Session: ${bundle.session.id} (${bundle.session.status})`);
emitBare(`Events: ${bundle.events.length}`);
emitBare(`Scoped files: ${bundle.files.length}`);
emitBare(`Partial errors: ${bundle.errors.length}`);
for (const error of bundle.errors) emitBare(` ${error.component}: ${error.message}`);
},
});
export const managedAgentSessionExport = defineCommand({
description: {
"en-US": "Export session diagnostics as a ZIP",
"zh-CN": "将 Session 诊断信息导出为 ZIP",
},
auth: "apiKey",
usageArgs: "--session-id <id> --output-file <path> [--force]",
flags: EXPORT_FLAGS,
exampleArgs: ["--session-id sess_abc --output-file ./session-debug.zip"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US":
"The ZIP contains metadata only; file bodies and credential-like values are excluded/redacted.",
"zh-CN": "ZIP 仅包含元数据;不会包含文件正文,凭证类字段会被移除或脱敏。",
},
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{ would_export_session: ctx.flags.sessionId, output_file: ctx.flags.outputFile },
format,
);
return;
}
const bundle = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian");
}),
);
const manifest = {
schema_version: 1,
generated_at: new Date().toISOString(),
session_id: bundle.session.id,
partial: bundle.errors.length > 0,
counts: {
events: bundle.events.length,
files: bundle.files.length,
errors: bundle.errors.length,
},
contents: ["session.json", "events.json", "files.json", "capabilities.json", "errors.json"],
note: "File bodies are not included. Credential-like fields are redacted.",
};
const zip = createZip([
{ name: "manifest.json", content: jsonBytes(manifest) },
{ name: "session.json", content: jsonBytes(bundle.session) },
{ name: "events.json", content: jsonBytes(bundle.events) },
{ name: "files.json", content: jsonBytes(bundle.files) },
{ name: "capabilities.json", content: jsonBytes(bundle.capabilities) },
{ name: "errors.json", content: jsonBytes(bundle.errors) },
]);
const outputFile = await writeOutputFile(ctx.flags.outputFile, zip, ctx.flags.force);
if (format === "json") {
emitResult(
{ exported: bundle.session.id, output_file: outputFile, partial: manifest.partial },
format,
);
} else {
emitBare(
`Session diagnostics exported to ${outputFile}${manifest.partial ? " (partial)" : ""}`,
);
}
},
});
@@ -20,19 +20,21 @@ const SESSION_DELETE_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Delete a session", "zh-CN": "删除 Session" },
auth: "apiKey",
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
risk: {
level: "high",
message: {
"en-US": "This deletes the specified remote managed Agent Session.",
"zh-CN": "该操作会删除指定的远端托管 Agent Session。",
},
},
usageArgs: "--session-id <id> [--file <path>]",
flags: SESSION_DELETE_FLAGS,
exampleArgs: ["--session-id sess_abc123"],
exampleArgs: ["--session-id sess_abc123 --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
@@ -43,7 +45,7 @@ export default defineCommand({
emitResult(
{
would_delete_session: flags.sessionId,
provider: flags.provider ?? "auto",
provider: "bailian",
config_file: file,
},
format,
@@ -54,7 +56,7 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
await deleteSession(runtime, flags.sessionId, flags.provider);
await deleteSession(runtime, flags.sessionId, "bailian");
}),
);
@@ -0,0 +1,260 @@
import type { ProviderSessionEvent, SessionEventInput } from "@openagentpack/sdk";
import {
isTerminalSessionStatus,
listSessionEvents,
sendRemoteSessionEvents,
SessionEventSchema,
streamSessionEvents,
} from "@openagentpack/sdk";
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
emitCollection,
splitCommaSeparated,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { readJsonInput } from "./_engine/output-file.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const SESSION_ID_FLAG = {
sessionId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
},
} as const;
const EVENT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
...CURSOR_FLAGS,
order: {
type: "string",
valueHint: "<order>",
choices: ["asc", "desc"] as const,
description: { "en-US": "Event order: asc or desc", "zh-CN": "事件顺序asc 或 desc" },
},
types: {
type: "string",
valueHint: "<types>",
description: {
"en-US": "Comma-separated raw event types",
"zh-CN": "原始事件类型,多个以逗号分隔",
},
},
createdAtGte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or after this timestamp",
"zh-CN": "创建时间不早于该时间戳",
},
},
createdAtLte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or before this timestamp",
"zh-CN": "创建时间不晚于该时间戳",
},
},
} as const;
const EVENT_SEND_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
event: {
type: "string",
valueHint: "<json|@path>",
required: true,
description: {
"en-US": "Raw event object/array as JSON or @event.json",
"zh-CN": "原始事件对象/数组 JSON或 @event.json",
},
},
} as const;
const EVENT_STREAM_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
afterId: {
type: "string",
valueHint: "<event-id>",
description: { "en-US": "Resume after this event ID", "zh-CN": "从该 Event ID 之后继续" },
},
} as const;
function eventRows(events: ProviderSessionEvent[]): string[][] {
return events.map((event) => [
displayValue(event.id),
event.raw_type,
displayValue(event.session_thread_id),
displayValue(event.content ?? event.status ?? event.tool_name, 70),
]);
}
function normalizeEventInput(value: unknown): SessionEventInput[] {
const values = Array.isArray(value) ? value : [value];
if (
values.length === 0 ||
values.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry))
) {
throw new BailianError(
"Event input must be a JSON object or a non-empty array of objects.",
ExitCode.USAGE,
);
}
for (const [index, entry] of values.entries()) {
const validation = SessionEventSchema.safeParse(entry);
if (validation.success) continue;
const issue = validation.error.issues[0];
const path = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
throw new BailianError(
`Invalid event at index ${index}${path}: ${issue?.message ?? "does not match the Session event schema"}.`,
ExitCode.USAGE,
);
}
return values as SessionEventInput[];
}
export const managedAgentSessionEventList = defineCommand({
description: {
"en-US": "List events for a Managed Agent session",
"zh-CN": "列出托管 Agent Session 事件",
},
auth: "apiKey",
usageArgs:
"--session-id <id> [--types <types>] [--order asc|desc] [--limit <n>] [--page <cursor>] [--all]",
flags: EVENT_LIST_FLAGS,
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --all --output json"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US": "--types is applied client-side to each page returned by the provider.",
"zh-CN": "--types 会在客户端对 Provider 返回的每一页结果进行过滤。",
},
],
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listSessionEvents(runtime, ctx.flags.sessionId, {
provider: "bailian",
limit: ctx.flags.limit,
page_token: page,
order: ctx.flags.order,
types: splitCommaSeparated(ctx.flags.types),
created_at_gte: ctx.flags.createdAtGte,
created_at_lte: ctx.flags.createdAtLte,
});
return {
items: response.events,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "events",
items: sanitizeSessionEvents(result.items),
headers: ["ID", "TYPE", "THREAD", "CONTENT"],
rows: eventRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No events found.",
});
},
});
export const managedAgentSessionEventSend = defineCommand({
description: {
"en-US": "Send raw events to a Managed Agent session",
"zh-CN": "向托管 Agent Session 发送原始事件",
},
auth: "apiKey",
usageArgs: "--session-id <id> --event <json|@path>",
flags: EVENT_SEND_FLAGS,
exampleArgs: [
'--session-id sess_abc --event \'{"type":"message","role":"user","content":[{"type":"text","text":"hello"}]}\'',
"--session-id sess_abc --event @event.json",
],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const events = normalizeEventInput(await readJsonInput(ctx.flags.event));
if (ctx.settings.dryRun) {
emitResult({ would_send_events: events, session_id: ctx.flags.sessionId }, format);
return;
}
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return sendRemoteSessionEvents(runtime, ctx.flags.sessionId, events, {
provider: "bailian",
});
}),
);
if (format === "json") emitResult({ session_id: ctx.flags.sessionId, ...result }, format);
else emitBare(`Sent ${events.length} event(s): ${result.event_ids.join(", ")}`);
},
});
export const managedAgentSessionEventStream = defineCommand({
description: {
"en-US": "Stream events from a Managed Agent session",
"zh-CN": "流式读取托管 Agent Session 事件",
},
auth: "apiKey",
usageArgs: "--session-id <id> [--after-id <event-id>]",
flags: EVENT_STREAM_FLAGS,
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --after-id evt_123 --output json"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US":
"When the provider has no native event cursor, --after-id resumes through paginated history polling and event ID de-duplication.",
"zh-CN":
"当 Provider 不支持原生事件 Cursor 时,--after-id 会通过分页历史轮询和 Event ID 去重实现续传。",
},
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
const events: ProviderSessionEvent[] = [];
for await (const event of streamSessionEvents(runtime, ctx.flags.sessionId, {
provider: "bailian",
after_id: ctx.flags.afterId,
})) {
events.push(event);
if (format !== "json") emitBare(JSON.stringify(sanitizeSessionEvents([event])[0]));
if (event.type === "status" && isTerminalSessionStatus(event.status)) break;
}
if (format === "json") {
emitResult(
{ session_id: ctx.flags.sessionId, events: sanitizeSessionEvents(events) },
format,
);
}
}),
);
},
});
@@ -1,98 +1,2 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import { listSessionEvents } from "@openagentpack/sdk";
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const SESSION_EVENTS_FLAGS = {
sessionId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Session ID (required)", "zh-CN": "Session ID必填" },
required: true,
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
limit: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Maximum number of events to fetch", "zh-CN": "要获取的最大事件数" },
},
all: {
type: "switch",
description: {
"en-US": "Fetch all pages by following the cursor",
"zh-CN": "跟随 Cursor 获取全部分页",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "List event history for a session", "zh-CN": "列出 Session 的事件历史" },
auth: "apiKey",
usageArgs: "--session-id <id> [--limit <n>] [--all] [--file <path>]",
flags: SESSION_EVENTS_FLAGS,
exampleArgs: ["--session-id sess_abc123", "--session-id sess_abc123 --all"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const { items: events, hasMore } = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return fetchAllPages(async (page) => {
const result = await listSessionEvents(runtime, flags.sessionId, {
provider: flags.provider,
limit: flags.limit,
page_token: page,
});
return {
items: result.events,
hasMore: result.has_more,
nextPage: result.next_page,
};
}, flags.all);
}),
);
if (format === "json") {
emitResult({ events: sanitizeSessionEvents(events), has_more: hasMore }, format);
return;
}
if (events.length === 0) {
emitBare("No events found.");
return;
}
const headers = ["#", "TYPE", "CONTENT"];
const rows = events.map((event, index) => {
let preview = "";
if (event.type === "message") preview = (event.content ?? "").slice(0, 60);
else if (event.type === "tool_use") preview = event.tool_name ?? "";
else if (event.type === "tool_result") preview = (event.content ?? "").slice(0, 60);
else if (event.type === "status") preview = event.status ?? "";
else if (event.type === "error") preview = (event.content ?? "").slice(0, 60);
else preview = event.raw_type;
return [String(index + 1), event.type, preview];
});
for (const line of formatTable(headers, rows)) emitBare(line);
emitBare(`\nTotal: ${events.length}`);
if (hasMore) emitBare("More events available. Use --all to fetch all.");
},
});
// Backward-compatible alias for `managed-agent session event list`.
export { managedAgentSessionEventList as default } from "./session-event.ts";
@@ -20,17 +20,12 @@ const SESSION_GET_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Get details of a session", "zh-CN": "获取 Session 详情" },
auth: "apiKey",
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
usageArgs: "--session-id <id> [--file <path>]",
flags: SESSION_GET_FLAGS,
exampleArgs: ["--session-id sess_abc123"],
notes: CREDENTIALS_NOTE,
@@ -42,7 +37,7 @@ export default defineCommand({
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return getSession(runtime, flags.sessionId, flags.provider);
return getSession(runtime, flags.sessionId, "bailian");
}),
);
@@ -5,6 +5,11 @@ import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
import {
CURSOR_FLAGS,
splitCommaSeparated,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
const SESSION_LIST_FLAGS = {
file: {
@@ -27,10 +32,31 @@ const SESSION_LIST_FLAGS = {
"zh-CN": "跟随 Cursor 获取全部分页",
},
},
provider: {
limit: CURSOR_FLAGS.limit,
page: CURSOR_FLAGS.page,
statuses: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
valueHint: "<statuses>",
description: {
"en-US": "Comma-separated session statuses",
"zh-CN": "Session 状态,多个以逗号分隔",
},
},
createdAtGte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or after this RFC 3339 timestamp",
"zh-CN": "创建时间不早于该 RFC 3339 时间戳",
},
},
createdAtLte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or before this RFC 3339 timestamp",
"zh-CN": "创建时间不晚于该 RFC 3339 时间戳",
},
},
} satisfies FlagsDef;
@@ -40,36 +66,51 @@ export default defineCommand({
"zh-CN": "列出 Provider 中的 Session",
},
auth: "apiKey",
usageArgs: "[--agent <name>] [--all] [--provider <name>] [--file <path>]",
usageArgs: "[--agent <name>] [--statuses <statuses>] [--limit <n>] [--page <cursor>] [--all]",
flags: SESSION_LIST_FLAGS,
exampleArgs: ["", "--agent assistant", "--all"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const { items: summaries, hasMore } = await withAgentErrors(() =>
const {
items: summaries,
hasMore,
nextPage,
} = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return fetchAllPages(async (page) => {
const result = await listSessionSummaries(runtime, {
agent: flags.agent,
provider: flags.provider,
filter: page ? { page } : undefined,
});
return {
items: result.summaries,
hasMore: result.hasMore,
nextPage: result.nextPage,
};
}, flags.all);
return fetchAllPages(
async (page) => {
const result = await listSessionSummaries(runtime, {
agent: flags.agent,
provider: "bailian",
filter: {
page,
limit: flags.limit,
statuses: splitCommaSeparated(flags.statuses),
created_at_gte: flags.createdAtGte,
created_at_lte: flags.createdAtLte,
},
});
return {
items: result.summaries,
hasMore: result.hasMore,
nextPage: result.nextPage,
};
},
flags.all,
flags.page,
);
}),
);
const sessions = summaries.map((summary) => summary.session);
if (format === "json") {
emitResult({ sessions, has_more: hasMore }, format);
emitResult({ sessions, has_more: hasMore, next_page: nextPage }, format);
return;
}
if (sessions.length === 0) {
@@ -0,0 +1,242 @@
import type { ProviderSessionInfo } from "@openagentpack/sdk";
import {
archiveRemoteSession,
listSessionSummaries,
updateRemoteSession,
} from "@openagentpack/sdk";
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
emitCollection,
matchesQuery,
SEARCH_FLAGS,
searchCursorPages,
splitCommaSeparated,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { readJsonInput } from "./_engine/output-file.ts";
const SESSION_ID_FLAG = {
sessionId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
},
} as const;
const SEARCH_RESOURCE_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
agent: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Filter by configured agent name",
"zh-CN": "按配置中的 Agent 名称筛选",
},
},
statuses: {
type: "string",
valueHint: "<statuses>",
description: {
"en-US": "Comma-separated session statuses",
"zh-CN": "Session 状态,多个以逗号分隔",
},
},
createdAtGte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or after this timestamp",
"zh-CN": "创建时间不早于该时间戳",
},
},
createdAtLte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or before this timestamp",
"zh-CN": "创建时间不晚于该时间戳",
},
},
} as const;
const UPDATE_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
title: {
type: "string",
valueHint: "<title>",
description: { "en-US": "New session title", "zh-CN": "新的 Session 标题" },
},
metadata: {
type: "string",
valueHint: "<json|@path>",
description: {
"en-US": "String-valued metadata JSON or @file",
"zh-CN": "值为字符串的 Metadata JSON 或 @file",
},
},
} as const;
const ARCHIVE_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
} as const;
function sessionRows(sessions: ProviderSessionInfo[]): string[][] {
return sessions.map((session) => [
session.id,
displayValue(session.title),
displayValue(session.agent_id),
session.status,
displayValue(session.updated_at),
]);
}
async function parseMetadata(value?: string): Promise<Record<string, string> | undefined> {
if (!value) return undefined;
const parsed = await readJsonInput(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new BailianError("--metadata must be a JSON object.", ExitCode.USAGE);
}
for (const [key, entry] of Object.entries(parsed)) {
if (typeof entry !== "string") {
throw new BailianError(`Metadata value for '${key}' must be a string.`, ExitCode.USAGE);
}
}
return parsed as Record<string, string>;
}
export const managedAgentSessionSearch = defineCommand({
description: { "en-US": "Search Managed Agent sessions", "zh-CN": "搜索托管 Agent Session" },
auth: "apiKey",
usageArgs:
"--query <text> [--agent <name>] [--statuses <statuses>] [--limit <n>] [--page-limit <n>]",
flags: SEARCH_RESOURCE_FLAGS,
exampleArgs: ["--query debug", "--query failed --statuses failed --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listSessionSummaries(runtime, {
agent: ctx.flags.agent,
provider: "bailian",
filter: {
page,
limit: ctx.flags.limit ?? 100,
statuses: splitCommaSeparated(ctx.flags.statuses),
created_at_gte: ctx.flags.createdAtGte,
created_at_lte: ctx.flags.createdAtLte,
},
});
return {
items: response.summaries,
hasMore: response.hasMore,
nextPage: response.nextPage,
};
},
(summary) =>
matchesQuery(
ctx.flags.query,
summary.session.id,
summary.session.title,
summary.session.status,
summary.session.agent_id,
summary.agentName,
),
ctx.flags.pageLimit,
);
}),
);
const sessions = result.items.map((summary) => summary.session);
emitCollection({
format,
key: "sessions",
items: sessions,
headers: ["ID", "TITLE", "AGENT", "STATUS", "UPDATED"],
rows: sessionRows(sessions),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching sessions found.",
});
},
});
export const managedAgentSessionUpdate = defineCommand({
description: { "en-US": "Update a Managed Agent session", "zh-CN": "更新托管 Agent Session" },
auth: "apiKey",
usageArgs: "--session-id <id> [--title <title>] [--metadata <json|@path>]",
flags: UPDATE_FLAGS,
exampleArgs: [
"--session-id sess_abc --title 'investigation'",
"--session-id sess_abc --metadata @metadata.json",
],
notes: CREDENTIALS_NOTE,
validate: (flags) =>
!flags.title && !flags.metadata ? "Provide --title or --metadata." : undefined,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const input = { title: ctx.flags.title, metadata: await parseMetadata(ctx.flags.metadata) };
if (ctx.settings.dryRun) {
emitResult({ would_update_session: ctx.flags.sessionId, input }, format);
return;
}
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return updateRemoteSession(runtime, ctx.flags.sessionId, input, {
provider: "bailian",
});
}),
);
if (format === "json") emitResult(session, format);
else emitBare(`Session ${session.id} updated.`);
},
});
export const managedAgentSessionArchive = defineCommand({
description: { "en-US": "Archive a Managed Agent session", "zh-CN": "归档托管 Agent Session" },
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This archives the specified remote Managed Agent Session.",
"zh-CN": "该操作会归档指定的远端托管 Agent Session。",
},
},
usageArgs: "--session-id <id>",
flags: ARCHIVE_FLAGS,
exampleArgs: ["--session-id sess_abc --dry-run", "--session-id sess_abc --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult({ would_archive_session: ctx.flags.sessionId }, format);
return;
}
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return archiveRemoteSession(runtime, ctx.flags.sessionId, { provider: "bailian" });
}),
);
if (format === "json") emitResult({ archived: ctx.flags.sessionId, session }, format);
else emitBare(`Session ${ctx.flags.sessionId} archived.`);
},
});
@@ -59,11 +59,6 @@ const SESSION_RUN_FLAGS = {
valueHint: "<title>",
description: { "en-US": "Session title", "zh-CN": "Session 标题" },
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
noStream: {
type: "switch",
description: {
@@ -108,7 +103,7 @@ export default defineCommand({
const runOptions = {
agent: flags.agent,
provider: flags.provider,
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memoryStores: parseMemoryStores(flags.memoryStores),
@@ -121,7 +116,7 @@ export default defineCommand({
would_run: {
prompt: flags.prompt,
agent: flags.agent ?? "auto",
provider: flags.provider ?? "auto",
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memory_stores: runOptions.memoryStores,
@@ -27,11 +27,6 @@ const SESSION_SEND_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
noStream: {
type: "switch",
description: {
@@ -68,7 +63,7 @@ export default defineCommand({
would_send: {
session_id: flags.sessionId,
message: flags.message,
provider: flags.provider ?? "auto",
provider: "bailian",
mode: flags.noStream ? "polling" : "streaming",
},
config_file: file,
@@ -83,7 +78,7 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file);
if (flags.noStream) {
const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, {
provider: flags.provider,
provider: "bailian",
});
renderCollectedEvents(result, asJson, {
session_id: flags.sessionId,
@@ -94,7 +89,7 @@ export default defineCommand({
flags.sessionId,
flags.message,
{
provider: flags.provider,
provider: "bailian",
},
);
await streamAndRenderEvents(events, asJson, {
@@ -1,115 +1,2 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import { listSkills } from "@openagentpack/sdk";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
const SKILL_SOURCES = ["custom", "official", "all"] as const;
type SkillSource = (typeof SKILL_SOURCES)[number];
const SKILL_LIST_FLAGS = {
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
source: {
type: "string",
valueHint: "<source>",
description: {
"en-US":
"Skill catalog: custom (workspace-uploaded, default), official (built-in), or all (both catalogs in one call)",
"zh-CN":
"Skill CatalogcustomWorkspace 上传默认、official内置或 all一次调用获取两类 Catalog",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "List skills from the provider's skill catalog",
"zh-CN": "列出 Provider Skill Catalog 中的 Skill",
},
auth: "apiKey",
usageArgs: "[--source custom|official|all] [--provider <name>] [--file <path>]",
flags: SKILL_LIST_FLAGS,
exampleArgs: [
"",
"--source official",
"--source all --output json",
"--source custom --provider bailian",
],
notes: [
...CREDENTIALS_NOTE,
{
"en-US": "Providers without a skill listing API (e.g. ark) return an empty list.",
"zh-CN": "没有 Skill 列表 API 的 Provider例如 ark会返回空列表。",
},
{
"en-US":
"For agent-driven skill selection, use `--source all --output json`: one call returns both catalogs with per-skill `source` and `description` fields to pick from.",
"zh-CN":
"由 Agent 选择 Skill 时,请使用 `--source all --output json`:一次调用返回两类 Catalog并为每个 Skill 提供 `source` 和 `description` 字段用于选择。",
},
{
"en-US":
"When generating a task that needs a suitable skill, call this command to match official or custom skills before wiring them into the task.",
"zh-CN": "生成需要合适 Skill 的任务时,先调用此命令匹配官方或自定义 Skill再将其接入任务。",
},
],
validate: (f) =>
f.source && !SKILL_SOURCES.includes(f.source as SkillSource)
? "--source must be one of: custom, official, all."
: undefined,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const source = (flags.source as SkillSource | undefined) ?? "custom";
const skills = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
if (source !== "all") {
return listSkills(runtime, { provider: flags.provider, source });
}
// Both catalogs in one call; each entry carries its own `source` field.
const [customSkills, officialSkills] = await Promise.all([
listSkills(runtime, { provider: flags.provider, source: "custom" }),
listSkills(runtime, { provider: flags.provider, source: "official" }),
]);
return [...customSkills, ...officialSkills];
}),
);
if (format === "json") {
emitResult({ source, skills }, format);
return;
}
if (skills.length === 0) {
emitBare(source === "all" ? "No skills found." : `No ${source} skills found.`);
return;
}
const headers = ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "CREATED"];
const rows = skills.map((skill) => [
skill.id,
skill.name.slice(0, 32),
skill.source,
skill.status,
skill.latest_version ?? "-",
skill.created_at ?? "-",
]);
for (const line of formatTable(headers, rows)) emitBare(line);
emitBare(`\nTotal: ${skills.length} (${source})`);
},
});
// Backward-compatible alias for the API-oriented `managed-agent skill list` command.
export { default } from "./skill/list.ts";
@@ -0,0 +1,87 @@
import type { ProviderSkillInfo, SkillVersionInfo } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const SKILL_SOURCES = ["custom", "official", "all"] as const;
export type SkillSource = (typeof SKILL_SOURCES)[number];
const SOURCE_FLAG = {
source: {
type: "string",
valueHint: "<source>",
choices: SKILL_SOURCES,
description: {
"en-US": "Skill catalog: custom (default), official, or all",
"zh-CN": "Skill Catalogcustom默认、official 或 all",
},
},
} as const;
export const SKILL_LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SOURCE_FLAG };
export const SKILL_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...SOURCE_FLAG,
};
export const SKILL_GET_FLAGS = {
...API_TARGET_FLAGS,
skillId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Skill ID", "zh-CN": "Skill ID" },
},
} as const;
export const SKILL_VERSIONS_FLAGS = {
...SKILL_GET_FLAGS,
...CURSOR_FLAGS,
};
export const SKILL_DOWNLOAD_FLAGS = {
...SKILL_GET_FLAGS,
skillVersion: {
type: "string",
valueHint: "<version>",
required: true,
description: { "en-US": "Skill version", "zh-CN": "Skill 版本" },
},
outputFile: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Destination ZIP path", "zh-CN": "目标 ZIP 路径" },
},
force: {
type: "switch",
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
},
} as const;
export function skillRows(skills: ProviderSkillInfo[]): string[][] {
return skills.map((skill) => [
skill.id,
displayValue(skill.name),
skill.source,
skill.status,
displayValue(skill.latest_version),
displayValue(skill.updated_at ?? skill.created_at),
]);
}
export function versionRows(versions: SkillVersionInfo[]): string[][] {
return versions.map((version) => [
displayValue(version.version),
displayValue(version.name),
displayValue(version.type),
displayValue(version.status),
displayValue(version.updated_at ?? version.created_at),
]);
}
@@ -0,0 +1,112 @@
import { dirname, relative, sep } from "node:path";
import { inspectSkillSource } from "@openagentpack/sdk";
import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import {
loadScopedCreateProject,
resolveCandidateDeclaration,
runScopedTopLevelCreate,
SCOPED_CREATE_NOTE,
} from "../_engine/scoped-create.ts";
const FLAGS = {
source: {
type: "string",
valueHint: "<directory|zip|SKILL.md>",
required: true,
description: {
"en-US": "Local Skill directory, ZIP archive, or single SKILL.md",
"zh-CN": "本地 Skill 目录、ZIP 压缩包或单个 SKILL.md",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and upload the Skill through the scoped create",
"zh-CN": "写入 YAML 并通过定向创建上传 Skill",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Declare and create one custom Managed Agent Skill from a local source",
"zh-CN": "从本地来源声明并创建一个自定义托管 Agent Skill",
},
auth: "apiKey",
usageArgs: "--source <directory|zip|SKILL.md> [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: ["--source ./skills/code-review", "--source ./skill.zip --yes"],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US":
"The YAML key is derived from SKILL.md frontmatter name. Remote URLs remain available through handwritten YAML plus full apply.",
"zh-CN":
"YAML key 从 SKILL.md frontmatter 的 name 生成。远程 URL 仍可手工写入 YAML 后执行全量 Apply。",
},
],
validate: (flags) => (!flags.source.trim() ? "--source must not be empty." : undefined),
async run(ctx) {
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
const inspected = await inspectSkillSource(ctx.flags.source, { basePath: process.cwd() });
const source =
relative(dirname(project.configPath), inspected.sourcePath).split(sep).join("/") || ".";
const existingSkills = (project.config.skills ?? {}) as unknown as Record<
string,
Record<string, unknown>
>;
const trackedSameName = await project.stateBackend.read(project.stateScope, (state) =>
state.listResources().some((resource) => {
if (resource.address.provider !== project.provider || resource.address.type !== "skill") {
return false;
}
const declaration = existingSkills[resource.address.name];
const canonicalName =
typeof declaration?.name === "string" ? declaration.name : resource.address.name;
return canonicalName === inspected.name;
}),
);
if (trackedSameName) {
throw new BailianError(
`Skill '${inspected.name}' is already tracked.`,
ExitCode.USAGE,
"Modify its existing YAML declaration and run full `bl managed-agent apply` to create a new version.",
);
}
const rawDeclaration: Record<string, unknown> = {
name: inspected.name,
source,
origin: "custom",
provider: project.provider,
};
const resolvedDeclaration = await resolveCandidateDeclaration({
project,
group: "skills",
rawDeclaration,
});
await runScopedTopLevelCreate({
host: ctx,
project,
group: "skills",
resourceType: "skill",
displayName: inspected.name,
rawDeclaration,
resolvedDeclaration,
existingDeclarations: existingSkills,
effectiveName: (key, declaration) =>
typeof declaration.name === "string" ? declaration.name : key,
fallbackKey: "skill",
yes: ctx.flags.yes,
});
},
});
@@ -0,0 +1,49 @@
import { downloadRemoteSkill } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { writeOutputFile } from "../_engine/output-file.ts";
import { SKILL_DOWNLOAD_FLAGS } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Download a Managed Agent skill version",
"zh-CN": "下载托管 Agent Skill 版本",
},
auth: "apiKey",
usageArgs: "--skill-id <id> --skill-version <version> --output-file <path> [--force]",
flags: SKILL_DOWNLOAD_FLAGS,
exampleArgs: ["--skill-id skill_abc --skill-version 3 --output-file ./skill.zip"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{
would_download_skill: ctx.flags.skillId,
version: ctx.flags.skillVersion,
output_file: ctx.flags.outputFile,
},
format,
);
return;
}
const content = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return downloadRemoteSkill(runtime, ctx.flags.skillId, ctx.flags.skillVersion, {
provider: "bailian",
});
}),
);
const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force);
if (format === "json")
emitResult(
{ downloaded: ctx.flags.skillId, version: ctx.flags.skillVersion, output_file: outputFile },
format,
);
else emitBare(`Skill downloaded to ${outputFile}`);
},
});
@@ -0,0 +1,36 @@
import { getRemoteSkill } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { SKILL_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Get a Managed Agent skill", "zh-CN": "获取托管 Agent Skill 详情" },
auth: "apiKey",
usageArgs: "--skill-id <id>",
flags: SKILL_GET_FLAGS,
exampleArgs: ["--skill-id skill_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const skill = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteSkill(runtime, ctx.flags.skillId, { provider: "bailian" });
}),
);
if (format === "json") {
emitResult(skill, format);
return;
}
emitBare(`ID: ${skill.id}`);
emitBare(`Name: ${skill.name}`);
emitBare(`Description: ${displayValue(skill.description, 120)}`);
emitBare(`Source: ${skill.source}`);
emitBare(`Status: ${skill.status}`);
emitBare(`Version: ${displayValue(skill.latest_version)}`);
},
});
@@ -0,0 +1,108 @@
import { listRemoteSkills } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { SKILL_LIST_FLAGS, skillRows, type SkillSource } from "./_shared.ts";
export function buildSkillListJsonResult<T>(
source: SkillSource,
result: { items: T[]; hasMore: boolean; nextPage?: string },
) {
return {
source,
skills: result.items,
has_more: result.hasMore,
next_page: result.nextPage,
};
}
async function listOneCatalog(
runtime: Parameters<typeof listRemoteSkills>[0],
source: Exclude<SkillSource, "all">,
options: { limit?: number; page?: string; all?: boolean },
) {
return fetchAllPages(
async (page) => {
const response = await listRemoteSkills(runtime, {
provider: "bailian",
source,
limit: options.limit,
page,
});
return { items: response.data, hasMore: response.has_more, nextPage: response.next_page };
},
options.all,
options.page,
);
}
export default defineCommand({
description: { "en-US": "List Managed Agent skills", "zh-CN": "列出托管 Agent Skill" },
auth: "apiKey",
usageArgs: "[--source custom|official|all] [--limit <n>] [--page <cursor>] [--all]",
flags: SKILL_LIST_FLAGS,
exampleArgs: ["", "--source official", "--source all --all --output json"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US":
"--source all combines one page from each catalog, or every page with --all; it does not accept --page.",
"zh-CN":
"--source all 默认合并两个 Catalog 的各一页,传入 --all 时获取全部分页;该模式不接受 --page。",
},
],
validate: (flags) =>
validateLimitAndPageLimit(flags) ??
(flags.source === "all" && flags.page
? "--source all cannot be combined with --page."
: undefined),
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const source = (ctx.flags.source as SkillSource | undefined) ?? "custom";
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
if (source !== "all") {
return listOneCatalog(runtime, source, {
limit: ctx.flags.limit,
page: ctx.flags.page,
all: ctx.flags.all,
});
}
const [custom, official] = await Promise.all([
listOneCatalog(runtime, "custom", {
limit: ctx.flags.limit,
all: ctx.flags.all,
}),
listOneCatalog(runtime, "official", {
limit: ctx.flags.limit,
all: ctx.flags.all,
}),
]);
return {
items: [...custom.items, ...official.items],
hasMore: custom.hasMore || official.hasMore,
nextPage: undefined,
};
}),
);
if (format === "json") {
emitResult(buildSkillListJsonResult(source, result), format);
return;
}
emitCollection({
format,
key: "skills",
items: result.items,
headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"],
rows: skillRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No skills found.",
});
},
});
@@ -0,0 +1,73 @@
import { listRemoteSkills } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import {
emitCollection,
matchesQuery,
searchCursorPages,
validateLimitAndPageLimit,
} from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { SKILL_SEARCH_FLAGS, skillRows, type SkillSource } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Search Managed Agent skills", "zh-CN": "搜索托管 Agent Skill" },
auth: "apiKey",
usageArgs: "--query <text> [--source custom|official|all] [--limit <n>] [--page-limit <n>]",
flags: SKILL_SEARCH_FLAGS,
exampleArgs: ["--query browser --source official", "--query report --source all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const source = (ctx.flags.source as SkillSource | undefined) ?? "custom";
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
const searchCatalog = (catalog: Exclude<SkillSource, "all">) =>
searchCursorPages(
async (page) => {
const response = await listRemoteSkills(runtime, {
provider: "bailian",
source: catalog,
limit: ctx.flags.limit ?? 100,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
(skill) => matchesQuery(ctx.flags.query, skill.id, skill.name, skill.description),
ctx.flags.pageLimit,
);
if (source !== "all") return searchCatalog(source);
const [custom, official] = await Promise.all([
searchCatalog("custom"),
searchCatalog("official"),
]);
return {
items: [...custom.items, ...official.items],
hasMore: custom.hasMore || official.hasMore,
nextPage: undefined,
scannedPages: custom.scannedPages + official.scannedPages,
truncated: custom.truncated || official.truncated,
};
}),
);
emitCollection({
format,
key: "skills",
items: result.items,
headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"],
rows: skillRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching skills found.",
});
},
});
@@ -0,0 +1,55 @@
import { listRemoteSkillVersions } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { SKILL_VERSIONS_FLAGS, versionRows } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "List Managed Agent skill versions",
"zh-CN": "列出托管 Agent Skill 版本",
},
auth: "apiKey",
usageArgs: "--skill-id <id> [--limit <n>] [--page <cursor>] [--all]",
flags: SKILL_VERSIONS_FLAGS,
exampleArgs: ["--skill-id skill_abc", "--skill-id skill_abc --all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteSkillVersions(runtime, ctx.flags.skillId, {
provider: "bailian",
limit: ctx.flags.limit,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "versions",
items: result.items,
headers: ["VERSION", "NAME", "TYPE", "STATUS", "UPDATED"],
rows: versionRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No skill versions found.",
});
},
});
@@ -1,4 +1,10 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { importResource, parseStateAddress } from "@openagentpack/sdk";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
@@ -8,7 +14,7 @@ import { withAgentErrors } from "./_engine/errors.ts";
const STATE_IMPORT_FLAGS = {
address: {
type: "string",
valueHint: "<provider.type.name>",
valueHint: "<bailian.type.name>",
description: {
"en-US": "Resource state address (required)",
"zh-CN": "资源 State 地址(必填)",
@@ -42,6 +48,17 @@ const STATE_IMPORT_FLAGS = {
},
} satisfies FlagsDef;
function parseBailianStateAddress(address: string) {
const parsed = parseStateAddress(address, { requireProvider: true });
if (parsed.provider !== "bailian") {
throw new BailianError(
`bl managed-agent can import only Bailian resources; address provider was '${parsed.provider}'. / bl managed-agent 只能导入百炼资源;地址中的 Provider 为 '${parsed.provider}'。`,
ExitCode.USAGE,
);
}
return parsed;
}
export default defineCommand({
description: {
"en-US": "Import an existing remote resource into agents state",
@@ -49,7 +66,7 @@ export default defineCommand({
},
auth: "apiKey",
usageArgs:
"--address <provider.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
"--address <bailian.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
flags: STATE_IMPORT_FLAGS,
exampleArgs: ["--address bailian.agent.assistant --remote-id agent-abc123"],
notes: CREDENTIALS_NOTE,
@@ -61,7 +78,7 @@ export default defineCommand({
if (settings.dryRun) {
// Validate the address shape locally so dry-run still catches usage errors.
await withAgentErrors(async () => {
parseStateAddress(flags.address, { requireProvider: true });
parseBailianStateAddress(flags.address);
});
emitResult(
{
@@ -78,9 +95,7 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
// Parse first so a malformed address fails fast, before any config I/O.
const parsed = parseStateAddress(flags.address, {
requireProvider: true,
});
const parsed = parseBailianStateAddress(flags.address);
const runtime = await buildAgentRuntime(ctx, file);
await importResource(runtime, parsed, flags.remoteId, {
resourceVersion: flags.resourceVersion,
@@ -37,9 +37,17 @@ export default defineCommand({
"zh-CN": "从 State 中移除资源,但不销毁远端资源",
},
auth: "none",
risk: {
level: "high",
message: {
"en-US":
"This removes the resource from local state without deleting it remotely, so this project will no longer track it.",
"zh-CN": "该操作会从本地 State 中移除资源但不会删除远端资源,此项目将不再跟踪该资源。",
},
},
usageArgs: "--address <provider.type.name> [--file <path>]",
flags: STATE_RM_FLAGS,
exampleArgs: ["--address bailian.agent.assistant"],
exampleArgs: ["--address bailian.agent.assistant --yes"],
notes: OFFLINE_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
@@ -8,7 +8,7 @@ import {
import { emitBare, emitResult } from "bailian-cli-runtime";
import { validateProjectConfig } from "@openagentpack/sdk";
import { OFFLINE_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
const VALIDATE_FLAGS = {
file: {
@@ -56,7 +56,10 @@ export default defineCommand({
}
if (errorCount > 0) {
throw new BailianError(`Validation failed with ${errorCount} error(s).`, ExitCode.GENERAL);
throw new BailianError(
formatAgentDiagnosticFailure(diagnostics, `Validation failed with ${errorCount} error(s).`),
ExitCode.GENERAL,
);
}
},
});
@@ -0,0 +1,41 @@
import type { CloudVault } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
INCLUDE_ARCHIVED_FLAG,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const VAULT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const VAULT_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const VAULT_GET_FLAGS = {
...API_TARGET_FLAGS,
vaultId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Vault ID", "zh-CN": "Vault ID" },
},
} as const;
export function vaultRows(vaults: CloudVault[]): string[][] {
return vaults.map((vault) => [
vault.id,
displayValue(vault.display_name),
displayValue(vault.type),
displayValue(vault.created_at),
displayValue(vault.updated_at),
]);
}
@@ -0,0 +1,95 @@
import { defineCommand, type FlagsDef } from "bailian-cli-core";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import {
loadScopedCreateProject,
parseMetadata,
resolveCandidateDeclaration,
runScopedTopLevelCreate,
SCOPED_CREATE_NOTE,
} from "../_engine/scoped-create.ts";
const FLAGS = {
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Remote Vault display name; the YAML key is generated automatically",
"zh-CN": "远端 Vault 显示名称YAML key 将自动生成",
},
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Metadata entry (repeatable)",
"zh-CN": "Metadata 条目(可重复)",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and run the scoped remote create",
"zh-CN": "写入 YAML 并执行定向远端创建",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Declare and create one empty Managed Agent Vault through a scoped YAML apply",
"zh-CN": "通过定向 YAML Apply 声明并创建一个空的托管 Agent Vault",
},
auth: "apiKey",
usageArgs: "--name <name> [--metadata <key=value>...] [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: ["--name Production", "--name Production --metadata owner=platform --yes"],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US": "Creates an empty Vault. Add secrets later with `vault credential create`.",
"zh-CN": "创建空 Vault随后使用 `vault credential create` 添加 Secret。",
},
],
validate: (flags) => (!flags.name.trim() ? "--name must not be empty." : undefined),
async run(ctx) {
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
const rawDeclaration: Record<string, unknown> = {
display_name: ctx.flags.name.trim(),
provider: project.provider,
credentials: [],
metadata: parseMetadata(ctx.flags.metadata),
};
const resolvedDeclaration = await resolveCandidateDeclaration({
project,
group: "vaults",
rawDeclaration,
});
await runScopedTopLevelCreate({
host: ctx,
project,
group: "vaults",
resourceType: "vault",
displayName: ctx.flags.name.trim(),
rawDeclaration,
resolvedDeclaration,
existingDeclarations: (project.config.vaults ?? {}) as unknown as Record<
string,
Record<string, unknown>
>,
effectiveName: (key, declaration) =>
typeof declaration.display_name === "string" ? declaration.display_name : key,
fallbackKey: "vault",
yes: ctx.flags.yes,
});
},
});
@@ -0,0 +1,337 @@
import { dirname } from "node:path";
import {
bootstrapRuntimeCredentialsSync,
createVaultCredentialWithStateBackend,
planVaultCredentialCreateWithStateBackend,
type ResolvedProjectConfig,
resolveProjectConfigFromObject,
} from "@openagentpack/sdk";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { parseDocument } from "yaml";
import { CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
import { withStdoutProtected } from "../../_engine/console-capture.ts";
import { retainAgentError, withAgentErrors } from "../../_engine/errors.ts";
import {
loadScopedCreateProject,
parseMetadata,
replaceConfigAtomically,
SCOPED_CREATE_NOTE,
} from "../../_engine/scoped-create.ts";
const FLAGS = {
vault: {
type: "string",
valueHint: "<yaml-key>",
required: true,
description: {
"en-US": "Existing tracked Vault key from agents.yaml",
"zh-CN": "agents.yaml 中已跟踪的 Vault key",
},
},
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Credential display name",
"zh-CN": "Credential 显示名称",
},
},
secretName: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Environment variable name exposed to the Agent",
"zh-CN": "提供给 Agent 的环境变量名",
},
},
secretEnv: {
type: "string",
valueHint: "<env-name>",
required: true,
description: {
"en-US": "Local environment variable containing the secret value",
"zh-CN": "保存真实 Secret 的本地环境变量名",
},
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Metadata entry (repeatable)",
"zh-CN": "Metadata 条目(可重复)",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and create the remote Credential",
"zh-CN": "写入 YAML 并创建远端 Credential",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Append and create one environment-variable Credential in a tracked Vault",
"zh-CN": "在已跟踪 Vault 中追加并创建一个环境变量 Credential",
},
auth: "apiKey",
usageArgs:
"--vault <yaml-key> --name <name> --secret-name <name> --secret-env <env-name> [--metadata <key=value>...] [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: [
"--vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN",
"--vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN --yes",
],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US":
"--secret-env is an environment variable name, not the secret itself. The CLI auto-loads the nearest .env from the current directory upward; shell exports and CI secret injection also work.",
"zh-CN":
"--secret-env 接收环境变量名,不接收 Secret 本身。CLI 会从当前目录向上自动加载最近的 .env也支持 Shell export 和 CI Secret 注入。",
},
{
"en-US":
"YAML stores only ${ENV_NAME}. Never commit .env; subsequent full apply runs must provide the same environment variable.",
"zh-CN":
"YAML 只保存 ${ENV_NAME}。不要提交 .env后续执行全量 Apply 时仍需提供同名环境变量。",
},
],
validate: (flags) => {
if (!flags.vault.trim()) return "--vault must not be empty.";
if (!flags.name.trim()) return "--name must not be empty.";
if (!flags.secretName.trim()) return "--secret-name must not be empty.";
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(flags.secretEnv)) {
return "--secret-env must be a valid environment variable name.";
}
return undefined;
},
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
bootstrapRuntimeCredentialsSync();
const secretValue = process.env[ctx.flags.secretEnv];
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml").finally(
() => {
delete process.env[ctx.flags.secretEnv];
},
);
const vault = project.config.vaults?.[ctx.flags.vault];
if (!vault) {
throw new BailianError(
`Vault '${ctx.flags.vault}' is not declared in agents.yaml.`,
ExitCode.USAGE,
);
}
if (ctx.flags.yes && !ctx.settings.dryRun && !secretValue) {
throw new BailianError(
`Environment variable '${ctx.flags.secretEnv}' is not set or is empty.`,
ExitCode.USAGE,
`Set it in your shell, CI secret store, or a local .env file, then re-run with --secret-env ${ctx.flags.secretEnv}.`,
);
}
const metadata = parseMetadata(ctx.flags.metadata);
const rawCredential = {
name: ctx.flags.name.trim(),
type: "environment_variable" as const,
secret_name: ctx.flags.secretName.trim(),
secret_value: `\${${ctx.flags.secretEnv}}`,
networking: { type: "unrestricted" as const },
...(metadata ? { metadata } : {}),
};
const runtimeCredential = {
...rawCredential,
secret_value: secretValue ?? "__dry_run_secret__",
};
const document = parseDocument(project.source);
if (document.errors.length > 0) {
throw new BailianError(
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
ExitCode.USAGE,
);
}
const rawConfig = document.toJS() as {
vaults?: Record<string, { credentials?: unknown[] }>;
};
const rawCredentials = rawConfig.vaults?.[ctx.flags.vault]?.credentials ?? [];
const sameNameCredentials = rawCredentials.filter(
(credential): credential is Record<string, unknown> =>
Boolean(
credential &&
typeof credential === "object" &&
!Array.isArray(credential) &&
(credential as Record<string, unknown>).name === rawCredential.name,
),
);
let reusedPending = false;
if (sameNameCredentials.length > 0) {
const lastCredential = rawCredentials.at(-1);
const existing = sameNameCredentials[0]!;
if (
sameNameCredentials.length !== 1 ||
existing !== lastCredential ||
!sameCredentialDeclaration(existing, rawCredential)
) {
throw new BailianError(
`Vault '${ctx.flags.vault}' already declares a different credential named '${rawCredential.name}'.`,
ExitCode.USAGE,
"Modify the existing YAML declaration and run full `bl managed-agent apply`; create never overwrites it.",
);
}
reusedPending = true;
}
const rawCandidate = structuredClone(project.config) as unknown as Record<string, unknown>;
const rawVaults = rawCandidate.vaults as Record<string, Record<string, unknown>>;
const rawVault = rawVaults[ctx.flags.vault]!;
if (!reusedPending) {
rawVaults[ctx.flags.vault] = {
...rawVault,
credentials: [
...((rawVault.credentials as unknown[] | undefined) ?? []),
runtimeCredential,
],
};
}
const resolvedCandidate = await withAgentErrors(() =>
resolveProjectConfigFromObject(rawCandidate, {
projectName: project.projectName,
basePath: dirname(project.configPath),
}),
);
const candidateConfig = resolvedCandidate.config;
if (!reusedPending) {
document.addIn(["vaults", ctx.flags.vault, "credentials"], rawCredential);
}
const nextSource = reusedPending ? project.source : document.toString();
const backendInput = {
projectName: project.projectName,
config: candidateConfig as ResolvedProjectConfig,
configPath: project.configPath,
providers: { [project.provider]: candidateConfig.providers[project.provider] },
stateBackend: project.stateBackend,
stateScope: project.stateScope,
};
if (ctx.settings.dryRun || !ctx.flags.yes) {
const planned = await withAgentErrors(() =>
withStdoutProtected(() =>
planVaultCredentialCreateWithStateBackend(
backendInput,
ctx.flags.vault,
ctx.flags.name.trim(),
{
provider: project.provider,
refresh: !ctx.settings.dryRun,
quiet: format === "json",
checkRemote: !ctx.settings.dryRun,
},
),
),
);
const result = {
vault: { key: ctx.flags.vault, remote_id: planned.vaultRemoteId },
credential: {
name: ctx.flags.name.trim(),
secret_name: ctx.flags.secretName.trim(),
secret_env: ctx.flags.secretEnv,
},
config_file: project.configPath,
yaml_written: false,
reused_pending: reusedPending,
requires_confirmation: !ctx.settings.dryRun,
ready_to_create: true,
remote_checked: planned.remoteChecked,
reuse_remote: planned.reuseRemote,
};
if (format === "json") emitResult(result, format);
else {
emitBare(`Vault: ${ctx.flags.vault}`);
emitBare(`Credential: ${ctx.flags.name.trim()} (secret from ${ctx.flags.secretEnv})`);
emitBare(
ctx.settings.dryRun
? "Dry run: YAML, State, and remote resources were not changed."
: "Preview only: re-run with --yes to append YAML and create this Credential.",
);
}
return;
}
await replaceConfigAtomically(project.configPath, project.source, nextSource);
let created;
try {
created = await withAgentErrors(() =>
withStdoutProtected(() =>
createVaultCredentialWithStateBackend(
backendInput,
ctx.flags.vault,
ctx.flags.name.trim(),
{ provider: project.provider, refresh: true, quiet: format === "json" },
),
),
);
} catch (error) {
throw retainAgentError(
error,
"The YAML declaration was kept. Restore the same secret environment variable and re-run this command; an already-created matching Credential will be adopted.",
"Vault Credential create failed.",
);
}
const result = {
vault: { key: ctx.flags.vault, remote_id: created.vaultRemoteId },
credential: {
id: created.credentialId,
name: ctx.flags.name.trim(),
secret_name: ctx.flags.secretName.trim(),
secret_env: ctx.flags.secretEnv,
},
config_file: project.configPath,
yaml_written: true,
reused_pending: reusedPending,
adopted: created.adopted,
status: "completed",
};
if (format === "json") emitResult(result, format);
else {
emitBare(`Wrote ${project.configPath} and created Credential '${ctx.flags.name.trim()}'.`);
}
},
});
function sameCredentialDeclaration(
existing: Record<string, unknown>,
candidate: Record<string, unknown>,
): boolean {
return canonicalJson(existing) === canonicalJson(candidate);
}
function canonicalJson(value: unknown): string {
const normalize = (candidate: unknown): unknown => {
if (Array.isArray(candidate)) return candidate.map(normalize);
if (!candidate || typeof candidate !== "object") return candidate;
return Object.fromEntries(
Object.entries(candidate as Record<string, unknown>)
.filter(([, entry]) => entry !== undefined)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => [key, normalize(entry)]),
);
};
return JSON.stringify(normalize(value));
}
@@ -0,0 +1,35 @@
import { getRemoteVault } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { VAULT_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Get a Managed Agent vault", "zh-CN": "获取托管 Agent Vault 详情" },
auth: "apiKey",
usageArgs: "--vault-id <id>",
flags: VAULT_GET_FLAGS,
exampleArgs: ["--vault-id vault_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const vault = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteVault(runtime, ctx.flags.vaultId, { provider: "bailian" });
}),
);
if (format === "json") {
emitResult(vault, format);
return;
}
emitBare(`ID: ${vault.id}`);
emitBare(`Name: ${displayValue(vault.display_name)}`);
emitBare(`Type: ${displayValue(vault.type)}`);
emitBare(`Created: ${displayValue(vault.created_at)}`);
emitBare(`Updated: ${displayValue(vault.updated_at)}`);
},
});
@@ -0,0 +1,53 @@
import { listRemoteVaults } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { VAULT_LIST_FLAGS, vaultRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "List Managed Agent vaults", "zh-CN": "列出托管 Agent Vault" },
auth: "apiKey",
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]",
flags: VAULT_LIST_FLAGS,
exampleArgs: ["", "--all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteVaults(runtime, {
provider: "bailian",
limit: ctx.flags.limit,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "vaults",
items: result.items,
headers: ["ID", "NAME", "TYPE", "CREATED", "UPDATED"],
rows: vaultRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No vaults found.",
});
},
});

Some files were not shown because too many files have changed in this diff Show More