Compare commits

...

31 Commits

Author SHA1 Message Date
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
chenanran555 aa1484264e fix(managed-agent): improve project initialization and build validation 2026-09-08 14:46:56 +08:00
chenanran555 2767491ce6 Merge pull request #182 from modelstudioai/feat/managed-agent-ci
Feat/managed agent ci 添加managed agents模块的ci能力
2026-09-07 21:42:35 +08:00
chenanran555 efb832815a chore(release): prepare 1.21.0 2026-09-07 21:11:51 +08:00
chenanran555 902516bbad fix(managed-agent): check latest Playground version before launch
Resolve the requested npm version before reusing local builds or running processes, fetch mismatched versions on demand, and preserve explicit binary overrides. Add launcher regression tests and sync the SDK 0.7.0 lockfile.
2026-09-07 20:46:07 +08:00
chenanran555 f44fb2e95f refactor(managed-agent): use SDK project service subpaths
Replace standalone project packages with @openagentpack/sdk 0.7.0 and update project/version imports.

The pnpm lockfile remains pending until SDK 0.7.0 is published to npm; this integration has been validated against the local SDK build.
2026-09-07 19:58:47 +08:00
chenanran555 c6b9931d6f feat(managed-agent): integrate directory resource scaffolds and safe builds 2026-09-04 17:20:50 +08:00
chenanran555 a92c58420b fix(managed-agent): use CLI credentials for directory projects
Make project init fully offline, stop generating project-local credential files, bind Publish to the reviewed plan fingerprint, remove the unsafe Apply refresh-only mode, and document Agent-owned File mounts.
2026-09-04 11:22:11 +08:00
chenanran555 bcf68a31f3 Merge remote-tracking branch 'origin/main' into feat/managed-agent-ci
# Conflicts:
#	packages/commands/package.json
#	packages/commands/src/commands/managed-agent/apply.ts
#	packages/commands/src/commands/managed-agent/init.ts
#	packages/commands/tests/e2e/managed-agent.e2e.test.ts
#	skills/bailian-managed-agent/SKILL.md
#	skills/bailian-managed-agent/reference/index.md
#	skills/bailian-managed-agent/reference/managed-agent.md
2026-09-03 11:38:44 +08:00
chenanran555 8e5dd127a9 Merge pull request #177 from modelstudioai/feat/cma-command
Feat/cma command 补齐managed agent cli能力
2026-09-03 11:00:47 +08:00
chenanran555 4f41eaabc2 chore(release): prepare 1.20.0 2026-09-03 10:47:23 +08:00
chenanran555 e1634c40ed chore(deps): upgrade @openagentpack/sdk to 0.5.0 2026-09-02 22:26:33 +08:00
chenanran555 4c5688ad4a feat(managed-agent)!: restrict CLI to Bailian provider
Remove multi-provider CLI flags and reject non-Bailian provider configs while preserving SDK support.
2026-09-02 19:23:40 +08:00
chenanran555 176961dc56 feat(managed-agent): hide capabilities command from public CLI 2026-09-02 15:33:50 +08:00
Gong Shiqi 888031562f Merge pull request #189 from modelstudioai/docs/console-gateway-backend-attribution
docs(telemetry): update Console Gateway backend attribution
2026-09-02 15:18:51 +08:00
若麒 d017954ae8 docs(telemetry): update Console Gateway backend attribution 2026-09-02 15:11:24 +08:00
chenanran555 9d19558645 feat(managed-agent): enforce high-risk confirmation for runtime operations 2026-09-02 15:07:22 +08:00
chenanran555 a9200a3f85 Merge remote-tracking branch 'origin/main' into feat/cma-command
# Conflicts:
#	packages/commands/src/commands/managed-agent/session-delete.ts
#	packages/commands/tests/e2e/managed-agent.e2e.test.ts
#	skills/bailian-managed-agent/SKILL.md
#	skills/bailian-managed-agent/reference/managed-agent.md
2026-09-02 14:51:49 +08:00
chenanran555 83d7982ecb feat(managed-agent): support creating agents with remote and local skills 2026-09-02 14:36:22 +08:00
chenanran555 e53daf05eb feat(managed-agent): add directory project workflow 2026-08-31 15:55:10 +08:00
chenanran555 449eede237 fix(managed-agent): upgrade OpenAgentPack SDK to 0.4.0 beta 2026-08-27 20:47: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
chenanran555 4c7ef0c7a5 feat(managed-agent): add scoped agent create command
Add `bl managed-agent agent create` with automatic YAML key generation,
atomic config updates, preview-by-default behavior, and create-only scoped
apply that ignores unrelated resource drift.
2026-08-27 11:44:00 +08:00
chenanran555 9469556671 refactor(managed-agent)!: replace Git versioning with local snapshots
Replace @openagentpack/local-git with @openagentpack/project-versions so
the CLI and Workbench share a Git-independent, lock-protected YAML
snapshot store.

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

BREAKING CHANGE: remove `managed-agent init --git` and
`managed-agent apply --ci`; version preview and restore now use
`--version-id` instead of `--commit`.
2026-08-26 18:00:05 +08:00
chenanran555 b93e0d586d feat(managed-agent): add Workbench and Git-backed CI versioning
Add Workbench launch and local Git version commands, enable automatic
agents.yaml versioning after successful applies, introduce CI-safe apply
policies, and reuse the shared @openagentpack/local-git package.
2026-08-25 20:41:47 +08:00
chenanran555 69c1fb9f08 feat(managed-agent): 增加托管 Agent 全面管理命令集及功能实现
- 扩展 CLI 命令,新增托管 Agent 相关全部操作命令包括能力、Agent、环境、技能、保管库、部署、会话及文件管理
- 实现 Agent 列表、详情、版本、搜索等功能
- 实现环境列表、详情及搜索功能
- 实现部署列表、详情、搜索、运行、暂停与恢复功能
- 实现会话的搜索、更新、归档及事件操作功能
- 实现文件上传、列表、下载、删除及搜索功能
- 添加托管 Agent API 操作能力展示命令
- 统一异常处理及输出格式支持,支持 JSON 与表格展示
- 完善分页、查询参数及权限验证逻辑,支持按需筛选与翻页
- 新增输入输出文件支持与控制台保护机制,提升命令行体验和稳定性
2026-08-24 20:15:09 +08:00
101 changed files with 11539 additions and 699 deletions
+53
View File
@@ -6,6 +6,59 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
## [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
+53
View File
@@ -6,6 +6,59 @@
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
## [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
### 新增
+7 -5
View File
@@ -62,17 +62,19 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
`bl managed-agent *` 按调用链分两层:
- **离线命令** — `init``validate``state list/show/rm`:`auth: "none"`,只读写本地文件,无需登录;引擎侧传 `credentials: "none"` 跳过凭证断言
- **联网命令** — `plan``apply``destroy``state import``skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁 —— 无论目标 provider 是谁authStage `resolveApiKey(sources)` 解析 bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层 `assertProviderCredentials` 再对 agents.yaml 里**全部已声明 provider** 的空 key 拦截并给 provider 专属 hint。例外:`plan --no-refresh` / `plan --dry-run``credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state不查 provider key其中 `--dry-run` 连登录也不要求authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
- **联网命令** — `plan``apply``destroy``state import``skill-list`、全部 `session *`:统一声明 `auth: "apiKey"` 硬门禁authStage 经 `resolveApiKey(sources)` 解析 Bailian 凭证(flag > env > active profile config),缺失报统一 AUTH;引擎层再断言 Bailian key 非空。例外:`plan --no-refresh` / `plan --dry-run``credentials: "none"` 并强制 `refresh: false`(不联网、不回写 state不查 provider key其中 `--dry-run` 连登录也不要求authStage 的 dry-run 豁免),`--no-refresh` 仍需登录。
`bl managed-agent` 是 Bailian-only 产品入口:命令不暴露 `--provider``init` 只生成 `providers.bailian`,所有远端调用固定传 `provider: "bailian"``resolveAgentProjectConfig` 在创建 SDK runtime 前通过 `assertBailianOnlyProviders` 拒绝包含非 Bailian Provider 的手写配置;共享 `@openagentpack/sdk` 仍可保留多 Provider 能力。
凭证不以真实值写入 `process.env`,而是经 `packages/commands/src/commands/managed-agent/_engine/` 的**内存注入管道**(`resolveAgentProjectConfig`)注入 SDK管道五步:
1. `prepareProviderEnv()``bootstrapRuntimeCredentialsSync()`(SDK 把 `.env` / `~/.agents/config.json` 灌进 env服务 claude/ark/qoder 等非 bailian provider),再把全部凭证类 env(`CREDENTIAL_ENV_KEYS`,含别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值不因缺变量抛错
2. `resolveProjectConfig`插值发生:bailian 插值拿到占位空串claude/ark 拿到真实 env 值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`(避免离线命令下空 key 在 SDK zod 层报 "received null")
1. `prepareProviderEnv()`调用 SDK 的凭证 bootstrap,再把凭证类 env(`CREDENTIAL_ENV_KEYS`,含兼容别名)中仍为 undefined 的占位为 `""`,使 agents.yaml 插值阶段能够完成并由 CLI 输出明确的 Bailian-only 配置错误
2. `resolveProjectConfig`完成插值;随后 `normalizeInterpolatedProviderBlocks()` 把插值为空导致的 YAML `null` 归一为 `""`避免空 key 在 SDK zod 层提前报 "received null"
3. `injectProviderCredentials()` — 用 `ctx.client.exportApiCredential()`(lint 限定 `managed-agent/_engine/**` 可用)覆写内存 config 对象的 bailian 块:有凭证时 `api_key` 无条件覆写;`base_url`(拼 `/api/v1/agentstudio` 后缀,无凭证时用 client 默认域名补齐以满足 schema)/`workspace_id`(取 `settings.workspaceId`)仅在引用且为空时填充
4. `scrubCredentialEnv()` — 从 `process.env` 删除全部凭证变量(真实凭证此后只存于 config 对象 → provider adapter 实例内存,不驻留 env / 不被子进程继承)
5. `assertProviderCredentials(providers)` — 任一已声明 provider 的 `api_key` 为空 CLI 权威 `AUTH` 错误 + provider 专属 hint(取代 SDK 原始插值/zod 报错);离线命令传 `credentials: "none"` 整体跳过
5. `assertBailianOnlyProviders(providers)` — 拒绝非 Bailian Provider随后 `assertProviderCredentials(providers)` 在 Bailian `api_key` 为空时给出 CLI 权威 `AUTH` 错误和登录 hint离线命令传 `credentials: "none"` 跳过 key 断言,但仍执行 Bailian-only 配置校验
`bl auth login` 仅管理 bailian(DashScope)凭证;claude/ark/qoder 的 key 从 env(shell / `.env` / `~/.agents/config.json`)经插值进入 config 对象,同样被清扫。禁止命令层直接 `readConfigFile` 裸读凭证;bailian 字段以 CLI 鉴权链为唯一信源。
禁止命令层直接 `readConfigFile` 裸读凭证Bailian 字段以 CLI 鉴权链为唯一信源。SDK bootstrap 期间读取到的兼容凭证变量也会在配置解析后统一清扫。
## 必查清单
+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. 鉴权 / 全局选项
+2 -1
View File
@@ -30,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` |
@@ -41,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. 后端渠道参数
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.19.0",
"version": "1.22.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
+109
View File
@@ -140,6 +140,18 @@ import {
managedAgentPlan,
managedAgentApply,
managedAgentDestroy,
managedAgentPlayground,
managedAgentProjectInit,
managedAgentProjectValidate,
managedAgentProjectBuild,
managedAgentProjectPublish,
managedAgentProjectWorkbench,
managedAgentProjectVersionEnable,
managedAgentProjectVersionDisable,
managedAgentProjectVersionStatus,
managedAgentProjectVersionList,
managedAgentProjectVersionPreview,
managedAgentProjectVersionRestore,
managedAgentStateList,
managedAgentStateShow,
managedAgentStateRm,
@@ -152,6 +164,48 @@ import {
managedAgentSessionSend,
managedAgentSessionEvents,
managedAgentSkillList,
managedAgentAgentCreate,
managedAgentEnvironmentCreate,
managedAgentSkillCreate,
managedAgentVaultCreate,
managedAgentVaultCredentialCreate,
managedAgentDeploymentCreate,
managedAgentAgentList,
managedAgentAgentGet,
managedAgentAgentSearch,
managedAgentAgentVersions,
managedAgentEnvironmentList,
managedAgentEnvironmentGet,
managedAgentEnvironmentSearch,
managedAgentSkillGet,
managedAgentSkillSearch,
managedAgentSkillVersions,
managedAgentSkillDownload,
managedAgentVaultList,
managedAgentVaultGet,
managedAgentVaultSearch,
managedAgentDeploymentList,
managedAgentDeploymentGet,
managedAgentDeploymentSearch,
managedAgentDeploymentRunsList,
managedAgentDeploymentRunsGet,
managedAgentDeploymentRun,
managedAgentDeploymentPause,
managedAgentDeploymentUnpause,
managedAgentSessionSearch,
managedAgentSessionUpdate,
managedAgentSessionArchive,
managedAgentSessionEventList,
managedAgentSessionEventSend,
managedAgentSessionEventStream,
managedAgentSessionDebug,
managedAgentSessionExport,
managedAgentFileUpload,
managedAgentFileList,
managedAgentFileGet,
managedAgentFileSearch,
managedAgentFileDownload,
managedAgentFileDelete,
} from "bailian-cli-commands";
// Full bailian-cli product: every command, exposed under the `bl` binary.
@@ -302,6 +356,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,
@@ -314,6 +380,49 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent session send": managedAgentSessionSend,
"managed-agent session events": managedAgentSessionEvents,
"managed-agent skill-list": managedAgentSkillList,
"managed-agent agent create": managedAgentAgentCreate,
"managed-agent agent list": managedAgentAgentList,
"managed-agent agent get": managedAgentAgentGet,
"managed-agent agent search": managedAgentAgentSearch,
"managed-agent agent versions": managedAgentAgentVersions,
"managed-agent environment create": managedAgentEnvironmentCreate,
"managed-agent environment list": managedAgentEnvironmentList,
"managed-agent environment get": managedAgentEnvironmentGet,
"managed-agent environment search": managedAgentEnvironmentSearch,
"managed-agent skill create": managedAgentSkillCreate,
"managed-agent skill list": managedAgentSkillList,
"managed-agent skill get": managedAgentSkillGet,
"managed-agent skill search": managedAgentSkillSearch,
"managed-agent skill versions": managedAgentSkillVersions,
"managed-agent skill download": managedAgentSkillDownload,
"managed-agent vault create": managedAgentVaultCreate,
"managed-agent vault credential create": managedAgentVaultCredentialCreate,
"managed-agent vault list": managedAgentVaultList,
"managed-agent vault get": managedAgentVaultGet,
"managed-agent vault search": managedAgentVaultSearch,
"managed-agent deployment create": managedAgentDeploymentCreate,
"managed-agent deployment list": managedAgentDeploymentList,
"managed-agent deployment get": managedAgentDeploymentGet,
"managed-agent deployment search": managedAgentDeploymentSearch,
"managed-agent deployment runs list": managedAgentDeploymentRunsList,
"managed-agent deployment runs get": managedAgentDeploymentRunsGet,
"managed-agent deployment run": managedAgentDeploymentRun,
"managed-agent deployment pause": managedAgentDeploymentPause,
"managed-agent deployment unpause": managedAgentDeploymentUnpause,
"managed-agent session search": managedAgentSessionSearch,
"managed-agent session update": managedAgentSessionUpdate,
"managed-agent session archive": managedAgentSessionArchive,
"managed-agent session event list": managedAgentSessionEventList,
"managed-agent session event send": managedAgentSessionEventSend,
"managed-agent session event stream": managedAgentSessionEventStream,
"managed-agent session debug": managedAgentSessionDebug,
"managed-agent session export": managedAgentSessionExport,
"managed-agent file upload": managedAgentFileUpload,
"managed-agent file list": managedAgentFileList,
"managed-agent file get": managedAgentFileGet,
"managed-agent file search": managedAgentFileSearch,
"managed-agent file download": managedAgentFileDownload,
"managed-agent file delete": managedAgentFileDelete,
};
/**
@@ -5,9 +5,18 @@ 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("every generated high-risk command reference requires user confirmation before --yes", () => {
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;
@@ -27,9 +36,21 @@ test("every generated high-risk command reference requires user confirmation bef
const commandSections = markdown.split(/(?=^### `bl )/m).slice(1);
for (const commandSection of commandSections) {
if (!commandSection.includes("`--yes`")) continue;
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(commandSection).toMatch(/\|\s+\*\*Risk\*\*\s+\|\s+`high`\s+\|/);
expect(hasConfirmationFlag).toBe(true);
expect(commandSection).toMatch(/\|\s+\*\*Risk message\*\*\s+\|\s+.+\|/);
expect(commandSection).toMatch(/type=.*requires_confirmation/);
const agentSafetyLine = commandSection
@@ -44,4 +65,5 @@ test("every generated high-risk command reference requires user confirmation bef
}
expect(highRiskCommandCount).toBeGreaterThan(0);
expect([...seenScopedCreateCommands].sort()).toEqual([...scopedCreateCommands].sort());
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.19.0",
"version": "1.22.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:",
@@ -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,14 +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默认全部已配置项",
},
},
noRefresh: {
type: "switch",
description: {
@@ -65,9 +53,9 @@ export default defineCommand({
"zh-CN": "该操作会应用当前计划,可能创建、更新或删除远端托管 Agent 资源。",
},
},
usageArgs: "[--file <path>] [--provider <name>] [--concurrency <n>]",
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;
@@ -78,7 +66,7 @@ export default defineCommand({
emitResult(
{
would_apply: {
provider: flags.provider ?? "all",
provider: "bailian",
refresh: !flags.noRefresh,
concurrency: flags.concurrency,
},
@@ -93,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,
@@ -110,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");
@@ -141,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") {
@@ -150,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);
@@ -68,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",
),
};
}),
);
@@ -0,0 +1,41 @@
import type { CloudEnvironment } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
INCLUDE_ARCHIVED_FLAG,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const ENVIRONMENT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const ENVIRONMENT_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const ENVIRONMENT_GET_FLAGS = {
...API_TARGET_FLAGS,
environmentId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Environment ID", "zh-CN": "Environment ID" },
},
} as const;
export function environmentRows(environments: CloudEnvironment[]): string[][] {
return environments.map((environment) => [
environment.id,
displayValue(environment.name),
displayValue(environment.scope),
displayValue(environment.version),
displayValue(environment.updated_at),
]);
}
@@ -0,0 +1,141 @@
import { defineCommand, type FlagsDef } from "bailian-cli-core";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import {
loadScopedCreateProject,
parseMetadata,
resolveCandidateDeclaration,
runScopedTopLevelCreate,
SCOPED_CREATE_NOTE,
} from "../_engine/scoped-create.ts";
const FLAGS = {
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Remote Environment display name; the YAML key is generated automatically",
"zh-CN": "远端 Environment 显示名称YAML key 将自动生成",
},
},
description: {
type: "string",
valueHint: "<text>",
description: { "en-US": "Environment description", "zh-CN": "Environment 描述" },
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Metadata entry (repeatable)",
"zh-CN": "Metadata 条目(可重复)",
},
},
apt: packageFlag("APT"),
pip: packageFlag("pip"),
npm: packageFlag("npm"),
cargo: packageFlag("Cargo"),
gem: packageFlag("Ruby gem"),
go: packageFlag("Go"),
file: fileFlag(),
yes: yesFlag(),
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Declare and create one Managed Agent Environment through a scoped YAML apply",
"zh-CN": "通过定向 YAML Apply 声明并创建一个托管 Agent Environment",
},
auth: "apiKey",
usageArgs:
"--name <name> [--description <text>] [--metadata <key=value>...] [--apt <package>...] [--pip <package>...] [--npm <package>...] [--cargo <package>...] [--gem <package>...] [--go <package>...] [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: [
"--name Development",
"--name Development --pip pandas --npm typescript --metadata owner=platform --yes",
],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US":
"Creates a cloud Environment with unrestricted networking. Without --yes, only previews the generated YAML key and scoped plan.",
"zh-CN":
"创建使用 unrestricted 网络的 cloud Environment。不带 --yes 时仅预览自动生成的 YAML key 和定向计划。",
},
],
validate: (flags) => (!flags.name.trim() ? "--name must not be empty." : undefined),
async run(ctx) {
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
const packages = Object.fromEntries(
["apt", "pip", "npm", "cargo", "gem", "go"]
.map((manager) => [manager, ctx.flags[manager as keyof typeof ctx.flags]])
.filter((entry) => Array.isArray(entry[1]) && entry[1].length > 0),
);
const rawDeclaration: Record<string, unknown> = {
name: ctx.flags.name.trim(),
description: ctx.flags.description,
provider: project.provider,
config: {
type: "cloud",
networking: { type: "unrestricted" },
...(Object.keys(packages).length > 0 ? { packages } : {}),
},
metadata: parseMetadata(ctx.flags.metadata),
};
const resolvedDeclaration = await resolveCandidateDeclaration({
project,
group: "environments",
rawDeclaration,
});
await runScopedTopLevelCreate({
host: ctx,
project,
group: "environments",
resourceType: "environment",
displayName: ctx.flags.name.trim(),
rawDeclaration,
resolvedDeclaration,
existingDeclarations: (project.config.environments ?? {}) as unknown as Record<
string,
Record<string, unknown>
>,
effectiveName: (key, declaration) =>
typeof declaration.name === "string" ? declaration.name : key,
fallbackKey: "environment",
yes: ctx.flags.yes,
});
},
});
function packageFlag(manager: string) {
return {
type: "array" as const,
valueHint: "<package>",
description: {
"en-US": `${manager} package (repeatable)`,
"zh-CN": `${manager} 软件包(可重复)`,
},
};
}
function fileFlag() {
return {
type: "string" as const,
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
};
}
function yesFlag() {
return {
type: "switch" as const,
description: {
"en-US": "Write YAML and run the scoped remote create",
"zh-CN": "写入 YAML 并执行定向远端创建",
},
};
}
@@ -0,0 +1,41 @@
import { getRemoteEnvironment } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { ENVIRONMENT_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Get a Managed Agent environment",
"zh-CN": "获取托管 Agent 环境详情",
},
auth: "apiKey",
usageArgs: "--environment-id <id>",
flags: ENVIRONMENT_GET_FLAGS,
exampleArgs: ["--environment-id env_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const environment = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteEnvironment(runtime, ctx.flags.environmentId, {
provider: "bailian",
});
}),
);
if (format === "json") {
emitResult(environment, format);
return;
}
emitBare(`ID: ${environment.id}`);
emitBare(`Name: ${displayValue(environment.name)}`);
emitBare(`Description: ${displayValue(environment.description, 120)}`);
emitBare(`Scope: ${displayValue(environment.scope)}`);
emitBare(`Version: ${displayValue(environment.version)}`);
emitBare(`Updated: ${displayValue(environment.updated_at)}`);
},
});
@@ -0,0 +1,53 @@
import { listRemoteEnvironments } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { ENVIRONMENT_LIST_FLAGS, environmentRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "List Managed Agent environments", "zh-CN": "列出托管 Agent 环境" },
auth: "apiKey",
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]",
flags: ENVIRONMENT_LIST_FLAGS,
exampleArgs: ["", "--all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteEnvironments(runtime, {
provider: "bailian",
limit: ctx.flags.limit,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "environments",
items: result.items,
headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"],
rows: environmentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No environments found.",
});
},
});
@@ -0,0 +1,68 @@
import { listRemoteEnvironments } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import {
emitCollection,
matchesQuery,
searchCursorPages,
validateLimitAndPageLimit,
} from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { ENVIRONMENT_SEARCH_FLAGS, environmentRows } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Search Managed Agent environments",
"zh-CN": "搜索托管 Agent 环境",
},
auth: "apiKey",
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
flags: ENVIRONMENT_SEARCH_FLAGS,
exampleArgs: ["--query sandbox", "--query production --page-limit 20 --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listRemoteEnvironments(runtime, {
provider: "bailian",
limit: ctx.flags.limit ?? 100,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
(environment) =>
matchesQuery(
ctx.flags.query,
environment.id,
environment.name,
environment.description,
),
ctx.flags.pageLimit,
);
}),
);
emitCollection({
format,
key: "environments",
items: result.items,
headers: ["ID", "NAME", "SCOPE", "VERSION", "UPDATED"],
rows: environmentRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching environments found.",
});
},
});
@@ -0,0 +1,316 @@
import { basename } from "node:path";
import type { ProviderFileInfo } from "@openagentpack/sdk";
import {
deleteFile,
downloadRemoteFile,
getFileInfo,
listRemoteFiles,
uploadFile,
} from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
emitCollection,
matchesQuery,
SEARCH_FLAGS,
searchCursorPages,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { inferMimeType, readInputFile, writeOutputFile } from "./_engine/output-file.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const FILE_ID_FLAG = {
fileId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Remote file ID", "zh-CN": "远端 File ID" },
},
} as const;
const SCOPE_ID_FLAG = {
scopeId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Filter by scope ID", "zh-CN": "按 Scope ID 筛选" },
},
} as const;
const UPLOAD_FLAGS = {
...API_TARGET_FLAGS,
path: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Local file path", "zh-CN": "本地文件路径" },
},
filename: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" },
},
mimeType: {
type: "string",
valueHint: "<type>",
description: { "en-US": "MIME type override", "zh-CN": "覆盖 MIME 类型" },
},
} as const;
const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SCOPE_ID_FLAG };
const SEARCH_RESOURCE_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...SCOPE_ID_FLAG,
};
const GET_FLAGS = { ...API_TARGET_FLAGS, ...FILE_ID_FLAG };
const DOWNLOAD_FLAGS = {
...GET_FLAGS,
outputFile: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Destination path", "zh-CN": "目标路径" },
},
force: {
type: "switch",
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
},
} as const;
const DELETE_FLAGS = {
...GET_FLAGS,
} as const;
function fileRows(files: ProviderFileInfo[]): string[][] {
return files.map((file) => [
file.id,
displayValue(file.filename),
displayValue(file.status),
String(file.size_bytes),
displayValue(file.scope?.id),
displayValue(file.created_at),
]);
}
export const managedAgentFileUpload = defineCommand({
description: { "en-US": "Upload a Managed Agent file", "zh-CN": "上传托管 Agent 文件" },
auth: "apiKey",
usageArgs: "--path <path> [--filename <name>] [--mime-type <type>]",
flags: UPLOAD_FLAGS,
exampleArgs: ["--path ./report.pdf"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{
would_upload_file: ctx.flags.path,
filename: ctx.flags.filename ?? basename(ctx.flags.path),
mime_type: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path),
},
format,
);
return;
}
const content = await readInputFile(ctx.flags.path);
const file = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return uploadFile(runtime, content, ctx.flags.filename ?? basename(ctx.flags.path), {
provider: "bailian",
mimeType: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path),
});
}),
);
if (format === "json") emitResult(file, format);
else emitBare(`File uploaded: ${file.id} (${file.filename})`);
},
});
export const managedAgentFileList = defineCommand({
description: { "en-US": "List Managed Agent files", "zh-CN": "列出托管 Agent 文件" },
auth: "apiKey",
usageArgs: "[--scope-id <id>] [--limit <n>] [--page <cursor>] [--all]",
flags: LIST_FLAGS,
exampleArgs: ["", "--scope-id sess_abc --all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteFiles(runtime, {
provider: "bailian",
scope_id: ctx.flags.scopeId,
limit: ctx.flags.limit,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "files",
items: result.items,
headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"],
rows: fileRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No files found.",
});
},
});
export const managedAgentFileGet = defineCommand({
description: { "en-US": "Get Managed Agent file metadata", "zh-CN": "获取托管 Agent 文件元数据" },
auth: "apiKey",
usageArgs: "--file-id <id>",
flags: GET_FLAGS,
exampleArgs: ["--file-id file_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const file = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getFileInfo(runtime, ctx.flags.fileId, { provider: "bailian" });
}),
);
if (format === "json") emitResult(file, format);
else {
emitBare(`ID: ${file.id}`);
emitBare(`Filename: ${file.filename}`);
emitBare(`MIME: ${file.mime_type}`);
emitBare(`Bytes: ${file.size_bytes}`);
emitBare(`Status: ${displayValue(file.status)}`);
emitBare(`Scope: ${displayValue(file.scope?.id)}`);
}
},
});
export const managedAgentFileSearch = defineCommand({
description: { "en-US": "Search Managed Agent files", "zh-CN": "搜索托管 Agent 文件" },
auth: "apiKey",
usageArgs: "--query <text> [--scope-id <id>] [--limit <n>] [--page-limit <n>]",
flags: SEARCH_RESOURCE_FLAGS,
exampleArgs: ["--query report", "--query pdf --scope-id sess_abc --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listRemoteFiles(runtime, {
provider: "bailian",
scope_id: ctx.flags.scopeId,
limit: ctx.flags.limit ?? 100,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
(file) => matchesQuery(ctx.flags.query, file.id, file.filename, file.mime_type),
ctx.flags.pageLimit,
);
}),
);
emitCollection({
format,
key: "files",
items: result.items,
headers: ["ID", "FILENAME", "STATUS", "BYTES", "SCOPE", "CREATED"],
rows: fileRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching files found.",
});
},
});
export const managedAgentFileDownload = defineCommand({
description: {
"en-US": "Download Managed Agent file content",
"zh-CN": "下载托管 Agent 文件内容",
},
auth: "apiKey",
usageArgs: "--file-id <id> --output-file <path> [--force]",
flags: DOWNLOAD_FLAGS,
exampleArgs: ["--file-id file_abc --output-file ./artifact.pdf"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{ would_download_file: ctx.flags.fileId, output_file: ctx.flags.outputFile },
format,
);
return;
}
const content = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return downloadRemoteFile(runtime, ctx.flags.fileId, { provider: "bailian" });
}),
);
const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force);
if (format === "json")
emitResult({ downloaded: ctx.flags.fileId, output_file: outputFile }, format);
else emitBare(`File downloaded to ${outputFile}`);
},
});
export const managedAgentFileDelete = defineCommand({
description: { "en-US": "Delete a Managed Agent file", "zh-CN": "删除托管 Agent 文件" },
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This permanently deletes the specified remote Managed Agent file.",
"zh-CN": "该操作会永久删除指定的远端托管 Agent 文件。",
},
},
usageArgs: "--file-id <id>",
flags: DELETE_FLAGS,
exampleArgs: ["--file-id file_abc --dry-run", "--file-id file_abc --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult({ would_delete_file: ctx.flags.fileId }, format);
return;
}
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
await deleteFile(runtime, ctx.flags.fileId, { provider: "bailian" });
}),
);
if (format === "json") emitResult({ deleted: ctx.flags.fileId }, format);
else emitBare(`File ${ctx.flags.fileId} deleted.`);
},
});
@@ -1,5 +1,6 @@
import { existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import {
BailianError,
defineCommand,
@@ -12,48 +13,21 @@ import { emitBare, emitResult } from "bailian-cli-runtime";
const GITIGNORE_ADDITIONS = `
# agents
agents.state.json
.openagentpack/versions/
.env
`;
const PROVIDERS = ["bailian", "claude", "qoder", "ark", "all"] as const;
const PROVIDER_BLOCKS: Record<string, string> = {
bailian: ` bailian:\n # bl auth login --api-key <key> sets DASHSCOPE_API_KEY; --base-url <url> sets BAILIAN_BASE_URL\n api_key: \${DASHSCOPE_API_KEY}\n base_url: \${BAILIAN_BASE_URL}`,
claude: ` claude:\n api_key: \${ANTHROPIC_API_KEY}`,
qoder: ` qoder:\n api_key: \${QODER_PAT}\n gateway: "https://api.qoder.com/api/v1/cloud"`,
ark: ` ark:\n api_key: \${ARK_API_KEY}`,
};
const SINGLE_MODEL: Record<string, string> = {
bailian: ` model: qwen3.8-max`,
claude: ` model: claude-sonnet-4-6`,
qoder: ` model: ultimate`,
ark: ` model: doubao-seed-2-1-pro-260628`,
};
function buildTemplate(options: { provider: string; agentName: string }): string {
const providerBlock =
options.provider === "all"
? `${PROVIDER_BLOCKS.bailian}\n${PROVIDER_BLOCKS.claude}\n${PROVIDER_BLOCKS.qoder}\n${PROVIDER_BLOCKS.ark}`
: PROVIDER_BLOCKS[options.provider]!;
const modelBlock =
options.provider === "all"
? ` model:\n bailian: qwen3.8-max\n claude: claude-sonnet-4-6\n qoder: ultimate\n ark: doubao-seed-2-1-pro-260628`
: SINGLE_MODEL[options.provider]!;
const toolBlock =
options.provider === "bailian"
? "[bash, read, glob, grep]"
: "[read, glob, grep, web_search, web_fetch]";
function buildTemplate(options: { agentName: string }): string {
return `version: "1"
providers:
${providerBlock}
bailian:
# bl auth login --api-key <key> sets DASHSCOPE_API_KEY; --base-url <url> sets BAILIAN_BASE_URL
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: ${options.provider === "all" ? "all" : options.provider}
provider: bailian
environments:
dev:
@@ -65,25 +39,16 @@ environments:
agents:
${options.agentName}:
description: "General-purpose assistant"
${modelBlock}
model: qwen3.8-max
instructions: |
You are a helpful assistant.
environment: dev
tools:
builtin: ${toolBlock}
builtin: [bash, read, glob, grep]
`;
}
const INIT_FLAGS = {
provider: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Provider: bailian, claude, qoder, ark, all (default: bailian)",
"zh-CN": "Providerbailian、claude、qoder、ark、all默认bailian",
},
choices: PROVIDERS,
},
agentName: {
type: "string",
valueHint: "<name>",
@@ -108,23 +73,22 @@ const INIT_FLAGS = {
export default defineCommand({
description: {
"en-US": "Create a new agents.yaml template",
"zh-CN": "创建新的 agents.yaml 模板",
"en-US": "Create an agents.yaml template",
"zh-CN": "创建 agents.yaml 模板",
},
auth: "none",
usageArgs: "[--provider <name>] [--agent-name <name>] [--file <path>] [--force]",
usageArgs: "[--agent-name <name>] [--file <path>] [--force]",
flags: INIT_FLAGS,
exampleArgs: ["", "--provider bailian --agent-name assistant", "--provider all"],
exampleArgs: ["", "--agent-name assistant"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const provider = flags.provider ?? "bailian";
const agentName = flags.agentName ?? "assistant";
const file = flags.file ?? "agents.yaml";
if (existsSync(file) && !flags.force) {
throw new BailianError(
`${file} already exists.`,
`${resolve(file)} already exists.`,
ExitCode.USAGE,
"Pass --force to overwrite.",
);
@@ -141,7 +105,7 @@ export default defineCommand({
emitResult(
{
would_create: file,
provider,
provider: "bailian",
agent: agentName,
would_update_gitignore: wouldUpdateGitignore,
},
@@ -150,7 +114,7 @@ export default defineCommand({
return;
}
const template = buildTemplate({ provider, agentName });
const template = buildTemplate({ agentName });
await writeFile(file, template, "utf8");
if (existsSync(gitignorePath)) {
@@ -163,14 +127,12 @@ export default defineCommand({
}
if (format === "json") {
emitResult({ created: file, provider, agent: agentName }, format);
emitResult({ created: file, provider: "bailian", agent: agentName }, format);
} else {
emitBare(`Created ${file}`);
if (provider === "bailian" || provider === "all") {
emitBare(
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
);
}
emitBare(`Created ${resolve(file)}`);
emitBare(
"Credentials: run `bl auth login --api-key <key> --base-url <url>`, or set DASHSCOPE_API_KEY / BAILIAN_BASE_URL.",
);
emitBare("Next: edit agents.yaml, then run `bl managed-agent plan`.");
}
},
@@ -8,13 +8,9 @@ import {
import { emitBare, emitResult } from "bailian-cli-runtime";
import { planProjectContext } from "@openagentpack/sdk";
import { formatResourceLabel } from "./_engine/address-utils.ts";
import {
assertProviderConfigured,
buildAgentRuntime,
CREDENTIALS_NOTE,
} from "./_engine/config-loader.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
const PLAN_FLAGS = {
@@ -26,14 +22,6 @@ const PLAN_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Target provider (default: all configured)",
"zh-CN": "目标 Provider默认全部已配置项",
},
},
noRefresh: {
type: "switch",
description: {
@@ -56,9 +44,9 @@ export default defineCommand({
"zh-CN": "显示将应用到 Agent 基础设施的变更",
},
auth: "apiKey",
usageArgs: "[--file <path>] [--provider <name>] [--no-refresh] [--refresh-only]",
usageArgs: "[--file <path>] [--no-refresh] [--refresh-only]",
flags: PLAN_FLAGS,
exampleArgs: ["", "--provider bailian", "--no-refresh"],
exampleArgs: ["", "--no-refresh"],
notes: [
...CREDENTIALS_NOTE,
{
@@ -83,9 +71,8 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file, {
credentials: offline ? "none" : "all",
});
assertProviderConfigured(runtime, flags.provider);
return planProjectContext(runtime, {
provider: flags.provider,
provider: "bailian",
refresh: !offline,
quiet: format === "json",
onFeedback: format === "json" ? undefined : renderAgentFeedback,
@@ -95,17 +82,18 @@ export default defineCommand({
const plan = planned.plan;
const hasErrors = plan.diagnostics.some((diag) => diag.severity === "error");
const failureMessage = formatAgentDiagnosticFailure(plan.diagnostics, "Plan contains errors.");
if (format === "json") {
emitResult(plan, format);
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL);
return;
}
for (const diag of plan.diagnostics) {
emitBare(`[${diag.severity}] ${diag.code}: ${diag.message}`);
}
if (hasErrors) throw new BailianError("Plan contains errors.", ExitCode.GENERAL);
if (hasErrors) throw new BailianError(failureMessage, ExitCode.GENERAL);
const creates = plan.actions.filter((action) => action.action === "create");
const updates = plan.actions.filter((action) => action.action === "update");
@@ -0,0 +1,468 @@
import { stat } from "node:fs/promises";
import { join } from "node:path";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
commitProjectBuild,
createDirectoryWorkspaceVersionService,
executeProjectPublish,
initializeDirectoryProject,
planProjectPublish,
previewProjectBuild,
type ProjectBuildResolver,
resolveDirectoryProjectRoot,
validateDirectoryProject,
} from "@openagentpack/sdk/project-workspace";
import { CREDENTIALS_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { renderAgentFeedback } from "./_engine/feedback.ts";
import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts";
import { installSdkTransport } from "./_engine/transport.ts";
import { formatResourceLabel } from "./_engine/address-utils.ts";
const PROJECT_FLAG = {
project: {
type: "string",
valueHint: "<directory>",
description: {
"en-US": "Directory project root (default: current directory)",
"zh-CN": "目录项目根路径(默认:当前目录)",
},
},
} satisfies FlagsDef;
const JSON_FORMAT = "json" as const;
export const managedAgentProjectInit = defineCommand({
description: {
"en-US": "Create a directory project or convert the local agents.yaml",
"zh-CN": "创建目录项目,或转换当前 agents.yaml",
},
auth: "none",
usageArgs: "[--project <directory>]",
flags: {
project: {
...PROJECT_FLAG.project,
description: {
"en-US": "Directory project root (default: ./managed-agent under the current directory)",
"zh-CN": "目录项目根路径(默认:当前目录下的 ./managed-agent",
},
},
},
exampleArgs: ["", "--project ./my-agent", "--project ."],
notes: [
{
"en-US":
"Without --project, creates a managed-agent/ subdirectory. Enter it before running other project commands. Use --project . to initialize in place or convert the current agents.yaml; existing project files are not overwritten.",
"zh-CN":
"不传 --project 时创建 managed-agent/ 子目录;后续项目操作请先进入该目录。使用 --project . 可在当前目录初始化或转换 agents.yaml不会覆盖已有项目文件。",
},
{
"en-US":
"New projects include Skill, File, Vault, and Environment examples under each resource directory's _examples/. They are not referenced by agent.json and are excluded from Build/Publish. Copy an example outside _examples/ to enable it, then configure its Agent reference.",
"zh-CN":
"新项目在四类资源目录的 _examples/ 下生成配置示例,不写入 agent.json 引用,也不参与 Build/Publish。需要使用时将示例复制到 _examples/ 外,再配置 Agent 引用。",
},
],
async run(ctx) {
const projectRoot = ctx.flags.project ?? "./managed-agent";
if (ctx.settings.dryRun) {
emitResult(
{
would_initialize_project: projectRoot,
},
detectOutputFormat(ctx.settings.output),
);
return;
}
const result = await initializeDirectoryProject({ projectRoot });
emitResult(result, detectOutputFormat(ctx.settings.output));
},
});
export const managedAgentProjectValidate = defineCommand({
description: { "en-US": "Validate a directory Agent project", "zh-CN": "校验目录式 Agent 项目" },
auth: "none",
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--project ./my-agent"],
async run(ctx) {
const result = await validateDirectoryProject(ctx.flags.project ?? ".");
const format = detectOutputFormat(ctx.settings.output);
if (format === JSON_FORMAT) emitResult(safeInspection(result), format);
else {
for (const diagnostic of [...result.diagnostics, ...result.warnings]) {
emitBare(`${diagnostic.severity}: ${diagnostic.code}: ${diagnostic.message}`);
}
if (!result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
emitBare(`Project is valid (${result.project_revision.slice(0, 12)}).`);
}
}
if (result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
throw new BailianError("Directory project validation failed.", ExitCode.GENERAL);
}
},
});
export const managedAgentProjectBuild = defineCommand({
description: {
"en-US": "Organize directory source and generate the immutable Publish Build",
"zh-CN": "整理目录源文件并生成不可变的发布 Build",
},
auth: "none",
notes: [
{
"en-US":
"Build writes local project files without confirmation, including inferred resource associations and migration of plaintext Vault secrets into .env. Use --dry-run to preview without writing. Publish still requires explicit confirmation before remote changes.",
"zh-CN":
"Build 无需确认即可写入本地项目文件,包括推断的资源关联及将 Vault 明文密钥移入 .env。使用 --dry-run 可只预览不写入。Publish 变更远端资源前仍需显式确认。",
},
],
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--dry-run", "--project ./my-agent"],
async validate(flags) {
await withAgentErrors(async () => {
const root = await resolveDirectoryProjectRoot(flags.project ?? ".");
const metadata = await stat(join(root, "project.json")).catch((error: unknown) => {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
throw error;
});
if (!metadata?.isFile()) {
throw new BailianError(
`Not a project root: ${root} (project.json is missing).`,
ExitCode.USAGE,
"Run from the directory containing project.json, or pass --project <directory>.",
);
}
});
return undefined;
},
async run(ctx) {
const root = ctx.flags.project ?? ".";
const preview = await previewProjectBuild(root);
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(safeInspection(preview), format);
return;
}
if (!preview.can_build)
throw new BailianError(
preview.diagnostics.find((diagnostic) => diagnostic.severity === "error")?.message ??
"Directory project is invalid and cannot be built.",
ExitCode.GENERAL,
);
const built = await commitProjectBuild({
projectRoot: root,
baseRevision: preview.project_revision,
});
emitResult(
{ manifest: built.manifest, organization_moves: preview.organization_moves },
format,
);
},
});
export const managedAgentProjectPublish = defineCommand({
description: {
"en-US": "Publish the current directory-project Build and record a version",
"zh-CN": "发布当前目录项目 Build 并记录版本",
},
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US":
"This publishes the current directory-project Build and may create, update, or delete remote managed Agent resources.",
"zh-CN": "该操作会发布当前目录项目 Build可能创建、更新或删除远端托管 Agent 资源。",
},
},
usageArgs: "[--project <directory>] [--no-refresh] [--concurrency <n>]",
flags: {
...PROJECT_FLAG,
noRefresh: {
type: "switch",
description: {
"en-US": "Skip remote refresh before planning",
"zh-CN": "规划前跳过远端刷新",
},
},
concurrency: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Maximum parallel resource operations",
"zh-CN": "最大并行资源操作数",
},
},
},
exampleArgs: ["--yes", "--project ./my-agent --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
installSdkTransport(ctx);
const root = ctx.flags.project ?? ".";
const resolveBuild: ProjectBuildResolver = async (buildPath, options) =>
(await resolveAgentProjectConfig(ctx, buildPath, {
environment: options?.environment,
overrideBailianBaseUrl: true,
})) as unknown as Awaited<ReturnType<ProjectBuildResolver>>;
const planned = await withAgentErrors(() =>
withStdoutProtected(() =>
planProjectPublish(root, {
provider: "bailian",
refresh: !ctx.flags.noRefresh,
quiet: true,
onFeedback: renderAgentFeedback,
resolveBuild,
}),
),
);
const actions = planned.planned.plan.actions.filter((action) => action.action !== "no-op");
if (ctx.settings.dryRun) {
emitResult(
{
project_revision: planned.project_revision,
build_manifest: planned.build_manifest,
plan: planned.planned.plan,
},
detectOutputFormat(ctx.settings.output),
);
return;
}
for (const action of actions) {
const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "-";
emitBare(`${marker} ${formatResourceLabel(action.address)}`);
}
const result = await withAgentErrors(() =>
withStdoutProtected(() =>
executeProjectPublish({
projectRoot: planned.project_root,
expectedProjectRevision: planned.project_revision,
expectedYamlHash: planned.build_manifest.yaml_hash,
expectedPlanFingerprint: planned.plan_fingerprint,
provider: "bailian",
refresh: !ctx.flags.noRefresh,
concurrency: ctx.flags.concurrency,
policy: "force",
onFeedback: renderAgentFeedback,
resolveBuild,
}),
),
);
emitResult(result, detectOutputFormat(ctx.settings.output));
},
});
export const managedAgentProjectWorkbench = defineCommand({
description: {
"en-US": "Launch the directory project Workbench",
"zh-CN": "启动目录项目 Workbench",
},
auth: "apiKey",
usageArgs: "[--project <directory>] [--port <n>] [--no-open]",
flags: {
...PROJECT_FLAG,
port: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Local port (default: 4848)", "zh-CN": "本地端口默认4848" },
},
noOpen: {
type: "switch",
description: { "en-US": "Do not open a browser", "zh-CN": "不自动打开浏览器" },
},
},
exampleArgs: ["", "--project ./my-agent --no-open"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const root = ctx.flags.project ?? ".";
if (ctx.settings.dryRun) {
emitResult(
{ would_launch: "workbench", project_root: root, port: ctx.flags.port ?? 4848 },
detectOutputFormat(ctx.settings.output),
);
return;
}
await launchManagedAgentPlayground({
project: root,
port: ctx.flags.port ?? 4848,
open: !ctx.flags.noOpen,
surface: "workbench",
client: ctx.client,
settings: ctx.settings,
});
},
});
export const managedAgentProjectVersionEnable = versionToggleCommand(true);
export const managedAgentProjectVersionDisable = versionToggleCommand(false);
function versionToggleCommand(enabled: boolean) {
return defineCommand({
description: enabled
? { "en-US": "Enable directory project versions", "zh-CN": "启用目录项目版本管理" }
: { "en-US": "Disable directory project versions", "zh-CN": "关闭目录项目版本管理" },
auth: "none",
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--project ./my-agent"],
async run(ctx) {
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
if (ctx.settings.dryRun) {
emitResult(
{ would_set_enabled: enabled, status: await service.status() },
detectOutputFormat(ctx.settings.output),
);
return;
}
const result = enabled
? await service.enable("Enable project versions")
: await service.disable();
emitResult(result, detectOutputFormat(ctx.settings.output));
},
});
}
export const managedAgentProjectVersionStatus = defineCommand({
description: {
"en-US": "Show directory project version status",
"zh-CN": "显示目录项目版本状态",
},
auth: "none",
usageArgs: "[--project <directory>]",
flags: PROJECT_FLAG,
exampleArgs: ["", "--project ./my-agent --output json"],
async run(ctx) {
emitResult(
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").status(),
detectOutputFormat(ctx.settings.output),
);
},
});
export const managedAgentProjectVersionList = defineCommand({
description: { "en-US": "List directory project versions", "zh-CN": "列出目录项目版本" },
auth: "none",
usageArgs: "[--project <directory>] [--limit <n>] [--cursor <cursor>]",
flags: {
...PROJECT_FLAG,
limit: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Maximum versions to return", "zh-CN": "最多返回的版本数" },
},
cursor: {
type: "string",
valueHint: "<cursor>",
description: { "en-US": "Pagination cursor", "zh-CN": "分页游标" },
},
},
exampleArgs: ["", "--limit 20 --output json"],
async run(ctx) {
emitResult(
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").listVersions({
limit: ctx.flags.limit,
cursor: ctx.flags.cursor,
}),
detectOutputFormat(ctx.settings.output),
);
},
});
const VERSION_ID_FLAG = {
versionId: {
type: "string",
valueHint: "<full-version>",
required: true,
description: { "en-US": "Full project version ID", "zh-CN": "完整的项目版本 ID" },
},
} satisfies FlagsDef;
export const managedAgentProjectVersionPreview = defineCommand({
description: { "en-US": "Preview a directory project version", "zh-CN": "预览目录项目历史版本" },
auth: "none",
usageArgs: "--version-id <full-version> [--project <directory>]",
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
exampleArgs: ["--version-id <full-version>"],
async run(ctx) {
emitResult(
await createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".").previewVersion(
ctx.flags.versionId,
),
detectOutputFormat(ctx.settings.output),
);
},
});
export const managedAgentProjectVersionRestore = defineCommand({
description: {
"en-US": "Restore a version to the project working directory",
"zh-CN": "将历史版本恢复到项目工作目录",
},
auth: "none",
risk: {
level: "high",
message: {
"en-US":
"This restores the full directory source to the working tree. Version history and remote State will not move.",
"zh-CN": "该操作会将完整目录源文件恢复到工作区;版本历史和远端 State 不会移动。",
},
},
usageArgs: "--version-id <full-version> [--project <directory>]",
flags: { ...PROJECT_FLAG, ...VERSION_ID_FLAG },
exampleArgs: ["--version-id <full-version>", "--version-id <full-version> --yes"],
async run(ctx) {
const service = createDirectoryWorkspaceVersionService(ctx.flags.project ?? ".");
const preview = await service.previewVersion(ctx.flags.versionId);
if (!preview.can_restore)
throw new BailianError(
preview.blockers[0] ?? preview.diagnostics[0]?.message ?? "Version cannot be restored.",
ExitCode.GENERAL,
);
if (ctx.settings.dryRun) {
emitResult(
{ would_restore: ctx.flags.versionId, preview },
detectOutputFormat(ctx.settings.output),
);
return;
}
const restored = await service.restoreVersion(ctx.flags.versionId, {
headVersion: preview.base_head_version,
projectRevision: preview.base_project_revision,
});
emitResult(restored, detectOutputFormat(ctx.settings.output));
},
});
function safeInspection(result: {
project_root: string;
project_revision: string;
source_manifest_hash: string;
yaml_hash: string;
diagnostics: unknown[];
warnings: unknown[];
organization_moves: unknown[];
canonical_yaml: string;
before_yaml?: string;
can_build?: boolean;
}) {
return {
project_root: result.project_root,
project_revision: result.project_revision,
source_manifest_hash: result.source_manifest_hash,
yaml_hash: result.yaml_hash,
diagnostics: result.diagnostics,
warnings: result.warnings,
organization_moves: result.organization_moves,
before_yaml: "before_yaml" in result ? result.before_yaml : undefined,
after_yaml: result.canonical_yaml,
can_build: "can_build" in result ? result.can_build : undefined,
};
}
@@ -49,14 +49,6 @@ const SESSION_CREATE_FLAGS = {
valueHint: "<title>",
description: { "en-US": "Session title", "zh-CN": "Session 标题" },
},
provider: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Target provider (multi-provider agents)",
"zh-CN": "目标 Provider多 Provider Agent",
},
},
} satisfies FlagsDef;
export default defineCommand({
@@ -83,7 +75,7 @@ export default defineCommand({
{
would_create_session: {
agent: flags.agent ?? "auto",
provider: flags.provider ?? "auto",
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memory_stores: parseMemoryStores(flags.memoryStores),
@@ -101,7 +93,7 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file);
return createSessionForAgent(runtime, {
agent: flags.agent,
provider: flags.provider,
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memoryStores: parseMemoryStores(flags.memoryStores),
@@ -0,0 +1,348 @@
import {
getManagedAgentProviderCapabilities,
getSession,
listRemoteFiles,
listSessionEvents,
type ProjectRuntimeContext,
type ProviderFileInfo,
type ProviderSessionEvent,
type ProviderSessionInfo,
} from "@openagentpack/sdk";
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { API_TARGET_FLAGS } from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { writeOutputFile } from "./_engine/output-file.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const SESSION_ID_FLAG = {
sessionId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
},
} as const;
const DEBUG_FLAGS = { ...API_TARGET_FLAGS, ...SESSION_ID_FLAG };
const EXPORT_FLAGS = {
...DEBUG_FLAGS,
outputFile: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Destination ZIP path", "zh-CN": "目标 ZIP 路径" },
},
force: {
type: "switch",
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
},
} as const;
interface SessionDiagnosticBundle {
session: ProviderSessionInfo;
events: unknown[];
files: ProviderFileInfo[];
capabilities: ReturnType<typeof getManagedAgentProviderCapabilities>;
errors: Array<{ component: "events" | "files"; message: string }>;
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
async function collectSessionDiagnostics(
runtime: ProjectRuntimeContext,
sessionId: string,
provider?: string,
): Promise<SessionDiagnosticBundle> {
// Session lookup is the identity anchor. If it fails, the aggregate is not meaningful.
const session = await getSession(runtime, sessionId, provider);
const providerName =
provider ??
(runtime.providers.size === 1 ? Array.from(runtime.providers.keys())[0]! : "bailian");
const errors: SessionDiagnosticBundle["errors"] = [];
let events: ProviderSessionEvent[] = [];
let files: ProviderFileInfo[] = [];
try {
const result = await fetchAllPages(async (page) => {
const response = await listSessionEvents(runtime, sessionId, {
provider,
limit: 100,
page_token: page,
order: "asc",
});
return { items: response.events, hasMore: response.has_more, nextPage: response.next_page };
}, true);
events = result.items;
} catch (error) {
errors.push({ component: "events", message: errorMessage(error) });
}
try {
const result = await fetchAllPages(async (page) => {
const response = await listRemoteFiles(runtime, {
provider,
scope_id: sessionId,
limit: 100,
page,
});
return { items: response.data, hasMore: response.has_more, nextPage: response.next_page };
}, true);
files = result.items;
} catch (error) {
errors.push({ component: "files", message: errorMessage(error) });
}
return {
session,
events: sanitizeSessionEvents(events),
files,
capabilities: getManagedAgentProviderCapabilities(providerName),
errors,
};
}
const SENSITIVE_MARKER = String.raw`(?:api[_-]?key|access[_-]?key|private[_-]?key|secret|token|authorization|credential|password|signature|signed[_-]?url)`;
const SENSITIVE_KEY = new RegExp(`${SENSITIVE_MARKER}|tool[_-]?input`, "i");
const SENSITIVE_URL_PARAMETER = new RegExp(
`([?&][^=&#\\s]*(?:${SENSITIVE_MARKER})[^=&#\\s]*=)[^&#\\s"'<>),}\\]]*`,
"gi",
);
const SENSITIVE_DOUBLE_QUOTED_VALUE = new RegExp(
`("[^"]*(?:${SENSITIVE_MARKER})[^"]*"\\s*:\\s*")[^"]*(")`,
"gi",
);
const SENSITIVE_SINGLE_QUOTED_VALUE = new RegExp(
`('[^']*(?:${SENSITIVE_MARKER})[^']*'\\s*:\\s*')[^']*(')`,
"gi",
);
const SENSITIVE_ASSIGNMENT = new RegExp(
`(\\b[^\\s=:,;]*(?:${SENSITIVE_MARKER})[^\\s=:,;]*\\s*=\\s*)(?!\\[REDACTED\\])(?:"[^"]*"|'[^']*'|[^\\s,;&]+)`,
"gi",
);
const AUTHORIZATION_VALUE = /\b(authorization\s*:\s*)(?:bearer\s+)?[^\s,;]+/gi;
const BEARER_TOKEN = /\b(bearer\s+)[A-Za-z0-9._~+/=-]+/gi;
const API_KEY_LITERAL = /\bsk-[A-Za-z0-9_-]{8,}\b/g;
function redactSensitiveText(value: string): string {
return value
.replace(SENSITIVE_URL_PARAMETER, "$1[REDACTED]")
.replace(SENSITIVE_DOUBLE_QUOTED_VALUE, "$1[REDACTED]$2")
.replace(SENSITIVE_SINGLE_QUOTED_VALUE, "$1[REDACTED]$2")
.replace(SENSITIVE_ASSIGNMENT, "$1[REDACTED]")
.replace(AUTHORIZATION_VALUE, "$1[REDACTED]")
.replace(BEARER_TOKEN, "$1[REDACTED]")
.replace(API_KEY_LITERAL, "[REDACTED]");
}
export function redactSensitiveValues(value: unknown): unknown {
if (Array.isArray(value)) return value.map(redactSensitiveValues);
if (typeof value === "string") return redactSensitiveText(value);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [
key,
SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactSensitiveValues(entry),
]),
);
}
function jsonBytes(value: unknown): Uint8Array {
return new TextEncoder().encode(`${JSON.stringify(redactSensitiveValues(value), null, 2)}\n`);
}
// Minimal ZIP writer using stored (uncompressed) entries. This avoids shelling out
// and keeps export deterministic across supported Node runtimes.
function crc32(bytes: Uint8Array): number {
let checksum = 0xffffffff;
for (const byte of bytes) {
checksum ^= byte;
for (let bit = 0; bit < 8; bit += 1) {
checksum = (checksum >>> 1) ^ (checksum & 1 ? 0xedb88320 : 0);
}
}
return (checksum ^ 0xffffffff) >>> 0;
}
function concatBytes(parts: Uint8Array[]): Uint8Array {
const total = parts.reduce((sum, part) => sum + part.length, 0);
const output = new Uint8Array(total);
let offset = 0;
for (const part of parts) {
output.set(part, offset);
offset += part.length;
}
return output;
}
function littleEndian(values: Array<[number, number]>): Uint8Array {
const size = values.reduce((sum, [, bytes]) => sum + bytes, 0);
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
let offset = 0;
for (const [value, bytes] of values) {
if (bytes === 2) view.setUint16(offset, value, true);
else view.setUint32(offset, value, true);
offset += bytes;
}
return new Uint8Array(buffer);
}
export function createZip(entries: Array<{ name: string; content: Uint8Array }>): Uint8Array {
const encoder = new TextEncoder();
const localParts: Uint8Array[] = [];
const centralParts: Uint8Array[] = [];
let localOffset = 0;
for (const entry of entries) {
const name = encoder.encode(entry.name);
const checksum = crc32(entry.content);
const localHeader = littleEndian([
[0x04034b50, 4],
[20, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 2],
[checksum, 4],
[entry.content.length, 4],
[entry.content.length, 4],
[name.length, 2],
[0, 2],
]);
const local = concatBytes([localHeader, name, entry.content]);
localParts.push(local);
const centralHeader = littleEndian([
[0x02014b50, 4],
[20, 2],
[20, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 2],
[checksum, 4],
[entry.content.length, 4],
[entry.content.length, 4],
[name.length, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 2],
[0, 4],
[localOffset, 4],
]);
centralParts.push(concatBytes([centralHeader, name]));
localOffset += local.length;
}
const central = concatBytes(centralParts);
const end = littleEndian([
[0x06054b50, 4],
[0, 2],
[0, 2],
[entries.length, 2],
[entries.length, 2],
[central.length, 4],
[localOffset, 4],
[0, 2],
]);
return concatBytes([...localParts, central, end]);
}
export const managedAgentSessionDebug = defineCommand({
description: { "en-US": "Aggregate session diagnostics", "zh-CN": "聚合 Session 诊断信息" },
auth: "apiKey",
usageArgs: "--session-id <id>",
flags: DEBUG_FLAGS,
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --output json"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const bundle = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian");
}),
);
const redacted = redactSensitiveValues(bundle);
if (format === "json") {
emitResult(redacted, format);
return;
}
emitBare(`Session: ${bundle.session.id} (${bundle.session.status})`);
emitBare(`Events: ${bundle.events.length}`);
emitBare(`Scoped files: ${bundle.files.length}`);
emitBare(`Partial errors: ${bundle.errors.length}`);
for (const error of bundle.errors) emitBare(` ${error.component}: ${error.message}`);
},
});
export const managedAgentSessionExport = defineCommand({
description: {
"en-US": "Export session diagnostics as a ZIP",
"zh-CN": "将 Session 诊断信息导出为 ZIP",
},
auth: "apiKey",
usageArgs: "--session-id <id> --output-file <path> [--force]",
flags: EXPORT_FLAGS,
exampleArgs: ["--session-id sess_abc --output-file ./session-debug.zip"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US":
"The ZIP contains metadata only; file bodies and credential-like values are excluded/redacted.",
"zh-CN": "ZIP 仅包含元数据;不会包含文件正文,凭证类字段会被移除或脱敏。",
},
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{ would_export_session: ctx.flags.sessionId, output_file: ctx.flags.outputFile },
format,
);
return;
}
const bundle = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return collectSessionDiagnostics(runtime, ctx.flags.sessionId, "bailian");
}),
);
const manifest = {
schema_version: 1,
generated_at: new Date().toISOString(),
session_id: bundle.session.id,
partial: bundle.errors.length > 0,
counts: {
events: bundle.events.length,
files: bundle.files.length,
errors: bundle.errors.length,
},
contents: ["session.json", "events.json", "files.json", "capabilities.json", "errors.json"],
note: "File bodies are not included. Credential-like fields are redacted.",
};
const zip = createZip([
{ name: "manifest.json", content: jsonBytes(manifest) },
{ name: "session.json", content: jsonBytes(bundle.session) },
{ name: "events.json", content: jsonBytes(bundle.events) },
{ name: "files.json", content: jsonBytes(bundle.files) },
{ name: "capabilities.json", content: jsonBytes(bundle.capabilities) },
{ name: "errors.json", content: jsonBytes(bundle.errors) },
]);
const outputFile = await writeOutputFile(ctx.flags.outputFile, zip, ctx.flags.force);
if (format === "json") {
emitResult(
{ exported: bundle.session.id, output_file: outputFile, partial: manifest.partial },
format,
);
} else {
emitBare(
`Session diagnostics exported to ${outputFile}${manifest.partial ? " (partial)" : ""}`,
);
}
},
});
@@ -20,11 +20,6 @@ const SESSION_DELETE_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
} satisfies FlagsDef;
export default defineCommand({
@@ -37,7 +32,7 @@ export default defineCommand({
"zh-CN": "该操作会删除指定的远端托管 Agent Session。",
},
},
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
usageArgs: "--session-id <id> [--file <path>]",
flags: SESSION_DELETE_FLAGS,
exampleArgs: ["--session-id sess_abc123 --yes"],
notes: CREDENTIALS_NOTE,
@@ -50,7 +45,7 @@ export default defineCommand({
emitResult(
{
would_delete_session: flags.sessionId,
provider: flags.provider ?? "auto",
provider: "bailian",
config_file: file,
},
format,
@@ -61,7 +56,7 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
await deleteSession(runtime, flags.sessionId, flags.provider);
await deleteSession(runtime, flags.sessionId, "bailian");
}),
);
@@ -0,0 +1,260 @@
import type { ProviderSessionEvent, SessionEventInput } from "@openagentpack/sdk";
import {
isTerminalSessionStatus,
listSessionEvents,
sendRemoteSessionEvents,
SessionEventSchema,
streamSessionEvents,
} from "@openagentpack/sdk";
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
emitCollection,
splitCommaSeparated,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { readJsonInput } from "./_engine/output-file.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const SESSION_ID_FLAG = {
sessionId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
},
} as const;
const EVENT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
...CURSOR_FLAGS,
order: {
type: "string",
valueHint: "<order>",
choices: ["asc", "desc"] as const,
description: { "en-US": "Event order: asc or desc", "zh-CN": "事件顺序asc 或 desc" },
},
types: {
type: "string",
valueHint: "<types>",
description: {
"en-US": "Comma-separated raw event types",
"zh-CN": "原始事件类型,多个以逗号分隔",
},
},
createdAtGte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or after this timestamp",
"zh-CN": "创建时间不早于该时间戳",
},
},
createdAtLte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or before this timestamp",
"zh-CN": "创建时间不晚于该时间戳",
},
},
} as const;
const EVENT_SEND_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
event: {
type: "string",
valueHint: "<json|@path>",
required: true,
description: {
"en-US": "Raw event object/array as JSON or @event.json",
"zh-CN": "原始事件对象/数组 JSON或 @event.json",
},
},
} as const;
const EVENT_STREAM_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
afterId: {
type: "string",
valueHint: "<event-id>",
description: { "en-US": "Resume after this event ID", "zh-CN": "从该 Event ID 之后继续" },
},
} as const;
function eventRows(events: ProviderSessionEvent[]): string[][] {
return events.map((event) => [
displayValue(event.id),
event.raw_type,
displayValue(event.session_thread_id),
displayValue(event.content ?? event.status ?? event.tool_name, 70),
]);
}
function normalizeEventInput(value: unknown): SessionEventInput[] {
const values = Array.isArray(value) ? value : [value];
if (
values.length === 0 ||
values.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry))
) {
throw new BailianError(
"Event input must be a JSON object or a non-empty array of objects.",
ExitCode.USAGE,
);
}
for (const [index, entry] of values.entries()) {
const validation = SessionEventSchema.safeParse(entry);
if (validation.success) continue;
const issue = validation.error.issues[0];
const path = issue?.path.length ? ` at ${issue.path.join(".")}` : "";
throw new BailianError(
`Invalid event at index ${index}${path}: ${issue?.message ?? "does not match the Session event schema"}.`,
ExitCode.USAGE,
);
}
return values as SessionEventInput[];
}
export const managedAgentSessionEventList = defineCommand({
description: {
"en-US": "List events for a Managed Agent session",
"zh-CN": "列出托管 Agent Session 事件",
},
auth: "apiKey",
usageArgs:
"--session-id <id> [--types <types>] [--order asc|desc] [--limit <n>] [--page <cursor>] [--all]",
flags: EVENT_LIST_FLAGS,
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --all --output json"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US": "--types is applied client-side to each page returned by the provider.",
"zh-CN": "--types 会在客户端对 Provider 返回的每一页结果进行过滤。",
},
],
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listSessionEvents(runtime, ctx.flags.sessionId, {
provider: "bailian",
limit: ctx.flags.limit,
page_token: page,
order: ctx.flags.order,
types: splitCommaSeparated(ctx.flags.types),
created_at_gte: ctx.flags.createdAtGte,
created_at_lte: ctx.flags.createdAtLte,
});
return {
items: response.events,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "events",
items: sanitizeSessionEvents(result.items),
headers: ["ID", "TYPE", "THREAD", "CONTENT"],
rows: eventRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No events found.",
});
},
});
export const managedAgentSessionEventSend = defineCommand({
description: {
"en-US": "Send raw events to a Managed Agent session",
"zh-CN": "向托管 Agent Session 发送原始事件",
},
auth: "apiKey",
usageArgs: "--session-id <id> --event <json|@path>",
flags: EVENT_SEND_FLAGS,
exampleArgs: [
'--session-id sess_abc --event \'{"type":"message","role":"user","content":[{"type":"text","text":"hello"}]}\'',
"--session-id sess_abc --event @event.json",
],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const events = normalizeEventInput(await readJsonInput(ctx.flags.event));
if (ctx.settings.dryRun) {
emitResult({ would_send_events: events, session_id: ctx.flags.sessionId }, format);
return;
}
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return sendRemoteSessionEvents(runtime, ctx.flags.sessionId, events, {
provider: "bailian",
});
}),
);
if (format === "json") emitResult({ session_id: ctx.flags.sessionId, ...result }, format);
else emitBare(`Sent ${events.length} event(s): ${result.event_ids.join(", ")}`);
},
});
export const managedAgentSessionEventStream = defineCommand({
description: {
"en-US": "Stream events from a Managed Agent session",
"zh-CN": "流式读取托管 Agent Session 事件",
},
auth: "apiKey",
usageArgs: "--session-id <id> [--after-id <event-id>]",
flags: EVENT_STREAM_FLAGS,
exampleArgs: ["--session-id sess_abc", "--session-id sess_abc --after-id evt_123 --output json"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US":
"When the provider has no native event cursor, --after-id resumes through paginated history polling and event ID de-duplication.",
"zh-CN":
"当 Provider 不支持原生事件 Cursor 时,--after-id 会通过分页历史轮询和 Event ID 去重实现续传。",
},
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
const events: ProviderSessionEvent[] = [];
for await (const event of streamSessionEvents(runtime, ctx.flags.sessionId, {
provider: "bailian",
after_id: ctx.flags.afterId,
})) {
events.push(event);
if (format !== "json") emitBare(JSON.stringify(sanitizeSessionEvents([event])[0]));
if (event.type === "status" && isTerminalSessionStatus(event.status)) break;
}
if (format === "json") {
emitResult(
{ session_id: ctx.flags.sessionId, events: sanitizeSessionEvents(events) },
format,
);
}
}),
);
},
});
@@ -1,98 +1,2 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import { listSessionEvents } from "@openagentpack/sdk";
import { sanitizeSessionEvents } from "@openagentpack/sdk/session-events";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
const SESSION_EVENTS_FLAGS = {
sessionId: {
type: "string",
valueHint: "<id>",
description: { "en-US": "Session ID (required)", "zh-CN": "Session ID必填" },
required: true,
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
limit: {
type: "number",
valueHint: "<n>",
description: { "en-US": "Maximum number of events to fetch", "zh-CN": "要获取的最大事件数" },
},
all: {
type: "switch",
description: {
"en-US": "Fetch all pages by following the cursor",
"zh-CN": "跟随 Cursor 获取全部分页",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "List event history for a session", "zh-CN": "列出 Session 的事件历史" },
auth: "apiKey",
usageArgs: "--session-id <id> [--limit <n>] [--all] [--file <path>]",
flags: SESSION_EVENTS_FLAGS,
exampleArgs: ["--session-id sess_abc123", "--session-id sess_abc123 --all"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const { items: events, hasMore } = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return fetchAllPages(async (page) => {
const result = await listSessionEvents(runtime, flags.sessionId, {
provider: flags.provider,
limit: flags.limit,
page_token: page,
});
return {
items: result.events,
hasMore: result.has_more,
nextPage: result.next_page,
};
}, flags.all);
}),
);
if (format === "json") {
emitResult({ events: sanitizeSessionEvents(events), has_more: hasMore }, format);
return;
}
if (events.length === 0) {
emitBare("No events found.");
return;
}
const headers = ["#", "TYPE", "CONTENT"];
const rows = events.map((event, index) => {
let preview = "";
if (event.type === "message") preview = (event.content ?? "").slice(0, 60);
else if (event.type === "tool_use") preview = event.tool_name ?? "";
else if (event.type === "tool_result") preview = (event.content ?? "").slice(0, 60);
else if (event.type === "status") preview = event.status ?? "";
else if (event.type === "error") preview = (event.content ?? "").slice(0, 60);
else preview = event.raw_type;
return [String(index + 1), event.type, preview];
});
for (const line of formatTable(headers, rows)) emitBare(line);
emitBare(`\nTotal: ${events.length}`);
if (hasMore) emitBare("More events available. Use --all to fetch all.");
},
});
// Backward-compatible alias for `managed-agent session event list`.
export { managedAgentSessionEventList as default } from "./session-event.ts";
@@ -20,17 +20,12 @@ const SESSION_GET_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
} satisfies FlagsDef;
export default defineCommand({
description: { "en-US": "Get details of a session", "zh-CN": "获取 Session 详情" },
auth: "apiKey",
usageArgs: "--session-id <id> [--provider <name>] [--file <path>]",
usageArgs: "--session-id <id> [--file <path>]",
flags: SESSION_GET_FLAGS,
exampleArgs: ["--session-id sess_abc123"],
notes: CREDENTIALS_NOTE,
@@ -42,7 +37,7 @@ export default defineCommand({
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return getSession(runtime, flags.sessionId, flags.provider);
return getSession(runtime, flags.sessionId, "bailian");
}),
);
@@ -5,6 +5,11 @@ import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { fetchAllPages } from "./_engine/pagination.ts";
import {
CURSOR_FLAGS,
splitCommaSeparated,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
const SESSION_LIST_FLAGS = {
file: {
@@ -27,10 +32,31 @@ const SESSION_LIST_FLAGS = {
"zh-CN": "跟随 Cursor 获取全部分页",
},
},
provider: {
limit: CURSOR_FLAGS.limit,
page: CURSOR_FLAGS.page,
statuses: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
valueHint: "<statuses>",
description: {
"en-US": "Comma-separated session statuses",
"zh-CN": "Session 状态,多个以逗号分隔",
},
},
createdAtGte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or after this RFC 3339 timestamp",
"zh-CN": "创建时间不早于该 RFC 3339 时间戳",
},
},
createdAtLte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or before this RFC 3339 timestamp",
"zh-CN": "创建时间不晚于该 RFC 3339 时间戳",
},
},
} satisfies FlagsDef;
@@ -40,36 +66,51 @@ export default defineCommand({
"zh-CN": "列出 Provider 中的 Session",
},
auth: "apiKey",
usageArgs: "[--agent <name>] [--all] [--provider <name>] [--file <path>]",
usageArgs: "[--agent <name>] [--statuses <statuses>] [--limit <n>] [--page <cursor>] [--all]",
flags: SESSION_LIST_FLAGS,
exampleArgs: ["", "--agent assistant", "--all"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const { items: summaries, hasMore } = await withAgentErrors(() =>
const {
items: summaries,
hasMore,
nextPage,
} = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
return fetchAllPages(async (page) => {
const result = await listSessionSummaries(runtime, {
agent: flags.agent,
provider: flags.provider,
filter: page ? { page } : undefined,
});
return {
items: result.summaries,
hasMore: result.hasMore,
nextPage: result.nextPage,
};
}, flags.all);
return fetchAllPages(
async (page) => {
const result = await listSessionSummaries(runtime, {
agent: flags.agent,
provider: "bailian",
filter: {
page,
limit: flags.limit,
statuses: splitCommaSeparated(flags.statuses),
created_at_gte: flags.createdAtGte,
created_at_lte: flags.createdAtLte,
},
});
return {
items: result.summaries,
hasMore: result.hasMore,
nextPage: result.nextPage,
};
},
flags.all,
flags.page,
);
}),
);
const sessions = summaries.map((summary) => summary.session);
if (format === "json") {
emitResult({ sessions, has_more: hasMore }, format);
emitResult({ sessions, has_more: hasMore, next_page: nextPage }, format);
return;
}
if (sessions.length === 0) {
@@ -0,0 +1,242 @@
import type { ProviderSessionInfo } from "@openagentpack/sdk";
import {
archiveRemoteSession,
listSessionSummaries,
updateRemoteSession,
} from "@openagentpack/sdk";
import { BailianError, defineCommand, detectOutputFormat, ExitCode } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
emitCollection,
matchesQuery,
SEARCH_FLAGS,
searchCursorPages,
splitCommaSeparated,
validateLimitAndPageLimit,
} from "./_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { readJsonInput } from "./_engine/output-file.ts";
const SESSION_ID_FLAG = {
sessionId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Session ID", "zh-CN": "Session ID" },
},
} as const;
const SEARCH_RESOURCE_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
agent: {
type: "string",
valueHint: "<name>",
description: {
"en-US": "Filter by configured agent name",
"zh-CN": "按配置中的 Agent 名称筛选",
},
},
statuses: {
type: "string",
valueHint: "<statuses>",
description: {
"en-US": "Comma-separated session statuses",
"zh-CN": "Session 状态,多个以逗号分隔",
},
},
createdAtGte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or after this timestamp",
"zh-CN": "创建时间不早于该时间戳",
},
},
createdAtLte: {
type: "string",
valueHint: "<timestamp>",
description: {
"en-US": "Created at or before this timestamp",
"zh-CN": "创建时间不晚于该时间戳",
},
},
} as const;
const UPDATE_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
title: {
type: "string",
valueHint: "<title>",
description: { "en-US": "New session title", "zh-CN": "新的 Session 标题" },
},
metadata: {
type: "string",
valueHint: "<json|@path>",
description: {
"en-US": "String-valued metadata JSON or @file",
"zh-CN": "值为字符串的 Metadata JSON 或 @file",
},
},
} as const;
const ARCHIVE_FLAGS = {
...API_TARGET_FLAGS,
...SESSION_ID_FLAG,
} as const;
function sessionRows(sessions: ProviderSessionInfo[]): string[][] {
return sessions.map((session) => [
session.id,
displayValue(session.title),
displayValue(session.agent_id),
session.status,
displayValue(session.updated_at),
]);
}
async function parseMetadata(value?: string): Promise<Record<string, string> | undefined> {
if (!value) return undefined;
const parsed = await readJsonInput(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new BailianError("--metadata must be a JSON object.", ExitCode.USAGE);
}
for (const [key, entry] of Object.entries(parsed)) {
if (typeof entry !== "string") {
throw new BailianError(`Metadata value for '${key}' must be a string.`, ExitCode.USAGE);
}
}
return parsed as Record<string, string>;
}
export const managedAgentSessionSearch = defineCommand({
description: { "en-US": "Search Managed Agent sessions", "zh-CN": "搜索托管 Agent Session" },
auth: "apiKey",
usageArgs:
"--query <text> [--agent <name>] [--statuses <statuses>] [--limit <n>] [--page-limit <n>]",
flags: SEARCH_RESOURCE_FLAGS,
exampleArgs: ["--query debug", "--query failed --statuses failed --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listSessionSummaries(runtime, {
agent: ctx.flags.agent,
provider: "bailian",
filter: {
page,
limit: ctx.flags.limit ?? 100,
statuses: splitCommaSeparated(ctx.flags.statuses),
created_at_gte: ctx.flags.createdAtGte,
created_at_lte: ctx.flags.createdAtLte,
},
});
return {
items: response.summaries,
hasMore: response.hasMore,
nextPage: response.nextPage,
};
},
(summary) =>
matchesQuery(
ctx.flags.query,
summary.session.id,
summary.session.title,
summary.session.status,
summary.session.agent_id,
summary.agentName,
),
ctx.flags.pageLimit,
);
}),
);
const sessions = result.items.map((summary) => summary.session);
emitCollection({
format,
key: "sessions",
items: sessions,
headers: ["ID", "TITLE", "AGENT", "STATUS", "UPDATED"],
rows: sessionRows(sessions),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching sessions found.",
});
},
});
export const managedAgentSessionUpdate = defineCommand({
description: { "en-US": "Update a Managed Agent session", "zh-CN": "更新托管 Agent Session" },
auth: "apiKey",
usageArgs: "--session-id <id> [--title <title>] [--metadata <json|@path>]",
flags: UPDATE_FLAGS,
exampleArgs: [
"--session-id sess_abc --title 'investigation'",
"--session-id sess_abc --metadata @metadata.json",
],
notes: CREDENTIALS_NOTE,
validate: (flags) =>
!flags.title && !flags.metadata ? "Provide --title or --metadata." : undefined,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const input = { title: ctx.flags.title, metadata: await parseMetadata(ctx.flags.metadata) };
if (ctx.settings.dryRun) {
emitResult({ would_update_session: ctx.flags.sessionId, input }, format);
return;
}
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return updateRemoteSession(runtime, ctx.flags.sessionId, input, {
provider: "bailian",
});
}),
);
if (format === "json") emitResult(session, format);
else emitBare(`Session ${session.id} updated.`);
},
});
export const managedAgentSessionArchive = defineCommand({
description: { "en-US": "Archive a Managed Agent session", "zh-CN": "归档托管 Agent Session" },
auth: "apiKey",
risk: {
level: "high",
message: {
"en-US": "This archives the specified remote Managed Agent Session.",
"zh-CN": "该操作会归档指定的远端托管 Agent Session。",
},
},
usageArgs: "--session-id <id>",
flags: ARCHIVE_FLAGS,
exampleArgs: ["--session-id sess_abc --dry-run", "--session-id sess_abc --yes"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult({ would_archive_session: ctx.flags.sessionId }, format);
return;
}
const session = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return archiveRemoteSession(runtime, ctx.flags.sessionId, { provider: "bailian" });
}),
);
if (format === "json") emitResult({ archived: ctx.flags.sessionId, session }, format);
else emitBare(`Session ${ctx.flags.sessionId} archived.`);
},
});
@@ -59,11 +59,6 @@ const SESSION_RUN_FLAGS = {
valueHint: "<title>",
description: { "en-US": "Session title", "zh-CN": "Session 标题" },
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
noStream: {
type: "switch",
description: {
@@ -108,7 +103,7 @@ export default defineCommand({
const runOptions = {
agent: flags.agent,
provider: flags.provider,
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memoryStores: parseMemoryStores(flags.memoryStores),
@@ -121,7 +116,7 @@ export default defineCommand({
would_run: {
prompt: flags.prompt,
agent: flags.agent ?? "auto",
provider: flags.provider ?? "auto",
provider: "bailian",
environment: flags.environment,
vault: flags.vault,
memory_stores: runOptions.memoryStores,
@@ -27,11 +27,6 @@ const SESSION_SEND_FLAGS = {
"zh-CN": "配置文件路径默认agents.yaml",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
noStream: {
type: "switch",
description: {
@@ -68,7 +63,7 @@ export default defineCommand({
would_send: {
session_id: flags.sessionId,
message: flags.message,
provider: flags.provider ?? "auto",
provider: "bailian",
mode: flags.noStream ? "polling" : "streaming",
},
config_file: file,
@@ -83,7 +78,7 @@ export default defineCommand({
const runtime = await buildAgentRuntime(ctx, file);
if (flags.noStream) {
const result = await sendSessionMessagePolling(runtime, flags.sessionId, flags.message, {
provider: flags.provider,
provider: "bailian",
});
renderCollectedEvents(result, asJson, {
session_id: flags.sessionId,
@@ -94,7 +89,7 @@ export default defineCommand({
flags.sessionId,
flags.message,
{
provider: flags.provider,
provider: "bailian",
},
);
await streamAndRenderEvents(events, asJson, {
@@ -1,115 +1,2 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
import { listSkills } from "@openagentpack/sdk";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
const SKILL_SOURCES = ["custom", "official", "all"] as const;
type SkillSource = (typeof SKILL_SOURCES)[number];
const SKILL_LIST_FLAGS = {
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
source: {
type: "string",
valueHint: "<source>",
description: {
"en-US":
"Skill catalog: custom (workspace-uploaded, default), official (built-in), or all (both catalogs in one call)",
"zh-CN":
"Skill CatalogcustomWorkspace 上传默认、official内置或 all一次调用获取两类 Catalog",
},
},
provider: {
type: "string",
valueHint: "<name>",
description: { "en-US": "Target provider", "zh-CN": "目标 Provider" },
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "List skills from the provider's skill catalog",
"zh-CN": "列出 Provider Skill Catalog 中的 Skill",
},
auth: "apiKey",
usageArgs: "[--source custom|official|all] [--provider <name>] [--file <path>]",
flags: SKILL_LIST_FLAGS,
exampleArgs: [
"",
"--source official",
"--source all --output json",
"--source custom --provider bailian",
],
notes: [
...CREDENTIALS_NOTE,
{
"en-US": "Providers without a skill listing API (e.g. ark) return an empty list.",
"zh-CN": "没有 Skill 列表 API 的 Provider例如 ark会返回空列表。",
},
{
"en-US":
"For agent-driven skill selection, use `--source all --output json`: one call returns both catalogs with per-skill `source` and `description` fields to pick from.",
"zh-CN":
"由 Agent 选择 Skill 时,请使用 `--source all --output json`:一次调用返回两类 Catalog并为每个 Skill 提供 `source` 和 `description` 字段用于选择。",
},
{
"en-US":
"When generating a task that needs a suitable skill, call this command to match official or custom skills before wiring them into the task.",
"zh-CN": "生成需要合适 Skill 的任务时,先调用此命令匹配官方或自定义 Skill再将其接入任务。",
},
],
validate: (f) =>
f.source && !SKILL_SOURCES.includes(f.source as SkillSource)
? "--source must be one of: custom, official, all."
: undefined,
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const file = flags.file ?? "agents.yaml";
const source = (flags.source as SkillSource | undefined) ?? "custom";
const skills = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, file);
if (source !== "all") {
return listSkills(runtime, { provider: flags.provider, source });
}
// Both catalogs in one call; each entry carries its own `source` field.
const [customSkills, officialSkills] = await Promise.all([
listSkills(runtime, { provider: flags.provider, source: "custom" }),
listSkills(runtime, { provider: flags.provider, source: "official" }),
]);
return [...customSkills, ...officialSkills];
}),
);
if (format === "json") {
emitResult({ source, skills }, format);
return;
}
if (skills.length === 0) {
emitBare(source === "all" ? "No skills found." : `No ${source} skills found.`);
return;
}
const headers = ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "CREATED"];
const rows = skills.map((skill) => [
skill.id,
skill.name.slice(0, 32),
skill.source,
skill.status,
skill.latest_version ?? "-",
skill.created_at ?? "-",
]);
for (const line of formatTable(headers, rows)) emitBare(line);
emitBare(`\nTotal: ${skills.length} (${source})`);
},
});
// Backward-compatible alias for the API-oriented `managed-agent skill list` command.
export { default } from "./skill/list.ts";
@@ -0,0 +1,87 @@
import type { ProviderSkillInfo, SkillVersionInfo } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const SKILL_SOURCES = ["custom", "official", "all"] as const;
export type SkillSource = (typeof SKILL_SOURCES)[number];
const SOURCE_FLAG = {
source: {
type: "string",
valueHint: "<source>",
choices: SKILL_SOURCES,
description: {
"en-US": "Skill catalog: custom (default), official, or all",
"zh-CN": "Skill Catalogcustom默认、official 或 all",
},
},
} as const;
export const SKILL_LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SOURCE_FLAG };
export const SKILL_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...SOURCE_FLAG,
};
export const SKILL_GET_FLAGS = {
...API_TARGET_FLAGS,
skillId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Skill ID", "zh-CN": "Skill ID" },
},
} as const;
export const SKILL_VERSIONS_FLAGS = {
...SKILL_GET_FLAGS,
...CURSOR_FLAGS,
};
export const SKILL_DOWNLOAD_FLAGS = {
...SKILL_GET_FLAGS,
skillVersion: {
type: "string",
valueHint: "<version>",
required: true,
description: { "en-US": "Skill version", "zh-CN": "Skill 版本" },
},
outputFile: {
type: "string",
valueHint: "<path>",
required: true,
description: { "en-US": "Destination ZIP path", "zh-CN": "目标 ZIP 路径" },
},
force: {
type: "switch",
description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" },
},
} as const;
export function skillRows(skills: ProviderSkillInfo[]): string[][] {
return skills.map((skill) => [
skill.id,
displayValue(skill.name),
skill.source,
skill.status,
displayValue(skill.latest_version),
displayValue(skill.updated_at ?? skill.created_at),
]);
}
export function versionRows(versions: SkillVersionInfo[]): string[][] {
return versions.map((version) => [
displayValue(version.version),
displayValue(version.name),
displayValue(version.type),
displayValue(version.status),
displayValue(version.updated_at ?? version.created_at),
]);
}
@@ -0,0 +1,112 @@
import { dirname, relative, sep } from "node:path";
import { inspectSkillSource } from "@openagentpack/sdk";
import { BailianError, defineCommand, ExitCode, type FlagsDef } from "bailian-cli-core";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import {
loadScopedCreateProject,
resolveCandidateDeclaration,
runScopedTopLevelCreate,
SCOPED_CREATE_NOTE,
} from "../_engine/scoped-create.ts";
const FLAGS = {
source: {
type: "string",
valueHint: "<directory|zip|SKILL.md>",
required: true,
description: {
"en-US": "Local Skill directory, ZIP archive, or single SKILL.md",
"zh-CN": "本地 Skill 目录、ZIP 压缩包或单个 SKILL.md",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and upload the Skill through the scoped create",
"zh-CN": "写入 YAML 并通过定向创建上传 Skill",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Declare and create one custom Managed Agent Skill from a local source",
"zh-CN": "从本地来源声明并创建一个自定义托管 Agent Skill",
},
auth: "apiKey",
usageArgs: "--source <directory|zip|SKILL.md> [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: ["--source ./skills/code-review", "--source ./skill.zip --yes"],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US":
"The YAML key is derived from SKILL.md frontmatter name. Remote URLs remain available through handwritten YAML plus full apply.",
"zh-CN":
"YAML key 从 SKILL.md frontmatter 的 name 生成。远程 URL 仍可手工写入 YAML 后执行全量 Apply。",
},
],
validate: (flags) => (!flags.source.trim() ? "--source must not be empty." : undefined),
async run(ctx) {
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
const inspected = await inspectSkillSource(ctx.flags.source, { basePath: process.cwd() });
const source =
relative(dirname(project.configPath), inspected.sourcePath).split(sep).join("/") || ".";
const existingSkills = (project.config.skills ?? {}) as unknown as Record<
string,
Record<string, unknown>
>;
const trackedSameName = await project.stateBackend.read(project.stateScope, (state) =>
state.listResources().some((resource) => {
if (resource.address.provider !== project.provider || resource.address.type !== "skill") {
return false;
}
const declaration = existingSkills[resource.address.name];
const canonicalName =
typeof declaration?.name === "string" ? declaration.name : resource.address.name;
return canonicalName === inspected.name;
}),
);
if (trackedSameName) {
throw new BailianError(
`Skill '${inspected.name}' is already tracked.`,
ExitCode.USAGE,
"Modify its existing YAML declaration and run full `bl managed-agent apply` to create a new version.",
);
}
const rawDeclaration: Record<string, unknown> = {
name: inspected.name,
source,
origin: "custom",
provider: project.provider,
};
const resolvedDeclaration = await resolveCandidateDeclaration({
project,
group: "skills",
rawDeclaration,
});
await runScopedTopLevelCreate({
host: ctx,
project,
group: "skills",
resourceType: "skill",
displayName: inspected.name,
rawDeclaration,
resolvedDeclaration,
existingDeclarations: existingSkills,
effectiveName: (key, declaration) =>
typeof declaration.name === "string" ? declaration.name : key,
fallbackKey: "skill",
yes: ctx.flags.yes,
});
},
});
@@ -0,0 +1,49 @@
import { downloadRemoteSkill } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { writeOutputFile } from "../_engine/output-file.ts";
import { SKILL_DOWNLOAD_FLAGS } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "Download a Managed Agent skill version",
"zh-CN": "下载托管 Agent Skill 版本",
},
auth: "apiKey",
usageArgs: "--skill-id <id> --skill-version <version> --output-file <path> [--force]",
flags: SKILL_DOWNLOAD_FLAGS,
exampleArgs: ["--skill-id skill_abc --skill-version 3 --output-file ./skill.zip"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
if (ctx.settings.dryRun) {
emitResult(
{
would_download_skill: ctx.flags.skillId,
version: ctx.flags.skillVersion,
output_file: ctx.flags.outputFile,
},
format,
);
return;
}
const content = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return downloadRemoteSkill(runtime, ctx.flags.skillId, ctx.flags.skillVersion, {
provider: "bailian",
});
}),
);
const outputFile = await writeOutputFile(ctx.flags.outputFile, content, ctx.flags.force);
if (format === "json")
emitResult(
{ downloaded: ctx.flags.skillId, version: ctx.flags.skillVersion, output_file: outputFile },
format,
);
else emitBare(`Skill downloaded to ${outputFile}`);
},
});
@@ -0,0 +1,36 @@
import { getRemoteSkill } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { SKILL_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Get a Managed Agent skill", "zh-CN": "获取托管 Agent Skill 详情" },
auth: "apiKey",
usageArgs: "--skill-id <id>",
flags: SKILL_GET_FLAGS,
exampleArgs: ["--skill-id skill_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const skill = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteSkill(runtime, ctx.flags.skillId, { provider: "bailian" });
}),
);
if (format === "json") {
emitResult(skill, format);
return;
}
emitBare(`ID: ${skill.id}`);
emitBare(`Name: ${skill.name}`);
emitBare(`Description: ${displayValue(skill.description, 120)}`);
emitBare(`Source: ${skill.source}`);
emitBare(`Status: ${skill.status}`);
emitBare(`Version: ${displayValue(skill.latest_version)}`);
},
});
@@ -0,0 +1,108 @@
import { listRemoteSkills } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { SKILL_LIST_FLAGS, skillRows, type SkillSource } from "./_shared.ts";
export function buildSkillListJsonResult<T>(
source: SkillSource,
result: { items: T[]; hasMore: boolean; nextPage?: string },
) {
return {
source,
skills: result.items,
has_more: result.hasMore,
next_page: result.nextPage,
};
}
async function listOneCatalog(
runtime: Parameters<typeof listRemoteSkills>[0],
source: Exclude<SkillSource, "all">,
options: { limit?: number; page?: string; all?: boolean },
) {
return fetchAllPages(
async (page) => {
const response = await listRemoteSkills(runtime, {
provider: "bailian",
source,
limit: options.limit,
page,
});
return { items: response.data, hasMore: response.has_more, nextPage: response.next_page };
},
options.all,
options.page,
);
}
export default defineCommand({
description: { "en-US": "List Managed Agent skills", "zh-CN": "列出托管 Agent Skill" },
auth: "apiKey",
usageArgs: "[--source custom|official|all] [--limit <n>] [--page <cursor>] [--all]",
flags: SKILL_LIST_FLAGS,
exampleArgs: ["", "--source official", "--source all --all --output json"],
notes: [
...CREDENTIALS_NOTE,
{
"en-US":
"--source all combines one page from each catalog, or every page with --all; it does not accept --page.",
"zh-CN":
"--source all 默认合并两个 Catalog 的各一页,传入 --all 时获取全部分页;该模式不接受 --page。",
},
],
validate: (flags) =>
validateLimitAndPageLimit(flags) ??
(flags.source === "all" && flags.page
? "--source all cannot be combined with --page."
: undefined),
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const source = (ctx.flags.source as SkillSource | undefined) ?? "custom";
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
if (source !== "all") {
return listOneCatalog(runtime, source, {
limit: ctx.flags.limit,
page: ctx.flags.page,
all: ctx.flags.all,
});
}
const [custom, official] = await Promise.all([
listOneCatalog(runtime, "custom", {
limit: ctx.flags.limit,
all: ctx.flags.all,
}),
listOneCatalog(runtime, "official", {
limit: ctx.flags.limit,
all: ctx.flags.all,
}),
]);
return {
items: [...custom.items, ...official.items],
hasMore: custom.hasMore || official.hasMore,
nextPage: undefined,
};
}),
);
if (format === "json") {
emitResult(buildSkillListJsonResult(source, result), format);
return;
}
emitCollection({
format,
key: "skills",
items: result.items,
headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"],
rows: skillRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No skills found.",
});
},
});
@@ -0,0 +1,73 @@
import { listRemoteSkills } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import {
emitCollection,
matchesQuery,
searchCursorPages,
validateLimitAndPageLimit,
} from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { SKILL_SEARCH_FLAGS, skillRows, type SkillSource } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Search Managed Agent skills", "zh-CN": "搜索托管 Agent Skill" },
auth: "apiKey",
usageArgs: "--query <text> [--source custom|official|all] [--limit <n>] [--page-limit <n>]",
flags: SKILL_SEARCH_FLAGS,
exampleArgs: ["--query browser --source official", "--query report --source all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const source = (ctx.flags.source as SkillSource | undefined) ?? "custom";
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
const searchCatalog = (catalog: Exclude<SkillSource, "all">) =>
searchCursorPages(
async (page) => {
const response = await listRemoteSkills(runtime, {
provider: "bailian",
source: catalog,
limit: ctx.flags.limit ?? 100,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
(skill) => matchesQuery(ctx.flags.query, skill.id, skill.name, skill.description),
ctx.flags.pageLimit,
);
if (source !== "all") return searchCatalog(source);
const [custom, official] = await Promise.all([
searchCatalog("custom"),
searchCatalog("official"),
]);
return {
items: [...custom.items, ...official.items],
hasMore: custom.hasMore || official.hasMore,
nextPage: undefined,
scannedPages: custom.scannedPages + official.scannedPages,
truncated: custom.truncated || official.truncated,
};
}),
);
emitCollection({
format,
key: "skills",
items: result.items,
headers: ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "UPDATED"],
rows: skillRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching skills found.",
});
},
});
@@ -0,0 +1,55 @@
import { listRemoteSkillVersions } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { SKILL_VERSIONS_FLAGS, versionRows } from "./_shared.ts";
export default defineCommand({
description: {
"en-US": "List Managed Agent skill versions",
"zh-CN": "列出托管 Agent Skill 版本",
},
auth: "apiKey",
usageArgs: "--skill-id <id> [--limit <n>] [--page <cursor>] [--all]",
flags: SKILL_VERSIONS_FLAGS,
exampleArgs: ["--skill-id skill_abc", "--skill-id skill_abc --all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteSkillVersions(runtime, ctx.flags.skillId, {
provider: "bailian",
limit: ctx.flags.limit,
page,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "versions",
items: result.items,
headers: ["VERSION", "NAME", "TYPE", "STATUS", "UPDATED"],
rows: versionRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No skill versions found.",
});
},
});
@@ -1,4 +1,10 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { importResource, parseStateAddress } from "@openagentpack/sdk";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
@@ -8,7 +14,7 @@ import { withAgentErrors } from "./_engine/errors.ts";
const STATE_IMPORT_FLAGS = {
address: {
type: "string",
valueHint: "<provider.type.name>",
valueHint: "<bailian.type.name>",
description: {
"en-US": "Resource state address (required)",
"zh-CN": "资源 State 地址(必填)",
@@ -42,6 +48,17 @@ const STATE_IMPORT_FLAGS = {
},
} satisfies FlagsDef;
function parseBailianStateAddress(address: string) {
const parsed = parseStateAddress(address, { requireProvider: true });
if (parsed.provider !== "bailian") {
throw new BailianError(
`bl managed-agent can import only Bailian resources; address provider was '${parsed.provider}'. / bl managed-agent 只能导入百炼资源;地址中的 Provider 为 '${parsed.provider}'。`,
ExitCode.USAGE,
);
}
return parsed;
}
export default defineCommand({
description: {
"en-US": "Import an existing remote resource into agents state",
@@ -49,7 +66,7 @@ export default defineCommand({
},
auth: "apiKey",
usageArgs:
"--address <provider.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
"--address <bailian.type.name> --remote-id <id> [--resource-version <n>] [--file <path>]",
flags: STATE_IMPORT_FLAGS,
exampleArgs: ["--address bailian.agent.assistant --remote-id agent-abc123"],
notes: CREDENTIALS_NOTE,
@@ -61,7 +78,7 @@ export default defineCommand({
if (settings.dryRun) {
// Validate the address shape locally so dry-run still catches usage errors.
await withAgentErrors(async () => {
parseStateAddress(flags.address, { requireProvider: true });
parseBailianStateAddress(flags.address);
});
emitResult(
{
@@ -78,9 +95,7 @@ export default defineCommand({
await withAgentErrors(() =>
withStdoutProtected(async () => {
// Parse first so a malformed address fails fast, before any config I/O.
const parsed = parseStateAddress(flags.address, {
requireProvider: true,
});
const parsed = parseBailianStateAddress(flags.address);
const runtime = await buildAgentRuntime(ctx, file);
await importResource(runtime, parsed, flags.remoteId, {
resourceVersion: flags.resourceVersion,
@@ -8,7 +8,7 @@ import {
import { emitBare, emitResult } from "bailian-cli-runtime";
import { validateProjectConfig } from "@openagentpack/sdk";
import { OFFLINE_NOTE, resolveAgentProjectConfig } from "./_engine/config-loader.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import { formatAgentDiagnosticFailure, withAgentErrors } from "./_engine/errors.ts";
const VALIDATE_FLAGS = {
file: {
@@ -56,7 +56,10 @@ export default defineCommand({
}
if (errorCount > 0) {
throw new BailianError(`Validation failed with ${errorCount} error(s).`, ExitCode.GENERAL);
throw new BailianError(
formatAgentDiagnosticFailure(diagnostics, `Validation failed with ${errorCount} error(s).`),
ExitCode.GENERAL,
);
}
},
});
@@ -0,0 +1,41 @@
import type { CloudVault } from "@openagentpack/sdk";
import {
API_TARGET_FLAGS,
CURSOR_FLAGS,
displayValue,
INCLUDE_ARCHIVED_FLAG,
SEARCH_FLAGS,
} from "../_engine/api-helpers.ts";
export const VAULT_LIST_FLAGS = {
...API_TARGET_FLAGS,
...CURSOR_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const VAULT_SEARCH_FLAGS = {
...API_TARGET_FLAGS,
limit: CURSOR_FLAGS.limit,
...SEARCH_FLAGS,
...INCLUDE_ARCHIVED_FLAG,
};
export const VAULT_GET_FLAGS = {
...API_TARGET_FLAGS,
vaultId: {
type: "string",
valueHint: "<id>",
required: true,
description: { "en-US": "Vault ID", "zh-CN": "Vault ID" },
},
} as const;
export function vaultRows(vaults: CloudVault[]): string[][] {
return vaults.map((vault) => [
vault.id,
displayValue(vault.display_name),
displayValue(vault.type),
displayValue(vault.created_at),
displayValue(vault.updated_at),
]);
}
@@ -0,0 +1,95 @@
import { defineCommand, type FlagsDef } from "bailian-cli-core";
import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import {
loadScopedCreateProject,
parseMetadata,
resolveCandidateDeclaration,
runScopedTopLevelCreate,
SCOPED_CREATE_NOTE,
} from "../_engine/scoped-create.ts";
const FLAGS = {
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Remote Vault display name; the YAML key is generated automatically",
"zh-CN": "远端 Vault 显示名称YAML key 将自动生成",
},
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Metadata entry (repeatable)",
"zh-CN": "Metadata 条目(可重复)",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and run the scoped remote create",
"zh-CN": "写入 YAML 并执行定向远端创建",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Declare and create one empty Managed Agent Vault through a scoped YAML apply",
"zh-CN": "通过定向 YAML Apply 声明并创建一个空的托管 Agent Vault",
},
auth: "apiKey",
usageArgs: "--name <name> [--metadata <key=value>...] [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: ["--name Production", "--name Production --metadata owner=platform --yes"],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US": "Creates an empty Vault. Add secrets later with `vault credential create`.",
"zh-CN": "创建空 Vault随后使用 `vault credential create` 添加 Secret。",
},
],
validate: (flags) => (!flags.name.trim() ? "--name must not be empty." : undefined),
async run(ctx) {
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml");
const rawDeclaration: Record<string, unknown> = {
display_name: ctx.flags.name.trim(),
provider: project.provider,
credentials: [],
metadata: parseMetadata(ctx.flags.metadata),
};
const resolvedDeclaration = await resolveCandidateDeclaration({
project,
group: "vaults",
rawDeclaration,
});
await runScopedTopLevelCreate({
host: ctx,
project,
group: "vaults",
resourceType: "vault",
displayName: ctx.flags.name.trim(),
rawDeclaration,
resolvedDeclaration,
existingDeclarations: (project.config.vaults ?? {}) as unknown as Record<
string,
Record<string, unknown>
>,
effectiveName: (key, declaration) =>
typeof declaration.display_name === "string" ? declaration.display_name : key,
fallbackKey: "vault",
yes: ctx.flags.yes,
});
},
});
@@ -0,0 +1,337 @@
import { dirname } from "node:path";
import {
bootstrapRuntimeCredentialsSync,
createVaultCredentialWithStateBackend,
planVaultCredentialCreateWithStateBackend,
type ResolvedProjectConfig,
resolveProjectConfigFromObject,
} from "@openagentpack/sdk";
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { parseDocument } from "yaml";
import { CREDENTIALS_NOTE } from "../../_engine/config-loader.ts";
import { withStdoutProtected } from "../../_engine/console-capture.ts";
import { retainAgentError, withAgentErrors } from "../../_engine/errors.ts";
import {
loadScopedCreateProject,
parseMetadata,
replaceConfigAtomically,
SCOPED_CREATE_NOTE,
} from "../../_engine/scoped-create.ts";
const FLAGS = {
vault: {
type: "string",
valueHint: "<yaml-key>",
required: true,
description: {
"en-US": "Existing tracked Vault key from agents.yaml",
"zh-CN": "agents.yaml 中已跟踪的 Vault key",
},
},
name: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Credential display name",
"zh-CN": "Credential 显示名称",
},
},
secretName: {
type: "string",
valueHint: "<name>",
required: true,
description: {
"en-US": "Environment variable name exposed to the Agent",
"zh-CN": "提供给 Agent 的环境变量名",
},
},
secretEnv: {
type: "string",
valueHint: "<env-name>",
required: true,
description: {
"en-US": "Local environment variable containing the secret value",
"zh-CN": "保存真实 Secret 的本地环境变量名",
},
},
metadata: {
type: "array",
valueHint: "<key=value>",
description: {
"en-US": "Metadata entry (repeatable)",
"zh-CN": "Metadata 条目(可重复)",
},
},
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
yes: {
type: "switch",
description: {
"en-US": "Write YAML and create the remote Credential",
"zh-CN": "写入 YAML 并创建远端 Credential",
},
},
} satisfies FlagsDef;
export default defineCommand({
description: {
"en-US": "Append and create one environment-variable Credential in a tracked Vault",
"zh-CN": "在已跟踪 Vault 中追加并创建一个环境变量 Credential",
},
auth: "apiKey",
usageArgs:
"--vault <yaml-key> --name <name> --secret-name <name> --secret-env <env-name> [--metadata <key=value>...] [--file <path>] [--yes]",
flags: FLAGS,
exampleArgs: [
"--vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN",
"--vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN --yes",
],
notes: [
...CREDENTIALS_NOTE,
...SCOPED_CREATE_NOTE,
{
"en-US":
"--secret-env is an environment variable name, not the secret itself. The CLI auto-loads the nearest .env from the current directory upward; shell exports and CI secret injection also work.",
"zh-CN":
"--secret-env 接收环境变量名,不接收 Secret 本身。CLI 会从当前目录向上自动加载最近的 .env也支持 Shell export 和 CI Secret 注入。",
},
{
"en-US":
"YAML stores only ${ENV_NAME}. Never commit .env; subsequent full apply runs must provide the same environment variable.",
"zh-CN":
"YAML 只保存 ${ENV_NAME}。不要提交 .env后续执行全量 Apply 时仍需提供同名环境变量。",
},
],
validate: (flags) => {
if (!flags.vault.trim()) return "--vault must not be empty.";
if (!flags.name.trim()) return "--name must not be empty.";
if (!flags.secretName.trim()) return "--secret-name must not be empty.";
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(flags.secretEnv)) {
return "--secret-env must be a valid environment variable name.";
}
return undefined;
},
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
bootstrapRuntimeCredentialsSync();
const secretValue = process.env[ctx.flags.secretEnv];
const project = await loadScopedCreateProject(ctx, ctx.flags.file ?? "agents.yaml").finally(
() => {
delete process.env[ctx.flags.secretEnv];
},
);
const vault = project.config.vaults?.[ctx.flags.vault];
if (!vault) {
throw new BailianError(
`Vault '${ctx.flags.vault}' is not declared in agents.yaml.`,
ExitCode.USAGE,
);
}
if (ctx.flags.yes && !ctx.settings.dryRun && !secretValue) {
throw new BailianError(
`Environment variable '${ctx.flags.secretEnv}' is not set or is empty.`,
ExitCode.USAGE,
`Set it in your shell, CI secret store, or a local .env file, then re-run with --secret-env ${ctx.flags.secretEnv}.`,
);
}
const metadata = parseMetadata(ctx.flags.metadata);
const rawCredential = {
name: ctx.flags.name.trim(),
type: "environment_variable" as const,
secret_name: ctx.flags.secretName.trim(),
secret_value: `\${${ctx.flags.secretEnv}}`,
networking: { type: "unrestricted" as const },
...(metadata ? { metadata } : {}),
};
const runtimeCredential = {
...rawCredential,
secret_value: secretValue ?? "__dry_run_secret__",
};
const document = parseDocument(project.source);
if (document.errors.length > 0) {
throw new BailianError(
`YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`,
ExitCode.USAGE,
);
}
const rawConfig = document.toJS() as {
vaults?: Record<string, { credentials?: unknown[] }>;
};
const rawCredentials = rawConfig.vaults?.[ctx.flags.vault]?.credentials ?? [];
const sameNameCredentials = rawCredentials.filter(
(credential): credential is Record<string, unknown> =>
Boolean(
credential &&
typeof credential === "object" &&
!Array.isArray(credential) &&
(credential as Record<string, unknown>).name === rawCredential.name,
),
);
let reusedPending = false;
if (sameNameCredentials.length > 0) {
const lastCredential = rawCredentials.at(-1);
const existing = sameNameCredentials[0]!;
if (
sameNameCredentials.length !== 1 ||
existing !== lastCredential ||
!sameCredentialDeclaration(existing, rawCredential)
) {
throw new BailianError(
`Vault '${ctx.flags.vault}' already declares a different credential named '${rawCredential.name}'.`,
ExitCode.USAGE,
"Modify the existing YAML declaration and run full `bl managed-agent apply`; create never overwrites it.",
);
}
reusedPending = true;
}
const rawCandidate = structuredClone(project.config) as unknown as Record<string, unknown>;
const rawVaults = rawCandidate.vaults as Record<string, Record<string, unknown>>;
const rawVault = rawVaults[ctx.flags.vault]!;
if (!reusedPending) {
rawVaults[ctx.flags.vault] = {
...rawVault,
credentials: [
...((rawVault.credentials as unknown[] | undefined) ?? []),
runtimeCredential,
],
};
}
const resolvedCandidate = await withAgentErrors(() =>
resolveProjectConfigFromObject(rawCandidate, {
projectName: project.projectName,
basePath: dirname(project.configPath),
}),
);
const candidateConfig = resolvedCandidate.config;
if (!reusedPending) {
document.addIn(["vaults", ctx.flags.vault, "credentials"], rawCredential);
}
const nextSource = reusedPending ? project.source : document.toString();
const backendInput = {
projectName: project.projectName,
config: candidateConfig as ResolvedProjectConfig,
configPath: project.configPath,
providers: { [project.provider]: candidateConfig.providers[project.provider] },
stateBackend: project.stateBackend,
stateScope: project.stateScope,
};
if (ctx.settings.dryRun || !ctx.flags.yes) {
const planned = await withAgentErrors(() =>
withStdoutProtected(() =>
planVaultCredentialCreateWithStateBackend(
backendInput,
ctx.flags.vault,
ctx.flags.name.trim(),
{
provider: project.provider,
refresh: !ctx.settings.dryRun,
quiet: format === "json",
checkRemote: !ctx.settings.dryRun,
},
),
),
);
const result = {
vault: { key: ctx.flags.vault, remote_id: planned.vaultRemoteId },
credential: {
name: ctx.flags.name.trim(),
secret_name: ctx.flags.secretName.trim(),
secret_env: ctx.flags.secretEnv,
},
config_file: project.configPath,
yaml_written: false,
reused_pending: reusedPending,
requires_confirmation: !ctx.settings.dryRun,
ready_to_create: true,
remote_checked: planned.remoteChecked,
reuse_remote: planned.reuseRemote,
};
if (format === "json") emitResult(result, format);
else {
emitBare(`Vault: ${ctx.flags.vault}`);
emitBare(`Credential: ${ctx.flags.name.trim()} (secret from ${ctx.flags.secretEnv})`);
emitBare(
ctx.settings.dryRun
? "Dry run: YAML, State, and remote resources were not changed."
: "Preview only: re-run with --yes to append YAML and create this Credential.",
);
}
return;
}
await replaceConfigAtomically(project.configPath, project.source, nextSource);
let created;
try {
created = await withAgentErrors(() =>
withStdoutProtected(() =>
createVaultCredentialWithStateBackend(
backendInput,
ctx.flags.vault,
ctx.flags.name.trim(),
{ provider: project.provider, refresh: true, quiet: format === "json" },
),
),
);
} catch (error) {
throw retainAgentError(
error,
"The YAML declaration was kept. Restore the same secret environment variable and re-run this command; an already-created matching Credential will be adopted.",
"Vault Credential create failed.",
);
}
const result = {
vault: { key: ctx.flags.vault, remote_id: created.vaultRemoteId },
credential: {
id: created.credentialId,
name: ctx.flags.name.trim(),
secret_name: ctx.flags.secretName.trim(),
secret_env: ctx.flags.secretEnv,
},
config_file: project.configPath,
yaml_written: true,
reused_pending: reusedPending,
adopted: created.adopted,
status: "completed",
};
if (format === "json") emitResult(result, format);
else {
emitBare(`Wrote ${project.configPath} and created Credential '${ctx.flags.name.trim()}'.`);
}
},
});
function sameCredentialDeclaration(
existing: Record<string, unknown>,
candidate: Record<string, unknown>,
): boolean {
return canonicalJson(existing) === canonicalJson(candidate);
}
function canonicalJson(value: unknown): string {
const normalize = (candidate: unknown): unknown => {
if (Array.isArray(candidate)) return candidate.map(normalize);
if (!candidate || typeof candidate !== "object") return candidate;
return Object.fromEntries(
Object.entries(candidate as Record<string, unknown>)
.filter(([, entry]) => entry !== undefined)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, entry]) => [key, normalize(entry)]),
);
};
return JSON.stringify(normalize(value));
}
@@ -0,0 +1,35 @@
import { getRemoteVault } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitBare, emitResult } from "bailian-cli-runtime";
import { displayValue } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { VAULT_GET_FLAGS } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Get a Managed Agent vault", "zh-CN": "获取托管 Agent Vault 详情" },
auth: "apiKey",
usageArgs: "--vault-id <id>",
flags: VAULT_GET_FLAGS,
exampleArgs: ["--vault-id vault_abc"],
notes: CREDENTIALS_NOTE,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const vault = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return getRemoteVault(runtime, ctx.flags.vaultId, { provider: "bailian" });
}),
);
if (format === "json") {
emitResult(vault, format);
return;
}
emitBare(`ID: ${vault.id}`);
emitBare(`Name: ${displayValue(vault.display_name)}`);
emitBare(`Type: ${displayValue(vault.type)}`);
emitBare(`Created: ${displayValue(vault.created_at)}`);
emitBare(`Updated: ${displayValue(vault.updated_at)}`);
},
});
@@ -0,0 +1,53 @@
import { listRemoteVaults } from "@openagentpack/sdk";
import { defineCommand, detectOutputFormat } from "bailian-cli-core";
import { emitCollection, validateLimitAndPageLimit } from "../_engine/api-helpers.ts";
import { buildAgentRuntime, CREDENTIALS_NOTE } from "../_engine/config-loader.ts";
import { withStdoutProtected } from "../_engine/console-capture.ts";
import { withAgentErrors } from "../_engine/errors.ts";
import { fetchAllPages } from "../_engine/pagination.ts";
import { VAULT_LIST_FLAGS, vaultRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "List Managed Agent vaults", "zh-CN": "列出托管 Agent Vault" },
auth: "apiKey",
usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]",
flags: VAULT_LIST_FLAGS,
exampleArgs: ["", "--all --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return fetchAllPages(
async (page) => {
const response = await listRemoteVaults(runtime, {
provider: "bailian",
limit: ctx.flags.limit,
page,
include_archived: ctx.flags.includeArchived,
});
return {
items: response.data,
hasMore: response.has_more,
nextPage: response.next_page,
};
},
ctx.flags.all,
ctx.flags.page,
);
}),
);
emitCollection({
format,
key: "vaults",
items: result.items,
headers: ["ID", "NAME", "TYPE", "CREATED", "UPDATED"],
rows: vaultRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
emptyMessage: "No vaults found.",
});
},
});
@@ -0,0 +1,59 @@
import { listRemoteVaults } 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 { VAULT_SEARCH_FLAGS, vaultRows } from "./_shared.ts";
export default defineCommand({
description: { "en-US": "Search Managed Agent vaults", "zh-CN": "搜索托管 Agent Vault" },
auth: "apiKey",
usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]",
flags: VAULT_SEARCH_FLAGS,
exampleArgs: ["--query github", "--query production --page-limit 20 --output json"],
notes: CREDENTIALS_NOTE,
validate: validateLimitAndPageLimit,
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const result = await withAgentErrors(() =>
withStdoutProtected(async () => {
const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml");
return searchCursorPages(
async (page) => {
const response = await listRemoteVaults(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,
};
},
(vault) => matchesQuery(ctx.flags.query, vault.id, vault.display_name, vault.metadata),
ctx.flags.pageLimit,
);
}),
);
emitCollection({
format,
key: "vaults",
items: result.items,
headers: ["ID", "NAME", "TYPE", "CREATED", "UPDATED"],
rows: vaultRows(result.items),
hasMore: result.hasMore,
nextPage: result.nextPage,
truncated: result.truncated,
scannedPages: result.scannedPages,
emptyMessage: "No matching vaults found.",
});
},
});
@@ -0,0 +1,90 @@
import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { launchManagedAgentPlayground } from "./_engine/playground-launcher.ts";
const PLAYGROUND_BASE_FLAGS = {
file: {
type: "string",
valueHint: "<path>",
description: {
"en-US": "Config file path (default: agents.yaml)",
"zh-CN": "配置文件路径默认agents.yaml",
},
},
port: {
type: "number",
valueHint: "<n>",
description: {
"en-US": "Local port (default: 4848)",
"zh-CN": "本地端口默认4848",
},
},
noOpen: {
type: "switch",
description: {
"en-US": "Do not open a browser automatically",
"zh-CN": "不自动打开浏览器",
},
},
} satisfies FlagsDef;
const PLAYGROUND_FLAGS = {
...PLAYGROUND_BASE_FLAGS,
agent: {
type: "string",
valueHint: "<id>",
description: {
"en-US": "Agent to preview (required when the project declares multiple Agents)",
"zh-CN": "要预览的 Agent项目包含多个 Agent 时需要指定)",
},
},
} satisfies FlagsDef;
const PLAYGROUND_NOTES = [
...CREDENTIALS_NOTE,
{
"en-US":
"Session Preview requires Node.js 22+ and keeps using an agents.yaml source. Directory Workbench is available under managed-agent project workbench.",
"zh-CN":
"会话预览需要 Node.js 22+,并继续使用 agents.yaml目录 Workbench 位于 managed-agent project workbench。",
},
];
export const managedAgentPlayground = defineCommand({
description: {
"en-US": "Launch a Session Preview for an agents.yaml Agent",
"zh-CN": "为 agents.yaml 中的 Agent 启动会话预览",
},
auth: "apiKey",
usageArgs: "[--file <path>] [--agent <id>] [--port <n>] [--no-open]",
flags: PLAYGROUND_FLAGS,
exampleArgs: ["", "--agent assistant", "--file agents.yaml --no-open"],
notes: PLAYGROUND_NOTES,
async run(ctx) {
const file = ctx.flags.file ?? "agents.yaml";
const port = ctx.flags.port ?? 4848;
if (ctx.settings.dryRun) {
emitResult(
{
would_launch: "playground",
config_file: file,
agent: ctx.flags.agent,
port,
open_browser: !ctx.flags.noOpen,
},
detectOutputFormat(ctx.settings.output),
);
return;
}
await launchManagedAgentPlayground({
file,
agent: ctx.flags.agent,
port,
open: !ctx.flags.noOpen,
surface: "preview",
client: ctx.client,
settings: ctx.settings,
});
},
});
+64
View File
@@ -137,6 +137,20 @@ export { default as managedAgentValidate } from "./commands/managed-agent/valida
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
export { default as managedAgentApply } from "./commands/managed-agent/apply.ts";
export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.ts";
export { managedAgentPlayground } from "./commands/managed-agent/workbench.ts";
export {
managedAgentProjectBuild,
managedAgentProjectInit,
managedAgentProjectPublish,
managedAgentProjectValidate,
managedAgentProjectVersionDisable,
managedAgentProjectVersionEnable,
managedAgentProjectVersionList,
managedAgentProjectVersionPreview,
managedAgentProjectVersionRestore,
managedAgentProjectVersionStatus,
managedAgentProjectWorkbench,
} from "./commands/managed-agent/project.ts";
export { default as managedAgentStateList } from "./commands/managed-agent/state-list.ts";
export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts";
export { default as managedAgentStateRm } from "./commands/managed-agent/state-rm.ts";
@@ -149,6 +163,56 @@ export { default as managedAgentSessionRun } from "./commands/managed-agent/sess
export { default as managedAgentSessionSend } from "./commands/managed-agent/session-send.ts";
export { default as managedAgentSessionEvents } from "./commands/managed-agent/session-events.ts";
export { default as managedAgentSkillList } from "./commands/managed-agent/skill-list.ts";
export { default as managedAgentAgentCreate } from "./commands/managed-agent/agent/create.ts";
export { default as managedAgentEnvironmentCreate } from "./commands/managed-agent/environment/create.ts";
export { default as managedAgentSkillCreate } from "./commands/managed-agent/skill/create.ts";
export { default as managedAgentVaultCreate } from "./commands/managed-agent/vault/create.ts";
export { default as managedAgentVaultCredentialCreate } from "./commands/managed-agent/vault/credential/create.ts";
export { default as managedAgentDeploymentCreate } from "./commands/managed-agent/deployment/create.ts";
export { default as managedAgentAgentGet } from "./commands/managed-agent/agent/get.ts";
export { default as managedAgentAgentList } from "./commands/managed-agent/agent/list.ts";
export { default as managedAgentAgentSearch } from "./commands/managed-agent/agent/search.ts";
export { default as managedAgentAgentVersions } from "./commands/managed-agent/agent/versions.ts";
export { default as managedAgentEnvironmentGet } from "./commands/managed-agent/environment/get.ts";
export { default as managedAgentEnvironmentList } from "./commands/managed-agent/environment/list.ts";
export { default as managedAgentEnvironmentSearch } from "./commands/managed-agent/environment/search.ts";
export { default as managedAgentSkillDownload } from "./commands/managed-agent/skill/download.ts";
export { default as managedAgentSkillGet } from "./commands/managed-agent/skill/get.ts";
export { default as managedAgentSkillSearch } from "./commands/managed-agent/skill/search.ts";
export { default as managedAgentSkillVersions } from "./commands/managed-agent/skill/versions.ts";
export { default as managedAgentVaultGet } from "./commands/managed-agent/vault/get.ts";
export { default as managedAgentVaultList } from "./commands/managed-agent/vault/list.ts";
export { default as managedAgentVaultSearch } from "./commands/managed-agent/vault/search.ts";
export { default as managedAgentDeploymentGet } from "./commands/managed-agent/deployment/get.ts";
export { default as managedAgentDeploymentList } from "./commands/managed-agent/deployment/list.ts";
export { default as managedAgentDeploymentPause } from "./commands/managed-agent/deployment/pause.ts";
export { default as managedAgentDeploymentRun } from "./commands/managed-agent/deployment/run.ts";
export { default as managedAgentDeploymentRunsGet } from "./commands/managed-agent/deployment/runs/get.ts";
export { default as managedAgentDeploymentRunsList } from "./commands/managed-agent/deployment/runs/list.ts";
export { default as managedAgentDeploymentSearch } from "./commands/managed-agent/deployment/search.ts";
export { default as managedAgentDeploymentUnpause } from "./commands/managed-agent/deployment/unpause.ts";
export {
managedAgentSessionArchive,
managedAgentSessionSearch,
managedAgentSessionUpdate,
} from "./commands/managed-agent/session-management.ts";
export {
managedAgentSessionEventList,
managedAgentSessionEventSend,
managedAgentSessionEventStream,
} from "./commands/managed-agent/session-event.ts";
export {
managedAgentSessionDebug,
managedAgentSessionExport,
} from "./commands/managed-agent/session-debug.ts";
export {
managedAgentFileDelete,
managedAgentFileDownload,
managedAgentFileGet,
managedAgentFileList,
managedAgentFileSearch,
managedAgentFileUpload,
} from "./commands/managed-agent/file.ts";
export { default as workspaceInit } from "./commands/workspace/init.ts";
export { default as pluginInstall } from "./commands/plugin/install.ts";
export { default as pluginLink } from "./commands/plugin/link.ts";
@@ -104,6 +104,20 @@ test("inject:base_url 已带后缀不重复拼;非空字面量 base_url 保留",
expect(literal.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio");
});
test("inject:目录项目可强制使用 Bailian CLI 解析出的 base_url", () => {
const providers = {
bailian: {
api_key: "from-project-env",
base_url: "https://project-env.example.com/api/v1/agentstudio",
},
};
injectProviderCredentials(providers, makeHost({ apiCred: bailianCred() }), {
overrideBaseUrl: true,
});
expect(providers.bailian.api_key).toBe("sk-auth-chain");
expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio");
});
test("inject:base_url 尾斜杠被规范化,不产生双斜杠", () => {
const providers = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(
@@ -166,6 +180,18 @@ test("assert:所有已声明 provider 的 key 非空时通过", () => {
).not.toThrow();
});
test("assert:定向检查不会被无关 provider 的空 key 阻塞", () => {
expect(() =>
assertProviderCredentials(
{
bailian: { api_key: "x" },
claude: { api_key: "" },
},
["bailian"],
),
).not.toThrow();
});
test("assert:claude key 为空抛 AUTH 且 hint 指向 ANTHROPIC_API_KEY", () => {
let thrown: unknown;
try {
@@ -0,0 +1,3 @@
{
"resources": []
}
@@ -22,3 +22,16 @@ agents:
instructions: |
You are a helpful assistant.
environment: dev
test-create:
name: test-create
model: qwen3.8-max
instructions: "111"
provider: bailian
tools:
builtin: []
mcp: []
mcp_servers: []
skills:
- type: custom
skill_id: skill_M2I0ZTZhOWJiOWY5NDFkMzk2YT
metadata: {}
@@ -9,8 +9,8 @@ import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
/**
* managed-agent e2e bl config / Profile /
* logout SDK 线 `managed-agent plan`
* state auth: "apiKey" + provider key
* `validate` / `state list` 线
* state auth: "apiKey" + Bailian key
* `validate` / `state list` 线
* BAILIAN_CONFIG_DIR
*/
@@ -52,11 +52,6 @@ function planArgs(file: string): string[] {
return ["managed-agent", "plan", "--file", file, "--quiet"];
}
/** 隔离宿主机的 ~/.agents/config.json避免它强制覆盖 provider 凭证 env。 */
function isolatedAgentsConfigEnv(): NodeJS.ProcessEnv {
return { AGENTS_CONFIG_PATH: join(tmpdir(), "bl-e2e-no-agents-config.json") };
}
/**
* state agents.yaml provider fixture
* agents.state.json state plan refresh
@@ -214,51 +209,24 @@ describe("e2e: managed-agent 鉴权分层(离线命令免登录 / 联网命令
expect(stderr).toMatch(/auth login|API key/i);
});
test("已登录 bailian 时,多 provider 配置下 plan --no-refresh 离线通过,不查其他 provider key (0)", async () => {
const env = {
...makeConfigEnv({ api_key: "sk-e2e-no-refresh" }),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "",
CLAUDE_API_KEY: "",
};
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--no-refresh"],
env,
);
expect(exitCode, stderr).toBe(0);
test("CLI 边界拒绝 agents.yaml 中的非 Bailian Provider (2)", async () => {
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML_MULTI), env);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/only supports provider 'bailian'/i);
expect(stderr).toContain("claude");
});
test("统一登录门禁:只配 claude key 未登录 bailian 时plan --provider claude 仍报 AUTH (3)", async () => {
const env = {
...makeConfigEnv({}),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "sk-ant-e2e-scope",
CLAUDE_API_KEY: "",
};
test("agent list 在发请求前拒绝多 Provider 配置,不再校验 Claude Key", async () => {
const env = makeConfigEnv({ api_key: "sk-e2e-bailian-only" });
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--provider", "claude"],
["managed-agent", "agent", "list", "--file", AGENTS_YAML_MULTI],
env,
);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/auth login|API key/i);
});
test("已登录但缺 claude key 时,全量断言拦住并给 ANTHROPIC_API_KEY hint (3)", async () => {
const env = {
...makeConfigEnv({ api_key: "sk-e2e-bailian-present" }),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "",
CLAUDE_API_KEY: "",
};
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--provider", "claude"],
env,
);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/ANTHROPIC_API_KEY/);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/only supports provider 'bailian'/i);
expect(stderr).not.toContain("ANTHROPIC_API_KEY");
});
});
@@ -0,0 +1,112 @@
import { mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { runNodeMain } from "e2e/runner";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { parseStdoutJson } from "./helpers.ts";
const directories: string[] = [];
async function temporaryDirectory() {
const directory = await realpath(await mkdtemp(join(tmpdir(), "bailian-yaml-init-")));
directories.push(directory);
return directory;
}
afterEach(async () => {
for (const directory of directories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});
async function runInit(directory: string, args: string[] = []) {
return runNodeMain(
fileURLToPath(new URL("./harness/main.ts", import.meta.url)),
["managed-agent", "init", ...args],
{
cwd: directory,
env: {
BAILIAN_CONFIG_DIR: await temporaryDirectory(),
BAILIAN_E2E_ROUTES: JSON.stringify([
{ path: "managed-agent init", export: "managedAgentInit" },
]),
},
},
);
}
describe("e2e: managed-agent init output path", () => {
test.each(["default", "relative", "absolute"] as const)(
"reports the absolute YAML path for a %s output path",
async (pathKind) => {
const directory = await temporaryDirectory();
const relativePath =
pathKind === "default" ? "agents.yaml" : join("config files", "custom agents.yaml");
const outputPath = join(directory, relativePath);
await mkdir(dirname(outputPath), { recursive: true });
const args =
pathKind === "default"
? []
: ["--file", pathKind === "absolute" ? outputPath : relativePath];
const result = await runInit(directory, args);
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout.split(/\r?\n/)[0]).toBe(`Created ${outputPath}`);
expect(await readFile(outputPath, "utf8")).toContain("agents:");
},
);
test("preserves the JSON output contract", async () => {
const directory = await temporaryDirectory();
const result = await runInit(directory, ["--file", "custom.yaml", "--output", "json"]);
expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson(result.stdout)).toEqual({
created: "custom.yaml",
provider: "bailian",
agent: "assistant",
});
expect((await stat(join(directory, "custom.yaml"))).isFile()).toBe(true);
});
test("keeps dry-run read-only without reporting a created file", async () => {
const directory = await temporaryDirectory();
const result = await runInit(directory, ["--dry-run", "--output", "json"]);
expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson(result.stdout)).toEqual({
would_create: "agents.yaml",
provider: "bailian",
agent: "assistant",
would_update_gitignore: true,
});
expect(await stat(join(directory, "agents.yaml")).catch(() => null)).toBeNull();
expect(await stat(join(directory, ".gitignore")).catch(() => null)).toBeNull();
});
test.each(["default", "relative", "absolute"] as const)(
"reports the absolute existing %s path without overwriting the YAML",
async (pathKind) => {
const directory = await temporaryDirectory();
const relativePath =
pathKind === "default" ? "agents.yaml" : join("config files", "custom agents.yaml");
const outputPath = join(directory, relativePath);
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, "Keep user configuration.\n");
const args =
pathKind === "default"
? []
: ["--file", pathKind === "absolute" ? outputPath : relativePath];
const result = await runInit(directory, args);
expect(result.exitCode).toBe(2);
expect(result.stderr).toContain(`${outputPath} already exists.`);
expect(result.stderr).toContain("Pass --force to overwrite.");
expect(result.stdout).not.toContain("Created ");
expect(await readFile(outputPath, "utf8")).toBe("Keep user configuration.\n");
},
);
});
@@ -0,0 +1,247 @@
import {
cp,
mkdir,
mkdtemp,
readFile,
realpath,
rm,
stat,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { runNodeMain } from "e2e/runner";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { parseStdoutJson, runCommandHelp } from "./helpers.ts";
const routes = {
"managed-agent project init": "managedAgentProjectInit",
"managed-agent project build": "managedAgentProjectBuild",
"managed-agent project publish": "managedAgentProjectPublish",
"managed-agent project validate": "managedAgentProjectValidate",
} as const;
const directories: string[] = [];
async function temporaryDirectory() {
const directory = await realpath(await mkdtemp(join(tmpdir(), "bailian-project-init-")));
directories.push(directory);
return directory;
}
afterEach(async () => {
for (const directory of directories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});
function runInit(directory: string, args: string[] = []) {
return runProject(directory, "init", args);
}
async function runProject(directory: string, subcommand: string, args: string[] = [], json = true) {
const configRoot = await temporaryDirectory();
return runNodeMain(
fileURLToPath(new URL("./harness/main.ts", import.meta.url)),
["managed-agent", "project", subcommand, ...args, ...(json ? ["--output", "json"] : [])],
{
cwd: directory,
env: {
BAILIAN_CONFIG_DIR: configRoot,
BAILIAN_E2E_ROUTES: JSON.stringify(
Object.entries(routes).map(([path, exportName]) => ({ path, export: exportName })),
),
},
},
);
}
describe("e2e: managed-agent project init directory defaults", () => {
test("build links copied resources and reports ambiguous environment bindings without writing", async () => {
const directory = await temporaryDirectory();
const initialized = await runInit(directory);
expect(initialized.exitCode, initialized.stderr).toBe(0);
const root = join(directory, "managed-agent");
for (const [section, id] of [
["skills", "example-skill"],
["files", "example-file"],
["environments", "example-env"],
["vaults", "example-vault"],
] as const) {
await cp(
join(root, "agents/assistant", section, "_examples", id),
join(root, "agents/assistant", section, id),
{ recursive: true },
);
}
const agentPath = join(root, "agents/assistant/agent.json");
const original = await readFile(agentPath, "utf8");
const preview = await runProject(root, "build", ["--dry-run"]);
expect(preview.exitCode, preview.stderr).toBe(0);
expect(parseStdoutJson<{ can_build: boolean }>(preview.stdout).can_build).toBe(true);
expect(await readFile(agentPath, "utf8")).toBe(original);
const built = await runProject(root, "build");
expect(built.exitCode, built.stderr).toBe(0);
const agent = JSON.parse(await readFile(agentPath, "utf8"));
expect(agent).toMatchObject({
environment: "example-env",
vault: "example-vault",
skills: ["example-skill"],
files: [{ file: "example-file", mount_path: "/mnt/example.md" }],
});
const alternatePath = join(root, "agents/assistant/environments/alternate");
await mkdir(alternatePath);
await writeFile(
join(alternatePath, "environment.json"),
JSON.stringify({ id: "alternate", config: { type: "cloud" } }),
);
delete agent.environment;
await writeFile(agentPath, JSON.stringify(agent));
const beforeConflict = await readFile(agentPath, "utf8");
const buildPath = join(root, ".openagentpack/build/agents.yaml");
const beforeBuild = await readFile(buildPath, "utf8");
for (const json of [false, true]) {
const conflict = await runProject(root, "build", [], json);
expect(conflict.exitCode).toBe(1);
expect(conflict.stderr).toContain("multiple local environment resources");
expect(conflict.stderr).toContain("Set 'environment' explicitly");
expect(conflict.stderr).not.toMatch(/\p{Script=Han}/u);
}
expect(await readFile(agentPath, "utf8")).toBe(beforeConflict);
expect(await readFile(buildPath, "utf8")).toBe(beforeBuild);
});
test("build checks directories and writes without confirmation while publish stays gated", async () => {
const directory = await temporaryDirectory();
const initialized = await runInit(directory);
expect(initialized.exitCode, initialized.stderr).toBe(0);
const root = join(directory, "managed-agent");
const nested = join(root, "agents/assistant/skills");
for (const args of [[], ["--dry-run"]]) {
const result = await runProject(nested, "build", args, false);
expect(result.exitCode, result.stderr).toBe(2);
expect(result.stderr).toContain("Not a project root:");
expect(result.stderr).not.toMatch(/\p{Script=Han}/u);
expect(result.stderr).toContain(`cd '${root}'`);
expect(result.stderr).not.toContain("high-risk");
expect(result.stderr).not.toContain("Usage:");
}
const explicit = await runProject(root, "build", ["--project", nested]);
expect(explicit.exitCode).toBe(2);
expect(explicit.stderr).toContain(`--project '${root}'`);
const noMarker = await runProject(directory, "build", [], false);
expect(noMarker.exitCode).toBe(2);
expect(noMarker.stderr).toContain("project.json");
expect(noMarker.stderr).not.toMatch(/\p{Script=Han}/u);
expect(noMarker.stderr).not.toContain("high-risk");
expect(await stat(join(nested, ".openagentpack")).catch(() => null)).toBeNull();
const preview = await runProject(root, "build", ["--dry-run"]);
expect(preview.exitCode, preview.stderr).toBe(0);
expect(await stat(join(root, ".openagentpack/build")).catch(() => null)).toBeNull();
const built = await runProject(root, "build");
expect(built.exitCode, built.stderr).toBe(0);
expect(built.stderr).not.toContain("requires_confirmation");
expect((await stat(join(root, ".openagentpack/build/agents.yaml"))).isFile()).toBe(true);
const explicitValid = await runProject(nested, "build", ["--project", root]);
expect(explicitValid.exitCode, explicitValid.stderr).toBe(0);
const storePath = join(root, ".openagentpack/versions/project/store.json");
const beforePublish = await readFile(storePath, "utf8");
const publish = await runProject(root, "publish");
expect(publish.exitCode).toBe(7);
expect(publish.stderr).toContain("requires_confirmation");
expect(await readFile(storePath, "utf8")).toBe(beforePublish);
});
test("only publish help includes confirmation; build keeps dry-run", async () => {
const build = await runCommandHelp(routes, ["managed-agent", "project", "build", "--help"]);
expect(build.stderr).not.toContain("--yes");
expect(build.stderr).toContain("--dry-run");
const publish = await runCommandHelp(routes, ["managed-agent", "project", "publish", "--help"]);
expect(publish.stderr).toContain("--yes");
const directory = await temporaryDirectory();
const removedFlag = await runProject(directory, "build", ["--yes"]);
expect(removedFlag.exitCode).toBe(2);
expect(removedFlag.stderr).toMatch(/Unknown flag.*--yes/);
});
test("nested build and validate explain the project root without changing directories", async () => {
const directory = await temporaryDirectory();
const initialized = await runInit(directory);
expect(initialized.exitCode, initialized.stderr).toBe(0);
const root = join(directory, "managed-agent");
const nested = join(root, "agents/assistant/skills");
for (const command of ["build", "validate"]) {
const result = await runProject(nested, command, ["--dry-run"]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("Not a project root:");
expect(result.stderr).not.toMatch(/\p{Script=Han}/u);
expect(result.stderr).toContain(`cd '${root}'`);
expect(result.stderr).toContain(`--project '${root}'`);
expect(result.stderr).not.toContain("ERR_MODULE_NOT_FOUND");
}
expect(await stat(join(nested, ".openagentpack")).catch(() => null)).toBeNull();
const corrected = await runProject(nested, "build", ["--project", root, "--dry-run"]);
expect(corrected.exitCode, corrected.stderr).toBe(0);
});
test("help describes the subdirectory default and explicit in-place initialization", async () => {
const result = await runCommandHelp(routes, ["managed-agent", "project", "init", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stderr).toContain("./managed-agent");
expect(result.stderr).toContain("--project .");
});
test("default dry-run reports the child directory without creating it", async () => {
const directory = await temporaryDirectory();
const result = await runInit(directory, ["--dry-run"]);
expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson(result.stdout)).toEqual({ would_initialize_project: "./managed-agent" });
expect(await stat(join(directory, "managed-agent")).catch(() => null)).toBeNull();
});
test("initializes only the child, ignores parent source, and rejects repeated init without overwriting", async () => {
const directory = await temporaryDirectory();
const parentYaml = "not a valid project declaration";
await writeFile(join(directory, "agents.yaml"), parentYaml);
if (process.platform !== "win32") {
await symlink("missing-instructions.md", join(directory, "CLAUDE.md"));
}
const result = await runInit(directory);
expect(result.exitCode, result.stderr).toBe(0);
const projectRoot = join(directory, "managed-agent");
const initialized = parseStdoutJson<{
project_root: string;
baseline_version: string;
converted_from_yaml: boolean;
}>(result.stdout);
expect(initialized.project_root).toBe(projectRoot);
expect(initialized.baseline_version).toHaveLength(64);
expect(initialized.converted_from_yaml).toBe(false);
expect(await stat(join(directory, "project.json")).catch(() => null)).toBeNull();
expect(await readFile(join(directory, "agents.yaml"), "utf8")).toBe(parentYaml);
expect((await stat(join(projectRoot, "agents/assistant/agent.json"))).isFile()).toBe(true);
expect(
(await stat(join(projectRoot, ".openagentpack/versions/project/store.json"))).isFile(),
).toBe(true);
const instructions = join(projectRoot, "agents/assistant/instructions.md");
await writeFile(instructions, "user changes");
const repeated = await runInit(directory);
expect(repeated.exitCode).not.toBe(0);
expect(repeated.stderr).toContain("already exists");
expect(await readFile(instructions, "utf8")).toBe("user changes");
});
test("explicit paths, including dot, remain exact project roots", async () => {
for (const target of ["custom-agent", "."]) {
const directory = await temporaryDirectory();
const result = await runInit(directory, ["--project", target]);
expect(result.exitCode, result.stderr).toBe(0);
expect(parseStdoutJson<{ project_root: string }>(result.stdout).project_root).toBe(
join(directory, target),
);
expect(await stat(join(directory, "managed-agent")).catch(() => null)).toBeNull();
}
});
});
File diff suppressed because it is too large Load Diff
@@ -188,6 +188,18 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"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 rm": "managedAgentStateRm",
"managed-agent state import": "managedAgentStateImport",
@@ -196,7 +208,52 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"managed-agent session delete": "managedAgentSessionDelete",
"managed-agent session run": "managedAgentSessionRun",
"managed-agent session send": "managedAgentSessionSend",
"managed-agent session list": "managedAgentSessionList",
"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",
};
export const KNOWLEDGE_KB_LIST_ROUTES: E2eRouteExports = {
@@ -0,0 +1,74 @@
import { expect, test } from "vite-plus/test";
import { createZip, redactSensitiveValues } from "../src/commands/managed-agent/session-debug.ts";
import { searchCursorPages } from "../src/commands/managed-agent/_engine/api-helpers.ts";
import { buildSkillListJsonResult } from "../src/commands/managed-agent/skill/list.ts";
test("searchCursorPages preserves opaque cursors and marks truncated scans", async () => {
const cursors: Array<string | undefined> = [];
const result = await searchCursorPages(
async (page) => {
cursors.push(page);
if (!page) return { items: ["one"], hasMore: true, nextPage: "opaque-A" };
return { items: ["two"], hasMore: true, nextPage: "opaque-B" };
},
(item) => item.includes("o"),
2,
);
expect(cursors).toEqual([undefined, "opaque-A"]);
expect(result.items).toEqual(["one", "two"]);
expect(result.truncated).toBe(true);
expect(result.nextPage).toBe("opaque-B");
});
test("legacy skill-list JSON keeps source alongside pagination fields", () => {
expect(
buildSkillListJsonResult("official", {
items: [{ id: "skill-official" }],
hasMore: true,
nextPage: "opaque-next",
}),
).toEqual({
source: "official",
skills: [{ id: "skill-official" }],
has_more: true,
next_page: "opaque-next",
});
});
test("session export ZIP is valid-shaped and redacts credential-like values", () => {
const redacted = redactSensitiveValues({
api_key: "secret",
nested: {
authorization: "Bearer secret",
normal: "kept",
tool_input: "https://example.test/private?X-Amz-Signature=must-not-leak",
download_url:
"https://example.test/private?X-Amz-Credential=credential&X-Amz-Signature=must-not-leak",
json_url:
'{"url":"https://example.test/private?X-Amz-Signature=must-not-leak","status":"ok"}',
message: 'request={"token":"must-not-leak"} Authorization: Bearer must-not-leak',
},
});
expect(redacted).toEqual({
api_key: "[REDACTED]",
nested: {
authorization: "[REDACTED]",
normal: "kept",
tool_input: "[REDACTED]",
download_url:
"https://example.test/private?X-Amz-Credential=[REDACTED]&X-Amz-Signature=[REDACTED]",
json_url: '{"url":"https://example.test/private?X-Amz-Signature=[REDACTED]","status":"ok"}',
message: 'request={"token":"[REDACTED]"} Authorization: [REDACTED]',
},
});
const zip = createZip([
{ name: "manifest.json", content: new TextEncoder().encode('{"schema_version":1}\n') },
{ name: "events.json", content: new TextEncoder().encode("[]\n") },
]);
expect(Array.from(zip.slice(0, 4))).toEqual([0x50, 0x4b, 0x03, 0x04]);
const decoded = new TextDecoder().decode(zip);
expect(decoded).toContain("manifest.json");
expect(decoded).toContain("events.json");
expect(Array.from(zip.slice(-22, -18))).toEqual([0x50, 0x4b, 0x05, 0x06]);
});
@@ -0,0 +1,179 @@
import { chmod, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildAgentDecl, StateManager } from "@openagentpack/sdk";
import { expect, test } from "vite-plus/test";
import {
buildAgentSkillRefs,
normalizeAgentKey,
replaceConfigAtomically,
selectAgentKey,
selectLocalSkillKey,
} from "../src/commands/managed-agent/agent/create.ts";
import {
normalizeResourceKey,
parseMetadata,
} from "../src/commands/managed-agent/_engine/scoped-create.ts";
function candidate(instructions = "help") {
return buildAgentDecl(undefined, {
name: "Assistant",
model: "qwen3.8-max",
instructions,
provider: "bailian",
}).agent;
}
test("Agent key 从显示名生成并保留 Unicode", () => {
expect(normalizeAgentKey(" Data Assistant ")).toBe("data-assistant");
expect(normalizeAgentKey("数据分析 助手")).toBe("数据分析-助手");
expect(normalizeAgentKey("***")).toBe("agent");
});
test("通用资源 key 与 metadata 输入保持稳定", () => {
expect(normalizeResourceKey(" Production Vault ", "vault")).toBe("production-vault");
expect(normalizeResourceKey("生产 环境", "environment")).toBe("生产-环境");
expect(normalizeResourceKey("***", "vault")).toBe("vault");
expect(parseMetadata(["owner=platform", "empty="])).toEqual({
owner: "platform",
empty: "",
});
expect(() => parseMetadata(["missing-separator"])).toThrow(/Invalid metadata/);
});
test("同名已跟踪 Agent 分配递增 key", () => {
const state = StateManager.initialize("/tmp/bailian-cli-agent-create-state.json");
state.setResource({
address: { type: "agent", name: "assistant", provider: "bailian" },
remote_id: "agent_existing",
content_hash: "hash",
});
expect(
selectAgentKey({
displayName: "Assistant",
provider: "bailian",
agents: {
assistant: candidate(),
"assistant-2": { ...candidate(), instructions: "other" },
},
candidate: candidate(),
state,
}),
).toEqual({ key: "assistant-3", reusedPending: false });
});
test("远端失败后复用完全相同的待创建声明", () => {
const state = StateManager.initialize("/tmp/bailian-cli-agent-create-pending-state.json");
expect(
selectAgentKey({
displayName: "Assistant",
provider: "bailian",
agents: { assistant: candidate() },
candidate: candidate(),
state,
}),
).toEqual({ key: "assistant", reusedPending: true });
});
test("同名待创建声明配置不同则创建新的逻辑 key", () => {
const state = StateManager.initialize("/tmp/bailian-cli-agent-create-different-state.json");
expect(
selectAgentKey({
displayName: "Assistant",
provider: "bailian",
agents: { assistant: candidate("old") },
candidate: candidate("new"),
state,
}),
).toEqual({ key: "assistant-2", reusedPending: false });
});
test("Agent Skill ID 默认生成 custom 外部引用并去重", () => {
expect(buildAgentSkillRefs(["skill_report", "skill_report", "skill_chart"])).toEqual([
{ type: "custom", skill_id: "skill_report" },
{ type: "custom", skill_id: "skill_chart" },
]);
});
test("Agent Skill ID 可生成 official 外部引用", () => {
expect(buildAgentSkillRefs(["skill_pptx"], "official")).toEqual([
{ type: "official", skill_id: "skill_pptx" },
]);
expect(() => buildAgentSkillRefs([" "])).toThrow(/must not be empty/);
});
test("本地 Skill 来源复用同声明的待创建或已跟踪 key", () => {
const declaration = {
name: "local-report",
source: "skills/local-report.zip",
origin: "custom",
provider: "bailian",
};
const pendingState = StateManager.initialize("/tmp/bailian-cli-local-skill-pending.json");
expect(
selectLocalSkillKey({
displayName: "local-report",
provider: "bailian",
declarations: { "local-report": declaration },
candidate: declaration,
state: pendingState,
}),
).toEqual({
key: "local-report",
needsCreate: true,
reusedPending: true,
reusedTracked: false,
});
const trackedState = StateManager.initialize("/tmp/bailian-cli-local-skill-tracked.json");
trackedState.setResource({
address: { type: "skill", name: "local-report", provider: "bailian" },
remote_id: "skill_local_report",
content_hash: "hash",
});
expect(
selectLocalSkillKey({
displayName: "local-report",
provider: "bailian",
declarations: { "local-report": declaration },
candidate: declaration,
state: trackedState,
}),
).toEqual({
key: "local-report",
needsCreate: false,
reusedPending: false,
reusedTracked: true,
});
expect(() =>
selectLocalSkillKey({
displayName: "local-report",
provider: "bailian",
declarations: { "local-report": declaration },
candidate: { ...declaration, source: "skills/new-local-report.zip" },
state: trackedState,
}),
).toThrow(/already tracked with a different declaration/);
});
test("YAML 替换校验原内容并保留文件权限", async () => {
const directory = await mkdtemp(join(tmpdir(), "bailian-agent-create-"));
const configPath = join(directory, "agents.yaml");
try {
await writeFile(configPath, "version: old\n", "utf8");
await chmod(configPath, 0o640);
await replaceConfigAtomically(configPath, "version: old\n", "version: new\n");
expect(await readFile(configPath, "utf8")).toBe("version: new\n");
expect((await stat(configPath)).mode & 0o777).toBe(0o640);
await expect(
replaceConfigAtomically(configPath, "version: stale\n", "version: overwritten\n"),
).rejects.toThrow(/changed while resource create was being prepared/);
expect(await readFile(configPath, "utf8")).toBe("version: new\n");
} finally {
await rm(directory, { recursive: true, force: true });
}
});
@@ -1,7 +1,11 @@
import { UserError } from "@openagentpack/sdk";
import { BailianError, ExitCode } from "bailian-cli-core";
import { expect, test } from "vite-plus/test";
import { withAgentErrors } from "../src/commands/managed-agent/_engine/errors.ts";
import {
formatAgentDiagnosticFailure,
retainAgentError,
withAgentErrors,
} from "../src/commands/managed-agent/_engine/errors.ts";
/**
* Structural stand-in for the SDK's internal `ApiError` (not exported by the
@@ -116,3 +120,45 @@ test("fetch transport TypeError is rethrown untouched for the runtime NETWORK ma
expect(error).toBe(transportError);
}
});
test("diagnostic failure surfaces the first error and reports the remaining count", () => {
const message = formatAgentDiagnosticFailure(
[
{ severity: "warning", code: "warning.code", message: "warning" },
{ severity: "error", code: "first.code", message: "first failure" },
{ severity: "error", code: "second.code", message: "second failure" },
{ severity: "error", code: "third.code", message: "third failure" },
],
"fallback",
);
expect(message).toBe("[first.code] first failure (+2 more errors)");
});
test("diagnostic failure uses its fallback only when no error detail exists", () => {
expect(
formatAgentDiagnosticFailure(
[{ severity: "warning", code: "warning.code", message: "warning" }],
"fallback",
),
).toBe("fallback");
});
test("retaining an Agent error adds a hint without losing structured context", () => {
const cause = new Error("provider cause");
const original = new BailianError("provider failure", ExitCode.GENERAL, undefined, {
api: { httpStatus: 500, apiCode: "PROVIDER_FAILURE", requestId: "req-retain" },
rawResponse: "raw provider response",
cause,
});
const retained = retainAgentError(original, "retry the create", "fallback");
expect(retained).not.toBe(original);
expect(retained.message).toBe("provider failure");
expect(retained.exitCode).toBe(ExitCode.GENERAL);
expect(retained.hint).toBe("retry the create");
expect(retained.api).toEqual(original.api);
expect(retained.rawResponse).toBe("raw provider response");
expect(retained.cause).toBe(cause);
});
@@ -0,0 +1,139 @@
import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
commitPreparedProjectVersion,
disableProjectVersioning,
enableProjectVersioning,
getProjectVersionStatus,
listProjectVersions,
prepareProjectVersion,
previewProjectVersion,
restoreProjectVersion,
} from "@openagentpack/sdk/project-versions";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { playgroundBrowserTargetFromSummary } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
const temporaryDirectories: string[] = [];
afterEach(async () => {
for (const directory of temporaryDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});
describe("managed-agent local snapshot versions", () => {
test("uses the shared path-scoped switch and stores full YAML outside store.json", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
const siblingDirectory = join(root, "nested");
const siblingConfigPath = join(siblingDirectory, "agents.yaml");
await mkdir(siblingDirectory);
await writeFile(configPath, projectYaml("First"));
await writeFile(siblingConfigPath, projectYaml("Second"));
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
expect(enabled.versioning.enabled).toBe(true);
expect(enabled.version?.message).toBe("Enable Bailian CLI versioning");
expect((await getProjectVersionStatus(siblingConfigPath)).enabled).toBe(false);
const storeSource = await readFile(join(root, ".openagentpack/versions/store.json"), "utf8");
expect(storeSource).not.toContain("instructions: First");
const snapshotSource = await readFile(
join(root, ".openagentpack/versions/blobs", `${enabled.version!.source_hash}.yaml`),
"utf8",
);
expect(snapshotSource).toContain("instructions: First");
await writeFile(configPath, projectYaml("First updated"));
const repeated = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
expect(repeated.version?.message).toBe("Enable Bailian CLI versioning");
expect((await listProjectVersions(configPath)).versions).toHaveLength(2);
const disabled = await disableProjectVersioning(configPath);
expect(disabled.enabled).toBe(false);
});
test("auto-snapshots after success and restores without changing history or permissions", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
await writeFile(configPath, projectYaml("Version one"));
await chmod(configPath, 0o640);
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
const firstVersion = enabled.version!.version_id;
const secondSource = projectYaml("Version two");
await writeFile(configPath, secondSource);
const prepared = await prepareProjectVersion(configPath, secondSource);
const version = await commitPreparedProjectVersion(prepared!);
const currentVersionBeforeRestore = (await getProjectVersionStatus(configPath)).head_version;
expect(version?.message).toBe("Apply agents.yaml");
const preview = await previewProjectVersion(configPath, firstVersion);
expect(preview.can_restore).toBe(true);
expect(preview.after_yaml).toContain("Version one");
await restoreProjectVersion(configPath, firstVersion, {
headVersion: preview.base_head_version,
sourceRevision: preview.base_source_revision,
});
expect(await readFile(configPath, "utf8")).toContain("Version one");
expect((await getProjectVersionStatus(configPath)).head_version).toBe(
currentVersionBeforeRestore,
);
expect((await stat(configPath)).mode & 0o777).toBe(0o640);
});
test("rejects abbreviated version IDs and plaintext credentials", async () => {
const root = await temporaryDirectory();
const configPath = join(root, "agents.yaml");
await writeFile(configPath, projectYaml("Safe"));
const enabled = await enableProjectVersioning(configPath, "Enable Bailian CLI versioning");
await expect(previewProjectVersion(configPath, enabled.version!.short_version)).rejects.toThrow(
/full 64-character hexadecimal/i,
);
await disableProjectVersioning(configPath);
await writeFile(
configPath,
projectYaml("Unsafe").replace("qoder: {}", "qoder:\n api_key: plaintext-secret"),
);
await expect(
enableProjectVersioning(configPath, "Enable Bailian CLI versioning"),
).rejects.toThrow(/environment variable reference/i);
});
});
describe("managed-agent Workbench policy", () => {
test("Session Preview opens the requested Agent or falls back to Workbench", () => {
const summary = {
status: "valid",
agents: [{ agent: { id: "assistant" } }, { agent: { id: "reviewer" } }],
};
expect(
playgroundBrowserTargetFromSummary("http://localhost:4848", summary, "reviewer"),
).toEqual({ url: "http://localhost:4848/agents/reviewer/preview" });
expect(playgroundBrowserTargetFromSummary("http://localhost:4848", summary)).toEqual(
expect.objectContaining({ url: "http://localhost:4848", warning: expect.any(String) }),
);
});
});
async function temporaryDirectory(): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), "bailian-cli-local-versions-"));
temporaryDirectories.push(directory);
return directory;
}
function projectYaml(instructions: string): string {
return `version: "1"
providers:
qoder: {}
defaults:
provider: qoder
agents:
assistant:
model: ultimate
instructions: ${instructions}
`;
}
@@ -0,0 +1,137 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, test, vi } from "vite-plus/test";
const registry = vi.hoisted(() => ({
version: '"0.7.0"',
error: null as Error | null,
manifest: "",
query: vi.fn(),
}));
vi.mock("node:child_process", async (importOriginal) => ({
...(await importOriginal<typeof import("node:child_process")>()),
execFile: (
command: string,
args: string[],
options: unknown,
callback: (error: Error | null, output: { stdout: string }) => void,
) => {
registry.query(command, args, options);
callback(registry.error, { stdout: registry.version });
},
}));
vi.mock("node:module", () => ({
createRequire: () => ({ resolve: () => registry.manifest }),
}));
import { resolveLauncher } from "../src/commands/managed-agent/_engine/playground-launcher.ts";
let directory: string;
let binary: string;
beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), "playground-launcher-"));
binary = join(directory, "playground.js");
registry.manifest = join(directory, "package.json");
registry.version = '"0.7.0"';
registry.error = null;
registry.query.mockClear();
await writeFile(binary, "");
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN", "");
vi.stubEnv("AGENTS_PLAYGROUND_BIN", "");
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION", "");
vi.spyOn(process, "cwd").mockReturnValue(directory);
});
afterEach(async () => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
await rm(directory, { recursive: true, force: true });
});
async function install(version: string) {
await writeFile(registry.manifest, JSON.stringify({ version, bin: "playground.js" }));
}
test("checks latest on every resolution and reuses only the matching installed version", async () => {
await install("0.7.0");
expect(await resolveLauncher()).toMatchObject({
args: [binary],
version: "0.7.0",
fetched: false,
});
registry.version = '"0.8.0"';
expect(await resolveLauncher()).toMatchObject({
command: "npx",
args: ["-y", "@openagentpack/playground@0.8.0"],
version: "0.8.0",
fetched: true,
});
expect(registry.query).toHaveBeenCalledTimes(2);
expect(registry.query).toHaveBeenCalledWith(
"npm",
expect.arrayContaining([
"view",
"@openagentpack/playground@latest",
"--prefer-online",
"--fetch-timeout=10000",
]),
expect.objectContaining({ timeout: 15_000 }),
);
});
test("downloads the resolved exact version when local installation is old or missing", async () => {
for (const version of ["0.6.0", undefined]) {
if (version) await install(version);
else await rm(registry.manifest);
expect(await resolveLauncher()).toMatchObject({
command: "npx",
args: ["-y", "@openagentpack/playground@0.7.0"],
});
}
});
test("explicit version overrides an incompatible installed version", async () => {
await install("0.6.0");
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_VERSION", "0.7.0");
expect(await resolveLauncher()).toMatchObject({ fetched: true, version: "0.7.0" });
expect(registry.query.mock.calls[0]?.[1]).toContain("@openagentpack/playground@0.7.0");
});
test("explicit binary remains an offline development override", async () => {
vi.stubEnv("BAILIAN_MANAGED_AGENT_PLAYGROUND_BIN", binary);
expect(await resolveLauncher()).toMatchObject({ args: [binary], fetched: false });
expect(registry.query).not.toHaveBeenCalled();
});
test("registry failure does not silently reuse an old installation", async () => {
await install("0.6.0");
registry.error = new Error("registry unavailable");
await expect(resolveLauncher()).rejects.toThrow("registry unavailable");
});
test("rejects malformed or ambiguous registry metadata with localized diagnostics", async () => {
for (const output of ["invalid", '["0.6.0","0.7.0"]', '"--unsafe"']) {
registry.version = output;
await expect(resolveLauncher()).rejects.toThrow("single valid Playground version");
await expect(resolveLauncher()).rejects.toThrow("唯一有效的 Playground 版本号");
}
});
test("local source builds must also match the resolved version", async () => {
const packageRoot = join(directory, "packages/playground");
const sourceBinary = join(packageRoot, "dist/bin/playground.js");
await mkdir(join(packageRoot, "dist/bin"), { recursive: true });
await writeFile(sourceBinary, "");
await writeFile(join(packageRoot, "package.json"), JSON.stringify({ version: "0.6.0" }));
expect(await resolveLauncher()).toMatchObject({ fetched: true });
await writeFile(join(packageRoot, "package.json"), JSON.stringify({ version: "0.7.0" }));
expect(await resolveLauncher()).toMatchObject({
args: [sourceBinary],
version: "0.7.0",
fetched: false,
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-core",
"version": "1.19.0",
"version": "1.22.0",
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
+3 -1
View File
@@ -287,8 +287,10 @@ export interface Command<F extends FlagsDef = FlagsDef> {
* Cross-flag validation, after parsing and before run. Return an error message
* UsageError; undefined to pass. Single-flag `required` is enforced by the
* parser use this for rules spanning flags or depending on a flag's *value*.
* May be async for read-only local preflight. Runtime awaits it before auth
* and confirmation. Do not perform remote requests or local writes here.
*/
validate?: (flags: ParsedFlags<F>) => string | undefined;
validate?: (flags: ParsedFlags<F>) => string | undefined | Promise<string | undefined>;
run: (ctx: CommandContext<F>) => Promise<void>;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "knowledge-studio-cli",
"version": "1.19.0",
"version": "1.22.0",
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
"keywords": [
"alibaba-cloud",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-runtime",
"version": "1.19.0",
"version": "1.22.0",
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
+1 -1
View File
@@ -217,7 +217,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
parsedFlags,
Object.keys(res.command.flags ?? {}),
) as ParsedFlags<FlagsDef>;
const invalid = res.command.validate?.(ownFlags);
const invalid = await res.command.validate?.(ownFlags);
if (invalid) throw new UsageError(invalid);
// 校验通过 → 建源、解析 settings、组 ctx,进中间件执行命令。
+5 -5
View File
@@ -189,8 +189,8 @@ importers:
packages/commands:
dependencies:
'@openagentpack/sdk':
specifier: 0.3.2
version: 0.3.2
specifier: 0.7.1
version: 0.7.1
bailian-cli-core:
specifier: workspace:*
version: link:../core
@@ -1124,8 +1124,8 @@ packages:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
'@openagentpack/sdk@0.3.2':
resolution: {integrity: sha512-FvMHtIMIVLASGlt5q4kYSpD7BK+LWwN8vwnsckmD1SvPlWc9wayzA04DXU98IM74Ep5/HAqIKmNL8JAMQoeX1g==}
'@openagentpack/sdk@0.7.1':
resolution: {integrity: sha512-OENks8UdfWFanvhJIVbqyUX1+XGavHnrxDiFx4SoJoGRXi45cOgyYtWWbG5VdDI2I5Dpo9eGNaw/JtDnkdxUcg==}
engines: {node: '>=18.17.0'}
'@oxc-project/runtime@0.129.0':
@@ -4287,7 +4287,7 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@openagentpack/sdk@0.3.2':
'@openagentpack/sdk@0.7.1':
dependencies:
jszip: 3.10.1
yaml: 2.9.0
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-cli
metadata:
version: "1.19.0"
version: "1.22.0"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-finetune
metadata:
version: "1.19.0"
version: "1.22.0"
requires:
bins: ["bl"]
description: >-
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-gen
metadata:
version: "1.19.0"
version: "1.22.0"
requires:
bins: ["bl"]
description: >-
+112 -11
View File
@@ -1,14 +1,16 @@
---
name: bailian-managed-agent
metadata:
version: "1.19.0"
version: "1.22.0"
requires:
bins: ["bl"]
description: >-
阿里云百炼托管 Agent 声明式基础设施入口用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、
创建/更新/销毁百炼托管 Agent 或 Deployment、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用
`bl managed-agent`。以 agents.yaml 为唯一事实源做 IaCinit 建脚手架、validate 离线校验、plan 预览 diff、
apply / destroy 变更远端资源且受统一高风险确认闸门保护,务必先 plan 给用户看 diff 再让其确认,禁止自动添加 `--yes`
阿里云百炼托管 Agent 声明式基础设施与 API 命令入口用户要创建agent、初始化 agents.yaml、校验或预览配置变更、
创建/更新/销毁托管 Agent 或 Deployment、在 Workbench 编辑和调试目录项目、管理本地快照版本,或查询
Agent/Environment/Skill/Vault/Deployment、管理 Session/Event/File、运行/暂停 Deployment 时使用
`bl managed-agent`。持久资源仍以 agents.yaml 为唯一事实源做 IaC公开 API 能力按资源透出
list/get/search/versions/download、数据面和运行时动作命令。apply / destroy 与破坏性 API 命令受统一高风险确认闸门保护;
务必先展示预览再让用户确认,禁止自动添加 `--yes`
反触发:调用已上线的百炼应用/智能体走 bailian-app-call 或 `bl app`;宿主 agent 自身的记忆、技能、
子代理不走本 skill生图生视频走 bailian-gen。
官方安装:`bl skill init`(与共享协议 bailian-protocol 同装)。
@@ -20,13 +22,20 @@ description: >-
## Safety guardrail (the most important rule)
`apply` / `destroy` **mutate remote resources** and add a domain-specific preview requirement on top of the shared high-risk confirmation protocol:
`apply` / `destroy` and single-resource `create --yes` **mutate persistent remote resources**:
1. Always run `bl managed-agent plan` first and show the diff to the user.
2. Ask the user to confirm the exact action and scope shown in the plan.
3. Only then run `apply` / `destroy` with `--yes`; a changed plan requires confirmation again.
1. For `agents.yaml` resource changes, always run `bl managed-agent plan` first and show the diff to the user.
2. A single-resource create command previews its scoped plan when `--yes` is absent; show that preview before retrying it with `--yes`.
3. Only after explicit user confirmation, retry `apply` / `destroy` / single-resource create with `--yes`.
4. Never add `--yes` on your own initiative before the user has confirmed.
`state rm`, `session delete`, and future `risk: high` commands follow the shared protocol.
API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault / Deployment 的 create 命令仍通过
`agents.yaml → scoped plan → scoped apply` 管理;查询命令和 Session、Event、File、Deployment 运行时动作直接调用 API。
`session archive|delete``file delete``deployment run|pause|unpause` 也需要先 `--dry-run`,确认后才传 `--yes`
`state rm`, `session archive|delete`, `file delete`, `deployment run|pause|unpause`, and future
`risk: high` commands follow the shared protocol: show the risk message and exact scope, then wait
for explicit confirmation before re-running with `--yes`.
## IaC lifecycle
@@ -39,10 +48,77 @@ description: >-
6. Destroy bl managed-agent destroy --yes # separate explicit confirmation
```
## Directory projects, Workbench, and local versions
| Intent | Command |
| --------------------------------------- | ------------------------------------------------------ |
| Create or convert a directory project | `bl managed-agent project init` |
| Validate and Build directory source | `bl managed-agent project validate` / `build` |
| Publish the current immutable Build | `bl managed-agent project publish --yes` |
| Launch project resource editing | `bl managed-agent project workbench` |
| Launch one Agent Session Preview | `bl managed-agent playground --agent <id>` |
| Enable/disable project versions | `bl managed-agent project version enable` / `disable` |
| Inspect local version state and history | `bl managed-agent project version status` / `list` |
| Preview or restore project source | `bl managed-agent project version preview` / `restore` |
- Bailian CLI and Workbench use the same `.openagentpack/versions/project` store and enable switch. Git is not required.
- Directory projects always use Bailian. `project.json` does not declare a Provider; Build supplies the Bailian Provider configuration automatically.
- Fresh `project init` includes complete Skill/File/Vault/Environment examples with bilingual README files under `agents/assistant/<resource-type>/_examples/`. They are not linked in `agent.json` and are excluded from Build discovery, Workbench declarations, and remote Publish. Copy a resource outside `_examples/` and configure its Agent reference to use it. Examples remain local versioned source; never put real secrets into them. / 新项目的四类资源示例默认不启用、不发布;请按 README 复制到 `_examples/` 外再配置引用,不要向示例写入真实密钥。
- `project init`, `validate`, `build`, and version commands are local-only. Publish and Workbench resolve credentials from Bailian CLI flags, shell environment, or the active Profile; project initialization does not write credentials into the project directory.
- Build is local-only. Publish never runs Build implicitly and consumes only a current `.openagentpack/build/agents.yaml` plus manifest.
- Build moves literal Vault `secret_value` / `access_token` values from Agent-local or shared `vault.json` into project-root `.env`, replacing them with generated environment references. Existing references and `.env` entries are preserved; conflicts receive suffixed variable names. Preview/dry-run never write or print secrets. Publish and Workbench read the selected project's root `.env` as a fallback to inherited environment variables, even when invoked elsewhere. `.env` is private plaintext storage, excluded from local versions but not automatically ignored by Git; keep it backed up securely.
- Build 会将 Agent 本地或共享 `vault.json` 中的明文密钥移入项目根目录 `.env`,再写回环境变量引用;保留已有引用和变量,重名时生成后缀。预览不写文件或输出密钥。`.env` 不进入版本快照,也不加密;请自行备份并加入 Git 忽略规则。
- Agent-local File and Skill content supports Build-time association. A File may be copied directly into `agents/<agent>/files/`, or placed in `agents/<agent>/files/<id>/` when that directory contains exactly one content file; Build generates `file.json` and a `/mnt/<filename>` entry in `agent.json.files`. A directory under `agents/<agent>/skills/<id>/` containing `SKILL.md` generates `skill.json` and its `agent.json.skills` entry. Explicit JSON always wins; shared root resources remain explicit. Resources referenced by multiple Agents are promoted to the corresponding root shared directory during Build.
- A successful Publish versions the canonical YAML and the complete project source tree, including Skill scripts/assets and binary files. Remote State is never versioned or restored.
- `project version restore` restores source files to the working directory, invalidates Build, and does not move version history or remote State.
- `managed-agent playground` remains the standalone `agents.yaml` Session Preview path; directory Workbench is only under `managed-agent project workbench`.
## Scoped single-resource create
以下命令都先构造 `agents.yaml` 声明,再通过 SDK 的定向 Plan/Apply 创建远端资源,不绕过 State
| Resource | Command |
| --------------------- | ------------------------------------------ |
| Agent | `bl managed-agent agent create` |
| Environment | `bl managed-agent environment create` |
| Custom Skill | `bl managed-agent skill create` |
| Empty Vault | `bl managed-agent vault create` |
| Credential in a Vault | `bl managed-agent vault credential create` |
| Deployment | `bl managed-agent deployment create` |
- 用户只提供资源 `name`CLI 自动生成稳定的 YAML 逻辑 key同名资源用递增后缀并存。Credential 追加到指定 Vault不单独生成 key。
- `agent create --skill <id>` 直接绑定已存在的远端 Skill不要求顶层 `skills` 声明;默认写为 `type: custom`,平台 Skill 显式增加 `--type official`。同一次命令中的全部 `--skill` 共用该类型。
- `agent create --skill-dir <path>` 接收可重复的本地 Skill 目录或 ZIPCLI 从其中的 `SKILL.md` 读取 name自动生成顶层 `skills.<key>` 声明,并把该 key 写入 Agent 的 `skills` 列表;定向 Apply 会先上传 custom Skill再创建 Agent。`--type` 只作用于 `--skill <id>`,不改变本地 Skill 的 custom 类型。
- Environment 和 Vault 属于 Session/Deployment 运行时绑定,不是 Agent 创建参数;在 `session create|run``deployment create` 中传入。
- 默认只预览自动 key 和定向计划;`--dry-run` 完全离线,只有显式 `--yes` 才写 YAML 并创建远端资源。
- 定向流程只刷新目标资源及其传递依赖;无关资源不检测 Drift、不产生 action也不阻塞。
- 目标资源必须是 `create`,相关依赖必须已经处于 `no-op`;项目级 Drift 和删除仍由全量 `plan/apply` 处理。
- 远端创建失败时保留 YAML 声明;修复相关依赖或 Provider 错误后,重复相同命令会复用待创建 key。
- `skill create` 接受本地目录、ZIP 或单个 `SKILL.md`;远程 URL 仍需手工声明到 YAML再执行全量 Apply。
### Credential secret input
`vault credential create --secret-env <ENV_NAME>` 中的参数是变量名,不是 Secret 明文。Secret 可来自 Shell export、CI Secret 注入或可选的 `.env`CLI 会从当前目录向上自动加载最近的 `.env`,用户不必创建该文件。
```bash
export PROD_API_TOKEN="..."
bl managed-agent vault credential create \
--vault production \
--name api-token \
--secret-name API_TOKEN \
--secret-env PROD_API_TOKEN
```
- YAML 只保存 `secret_value: ${PROD_API_TOKEN}`,输出、诊断和 State 都不保存明文。
- 不要提交 `.env`;若使用 `.env`,先确认项目 `.gitignore` 已忽略它。
- 预览后再带 `--yes` 重试。后续执行全量 Apply 时也必须提供同名环境变量。
具体 flags、usage 和 examples 以 `reference/` 或对应命令的 `--help` 为准。
## Deployment as IaC
Deployment 与 Agent 一样声明在 `agents.yaml` 中,并复用同一条 `validate → plan → apply → destroy` IaC 链路;
CLI 不提供绕过 state 的命令式 Deployment CRUD。最小配置:
`deployment create` 可追加一条声明并走定向 ApplyCLI 不提供绕过 state 的 Deployment create/update/delete。最小配置:
```yaml
deployments:
@@ -69,6 +145,31 @@ deployments:
| List session event history | `bl managed-agent session events` |
| Delete a session | `bl managed-agent session delete` |
规范路径是 `session event list|send|stream``session events` 保留为 `session event list` 的兼容别名。
Managed Agents 的子线程通过 Event 中的 `session_thread_id` 暴露;公开 API 当前没有独立 Thread 资源 CRUD
不要构造 `session thread list|get|archive|events` 命令。
## API-oriented resource commands
| Intent | Command family |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| Discover agents and versions | `agent list`, `agent get`, `agent search`, `agent versions` |
| Discover environments | `environment list`, `environment get`, `environment search` |
| Discover skills and download a version | `skill list`, `skill get`, `skill search`, `skill versions`, `skill download` |
| Inspect vault envelopes | `vault list`, `vault get`, `vault search` |
| Inspect deployments and run history | `deployment list`, `deployment get`, `deployment search`, `deployment runs list`, `deployment runs get` |
| Run or pause deployments | `deployment run`, `deployment pause`, `deployment unpause` |
| Manage session metadata/lifecycle | `session list`, `session get`, `session search`, `session update`, `session archive`, `session delete` |
| Work with raw events | `session event send`, `session event list`, `session event stream` |
| Diagnose/export a session | `session debug`, `session export` |
| Work with files | `file upload`, `file list`, `file get`, `file search`, `file download`, `file delete` |
- 所有 Cursor 都是不透明字符串:只回传 `next_page`,不得转换为数字页码。
- 客户端搜索默认最多扫描 10 页;需要扩大范围时显式传 `--page-limit`。Deployment 搜索直接映射服务端 `keyword`
- 下载必须给出 `--output-file`;默认不覆盖已有文件,只有用户确认后才可加 `--force`
- `session export` 只导出诊断元数据,不含 File 正文,并会脱敏凭证类字段。
- 公开 Managed Agents API 没有模型 Catalog也没有 MCP OAuth LoginCLI 不注册对应命令。
## Local state management
| Intent | Command |
+77 -22
View File
@@ -9,31 +9,86 @@ Use this index for the skill-scoped quick index and global flags.
## Quick index
| Command | Authentication | Description | Detail |
| --------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | No Auth | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | API Key | List event history for a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
| Command | Authentication | Description | Detail |
| ------------------------------------------ | -------------- | ---------------------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | [managed-agent.md](managed-agent.md) |
| `bl managed-agent agent get` | API Key | Get a Managed Agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent agent list` | API Key | List Managed Agents | [managed-agent.md](managed-agent.md) |
| `bl managed-agent agent search` | API Key | Search Managed Agents | [managed-agent.md](managed-agent.md) |
| `bl managed-agent agent versions` | API Key | List Managed Agent versions | [managed-agent.md](managed-agent.md) |
| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment create` | API Key | Declare and create one Managed Agent Deployment through a scoped YAML apply | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | [managed-agent.md](managed-agent.md) |
| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent environment create` | API Key | Declare and create one Managed Agent Environment through a scoped YAML apply | [managed-agent.md](managed-agent.md) |
| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | [managed-agent.md](managed-agent.md) |
| `bl managed-agent environment list` | API Key | List Managed Agent environments | [managed-agent.md](managed-agent.md) |
| `bl managed-agent environment search` | API Key | Search Managed Agent environments | [managed-agent.md](managed-agent.md) |
| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | [managed-agent.md](managed-agent.md) |
| `bl managed-agent file download` | API Key | Download Managed Agent file content | [managed-agent.md](managed-agent.md) |
| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | [managed-agent.md](managed-agent.md) |
| `bl managed-agent file list` | API Key | List Managed Agent files | [managed-agent.md](managed-agent.md) |
| `bl managed-agent file search` | API Key | Search Managed Agent files | [managed-agent.md](managed-agent.md) |
| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | No Auth | Create an agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent playground` | API Key | Launch a Session Preview for an agents.yaml Agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project build` | No Auth | Organize directory source and generate the immutable Publish Build | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project init` | No Auth | Create a directory project or convert the local agents.yaml | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project publish` | API Key | Publish the current directory-project Build and record a version | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project validate` | No Auth | Validate a directory Agent project | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project version disable` | No Auth | Disable directory project versions | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project version enable` | No Auth | Enable directory project versions | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project version list` | No Auth | List directory project versions | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project version preview` | No Auth | Preview a directory project version | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project version restore` | No Auth | Restore a version to the project working directory | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project version status` | No Auth | Show directory project version status | [managed-agent.md](managed-agent.md) |
| `bl managed-agent project workbench` | API Key | Launch the directory project Workbench | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | API Key | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | API Key | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | API Key | List events for a Managed Agent session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | API Key | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | API Key | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session search` | API Key | Search Managed Agent sessions | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session update` | API Key | Update a Managed Agent session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill create` | API Key | Declare and create one custom Managed Agent Skill from a local source | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill list` | API Key | List Managed Agent skills | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill search` | API Key | Search Managed Agent skills | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | API Key | List Managed Agent skills | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | No Auth | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
| `bl managed-agent vault create` | API Key | Declare and create one empty Managed Agent Vault through a scoped YAML apply | [managed-agent.md](managed-agent.md) |
| `bl managed-agent vault credential create` | API Key | Append and create one environment-variable Credential in a tracked Vault | [managed-agent.md](managed-agent.md) |
| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | [managed-agent.md](managed-agent.md) |
| `bl managed-agent vault list` | API Key | List Managed Agent vaults | [managed-agent.md](managed-agent.md) |
| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | [managed-agent.md](managed-agent.md) |
## By group
| Group | Commands | Reference |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) |
| Group | Commands | Reference |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `deployment create`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment create`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `playground`, `project build`, `project init`, `project publish`, `project validate`, `project version disable`, `project version enable`, `project version list`, `project version preview`, `project version restore`, `project version status`, `project workbench`, `session archive`, `session create`, `session debug`, `session delete`, `session event list`, `session event send`, `session event stream`, `session events`, `session export`, `session get`, `session list`, `session run`, `session search`, `session send`, `session update`, `skill create`, `skill download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault create`, `vault credential create`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) |
## Global flags
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: bailian-protocol
metadata:
version: "1.19.0"
version: "1.22.0"
requires:
bins: ["bl"]
description: >-

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