Compare commits

...

70 Commits

Author SHA1 Message Date
zeyu.fz 75699966d4 Merge remote-tracking branch 'origin/main' into feat/memory 2026-09-10 20:43:23 +08:00
Gong Shiqi 2220f932b5 Merge pull request #198 from modelstudioai/codex/release-1.23.0
chore(release): prepare 1.23.0
2026-09-10 17:37:40 +08:00
zeyu.fz d9134d6f0a feat(memory): 增加长度校验及限流提示并强化安全确认
- 在多条命令中统一添加对 userId 和 memoryLibraryId 长度的校验
- 新增 profile schema 名称、描述及属性字段长度断言
- 为 delete 内存节点命令添加高风险删除确认机制及 --yes 标志
- 增加账号级别限流说明,提示遇 HTTP 429 需降低请求频率
- 统一移除分散的长度常量,集中定义并使用共享常量
- 相关 E2E 测试覆盖长度限制和高风险操作确认情况
- 支持内存消息内容多样化,新增 MemoryContentPart 类型定义
- 内存文档中新增限流描述及删除命令风险提示,强调确认操作要求
2026-09-10 17:31:42 +08:00
若麒 5c390fad74 chore(release): prepare 1.23.0 2026-09-10 16:57:58 +08:00
Gong Shiqi 6218637f7c Merge pull request #194 from modelstudioai/feat/global-watermark-config
Feat/global watermark config
2026-09-10 16:49:33 +08:00
若麒 0c7ba721a7 Merge remote-tracking branch 'origin/main' into feat/global-watermark-config 2026-09-10 15:39:12 +08:00
Gong Shiqi 0dbf071368 Merge pull request #197 from modelstudioai/feat/speech-vocabulary
Add ASR instant hotwords, context, and vocabulary CRUD
2026-09-10 15:11:35 +08:00
zeyu.fz 75a7100702 feat(memory): 完善记忆及画像相关命令支持并统一参数
- 在 CLI 文档中新增记忆 live 场景说明与变量需求
- 扩展 CLI 命令集,添加记忆画像 Profile 的增删改查功能
- 统一所有记忆相关命令加入 workspace 和 library 参数支持
- 记忆添加命令支持自定义 meta_data,完善输入校验和互斥说明
- 记忆删除、列表、获取用户画像等命令增加工作空间参数支持
- 画像模板相关命令新增分页参数及属性操作接口
- 重构知识命令共享模块,移除重复的 workspace 解析实现,统一导出
- 优化命令输出格式,文本模式下显示更多详细信息和异常提示
- 提供完整的示例参数,增强命令使用说明和提示文档
2026-09-10 14:40:39 +08:00
clh02467605 16e30686f0 fix(speech): validate --words JSON before auth
Invalid vocabulary JSON was parsed in run(), so CI without an API key failed with AUTH(3) instead of USAGE(2).
2026-09-10 14:28:16 +08:00
clh02467605 7803d91e3f chore: fixed Formatting issues 2026-09-10 14:07:37 +08:00
clh02467605 c2d17707c0 feat(speech): add instant hotwords, context, and vocabulary CRUD 2026-09-10 14:02:05 +08:00
rendianmeng 1dfb4400cc Merge branch 'main' of github.com:modelstudioai/cli into feat/global-watermark-config 2026-09-10 10:33:23 +08:00
rendianmeng fd2077b373 feat: pipeline watermark 处理完善 2026-09-09 17:40:49 +08:00
chenanran555 2090293f85 Merge pull request #192 from modelstudioai/feat/managed-agent-ci
fix(managed-agent): improve project initialization and build validation
2026-09-08 19:39:03 +08:00
chenanran555 a245359792 chore(release): prepare 1.22.0 2026-09-08 19:26:17 +08:00
chenanran555 19bd5a5ea8 test(managed-agent): remove obsolete build confirmation flag 2026-09-08 19:01:47 +08:00
chenanran555 e262f2e574 chore(deps): bump OpenAgentPack SDK to 0.7.1 2026-09-08 18:01:41 +08:00
chenanran555 180aac9f28 fix(managed-agent): show absolute YAML paths during init 2026-09-08 16:57:15 +08:00
若麒 3477a08dad feat(config): add profile-level watermark setting 2026-09-08 16:33:40 +08:00
chenanran555 aa1484264e fix(managed-agent): improve project initialization and build validation 2026-09-08 14:46:56 +08:00
chenanran555 2767491ce6 Merge pull request #182 from modelstudioai/feat/managed-agent-ci
Feat/managed agent ci 添加managed agents模块的ci能力
2026-09-07 21:42:35 +08:00
chenanran555 efb832815a chore(release): prepare 1.21.0 2026-09-07 21:11:51 +08:00
chenanran555 902516bbad fix(managed-agent): check latest Playground version before launch
Resolve the requested npm version before reusing local builds or running processes, fetch mismatched versions on demand, and preserve explicit binary overrides. Add launcher regression tests and sync the SDK 0.7.0 lockfile.
2026-09-07 20:46:07 +08:00
chenanran555 f44fb2e95f refactor(managed-agent): use SDK project service subpaths
Replace standalone project packages with @openagentpack/sdk 0.7.0 and update project/version imports.

The pnpm lockfile remains pending until SDK 0.7.0 is published to npm; this integration has been validated against the local SDK build.
2026-09-07 19:58:47 +08:00
chenanran555 c6b9931d6f feat(managed-agent): integrate directory resource scaffolds and safe builds 2026-09-04 17:20:50 +08:00
chenanran555 a92c58420b fix(managed-agent): use CLI credentials for directory projects
Make project init fully offline, stop generating project-local credential files, bind Publish to the reviewed plan fingerprint, remove the unsafe Apply refresh-only mode, and document Agent-owned File mounts.
2026-09-04 11:22:11 +08:00
chenanran555 bcf68a31f3 Merge remote-tracking branch 'origin/main' into feat/managed-agent-ci
# Conflicts:
#	packages/commands/package.json
#	packages/commands/src/commands/managed-agent/apply.ts
#	packages/commands/src/commands/managed-agent/init.ts
#	packages/commands/tests/e2e/managed-agent.e2e.test.ts
#	skills/bailian-managed-agent/SKILL.md
#	skills/bailian-managed-agent/reference/index.md
#	skills/bailian-managed-agent/reference/managed-agent.md
2026-09-03 11:38:44 +08:00
chenanran555 8e5dd127a9 Merge pull request #177 from modelstudioai/feat/cma-command
Feat/cma command 补齐managed agent cli能力
2026-09-03 11:00:47 +08:00
chenanran555 4f41eaabc2 chore(release): prepare 1.20.0 2026-09-03 10:47:23 +08:00
chenanran555 e1634c40ed chore(deps): upgrade @openagentpack/sdk to 0.5.0 2026-09-02 22:26:33 +08:00
chenanran555 4c5688ad4a feat(managed-agent)!: restrict CLI to Bailian provider
Remove multi-provider CLI flags and reject non-Bailian provider configs while preserving SDK support.
2026-09-02 19:23:40 +08:00
chenanran555 176961dc56 feat(managed-agent): hide capabilities command from public CLI 2026-09-02 15:33:50 +08:00
Gong Shiqi 888031562f Merge pull request #189 from modelstudioai/docs/console-gateway-backend-attribution
docs(telemetry): update Console Gateway backend attribution
2026-09-02 15:18:51 +08:00
若麒 d017954ae8 docs(telemetry): update Console Gateway backend attribution 2026-09-02 15:11:24 +08:00
chenanran555 9d19558645 feat(managed-agent): enforce high-risk confirmation for runtime operations 2026-09-02 15:07:22 +08:00
chenanran555 a9200a3f85 Merge remote-tracking branch 'origin/main' into feat/cma-command
# Conflicts:
#	packages/commands/src/commands/managed-agent/session-delete.ts
#	packages/commands/tests/e2e/managed-agent.e2e.test.ts
#	skills/bailian-managed-agent/SKILL.md
#	skills/bailian-managed-agent/reference/managed-agent.md
2026-09-02 14:51:49 +08:00
chenanran555 83d7982ecb feat(managed-agent): support creating agents with remote and local skills 2026-09-02 14:36:22 +08:00
Gong Shiqi d33cb71cc7 Merge pull request #180 from modelstudioai/feat/runtime-risk-confirmation
feat(cli): add unified confirmation for high-risk operations
2026-09-02 14:31:18 +08:00
若麒 737f8db359 chore(release): prepare 1.19.0 2026-09-01 20:21:21 +08:00
若麒 a402026cce Merge remote-tracking branch 'origin/main' into feat/runtime-risk-confirmation 2026-09-01 19:49:04 +08:00
若麒 da6e1313e5 feat(commands): migrate delete commands to runtime risk confirmation 2026-09-01 19:25:50 +08:00
若麒 2d2ec1c628 Merge remote-tracking branch 'origin/feat/quota-delete-split' into feat/runtime-risk-confirmation 2026-09-01 19:16:40 +08:00
Gong Shiqi a78ed7ffe8 Merge pull request #187 from modelstudioai/release/1.18.2
chore(release): prepare 1.18.2
2026-09-01 19:02:29 +08:00
若麒 5a1dfa5227 chore(release): prepare 1.18.2 2026-09-01 18:56:28 +08:00
duck-bubi-ya b5dec926b7 Merge pull request #185 from modelstudioai/fix/skill-init-issue
fix(skill): retry registry fetches and tolerate blocked backup cleanup
2026-09-01 17:37:22 +08:00
故璃 fd96fd664c feat(quota): split --delete into a dedicated quota delete command
The destructive --delete flag sat on "quota update", but the --yes
high-risk confirmation guards command paths rather than individual flags.
Promote it to its own "quota delete" command with a --yes guardrail, and
reduce "quota update" to QPM/TPM updates only.
2026-09-01 17:35:46 +08:00
若麒 719c0b68bb Merge remote-tracking branch 'origin/main' into feat/runtime-risk-confirmation 2026-09-01 17:06:41 +08:00
故璃 9e6f5ca9cb fix(skill): retry registry fetches and tolerate blocked backup cleanup
- bl skill init: raise index timeout 10s→30s and retry transient network
  failures (3 attempts); advisor sync silent channel stays fail-fast
- make post-swap backup deletion best-effort so host safe-delete guards
  cannot fail a completed install (postinstall.js mirror included)
- add unit coverage for guard-blocked cleanup and registry retry policy
2026-09-01 16:37:12 +08:00
gujieye 3f78de2dc1 Merge pull request #183 from modelstudioai/feat/confirm-destructive-ops
feat: require --yes confirmation for destructive operations
2026-08-31 19:47:17 +08:00
故璃 38926aeb22 feat: require --yes confirmation for destructive operations
- finetune/deploy/dataset delete: add --yes flag + confirmDangerousAction
- quota update --delete: confirm before clearing rate limits
- non-TTY runs without --yes fail with a USAGE error
- add e2e coverage and regenerate skill references
2026-08-31 19:26:31 +08:00
chenanran555 e53daf05eb feat(managed-agent): add directory project workflow 2026-08-31 15:55:10 +08:00
chenanran555 67ae182ec3 feat(managed-agent): require confirmation for removals 2026-08-31 11:06:43 +08:00
Gong Shiqi e3f2996291 Merge pull request #181 from modelstudioai/perf/registry-smoke-inprocess
perf(test): reduce CLI E2E startup overhead
2026-08-31 10:59:51 +08:00
若麒 59250c4f44 perf(test): streamline command e2e execution 2026-08-28 19:14:06 +08:00
若麒 676b6c2ece test(runtime): update auth fallback fixture for confirmation context 2026-08-28 17:48:07 +08:00
若麒 4f59ca5118 Merge branch 'main' into feat/runtime-risk-confirmation 2026-08-28 17:42:57 +08:00
若麒 0872ff6a20 feat(cli): expose high-risk confirmation guidance in help and skills 2026-08-28 17:19:24 +08:00
若麒 98ebeb398c perf(test): run registry smoke help checks in process 2026-08-28 16:54:55 +08:00
Gong Shiqi 5c96077d9f Merge pull request #179 from modelstudioai/feat/cancel-key-auth
chore(release): prepare 1.18.1
2026-08-28 15:50:42 +08:00
若麒 8a7674e7c3 chore(release): prepare 1.18.1 2026-08-28 15:37:15 +08:00
若麒 afb547e0c8 refactor(commands): migrate remaining confirmations to runtime gate 2026-08-28 14:35:17 +08:00
chenanran555 449eede237 fix(managed-agent): upgrade OpenAgentPack SDK to 0.4.0 beta 2026-08-27 20:47:51 +08:00
若麒 8906108744 refactor(knowledge): migrate high-risk commands to runtime confirmation 2026-08-27 19:42:51 +08:00
chenanran555 2fbb6efe67 Merge remote-tracking branch 'origin/main' into feat/cma-command 2026-08-27 17:27:55 +08:00
chenanran555 47e9bcc634 feat(managed-agent): add scoped resource create commands 2026-08-27 17:01:27 +08:00
若麒 00bcee36a6 feat(runtime): add unified confirmation gate for high-risk commands 2026-08-27 15:54:23 +08:00
chenanran555 4c7ef0c7a5 feat(managed-agent): add scoped agent create command
Add `bl managed-agent agent create` with automatic YAML key generation,
atomic config updates, preview-by-default behavior, and create-only scoped
apply that ignores unrelated resource drift.
2026-08-27 11:44:00 +08:00
chenanran555 9469556671 refactor(managed-agent)!: replace Git versioning with local snapshots
Replace @openagentpack/local-git with @openagentpack/project-versions so
the CLI and Workbench share a Git-independent, lock-protected YAML
snapshot store.

- create versions only after a successful Apply
- migrate version commands to local version IDs
- remove Git project scaffolding and CI-specific Apply policy
- update tests and managed-agent skill references

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

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