mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f41eaabc2 | |||
| e1634c40ed | |||
| 4c5688ad4a | |||
| 176961dc56 | |||
| 9d19558645 | |||
| a9200a3f85 | |||
| 83d7982ecb | |||
| d33cb71cc7 | |||
| 737f8db359 | |||
| a402026cce | |||
| da6e1313e5 | |||
| 2d2ec1c628 | |||
| a78ed7ffe8 | |||
| 5a1dfa5227 | |||
| b5dec926b7 | |||
| fd96fd664c | |||
| 719c0b68bb | |||
| 9e6f5ca9cb | |||
| 3f78de2dc1 | |||
| 38926aeb22 | |||
| 67ae182ec3 | |||
| e3f2996291 | |||
| 59250c4f44 | |||
| 676b6c2ece | |||
| 4f59ca5118 | |||
| 0872ff6a20 | |||
| 98ebeb398c | |||
| 5c96077d9f | |||
| 8a7674e7c3 | |||
| afb547e0c8 | |||
| 449eede237 | |||
| 8906108744 | |||
| 2fbb6efe67 | |||
| 47e9bcc634 | |||
| 00bcee36a6 | |||
| 4c7ef0c7a5 | |||
| 69c1fb9f08 |
@@ -6,6 +6,55 @@ 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
|
||||
|
||||
- Removed API Key validation from `bl auth login`.
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### Added
|
||||
|
||||
@@ -6,6 +6,55 @@
|
||||
|
||||
[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
|
||||
|
||||
### 已移除
|
||||
|
||||
- 移除 `bl auth login` 的 API Key 校验。
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -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 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
|
||||
|
||||
## 必查清单
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## 架构分层
|
||||
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组;identity(`--version`、`search --help` path) |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、registry help 捕获、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | 进程内 help、子进程缺参/dry-run/live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path/分组的进程内 help;根 help、鉴权域等代表性子进程冒烟 |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | map 全部 path/分组的进程内 help;`--version`、`search --help` 等代表性子进程冒烟 |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
|
||||
## 触发条件
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
### commands E2E
|
||||
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
|
||||
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- help:`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`(当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程)
|
||||
- 子进程:缺参、dry-run、live 使用 `runCommandE2e(routes, args)`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- fixtures:`packages/commands/tests/e2e/fixtures/`
|
||||
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map)
|
||||
|
||||
@@ -37,10 +38,12 @@
|
||||
|
||||
- bl:`runCli` from `packages/cli/tests/e2e/helpers.ts`
|
||||
- kscli:`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
|
||||
- 全量 leaf/group help 使用产品 `commands` 创建 `CommandRegistry`,先通过 `resolve([...path, "--help"])` 检查 help 路由,再用 `captureRegistryHelp` 检查完整 Usage;禁止在 `test.each(commandPaths/groupPaths)` 中逐条启动 `tsx` 子进程
|
||||
- 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
|
||||
|
||||
### 共享
|
||||
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`;runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
|
||||
- globalSetup:根 `vite.config.ts` → `packages/e2e/src/global-setup.ts`
|
||||
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
|
||||
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
|
||||
@@ -48,9 +51,12 @@
|
||||
## 双层 describe(固定结构)
|
||||
|
||||
```ts
|
||||
// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
describe("e2e: <topic>", () => {
|
||||
test("<subcommand> --help 正常退出", ...);
|
||||
test("<subcommand> --help 正常退出", async () => {
|
||||
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 2) skipIf:缺参 / dry-run / 真实集成
|
||||
@@ -74,11 +80,18 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
|
||||
## 用例类型
|
||||
|
||||
1. **--help**:`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
|
||||
1. **--help**:`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
|
||||
2. **缺参**:带无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2`
|
||||
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 重复
|
||||
@@ -105,7 +118,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
|
||||
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
|
||||
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
|
||||
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
|
||||
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
|
||||
- [ ] `vp test packages/commands/tests/e2e/<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`;纯命令语法示例可保留为字符串,服务端错误不翻译
|
||||
|
||||
@@ -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) 一并核对
|
||||
|
||||
## 完成后自查
|
||||
|
||||
@@ -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 或本地路径
|
||||
|
||||
|
||||
@@ -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 | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -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 | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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 | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -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、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
|
||||
@@ -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 模式
|
||||
|
||||
|
||||
@@ -270,11 +270,11 @@ bl knowledge service deploy --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -319,10 +319,10 @@ bl knowledge service delete --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
+1
-1
@@ -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 | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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、执行删除前返回确认请求。
|
||||
|
||||
**示例**
|
||||
|
||||
|
||||
@@ -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 模式
|
||||
|
||||
|
||||
@@ -270,11 +270,11 @@ kscli service deploy --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--version-desc <text>` | string | 否 | 新版本的描述说明 |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
@@ -319,10 +319,10 @@ kscli service delete --agent-id <id> [flags]
|
||||
|
||||
**参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | --------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 跳过确认提示 |
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------------- | ------ | ---- | ---------------------- |
|
||||
| `--agent-id <id>` | string | 是 | 服务(agent)ID |
|
||||
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
|
||||
|
||||
**输出**
|
||||
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.18.0",
|
||||
"version": "1.20.0",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -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,6 +85,7 @@ import {
|
||||
workspaceList,
|
||||
quotaList,
|
||||
quotaUpdate,
|
||||
quotaDelete,
|
||||
quotaHistory,
|
||||
quotaCheck,
|
||||
permissionList,
|
||||
@@ -151,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.
|
||||
@@ -246,6 +289,7 @@ export const commands: Record<string, AnyCommand> = {
|
||||
"workspace list": workspaceList,
|
||||
"quota list": quotaList,
|
||||
"quota update": quotaUpdate,
|
||||
"quota delete": quotaDelete,
|
||||
"quota history": quotaHistory,
|
||||
"quota check": quotaCheck,
|
||||
"permission list": permissionList,
|
||||
@@ -312,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",
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { mkdtempSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterAll, describe, expect, test } from "vite-plus/test";
|
||||
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import { commands } from "../../src/commands.ts";
|
||||
import { runCli } from "./helpers.ts";
|
||||
|
||||
const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
const registry = new CommandRegistry(commands, "bl");
|
||||
const isolatedConfigDir = mkdtempSync(join(tmpdir(), "bl-registry-smoke-"));
|
||||
|
||||
afterAll(() => rmSync(isolatedConfigDir, { recursive: true, force: true }));
|
||||
|
||||
function runCliSmoke(args: string[]) {
|
||||
return runCli(args, { BAILIAN_CONFIG_DIR: isolatedConfigDir });
|
||||
}
|
||||
|
||||
describe("e2e: bl registry smoke", () => {
|
||||
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
|
||||
const { stderr, exitCode } = await runCli(["--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/\bbl\b/i);
|
||||
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
|
||||
@@ -24,7 +36,7 @@ describe("e2e: bl registry smoke", () => {
|
||||
});
|
||||
|
||||
test("分组帮助按叶子命令展示不同鉴权域", async () => {
|
||||
const { stderr, exitCode } = await runCli(["app", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
@@ -36,13 +48,13 @@ describe("e2e: bl registry smoke", () => {
|
||||
[["token-plan", "list-seats"], "AK/SK"],
|
||||
[["config", "show"], "No Auth"],
|
||||
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
|
||||
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke([...commandPath, "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain(`Authentication: ${authLabel}`);
|
||||
});
|
||||
|
||||
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Global Flags:/);
|
||||
expect(stderr).toMatch(/--console-region <region>/);
|
||||
@@ -52,13 +64,25 @@ describe("e2e: bl registry smoke", () => {
|
||||
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
|
||||
});
|
||||
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: bl ${path}`);
|
||||
});
|
||||
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(
|
||||
`Usage: bl ${path} <command> [flags]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.18.0",
|
||||
"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:",
|
||||
|
||||
@@ -13,9 +13,20 @@ const DELETE_FLAGS = {
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
|
||||
auth: "apiKey",
|
||||
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"],
|
||||
exampleArgs: [
|
||||
"--file-id file-id-xxx",
|
||||
"--file-id file-id-xxx --dry-run",
|
||||
"--file-id file-id-xxx --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const fileId = flags.fileId;
|
||||
|
||||
@@ -40,9 +40,20 @@ export default defineCommand({
|
||||
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
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"],
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
"--deployed-model dep-... --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
|
||||
@@ -13,9 +13,16 @@ const DELETE_FLAGS = {
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
|
||||
auth: "apiKey",
|
||||
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"],
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
|
||||
notes: [
|
||||
{
|
||||
"en-US":
|
||||
|
||||
@@ -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": "服务(Agent)ID" },
|
||||
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 必须包含 1–50 条 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": "Provider:bailian、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 Catalog:custom(Workspace 上传,默认)、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 Catalog:custom(默认)、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)}`);
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user