Compare commits

..

1 Commits

Author SHA1 Message Date
Gong Shiqi 999aeebb55 Revert "feat: cancel validate API key" 2026-08-28 14:29:52 +08:00
309 changed files with 2159 additions and 21985 deletions
-2
View File
@@ -15,8 +15,6 @@ git add \
skills/bailian-finetune/reference \
skills/bailian-managed-agent/SKILL.md \
skills/bailian-managed-agent/reference \
skills/bailian-sandbox/SKILL.md \
skills/bailian-sandbox/reference \
skills/bailian-web-search/SKILL.md
vp staged
+1 -1
View File
@@ -36,7 +36,7 @@ packages/core/src/auth/ # apiKey / console credential 解析与落盘
packages/core/src/client/ # HTTP client / endpoints / console gateway
```
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-sandbox` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
Skill / 命令手册随 `skills/bailian-*/` 经 `bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill(`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts` 从 **`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts` 从 `packages/cli/package.json` 同步各 `skills/*/SKILL.md` 的 `metadata.version`。两者由根脚本 `pnpm run sync:skill-assets` 和 `.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细;SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
约定:
-112
View File
@@ -6,118 +6,6 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.25.0] - 2026-09-14
### Added
- **Token Plan harness quota** — `bl token-plan harness-quota` shows Token Plan harness entitlement quota usage (Console auth), joining the harness list with issued entitlements to display used/total quota, usage ratio, and reset time; harnesses with a pending entitlement are listed as issuing.
- Filter the harness list with `--type official_tool|infrastructure`; render as a quota box or `--output json`.
## [1.24.0] - 2026-09-11
### Added
- Sandbox instance and template lifecycle commands, with automatic build polling and `--async` submission.
- `bl sandbox official-images` and template `--image` presets for Code Interpreter, Browser, and All-in-One.
- `bl sandbox file upload` uploads template mount files with `source=sandbox_template`; use the returned File ID in `mntConfig`.
- Sandbox supports shared `--base-url`, environment and Profile configuration, with workspace-based endpoint fallback.
- Dedicated `bailian-sandbox` Skill with command references and instance connection guidance.
### Fixed
- Preserve submitted `templateID` and `buildID` when template-build polling fails, so users can check the existing build before resubmitting.
### Security
- Sandbox REST calls use Bailian Bearer authentication without an E2B SDK or E2B API key. Connection credentials and dry-run environment values are redacted by default.
## [1.23.0] - 2026-09-10
### Added
- **Profile-level watermark control** — configure `watermark` with `bl config set --key watermark --value true|false` to control the default watermark behavior for image generation and editing, video generation and editing, and reference-to-video commands.
- **ASR accuracy controls** — `bl speech recognize` now supports instant hot words with `--vocabulary`, contextual word enhancement with `--context`, and reusable pre-built vocabularies with `--vocabulary-id` for supported ASR models.
- **Speech vocabulary management** — added `bl speech vocabulary create|list|get|update|delete` to manage reusable pre-built hot-word vocabularies.
## [1.22.0] - 2026-09-08
### Changed
- **Project initialization** — `managed-agent project init` now creates `./managed-agent` by default. Use `--project .` to initialize in place. **(BREAKING)**
- **Build confirmation** — `managed-agent project build` no longer requires confirmation and rejects `--yes`. Use `--dry-run` for a read-only preview; Publish still requires confirmation. **(BREAKING)**
- **Managed Agent SDK** — upgrade to `0.7.1`. Build automatically associates active Agent-local resources while preserving explicit bindings, Skill versions, and File mount paths. Ambiguous Environment or Vault selections are rejected before writing.
### Fixed
- **Project diagnostics** — provide actionable project-root guidance and surface the underlying Build validation error.
- **YAML initialization paths** — show the absolute YAML path in creation messages and existing-file errors.
### Internal
- Expand project initialization and Build regression coverage, and remove the obsolete Build confirmation flag from the local lifecycle E2E test.
## [1.21.0] - 2026-09-07
### Added
- **Managed Agent directory projects** — initialize, validate, build, and publish local projects with Agent-scoped resource configuration and resource examples.
- **Local project versions** — enable or disable snapshot versioning, list and preview history, and restore project files without Git.
- **Project Workbench** — edit resources, review changes, publish updates, and manage local versions in a browser.
### Changed
- **Playground version resolution** — check npm before launch, reuse matching local versions, and fetch the latest version when needed; retain explicit version and binary overrides.
- **Managed Agent SDK** — upgrade to `0.7.0` and consume project workspace and version services through SDK subpath exports.
## [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
-112
View File
@@ -6,118 +6,6 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.25.0] - 2026-09-14
### 新增
- **Token Plan harness 权益额度** —— `bl token-plan harness-quota` 查看 Token Plan harness 权益额度用量(Console 认证),联合 harness 列表与已发放权益,展示已用/总额度、使用比例和重置时间;待发放权益的 harness 以「发放中」状态列出。
- 通过 `--type official_tool|infrastructure` 筛选 harness 列表;支持额度框输出或 `--output json`。
## [1.24.0] - 2026-09-11
### 新增
- Sandbox 实例与模版生命周期命令,支持自动轮询构建状态和 `--async` 异步提交。
- `bl sandbox official-images` 与模版 `--image` 参数,内置代码解释器、浏览器和全能型镜像预设。
- `bl sandbox file upload` 使用 `source=sandbox_template` 上传模版挂载文件,返回的 File ID 可用于 `mntConfig`。
- Sandbox 复用 `--base-url`、环境变量和 Profile 配置,未配置时通过工作空间拼接接入地址。
- 独立的 `bailian-sandbox` Skill,提供命令参考和实例连接指南。
### 修复
- 模版构建轮询失败时保留已提交的 `templateID` 和 `buildID`,便于先查询已有构建,避免重复提交。
### 安全
- Sandbox REST 调用使用百炼 Bearer 鉴权,不依赖 E2B SDK 或 E2B API Key;连接凭据和 dry-run 环境变量默认脱敏。
## [1.23.0] - 2026-09-10
### 新增
- **Profile 级水印控制** —— 可通过 `bl config set --key watermark --value true|false` 设置图片生成与编辑、视频生成与编辑以及参考生视频命令的默认水印行为。
- **ASR 准确率增强** —— `bl speech recognize` 现支持通过 `--vocabulary` 传入即时热词、通过 `--context` 增强上下文词表,以及通过 `--vocabulary-id` 使用适用于对应 ASR 模型的预编译热词表。
- **语音热词表管理** —— 新增 `bl speech vocabulary create|list|get|update|delete`,用于管理可复用的预编译热词表。
## [1.22.0] - 2026-09-08
### 变更
- **项目初始化** —— `managed-agent project init` 默认创建 `./managed-agent` 子目录;如需原地初始化,请使用 `--project .`。**(BREAKING)**
- **Build 确认机制** —— `managed-agent project build` 无需确认,并且不再接受 `--yes`。使用 `--dry-run` 可只读预览;Publish 仍需显式确认。**(BREAKING)**
- **Managed Agent SDK** —— 升级至 `0.7.1`。Build 自动关联 Agent 目录下已启用的资源,保留显式引用、Skill 版本和 File 挂载路径;Environment 或 Vault 选择存在歧义时,在写入前报错。
### 修复
- **项目诊断** —— 提供可操作的项目根目录提示,并展示 Build 校验失败的具体原因。
- **YAML 初始化路径** —— 创建成功及文件已存在的错误信息均展示 YAML 绝对路径。
### 内部
- 补充项目初始化和 Build 回归覆盖,移除本地闭环 E2E 测试中过时的 Build 确认参数。
## [1.21.0] - 2026-09-07
### 新增
- **Managed Agent 目录项目** —— 支持初始化、校验、构建和发布本地项目,按 Agent 组织资源配置,并提供资源示例。
- **本地项目版本管理** —— 无需 Git 即可启停快照版本管理、查看和预览历史,以及恢复项目文件。
- **项目 Workbench** —— 在浏览器中编辑资源、审阅变更、发布更新和管理本地版本。
### 变更
- **Playground 版本选择** —— 启动前查询 npm,复用版本一致的本地安装,否则按需下载最新版;保留显式版本和启动文件覆盖。
- **Managed Agent SDK** —— 升级至 `0.7.0`,通过 SDK 子路径复用项目目录和版本服务。
## [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
### 新增
-1
View File
@@ -115,7 +115,6 @@ Once installed, just describe your task to your AI Agent — no need to assemble
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
-1
View File
@@ -114,7 +114,6 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
+8 -10
View File
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
`~/.bailian/config.json` 可同时保存 `api_key`、`access_token` 与 `access_key_*`。登录任一种方式不得删除另一种:
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 落盘成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 验证成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi` 与 `console` 域凭证
- `bl auth logout --console` 只清 `access_token`
@@ -47,9 +47,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
- `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录落盘成功后物化写入,升级 preset 需要重新登录
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
- 显式 `auth login --config <name>` 在凭证落盘成功后自动激活目标 Profile;未传
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
`--config` 时继续写当前激活项,失败和 dry-run 不切换
- `resolveConsole()` — `auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
- `resolveOpenApi()` — `auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
@@ -62,19 +62,17 @@ 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"` 硬门禁,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 能力。
- **联网命令** — `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` 仍需登录。
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK,管道五步:
1. `prepareProviderEnv()` — 调用 SDK 的凭证 bootstrap,再把凭证类 env(`CREDENTIAL_ENV_KEYS`,含兼容别名)中仍为 undefined 的项占位为 `""`,使 agents.yaml 插值阶段能够完成并由 CLI 输出明确的 Bailian-only 配置错误
2. `resolveProjectConfig` — 完成插值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`,避免空 key 在 SDK zod 层提前报 "received null"
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")
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. `assertBailianOnlyProviders(providers)` — 拒绝非 Bailian Provider;随后 `assertProviderCredentials(providers)` 在 Bailian `api_key` 为空时给出 CLI 权威 `AUTH` 错误和登录 hint;离线命令传 `credentials: "none"` 跳过 key 断言,但仍执行 Bailian-only 配置校验
5. `assertProviderCredentials(providers)` — 任一已声明 provider 的 `api_key` 为空 → CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
禁止命令层直接 `readConfigFile` 裸读凭证;Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
## 必查清单
+15 -28
View File
@@ -2,16 +2,16 @@
## 架构分层
| 层级 | 路径 | 测什么 |
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **共享基建** | `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 拒绝 |
| 层级 | 路径 | 测什么 |
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **共享基建** | `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 拒绝 |
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
## 触发条件
@@ -29,8 +29,7 @@
### commands E2E
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
- 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 映射)
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
- fixtures:`packages/commands/tests/e2e/fixtures/`
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map)
@@ -38,12 +37,10 @@
- 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`;runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`
- globalSetup:根 `vite.config.ts` → `packages/e2e/src/global-setup.ts`
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
@@ -51,12 +48,9 @@
## 双层 describe(固定结构)
```ts
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
describe("e2e: <topic>", () => {
test("<subcommand> --help 正常退出", async () => {
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
expect(result.exitCode, result.stderr).toBe(0);
});
test("<subcommand> --help 正常退出", ...);
});
// 2) skipIf:缺参 / dry-run / 真实集成
@@ -80,18 +74,11 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
## 用例类型
1. **--help**:`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
1. **--help**:`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
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 重复
@@ -118,7 +105,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` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
-2
View File
@@ -71,9 +71,7 @@ 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`;纯命令语法示例可保留为字符串,服务端错误不翻译
-1
View File
@@ -19,7 +19,6 @@
- 类型由 `ParsedFlags<typeof FLAGS>` 推导;避免手写 `flags.x as number` 这类断言
- 单 flag 必填用 `required: true`;跨 flag / 值相关校验放 `validate`
- 默认值 fallback 写在命令实现或 `Settings` 解析层,不要重复解析 env/config
- 需要在高风险确认前检查本地路径时,可用异步 `validate`;runtime 会在鉴权和确认前等待它完成。这里只允许本地只读检查,不写文件、不请求远端。非缺参的环境错误应抛出 `BailianError`,避免裸命令调用被当成缺参而仅显示 help。
### B. 鉴权 / 全局选项
+3 -3
View File
@@ -18,11 +18,11 @@
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`。
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
`auth login --config ...`,凭证落盘成功后自动激活该 Profile。
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过,file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录。
- 对命中内置 preset 的 Profile,API Key 登录落盘成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
- 对命中内置 preset 的 Profile,API Key 登录验证成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get` → `video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
@@ -30,7 +30,7 @@
## 3. 保持读写命令交互一致
- `auth login`、`config set` 等写命令未传 `--config` 时修改当前激活项。
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证实际落盘时
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
创建和激活;`config set --config <name>` 可创建但不自动激活。
- `config show`、`auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
-4
View File
@@ -40,8 +40,6 @@ 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」指向句
@@ -56,9 +54,7 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
### C. 归属与生成
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts` 的 `GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表;hub 最多加一行 hand-off
- [ ] 新增领域目录时:补齐 `SKILL.md`(含 `metadata.version`),同步 `packages/cli/package.json` 的 reference 格式化路径、`.vite-hooks/pre-commit` 的生成物暂存清单、`tools/release/check.mjs` 的生成物校验清单;同步 hub 与共享协议的领域路由
- [ ] 跑 `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) 一并核对
## 完成后自查
+18 -21
View File
@@ -17,12 +17,11 @@
│ ├─ ~/.bailian/telemetry.jsonl
│ └─ AEM(pid=bailian-cli-node, event name=命令路径)
│
└─ confirmationStage
└─ versionCheckStage → authStage
├─ apiKey → DashScope / 模型域
├─ console → Bailian Console Gateway
├─ openapi → 阿里云 OpenAPI
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
└─ authStage
├─ apiKey → DashScope / 模型域
├─ console → Bailian Console Gateway
├─ openapi → 阿里云 OpenAPI
└─ none → 无凭证域;本地命令也仍有 AEM 命令事件
```
### 1. 三套鉴权与埋点标识
@@ -30,7 +29,7 @@
| 命令声明 | 凭证 / 请求域 | 主要请求出口 | 后端埋点标识 | 前端埋点标识(AEM) |
| ----------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------ |
| `auth: "apiKey"` | API Key;DashScope / OpenAI-compatible 模型域 | `Client.request/requestJson`、`McpClient`、Managed Agent instrumented fetch、上传策略 | 有:`User-Agent`、`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=apiKey` |
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 有:Console Gateway 后端直接标识 | 有:`pid=bailian-cli-node`、`authMethod=console` |
| `auth: "console"` | Console access token;Bailian Console Gateway | `callConsoleGateway()` → `/cli/api.json` | 无 | 有:`pid=bailian-cli-node`、`authMethod=console` |
| `auth: "openapi"` | AccessKey ID/Secret,可选 STS token;阿里云 OpenAPI | `Client.openApiJson()` | 有:`x-dashscope-source-config` | 有:`pid=bailian-cli-node`、`authMethod=openapi` |
| `auth: "none"` | 无凭证域 | 本地逻辑或命令自行管理的登录/配置流程 | 无 | 有:`pid=bailian-cli-node`、`authMethod=none` |
@@ -41,7 +40,6 @@
- Managed Agent 的 `User-Agent` 对所有 SDK 请求注入;`x-dashscope-source-config` 仅对阿里云 host 注入
- DashScope 上传策略 `getPolicy` 只有 `x-dashscope-source-config`,没有显式 CLI `User-Agent`
- Console Gateway 用户数据由后端直接区分,不依赖 `x-dashscope-source-config`;AEM 的 `authMethod=console` 是独立的命令侧统计
- OpenAPI 的 ACS 签名头,以及 Console Gateway 的 `product`、`action`、`api` 是鉴权或路由字段,不计为埋点标识
### 2. 后端渠道参数
@@ -79,7 +77,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`:
@@ -94,7 +92,7 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
- `command`、`timestamp`、`durationMs`、`success`
- `cliVersion`、`nodeVersion`、`os`
- `authMethod`
- 失败时的 `errorMessage`、`exitCode`、`httpStatus`、`requestId`
- 失败时的 `errorMessage`、`httpStatus`、`requestId`
- 安全 allowlist 过滤后的 `params`
参数默认不上传,只有 `packages/core/src/telemetry/tracker.ts` 的 `PARAM_ALLOWLIST` 中字段会进入事件。不得加入 prompt、凭证、文件路径、URL、账号/租户/工作空间 ID 或其他用户内容。
@@ -110,16 +108,16 @@ source-config 只用于百炼 / DashScope API 侧消费,不发送到通用网
AEM 映射:
| AEM 字段 | 内容 |
| ---------- | ------------------------------------------------------------------ |
| event name | 命令路径 |
| `et` | `EXP` |
| `ext` | 除 `command`、`params` 外的结构化事件字段,包含失败时的 `exitCode` |
| `c1` | allowlist 参数 |
| `c2` | `success` / `failure` |
| `c3` | HTTP status |
| `c4` | 错误文案,最多 500 字符 |
| `c5` | request ID |
| AEM 字段 | 内容 |
| ---------- | ----------------------------------------- |
| event name | 命令路径 |
| `et` | `EXP` |
| `ext` | 除 `command`、`params` 外的结构化事件字段 |
| `c1` | allowlist 参数 |
| `c2` | `success` / `failure` |
| `c3` | HTTP status |
| `c4` | 错误文案,最多 500 字符 |
| `c5` | request ID |
远端发送是 best-effort,不得阻塞命令或改变退出码。正常退出最多等待 1 秒,SIGINT 最多等待 500 ms。
@@ -146,7 +144,6 @@ AEM 映射:
- [ ] 更新 `TrackingEvent`、`createTrackingEvent()` 与 `buildRemoteAemOptions()` 的字段映射
- [ ] 本地 JSONL 与远端 AEM 必须基于同一结构化事件,不能维护两套字段口径
- [ ] 成功与失败均覆盖;遥测异常必须静默且不改变业务退出码
- [ ] runtime 本地语义错误应记录 `exitCode`;新增字段默认随 AEM `ext` 上报,无需占用新的 `c1`—`c5`
- [ ] 检查 `DO_NOT_TRACK=1` 与 `telemetry: false` 两个关闭入口
- [ ] 错误字段不得额外拼接 token、请求体、prompt 或本地路径
+1 -1
View File
@@ -213,7 +213,7 @@ bl knowledge chunk delete --index-id <id> --chunk-id <id> [flags]
| ----------------- | ------ | ---- | ------------------------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
+4 -4
View File
@@ -232,10 +232,10 @@ bl knowledge category delete --category-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ---------------------- |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ------------ |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
+6 -6
View File
@@ -190,11 +190,11 @@ bl knowledge doc delete --index-id <id> --doc-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
| `--yes` | switch | 否 | 显式确认高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ----------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
# 删除单个文档
bl knowledge doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
# 用户明确确认后批量删除
# 批量删除,跳过确认
bl knowledge doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
```
+4 -4
View File
@@ -120,10 +120,10 @@ bl knowledge file delete --file-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | ---------------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | --------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
+5 -5
View File
@@ -254,10 +254,10 @@ bl knowledge delete --index-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ---------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
- **不可逆操作**:知识库及所有索引内容被永久删除。
- 数据中心中的源文件不受影响,仅删除知识库索引。
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要。
**示例**
+1 -1
View File
@@ -135,7 +135,7 @@
### 危险操作确认
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`、`service deploy`)在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
### Dry-run 模式
+9 -9
View File
@@ -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
View File
@@ -213,7 +213,7 @@ kscli chunk delete --index-id <id> --chunk-id <id> [flags]
| ----------------- | ------ | ---- | ------------------------------------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--chunk-id <id>` | array | 是 | Chunk ID(可重复,每批最多 10 个,超出自动分批) |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
+4 -4
View File
@@ -232,10 +232,10 @@ kscli category delete --category-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ---------------------- |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| -------------------- | ------ | ---- | ------------ |
| `--category-id <id>` | string | 是 | 分类 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
+6 -6
View File
@@ -190,11 +190,11 @@ kscli doc delete --index-id <id> --doc-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
| `--yes` | switch | 否 | 显式确认高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ----------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--doc-id <id>` | array | 是 | 文档 ID(可重复) |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
@@ -223,7 +223,7 @@ json 模式:返回 API 原始响应,`data.deleted[]` 为实际删除的 ID
# 删除单个文档
kscli doc delete --index-id idx-xxx --doc-id doc-xxx --workspace-id ws-xxx
# 用户明确确认后批量删除
# 批量删除,跳过确认
kscli doc delete --index-id idx-xxx --doc-id doc-a --doc-id doc-b --yes
```
+4 -4
View File
@@ -120,10 +120,10 @@ kscli file delete --file-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | ---------------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| ---------------- | ------ | ---- | --------------- |
| `--file-id <id>` | string | 是 | 数据中心文件 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
+5 -5
View File
@@ -254,10 +254,10 @@ kscli kb delete --index-id <id> [flags]
**参数**
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ---------------------- |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 显式确认执行高风险操作 |
| 参数 | 类型 | 必填 | 说明 |
| ----------------- | ------ | ---- | ------------ |
| `--index-id <id>` | string | 是 | 知识库 ID |
| `--yes` | switch | 否 | 跳过确认提示 |
**输出**
@@ -275,7 +275,7 @@ json 模式:返回 API 原始响应。
- **不可逆操作**:知识库及所有索引内容被永久删除。
- 数据中心中的源文件不受影响,仅删除知识库索引。
- 不带 `--yes` 时,runtime 会在调用知识库业务 API、执行删除前返回确认请求。
- 不带 `--yes` 时,CLI 会先查询知识库名称和文档数量作为确认摘要。
**示例**
+1 -1
View File
@@ -152,7 +152,7 @@ kscli --help
### 危险操作确认
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`)以及 `service deploy` 属于高风险操作。未带 `--yes` 时 CLI 不会执行,也不会弹出交互式 Y/N,而是返回 exit code 7 和 `requires_confirmation`;确认后在原命令中添加 `--yes` 重新执行。
涉及删除的命令(`kb delete`、`doc delete`、`chunk delete`、`file delete`、`category delete`、`service delete`)以及 `service deploy` 在执行前会弹出二次确认提示。使用 `--yes` 可跳过确认,适用于自动化脚本。
### Dry-run 模式
+9 -9
View File
@@ -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 | 否 | 跳过确认提示 |
**输出**
-1
View File
@@ -115,7 +115,6 @@ Once installed, just describe your task to your AI Agent — no need to assemble
| ------------------------ | --------------------------------------------------------------------------------- |
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
| Model selection | "Recommend a model for image understanding and customer support." |
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
-1
View File
@@ -114,7 +114,6 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
| ---------------- | ----------------------------------------------------------------------- |
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
+2 -2
View File
@@ -116,9 +116,9 @@
**Flags**:`--index-id` 必填;`--doc-id` array 必填(可重复);`--yes`。
**实现方案**:`doc-delete.ts`;命令在 `risk` 对象中同时声明 `level: "high"` 和双语 `message`,由 runtime 在 `run()` 前统一确认;输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
**实现方案**:`doc-delete.ts`;确认摘要含 index_id + doc_id 列表(≤5 个全列,超出显示前 5 + 总数);输出以 `data.deleted` 为准(与入参数量不一致时 text 模式警告差异)。
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 无 `--yes` 返回 exitCode 7 + `requires_confirmation` / live 配合 upload 清理链。
**测试方案**:help / 缺参×2 / dry-run 断言 `doc_ids` 数组 / 非 TTY 无 `--yes` exitCode 2 / live 配合 upload 清理链。
## doc tag — 批量更新文档标签
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.25.0",
"version": "1.18.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
@@ -41,7 +41,7 @@
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference skills/bailian-sandbox/reference'",
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference'",
"sync:skill-version": "tsx ../../tools/sync-skill-metadata.ts",
"build": "vp pack",
"dev": "tsx src/main.ts",
+2 -12
View File
@@ -181,13 +181,7 @@ function atomicSwap(tmpDir, catalogDir) {
if (existsSync(backup) && !existsSync(catalogDir)) renameSync(backup, catalogDir);
throw err;
}
// 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 */
}
if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
}
async function main() {
@@ -220,11 +214,7 @@ async function main() {
}
atomicSwap(tmpDir, catalogDir);
} catch (err) {
try {
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
} catch {
/* cleanup must not mask the original error */
}
if (existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
-153
View File
@@ -70,11 +70,6 @@ import {
searchWeb,
speechSynthesize,
speechRecognize,
speechVocabularyCreate,
speechVocabularyList,
speechVocabularyGet,
speechVocabularyUpdate,
speechVocabularyDelete,
fileUpload,
consoleCall,
usageFree,
@@ -90,7 +85,6 @@ import {
workspaceList,
quotaList,
quotaUpdate,
quotaDelete,
quotaHistory,
quotaCheck,
permissionList,
@@ -130,7 +124,6 @@ import {
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
tokenPlanHarnessQuota,
workspaceInit,
pluginInstall,
pluginLink,
@@ -146,18 +139,6 @@ import {
managedAgentPlan,
managedAgentApply,
managedAgentDestroy,
managedAgentPlayground,
managedAgentProjectInit,
managedAgentProjectValidate,
managedAgentProjectBuild,
managedAgentProjectPublish,
managedAgentProjectWorkbench,
managedAgentProjectVersionEnable,
managedAgentProjectVersionDisable,
managedAgentProjectVersionStatus,
managedAgentProjectVersionList,
managedAgentProjectVersionPreview,
managedAgentProjectVersionRestore,
managedAgentStateList,
managedAgentStateShow,
managedAgentStateRm,
@@ -170,63 +151,6 @@ 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,
sandboxCreate,
sandboxOfficialImages,
sandboxFileUpload,
sandboxList,
sandboxGet,
sandboxConnect,
sandboxPause,
sandboxResume,
sandboxDelete,
sandboxTemplateCreate,
sandboxTemplateList,
sandboxTemplateGet,
sandboxTemplateUpdate,
sandboxTemplateBuildStatus,
sandboxTemplateDelete,
} from "bailian-cli-commands";
// Full bailian-cli product: every command, exposed under the `bl` binary.
@@ -307,11 +231,6 @@ export const commands: Record<string, AnyCommand> = {
"search web": searchWeb,
"speech synthesize": speechSynthesize,
"speech recognize": speechRecognize,
"speech vocabulary create": speechVocabularyCreate,
"speech vocabulary list": speechVocabularyList,
"speech vocabulary get": speechVocabularyGet,
"speech vocabulary update": speechVocabularyUpdate,
"speech vocabulary delete": speechVocabularyDelete,
"file upload": fileUpload,
"console call": consoleCall,
"usage free": usageFree,
@@ -327,7 +246,6 @@ 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,
@@ -367,7 +285,6 @@ export const commands: Record<string, AnyCommand> = {
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
"token-plan add-member": tokenPlanAddMember,
"token-plan harness-quota": tokenPlanHarnessQuota,
"workspace init": workspaceInit,
"plugin install": pluginInstall,
"plugin link": pluginLink,
@@ -383,18 +300,6 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent plan": managedAgentPlan,
"managed-agent apply": managedAgentApply,
"managed-agent destroy": managedAgentDestroy,
"managed-agent playground": managedAgentPlayground,
"managed-agent project init": managedAgentProjectInit,
"managed-agent project validate": managedAgentProjectValidate,
"managed-agent project build": managedAgentProjectBuild,
"managed-agent project publish": managedAgentProjectPublish,
"managed-agent project workbench": managedAgentProjectWorkbench,
"managed-agent project version enable": managedAgentProjectVersionEnable,
"managed-agent project version disable": managedAgentProjectVersionDisable,
"managed-agent project version status": managedAgentProjectVersionStatus,
"managed-agent project version list": managedAgentProjectVersionList,
"managed-agent project version preview": managedAgentProjectVersionPreview,
"managed-agent project version restore": managedAgentProjectVersionRestore,
"managed-agent state list": managedAgentStateList,
"managed-agent state show": managedAgentStateShow,
"managed-agent state rm": managedAgentStateRm,
@@ -407,64 +312,6 @@ 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,
"sandbox create": sandboxCreate,
"sandbox official-images": sandboxOfficialImages,
"sandbox file upload": sandboxFileUpload,
"sandbox list": sandboxList,
"sandbox get": sandboxGet,
"sandbox connect": sandboxConnect,
"sandbox pause": sandboxPause,
"sandbox resume": sandboxResume,
"sandbox delete": sandboxDelete,
"sandbox template create": sandboxTemplateCreate,
"sandbox template list": sandboxTemplateList,
"sandbox template get": sandboxTemplateGet,
"sandbox template update": sandboxTemplateUpdate,
"sandbox template build-status": sandboxTemplateBuildStatus,
"sandbox template delete": sandboxTemplateDelete,
};
/**
@@ -65,7 +65,6 @@ describe("e2e: Command Pack", () => {
expect(linkedJson.linked.commands).toEqual([
"agent credential",
"agent credential-denied",
"agent dangerous",
"agent fail",
"agent output",
"agent ping",
@@ -171,34 +170,6 @@ 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);
@@ -212,7 +183,6 @@ describe("e2e: Command Pack", () => {
commands: [
"agent credential",
"agent credential-denied",
"agent dangerous",
"agent fail",
"agent output",
"agent ping",
@@ -1,26 +1,14 @@
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 { describe, expect, test } from "vite-plus/test";
import { deriveGroupPaths } from "e2e/registry-smoke";
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 runCliSmoke(["--help"]);
const { stderr, exitCode } = await runCli(["--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/\bbl\b/i);
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
@@ -36,7 +24,7 @@ describe("e2e: bl registry smoke", () => {
});
test("分组帮助按叶子命令展示不同鉴权域", async () => {
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
const { stderr, exitCode } = await runCli(["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/);
@@ -48,13 +36,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 runCliSmoke([...commandPath, "--help"]);
const { stderr, exitCode } = await runCli([...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 runCliSmoke(["quota", "check", "--help"]);
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/Global Flags:/);
expect(stderr).toMatch(/--console-region <region>/);
@@ -64,25 +52,13 @@ describe("e2e: bl registry smoke", () => {
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
});
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(commandPaths)("已注册命令 %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]`,
);
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
expect(exitCode, stderr).toBe(0);
});
});
-21
View File
@@ -19,26 +19,6 @@ 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",
@@ -77,7 +57,6 @@ const fail = {
export default {
"agent credential": credential,
"agent credential-denied": credentialDenied,
"agent dangerous": dangerous,
"agent fail": fail,
"agent output": output,
"agent ping": ping,
@@ -1,69 +0,0 @@
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,53 +0,0 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { validateSkillDir } from "bailian-cli-core";
import { expect, test } from "vite-plus/test";
import { parse } from "yaml";
import { commands } from "../src/commands.ts";
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
const sandboxSkillDirectory = join(repositoryRoot, "skills/bailian-sandbox");
test("Sandbox is a valid standalone skill aligned with the CLI version", () => {
const metadata = validateSkillDir(sandboxSkillDirectory, "bailian-sandbox");
expect(metadata.name).toBe("bailian-sandbox");
const skillMarkdown = readFileSync(join(sandboxSkillDirectory, "SKILL.md"), "utf8");
const frontmatter = parse(skillMarkdown.split(/^---\s*$/m)[1]) as {
metadata: { version: string };
};
const cliPackage = JSON.parse(
readFileSync(join(repositoryRoot, "packages/cli/package.json"), "utf8"),
) as { version: string };
expect(frontmatter.metadata.version).toBe(cliPackage.version);
for (const link of skillMarkdown.matchAll(/\]\(([^)]+)\)/g)) {
expect(existsSync(join(sandboxSkillDirectory, link[1]))).toBe(true);
}
});
test("Sandbox reference contains every registered Sandbox command only in its owning skill", () => {
const expectedCommands = Object.keys(commands)
.filter((commandPath) => commandPath.startsWith("sandbox "))
.map((commandPath) => `bl ${commandPath}`)
.sort();
expect(expectedCommands.length).toBeGreaterThan(0);
const reference = readFileSync(join(sandboxSkillDirectory, "reference/sandbox.md"), "utf8");
const documentedCommands = [...reference.matchAll(/^### `([^`]+)`/gm)]
.map((match) => match[1])
.sort();
expect(documentedCommands).toEqual(expectedCommands);
const index = readFileSync(join(sandboxSkillDirectory, "reference/index.md"), "utf8");
for (const commandPath of expectedCommands) {
expect(index).toContain(`\`${commandPath}\``);
}
expect(existsSync(join(repositoryRoot, "skills/bailian-cli/reference/sandbox.md"))).toBe(false);
const hubIndex = readFileSync(
join(repositoryRoot, "skills/bailian-cli/reference/index.md"),
"utf8",
);
expect(hubIndex).not.toContain("`bl sandbox ");
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.25.0",
"version": "1.18.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.7.1",
"@openagentpack/sdk": "0.3.2",
"bailian-cli-core": "workspace:*",
"bailian-cli-runtime": "workspace:*",
"boxen": "catalog:",
@@ -1,10 +1,23 @@
import { normalizeModelBaseUrl, type AuthPersistPatch, type AuthStore } from "bailian-cli-core";
import {
BailianError,
ExitCode,
chatPath,
requestJson,
normalizeModelBaseUrl,
type AuthPersistPatch,
type AuthStore,
type Identity,
type Settings,
} from "bailian-cli-core";
interface ApiKeyLoginDeps {
identity: Identity;
settings: Settings;
authStore: AuthStore;
}
interface ApiKeyLoginProfile {
baseUrl: string;
persistBaseUrl?: string;
defaultTextModel?: string;
defaultVideoModel?: string;
@@ -17,19 +30,65 @@ interface ApiKeyLoginProfile {
persistPatch?: AuthPersistPatch;
}
/**
* Persist an API key (and optional profile defaults) without a live model probe.
* Login is credential storage; connectivity is verified on the first API command.
* A former chat/completions smoke test conflated quota/model-access 403s with bad keys.
*/
export async function persistApiKey(
const RETRY_DELAY_BASE_MS = 500;
function canRetry(error: unknown): boolean {
if (error instanceof BailianError) {
if (error.exitCode === ExitCode.NETWORK || error.exitCode === ExitCode.TIMEOUT) return true;
const status = error.api?.httpStatus;
return status === 401 || (status !== undefined && status >= 500);
}
if (error instanceof Error) {
return (
error.name === "AbortError" ||
error.name === "TimeoutError" ||
error.message.includes("timed out") ||
error.message === "fetch failed"
);
}
return false;
}
export async function validateAndPersistApiKey(
deps: ApiKeyLoginDeps,
key: string,
profile: ApiKeyLoginProfile,
): Promise<void> {
process.stderr.write("Testing key... ");
const httpDeps = { identity: deps.identity, settings: deps.settings };
const baseUrl = normalizeModelBaseUrl(profile.baseUrl);
const persistBaseUrl = profile.persistBaseUrl
? normalizeModelBaseUrl(profile.persistBaseUrl)
: undefined;
const validationModel = "qwen3.8-max";
const requestOpts = {
url: baseUrl + chatPath(),
method: "POST",
headers: { Authorization: `Bearer ${key}` },
timeout: Math.min(deps.settings.timeout, 30),
body: {
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
},
};
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(httpDeps, requestOpts);
break;
} catch (error) {
if (attempt >= 3 || !canRetry(error)) {
process.stderr.write("Failed\n");
throw error;
}
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
process.stderr.write("Valid\n");
await deps.authStore.login({
...profile.persistPatch,
api_key: key,
@@ -11,7 +11,7 @@ import {
type Settings,
} from "bailian-cli-core";
import { listenLocalServer, openInBrowser } from "../shared/local-server.ts";
import { persistApiKey } from "./login-api-key.ts";
import { validateAndPersistApiKey } from "./login-api-key.ts";
/** 登录流程的能力面:身份(UA)、有效配置(timeout 等)、auth 域落盘。 */
export interface LoginDeps {
@@ -413,7 +413,9 @@ export async function runConsoleLogin(
workspace_id: workspaceId || undefined,
};
if (apiKey) {
await persistApiKey(deps, apiKey, {
const testBaseUrl = baseUrl || deps.authStore.resolveBaseUrl();
await validateAndPersistApiKey(deps, apiKey, {
baseUrl: testBaseUrl,
persistBaseUrl: baseUrl || undefined,
persistPatch: callbackPatch,
});
+7 -6
View File
@@ -5,7 +5,7 @@ import {
normalizeModelBaseUrl,
} from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
import { persistApiKey } from "./login-api-key.ts";
import { validateAndPersistApiKey } from "./login-api-key.ts";
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
const LOGIN_MODE_HINT = "Choose exactly one login mode: --api-key, --console, or --open-api";
@@ -33,8 +33,8 @@ export default defineCommand({
type: "string",
valueHint: "<url>",
description: {
"en-US": "Model API base URL to store with --api-key",
"zh-CN": "与 --api-key 一并保存的模型 API Base URL",
"en-US": "Model API base URL (used with --api-key for validation)",
"zh-CN": "模型 API Base URL(用于配合 --api-key 进行验证)",
},
},
console: {
@@ -164,17 +164,19 @@ export default defineCommand({
if (!key) return;
if (settings.dryRun) {
emitBare("Would save API key.");
emitBare("Would validate and save API key.");
return;
}
const profilePreset = getModelProfilePreset(settings.configName);
const stored = store.stored();
const storedBaseUrl = stored.baseUrl;
const resolvedBaseUrl = baseUrl || store.resolveBaseUrl(profilePreset?.baseUrl);
const persistBaseUrl = baseUrl || (!storedBaseUrl ? profilePreset?.baseUrl : undefined);
const apiKeyCapabilities = profilePreset
? [...new Set([...(stored.apiKeyCapabilities ?? []), ...profilePreset.apiKeyCapabilities])]
: stored.apiKeyCapabilities;
await persistApiKey(deps, key, {
await validateAndPersistApiKey(deps, key, {
baseUrl: resolvedBaseUrl,
persistBaseUrl,
defaultTextModel: profilePreset?.defaultTextModel,
defaultVideoModel: profilePreset?.defaultVideoModel,
@@ -185,6 +187,5 @@ export default defineCommand({
defaultSpeechRecognitionModel: profilePreset?.defaultSpeechRecognitionModel,
apiKeyCapabilities,
});
process.stderr.write(`API key saved to ${store.path}\n`);
},
});
+2 -3
View File
@@ -12,9 +12,9 @@ export default defineCommand({
valueHint: "<key>",
description: {
"en-US":
"Config key (language, base_url, output, output_dir, timeout, watermark, api_key, api_key_capabilities, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
"Config key (language, base_url, output, output_dir, timeout, api_key, api_key_capabilities, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
"zh-CN":
"配置项名称(language、base_url、output、output_dir、timeout、watermark、api_key、api_key_capabilities、access_token、access_key_id、access_key_secret、security_token、default_*_model、workspace_id)",
"配置项名称(language、base_url、output、output_dir、timeout、api_key、api_key_capabilities、access_token、access_key_id、access_key_secret、security_token、default_*_model、workspace_id)",
},
required: true,
},
@@ -29,7 +29,6 @@ export default defineCommand({
"--key language --value zh-CN",
"--key output --value json",
"--key timeout --value 600",
"--key watermark --value false",
"--key base_url --value https://dashscope.aliyuncs.com",
"--config company-plan --key api-key-capabilities --value text.chat,image.generate",
],
@@ -3,7 +3,6 @@ import {
ExitCode,
isApiKeyCapability,
normalizeModelBaseUrl,
parseBooleanValue,
SUPPORTED_LANGUAGES,
} from "bailian-cli-core";
@@ -14,7 +13,6 @@ export const VALID_KEYS = [
"output",
"output_dir",
"timeout",
"watermark",
"api_key",
"access_token",
"access_key_id",
@@ -64,7 +62,7 @@ export const UI_ENUM_KEYS: Record<string, string[]> = {
};
// Keys the UI renders as a true/false dropdown and stores as a boolean.
export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry", "watermark"]);
export const UI_BOOLEAN_KEYS = new Set<string>(["telemetry"]);
// Default model each `default_*_model` key falls back to when left unset. These
// mirror the inline `|| "<model>"` fallbacks in the generation commands
@@ -166,10 +164,7 @@ export function resolveKey(key: string): string {
* Validate a single config entry and coerce its value to the stored type.
* Throws BailianError(USAGE) for unknown keys or invalid values.
*/
export function validateAndCoerce(
key: string,
value: string,
): string | number | boolean | string[] {
export function validateAndCoerce(key: string, value: string): string | number | string[] {
const resolvedKey = resolveKey(key);
if (!(VALID_KEYS as readonly string[]).includes(resolvedKey)) {
@@ -206,8 +201,6 @@ export function validateAndCoerce(
if (resolvedKey === "base_url") return normalizeModelBaseUrl(value);
if (resolvedKey === "watermark") return parseBooleanValue(value, "watermark");
if (resolvedKey === "api_key_capabilities") {
let rawCapabilities: unknown;
if (value.trim().startsWith("[")) {
@@ -18,7 +18,6 @@ export default defineCommand({
base_url: client.baseUrl,
output: settings.output,
timeout: settings.timeout,
watermark: settings.watermark,
config: settings.configName ?? "default",
config_file: store.path,
};
@@ -13,20 +13,9 @@ 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",
"--file-id file-id-xxx --yes",
],
exampleArgs: ["--file-id file-id-xxx", "--file-id file-id-xxx --dry-run"],
async run(ctx) {
const { settings, flags } = ctx;
const fileId = flags.fileId;
@@ -40,20 +40,9 @@ 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",
"--deployed-model dep-... --yes",
],
exampleArgs: ["--deployed-model dep-...", "--deployed-model dep-... --dry-run"],
async run(ctx) {
const { settings, flags } = ctx;
const deployedModel = flags.deployedModel;
@@ -13,16 +13,9 @@ 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", "--job-id ft-xxx --yes"],
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"],
notes: [
{
"en-US":
+1 -1
View File
@@ -207,7 +207,7 @@ export default defineCommand({
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark, settings.watermark);
const watermark = resolveWatermark(flags.watermark);
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size: resolveImageSize(flags.size, route.sizeProfile),
@@ -185,7 +185,7 @@ export default defineCommand({
"prompt-extend",
);
const watermark = resolveWatermark(flags.watermark, settings.watermark);
const watermark = resolveWatermark(flags.watermark);
const parameters: NonNullable<DashScopeImageRequest["parameters"]> = {
size,
@@ -6,7 +6,7 @@ import {
type FlagsDef,
type RagConnectorResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CATEGORY_DELETE_FLAGS = {
@@ -16,19 +16,16 @@ 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: [
@@ -52,6 +49,11 @@ 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 } from "bailian-cli-runtime";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const CHUNK_DELETE_FLAGS = {
@@ -26,6 +26,10 @@ const CHUNK_DELETE_FLAGS = {
},
required: true,
},
yes: {
type: "switch",
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
},
...WORKSPACE_FLAG,
} satisfies FlagsDef;
@@ -44,13 +48,6 @@ 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: [
@@ -84,6 +81,11 @@ 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 } from "bailian-cli-runtime";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const DOC_DELETE_FLAGS = {
@@ -25,22 +25,28 @@ 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: [
@@ -66,7 +72,7 @@ export default defineCommand({
},
],
exampleArgs: [
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx --dry-run",
"--index-id idx-xxx --doc-id file-xxx --workspace-id ws-xxx",
"--index-id idx-xxx --doc-id file-a --doc-id file-b --yes",
],
async run(ctx) {
@@ -83,6 +89,11 @@ 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,10 +3,12 @@ import {
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type Client,
type FlagsDef,
type RagConnectorResponse,
type RagDescribeFileResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const FILE_DELETE_FLAGS = {
@@ -19,23 +21,39 @@ 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: [
@@ -64,6 +82,11 @@ 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,10 +4,12 @@ import {
RAG_PATHS,
detectOutputFormat,
type FlagsDef,
type RagIndexFilesResponse,
type RagMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
import { fetchIndexDetail } from "./kb-info.ts";
const KB_DELETE_FLAGS = {
indexId: {
@@ -16,23 +18,50 @@ 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: [
@@ -61,6 +90,11 @@ 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,10 +3,12 @@ import {
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type Client,
type FlagsDef,
type RagAgentGetResponse,
type RagAgentMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const SERVICE_DELETE_FLAGS = {
@@ -16,23 +18,48 @@ 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: [
@@ -64,6 +91,11 @@ 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,10 +3,12 @@ import {
ragEndpoint,
RAG_PATHS,
detectOutputFormat,
type Client,
type FlagsDef,
type RagAgentGetResponse,
type RagAgentMutationResponse,
} from "bailian-cli-core";
import { emitResult, emitBare } from "bailian-cli-runtime";
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
import { agentMutationField, resolveWorkspaceId, WORKSPACE_FLAG } from "./shared.ts";
const SERVICE_DEPLOY_FLAGS = {
@@ -24,23 +26,49 @@ 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: [
@@ -81,6 +109,11 @@ 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",
@@ -1,196 +0,0 @@
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,21 +20,17 @@ export { CREDENTIALS_NOTE, OFFLINE_NOTE } from "./credentials.ts";
/**
* Whether this run requires provider keys:
* - "all" (default) — online command: the Bailian provider must have a
* non-empty key after injection
* - "all" (default) — online command: every provider declared in agents.yaml
* 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;
environment?: Record<string, string>;
projectName?: string;
statePath?: string;
credentials?: CredentialScope;
overrideBailianBaseUrl?: boolean;
}
/**
@@ -46,8 +42,7 @@ 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. enforce Bailian-only config and fail with a CLI-authoritative AUTH error
* if its key is empty
* 5. fail with a CLI-authoritative AUTH error if any provider's key is empty
* (offline commands pass `credentials: "none"` to skip the check)
*/
export async function resolveAgentProjectConfig(
@@ -58,11 +53,8 @@ export async function resolveAgentProjectConfig(
prepareProviderEnv();
const resolved = await resolveProjectConfig(filePath, options);
normalizeInterpolatedProviderBlocks(resolved.config.providers);
injectProviderCredentials(resolved.config.providers, host, {
overrideBaseUrl: options.overrideBailianBaseUrl,
});
injectProviderCredentials(resolved.config.providers, host);
scrubCredentialEnv();
assertBailianOnlyProviders(resolved.config.providers);
if ((options.credentials ?? "all") !== "none") {
assertProviderCredentials(resolved.config.providers);
}
@@ -99,23 +91,15 @@ export async function buildAgentRuntime(
return { ...ctx, configPath };
}
/** 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,
/** 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}.`,
);
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,12 +34,6 @@ 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;
@@ -49,18 +43,24 @@ 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"`). 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"`); 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.
*/
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,7 +73,6 @@ 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 凭证。",
@@ -99,10 +98,9 @@ export function prepareProviderEnv(): void {
* Override the bailian provider block with bl's authStage-resolved credential, so
* the bailian API key is authoritatively the CLI auth chain's — never a config
* file bare-read or a stale env value. `api_key` is replaced unconditionally
* when a credential resolved. `base_url` is normally filled only when empty so
* a literal in agents.yaml remains supported; directory projects pass
* `overrideBaseUrl` so their connection always follows the Bailian CLI auth
* chain. `workspace_id` is filled only when empty.
* when a credential resolved; `base_url` / `workspace_id` are filled only when
* the block references them and the interpolated value is empty (a literal in
* agents.yaml is respected).
*
* `base_url` carries {@link AGENTSTUDIO_API_PATH} because the SDK appends resource
* paths onto it verbatim; a value already ending in the suffix is left as-is.
@@ -115,7 +113,6 @@ export function prepareProviderEnv(): void {
export function injectProviderCredentials(
providers: Record<string, unknown>,
host: CredentialHost,
options: { overrideBaseUrl?: boolean } = {},
): void {
const bailian = providers.bailian;
if (!bailian || typeof bailian !== "object") return;
@@ -123,7 +120,7 @@ export function injectProviderCredentials(
const cred = host.client.exportApiCredential();
if (cred) block.api_key = cred.token;
if ("base_url" in block && (options.overrideBaseUrl || !block.base_url)) {
if ("base_url" in block && !block.base_url) {
// Defensive normalization: the auth chain already normalizes base_url to
// an origin, but never let a trailing slash produce "//api/v1/agentstudio".
const origin = host.client.baseUrl.replace(/\/+$/, "");
@@ -169,16 +166,15 @@ export function normalizeInterpolatedProviderBlocks(providers: Record<string, un
}
/**
* 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.
* 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.
*/
export function assertProviderCredentials(
providers: Record<string, unknown>,
targetProviders?: readonly string[],
): void {
export function assertProviderCredentials(providers: Record<string, unknown>): 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,12 +12,6 @@ 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";
@@ -51,42 +45,6 @@ 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";
export function createFileStateScope(configPath: string, projectName?: string): StateScope {
function createStateScope(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(createFileStateScope(resolved, projectName));
const path = backend.getStatePath(createStateScope(resolved, projectName));
return StateManager.load(path);
}
@@ -1,68 +0,0 @@
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,9 +8,8 @@ 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(initialPage);
const first = await fetchPage();
const items = [...first.items];
let hasMore = first.hasMore;
let nextPage = first.nextPage;
@@ -1,432 +0,0 @@
import { execFile, spawn, type ChildProcess } from "node:child_process";
import { createHash, randomBytes } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, resolve } from "node:path";
import { promisify } from "node:util";
import { BailianError, type Client, ExitCode, type Settings } from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
const PLAYGROUND_PACKAGE = "@openagentpack/playground";
const execFileAsync = promisify(execFile);
const DEFAULT_PORT = 4848;
const PLAYGROUND_URL_PATTERN = /running at http:\/\/localhost:(\d+)/i;
export interface PlaygroundLaunchOptions {
port?: number;
open: boolean;
file?: string;
project?: string;
agent?: string;
surface: "preview" | "workbench";
client: Client;
settings: Settings;
}
interface Launcher {
command: string;
args: string[];
version?: string;
fetched: boolean;
}
interface ExistingPlayground {
version: string;
pid: number;
projectId?: string;
}
interface PlaygroundProjectSummary {
status?: string;
agents?: Array<{ agent?: { id?: string } }>;
}
export interface PlaygroundBrowserTarget {
url: string;
warning?: string;
}
export async function launchManagedAgentPlayground(
options: PlaygroundLaunchOptions,
): Promise<void> {
assertSupportedNodeVersion();
const port = options.port ?? DEFAULT_PORT;
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
throw new BailianError(`Invalid --port '${port}'.`, ExitCode.USAGE);
}
const sourcePath = resolve(
options.surface === "workbench" ? (options.project ?? ".") : (options.file ?? "agents.yaml"),
);
const projectId = createHash("sha256").update(sourcePath).digest("hex").slice(0, 16);
const launcher = await resolveLauncher();
const existing = await probeExistingPlayground(port);
if (existing) {
const reusable =
existing.projectId === projectId &&
(launcher.version === undefined || existing.version === launcher.version);
if (reusable) {
emitBare(`Workbench already running at http://localhost:${port} (pid ${existing.pid}).`);
await openPlaygroundSurface(port, options.surface, options.agent, options.open);
return;
}
const released = await replaceExistingPlayground(existing, port);
if (!released) {
throw new BailianError(
`Could not stop the existing Workbench process (pid ${existing.pid}) on port ${port}.`,
ExitCode.GENERAL,
"Stop it manually or choose another --port.",
);
}
}
const environment = buildPlaygroundEnvironment(options, port, sourcePath);
if (launcher.fetched) {
emitBare(`Fetching ${PLAYGROUND_PACKAGE} (first run may take a moment)...`);
}
const child = spawn(launcher.command, launcher.args, {
env: environment,
stdio: ["inherit", "pipe", "inherit"],
});
const removeSignalForwarding = forwardSignals(child);
try {
const readyPort = await waitForPlaygroundReady(child, port, 30_000, projectId);
if (readyPort === null) {
throw new BailianError(
`Workbench did not become ready in time. Check the logs above, then open http://localhost:${port}.`,
ExitCode.GENERAL,
);
}
emitBare(`Workbench ready at http://localhost:${readyPort}`);
await openPlaygroundSurface(readyPort, options.surface, options.agent, options.open);
const exitCode = await waitForChildExit(child);
if (exitCode !== 0) {
throw new BailianError(`Workbench exited with code ${exitCode}.`, ExitCode.GENERAL);
}
} finally {
removeSignalForwarding();
}
}
export function playgroundBrowserTargetFromSummary(
baseUrl: string,
summary: PlaygroundProjectSummary,
requestedAgent?: string,
): PlaygroundBrowserTarget {
if (summary.status !== "valid") return { url: baseUrl };
const agentIds = (summary.agents ?? [])
.map((entry) => entry.agent?.id?.trim())
.filter((agentId): agentId is string => Boolean(agentId));
const requested = requestedAgent?.trim();
if (requested) {
if (agentIds.includes(requested)) {
return { url: `${baseUrl}/agents/${encodeURIComponent(requested)}/preview` };
}
return {
url: baseUrl,
warning: `Agent '${requested}' was not found. Opening the project Workbench instead.`,
};
}
if (agentIds.length === 1) {
return { url: `${baseUrl}/agents/${encodeURIComponent(agentIds[0]!)}/preview` };
}
if (agentIds.length > 1) {
return {
url: baseUrl,
warning:
"This project declares multiple Agents. Opening the Workbench; rerun with --agent <id> for Preview.",
};
}
return { url: baseUrl };
}
function assertSupportedNodeVersion(): void {
const majorVersion = Number(process.versions.node.split(".")[0]);
if (Number.isFinite(majorVersion) && majorVersion >= 22) return;
throw new BailianError(
"Managed Agent Workbench requires Node.js 22 or later.",
ExitCode.USAGE,
"Upgrade Node.js for Workbench; other Bailian CLI commands continue to support Node.js 18.17+.",
);
}
export async function resolveLauncher(): Promise<Launcher> {
const explicit =
process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN?.trim() ||
process.env.AGENTS_PLAYGROUND_BIN?.trim();
if (explicit) {
if (!existsSync(explicit)) {
throw new BailianError(
`Configured Workbench binary does not exist: ${explicit}`,
ExitCode.USAGE,
);
}
return { command: process.execPath, args: [explicit], fetched: false };
}
const requestedVersion = process.env.BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION?.trim() || "latest";
const { stdout } = await execFileAsync(
"npm",
[
"view",
`${PLAYGROUND_PACKAGE}@${requestedVersion}`,
"version",
"--json",
"--prefer-online",
"--fetch-retries=0",
"--fetch-timeout=10000",
],
{ timeout: 15_000, maxBuffer: 1024 * 1024 },
);
let resolvedVersion: unknown;
try {
resolvedVersion = JSON.parse(stdout);
} catch {
resolvedVersion = undefined;
}
if (
typeof resolvedVersion !== "string" ||
!/^\d+\.\d+\.\d+(?:-[\da-zA-Z.-]+)?(?:\+[\da-zA-Z.-]+)?$/.test(resolvedVersion)
) {
throw new BailianError(
"npm did not return a single valid Playground version. Specify an exact version or dist-tag. / npm 未返回唯一有效的 Playground 版本号。请指定精确版本或 dist-tag。",
ExitCode.GENERAL,
);
}
const installed = resolveInstalledPlayground();
if (installed?.version === resolvedVersion) return installed;
const monorepoBinary = findLocalPlaygroundBin(process.cwd());
if (
monorepoBinary &&
readPlaygroundVersion(resolve(dirname(monorepoBinary), "../../package.json")) ===
resolvedVersion
) {
return {
command: process.execPath,
args: [monorepoBinary],
version: resolvedVersion,
fetched: false,
};
}
return {
command: "npx",
args: ["-y", `${PLAYGROUND_PACKAGE}@${resolvedVersion}`],
version: resolvedVersion,
fetched: true,
};
}
function readPlaygroundVersion(manifestPath: string): string | undefined {
try {
return (JSON.parse(readFileSync(manifestPath, "utf8")) as { version?: string }).version;
} catch {
return undefined;
}
}
function resolveInstalledPlayground(): Launcher | undefined {
try {
const require = createRequire(import.meta.url);
const packageJsonPath = require.resolve(`${PLAYGROUND_PACKAGE}/package.json`);
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
version?: string;
bin?: string | Record<string, string>;
};
const relativeBinary =
typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["agents-playground"];
if (!relativeBinary) return undefined;
const binaryPath = resolve(dirname(packageJsonPath), relativeBinary);
if (!existsSync(binaryPath)) return undefined;
return {
command: process.execPath,
args: [binaryPath],
version: manifest.version,
fetched: false,
};
} catch {
return undefined;
}
}
function findLocalPlaygroundBin(startDirectory: string): string | undefined {
let directory = startDirectory;
for (let depth = 0; depth < 10; depth += 1) {
const candidate = resolve(directory, "packages/playground/dist/bin/playground.js");
if (existsSync(candidate)) return candidate;
const parent = dirname(directory);
if (parent === directory) break;
directory = parent;
}
return undefined;
}
function buildPlaygroundEnvironment(
options: PlaygroundLaunchOptions,
port: number,
sourcePath: string,
): NodeJS.ProcessEnv {
const credential = options.client.exportApiCredential();
const environment: NodeJS.ProcessEnv = {
...process.env,
PORT: String(port),
AGENTS_PLAYGROUND_TOKEN: randomBytes(32).toString("hex"),
};
if (options.surface === "workbench") {
delete environment.AGENTS_CONFIG_PATH;
environment.AGENTS_PROJECT_ROOT = sourcePath;
} else {
delete environment.AGENTS_PROJECT_ROOT;
environment.AGENTS_CONFIG_PATH = sourcePath;
}
if (credential) environment.DASHSCOPE_API_KEY = credential.token;
const baseUrl = options.client.baseUrl.replace(/\/+$/, "");
environment.BAILIAN_BASE_URL = baseUrl.endsWith("/api/v1/agentstudio")
? baseUrl
: `${baseUrl}/api/v1/agentstudio`;
if (options.settings.workspaceId) {
environment.BAILIAN_WORKSPACE_ID = options.settings.workspaceId;
}
return environment;
}
async function waitForPlaygroundReady(
child: ChildProcess,
fallbackPort: number,
timeoutMs: number,
expectedProjectId: string,
): Promise<number | null> {
let port = fallbackPort;
let outputBuffer = "";
child.stdout?.on("data", (chunk: Buffer | string) => {
process.stdout.write(chunk);
outputBuffer += chunk.toString();
const match = outputBuffer.match(PLAYGROUND_URL_PATTERN);
if (match?.[1]) port = Number(match[1]);
});
const deadline = Date.now() + timeoutMs;
while (Date.now() <= deadline) {
if (child.exitCode !== null) return null;
try {
const response = await fetch(`http://localhost:${port}/health`, {
signal: AbortSignal.timeout(1_000),
});
const body = response.ok
? ((await response.json()) as { playground?: { project_id?: string } })
: undefined;
if (body?.playground?.project_id === expectedProjectId) return port;
} catch {
// Not ready yet.
}
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 300));
}
return null;
}
async function probeExistingPlayground(port: number): Promise<ExistingPlayground | null> {
try {
const response = await fetch(`http://localhost:${port}/health`, {
signal: AbortSignal.timeout(2_000),
});
if (!response.ok) return null;
const body = (await response.json()) as {
playground?: { version?: string; pid?: number; project_id?: string };
};
if (!body.playground?.pid) return null;
return {
version: body.playground.version ?? "unknown",
pid: body.playground.pid,
projectId: body.playground.project_id,
};
} catch {
return null;
}
}
async function replaceExistingPlayground(
existing: ExistingPlayground,
port: number,
): Promise<boolean> {
emitBare(`Replacing Workbench v${existing.version} (pid ${existing.pid}) on port ${port}...`);
try {
process.kill(existing.pid, "SIGTERM");
} catch {
return true;
}
for (let attempt = 0; attempt < 30; attempt += 1) {
await new Promise<void>((resolveWait) => setTimeout(resolveWait, 100));
if (!(await probeExistingPlayground(port))) return true;
}
return false;
}
async function openPlaygroundSurface(
port: number,
surface: "preview" | "workbench",
requestedAgent: string | undefined,
shouldOpen: boolean,
): Promise<void> {
if (!shouldOpen) return;
const target =
surface === "workbench"
? { url: `http://localhost:${port}` }
: await resolvePlaygroundBrowserTarget(port, requestedAgent);
if (target.warning) emitBare(`Warning: ${target.warning}`);
openBrowser(target.url);
}
async function resolvePlaygroundBrowserTarget(
port: number,
requestedAgent?: string,
): Promise<PlaygroundBrowserTarget> {
const baseUrl = `http://localhost:${port}`;
try {
const response = await fetch(`${baseUrl}/api/project`, {
signal: AbortSignal.timeout(3_000),
});
if (!response.ok) return { url: baseUrl };
return playgroundBrowserTargetFromSummary(
baseUrl,
(await response.json()) as PlaygroundProjectSummary,
requestedAgent,
);
} catch {
return { url: baseUrl };
}
}
function openBrowser(url: string): void {
const command =
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
const args = process.platform === "win32" ? ["", url] : [url];
try {
spawn(command, args, {
stdio: "ignore",
detached: true,
shell: process.platform === "win32",
}).unref();
} catch {
emitBare(`Could not open a browser automatically. Visit ${url}`);
}
}
function forwardSignals(child: ChildProcess): () => void {
const forwardInterrupt = () => child.kill("SIGINT");
const forwardTerminate = () => child.kill("SIGTERM");
process.on("SIGINT", forwardInterrupt);
process.on("SIGTERM", forwardTerminate);
return () => {
process.off("SIGINT", forwardInterrupt);
process.off("SIGTERM", forwardTerminate);
};
}
function waitForChildExit(child: ChildProcess): Promise<number> {
if (child.exitCode !== null) return Promise.resolve(child.exitCode);
return new Promise((resolveExit, rejectExit) => {
child.once("exit", (exitCode) => resolveExit(exitCode ?? 0));
child.once("error", rejectExit);
});
}
@@ -1,406 +0,0 @@
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)}`);
}
}
@@ -1,52 +0,0 @@
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),
]);
}
@@ -1,585 +0,0 @@
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.",
);
}
},
});
@@ -1,45 +0,0 @@
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)}`);
},
});
@@ -1,53 +0,0 @@
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.",
});
},
});
@@ -1,59 +0,0 @@
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.",
});
},
});
@@ -1,52 +0,0 @@
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,9 +8,13 @@ import {
import { emitBare, emitResult } from "bailian-cli-runtime";
import { executePlannedProject, planProjectContext } from "@openagentpack/sdk";
import { formatResourceLabel } from "./_engine/address-utils.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import {
assertProviderConfigured,
buildAgentRuntime,
CREDENTIALS_NOTE,
} from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
const APPLY_FLAGS = {
@@ -22,6 +26,21 @@ 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: {
@@ -45,17 +64,9 @@ export default defineCommand({
"zh-CN": "应用规划的变更,创建、更新或删除 Agent 资源",
},
auth: "apiKey",
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>]",
usageArgs: "[--file <path>] [--provider <name>] [--yes] [--concurrency <n>]",
flags: APPLY_FLAGS,
exampleArgs: ["--yes"],
exampleArgs: ["--yes", "--provider bailian --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
@@ -66,7 +77,7 @@ export default defineCommand({
emitResult(
{
would_apply: {
provider: "bailian",
provider: flags.provider ?? "all",
refresh: !flags.noRefresh,
concurrency: flags.concurrency,
},
@@ -81,8 +92,9 @@ export default defineCommand({
const planned = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, {
provider: "bailian",
provider: flags.provider,
refresh: !flags.noRefresh,
quiet: true,
onFeedback: renderAgentFeedback,
@@ -97,20 +109,11 @@ export default defineCommand({
if (format === "json") process.stderr.write(`${line}\n`);
else emitBare(line);
};
const errorDiagnostics = plan.diagnostics.filter(
(diagnostic) => diagnostic.severity === "error",
);
if (errorDiagnostics.length > 0) {
for (const diagnostic of errorDiagnostics) {
emitProgress(`[error] ${diagnostic.code}: ${diagnostic.message}`);
if (plan.diagnostics.some((diag) => diag.severity === "error")) {
for (const diag of plan.diagnostics) {
if (diag.severity === "error") emitProgress(`[error] ${diag.code}: ${diag.message}`);
}
throw new BailianError(
formatAgentDiagnosticFailure(
errorDiagnostics,
"Cannot apply: resolve the errors above first.",
),
ExitCode.GENERAL,
);
throw new BailianError("Cannot apply: resolve the errors above first.", ExitCode.GENERAL);
}
const actionable = plan.actions.filter((action) => action.action !== "no-op");
@@ -121,11 +124,23 @@ 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, {
@@ -137,8 +152,7 @@ export default defineCommand({
);
const succeeded = result.results.filter((entry) => entry.status === "success").length;
const failedResults = result.results.filter((entry) => entry.status === "failed");
const failed = failedResults.length;
const failed = result.results.filter((entry) => entry.status === "failed").length;
const skipped = result.results.filter((entry) => entry.status === "skipped").length;
if (format === "json") {
@@ -147,9 +161,6 @@ export default defineCommand({
emitBare(`\nApply finished: ${succeeded} succeeded, ${failed} failed, ${skipped} skipped.`);
}
if (failed > 0) {
const firstFailure = failedResults.find((entry) => entry.error);
throw new BailianError(firstFailure?.error ?? "Apply failed.", ExitCode.GENERAL);
}
if (failed > 0) throw new BailianError("Apply failed.", ExitCode.GENERAL);
},
});
@@ -1,71 +0,0 @@
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"}.`);
}
},
});
}
@@ -1,144 +0,0 @@
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;
}
@@ -1,284 +0,0 @@
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);
}
}
@@ -1,38 +0,0 @@
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)}`);
}
},
});
@@ -1,57 +0,0 @@
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.",
});
},
});
@@ -1,3 +0,0 @@
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
export default createSetDeploymentPausedCommand(true);
@@ -1,58 +0,0 @@
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 ?? "-"}`);
},
});
@@ -1,38 +0,0 @@
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)}`);
}
},
});
@@ -1,55 +0,0 @@
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.",
});
},
});
@@ -1,67 +0,0 @@
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.",
});
},
});
@@ -1,3 +0,0 @@
import { createSetDeploymentPausedCommand } from "./_set-paused.ts";
export default createSetDeploymentPausedCommand(false);
@@ -21,6 +21,13 @@ 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: {
@@ -36,15 +43,7 @@ export default defineCommand({
"zh-CN": "销毁 State 中跟踪的全部托管 Agent 资源",
},
auth: "apiKey",
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]",
usageArgs: "[--file <path>] [--yes] [--cascade]",
flags: DESTROY_FLAGS,
exampleArgs: ["--yes", "--yes --cascade"],
notes: CREDENTIALS_NOTE,
@@ -68,16 +67,7 @@ export default defineCommand({
const planned = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
const planned = planDestroyProjectContext(runtime);
return {
...planned,
resources: planned.resources.filter(
(resource) => resource.address.provider === "bailian",
),
defaultMemoryStores: (planned.defaultMemoryStores ?? []).filter(
(memoryStore) => memoryStore.provider === "bailian",
),
};
return planDestroyProjectContext(runtime);
}),
);
@@ -96,6 +86,14 @@ 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, {
@@ -1,41 +0,0 @@
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),
]);
}
@@ -1,141 +0,0 @@
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 并执行定向远端创建",
},
};
}
@@ -1,41 +0,0 @@
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)}`);
},
});
@@ -1,53 +0,0 @@
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.",
});
},
});
@@ -1,68 +0,0 @@
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.",
});
},
});
@@ -1,316 +0,0 @@
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.`);
},
});
@@ -1,6 +1,5 @@
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import {
BailianError,
defineCommand,
@@ -13,21 +12,48 @@ import { emitBare, emitResult } from "bailian-cli-runtime";
const GITIGNORE_ADDITIONS = `
# agents
agents.state.json
.openagentpack/versions/
.env
`;
function buildTemplate(options: { agentName: string }): string {
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]";
return `version: "1"
providers:
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}
${providerBlock}
defaults:
provider: bailian
provider: ${options.provider === "all" ? "all" : options.provider}
environments:
dev:
@@ -39,16 +65,25 @@ environments:
agents:
${options.agentName}:
description: "General-purpose assistant"
model: qwen3.8-max
${modelBlock}
instructions: |
You are a helpful assistant.
environment: dev
tools:
builtin: [bash, read, glob, grep]
builtin: ${toolBlock}
`;
}
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>",
@@ -73,22 +108,23 @@ const INIT_FLAGS = {
export default defineCommand({
description: {
"en-US": "Create an agents.yaml template",
"zh-CN": "创建 agents.yaml 模板",
"en-US": "Create a new agents.yaml template",
"zh-CN": "创建新的 agents.yaml 模板",
},
auth: "none",
usageArgs: "[--agent-name <name>] [--file <path>] [--force]",
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]",
flags: INIT_FLAGS,
exampleArgs: ["", "--agent-name assistant"],
exampleArgs: ["", "--provider bailian --agent-name assistant", "--provider all"],
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";
if (existsSync(file) && !flags.force) {
throw new BailianError(
`${resolve(file)} already exists.`,
`${file} already exists.`,
ExitCode.USAGE,
"Pass --force to overwrite.",
);
@@ -105,7 +141,7 @@ export default defineCommand({
emitResult(
{
would_create: file,
provider: "bailian",
provider,
agent: agentName,
would_update_gitignore: wouldUpdateGitignore,
},
@@ -114,7 +150,7 @@ export default defineCommand({
return;
}
const template = buildTemplate({ agentName });
const template = buildTemplate({ provider, agentName });
await writeFile(file, template, "utf8");
if (existsSync(gitignorePath)) {
@@ -127,12 +163,14 @@ export default defineCommand({
}
if (format === "json") {
emitResult({ created: file, provider: "bailian", agent: agentName }, format);
emitResult({ created: file, provider, agent: agentName }, format);
} else {
emitBare(`Created ${resolve(file)}`);
emitBare(
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
);
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("Next: edit agents.yaml, then run `bl managed-agent plan`.");
}
},
@@ -8,9 +8,13 @@ import {
import { emitBare, emitResult } from "bailian-cli-runtime";
import { planProjectContext } from "@openagentpack/sdk";
import { formatResourceLabel } from "./_engine/address-utils.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import {
assertProviderConfigured,
buildAgentRuntime,
CREDENTIALS_NOTE,
} from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
const PLAN_FLAGS = {
@@ -22,6 +26,14 @@ 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: {
@@ -44,9 +56,9 @@ export default defineCommand({
"zh-CN": "显示将应用到 Agent 基础设施的变更",
},
auth: "apiKey",
usageArgs: "[--file <path>] [--no-refresh] [--refresh-only]",
usageArgs: "[--file <path>] [--provider <name>] [--no-refresh] [--refresh-only]",
flags: PLAN_FLAGS,
exampleArgs: ["", "--no-refresh"],
exampleArgs: ["", "--provider bailian", "--no-refresh"],
notes: [
...CREDENTIALS_NOTE,
{
@@ -71,8 +83,9 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file, {
credentials: offline ? "none" : "all",
});
assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, {
provider: "bailian",
provider: flags.provider,
refresh: !offline,
quiet: format === "json",
onFeedback: format === "json" ? undefined : renderAgentFeedback,
@@ -82,18 +95,17 @@ 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(failureMessage, ExitCode.GENERAL);
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
return;
}
for (const diag of plan.diagnostics) {
emitBare(`[${diag.severity}] ${diag.code}: ${diag.message}`);
}
if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL);
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
const creates = plan.actions.filter((action) => action.action === "create");
const updates = plan.actions.filter((action) => action.action === "update");
@@ -1,468 +0,0 @@
import { stat } from "node:fs/promises";
import { join } from "node:path";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
commitProjectBuild,
createDirectoryWorkspaceVersionService,
executeProjectPublish,
initializeDirectoryProject,
planProjectPublish,
previewProjectBuild,
type ProjectBuildResolver,
resolveDirectoryProjectRoot,
validateDirectoryProject,
} from "@openagentpack/sdk/project-workspace";
import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts";
import { installSdkTransport } from "./_engine/transport.ts";
import { formatResourceLabel } from "./_engine/address-utils.ts";
const PROJECT_FLAG = {
project: {
type: "string",
valueHint: "<directory>",
description: {
"en-US": "Directory project root (default: current directory)",
"zh-CN": "目录项目根路径(默认:当前目录)",
},
},
} satisfies FlagsDef;
const JSON_FORMAT = "json" as const;
export const managedAgentProjectInit = defineCommand({
description: {
"en-US": "Create a directory project or convert the local agents.yaml",
"zh-CN": "创建目录项目,或转换当前 agents.yaml",
},
auth: "none",
usageArgs: "[--project <directory>]",
flags: {
project: {
...PROJECT_FLAG.project,
description: {
"en-US": "Directory project root (default: ./managed-agent under the current directory)",
"zh-CN": "目录项目根路径(默认:当前目录下的 ./managed-agent)",
},
},
},
exampleArgs: ["", "--project ./my-agent", "--project ."],
notes: [
{
"en-US":
"Without --project, creates a managed-agent/ subdirectory. Enter it before running other project commands. Use --project . to initialize in place or convert the current agents.yaml; existing project files are not overwritten.",
"zh-CN":
"不传 --project 时创建 managed-agent/ 子目录;后续项目操作请先进入该目录。使用 --project . 可在当前目录初始化或转换 agents.yaml;不会覆盖已有项目文件。",
},
{
"en-US":
"New projects include Skill, File, Vault, and Environment examples under each resource directory's _examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside _examples/ to enable it, then configure its Agent reference.",
"zh-CN":
"新项目在四类资源目录的 _examples/ 下生成配置示例,不写入 agent.json 引用,也不参与 Build/Publish。需要使用时,将示例复制到 _examples/ 外,再配置 Agent 引用。",
},
],
async run(ctx) {
const projectRoot = ctx.flags.project ?? "./managed-agent";
if (ctx.settings.dryRun) {
emitResult(
{
would_initialize_project: projectRoot,
},
detectOutputFormat(ctx.settings.output),
);
return;
}
const result = await initializeDirectoryProject({ projectRoot });
emitResult(result, detectOutputFormat(ctx.settings.output));
},
});
export const managedAgentProjectValidate = defineCommand({
description: { "en-US": "Validate a directory Agent project", "zh-CN": "校验目录式 Agent 项目" },
auth: "none",
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--project ./my-agent"],
async run(ctx) {
const result = await validateDirectoryProject(ctx.flags.project ?? ".");
const format = detectOutputFormat(ctx.settings.output);
if (format === JSON_FORMAT) emitResult(safeInspection(result), format);
else {
for (const diagnostic of [...result.diagnostics, ...result.warnings]) {
emitBare(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`);
}
if (!result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
emitBare(`Project is valid (${result.project_revision.slice(0, 12)}).`);
}
}
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
throw new BailianError("Directory project validation failed.", ExitCode.GENERAL);
}
},
});
export const managedAgentProjectBuild = defineCommand({
description: {
"en-US": "Organize directory source and generate the immutable Publish Build",
"zh-CN": "整理目录源文件并生成不可变的发布 Build",
},
auth: "none",
notes: [
{
"en-US":
"Build writes local project files without confirmation, including inferred resource associations and migration of plaintext Vault secrets into .env. Use --dry-run to preview without writing. Publish still requires explicit confirmation before remote changes.",
"zh-CN":
"Build 无需确认即可写入本地项目文件,包括推断的资源关联及将 Vault 明文密钥移入 .env。使用 --dry-run 可只预览不写入。Publish 变更远端资源前仍需显式确认。",
},
],
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--dry-run", "--project ./my-agent"],
async validate(flags) {
await withAgentErrors(async () => {
const root = await resolveDirectoryProjectRoot(flags.project ?? ".");
const metadata = await stat(join(root, "project.json")).catch((error: unknown) => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
});
if (!metadata?.isFile()) {
throw new BailianError(
`Not a project root: ${root} (project.json is missing).`,
ExitCode.USAGE,
"Run from the directory containing project.json, or pass --project <directory>.",
);
}
});
return undefined;
},
async run(ctx) {
const root = ctx.flags.project ?? ".";
const preview = await previewProjectBuild(root);
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(safeInspection(preview), format);
return;
}
if (!preview.can_build)
throw new BailianError(
preview.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ??
"Directory project is invalid and cannot be built.",
ExitCode.GENERAL,
);
const built = await commitProjectBuild({
projectRoot: root,
baseRevision: preview.project_revision,
});
emitResult(
{ manifest: built.manifest, organization_moves: preview.organization_moves },
format,
);
},
});
export const managedAgentProjectPublish = defineCommand({
description: {
"en-US": "Publish the current directory-project Build and record a version",
"zh-CN": "发布当前目录项目 Build 并记录版本",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This publishes the current directory-project Build and may create, update, or delete remote managed Agent resources.",
"zh-CN": "该操作会发布当前目录项目 Build,可能创建、更新或删除远端托管 Agent 资源。",
},
},
usageArgs: "[--project <directory>] [--no-refresh] [--concurrency <n>]",
flags: {
...PROJECT_FLAG,
noRefresh: {
type: "switch",
description: {
"en-US": "Skip remote refresh before planning",
"zh-CN": "规划前跳过远端刷新",
},
},
concurrency: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Maximum parallel resource operations",
"zh-CN": "最大并行资源操作数",
},
},
},
exampleArgs: ["--yes", "--project ./my-agent --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
installSdkTransport(ctx);
const root = ctx.flags.project ?? ".";
const resolveBuild: ProjectBuildResolver = async (buildPath, options) =>
(await resolveAgentProjectConfig(ctx, buildPath, {
environment: options?.environment,
overrideBailianBaseUrl: true,
})) as unknown as Awaited<ReturnType<ProjectBuildResolver>>;
const planned = await withAgentErrors(() =>
withStdoutProtected(() =>
planProjectPublish(root, {
provider: "bailian",
refresh: !ctx.flags.noRefresh,
quiet: true,
onFeedback: renderAgentFeedback,
resolveBuild,
}),
),
);
const actions = planned.planned.plan.actions.filter((action) => action.action !== "no-op");
if (ctx.settings.dryRun) {
emitResult(
{
project_revision: planned.project_revision,
build_manifest: planned.build_manifest,
plan: planned.planned.plan,
},
detectOutputFormat(ctx.settings.output),
);
return;
}
for (const action of actions) {
const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitBare(`${marker} ${formatResourceLabel(action.address)}`);
}
const result = await withAgentErrors(() =>
withStdoutProtected(() =>
executeProjectPublish({
projectRoot: planned.project_root,
expectedProjectRevision: planned.project_revision,
expectedYamlHash: planned.build_manifest.yaml_hash,
expectedPlanFingerprint: planned.plan_fingerprint,
provider: "bailian",
refresh: !ctx.flags.noRefresh,
concurrency: ctx.flags.concurrency,
policy: "force",
onFeedback: renderAgentFeedback,
resolveBuild,
}),
),
);
emitResult(result, detectOutputFormat(ctx.settings.output));
},
});
export const managedAgentProjectWorkbench = defineCommand({
description: {
"en-US": "Launch the directory project Workbench",
"zh-CN": "启动目录项目 Workbench",
},
auth: "apiKey",
usageArgs: "[--project <directory>] [--port <n>] [--no-open]",
flags: {
...PROJECT_FLAG,
port: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Local port (default: 4848)", "zh-CN": "本地端口(默认:4848)" },
},
noOpen: {
type: "switch",
description: { "en-US": "Do not open a browser", "zh-CN": "不自动打开浏览器" },
},
},
exampleArgs: ["", "--project ./my-agent --no-open"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const root = ctx.flags.project ?? ".";
if (ctx.settings.dryRun) {
emitResult(
{ would_launch: "workbench", project_root: root, port: ctx.flags.port ?? 4848 },
detectOutputFormat(ctx.settings.output),
);
return;
}
await launchManagedAgentPlayground({
project: root,
port: ctx.flags.port ?? 4848,
open: !ctx.flags.noOpen,
surface: "workbench",
client: ctx.client,
settings: ctx.settings,
});
},
});
export const managedAgentProjectVersionEnable = versionToggleCommand(true);
export const managedAgentProjectVersionDisable = versionToggleCommand(false);
function versionToggleCommand(enabled: boolean) {
return defineCommand({
description: enabled
? { "en-US": "Enable directory project versions", "zh-CN": "启用目录项目版本管理" }
: { "en-US": "Disable directory project versions", "zh-CN": "关闭目录项目版本管理" },
auth: "none",
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--project ./my-agent"],
async run(ctx) {
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
if (ctx.settings.dryRun) {
emitResult(
{ would_set_enabled: enabled, status: await service.status() },
detectOutputFormat(ctx.settings.output),
);
return;
}
const result = enabled
? await service.enable("Enable project versions")
: await service.disable();
emitResult(result, detectOutputFormat(ctx.settings.output));
},
});
}
export const managedAgentProjectVersionStatus = defineCommand({
description: {
"en-US": "Show directory project version status",
"zh-CN": "显示目录项目版本状态",
},
auth: "none",
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--project ./my-agent --output json"],
async run(ctx) {
emitResult(
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").status(),
detectOutputFormat(ctx.settings.output),
);
},
});
export const managedAgentProjectVersionList = defineCommand({
description: { "en-US": "List directory project versions", "zh-CN": "列出目录项目版本" },
auth: "none",
usageArgs: "[--project <directory>] [--limit <n>] [--cursor <cursor>]",
flags: {
...PROJECT_FLAG,
limit: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Maximum versions to return", "zh-CN": "最多返回的版本数" },
},
cursor: {
type: "string",
valueHint: "<cursor>",
description: { "en-US": "Pagination cursor", "zh-CN": "分页游标" },
},
},
exampleArgs: ["", "--limit 20 --output json"],
async run(ctx) {
emitResult(
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").listVersions({
limit: ctx.flags.limit,
cursor: ctx.flags.cursor,
}),
detectOutputFormat(ctx.settings.output),
);
},
});
const VERSION_ID_FLAG = {
versionId: {
type: "string",
valueHint: "<full-version>",
required: true,
description: { "en-US": "Full project version ID", "zh-CN": "完整的项目版本 ID" },
},
} satisfies FlagsDef;
export const managedAgentProjectVersionPreview = defineCommand({
description: { "en-US": "Preview a directory project version", "zh-CN": "预览目录项目历史版本" },
auth: "none",
usageArgs: "--version-id <full-version> [--project <directory>]",
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
exampleArgs: ["--version-id <full-version>"],
async run(ctx) {
emitResult(
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").previewVersion(
ctx.flags.versionId,
),
detectOutputFormat(ctx.settings.output),
);
},
});
export const managedAgentProjectVersionRestore = defineCommand({
description: {
"en-US": "Restore a version to the project working directory",
"zh-CN": "将历史版本恢复到项目工作目录",
},
auth: "none",
risk: {
level: "high",
message: {
"en-US":
"This restores the full directory source to the working tree. Version history and remote State will not move.",
"zh-CN": "该操作会将完整目录源文件恢复到工作区;版本历史和远端 State 不会移动。",
},
},
usageArgs: "--version-id <full-version> [--project <directory>]",
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --yes"],
async run(ctx) {
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
const preview = await service.previewVersion(ctx.flags.versionId);
if (!preview.can_restore)
throw new BailianError(
preview.blockers[0] ?? preview.diagnostics[0]?.message ?? "Version cannot be restored.",
ExitCode.GENERAL,
);
if (ctx.settings.dryRun) {
emitResult(
{ would_restore: ctx.flags.versionId, preview },
detectOutputFormat(ctx.settings.output),
);
return;
}
const restored = await service.restoreVersion(ctx.flags.versionId, {
headVersion: preview.base_head_version,
projectRevision: preview.base_project_revision,
});
emitResult(restored, detectOutputFormat(ctx.settings.output));
},
});
function safeInspection(result: {
project_root: string;
project_revision: string;
source_manifest_hash: string;
yaml_hash: string;
diagnostics: unknown[];
warnings: unknown[];
organization_moves: unknown[];
canonical_yaml: string;
before_yaml?: string;
can_build?: boolean;
}) {
return {
project_root: result.project_root,
project_revision: result.project_revision,
source_manifest_hash: result.source_manifest_hash,
yaml_hash: result.yaml_hash,
diagnostics: result.diagnostics,
warnings: result.warnings,
organization_moves: result.organization_moves,
before_yaml: "before_yaml" in result ? result.before_yaml : undefined,
after_yaml: result.canonical_yaml,
can_build: "can_build" in result ? result.can_build : undefined,
};
}
@@ -49,6 +49,14 @@ 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({
@@ -75,7 +83,7 @@ export default defineCommand({
{
would_create_session: {
agent: flags.agent ?? "auto",
provider: "bailian",
provider: flags.provider ?? "auto",
environment: flags.environment,
vault: flags.vault,
memory_stores: parseMemoryStores(flags.memoryStores),
@@ -93,7 +101,7 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file);
return createSessionForAgent(runtime, {
agent: flags.agent,
provider: "bailian",
provider: flags.provider,
environment: flags.environment,
vault: flags.vault,
memoryStores: parseMemoryStores(flags.memoryStores),
@@ -1,348 +0,0 @@
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,21 +20,19 @@ 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",
risk: {
level: "high",
message: {
"en-US": "This deletes the specified remote managed Agent Session.",
"zh-CN": "该操作会删除指定的远端托管 Agent Session。",
},
},
usageArgs: "--session-id <id> [--file <path>]",
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
flags: SESSION_DELETE_FLAGS,
exampleArgs: ["--session-id sess_abc123 --yes"],
exampleArgs: ["--session-id sess_abc123"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
@@ -45,7 +43,7 @@ export default defineCommand({
emitResult(
{
would_delete_session: flags.sessionId,
provider: "bailian",
provider: flags.provider ?? "auto",
config_file: file,
},
format,
@@ -56,7 +54,7 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
await deleteSession(runtime, flags.sessionId, "bailian");
await deleteSession(runtime, flags.sessionId, flags.provider);
}),
);
@@ -1,260 +0,0 @@
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,2 +1,98 @@
// Backward-compatible alias for `managed-agent session event list`.
export { managedAgentSessionEventList as default } from "./session-event.ts";
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.");
},
});
@@ -20,12 +20,17 @@ 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> [--file <path>]",
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
flags: SESSION_GET_FLAGS,
exampleArgs: ["--session-id sess_abc123"],
notes: CREDENTIALS_NOTE,
@@ -37,7 +42,7 @@ export default defineCommand({
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return getSession(runtime, flags.sessionId, "bailian");
return getSession(runtime, flags.sessionId, flags.provider);
}),
);

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