Compare commits

...

99 Commits

Author SHA1 Message Date
lisheng.lisheng 46b55b29ac chore(release): prepare 1.25.0 2026-09-14 19:07:20 +08:00
ls f953f428c5 Merge pull request #201 from VIISEVEN27/feat/tokenplan-harness
feat(token-plan): 新增 Token Plan harness 权益额度用量命令
2026-09-14 11:22:10 +08:00
wangmingxiang e03103364e feat(token-plan): 新增 Token Plan harness 权益额度用量命令
- 新增 token-plan harness-quota 命令用于显示 Harness 权益额度使用情况
- 实现权限基于 Console 认证,支持通过 --type 参数筛选 harness 类型
- 集成权益与 Harness 列表数据,展示已发放与待发放额度明细
- 支持标准输出和 JSON 格式输出展示额度及使用比例
- 添加单元测试覆盖多种输出及边界情况
- 更新 CLI 命令注册及相关文档和 Help 信息
- 添加 E2E 测试验证新命令行为及请求参数传递正确性
2026-09-14 10:04:31 +08:00
chenanran555 64b1e72238 Merge pull request #193 from modelstudioai/feat/sandbox
Feat/sandbox
2026-09-11 15:49:02 +08:00
chenanran555 fa3f5de728 chore(release): prepare 1.24.0 2026-09-11 14:55:08 +08:00
chenanran555 02d859ee18 Merge origin/main into feat/sandbox 2026-09-11 11:14:11 +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
若麒 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
chenanran555 fa1a8c7b84 docs(sandbox): add connection workflows and official references to skill 2026-09-10 16:07:41 +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
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
chenanran555 3e8df956fe fix(sandbox): retain submitted build IDs on polling failure 2026-09-10 11:21: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 ceef514a50 feat(sandbox): add template file upload with sandbox_template source 2026-09-09 17:35:23 +08:00
chenanran555 0dde03c783 Merge remote-tracking branch 'origin/main' into feat/sandbox 2026-09-09 16:53:57 +08:00
chenanran555 f4e479ce80 feat(sandbox): add official image presets and template image selection 2026-09-09 16:45:37 +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 e1eadad184 Merge remote-tracking branch 'origin/main' into feat/sandbox 2026-09-08 14:23:30 +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 c0fca869c2 fix(sandbox): support shared --base-url configuration with workspace fallback 2026-09-07 16:37:56 +08:00
chenanran555 332efb05b8 feat(sandbox): add Bailian Sandbox lifecycle commands 2026-09-07 15:11:51 +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
Gong Shiqi 4fdd8389f8 Merge pull request #178 from modelstudioai/feat/cancel-key-auth
feat: cancel validate API key
2026-08-28 14:29:45 +08:00
rendianmeng bd8b08b617 feat: cancel validate API key 2026-08-28 13:10:56 +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
Gong Shiqi e9479e4084 Merge pull request #176 from modelstudioai/feat/profile-capability-fallback
feat(auth): add Profile API key capability fallback
2026-08-27 11:45:03 +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
若麒 17ffde4ac2 chore(release): prepare 1.18.0 2026-08-27 11:33:50 +08:00
若麒 9600f06205 Merge remote-tracking branch 'origin/main' into feat/profile-capability-fallback
# Conflicts:
#	skills/bailian-gen/reference/index.md
2026-08-26 19:27:01 +08:00
若麒 fc3ac36d00 Merge branch 'main' into feat/profile-capability-fallback 2026-08-26 18:54:01 +08:00
Gong Shiqi fe11227789 Merge pull request #174 from modelstudioai/feat/update-def-video-model
Switch default video model to wan3.0-video
2026-08-26 18:00:38 +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
若麒 804a43f8ed docs(skill): use profile default model when unspecified 2026-08-26 17:55:44 +08:00
若麒 d343dc3265 fix(auth): only apply profile presets on login 2026-08-26 17:10:00 +08:00
clh02467605 0d24f46f73 fix(e2e): unbind wan3.0/--file dry-run tests from token-plan 2026-08-26 17:09:27 +08:00
Gong Shiqi cc209ce0b1 Merge pull request #173 from modelstudioai/feat/update-install.md
Clarify install guide skill fallback and post-install tips
2026-08-26 16:43:53 +08:00
clh02467605 8360a9f584 feat(e2e): add tests for video generation with --file option and mutual exclusivity checks 2026-08-26 15:48:08 +08:00
clh02467605 90d1ef9dc5 fix: format fix 2026-08-26 15:30:05 +08:00
clh02467605 124c425133 chore: 优化一键安装引导脚本中node版本兼容问题 2026-08-26 15:25:51 +08:00
若麒 670c15532b feat(auth): expand Token Plan capabilities and speech defaults 2026-08-26 13:42:27 +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
clh02467605 18fd2ed8e5 fix: fixed format 2026-08-24 18:38:02 +08:00
clh02467605 4c51c95f31 feat(video): switch default video model to wan3.0-video and adapt All-in-One support
- Update default models for video generate/ref and config UI to wan3.0-video
- Add --file file-to-video support for wan3.0; route kf2v through media[] instead of image2video
- Emit reference voices as standalone reference_audio entries on wan3.0 ref
- Sync e2e/stress tests and bailian-gen reference docs
2026-08-24 18:00:52 +08:00
clh02467605 f29f5fe632 feat: update install guide 2026-08-24 14:01:50 +08:00
325 changed files with 23737 additions and 2291 deletions
+2
View File
@@ -15,6 +15,8 @@ git add \
skills/bailian-finetune/reference \
skills/bailian-managed-agent/SKILL.md \
skills/bailian-managed-agent/reference \
skills/bailian-sandbox/SKILL.md \
skills/bailian-sandbox/reference \
skills/bailian-web-search/SKILL.md
vp staged
+1 -1
View File
@@ -36,7 +36,7 @@ packages/core/src/auth/ # apiKey / console credential 解析与落盘
packages/core/src/client/ # HTTP client / endpoints / console gateway
```
Skill / 命令手册随 `skills/bailian-*/``bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts`**`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts``packages/cli/package.json` 同步各 `skills/*/SKILL.md``metadata.version`。两者由根脚本 `pnpm run sync:skill-assets``.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
Skill / 命令手册随 `skills/bailian-*/``bl skill init` 安装(装齐 registry 中全部 `bailian-*`,含共享协议 `bailian-protocol`)。业务 skill`bailian-cli` / `bailian-gen` / `bailian-finetune` / `bailian-managed-agent` / `bailian-sandbox` / `bailian-web-search`)执行前读 `skills/bailian-protocol/`;不要依赖 frontmatter `companions`(安装器不强制)。`tools/generate-reference.ts`**`packages/cli/src/commands.ts`** 按一级命令归属表分流写入各 `skills/<skill>/reference/`(纳入 git);`tools/sync-skill-metadata.ts``packages/cli/package.json` 同步各 `skills/*/SKILL.md``metadata.version`。两者由根脚本 `pnpm run sync:skill-assets``.vite-hooks/pre-commit` 执行。hub `bailian-cli` 的路由表不复述领域命令明细SKILL 文案 / 安装约定 / hand-off 见 [docs/agents/skill-change.md](docs/agents/skill-change.md)。
约定:
+126
View File
@@ -6,6 +6,132 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [1.25.0] - 2026-09-14
### Added
- **Token Plan harness quota** — `bl token-plan harness-quota` shows Token Plan harness entitlement quota usage (Console auth), joining the harness list with issued entitlements to display used/total quota, usage ratio, and reset time; harnesses with a pending entitlement are listed as issuing.
- Filter the harness list with `--type official_tool|infrastructure`; render as a quota box or `--output json`.
## [1.24.0] - 2026-09-11
### Added
- Sandbox instance and template lifecycle commands, with automatic build polling and `--async` submission.
- `bl sandbox official-images` and template `--image` presets for Code Interpreter, Browser, and All-in-One.
- `bl sandbox file upload` uploads template mount files with `source=sandbox_template`; use the returned File ID in `mntConfig`.
- Sandbox supports shared `--base-url`, environment and Profile configuration, with workspace-based endpoint fallback.
- Dedicated `bailian-sandbox` Skill with command references and instance connection guidance.
### Fixed
- Preserve submitted `templateID` and `buildID` when template-build polling fails, so users can check the existing build before resubmitting.
### Security
- Sandbox REST calls use Bailian Bearer authentication without an E2B SDK or E2B API key. Connection credentials and dry-run environment values are redacted by default.
## [1.23.0] - 2026-09-10
### Added
- **Profile-level watermark control** — configure `watermark` with `bl config set --key watermark --value true|false` to control the default watermark behavior for image generation and editing, video generation and editing, and reference-to-video commands.
- **ASR accuracy controls** — `bl speech recognize` now supports instant hot words with `--vocabulary`, contextual word enhancement with `--context`, and reusable pre-built vocabularies with `--vocabulary-id` for supported ASR models.
- **Speech vocabulary management** — added `bl speech vocabulary create|list|get|update|delete` to manage reusable pre-built hot-word vocabularies.
## [1.22.0] - 2026-09-08
### Changed
- **Project initialization** — `managed-agent project init` now creates `./managed-agent` by default. Use `--project .` to initialize in place. **(BREAKING)**
- **Build confirmation** — `managed-agent project build` no longer requires confirmation and rejects `--yes`. Use `--dry-run` for a read-only preview; Publish still requires confirmation. **(BREAKING)**
- **Managed Agent SDK** — upgrade to `0.7.1`. Build automatically associates active Agent-local resources while preserving explicit bindings, Skill versions, and File mount paths. Ambiguous Environment or Vault selections are rejected before writing.
### Fixed
- **Project diagnostics** — provide actionable project-root guidance and surface the underlying Build validation error.
- **YAML initialization paths** — show the absolute YAML path in creation messages and existing-file errors.
### Internal
- Expand project initialization and Build regression coverage, and remove the obsolete Build confirmation flag from the local lifecycle E2E test.
## [1.21.0] - 2026-09-07
### Added
- **Managed Agent directory projects** — initialize, validate, build, and publish local projects with Agent-scoped resource configuration and resource examples.
- **Local project versions** — enable or disable snapshot versioning, list and preview history, and restore project files without Git.
- **Project Workbench** — edit resources, review changes, publish updates, and manage local versions in a browser.
### Changed
- **Playground version resolution** — check npm before launch, reuse matching local versions, and fetch the latest version when needed; retain explicit version and binary overrides.
- **Managed Agent SDK** — upgrade to `0.7.0` and consume project workspace and version services through SDK subpath exports.
## [1.20.0] - 2026-09-03
> Managed Agents now combines YAML-first infrastructure management with direct Bailian AgentStudio resource and runtime operations.
### Added
- **Managed Agent API commands** — added direct list, get, search, version, upload, download, run, pause, archive, event, and diagnostic operations for Agents, Environments, Skills, Vaults, Deployments, Sessions, and Files.
- **Scoped YAML-backed resource creation** — `agent create`, `environment create`, `skill create`, `vault create`, `vault credential create`, and `deployment create` update `agents.yaml` and apply only the target resource without unrelated drift blocking the operation.
- **Agent Skill attachment** — Agent creation supports existing custom or official Skill IDs as well as local Skill directories and ZIP archives.
### Changed
- **Bailian-only Managed Agent CLI** — `bl managed-agent` now targets the Bailian provider exclusively; provider-selection flags were removed and configurations containing other providers are rejected.
- **Runtime mutation confirmation** — Deployment run/pause/unpause, Session archive/delete, and File delete operations require explicit high-risk confirmation.
### Fixed
- **Managed Agent error reporting** — Apply and scoped-create failures preserve the underlying provider diagnostic instead of ending with only `Apply failed.`.
### Security
- Credentials are resolved in memory and removed from the process environment; Vault credential declarations reference environment variables without persisting plaintext secrets.
## [1.19.0] - 2026-09-01
### Added
- **`bl quota delete`** — clears all custom QPM/TPM rate limits for a model.
### Changed
- **High-risk operation confirmation** — high-risk commands show risk details in `--help` and Skill command references. Without `--yes`, the high-risk operation is not executed; JSON output returns exit code `7` with `error.type: "requires_confirmation"`. After confirmation, re-run with `--yes`; `--dry-run` does not require confirmation.
## [1.18.2] - 2026-09-01
### Changed
- **Confirmation before deleting or clearing resources** — `bl finetune delete`, `bl deploy delete`, `bl dataset delete`, and `bl quota update --delete` now ask for confirmation; pass `--yes` for non-interactive use.
### Fixed
- **Skill installation reliability** — `bl skill init` now retries transient network failures, and completed Skill updates are no longer reported as failed when backup cleanup is blocked.
## [1.18.1] - 2026-08-28
### Removed
- Removed API Key validation from `bl auth login`.
## [1.18.0] - 2026-08-27
### Added
- **Automatic Profile API Key fallback** — unsupported commands use the API Key and endpoint from `default` without requiring a Profile switch; explicit credentials still take priority.
- **Independent speech defaults** — Profiles support separate default models for TTS and ASR.
- **Wan3.0 file-to-video** — `bl video generate --file` accepts local files or URLs and validates incompatible inputs.
### Changed
- **Wan3.0 is now the default video model** — regular API Key text, image, and reference video tasks default to `wan3.0-video`, with first/last-frame and reference-audio support. Token Plan defaults remain unchanged.
- **Expanded Token Plan preset** — login adds speech defaults and missing capabilities while preserving existing user configuration.
- **Improved speech and Skill installation experience** — streaming synthesis defaults to PCM; installation docs add a Node 18-compatible fallback and clarify the Node.js and Git prerequisites.
## [1.17.1] - 2026-08-22
### Fixed
+126
View File
@@ -6,6 +6,132 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [1.25.0] - 2026-09-14
### 新增
- **Token Plan harness 权益额度** —— `bl token-plan harness-quota` 查看 Token Plan harness 权益额度用量Console 认证),联合 harness 列表与已发放权益,展示已用/总额度、使用比例和重置时间;待发放权益的 harness 以「发放中」状态列出。
- 通过 `--type official_tool|infrastructure` 筛选 harness 列表;支持额度框输出或 `--output json`
## [1.24.0] - 2026-09-11
### 新增
- Sandbox 实例与模版生命周期命令,支持自动轮询构建状态和 `--async` 异步提交。
- `bl sandbox official-images` 与模版 `--image` 参数,内置代码解释器、浏览器和全能型镜像预设。
- `bl sandbox file upload` 使用 `source=sandbox_template` 上传模版挂载文件,返回的 File ID 可用于 `mntConfig`
- Sandbox 复用 `--base-url`、环境变量和 Profile 配置,未配置时通过工作空间拼接接入地址。
- 独立的 `bailian-sandbox` Skill提供命令参考和实例连接指南。
### 修复
- 模版构建轮询失败时保留已提交的 `templateID``buildID`,便于先查询已有构建,避免重复提交。
### 安全
- Sandbox REST 调用使用百炼 Bearer 鉴权,不依赖 E2B SDK 或 E2B API Key连接凭据和 dry-run 环境变量默认脱敏。
## [1.23.0] - 2026-09-10
### 新增
- **Profile 级水印控制** —— 可通过 `bl config set --key watermark --value true|false` 设置图片生成与编辑、视频生成与编辑以及参考生视频命令的默认水印行为。
- **ASR 准确率增强** —— `bl speech recognize` 现支持通过 `--vocabulary` 传入即时热词、通过 `--context` 增强上下文词表,以及通过 `--vocabulary-id` 使用适用于对应 ASR 模型的预编译热词表。
- **语音热词表管理** —— 新增 `bl speech vocabulary create|list|get|update|delete`,用于管理可复用的预编译热词表。
## [1.22.0] - 2026-09-08
### 变更
- **项目初始化** —— `managed-agent project init` 默认创建 `./managed-agent` 子目录;如需原地初始化,请使用 `--project .`。**(BREAKING)**
- **Build 确认机制** —— `managed-agent project build` 无需确认,并且不再接受 `--yes`。使用 `--dry-run` 可只读预览Publish 仍需显式确认。**(BREAKING)**
- **Managed Agent SDK** —— 升级至 `0.7.1`。Build 自动关联 Agent 目录下已启用的资源保留显式引用、Skill 版本和 File 挂载路径Environment 或 Vault 选择存在歧义时,在写入前报错。
### 修复
- **项目诊断** —— 提供可操作的项目根目录提示,并展示 Build 校验失败的具体原因。
- **YAML 初始化路径** —— 创建成功及文件已存在的错误信息均展示 YAML 绝对路径。
### 内部
- 补充项目初始化和 Build 回归覆盖,移除本地闭环 E2E 测试中过时的 Build 确认参数。
## [1.21.0] - 2026-09-07
### 新增
- **Managed Agent 目录项目** —— 支持初始化、校验、构建和发布本地项目,按 Agent 组织资源配置,并提供资源示例。
- **本地项目版本管理** —— 无需 Git 即可启停快照版本管理、查看和预览历史,以及恢复项目文件。
- **项目 Workbench** —— 在浏览器中编辑资源、审阅变更、发布更新和管理本地版本。
### 变更
- **Playground 版本选择** —— 启动前查询 npm复用版本一致的本地安装否则按需下载最新版保留显式版本和启动文件覆盖。
- **Managed Agent SDK** —— 升级至 `0.7.0`,通过 SDK 子路径复用项目目录和版本服务。
## [1.20.0] - 2026-09-03
> Managed Agent 现在同时提供 YAML-first 基础设施管理与百炼 AgentStudio 资源、运行时 API 操作。
### 新增
- **Managed Agent API 命令** —— 新增 Agent、Environment、Skill、Vault、Deployment、Session 和 File 的列表、详情、搜索、版本、上传、下载、运行、暂停、归档、事件及诊断等操作。
- **基于 YAML 的单资源创建** —— `agent create``environment create``skill create``vault create``vault credential create``deployment create` 会更新 `agents.yaml`,并且只 Apply 目标资源,不受无关资源 Drift 阻塞。
- **Agent Skill 挂载** —— 创建 Agent 时支持引用已有的自定义或官方 Skill ID也支持本地 Skill 目录和 ZIP 文件。
### 变更
- **Managed Agent CLI 限定为百炼 Provider** —— `bl managed-agent` 现在只面向百炼,移除 Provider 选择参数,并拒绝包含其他 Provider 的配置。
- **运行时变更增加确认** —— Deployment 运行/暂停/恢复、Session 归档/删除以及 File 删除操作需要显式进行高风险确认。
### 修复
- **Managed Agent 错误输出** —— Apply 和单资源创建失败时会保留底层 Provider 的具体诊断,不再只显示 `Apply failed.`
### 安全
- 凭证仅在内存中解析并从进程环境清除Vault Credential 声明通过环境变量引用 Secret不会持久化明文。
## [1.19.0] - 2026-09-01
### 新增
- **`bl quota delete`** — 清除指定模型的全部自定义 QPM/TPM 限流配置。
### 变更
- **高风险操作确认** — 高风险命令会在 `--help` 和 Skill 命令参考中展示风险说明。未传入 `--yes`高风险操作不会执行JSON 输出会返回退出码 `7``error.type: "requires_confirmation"`。确认后可添加 `--yes` 重新执行;`--dry-run` 无需确认。
## [1.18.2] - 2026-09-01
### 变更
- **删除与清除操作增加确认** —— `bl finetune delete``bl deploy delete``bl dataset delete``bl quota update --delete` 现在会在执行前要求确认;非交互场景请传入 `--yes`
### 修复
- **Skill 安装可靠性** —— `bl skill init` 现在会重试临时性网络故障;备份清理受阻时,已完成的 Skill 更新不再被误报为失败。
## [1.18.1] - 2026-08-28
### 已移除
- 移除 `bl auth login` 的 API Key 校验。
## [1.18.0] - 2026-08-27
### 新增
- **Profile API Key 自动回退** —— 当前 Profile 不支持某条命令时,自动使用 `default` 中的 API Key 和接入地址,无需手动切换 Profile显式凭证仍然优先。
- **语音合成与识别支持独立默认模型** —— Profile 支持分别配置 TTS 和 ASR 默认模型。
- **Wan3.0 文件生视频** —— `bl video generate --file` 支持本地文件或 URL并校验互斥输入。
### 变更
- **视频默认模型升级至 Wan3.0** —— 普通 API Key 的文生、图生和参考生视频默认使用 `wan3.0-video`并支持首尾帧与参考音频Token Plan 默认模型保持不变。
- **扩展 Token Plan 预设** —— 登录后自动补充语音默认模型和缺失能力,同时保留用户已有配置。
- **优化语音与 Skill 安装体验** —— 流式语音合成默认输出 PCMSkill 安装文档补充兼容 Node 18 的降级方式及 Node.js、Git 前置条件。
## [1.17.1] - 2026-08-22
### 修复
+48 -16
View File
@@ -13,22 +13,25 @@ npm install -g bailian-cli
```
4. 校验:`bl --version`
5. 按下方「安装 skills」安装。
安装 skillsCLI 内置,无需 Git / npx skills
### 安装 skills
**Supported优先** 用 CLI 内置命令一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`
```bash
bl skill init
```
**Supported** `bl skill init` 一次装齐 registry 中全部 `bailian-*`(含共享协议 `bailian-protocol`)。
**Advanced / 按需子集:**
**降级** `bl skill init` 失败,先确认本机同时满足两个条件:`node -v` / `npm -v` 成功,**且** `git --version` 成功。两个条件都成立时,再回退到:
```bash
bl skill add --name bailian-protocol,bailian-gen
npx skills@1.5.18 add modelstudioai/cli --all -g
```
安装成功后,用中文简要说明已安装的 skills 及用户可做什么。
> 必须使用 `skills@1.5.18`:这是最后兼容 Node ≥18 的版本(与 Bailian CLI 的 Node ≥18.17.0 一致);
安装成功后,按第 5 节向用户说明语言切换与能力 tip若也装了 skills一并简要说明
## 2. 备选:二进制安装(无需 Node
@@ -67,8 +70,6 @@ bl --version
which bl # Windows: where.exe bl
```
若自动 skill 安装失败,再手动执行:`bl skill init`
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node回退到上方 npm 安装。
---
@@ -99,12 +100,43 @@ which bl # Windows: where.exe bl
bl auth status --output json
```
## 5. 常见问题
## 5. 安装成功后向用户说明(必做)
| 现象 | 可能原因 | 建议动作 |
| ------------------------ | ---------------------------- | ------------------------------------------------ |
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin``npm prefix -g` |
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node或改用 npm 版 CLI |
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
安装与校验完成后,**主动**用中文向用户说明下列两项;不要等用户追问。可执行 `bl`(已登录时)对照终端 tip但以下列表为准勿臆造。
### 5.1 界面语言
Bailian CLI 支持中英文界面help / 欢迎语 / Quick Start 主次文案)。默认 `en-US`,可切换为 `zh-CN`
```bash
bl config set --key language --value zh-CN
bl config set --key language --value en-US
```
向用户说明:可随时用上述命令切换语言。
### 5.2 能力 tipQuick Start
向用户展示「试试使用Bailian CLI完成这些任务」及下列 5 条(中英各一行,与 `bl` 根帮助 tip 一致):
1. 帮我创建一个能够生成短片分镜和视频的 Managed Agent。
Help me create a Managed Agent that can generate short-film storyboards and videos.
2. 生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。
Generate an image of a cat in a spacesuit standing on Mars, then turn it into a video.
3. 查看最近的模型用量、免费额度和限流情况。
Check my recent model usage, free quota, and rate limits.
4. 推荐一个适合图片理解和智能客服的模型。
Recommend a model suitable for image understanding and intelligent customer service.
5. 介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。
Explain what Bailian CLI can help me accomplish, and recommend how to use it based on my needs.
## 6. 常见问题
| 现象 | 可能原因 | 建议动作 |
| ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `bl: command not found` | bin 不在 PATH | 检查 `~/.local/bin``npm prefix -g` |
| curl 安装 404 | GitHub Release 资产未上传 | 改用 `npm install -g bailian-cli` |
| Windows `bl update` 失败 | 旧布局 / 文件锁 / 网络 | 重跑 `irm .../install.ps1 \| iex` 迁移布局后重试 |
| `plugin` 需要 npm | 二进制安装无本机 npm | 安装 Node或改用 npm 版 CLI |
| 安装报错 engines | Node 版本过低(仅 npm 路径) | 升级到 ≥ 18.17.0 |
| `bl skill init` 失败 | 网络 / registry 不可达等 | 有 Node 且有 Git 时降级 `npx skills@1.5.18 add modelstudioai/cli --all -g`;否则重试或告知用户 |
+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 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |
+14 -8
View File
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
`~/.bailian/config.json` 可同时保存 `api_key``access_token``access_key_*`。登录任一种方式不得删除另一种:
- `bl auth login --api-key ...` 更新 `api_key` / `base_url`
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址并把该预设的默认模型物化写入。API Key 落盘成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi``console` 域凭证
- `bl auth logout --console` 只清 `access_token`
@@ -44,8 +44,12 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
- `resolveApiKey()``auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin仅保留协议、host 和显式端口,去除 path、query、fragment
- `--config` 只选择 config 文件 block不提升该 block 的字段优先级;内置套餐 Profile当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile未传
- `--config` 只选择 config 文件 block不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
- `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profilepreset 只在 API Key 登录落盘成功后物化写入,升级 preset 需要重新登录
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败warning 与多行 `error` 对象以空行分隔stdout 仍只保留命令结果
- 显式 `auth login --config <name>` 在凭证落盘成功后自动激活目标 Profile未传
`--config` 时继续写当前激活项,失败和 dry-run 不切换
- `resolveConsole()``auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
- `resolveOpenApi()``auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
@@ -58,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 -17
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 / 真实集成
@@ -74,11 +80,18 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
## 用例类型
1. **--help**`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
1. **--help**`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
2. **缺参**:带无害全局 flag`--quiet`)且不传 required flag → `exitCode === 2`
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
4. **真实集成**:放在 skip 块**末尾**
高风险命令额外要求:
- `--help` 展示 runtime 注入的 `--yes`
-`--yes` 返回 exit code 7 和 JSON `type: "requires_confirmation"`
- `--dry-run` 无需 `--yes`,且必须证明在任何远端请求或本地写入之前返回
- runtime 的离线 high-risk fixture 必须覆盖带 `--yes` 确实进入 `run()`,并断言 `yes` 不进入 command 自有 flags
## Journey 层(用户旅程全链路)
- **定位**:命令 E2E 验单命令契约journey 验“用户带着目标跨命令走通回路”,结构性断言不在 journey 重复
@@ -94,8 +107,8 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
## 安全与例外
- **禁止破坏真实用户配置**`auth logout` 默认只用 `--dry-run`需要验证实际落盘时,必须通过
`BAILIAN_CONFIG_DIR` 指向隔离 fixture`config set` 只用 `--dry-run`
- **禁止破坏真实用户配置**`auth logout``config set` 默认只用 `--dry-run`只有验证持久化契约时,才允许通过
`BAILIAN_CONFIG_DIR` 指向每个用例独占的临时目录实际落盘,并必须在 `finally` 中清理;禁止写入或复用真实 `~/.bailian`
- **不加 dry-run**`dryRun``resolveFileUrl` / `resolveCredential` / 上传**之后**的命令(如 `image edit``speech recognize``--url`
- **`--list-voices` 等旁路**:先于 `--text` 校验的 flag缺参用例勿带该 flag
- 新增 required option → 至少一条缺参用例;改 dry-run 输出 → 更新对应断言
@@ -105,7 +118,7 @@ describe.skipIf(<ready>)("e2e: <topic>DashScope …)", () => {
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
- [ ] 至少一条真实集成(或说明为何仅 smoke不破坏已有集成用例顺序
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
+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
@@ -32,6 +32,8 @@
- [ ] Command Pack 不能覆盖内置命令、其他 pack 命令或重声明保留 flag。
- [ ] 普通网络请求走 `ctx.client`;基础 Context 提供 `identity/settings/flags/client/output/errors`,不提供原始凭据。
- [ ] `ctx.credentials.apiKey()` 仅限 policy 显式声明 `credentialAccess: ["apiKey"]`,且命令自身为 `auth: "apiKey"`
- [ ] API Key capability ID 由 host 按实际叶子命令路径生成(例如 `agent credential``agent.credential`Command Pack 不声明额外元数据Profile allowlist 对 pack 命令同样 fail closed。
- [ ] `ctx.credentials.apiKey()` 必须返回 `authStage` 已注入 `ctx.client` 的实际凭证,不能重新读取原 Profile 绕过 capability fallback。
- [ ] 不向 Command Pack 暴露原始 Console Token、OpenAPI AK/SK、`authStore``configStore`
- [ ] 不向 Command Pack 暴露宿主的 `commandPacks` manager避免 pack 安装或删除其他 pack。
- [ ] 单包失败必须 fail-open保留内置命令和其他合法 pack。
@@ -40,6 +42,7 @@
## 测试与文档
- [ ] `packages/runtime/tests/command-packs.test.ts` 覆盖产品 policy、安装目录隔离、协议版本、前缀和导出契约。
- [ ] Command Pack 测试覆盖 capability 格式/鉴权域、adapter 保留字段,以及 raw API Key 委托使用 fallback 后的实际 Base URL。
- [ ] `packages/cli/tests/e2e/command-packs.e2e.test.ts` 覆盖 help、link、执行、output/errors、凭据授权、list、remove。
- [ ] `packages/kscli/tests/e2e/command-packs.e2e.test.ts` 覆盖统一 host 和 runtime 默认空 policy 下不暴露管理命令。
- [ ] fixture 的包名必须在测试白名单内,且构建入口不依赖工作区运行时解析。
+4
View File
@@ -29,6 +29,7 @@ config 文件 ─┘
- `ConfigFile`(disk 形状,snake_case)加新字段(如果允许写文件)
- `parseConfigFile()` 解析新字段
- 如果是 enum 字段,加校验
- 如果是数组字段,明确“缺失 / 空数组 / 非法值”的不同语义;安全策略字段的非法值必须 fail closed
### B. 加载逻辑
@@ -51,6 +52,7 @@ config 文件 ─┘
- [ ] 配置展示 / 修改命令同步:
- `packages/commands/src/commands/config/show.ts` 显示新字段
- `packages/commands/src/commands/config/set.ts``VALID_KEYS` / `KEY_ALIASES` / description 允许 set
- `packages/commands/src/commands/config/ui.ts` / `ui-html.ts` 能按原类型往返数组字段,不能把 `[]` 保存成字段缺失
### E. 文档
@@ -61,6 +63,7 @@ config 文件 ─┘
- [ ] 单测覆盖优先级:flag > env > file
- [ ] 校验失败抛错(非法值)
- [ ] 默认值正确
- [ ] 数组配置覆盖 CLI 逗号/JSON 输入、Config UI 往返、去重和显式空数组
## 完成后自查
@@ -82,3 +85,4 @@ cat ~/.bailian/config.json
- ✗ 全局 switch 没标 `type: "switch"`,被当成需要值的 `--xxx <value>`
- ✗ 加了 env var 但 README 表格没更新,用户不知道有这条
-`config show` 不显示新字段,用户改了无法回查
- ✗ UI 用 `String([])` 把显式空数组渲染为空串,保存后意外关闭安全策略
+11 -2
View File
@@ -18,15 +18,19 @@
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
`auth login --config ...`,凭证落盘成功后自动激活该 Profile。
- 激活状态只选择配置 block不改变字段优先级字段仍为 flag > env > selected config > 默认值。
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset升级内置 Plan Profile 的 preset 需要重新登录。
- 对命中内置 preset 的 ProfileAPI Key 登录落盘成功后会把当前 preset 中缺少的 capability 追加落盘同时保留已有项且不做删除Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get``video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
## 3. 保持读写命令交互一致
- `auth login``config set` 等写命令未传 `--config` 时修改当前激活项。
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证实际落盘时
创建和激活;`config set --config <name>` 可创建但不自动激活。
- `config show``auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
@@ -55,6 +59,8 @@
- 旧配置无 `active_config` -> `default`
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
- 任意名称 Profile 的叶子路由 capability 命中时使用自身 API Key未命中或空白名单时使用 `default` API Key`--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一覆盖时跳过 fallback。
- 旧内置 Plan Profile 缺 capability 字段时不启用 fallback重新登录后使用并持久化当前 preset显式配置`[]`)按文件值生效。
- 显式命名 `--config``--config default` 均覆盖激活项且不修改磁盘状态。
- 激活不存在的 Profile 失败且不写盘。
- 悬空 `active_config` 明确失败。
@@ -63,6 +69,9 @@
- 显式 `auth login --config <name>` 成功后激活该 Profile失败或 dry-run 不创建、不切换;
`--config default` 成功后切回 `default`
- Console token 自动刷新不从其他 Profile 借用 AK/SK也不把新 token 写入其他 Profile。
- Console/OpenAPI/none 命令不参与 API Key capability fallbackfallback 后的 Client 特殊端点行为必须跟随最终解析的 `base_url`,不能根据原 `settings.configName` 推断端点类型。
- Fallback 反馈只描述 CLI 能权威确认的本地行为:当前 Profile 不支持空格分隔的用户可见叶子命令,本次将从 `default` 读取 API Key 配置;不得声称整个 Profile 已切换,也不得展示 capability ID 或 `<undeclared>` 等内部值。存在 `--api-key` / `--base-url``DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式覆盖时必须跳过 fallback 且不输出反馈。
- Fallback 反馈写 stderr`--quiet` 抑制text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象。dry-run 和后续鉴权失败仍保留反馈JSON 模式下多个 diagnostics 以空行分隔,任何模式都不得输出凭证值。
- `config list/show/use/ui``auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
- `config ui` 覆盖保存时保留顶层元数据(如 `active_config`),继续允许空值清除字段,并覆盖 `console_*`/`telemetry` 的类型归一化与枚举校验。
- Assets:`listAssets` 覆盖分类归类、时间倒序、目录缺失返回空;`resolveAssetPath` 覆盖目录穿越拦截;`contentType` 覆盖常见扩展名映射。
+1
View File
@@ -46,6 +46,7 @@ runtime/error-handler.ts:
- 内部 AUTH/USAGE/NETWORK/TIMEOUT → 走 enhanceHint(只 AUTH 还有增强)
- TypeError("fetch failed") → 读 err.cause.code 翻成 NETWORK
- Node fs errno → 翻成 GENERAL + errno hint
- `--output json` → stderr 输出两空格缩进的多行 error JSON前面已有结构化 warning 时,两个 diagnostics 以空行分隔
- 其它 Error → 默认走 cause 链
process.exit(err.exitCode)
+4
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」指向句
@@ -54,7 +56,9 @@ bailian-gen bailian-finetune bailian-managed-agent bailian-web-search
### C. 归属与生成
- [ ] 新一级命令组归属领域时:改 `tools/generate-reference.ts``GROUP_OWNER_SKILL`,并更新**拥有方** skill 的路由表hub 最多加一行 hand-off
- [ ] 新增领域目录时:补齐 `SKILL.md`(含 `metadata.version`),同步 `packages/cli/package.json` 的 reference 格式化路径、`.vite-hooks/pre-commit` 的生成物暂存清单、`tools/release/check.mjs` 的生成物校验清单;同步 hub 与共享协议的领域路由
- [ ]`pnpm run sync:skill-assets`(或 commit 走 pre-commit提交生成的 `reference/` 与 version 同步结果
- [ ] 高风险命令生成的 reference 必须包含 `Risk` / `Risk message` 和简短 Agent safety 提示;带 `--yes` 的示例必须标注只能在确认后执行,不要手改生成物
- [ ] 默认模型若写在领域路由表(如 `bailian-gen`):与命令 default / [model-add-remove.md](model-add-remove.md) 一并核对
## 完成后自查
+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 — 批量更新文档标签
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.17.1",
"version": "1.25.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
@@ -41,7 +41,7 @@
"registry": "https://registry.npmjs.org/"
},
"scripts": {
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference'",
"generate:reference": "tsx ../../tools/generate-reference.ts && sh -c 'cd ../.. && vp check --fix skills/bailian-cli/reference skills/bailian-gen/reference skills/bailian-finetune/reference skills/bailian-managed-agent/reference skills/bailian-sandbox/reference'",
"sync:skill-version": "tsx ../../tools/sync-skill-metadata.ts",
"build": "vp pack",
"dev": "tsx src/main.ts",
+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;
}
+153
View File
@@ -70,6 +70,11 @@ import {
searchWeb,
speechSynthesize,
speechRecognize,
speechVocabularyCreate,
speechVocabularyList,
speechVocabularyGet,
speechVocabularyUpdate,
speechVocabularyDelete,
fileUpload,
consoleCall,
usageFree,
@@ -85,6 +90,7 @@ import {
workspaceList,
quotaList,
quotaUpdate,
quotaDelete,
quotaHistory,
quotaCheck,
permissionList,
@@ -124,6 +130,7 @@ import {
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
tokenPlanHarnessQuota,
workspaceInit,
pluginInstall,
pluginLink,
@@ -139,6 +146,18 @@ import {
managedAgentPlan,
managedAgentApply,
managedAgentDestroy,
managedAgentPlayground,
managedAgentProjectInit,
managedAgentProjectValidate,
managedAgentProjectBuild,
managedAgentProjectPublish,
managedAgentProjectWorkbench,
managedAgentProjectVersionEnable,
managedAgentProjectVersionDisable,
managedAgentProjectVersionStatus,
managedAgentProjectVersionList,
managedAgentProjectVersionPreview,
managedAgentProjectVersionRestore,
managedAgentStateList,
managedAgentStateShow,
managedAgentStateRm,
@@ -151,6 +170,63 @@ import {
managedAgentSessionSend,
managedAgentSessionEvents,
managedAgentSkillList,
managedAgentAgentCreate,
managedAgentEnvironmentCreate,
managedAgentSkillCreate,
managedAgentVaultCreate,
managedAgentVaultCredentialCreate,
managedAgentDeploymentCreate,
managedAgentAgentList,
managedAgentAgentGet,
managedAgentAgentSearch,
managedAgentAgentVersions,
managedAgentEnvironmentList,
managedAgentEnvironmentGet,
managedAgentEnvironmentSearch,
managedAgentSkillGet,
managedAgentSkillSearch,
managedAgentSkillVersions,
managedAgentSkillDownload,
managedAgentVaultList,
managedAgentVaultGet,
managedAgentVaultSearch,
managedAgentDeploymentList,
managedAgentDeploymentGet,
managedAgentDeploymentSearch,
managedAgentDeploymentRunsList,
managedAgentDeploymentRunsGet,
managedAgentDeploymentRun,
managedAgentDeploymentPause,
managedAgentDeploymentUnpause,
managedAgentSessionSearch,
managedAgentSessionUpdate,
managedAgentSessionArchive,
managedAgentSessionEventList,
managedAgentSessionEventSend,
managedAgentSessionEventStream,
managedAgentSessionDebug,
managedAgentSessionExport,
managedAgentFileUpload,
managedAgentFileList,
managedAgentFileGet,
managedAgentFileSearch,
managedAgentFileDownload,
managedAgentFileDelete,
sandboxCreate,
sandboxOfficialImages,
sandboxFileUpload,
sandboxList,
sandboxGet,
sandboxConnect,
sandboxPause,
sandboxResume,
sandboxDelete,
sandboxTemplateCreate,
sandboxTemplateList,
sandboxTemplateGet,
sandboxTemplateUpdate,
sandboxTemplateBuildStatus,
sandboxTemplateDelete,
} from "bailian-cli-commands";
// Full bailian-cli product: every command, exposed under the `bl` binary.
@@ -231,6 +307,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 +327,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,
@@ -285,6 +367,7 @@ export const commands: Record<string, AnyCommand> = {
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
"token-plan add-member": tokenPlanAddMember,
"token-plan harness-quota": tokenPlanHarnessQuota,
"workspace init": workspaceInit,
"plugin install": pluginInstall,
"plugin link": pluginLink,
@@ -300,6 +383,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 +407,64 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent session send": managedAgentSessionSend,
"managed-agent session events": managedAgentSessionEvents,
"managed-agent skill-list": managedAgentSkillList,
"managed-agent agent create": managedAgentAgentCreate,
"managed-agent agent list": managedAgentAgentList,
"managed-agent agent get": managedAgentAgentGet,
"managed-agent agent search": managedAgentAgentSearch,
"managed-agent agent versions": managedAgentAgentVersions,
"managed-agent environment create": managedAgentEnvironmentCreate,
"managed-agent environment list": managedAgentEnvironmentList,
"managed-agent environment get": managedAgentEnvironmentGet,
"managed-agent environment search": managedAgentEnvironmentSearch,
"managed-agent skill create": managedAgentSkillCreate,
"managed-agent skill list": managedAgentSkillList,
"managed-agent skill get": managedAgentSkillGet,
"managed-agent skill search": managedAgentSkillSearch,
"managed-agent skill versions": managedAgentSkillVersions,
"managed-agent skill download": managedAgentSkillDownload,
"managed-agent vault create": managedAgentVaultCreate,
"managed-agent vault credential create": managedAgentVaultCredentialCreate,
"managed-agent vault list": managedAgentVaultList,
"managed-agent vault get": managedAgentVaultGet,
"managed-agent vault search": managedAgentVaultSearch,
"managed-agent deployment create": managedAgentDeploymentCreate,
"managed-agent deployment list": managedAgentDeploymentList,
"managed-agent deployment get": managedAgentDeploymentGet,
"managed-agent deployment search": managedAgentDeploymentSearch,
"managed-agent deployment runs list": managedAgentDeploymentRunsList,
"managed-agent deployment runs get": managedAgentDeploymentRunsGet,
"managed-agent deployment run": managedAgentDeploymentRun,
"managed-agent deployment pause": managedAgentDeploymentPause,
"managed-agent deployment unpause": managedAgentDeploymentUnpause,
"managed-agent session search": managedAgentSessionSearch,
"managed-agent session update": managedAgentSessionUpdate,
"managed-agent session archive": managedAgentSessionArchive,
"managed-agent session event list": managedAgentSessionEventList,
"managed-agent session event send": managedAgentSessionEventSend,
"managed-agent session event stream": managedAgentSessionEventStream,
"managed-agent session debug": managedAgentSessionDebug,
"managed-agent session export": managedAgentSessionExport,
"managed-agent file upload": managedAgentFileUpload,
"managed-agent file list": managedAgentFileList,
"managed-agent file get": managedAgentFileGet,
"managed-agent file search": managedAgentFileSearch,
"managed-agent file download": managedAgentFileDownload,
"managed-agent file delete": managedAgentFileDelete,
"sandbox create": sandboxCreate,
"sandbox official-images": sandboxOfficialImages,
"sandbox file upload": sandboxFileUpload,
"sandbox list": sandboxList,
"sandbox get": sandboxGet,
"sandbox connect": sandboxConnect,
"sandbox pause": sandboxPause,
"sandbox resume": sandboxResume,
"sandbox delete": sandboxDelete,
"sandbox template create": sandboxTemplateCreate,
"sandbox template list": sandboxTemplateList,
"sandbox template get": sandboxTemplateGet,
"sandbox template update": sandboxTemplateUpdate,
"sandbox template build-status": sandboxTemplateBuildStatus,
"sandbox template delete": sandboxTemplateDelete,
};
/**
@@ -1,4 +1,4 @@
import { mkdtemp, rm } from "node:fs/promises";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -9,7 +9,19 @@ const fixtureRoot = join(fileURLToPath(import.meta.url), "..", "..", "fixtures",
let configDir: string;
function env(): NodeJS.ProcessEnv {
return { BAILIAN_CONFIG_DIR: configDir, DO_NOT_TRACK: "1" };
return {
BAILIAN_CONFIG_DIR: configDir,
DO_NOT_TRACK: "1",
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
};
}
function parseStderrJsonDiagnostics(stderr: string): unknown[] {
return stderr
.trim()
.split(/\n\s*\n/)
.map((diagnostic) => JSON.parse(diagnostic) as unknown);
}
describe("e2e: Command Pack", () => {
@@ -53,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",
@@ -71,6 +84,68 @@ describe("e2e: Command Pack", () => {
expect(executed.exitCode, executed.stderr).toBe(0);
expect(executed.stdout).toContain("command-pack:hello");
await writeFile(
join(configDir, "config.json"),
JSON.stringify(
{
base_url: "https://default.example.com",
active_config: "company-plan",
"company-plan": {
api_key: "sk-plan",
base_url: "https://plan.example.com",
api_key_capabilities: [],
},
},
null,
2,
) + "\n",
);
const fallbackFailure = await runCli(["agent", "credential", "--output", "json"], env());
expect(fallbackFailure.exitCode).toBe(3);
expect(fallbackFailure.stderr).toMatch(/^\{\n {2}"warning": \{/);
expect(fallbackFailure.stderr).toContain("\n\n{\n");
expect(parseStderrJsonDiagnostics(fallbackFailure.stderr)).toEqual([
{
warning: {
code: "PROFILE_API_KEY_FALLBACK",
message:
'Profile "company-plan" does not support command "agent credential"; API Key settings will be read from Profile "default" for this run.',
profile: "company-plan",
command_path: ["agent", "credential"],
fallback_profile: "default",
credential_fields: ["api_key", "base_url"],
},
},
{
error: expect.objectContaining({
code: 3,
message: "No API key found.",
}),
},
]);
await writeFile(
join(configDir, "config.json"),
JSON.stringify(
{
api_key: "sk-default",
base_url: "https://default.example.com",
active_config: "company-plan",
"company-plan": {
api_key: "sk-plan",
base_url: "https://plan.example.com",
api_key_capabilities: [],
},
},
null,
2,
) + "\n",
);
const fallbackCredential = await runCli(["agent", "credential"], env());
expect(fallbackCredential.exitCode, fallbackCredential.stderr).toBe(0);
expect(fallbackCredential.stderr).toContain('command "agent credential"');
expect(fallbackCredential.stdout).toContain("credential-base-url:https://default.example.com");
const credential = await runCli(["agent", "credential", "--api-key", "fixture-key"], env());
expect(credential.exitCode, credential.stderr).toBe(0);
expect(credential.stdout).toContain("credential-source:flag");
@@ -96,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);
@@ -109,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]`,
);
});
});
+24 -1
View File
@@ -19,12 +19,34 @@ 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",
async run(ctx) {
const apiKey = ctx.credentials.apiKey();
process.stdout.write(`credential-source:${apiKey.source}\n`);
process.stdout.write(
`credential-source:${apiKey.source} credential-base-url:${apiKey.baseUrl}\n`,
);
},
};
@@ -55,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());
});
@@ -0,0 +1,53 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { validateSkillDir } from "bailian-cli-core";
import { expect, test } from "vite-plus/test";
import { parse } from "yaml";
import { commands } from "../src/commands.ts";
const repositoryRoot = join(dirname(fileURLToPath(import.meta.url)), "../../..");
const sandboxSkillDirectory = join(repositoryRoot, "skills/bailian-sandbox");
test("Sandbox is a valid standalone skill aligned with the CLI version", () => {
const metadata = validateSkillDir(sandboxSkillDirectory, "bailian-sandbox");
expect(metadata.name).toBe("bailian-sandbox");
const skillMarkdown = readFileSync(join(sandboxSkillDirectory, "SKILL.md"), "utf8");
const frontmatter = parse(skillMarkdown.split(/^---\s*$/m)[1]) as {
metadata: { version: string };
};
const cliPackage = JSON.parse(
readFileSync(join(repositoryRoot, "packages/cli/package.json"), "utf8"),
) as { version: string };
expect(frontmatter.metadata.version).toBe(cliPackage.version);
for (const link of skillMarkdown.matchAll(/\]\(([^)]+)\)/g)) {
expect(existsSync(join(sandboxSkillDirectory, link[1]))).toBe(true);
}
});
test("Sandbox reference contains every registered Sandbox command only in its owning skill", () => {
const expectedCommands = Object.keys(commands)
.filter((commandPath) => commandPath.startsWith("sandbox "))
.map((commandPath) => `bl ${commandPath}`)
.sort();
expect(expectedCommands.length).toBeGreaterThan(0);
const reference = readFileSync(join(sandboxSkillDirectory, "reference/sandbox.md"), "utf8");
const documentedCommands = [...reference.matchAll(/^### `([^`]+)`/gm)]
.map((match) => match[1])
.sort();
expect(documentedCommands).toEqual(expectedCommands);
const index = readFileSync(join(sandboxSkillDirectory, "reference/index.md"), "utf8");
for (const commandPath of expectedCommands) {
expect(index).toContain(`\`${commandPath}\``);
}
expect(existsSync(join(repositoryRoot, "skills/bailian-cli/reference/sandbox.md"))).toBe(false);
const hubIndex = readFileSync(
join(repositoryRoot, "skills/bailian-cli/reference/index.md"),
"utf8",
);
expect(hubIndex).not.toContain("`bl sandbox ");
});
+1 -1
View File
@@ -178,7 +178,7 @@ export async function ensurePrerequisites(ctx) {
"video",
"generate",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"压测前置短视频:海浪与静态远景,无明显人物。",
"--duration",
@@ -130,7 +130,7 @@ export async function generateCombinedFixtures({ suiteRoot, cliPackage }) {
"video",
"generate",
"--model",
"happyhorse-1.1-t2v",
"wan3.0-video",
"--prompt",
"压测前置短视频:海浪与静态远景,无明显人物。",
"--duration",
@@ -16,7 +16,7 @@ const motions = [
export const runStress = defineStressTarget({
canonical: "video-i2v",
defaultModel: "happyhorse-1.1-i2v",
defaultModel: "wan3.0-video",
batchDirPrefix: "video-i2v-batch",
helpText: "pnpm run test:stress -- video-i2v [--reuse-fixtures] -- --count 5 -c 2",
@@ -16,7 +16,7 @@ const prompts = [
export const runStress = defineStressTarget({
canonical: "video-ref",
defaultModel: "happyhorse-1.1-r2v",
defaultModel: "wan3.0-video",
batchDirPrefix: "video-ref-batch",
helpText: "pnpm run test:stress -- video-ref [--reuse-fixtures] -- --count 5 -c 2",
@@ -45,7 +45,7 @@ const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
export const runStress = defineStressTarget({
canonical: "video-t2v",
defaultModel: "happyhorse-1.1-t2v",
defaultModel: "wan3.0-video",
batchDirPrefix: "video-t2v-batch",
helpText: `用法pnpm run test:stress -- video-t2v -- --concurrency 1 --count 3
详见 docs/agents/stress-batch-tests.md`,
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.17.1",
"version": "1.25.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:",
@@ -1,91 +1,35 @@
import {
BailianError,
ExitCode,
chatPath,
requestJson,
normalizeModelBaseUrl,
type AuthPersistPatch,
type AuthStore,
type Identity,
type Settings,
} from "bailian-cli-core";
import { normalizeModelBaseUrl, type AuthPersistPatch, type AuthStore } from "bailian-cli-core";
interface ApiKeyLoginDeps {
identity: Identity;
settings: Settings;
authStore: AuthStore;
}
interface ApiKeyLoginProfile {
baseUrl: string;
persistBaseUrl?: string;
defaultTextModel?: string;
defaultVideoModel?: string;
defaultImageToVideoModel?: string;
defaultReferenceToVideoModel?: string;
defaultImageModel?: string;
defaultSpeechModel?: string;
defaultSpeechRecognitionModel?: string;
apiKeyCapabilities?: readonly string[];
persistPatch?: AuthPersistPatch;
}
const RETRY_DELAY_BASE_MS = 500;
function canRetry(error: unknown): boolean {
if (error instanceof BailianError) {
if (error.exitCode === ExitCode.NETWORK || error.exitCode === ExitCode.TIMEOUT) return true;
const status = error.api?.httpStatus;
return status === 401 || (status !== undefined && status >= 500);
}
if (error instanceof Error) {
return (
error.name === "AbortError" ||
error.name === "TimeoutError" ||
error.message.includes("timed out") ||
error.message === "fetch failed"
);
}
return false;
}
export async function validateAndPersistApiKey(
/**
* Persist an API key (and optional profile defaults) without a live model probe.
* Login is credential storage; connectivity is verified on the first API command.
* A former chat/completions smoke test conflated quota/model-access 403s with bad keys.
*/
export async function persistApiKey(
deps: ApiKeyLoginDeps,
key: string,
profile: ApiKeyLoginProfile,
): Promise<void> {
process.stderr.write("Testing key... ");
const httpDeps = { identity: deps.identity, settings: deps.settings };
const baseUrl = normalizeModelBaseUrl(profile.baseUrl);
const persistBaseUrl = profile.persistBaseUrl
? normalizeModelBaseUrl(profile.persistBaseUrl)
: undefined;
const validationModel = "qwen3.8-max";
const requestOpts = {
url: baseUrl + chatPath(),
method: "POST",
headers: { Authorization: `Bearer ${key}` },
timeout: Math.min(deps.settings.timeout, 30),
body: {
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
},
};
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(httpDeps, requestOpts);
break;
} catch (error) {
if (attempt >= 3 || !canRetry(error)) {
process.stderr.write("Failed\n");
throw error;
}
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
process.stderr.write("Valid\n");
await deps.authStore.login({
...profile.persistPatch,
api_key: key,
@@ -95,5 +39,8 @@ export async function validateAndPersistApiKey(
default_image_to_video_model: profile.defaultImageToVideoModel,
default_reference_to_video_model: profile.defaultReferenceToVideoModel,
default_image_model: profile.defaultImageModel,
default_speech_model: profile.defaultSpeechModel,
default_speech_recognition_model: profile.defaultSpeechRecognitionModel,
api_key_capabilities: profile.apiKeyCapabilities ? [...profile.apiKeyCapabilities] : undefined,
});
}
@@ -11,7 +11,7 @@ import {
type Settings,
} from "bailian-cli-core";
import { listenLocalServer, openInBrowser } from "../shared/local-server.ts";
import { validateAndPersistApiKey } from "./login-api-key.ts";
import { persistApiKey } from "./login-api-key.ts";
/** 登录流程的能力面:身份(UA)、有效配置(timeout 等)、auth 域落盘。 */
export interface LoginDeps {
@@ -413,9 +413,7 @@ export async function runConsoleLogin(
workspace_id: workspaceId || undefined,
};
if (apiKey) {
const testBaseUrl = baseUrl || deps.authStore.resolveBaseUrl();
await validateAndPersistApiKey(deps, apiKey, {
baseUrl: testBaseUrl,
await persistApiKey(deps, apiKey, {
persistBaseUrl: baseUrl || undefined,
persistPatch: callbackPatch,
});
+14 -8
View File
@@ -5,7 +5,7 @@ import {
normalizeModelBaseUrl,
} from "bailian-cli-core";
import { emitBare } from "bailian-cli-runtime";
import { validateAndPersistApiKey } from "./login-api-key.ts";
import { persistApiKey } from "./login-api-key.ts";
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
const LOGIN_MODE_HINT = "Choose exactly one login mode: --api-key, --console, or --open-api";
@@ -33,8 +33,8 @@ export default defineCommand({
type: "string",
valueHint: "<url>",
description: {
"en-US": "Model API base URL (used with --api-key for validation)",
"zh-CN": "模型 API Base URL(用于配合 --api-key 进行验证)",
"en-US": "Model API base URL to store with --api-key",
"zh-CN": "与 --api-key 一并保存的模型 API Base URL",
},
},
console: {
@@ -164,21 +164,27 @@ export default defineCommand({
if (!key) return;
if (settings.dryRun) {
emitBare("Would validate and save API key.");
emitBare("Would save API key.");
return;
}
const profilePreset = getModelProfilePreset(settings.configName);
const storedBaseUrl = store.stored().baseUrl;
const resolvedBaseUrl = baseUrl || store.resolveBaseUrl(profilePreset?.baseUrl);
const stored = store.stored();
const storedBaseUrl = stored.baseUrl;
const persistBaseUrl = baseUrl || (!storedBaseUrl ? profilePreset?.baseUrl : undefined);
await validateAndPersistApiKey(deps, key, {
baseUrl: resolvedBaseUrl,
const apiKeyCapabilities = profilePreset
? [...new Set([...(stored.apiKeyCapabilities ?? []), ...profilePreset.apiKeyCapabilities])]
: stored.apiKeyCapabilities;
await persistApiKey(deps, key, {
persistBaseUrl,
defaultTextModel: profilePreset?.defaultTextModel,
defaultVideoModel: profilePreset?.defaultVideoModel,
defaultImageToVideoModel: profilePreset?.defaultImageToVideoModel,
defaultReferenceToVideoModel: profilePreset?.defaultReferenceToVideoModel,
defaultImageModel: profilePreset?.defaultImageModel,
defaultSpeechModel: profilePreset?.defaultSpeechModel,
defaultSpeechRecognitionModel: profilePreset?.defaultSpeechRecognitionModel,
apiKeyCapabilities,
});
process.stderr.write(`API key saved to ${store.path}\n`);
},
});
+4 -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, 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、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,7 +29,9 @@ 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",
],
async run(ctx) {
const { settings, flags } = ctx;
@@ -1,7 +1,9 @@
import {
BailianError,
ExitCode,
isApiKeyCapability,
normalizeModelBaseUrl,
parseBooleanValue,
SUPPORTED_LANGUAGES,
} from "bailian-cli-core";
@@ -12,6 +14,7 @@ export const VALID_KEYS = [
"output",
"output_dir",
"timeout",
"watermark",
"api_key",
"access_token",
"access_key_id",
@@ -23,7 +26,9 @@ export const VALID_KEYS = [
"default_reference_to_video_model",
"default_image_model",
"default_speech_model",
"default_speech_recognition_model",
"default_omni_model",
"api_key_capabilities",
"workspace_id",
] as const;
@@ -59,18 +64,22 @@ 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
// (text/chat, image/generate, video/generate, speech/synthesize, omni/chat) and
// (text/chat, image/generate, video/generate, speech/synthesize,
// speech/recognize, omni/chat) and
// are surfaced as input placeholders so users can see the effective default
// without persisting a value that would pin the model.
export const UI_MODEL_DEFAULTS: Record<string, string> = {
default_text_model: "qwen3.8-max",
default_image_model: "qwen-image-3.0",
default_video_model: "happyhorse-1.1-t2v",
default_video_model: "wan3.0-video",
default_image_to_video_model: "wan3.0-video",
default_reference_to_video_model: "wan3.0-video",
default_speech_model: "cosyvoice-v3-flash",
default_speech_recognition_model: "fun-asr",
default_omni_model: "qwen3.5-omni-plus",
};
@@ -100,12 +109,26 @@ export const UI_MODEL_CATALOG: Record<string, ModelOption[]> = {
{ id: "wanx2.x", role: "image/generate · async series" },
],
default_video_model: [
{ id: "happyhorse-1.1-t2v", role: "video/generate default · text-to-video" },
{ id: "wan3.0-video", role: "video/generate · t2v / i2v / r2v default" },
{ id: "happyhorse-1.1-t2v", role: "video/generate · text-to-video" },
{ id: "happyhorse-1.1-i2v", role: "video/generate · image-to-video" },
{ id: "happyhorse-1.1-r2v", role: "video/ref · reference-to-video" },
],
default_image_to_video_model: [
{ id: "wan3.0-video", role: "video/generate default · image-to-video" },
{ id: "happyhorse-1.1-i2v", role: "video/generate · image-to-video" },
],
default_reference_to_video_model: [
{ id: "wan3.0-video", role: "video/ref default · reference-to-video" },
{ id: "happyhorse-1.1-r2v", role: "video/ref · reference-to-video" },
],
default_speech_model: [
{ id: "cosyvoice-v3-flash", role: "speech/synthesize (TTS) default" },
{ id: "fun-asr", role: "speech/recognize (ASR)" },
{ id: "qwen-audio-3.0-tts-plus", role: "Token Plan speech/synthesize (TTS)" },
],
default_speech_recognition_model: [
{ id: "fun-asr", role: "speech/recognize (ASR) default" },
{ id: "qwen-audio-3.0-asr-flash", role: "Token Plan speech/recognize (ASR)" },
],
default_omni_model: [
{ id: "qwen3.5-omni-plus", role: "omni/chat default" },
@@ -128,7 +151,9 @@ export const KEY_ALIASES: Record<string, string> = {
"default-reference-to-video-model": "default_reference_to_video_model",
"default-image-model": "default_image_model",
"default-speech-model": "default_speech_model",
"default-speech-recognition-model": "default_speech_recognition_model",
"default-omni-model": "default_omni_model",
"api-key-capabilities": "api_key_capabilities",
"workspace-id": "workspace_id",
};
@@ -141,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 {
export function validateAndCoerce(
key: string,
value: string,
): string | number | boolean | string[] {
const resolvedKey = resolveKey(key);
if (!(VALID_KEYS as readonly string[]).includes(resolvedKey)) {
@@ -178,6 +206,41 @@ 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("[")) {
try {
rawCapabilities = JSON.parse(value) as unknown;
} catch {
throw new BailianError(
`Invalid API Key capability list "${value}". Use comma-separated values or a JSON array.`,
ExitCode.USAGE,
);
}
} else {
rawCapabilities = value.split(",").map((capability) => capability.trim());
}
if (!Array.isArray(rawCapabilities)) {
throw new BailianError("Invalid API Key capability list. Expected an array.", ExitCode.USAGE);
}
const capabilities: string[] = [];
for (const rawCapability of rawCapabilities) {
if (typeof rawCapability !== "string" || !isApiKeyCapability(rawCapability.trim())) {
throw new BailianError(
`Invalid API Key capability "${String(rawCapability)}".`,
ExitCode.USAGE,
"Use lowercase alphanumeric segments separated by '.' or '-'.",
);
}
const capability = rawCapability.trim();
if (!capabilities.includes(capability)) capabilities.push(capability);
}
return capabilities;
}
return value;
}
@@ -187,7 +250,10 @@ export function validateAndCoerce(key: string, value: string): string | number {
* extras (console_*, telemetry) are validated here. Booleans are returned as
* real booleans so they persist correctly in config.json.
*/
export function validateAndCoerceUi(key: string, value: string): string | number | boolean {
export function validateAndCoerceUi(
key: string,
value: string,
): string | number | boolean | string[] {
const resolvedKey = resolveKey(key);
if ((VALID_KEYS as readonly string[]).includes(resolvedKey)) {
@@ -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,
};
@@ -1215,7 +1215,9 @@ const PAGE_HTML = `<!doctype html>
var row = el('div', 'row');
var label = el('label', '', key); label.htmlFor = 'f_' + key;
var val = data[key];
var strVal = (val === undefined || val === null) ? '' : String(val);
var strVal = Array.isArray(val)
? (val.length ? val.join(', ') : '[]')
: ((val === undefined || val === null) ? '' : String(val));
if (SECRETS.indexOf(key) >= 0) {
var input = el('input'); input.id = 'f_' + key; input.name = key; input.value = strVal;
input.type = 'password';
+11 -9
View File
@@ -139,15 +139,17 @@ function tokenMatches(provided: string | null, expected: string): boolean {
/** Build the request cleaned/validated config block from a posted `data` map. */
function buildProfilePatch(
data: Record<string, unknown>,
): Record<string, string | number | boolean> {
const cleaned: Record<string, string | number | boolean> = {};
for (const [k, v] of Object.entries(data)) {
): Record<string, string | number | boolean | string[]> {
const cleaned: Record<string, string | number | boolean | string[]> = {};
for (const [key, rawValue] of Object.entries(data)) {
let value = "";
if (typeof v === "string") value = v;
else if (typeof v === "number" || typeof v === "boolean") value = String(v);
// null/undefined/objects fall through as "" and clear the key
if (typeof rawValue === "string") value = rawValue;
else if (typeof rawValue === "number" || typeof rawValue === "boolean") {
value = String(rawValue);
} else if (Array.isArray(rawValue)) value = JSON.stringify(rawValue);
// null/undefined/non-array objects fall through as "" and clear the key
if (value === "") continue;
cleaned[resolveKey(k)] = validateAndCoerceUi(k, value);
cleaned[resolveKey(key)] = validateAndCoerceUi(key, value);
}
return cleaned;
}
@@ -155,7 +157,7 @@ function buildProfilePatch(
/** Preserve valid Config fields that the UI does not expose or manage. */
function mergeUnmanagedProfileFields(
existing: Record<string, unknown>,
managedPatch: Record<string, string | number | boolean>,
managedPatch: Record<string, string | number | boolean | string[]>,
): Record<string, unknown> {
const managedKeys = new Set<string>(UI_VALID_KEYS);
const merged: Record<string, unknown> = {};
@@ -635,7 +637,7 @@ export function createConfigUiServer(
return;
}
let normalized: string | undefined;
let cleaned: Record<string, string | number | boolean>;
let cleaned: Record<string, string | number | boolean | string[]>;
try {
normalized = normalizeConfigName(body.name);
cleaned = buildProfilePatch(body.data as Record<string, unknown>);
@@ -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",
@@ -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.",
});
},
});

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