From 69c1fb9f085aa8f5d3b6125419087f56dc4230db Mon Sep 17 00:00:00 2001 From: chenanran555 Date: Mon, 24 Aug 2026 20:15:09 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(managed-agent):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=89=98=E7=AE=A1=20Agent=20=E5=85=A8=E9=9D=A2=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=BD=E4=BB=A4=E9=9B=86=E5=8F=8A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 CLI 命令,新增托管 Agent 相关全部操作命令包括能力、Agent、环境、技能、保管库、部署、会话及文件管理 - 实现 Agent 列表、详情、版本、搜索等功能 - 实现环境列表、详情及搜索功能 - 实现部署列表、详情、搜索、运行、暂停与恢复功能 - 实现会话的搜索、更新、归档及事件操作功能 - 实现文件上传、列表、下载、删除及搜索功能 - 添加托管 Agent API 操作能力展示命令 - 统一异常处理及输出格式支持,支持 JSON 与表格展示 - 完善分页、查询参数及权限验证逻辑,支持按需筛选与翻页 - 新增输入输出文件支持与控制台保护机制,提升命令行体验和稳定性 --- packages/cli/src/commands.ts | 75 + .../managed-agent/_engine/api-helpers.ts | 201 ++ .../managed-agent/_engine/output-file.ts | 68 + .../managed-agent/_engine/pagination.ts | 3 +- .../src/commands/managed-agent/agent.ts | 236 +++ .../commands/managed-agent/capabilities.ts | 49 + .../src/commands/managed-agent/deployment.ts | 471 +++++ .../src/commands/managed-agent/environment.ts | 174 ++ .../src/commands/managed-agent/file.ts | 327 ++++ .../commands/managed-agent/session-debug.ts | 317 ++++ .../commands/managed-agent/session-delete.ts | 22 +- .../commands/managed-agent/session-event.ts | 249 +++ .../commands/managed-agent/session-events.ts | 100 +- .../commands/managed-agent/session-list.ts | 76 +- .../managed-agent/session-management.ts | 246 +++ .../src/commands/managed-agent/skill-list.ts | 117 +- .../src/commands/managed-agent/skill.ts | 363 ++++ .../src/commands/managed-agent/vault.ts | 165 ++ packages/commands/src/index.ts | 55 + .../tests/e2e/managed-agent.e2e.test.ts | 79 + packages/commands/tests/e2e/topic-routes.ts | 40 + .../tests/managed-agent-api-helpers.test.ts | 41 + skills/bailian-managed-agent/SKILL.md | 42 +- .../bailian-managed-agent/reference/index.md | 82 +- .../reference/managed-agent.md | 1686 +++++++++++++++-- 25 files changed, 4900 insertions(+), 384 deletions(-) create mode 100644 packages/commands/src/commands/managed-agent/_engine/api-helpers.ts create mode 100644 packages/commands/src/commands/managed-agent/_engine/output-file.ts create mode 100644 packages/commands/src/commands/managed-agent/agent.ts create mode 100644 packages/commands/src/commands/managed-agent/capabilities.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment.ts create mode 100644 packages/commands/src/commands/managed-agent/environment.ts create mode 100644 packages/commands/src/commands/managed-agent/file.ts create mode 100644 packages/commands/src/commands/managed-agent/session-debug.ts create mode 100644 packages/commands/src/commands/managed-agent/session-event.ts create mode 100644 packages/commands/src/commands/managed-agent/session-management.ts create mode 100644 packages/commands/src/commands/managed-agent/skill.ts create mode 100644 packages/commands/src/commands/managed-agent/vault.ts create mode 100644 packages/commands/tests/managed-agent-api-helpers.test.ts diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 27422935b..f5cff6b69 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -151,6 +151,43 @@ import { managedAgentSessionSend, managedAgentSessionEvents, managedAgentSkillList, + managedAgentCapabilities, + 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. @@ -312,6 +349,44 @@ export const commands: Record = { "managed-agent session send": managedAgentSessionSend, "managed-agent session events": managedAgentSessionEvents, "managed-agent skill-list": managedAgentSkillList, + "managed-agent capabilities": managedAgentCapabilities, + "managed-agent agent list": managedAgentAgentList, + "managed-agent agent get": managedAgentAgentGet, + "managed-agent agent search": managedAgentAgentSearch, + "managed-agent agent versions": managedAgentAgentVersions, + "managed-agent environment list": managedAgentEnvironmentList, + "managed-agent environment get": managedAgentEnvironmentGet, + "managed-agent environment search": managedAgentEnvironmentSearch, + "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 list": managedAgentVaultList, + "managed-agent vault get": managedAgentVaultGet, + "managed-agent vault search": managedAgentVaultSearch, + "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, }; /** diff --git a/packages/commands/src/commands/managed-agent/_engine/api-helpers.ts b/packages/commands/src/commands/managed-agent/_engine/api-helpers.ts new file mode 100644 index 000000000..5fce1c7e7 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/api-helpers.ts @@ -0,0 +1,201 @@ +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: "", + description: { + "en-US": "Config file path (default: agents.yaml)", + "zh-CN": "配置文件路径(默认:agents.yaml)", + }, + }, + provider: { + type: "string", + valueHint: "", + description: { "en-US": "Target provider", "zh-CN": "目标 Provider" }, + }, +} satisfies FlagsDef; + +export const CURSOR_FLAGS = { + limit: { + type: "number", + valueHint: "", + description: { + "en-US": "Page size (1-100)", + "zh-CN": "单页数量(1-100)", + }, + }, + page: { + type: "string", + valueHint: "", + 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: "", + required: true, + description: { + "en-US": "Case-insensitive text to find in IDs, names, and descriptions", + "zh-CN": "在 ID、名称和描述中进行不区分大小写的文本搜索", + }, + }, + pageLimit: { + type: "number", + valueHint: "", + 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 extends PagedResult { + scannedPages: number; + truncated: boolean; +} + +export async function searchCursorPages( + fetchPage: (page?: string) => Promise>, + matches: (item: T) => boolean, + pageLimit = 10, +): Promise> { + 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(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; +} diff --git a/packages/commands/src/commands/managed-agent/_engine/output-file.ts b/packages/commands/src/commands/managed-agent/_engine/output-file.ts new file mode 100644 index 000000000..6271a757e --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/output-file.ts @@ -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 { + return new Uint8Array(await readFile(resolve(path))); +} + +export async function readJsonInput(argument: string): Promise { + 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 { + 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"; +} diff --git a/packages/commands/src/commands/managed-agent/_engine/pagination.ts b/packages/commands/src/commands/managed-agent/_engine/pagination.ts index df2fd9343..23fe98b4f 100644 --- a/packages/commands/src/commands/managed-agent/_engine/pagination.ts +++ b/packages/commands/src/commands/managed-agent/_engine/pagination.ts @@ -8,8 +8,9 @@ export interface PagedResult { export async function fetchAllPages( fetchPage: (page?: string) => Promise>, all?: boolean, + initialPage?: string, ): Promise> { - const first = await fetchPage(); + const first = await fetchPage(initialPage); const items = [...first.items]; let hasMore = first.hasMore; let nextPage = first.nextPage; diff --git a/packages/commands/src/commands/managed-agent/agent.ts b/packages/commands/src/commands/managed-agent/agent.ts new file mode 100644 index 000000000..9289dee94 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/agent.ts @@ -0,0 +1,236 @@ +import type { CloudAgent } from "@openagentpack/sdk"; +import { getRemoteAgent, listRemoteAgents, listRemoteAgentVersions } 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, + INCLUDE_ARCHIVED_FLAG, + 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 { fetchAllPages } from "./_engine/pagination.ts"; + +const AGENT_LIST_FLAGS = { + ...API_TARGET_FLAGS, + ...CURSOR_FLAGS, + ...INCLUDE_ARCHIVED_FLAG, +}; + +const AGENT_SEARCH_FLAGS = { + ...API_TARGET_FLAGS, + limit: CURSOR_FLAGS.limit, + ...SEARCH_FLAGS, + ...INCLUDE_ARCHIVED_FLAG, +}; + +const AGENT_GET_FLAGS = { + ...API_TARGET_FLAGS, + agentId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Agent ID", "zh-CN": "Agent ID" }, + }, + agentVersion: { + type: "number", + valueHint: "", + description: { "en-US": "Specific agent version", "zh-CN": "指定 Agent 版本" }, + }, +} as const; + +const AGENT_VERSIONS_FLAGS = { + ...API_TARGET_FLAGS, + ...CURSOR_FLAGS, + agentId: AGENT_GET_FLAGS.agentId, +}; + +function agentRows(agents: CloudAgent[]): string[][] { + return agents.map((agent) => [ + agent.id, + displayValue(agent.name), + displayValue(agent.version), + displayValue(agent.type), + displayValue(agent.updated_at), + ]); +} + +export const managedAgentAgentList = defineCommand({ + description: { "en-US": "List Managed Agents", "zh-CN": "列出托管 Agent" }, + auth: "apiKey", + usageArgs: "[--limit ] [--page ] [--all] [--include-archived] [--file ]", + 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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentAgentGet = defineCommand({ + description: { "en-US": "Get a Managed Agent", "zh-CN": "获取托管 Agent 详情" }, + auth: "apiKey", + usageArgs: "--agent-id [--agent-version ] [--file ]", + 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: ctx.flags.provider, + 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)}`); + }, +}); + +export const managedAgentAgentSearch = defineCommand({ + description: { "en-US": "Search Managed Agents", "zh-CN": "搜索托管 Agent" }, + auth: "apiKey", + usageArgs: "--query [--limit ] [--page-limit ] [--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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentAgentVersions = defineCommand({ + description: { "en-US": "List Managed Agent versions", "zh-CN": "列出托管 Agent 版本" }, + auth: "apiKey", + usageArgs: "--agent-id [--limit ] [--page ] [--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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/capabilities.ts b/packages/commands/src/commands/managed-agent/capabilities.ts new file mode 100644 index 000000000..77e3167ae --- /dev/null +++ b/packages/commands/src/commands/managed-agent/capabilities.ts @@ -0,0 +1,49 @@ +import { getManagedAgentProviderCapabilities } from "@openagentpack/sdk"; +import { defineCommand, detectOutputFormat } from "bailian-cli-core"; +import { emitBare, emitResult, formatTable } from "bailian-cli-runtime"; + +const FLAGS = { + provider: { + type: "string", + valueHint: "", + description: { + "en-US": "Provider to inspect (default: bailian)", + "zh-CN": "要检查的 Provider(默认:bailian)", + }, + }, +} as const; + +export default defineCommand({ + description: { + "en-US": "Show operation-level Managed Agents API capabilities", + "zh-CN": "显示 Managed Agents API 的操作级能力", + }, + auth: "none", + usageArgs: "[--provider ]", + flags: FLAGS, + exampleArgs: ["", "--provider bailian --output json"], + notes: [ + { + "en-US": + "Capabilities distinguish public Managed Agents APIs from client-side compositions and unsupported resources.", + "zh-CN": "Capabilities 会区分公开 Managed Agents API、客户端组合能力和不支持的资源。", + }, + ], + async run(ctx) { + const format = detectOutputFormat(ctx.settings.output); + const capabilities = getManagedAgentProviderCapabilities(ctx.flags.provider ?? "bailian"); + if (format === "json") { + emitResult(capabilities, format); + return; + } + const rows = Object.entries(capabilities.operations).map(([operation, capability]) => [ + operation, + capability.supported ? "yes" : "no", + capability.auth ?? "-", + capability.reason ?? "-", + ]); + for (const line of formatTable(["OPERATION", "SUPPORTED", "AUTH", "REASON"], rows)) { + emitBare(line); + } + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment.ts b/packages/commands/src/commands/managed-agent/deployment.ts new file mode 100644 index 000000000..f74cc2262 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment.ts @@ -0,0 +1,471 @@ +import type { DeploymentInfo, DeploymentRunInfo, ProjectRuntimeContext } from "@openagentpack/sdk"; +import { + getRemoteDeployment, + getRemoteDeploymentRun, + listRemoteDeploymentRuns, + listRemoteDeployments, + runRemoteDeployment, + setRemoteDeploymentPaused, + UserError, +} 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, + INCLUDE_ARCHIVED_FLAG, + SEARCH_FLAGS, + 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"; + +const DEPLOYMENT_ID_FLAG = { + deploymentId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Deployment ID", "zh-CN": "Deployment ID" }, + }, +} as const; + +const LIST_FILTER_FLAGS = { + agentId: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by agent ID", "zh-CN": "按 Agent ID 筛选" }, + }, + status: { + type: "string", + valueHint: "", + choices: ["active", "paused"] as const, + description: { "en-US": "Filter by deployment status", "zh-CN": "按 Deployment 状态筛选" }, + }, + ...INCLUDE_ARCHIVED_FLAG, + createdAtGte: { + type: "string", + valueHint: "", + description: { + "en-US": "Created at or after this timestamp", + "zh-CN": "创建时间不早于该时间戳", + }, + }, + createdAtLte: { + type: "string", + valueHint: "", + description: { + "en-US": "Created at or before this timestamp", + "zh-CN": "创建时间不晚于该时间戳", + }, + }, +} as const; + +const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...LIST_FILTER_FLAGS }; +const GET_FLAGS = { ...API_TARGET_FLAGS, ...DEPLOYMENT_ID_FLAG }; +const SEARCH_RESOURCE_FLAGS = { + ...API_TARGET_FLAGS, + ...CURSOR_FLAGS, + query: SEARCH_FLAGS.query, + ...LIST_FILTER_FLAGS, +}; +const RUN_LIST_FLAGS = { ...API_TARGET_FLAGS, ...DEPLOYMENT_ID_FLAG, ...CURSOR_FLAGS }; +const RUN_GET_FLAGS = { + ...API_TARGET_FLAGS, + runId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Deployment run ID", "zh-CN": "Deployment Run ID" }, + }, +} as const; +const ACTION_TARGET_FLAGS = { + ...API_TARGET_FLAGS, + deployment: { + type: "string", + valueHint: "", + description: { + "en-US": "Logical deployment name in agents.yaml/state", + "zh-CN": "agents.yaml/State 中的逻辑 Deployment 名称", + }, + }, + deploymentId: { + type: "string", + valueHint: "", + description: { "en-US": "Direct deployment ID", "zh-CN": "直接指定 Deployment ID" }, + }, +} as const; +const RUN_ACTION_FLAGS = { + ...ACTION_TARGET_FLAGS, + yes: { + type: "switch", + description: { "en-US": "Confirm deployment run", "zh-CN": "确认运行 Deployment" }, + }, +} as const; + +function validateActionTarget(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; +} + +function deploymentRows(deployments: DeploymentInfo[]): string[][] { + return deployments.map((deployment) => [ + displayValue(deployment.id), + deployment.status, + displayValue(deployment.schedule?.expression), + displayValue(deployment.paused_reason?.type), + ]); +} + +function runRows(runs: DeploymentRunInfo[]): string[][] { + return runs.map((run) => [ + run.id, + displayValue(run.deployment_id), + displayValue(run.session_id), + displayValue(run.status), + displayValue(run.created_at), + ]); +} + +async function resolveDeploymentTarget( + runtime: ProjectRuntimeContext, + options: { provider?: string; deployment?: string; deploymentId?: string }, +): Promise { + if (options.deploymentId) return options.deploymentId; + const configuredProviders = Array.from(runtime.providers.keys()); + const provider = + options.provider ?? + (configuredProviders.length === 1 + ? configuredProviders[0] + : (() => { + throw new UserError("Multiple providers configured. Use --provider to specify one."); + })()); + const state = runtime.state.getResource({ + provider, + 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; +} + +export const managedAgentDeploymentList = defineCommand({ + description: { "en-US": "List Managed Agent deployments", "zh-CN": "列出托管 Agent Deployment" }, + auth: "apiKey", + usageArgs: "[--agent-id ] [--status active|paused] [--limit ] [--page ] [--all]", + flags: 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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentDeploymentGet = defineCommand({ + description: { + "en-US": "Get a Managed Agent deployment", + "zh-CN": "获取托管 Agent Deployment 详情", + }, + auth: "apiKey", + usageArgs: "--deployment-id ", + flags: 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: ctx.flags.provider, + }); + }), + ); + 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)}`); + } + }, +}); + +export const managedAgentDeploymentSearch = defineCommand({ + description: { + "en-US": "Search Managed Agent deployments", + "zh-CN": "搜索托管 Agent Deployment", + }, + auth: "apiKey", + usageArgs: "--query [--limit ] [--page ] [--all]", + flags: SEARCH_RESOURCE_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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentDeploymentRunsList = defineCommand({ + description: { + "en-US": "List runs for a Managed Agent deployment", + "zh-CN": "列出托管 Agent Deployment Run", + }, + auth: "apiKey", + usageArgs: "--deployment-id [--limit ] [--page ] [--all]", + flags: RUN_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: ctx.flags.provider, + 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: runRows(result.items), + hasMore: result.hasMore, + nextPage: result.nextPage, + emptyMessage: "No deployment runs found.", + }); + }, +}); + +export const managedAgentDeploymentRunsGet = defineCommand({ + description: { + "en-US": "Get a Managed Agent deployment run", + "zh-CN": "获取托管 Agent Deployment Run 详情", + }, + auth: "apiKey", + usageArgs: "--run-id ", + flags: RUN_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: ctx.flags.provider }); + }), + ); + 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)}`); + } + }, +}); + +export const managedAgentDeploymentRun = defineCommand({ + description: { + "en-US": "Run a Managed Agent deployment now", + "zh-CN": "立即运行托管 Agent Deployment", + }, + auth: "apiKey", + usageArgs: "(--deployment | --deployment-id ) --yes", + flags: RUN_ACTION_FLAGS, + exampleArgs: ["--deployment daily-report --dry-run", "--deployment-id dep_abc --yes"], + notes: CREDENTIALS_NOTE, + validate: validateActionTarget, + 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; + } + if (!ctx.flags.yes) { + throw new BailianError( + "Refusing to run the deployment without confirmation.", + ExitCode.USAGE, + "Re-run with --yes or preview with --dry-run.", + ); + } + 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: ctx.flags.provider }), + }; + }), + ); + if (format === "json") + emitResult({ deployment_id: result.deploymentId, ...result.run }, format); + else emitBare(`Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}`); + }, +}); + +function createPauseCommand(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", + usageArgs: "(--deployment | --deployment-id )", + flags: ACTION_TARGET_FLAGS, + exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc`], + notes: CREDENTIALS_NOTE, + validate: validateActionTarget, + 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: ctx.flags.provider, + }), + }; + }), + ); + if (format === "json") { + emitResult({ deployment_id: result.deploymentId, deployment: result.deployment }, format); + } else { + emitBare(`Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.`); + } + }, + }); +} + +export const managedAgentDeploymentPause = createPauseCommand(true); +export const managedAgentDeploymentUnpause = createPauseCommand(false); diff --git a/packages/commands/src/commands/managed-agent/environment.ts b/packages/commands/src/commands/managed-agent/environment.ts new file mode 100644 index 000000000..8ef2caa4d --- /dev/null +++ b/packages/commands/src/commands/managed-agent/environment.ts @@ -0,0 +1,174 @@ +import type { CloudEnvironment } from "@openagentpack/sdk"; +import { getRemoteEnvironment, listRemoteEnvironments } 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, + INCLUDE_ARCHIVED_FLAG, + 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 { fetchAllPages } from "./_engine/pagination.ts"; + +const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...INCLUDE_ARCHIVED_FLAG }; +const SEARCH_RESOURCE_FLAGS = { + ...API_TARGET_FLAGS, + limit: CURSOR_FLAGS.limit, + ...SEARCH_FLAGS, + ...INCLUDE_ARCHIVED_FLAG, +}; +const GET_FLAGS = { + ...API_TARGET_FLAGS, + environmentId: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Environment ID", "zh-CN": "Environment ID" }, + }, +} as const; + +function environmentRows(environments: CloudEnvironment[]): string[][] { + return environments.map((environment) => [ + environment.id, + displayValue(environment.name), + displayValue(environment.scope), + displayValue(environment.version), + displayValue(environment.updated_at), + ]); +} + +export const managedAgentEnvironmentList = defineCommand({ + description: { "en-US": "List Managed Agent environments", "zh-CN": "列出托管 Agent 环境" }, + auth: "apiKey", + usageArgs: "[--limit ] [--page ] [--all] [--include-archived]", + flags: 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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentEnvironmentGet = defineCommand({ + description: { "en-US": "Get a Managed Agent environment", "zh-CN": "获取托管 Agent 环境详情" }, + auth: "apiKey", + usageArgs: "--environment-id ", + flags: 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: ctx.flags.provider, + }); + }), + ); + 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)}`); + }, +}); + +export const managedAgentEnvironmentSearch = defineCommand({ + description: { "en-US": "Search Managed Agent environments", "zh-CN": "搜索托管 Agent 环境" }, + auth: "apiKey", + usageArgs: "--query [--limit ] [--page-limit ] [--include-archived]", + flags: SEARCH_RESOURCE_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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/file.ts b/packages/commands/src/commands/managed-agent/file.ts new file mode 100644 index 000000000..0e3037b51 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/file.ts @@ -0,0 +1,327 @@ +import { basename } from "node:path"; +import type { ProviderFileInfo } from "@openagentpack/sdk"; +import { + deleteFile, + downloadRemoteFile, + getFileInfo, + listRemoteFiles, + uploadFile, +} 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, + 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: "", + required: true, + description: { "en-US": "Remote file ID", "zh-CN": "远端 File ID" }, + }, +} as const; + +const SCOPE_ID_FLAG = { + scopeId: { + type: "string", + valueHint: "", + description: { "en-US": "Filter by scope ID", "zh-CN": "按 Scope ID 筛选" }, + }, +} as const; + +const UPLOAD_FLAGS = { + ...API_TARGET_FLAGS, + path: { + type: "string", + valueHint: "", + required: true, + description: { "en-US": "Local file path", "zh-CN": "本地文件路径" }, + }, + filename: { + type: "string", + valueHint: "", + description: { "en-US": "Remote filename override", "zh-CN": "覆盖远端文件名" }, + }, + mimeType: { + type: "string", + valueHint: "", + description: { "en-US": "MIME type override", "zh-CN": "覆盖 MIME 类型" }, + }, + purpose: { + type: "string", + valueHint: "", + description: { "en-US": "Provider upload purpose", "zh-CN": "Provider 上传用途" }, + }, +} 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: "", + 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, + yes: { + type: "switch", + description: { "en-US": "Confirm permanent file deletion", "zh-CN": "确认永久删除文件" }, + }, +} 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 [--filename ] [--mime-type ] [--purpose ]", + flags: UPLOAD_FLAGS, + exampleArgs: ["--path ./report.pdf", "--path ./data.json --purpose assistants"], + 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), + purpose: ctx.flags.purpose, + }, + 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: ctx.flags.provider, + mimeType: ctx.flags.mimeType ?? inferMimeType(ctx.flags.path), + purpose: ctx.flags.purpose, + }); + }), + ); + 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 ] [--limit ] [--page ] [--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: ctx.flags.provider, + 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 ", + 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: ctx.flags.provider }); + }), + ); + 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 [--scope-id ] [--limit ] [--page-limit ]", + 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: ctx.flags.provider, + 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 --output-file [--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: ctx.flags.provider }); + }), + ); + 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", + usageArgs: "--file-id --yes", + 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; + } + if (!ctx.flags.yes) { + throw new BailianError( + `Refusing to delete file ${ctx.flags.fileId} without confirmation.`, + ExitCode.USAGE, + "Re-run with --yes or preview with --dry-run.", + ); + } + await withAgentErrors(() => + withStdoutProtected(async () => { + const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); + await deleteFile(runtime, ctx.flags.fileId, { provider: ctx.flags.provider }); + }), + ); + if (format === "json") emitResult({ deleted: ctx.flags.fileId }, format); + else emitBare(`File ${ctx.flags.fileId} deleted.`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/session-debug.ts b/packages/commands/src/commands/managed-agent/session-debug.ts new file mode 100644 index 000000000..fc6d9c143 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/session-debug.ts @@ -0,0 +1,317 @@ +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: "", + 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: "", + 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; + 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 { + // 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_KEY = + /(api[_-]?key|access[_-]?key|secret|token|authorization|credential|password)/i; + +export function redactSensitiveValues(value: unknown): unknown { + if (Array.isArray(value)) return value.map(redactSensitiveValues); + 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 ", + 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, ctx.flags.provider); + }), + ); + 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 --output-file [--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, ctx.flags.provider); + }), + ); + 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)" : ""}`, + ); + } + }, +}); diff --git a/packages/commands/src/commands/managed-agent/session-delete.ts b/packages/commands/src/commands/managed-agent/session-delete.ts index 9cbd2ee9a..00faa687e 100644 --- a/packages/commands/src/commands/managed-agent/session-delete.ts +++ b/packages/commands/src/commands/managed-agent/session-delete.ts @@ -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 { deleteSession } from "@openagentpack/sdk"; import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; @@ -25,12 +31,16 @@ const SESSION_DELETE_FLAGS = { valueHint: "", description: { "en-US": "Target provider", "zh-CN": "目标 Provider" }, }, + yes: { + type: "switch", + description: { "en-US": "Confirm permanent session deletion", "zh-CN": "确认永久删除 Session" }, + }, } satisfies FlagsDef; export default defineCommand({ description: { "en-US": "Delete a session", "zh-CN": "删除 Session" }, auth: "apiKey", - usageArgs: "--session-id [--provider ] [--file ]", + usageArgs: "--session-id --yes [--provider ] [--file ]", flags: SESSION_DELETE_FLAGS, exampleArgs: ["--session-id sess_abc123"], notes: CREDENTIALS_NOTE, @@ -51,6 +61,14 @@ export default defineCommand({ return; } + if (!flags.yes) { + throw new BailianError( + `Refusing to delete session ${flags.sessionId} without confirmation.`, + ExitCode.USAGE, + "Re-run with --yes or preview with --dry-run.", + ); + } + await withAgentErrors(() => withStdoutProtected(async () => { const runtime = await buildAgentRuntime(ctx, file); diff --git a/packages/commands/src/commands/managed-agent/session-event.ts b/packages/commands/src/commands/managed-agent/session-event.ts new file mode 100644 index 000000000..21cd25544 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/session-event.ts @@ -0,0 +1,249 @@ +import type { ProviderSessionEvent, SessionEventInput } from "@openagentpack/sdk"; +import { + isTerminalSessionStatus, + listSessionEvents, + sendRemoteSessionEvents, + 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: "", + 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: "", + choices: ["asc", "desc"] as const, + description: { "en-US": "Event order: asc or desc", "zh-CN": "事件顺序:asc 或 desc" }, + }, + types: { + type: "string", + valueHint: "", + description: { + "en-US": "Comma-separated raw event types", + "zh-CN": "原始事件类型,多个以逗号分隔", + }, + }, + createdAtGte: { + type: "string", + valueHint: "", + description: { + "en-US": "Created at or after this timestamp", + "zh-CN": "创建时间不早于该时间戳", + }, + }, + createdAtLte: { + type: "string", + valueHint: "", + 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: "", + 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: "", + 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, + ); + } + 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 [--types ] [--order asc|desc] [--limit ] [--page ] [--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: ctx.flags.provider, + 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 --event ", + flags: EVENT_SEND_FLAGS, + exampleArgs: [ + '--session-id sess_abc --event \'{"type":"message","role":"user","content":"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: ctx.flags.provider, + }); + }), + ); + 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 [--after-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: ctx.flags.provider, + 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, + ); + } + }), + ); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/session-events.ts b/packages/commands/src/commands/managed-agent/session-events.ts index 84e15a6b5..e41d17970 100644 --- a/packages/commands/src/commands/managed-agent/session-events.ts +++ b/packages/commands/src/commands/managed-agent/session-events.ts @@ -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: "", - description: { "en-US": "Session ID (required)", "zh-CN": "Session ID(必填)" }, - required: true, - }, - file: { - type: "string", - valueHint: "", - description: { - "en-US": "Config file path (default: agents.yaml)", - "zh-CN": "配置文件路径(默认:agents.yaml)", - }, - }, - provider: { - type: "string", - valueHint: "", - description: { "en-US": "Target provider", "zh-CN": "目标 Provider" }, - }, - limit: { - type: "number", - valueHint: "", - 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 [--limit ] [--all] [--file ]", - 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"; diff --git a/packages/commands/src/commands/managed-agent/session-list.ts b/packages/commands/src/commands/managed-agent/session-list.ts index a87abd02f..b5a311ac8 100644 --- a/packages/commands/src/commands/managed-agent/session-list.ts +++ b/packages/commands/src/commands/managed-agent/session-list.ts @@ -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,6 +32,32 @@ const SESSION_LIST_FLAGS = { "zh-CN": "跟随 Cursor 获取全部分页", }, }, + limit: CURSOR_FLAGS.limit, + page: CURSOR_FLAGS.page, + statuses: { + type: "string", + valueHint: "", + description: { + "en-US": "Comma-separated session statuses", + "zh-CN": "Session 状态,多个以逗号分隔", + }, + }, + createdAtGte: { + type: "string", + valueHint: "", + description: { + "en-US": "Created at or after this RFC 3339 timestamp", + "zh-CN": "创建时间不早于该 RFC 3339 时间戳", + }, + }, + createdAtLte: { + type: "string", + valueHint: "", + description: { + "en-US": "Created at or before this RFC 3339 timestamp", + "zh-CN": "创建时间不晚于该 RFC 3339 时间戳", + }, + }, provider: { type: "string", valueHint: "", @@ -40,36 +71,51 @@ export default defineCommand({ "zh-CN": "列出 Provider 中的 Session", }, auth: "apiKey", - usageArgs: "[--agent ] [--all] [--provider ] [--file ]", + usageArgs: "[--agent ] [--statuses ] [--limit ] [--page ] [--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: flags.provider, + 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) { diff --git a/packages/commands/src/commands/managed-agent/session-management.ts b/packages/commands/src/commands/managed-agent/session-management.ts new file mode 100644 index 000000000..12101ad3e --- /dev/null +++ b/packages/commands/src/commands/managed-agent/session-management.ts @@ -0,0 +1,246 @@ +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: "", + 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: "", + description: { + "en-US": "Filter by configured agent name", + "zh-CN": "按配置中的 Agent 名称筛选", + }, + }, + statuses: { + type: "string", + valueHint: "", + description: { + "en-US": "Comma-separated session statuses", + "zh-CN": "Session 状态,多个以逗号分隔", + }, + }, + createdAtGte: { + type: "string", + valueHint: "", + description: { + "en-US": "Created at or after this timestamp", + "zh-CN": "创建时间不早于该时间戳", + }, + }, + createdAtLte: { + type: "string", + valueHint: "", + 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: "", + 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, + yes: { + type: "switch", + description: { "en-US": "Confirm session archive", "zh-CN": "确认归档 Session" }, + }, +} 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: ctx.flags.provider, + 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: ctx.flags.provider, + }); + }), + ); + 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", + usageArgs: "--session-id <id> --yes", + 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; + } + if (!ctx.flags.yes) { + throw new BailianError( + `Refusing to archive session ${ctx.flags.sessionId} without confirmation.`, + ExitCode.USAGE, + "Re-run with --yes or preview with --dry-run.", + ); + } + const session = await withAgentErrors(() => + withStdoutProtected(async () => { + const runtime = await buildAgentRuntime(ctx, ctx.flags.file ?? "agents.yaml"); + return archiveRemoteSession(runtime, ctx.flags.sessionId, { provider: ctx.flags.provider }); + }), + ); + if (format === "json") emitResult({ archived: ctx.flags.sessionId, session }, format); + else emitBare(`Session ${ctx.flags.sessionId} archived.`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/skill-list.ts b/packages/commands/src/commands/managed-agent/skill-list.ts index f8f77273b..ee42b96a4 100644 --- a/packages/commands/src/commands/managed-agent/skill-list.ts +++ b/packages/commands/src/commands/managed-agent/skill-list.ts @@ -1,115 +1,2 @@ -import { defineCommand, detectOutputFormat, type FlagsDef } from "bailian-cli-core"; -import { emitBare, emitResult, formatTable } from "bailian-cli-runtime"; -import { listSkills } from "@openagentpack/sdk"; -import { buildAgentRuntime, CREDENTIALS_NOTE } from "./_engine/config-loader.ts"; -import { withStdoutProtected } from "./_engine/console-capture.ts"; -import { withAgentErrors } from "./_engine/errors.ts"; - -const SKILL_SOURCES = ["custom", "official", "all"] as const; -type SkillSource = (typeof SKILL_SOURCES)[number]; - -const SKILL_LIST_FLAGS = { - file: { - type: "string", - valueHint: "<path>", - description: { - "en-US": "Config file path (default: agents.yaml)", - "zh-CN": "配置文件路径(默认:agents.yaml)", - }, - }, - source: { - type: "string", - valueHint: "<source>", - description: { - "en-US": - "Skill catalog: custom (workspace-uploaded, default), official (built-in), or all (both catalogs in one call)", - "zh-CN": - "Skill Catalog:custom(Workspace 上传,默认)、official(内置)或 all(一次调用获取两类 Catalog)", - }, - }, - provider: { - type: "string", - valueHint: "<name>", - description: { "en-US": "Target provider", "zh-CN": "目标 Provider" }, - }, -} satisfies FlagsDef; - -export default defineCommand({ - description: { - "en-US": "List skills from the provider's skill catalog", - "zh-CN": "列出 Provider Skill Catalog 中的 Skill", - }, - auth: "apiKey", - usageArgs: "[--source custom|official|all] [--provider <name>] [--file <path>]", - flags: SKILL_LIST_FLAGS, - exampleArgs: [ - "", - "--source official", - "--source all --output json", - "--source custom --provider bailian", - ], - notes: [ - ...CREDENTIALS_NOTE, - { - "en-US": "Providers without a skill listing API (e.g. ark) return an empty list.", - "zh-CN": "没有 Skill 列表 API 的 Provider(例如 ark)会返回空列表。", - }, - { - "en-US": - "For agent-driven skill selection, use `--source all --output json`: one call returns both catalogs with per-skill `source` and `description` fields to pick from.", - "zh-CN": - "由 Agent 选择 Skill 时,请使用 `--source all --output json`:一次调用返回两类 Catalog,并为每个 Skill 提供 `source` 和 `description` 字段用于选择。", - }, - { - "en-US": - "When generating a task that needs a suitable skill, call this command to match official or custom skills before wiring them into the task.", - "zh-CN": "生成需要合适 Skill 的任务时,先调用此命令匹配官方或自定义 Skill,再将其接入任务。", - }, - ], - validate: (f) => - f.source && !SKILL_SOURCES.includes(f.source as SkillSource) - ? "--source must be one of: custom, official, all." - : undefined, - async run(ctx) { - const { settings, flags } = ctx; - const format = detectOutputFormat(settings.output); - const file = flags.file ?? "agents.yaml"; - const source = (flags.source as SkillSource | undefined) ?? "custom"; - - const skills = await withAgentErrors(() => - withStdoutProtected(async () => { - const runtime = await buildAgentRuntime(ctx, file); - if (source !== "all") { - return listSkills(runtime, { provider: flags.provider, source }); - } - // Both catalogs in one call; each entry carries its own `source` field. - const [customSkills, officialSkills] = await Promise.all([ - listSkills(runtime, { provider: flags.provider, source: "custom" }), - listSkills(runtime, { provider: flags.provider, source: "official" }), - ]); - return [...customSkills, ...officialSkills]; - }), - ); - - if (format === "json") { - emitResult({ source, skills }, format); - return; - } - if (skills.length === 0) { - emitBare(source === "all" ? "No skills found." : `No ${source} skills found.`); - return; - } - - const headers = ["ID", "NAME", "SOURCE", "STATUS", "VERSION", "CREATED"]; - const rows = skills.map((skill) => [ - skill.id, - skill.name.slice(0, 32), - skill.source, - skill.status, - skill.latest_version ?? "-", - skill.created_at ?? "-", - ]); - for (const line of formatTable(headers, rows)) emitBare(line); - emitBare(`\nTotal: ${skills.length} (${source})`); - }, -}); +// Backward-compatible alias for the API-oriented `managed-agent skill list` command. +export { managedAgentSkillList as default } from "./skill.ts"; diff --git a/packages/commands/src/commands/managed-agent/skill.ts b/packages/commands/src/commands/managed-agent/skill.ts new file mode 100644 index 000000000..7a8b90a40 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill.ts @@ -0,0 +1,363 @@ +import type { ProviderSkillInfo, SkillVersionInfo } from "@openagentpack/sdk"; +import { + downloadRemoteSkill, + getRemoteSkill, + listRemoteSkills, + listRemoteSkillVersions, +} 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 { writeOutputFile } from "./_engine/output-file.ts"; +import { fetchAllPages } from "./_engine/pagination.ts"; + +const SKILL_SOURCES = ["custom", "official", "all"] as const; +type SkillSource = (typeof SKILL_SOURCES)[number]; + +const SOURCE_FLAG = { + source: { + type: "string", + valueHint: "<source>", + choices: SKILL_SOURCES, + description: { + "en-US": "Skill catalog: custom (default), official, or all", + "zh-CN": "Skill Catalog:custom(默认)、official 或 all", + }, + }, +} as const; + +const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SOURCE_FLAG }; +const SEARCH_RESOURCE_FLAGS = { + ...API_TARGET_FLAGS, + limit: CURSOR_FLAGS.limit, + ...SEARCH_FLAGS, + ...SOURCE_FLAG, +}; +const GET_FLAGS = { + ...API_TARGET_FLAGS, + skillId: { + type: "string", + valueHint: "<id>", + required: true, + description: { "en-US": "Skill ID", "zh-CN": "Skill ID" }, + }, +} as const; +const VERSIONS_FLAGS = { + ...GET_FLAGS, + ...CURSOR_FLAGS, +}; +const DOWNLOAD_FLAGS = { + ...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; + +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), + ]); +} + +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), + ]); +} + +async function listOneCatalog( + runtime: Parameters<typeof listRemoteSkills>[0], + source: Exclude<SkillSource, "all">, + options: { provider?: string; limit?: number; page?: string; all?: boolean }, +) { + return fetchAllPages( + async (page) => { + const response = await listRemoteSkills(runtime, { + provider: options.provider, + source, + limit: options.limit, + page, + }); + return { items: response.data, hasMore: response.has_more, nextPage: response.next_page }; + }, + options.all, + options.page, + ); +} + +export const managedAgentSkillList = 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: 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, { + provider: ctx.flags.provider, + limit: ctx.flags.limit, + page: ctx.flags.page, + all: ctx.flags.all, + }); + } + const [custom, official] = await Promise.all([ + listOneCatalog(runtime, "custom", { + provider: ctx.flags.provider, + limit: ctx.flags.limit, + all: ctx.flags.all, + }), + listOneCatalog(runtime, "official", { + provider: ctx.flags.provider, + limit: ctx.flags.limit, + all: ctx.flags.all, + }), + ]); + return { + items: [...custom.items, ...official.items], + hasMore: custom.hasMore || official.hasMore, + nextPage: undefined, + }; + }), + ); + 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.", + }); + }, +}); + +export const managedAgentSkillGet = defineCommand({ + description: { "en-US": "Get a Managed Agent skill", "zh-CN": "获取托管 Agent Skill 详情" }, + auth: "apiKey", + usageArgs: "--skill-id <id>", + flags: 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: ctx.flags.provider }); + }), + ); + 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)}`); + }, +}); + +export const managedAgentSkillSearch = 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: SEARCH_RESOURCE_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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentSkillVersions = 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: 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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentSkillDownload = 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: 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: ctx.flags.provider, + }); + }), + ); + 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}`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/vault.ts b/packages/commands/src/commands/managed-agent/vault.ts new file mode 100644 index 000000000..38e916f47 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/vault.ts @@ -0,0 +1,165 @@ +import type { CloudVault } from "@openagentpack/sdk"; +import { getRemoteVault, listRemoteVaults } 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, + INCLUDE_ARCHIVED_FLAG, + 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 { fetchAllPages } from "./_engine/pagination.ts"; + +const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...INCLUDE_ARCHIVED_FLAG }; +const SEARCH_RESOURCE_FLAGS = { + ...API_TARGET_FLAGS, + limit: CURSOR_FLAGS.limit, + ...SEARCH_FLAGS, + ...INCLUDE_ARCHIVED_FLAG, +}; +const GET_FLAGS = { + ...API_TARGET_FLAGS, + vaultId: { + type: "string", + valueHint: "<id>", + required: true, + description: { "en-US": "Vault ID", "zh-CN": "Vault ID" }, + }, +} as const; + +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), + ]); +} + +export const managedAgentVaultList = defineCommand({ + description: { "en-US": "List Managed Agent vaults", "zh-CN": "列出托管 Agent Vault" }, + auth: "apiKey", + usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]", + flags: 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: ctx.flags.provider, + 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.", + }); + }, +}); + +export const managedAgentVaultGet = defineCommand({ + description: { "en-US": "Get a Managed Agent vault", "zh-CN": "获取托管 Agent Vault 详情" }, + auth: "apiKey", + usageArgs: "--vault-id <id>", + flags: 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: ctx.flags.provider }); + }), + ); + 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)}`); + }, +}); + +export const managedAgentVaultSearch = 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: SEARCH_RESOURCE_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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 79e311ebf..6ec38d97e 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -148,6 +148,61 @@ 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 managedAgentCapabilities } from "./commands/managed-agent/capabilities.ts"; +export { + managedAgentAgentGet, + managedAgentAgentList, + managedAgentAgentSearch, + managedAgentAgentVersions, +} from "./commands/managed-agent/agent.ts"; +export { + managedAgentEnvironmentGet, + managedAgentEnvironmentList, + managedAgentEnvironmentSearch, +} from "./commands/managed-agent/environment.ts"; +export { + managedAgentSkillDownload, + managedAgentSkillGet, + managedAgentSkillSearch, + managedAgentSkillVersions, +} from "./commands/managed-agent/skill.ts"; +export { + managedAgentVaultGet, + managedAgentVaultList, + managedAgentVaultSearch, +} from "./commands/managed-agent/vault.ts"; +export { + managedAgentDeploymentGet, + managedAgentDeploymentList, + managedAgentDeploymentPause, + managedAgentDeploymentRun, + managedAgentDeploymentRunsGet, + managedAgentDeploymentRunsList, + managedAgentDeploymentSearch, + managedAgentDeploymentUnpause, +} from "./commands/managed-agent/deployment.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"; diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index fc6b05881..6bb31e620 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -199,9 +199,88 @@ describe("e2e: managed-agent", () => { expect(stderr).not.toMatch(/--source must be one of/i); expect(stderr).toMatch(/File not found.*agents\.e2e-missing\.yaml/i); }); + + test("managed-agent skill versions --help 展示 cursor 分页选项", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "skill", + "versions", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--skill-id|--limit|--page|--all/i); + }); }); describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => { + test("capabilities 明确区分 Session Event 与独立 Thread API", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "capabilities", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + operations?: Record<string, { supported?: boolean; reason?: string }>; + }>(stdout); + expect(data.operations?.["session.event.list"]?.supported).toBe(true); + expect(data.operations?.["session_thread.list"]?.supported).toBe(false); + expect(data.operations?.["session_thread.list"]?.reason).toMatch(/no independent Thread/i); + }); + + test.each([ + ["session archive", ["session", "archive", "--session-id", "sess_e2e"]], + ["session update", ["session", "update", "--session-id", "sess_e2e", "--title", "new"]], + [ + "session event send", + [ + "session", + "event", + "send", + "--session-id", + "sess_e2e", + "--event", + '{"type":"message","content":"hello"}', + ], + ], + [ + "session export", + ["session", "export", "--session-id", "sess_e2e", "--output-file", "debug.zip"], + ], + ["file upload", ["file", "upload", "--path", "missing.txt"]], + [ + "file download", + ["file", "download", "--file-id", "file_e2e", "--output-file", "artifact.bin"], + ], + ["file delete", ["file", "delete", "--file-id", "file_e2e"]], + [ + "skill download", + [ + "skill", + "download", + "--skill-id", + "skill_e2e", + "--skill-version", + "1", + "--output-file", + "skill.zip", + ], + ], + ["deployment run", ["deployment", "run", "--deployment-id", "dep_e2e"]], + ["deployment pause", ["deployment", "pause", "--deployment-id", "dep_e2e"]], + ["deployment unpause", ["deployment", "unpause", "--deployment-id", "dep_e2e"]], + ])("%s --dry-run 在构建 SDK runtime 前短路", async (_label, commandArgs) => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + ...commandArgs, + "--dry-run", + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + }); + test("init --dry-run 仅输出计划,不创建文件", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index fe249e8d7..5104072a9 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -194,7 +194,47 @@ 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 capabilities": "managedAgentCapabilities", + "managed-agent agent list": "managedAgentAgentList", + "managed-agent agent get": "managedAgentAgentGet", + "managed-agent agent search": "managedAgentAgentSearch", + "managed-agent agent versions": "managedAgentAgentVersions", + "managed-agent environment list": "managedAgentEnvironmentList", + "managed-agent environment get": "managedAgentEnvironmentGet", + "managed-agent environment search": "managedAgentEnvironmentSearch", + "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 list": "managedAgentVaultList", + "managed-agent vault get": "managedAgentVaultGet", + "managed-agent vault search": "managedAgentVaultSearch", + "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 = { diff --git a/packages/commands/tests/managed-agent-api-helpers.test.ts b/packages/commands/tests/managed-agent-api-helpers.test.ts new file mode 100644 index 000000000..ee468abd7 --- /dev/null +++ b/packages/commands/tests/managed-agent-api-helpers.test.ts @@ -0,0 +1,41 @@ +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"; + +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("session export ZIP is valid-shaped and redacts credential-like values", () => { + const redacted = redactSensitiveValues({ + api_key: "secret", + nested: { authorization: "Bearer secret", normal: "kept" }, + }); + expect(redacted).toEqual({ + api_key: "[REDACTED]", + nested: { authorization: "[REDACTED]", normal: "kept" }, + }); + + 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]); +}); diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 7277707b3..cf5005930 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -5,10 +5,10 @@ metadata: requires: bins: ["bl"] description: >- - 阿里云百炼托管 Agent 声明式基础设施入口:用户要创建agent、初始化 agents.yaml、校验或预览 agent 配置变更、 - 创建/更新/销毁百炼托管 Agent 或 Deployment、和托管 agent 对话、查会话事件历史、导入或取消跟踪远端资源时使用 - `bl managed-agent`。以 agents.yaml 为唯一事实源做 IaC:init 建脚手架、validate 离线校验、plan 预览 diff、 - apply / destroy 变更远端资源且必须带 `--yes`,务必先 plan 给用户看 diff 再让其确认。 + 阿里云百炼托管 Agent 声明式基础设施与 API 命令入口:用户要创建agent、初始化 agents.yaml、校验或预览配置变更、 + 创建/更新/销毁托管 Agent 或 Deployment,或查询 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,12 +20,16 @@ description: >- ## Safety guardrail (the most important rule) -`apply` / `destroy` **mutate remote resources** and only execute when `--yes` is passed: +`apply` / `destroy` **mutate persistent remote resources** and only execute when `--yes` is passed: -1. Always run `bl managed-agent plan` first and show the diff to the user. +1. For `agents.yaml` resource changes, always run `bl managed-agent plan` first and show the diff to the user. 2. Only after explicit user confirmation, retry `apply` / `destroy` with `--yes`. 3. Never add `--yes` on your own initiative before the user has confirmed. +API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault 的持久配置仍通过 +`agents.yaml → plan → apply` 管理;命令式写操作只覆盖 Session、Event、File 和 Deployment 运行时动作。 +`session archive|delete`、`file delete`、`deployment run` 也需要先 `--dry-run`,确认后才传 `--yes`。 + ## IaC lifecycle ``` @@ -66,6 +70,32 @@ 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 | +| -------------------------------------- | ------------------------------- | ------- | ------------------------------ | --------- | --------- | ------- | +| Check exact API support/auth/reason | `bl managed-agent capabilities` | +| Discover agents and versions | `agent list | get | search | versions` | +| Discover environments | `environment list | get | search` | +| Discover skills and download a version | `skill list | get | search | versions | download` | +| Inspect vault envelopes | `vault list | get | search` | +| Inspect deployments and run history | `deployment list | get | search`, `deployment runs list | get` | +| Run or pause deployments | `deployment run | pause | unpause` | +| Manage session metadata/lifecycle | `session list | get | search | update | archive | delete` | +| Work with raw events | `session event send | list | stream` | +| Diagnose/export a session | `session debug | export` | +| Work with files | `file upload | list | get | search | download | delete` | + +- 所有 Cursor 都是不透明字符串:只回传 `next_page`,不得转换为数字页码。 +- 客户端搜索默认最多扫描 10 页;需要扩大范围时显式传 `--page-limit`。Deployment 搜索直接映射服务端 `keyword`。 +- 下载必须给出 `--output-file`;默认不覆盖已有文件,只有用户确认后才可加 `--force`。 +- `session export` 只导出诊断元数据,不含 File 正文,并会脱敏凭证类字段。 +- 公开 Managed Agents API 没有模型 Catalog,也没有 MCP OAuth Login;以 `capabilities` 返回的 unsupported 原因为准。 + ## Local state management | Intent | Command | diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index a401eac0b..46f2a8721 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -9,31 +9,69 @@ 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 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 capabilities` | No Auth | Show operation-level Managed Agents API capabilities | [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 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 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 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 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 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 get`, `agent list`, `agent search`, `agent versions`, `apply`, `capabilities`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `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 download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) | ## Global flags diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index e3975b7e5..7836606b2 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -7,48 +7,742 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| --------------------------------- | -------------- | ------------------------------------------------------------- | -| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | -| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | -| `bl managed-agent init` | No Auth | Create a new agents.yaml template | -| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | -| `bl managed-agent session create` | API Key | Create a new session for an agent | -| `bl managed-agent session delete` | API Key | Delete a session | -| `bl managed-agent session events` | API Key | List event history for a session | -| `bl managed-agent session get` | API Key | Get details of a session | -| `bl managed-agent session list` | API Key | List sessions from the provider | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | -| `bl managed-agent skill-list` | API Key | List skills from the provider's skill catalog | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | -| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | -| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | -| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | +| Command | Authentication | Description | +| --------------------------------------- | -------------- | ------------------------------------------------------------- | +| `bl managed-agent agent get` | API Key | Get a Managed Agent | +| `bl managed-agent agent list` | API Key | List Managed Agents | +| `bl managed-agent agent search` | API Key | Search Managed Agents | +| `bl managed-agent agent versions` | API Key | List Managed Agent versions | +| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | +| `bl managed-agent capabilities` | No Auth | Show operation-level Managed Agents API capabilities | +| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | +| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | +| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | +| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | +| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | +| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | +| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | +| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | +| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | +| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | +| `bl managed-agent environment list` | API Key | List Managed Agent environments | +| `bl managed-agent environment search` | API Key | Search Managed Agent environments | +| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | +| `bl managed-agent file download` | API Key | Download Managed Agent file content | +| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | +| `bl managed-agent file list` | API Key | List Managed Agent files | +| `bl managed-agent file search` | API Key | Search Managed Agent files | +| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | +| `bl managed-agent init` | No Auth | Create a new agents.yaml template | +| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | +| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | +| `bl managed-agent session create` | API Key | Create a new session for an agent | +| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | +| `bl managed-agent session delete` | API Key | Delete a session | +| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | +| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | +| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | +| `bl managed-agent session events` | API Key | List events for a Managed Agent session | +| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | +| `bl managed-agent session get` | API Key | Get details of a session | +| `bl managed-agent session list` | API Key | List sessions from the provider | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | +| `bl managed-agent session search` | API Key | Search Managed Agent sessions | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | +| `bl managed-agent session update` | API Key | Update a Managed Agent session | +| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | +| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | +| `bl managed-agent skill list` | API Key | List Managed Agent skills | +| `bl managed-agent skill search` | API Key | Search Managed Agent skills | +| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | +| `bl managed-agent skill-list` | API Key | List Managed Agent skills | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | +| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | +| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | +| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | +| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | +| `bl managed-agent vault list` | API Key | List Managed Agent vaults | +| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | ## Command details +### `bl managed-agent agent get` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------- | +| **Name** | `managed-agent agent get` | +| **Description** | Get a Managed Agent | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent agent get --agent-id <id> [--agent-version <n>] [--file <path>]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--agent-id <id>` | string | yes | Agent ID | +| `--agent-version <n>` | number | no | Specific agent version | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent agent get --agent-id agent_abc +``` + +```bash +bl managed-agent agent get --agent-id agent_abc --agent-version 3 --output json +``` + +### `bl managed-agent agent list` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent agent list` | +| **Description** | List Managed Agents | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent agent list [--limit <n>] [--page <cursor>] [--all] [--include-archived] [--file <path>]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--include-archived` | switch | no | Include archived resources | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent agent list +``` + +```bash +bl managed-agent agent list --limit 50 +``` + +```bash +bl managed-agent agent list --all --include-archived --output json +``` + +### `bl managed-agent agent search` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent agent search` | +| **Description** | Search Managed Agents | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent agent search --query <text> [--limit <n>] [--page-limit <n>] [--include-archived]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------- | ------ | -------- | ------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--query <text>` | string | yes | Case-insensitive text to find in IDs, names, and descriptions | +| `--page-limit <n>` | number | no | Maximum pages to scan for client-side search (default: 10) | +| `--include-archived` | switch | no | Include archived resources | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent agent search --query assistant +``` + +```bash +bl managed-agent agent search --query code --page-limit 20 --output json +``` + +### `bl managed-agent agent versions` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------- | +| **Name** | `managed-agent agent versions` | +| **Description** | List Managed Agent versions | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent agent versions --agent-id <id> [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--agent-id <id>` | string | yes | Agent ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent agent versions --agent-id agent_abc +``` + +```bash +bl managed-agent agent versions --agent-id agent_abc --all --output json +``` + ### `bl managed-agent apply` -| Field | Value | -| ------------------ | ---------------------------------------------------------------------------------------- | -| **Name** | `managed-agent apply` | -| **Description** | Apply planned changes to create/update/delete agent resources | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes] [--concurrency <n>]` | +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------- | +| **Name** | `managed-agent apply` | +| **Description** | Apply planned changes to create/update/delete agent resources | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent apply [--file <path>] [--provider <name>] [--yes] [--concurrency <n>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | -------------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider (default: all configured) | +| `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) | +| `--no-refresh` | switch | no | Skip refreshing state from remote before planning | +| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent apply --yes +``` + +```bash +bl managed-agent apply --provider bailian --yes +``` + +### `bl managed-agent capabilities` + +| Field | Value | +| ------------------ | ---------------------------------------------------- | +| **Name** | `managed-agent capabilities` | +| **Description** | Show operation-level Managed Agents API capabilities | +| **Authentication** | No Auth | +| **Usage** | `bl managed-agent capabilities [--provider <name>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | -------------------------------------- | +| `--provider <name>` | string | no | Provider to inspect (default: bailian) | + +#### Notes + +- Capabilities distinguish public Managed Agents APIs from client-side compositions and unsupported resources. + +#### Examples + +```bash +bl managed-agent capabilities +``` + +```bash +bl managed-agent capabilities --provider bailian --output json +``` + +### `bl managed-agent deployment get` + +| Field | Value | +| ------------------ | ------------------------------------------------------ | +| **Name** | `managed-agent deployment get` | +| **Description** | Get a Managed Agent deployment | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment get --deployment-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--deployment-id <id>` | string | yes | Deployment ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent deployment get --deployment-id dep_abc +``` + +### `bl managed-agent deployment list` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment list` | +| **Description** | List Managed Agent deployments | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment list [--agent-id <id>] [--status active\|paused] [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--agent-id <id>` | string | no | Filter by agent ID | +| `--status <active\|paused>` | string | no | Filter by deployment status | +| `--include-archived` | switch | no | Include archived resources | +| `--created-at-gte <timestamp>` | string | no | Created at or after this timestamp | +| `--created-at-lte <timestamp>` | string | no | Created at or before this timestamp | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent deployment list +``` + +```bash +bl managed-agent deployment list --status active --all --output json +``` + +### `bl managed-agent deployment pause` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment pause` | +| **Description** | Pause a Managed Agent deployment | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment pause (--deployment <name> \| --deployment-id <id>)` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | -------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--deployment <name>` | string | no | Logical deployment name in agents.yaml/state | +| `--deployment-id <id>` | string | no | Direct deployment ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent deployment pause --deployment daily-report --dry-run +``` + +```bash +bl managed-agent deployment pause --deployment-id dep_abc +``` + +### `bl managed-agent deployment run` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment run` | +| **Description** | Run a Managed Agent deployment now | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment run (--deployment <name> \| --deployment-id <id>) --yes` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | -------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--deployment <name>` | string | no | Logical deployment name in agents.yaml/state | +| `--deployment-id <id>` | string | no | Direct deployment ID | +| `--yes` | switch | no | Confirm deployment run | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent deployment run --deployment daily-report --dry-run +``` + +```bash +bl managed-agent deployment run --deployment-id dep_abc --yes +``` + +### `bl managed-agent deployment runs get` + +| Field | Value | +| ------------------ | ---------------------------------------------------- | +| **Name** | `managed-agent deployment runs get` | +| **Description** | Get a Managed Agent deployment run | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment runs get --run-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--run-id <id>` | string | yes | Deployment run ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent deployment runs get --run-id run_abc +``` + +### `bl managed-agent deployment runs list` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment runs list` | +| **Description** | List runs for a Managed Agent deployment | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment runs list --deployment-id <id> [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--deployment-id <id>` | string | yes | Deployment ID | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent deployment runs list --deployment-id dep_abc +``` + +```bash +bl managed-agent deployment runs list --deployment-id dep_abc --all --output json +``` + +### `bl managed-agent deployment search` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment search` | +| **Description** | Search Managed Agent deployments | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment search --query <text> [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--query <text>` | string | yes | Case-insensitive text to find in IDs, names, and descriptions | +| `--agent-id <id>` | string | no | Filter by agent ID | +| `--status <active\|paused>` | string | no | Filter by deployment status | +| `--include-archived` | switch | no | Include archived resources | +| `--created-at-gte <timestamp>` | string | no | Created at or after this timestamp | +| `--created-at-lte <timestamp>` | string | no | Created at or before this timestamp | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- Deployment search maps --query to the provider's server-side keyword parameter. + +#### Examples + +```bash +bl managed-agent deployment search --query report +``` + +```bash +bl managed-agent deployment search --query nightly --all --output json +``` + +### `bl managed-agent deployment unpause` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment unpause` | +| **Description** | Unpause a Managed Agent deployment | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment unpause (--deployment <name> \| --deployment-id <id>)` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | -------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--deployment <name>` | string | no | Logical deployment name in agents.yaml/state | +| `--deployment-id <id>` | string | no | Direct deployment ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent deployment unpause --deployment daily-report --dry-run +``` + +```bash +bl managed-agent deployment unpause --deployment-id dep_abc +``` + +### `bl managed-agent destroy` + +| Field | Value | +| ------------------ | -------------------------------------------------------------- | +| **Name** | `managed-agent destroy` | +| **Description** | Destroy all managed agent resources tracked in state | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent destroy [--file <path>] [--yes] [--cascade]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------ | ------ | -------- | -------------------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Confirm and destroy without an interactive prompt (required) | +| `--cascade` | switch | no | Auto-delete dependent resources (e.g. sessions referencing an environment) | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent destroy --yes +``` + +```bash +bl managed-agent destroy --yes --cascade +``` + +### `bl managed-agent environment get` + +| Field | Value | +| ------------------ | -------------------------------------------------------- | +| **Name** | `managed-agent environment get` | +| **Description** | Get a Managed Agent environment | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent environment get --environment-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--environment-id <id>` | string | yes | Environment ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent environment get --environment-id env_abc +``` + +### `bl managed-agent environment list` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent environment list` | +| **Description** | List Managed Agent environments | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent environment list [--limit <n>] [--page <cursor>] [--all] [--include-archived]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--include-archived` | switch | no | Include archived resources | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent environment list +``` + +```bash +bl managed-agent environment list --all --output json +``` + +### `bl managed-agent environment search` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent environment search` | +| **Description** | Search Managed Agent environments | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent environment search --query <text> [--limit <n>] [--page-limit <n>] [--include-archived]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------- | ------ | -------- | ------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--query <text>` | string | yes | Case-insensitive text to find in IDs, names, and descriptions | +| `--page-limit <n>` | number | no | Maximum pages to scan for client-side search (default: 10) | +| `--include-archived` | switch | no | Include archived resources | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent environment search --query sandbox +``` + +```bash +bl managed-agent environment search --query production --page-limit 20 --output json +``` + +### `bl managed-agent file delete` + +| Field | Value | +| ------------------ | --------------------------------------------------- | +| **Name** | `managed-agent file delete` | +| **Description** | Delete a Managed Agent file | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent file delete --file-id <id> --yes` | #### Flags -| Flag | Type | Required | Description | -| ------------------- | ------ | -------- | -------------------------------------------------------------------- | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--provider <name>` | string | no | Target provider (default: all configured) | -| `--yes` | switch | no | Confirm and apply without an interactive prompt (required to mutate) | -| `--no-refresh` | switch | no | Skip refreshing state from remote before planning | -| `--concurrency <n>` | number | no | Max independent resources to apply in parallel (default 6, max 10) | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--file-id <id>` | string | yes | Remote file ID | +| `--yes` | switch | no | Confirm permanent file deletion | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes @@ -59,31 +753,33 @@ Index: [index.md](index.md) #### Examples ```bash -bl managed-agent apply --yes +bl managed-agent file delete --file-id file_abc --dry-run ``` ```bash -bl managed-agent apply --provider bailian --yes +bl managed-agent file delete --file-id file_abc --yes ``` -### `bl managed-agent destroy` +### `bl managed-agent file download` -| Field | Value | -| ------------------ | -------------------------------------------------------------- | -| **Name** | `managed-agent destroy` | -| **Description** | Destroy all managed agent resources tracked in state | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent destroy [--file <path>] [--yes] [--cascade]` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------ | +| **Name** | `managed-agent file download` | +| **Description** | Download Managed Agent file content | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent file download --file-id <id> --output-file <path> [--force]` | #### Flags -| Flag | Type | Required | Description | -| ------------------ | ------ | -------- | -------------------------------------------------------------------------- | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--yes` | switch | no | Confirm and destroy without an interactive prompt (required) | -| `--cascade` | switch | no | Auto-delete dependent resources (e.g. sessions referencing an environment) | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--file-id <id>` | string | yes | Remote file ID | +| `--output-file <path>` | string | yes | Destination path | +| `--force` | switch | no | Overwrite an existing output file | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes @@ -94,11 +790,152 @@ bl managed-agent apply --provider bailian --yes #### Examples ```bash -bl managed-agent destroy --yes +bl managed-agent file download --file-id file_abc --output-file ./artifact.pdf +``` + +### `bl managed-agent file get` + +| Field | Value | +| ------------------ | ------------------------------------------ | +| **Name** | `managed-agent file get` | +| **Description** | Get Managed Agent file metadata | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent file get --file-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--file-id <id>` | string | yes | Remote file ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent file get --file-id file_abc +``` + +### `bl managed-agent file list` + +| Field | Value | +| ------------------ | -------------------------------------------------------------------------------------- | +| **Name** | `managed-agent file list` | +| **Description** | List Managed Agent files | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent file list [--scope-id <id>] [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--scope-id <id>` | string | no | Filter by scope ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent file list ``` ```bash -bl managed-agent destroy --yes --cascade +bl managed-agent file list --scope-id sess_abc --all --output json +``` + +### `bl managed-agent file search` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent file search` | +| **Description** | Search Managed Agent files | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent file search --query <text> [--scope-id <id>] [--limit <n>] [--page-limit <n>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | ------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--query <text>` | string | yes | Case-insensitive text to find in IDs, names, and descriptions | +| `--page-limit <n>` | number | no | Maximum pages to scan for client-side search (default: 10) | +| `--scope-id <id>` | string | no | Filter by scope ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent file search --query report +``` + +```bash +bl managed-agent file search --query pdf --scope-id sess_abc --output json +``` + +### `bl managed-agent file upload` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent file upload` | +| **Description** | Upload a Managed Agent file | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent file upload --path <path> [--filename <name>] [--mime-type <type>] [--purpose <purpose>]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--path <path>` | string | yes | Local file path | +| `--filename <name>` | string | no | Remote filename override | +| `--mime-type <type>` | string | no | MIME type override | +| `--purpose <purpose>` | string | no | Provider upload purpose | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent file upload --path ./report.pdf +``` + +```bash +bl managed-agent file upload --path ./data.json --purpose assistants ``` ### `bl managed-agent init` @@ -174,6 +1011,42 @@ bl managed-agent plan --provider bailian bl managed-agent plan --no-refresh ``` +### `bl managed-agent session archive` + +| Field | Value | +| ------------------ | ---------------------------------------------------------- | +| **Name** | `managed-agent session archive` | +| **Description** | Archive a Managed Agent session | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session archive --session-id <id> --yes` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--yes` | switch | no | Confirm session archive | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent session archive --session-id sess_abc --dry-run +``` + +```bash +bl managed-agent session archive --session-id sess_abc --yes +``` + ### `bl managed-agent session create` | Field | Value | @@ -206,83 +1079,275 @@ bl managed-agent plan --no-refresh #### Examples ```bash -bl managed-agent session create -``` - -```bash -bl managed-agent session create --agent assistant +bl managed-agent session create +``` + +```bash +bl managed-agent session create --agent assistant +``` + +```bash +bl managed-agent session create --agent assistant --title 'debug run' +``` + +### `bl managed-agent session debug` + +| Field | Value | +| ------------------ | -------------------------------------------------- | +| **Name** | `managed-agent session debug` | +| **Description** | Aggregate session diagnostics | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session debug --session-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent session debug --session-id sess_abc +``` + +```bash +bl managed-agent session debug --session-id sess_abc --output json +``` + +### `bl managed-agent session delete` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent session delete` | +| **Description** | Delete a session | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session delete --session-id <id> --yes [--provider <name>] [--file <path>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--session-id <id>` | string | yes | Session ID (required) | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--yes` | switch | no | Confirm permanent session deletion | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent session delete --session-id sess_abc123 +``` + +### `bl managed-agent session event list` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent session event list` | +| **Description** | List events for a Managed Agent session | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session event list --session-id <id> [--types <types>] [--order asc\|desc] [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--order <asc\|desc>` | string | no | Event order: asc or desc | +| `--types <types>` | string | no | Comma-separated raw event types | +| `--created-at-gte <timestamp>` | string | no | Created at or after this timestamp | +| `--created-at-lte <timestamp>` | string | no | Created at or before this timestamp | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- --types is applied client-side to each page returned by the provider. + +#### Examples + +```bash +bl managed-agent session event list --session-id sess_abc +``` + +```bash +bl managed-agent session event list --session-id sess_abc --all --output json +``` + +### `bl managed-agent session event send` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------- | +| **Name** | `managed-agent session event send` | +| **Description** | Send raw events to a Managed Agent session | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session event send --session-id <id> --event <json\|@path>` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--event <json\|@path>` | string | yes | Raw event object/array as JSON or @event.json | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent session event send --session-id sess_abc --event '{"type":"message","role":"user","content":"hello"}' ``` ```bash -bl managed-agent session create --agent assistant --title 'debug run' +bl managed-agent session event send --session-id sess_abc --event @event.json ``` -### `bl managed-agent session delete` +### `bl managed-agent session event stream` -| Field | Value | -| ------------------ | --------------------------------------------------------------------------------------- | -| **Name** | `managed-agent session delete` | -| **Description** | Delete a session | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent session delete --session-id <id> [--provider <name>] [--file <path>]` | +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------- | +| **Name** | `managed-agent session event stream` | +| **Description** | Stream events from a Managed Agent session | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session event stream --session-id <id> [--after-id <event-id>]` | #### Flags -| Flag | Type | Required | Description | -| ------------------- | ------ | -------- | --------------------------------------- | -| `--session-id <id>` | string | yes | Session ID (required) | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--provider <name>` | string | no | Target provider | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--after-id <event-id>` | string | no | Resume after this event ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- When the provider has no native event cursor, --after-id resumes through paginated history polling and event ID de-duplication. #### Examples ```bash -bl managed-agent session delete --session-id sess_abc123 +bl managed-agent session event stream --session-id sess_abc +``` + +```bash +bl managed-agent session event stream --session-id sess_abc --after-id evt_123 --output json ``` ### `bl managed-agent session events` -| Field | Value | -| ------------------ | ----------------------------------------------------------------------------------------- | -| **Name** | `managed-agent session events` | -| **Description** | List event history for a session | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent session events --session-id <id> [--limit <n>] [--all] [--file <path>]` | +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent session events` | +| **Description** | List events for a Managed Agent session | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session events --session-id <id> [--types <types>] [--order asc\|desc] [--limit <n>] [--page <cursor>] [--all]` | #### Flags -| Flag | Type | Required | Description | -| ------------------- | ------ | -------- | --------------------------------------- | -| `--session-id <id>` | string | yes | Session ID (required) | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--provider <name>` | string | no | Target provider | -| `--limit <n>` | number | no | Maximum number of events to fetch | -| `--all` | switch | no | Fetch all pages by following the cursor | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--order <asc\|desc>` | string | no | Event order: asc or desc | +| `--types <types>` | string | no | Comma-separated raw event types | +| `--created-at-gte <timestamp>` | string | no | Created at or after this timestamp | +| `--created-at-lte <timestamp>` | string | no | Created at or before this timestamp | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- --types is applied client-side to each page returned by the provider. #### Examples ```bash -bl managed-agent session events --session-id sess_abc123 +bl managed-agent session events --session-id sess_abc +``` + +```bash +bl managed-agent session events --session-id sess_abc --all --output json ``` +### `bl managed-agent session export` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------- | +| **Name** | `managed-agent session export` | +| **Description** | Export session diagnostics as a ZIP | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session export --session-id <id> --output-file <path> [--force]` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--output-file <path>` | string | yes | Destination ZIP path | +| `--force` | switch | no | Overwrite an existing output file | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- The ZIP contains metadata only; file bodies and credential-like values are excluded/redacted. + +#### Examples + ```bash -bl managed-agent session events --session-id sess_abc123 --all +bl managed-agent session export --session-id sess_abc --output-file ./session-debug.zip ``` ### `bl managed-agent session get` @@ -318,23 +1383,28 @@ bl managed-agent session get --session-id sess_abc123 ### `bl managed-agent session list` -| Field | Value | -| ------------------ | -------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent session list` | -| **Description** | List sessions from the provider | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent session list [--agent <name>] [--all] [--provider <name>] [--file <path>]` | +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent session list` | +| **Description** | List sessions from the provider | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session list [--agent <name>] [--statuses <statuses>] [--limit <n>] [--page <cursor>] [--all]` | #### Flags -| Flag | Type | Required | Description | -| ------------------- | ------ | -------- | --------------------------------------- | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--agent <name>` | string | no | Filter by agent name | -| `--all` | switch | no | Fetch all pages by following the cursor | -| `--provider <name>` | string | no | Target provider | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--agent <name>` | string | no | Filter by agent name | +| `--all` | switch | no | Fetch all pages by following the cursor | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--statuses <statuses>` | string | no | Comma-separated session statuses | +| `--created-at-gte <timestamp>` | string | no | Created at or after this RFC 3339 timestamp | +| `--created-at-lte <timestamp>` | string | no | Created at or before this RFC 3339 timestamp | +| `--provider <name>` | string | no | Target provider | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes @@ -398,6 +1468,47 @@ bl managed-agent session run --prompt "hello" bl managed-agent session run --agent assistant --prompt "summarize this repo" ``` +### `bl managed-agent session search` + +| Field | Value | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent session search` | +| **Description** | Search Managed Agent sessions | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session search --query <text> [--agent <name>] [--statuses <statuses>] [--limit <n>] [--page-limit <n>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | ------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--query <text>` | string | yes | Case-insensitive text to find in IDs, names, and descriptions | +| `--page-limit <n>` | number | no | Maximum pages to scan for client-side search (default: 10) | +| `--agent <name>` | string | no | Filter by configured agent name | +| `--statuses <statuses>` | string | no | Comma-separated session statuses | +| `--created-at-gte <timestamp>` | string | no | Created at or after this timestamp | +| `--created-at-lte <timestamp>` | string | no | Created at or before this timestamp | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent session search --query debug +``` + +```bash +bl managed-agent session search --query failed --statuses failed --output json +``` + ### `bl managed-agent session send` | Field | Value | @@ -431,50 +1542,268 @@ bl managed-agent session run --agent assistant --prompt "summarize this repo" bl managed-agent session send --session-id sess_abc123 --message "continue" ``` -### `bl managed-agent skill-list` +### `bl managed-agent session update` -| Field | Value | -| ------------------ | -------------------------------------------------------------------------------------------------- | -| **Name** | `managed-agent skill-list` | -| **Description** | List skills from the provider's skill catalog | -| **Authentication** | API Key | -| **Usage** | `bl managed-agent skill-list [--source custom\|official\|all] [--provider <name>] [--file <path>]` | +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent session update` | +| **Description** | Update a Managed Agent session | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent session update --session-id <id> [--title <title>] [--metadata <json\|@path>]` | #### Flags -| Flag | Type | Required | Description | -| ------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------ | -| `--file <path>` | string | no | Config file path (default: agents.yaml) | -| `--source <source>` | string | no | Skill catalog: custom (workspace-uploaded, default), official (built-in), or all (both catalogs in one call) | -| `--provider <name>` | string | no | Target provider | -| `--api-key <key>` | string | no | API key | -| `--base-url <url>` | string | no | API base URL | +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--session-id <id>` | string | yes | Session ID | +| `--title <title>` | string | no | New session title | +| `--metadata <json\|@path>` | string | no | String-valued metadata JSON or @file | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | #### Notes - Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). - Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. - Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. -- Providers without a skill listing API (e.g. ark) return an empty list. -- 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. -- When generating a task that needs a suitable skill, call this command to match official or custom skills before wiring them into the task. #### Examples ```bash -bl managed-agent skill-list +bl managed-agent session update --session-id sess_abc --title 'investigation' ``` ```bash -bl managed-agent skill-list --source official +bl managed-agent session update --session-id sess_abc --metadata @metadata.json +``` + +### `bl managed-agent skill download` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent skill download` | +| **Description** | Download a Managed Agent skill version | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent skill download --skill-id <id> --skill-version <version> --output-file <path> [--force]` | + +#### Flags + +| Flag | Type | Required | Description | +| --------------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--skill-id <id>` | string | yes | Skill ID | +| `--skill-version <version>` | string | yes | Skill version | +| `--output-file <path>` | string | yes | Destination ZIP path | +| `--force` | switch | no | Overwrite an existing output file | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent skill download --skill-id skill_abc --skill-version 3 --output-file ./skill.zip +``` + +### `bl managed-agent skill get` + +| Field | Value | +| ------------------ | -------------------------------------------- | +| **Name** | `managed-agent skill get` | +| **Description** | Get a Managed Agent skill | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent skill get --skill-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--skill-id <id>` | string | yes | Skill ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent skill get --skill-id skill_abc +``` + +### `bl managed-agent skill list` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent skill list` | +| **Description** | List Managed Agent skills | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent skill list [--source custom\|official\|all] [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--source <custom\|official\|all>` | string | no | Skill catalog: custom (default), official, or all | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- --source all combines one page from each catalog, or every page with --all; it does not accept --page. + +#### Examples + +```bash +bl managed-agent skill list +``` + +```bash +bl managed-agent skill list --source official +``` + +```bash +bl managed-agent skill list --source all --all --output json +``` + +### `bl managed-agent skill search` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent skill search` | +| **Description** | Search Managed Agent skills | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent skill search --query <text> [--source custom\|official\|all] [--limit <n>] [--page-limit <n>]` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------------------- | ------ | -------- | ------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--query <text>` | string | yes | Case-insensitive text to find in IDs, names, and descriptions | +| `--page-limit <n>` | number | no | Maximum pages to scan for client-side search (default: 10) | +| `--source <custom\|official\|all>` | string | no | Skill catalog: custom (default), official, or all | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent skill search --query browser --source official +``` + +```bash +bl managed-agent skill search --query report --source all --output json +``` + +### `bl managed-agent skill versions` + +| Field | Value | +| ------------------ | ----------------------------------------------------------------------------------------- | +| **Name** | `managed-agent skill versions` | +| **Description** | List Managed Agent skill versions | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent skill versions --skill-id <id> [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--skill-id <id>` | string | yes | Skill ID | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent skill versions --skill-id skill_abc +``` + +```bash +bl managed-agent skill versions --skill-id skill_abc --all --output json +``` + +### `bl managed-agent skill-list` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent skill-list` | +| **Description** | List Managed Agent skills | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent skill-list [--source custom\|official\|all] [--limit <n>] [--page <cursor>] [--all]` | + +#### Flags + +| Flag | Type | Required | Description | +| ---------------------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--source <custom\|official\|all>` | string | no | Skill catalog: custom (default), official, or all | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- --source all combines one page from each catalog, or every page with --all; it does not accept --page. + +#### Examples + +```bash +bl managed-agent skill-list ``` ```bash -bl managed-agent skill-list --source all --output json +bl managed-agent skill-list --source official ``` ```bash -bl managed-agent skill-list --source custom --provider bailian +bl managed-agent skill-list --source all --all --output json ``` ### `bl managed-agent state import` @@ -618,3 +1947,110 @@ bl managed-agent validate ```bash bl managed-agent validate --file agents.yaml ``` + +### `bl managed-agent vault get` + +| Field | Value | +| ------------------ | -------------------------------------------- | +| **Name** | `managed-agent vault get` | +| **Description** | Get a Managed Agent vault | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent vault get --vault-id <id>` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------- | ------ | -------- | --------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--vault-id <id>` | string | yes | Vault ID | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent vault get --vault-id vault_abc +``` + +### `bl managed-agent vault list` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent vault list` | +| **Description** | List Managed Agent vaults | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent vault list [--limit <n>] [--page <cursor>] [--all] [--include-archived]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------- | ------ | -------- | ------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--page <cursor>` | string | no | Opaque page cursor returned by a previous request | +| `--all` | switch | no | Fetch all pages by following opaque cursors | +| `--include-archived` | switch | no | Include archived resources | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent vault list +``` + +```bash +bl managed-agent vault list --all --output json +``` + +### `bl managed-agent vault search` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent vault search` | +| **Description** | Search Managed Agent vaults | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent vault search --query <text> [--limit <n>] [--page-limit <n>] [--include-archived]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------- | ------ | -------- | ------------------------------------------------------------- | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--provider <name>` | string | no | Target provider | +| `--limit <n>` | number | no | Page size (1-100) | +| `--query <text>` | string | yes | Case-insensitive text to find in IDs, names, and descriptions | +| `--page-limit <n>` | number | no | Maximum pages to scan for client-side search (default: 10) | +| `--include-archived` | switch | no | Include archived resources | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. + +#### Examples + +```bash +bl managed-agent vault search --query github +``` + +```bash +bl managed-agent vault search --query production --page-limit 20 --output json +``` From 4c7ef0c7a5d067cf9f1a1d0cf4c1e8eeea627eba Mon Sep 17 00:00:00 2001 From: chenanran555 <car534511@alibaba-inc.com> Date: Thu, 27 Aug 2026 11:44:00 +0800 Subject: [PATCH 2/4] 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. --- packages/cli/src/commands.ts | 2 + .../managed-agent/_engine/credentials.ts | 11 +- .../_engine/file-state-manager.ts | 4 +- .../commands/managed-agent/agent-create.ts | 443 ++++++++++++++++++ packages/commands/src/index.ts | 1 + .../commands/tests/credentials-bridge.test.ts | 12 + .../tests/e2e/managed-agent.e2e.test.ts | 163 +++++++ packages/commands/tests/e2e/topic-routes.ts | 1 + .../tests/managed-agent-create.test.ts | 94 ++++ skills/bailian-managed-agent/SKILL.md | 13 + .../bailian-managed-agent/reference/index.md | 121 ++--- .../reference/managed-agent.md | 159 ++++--- 12 files changed, 902 insertions(+), 122 deletions(-) create mode 100644 packages/commands/src/commands/managed-agent/agent-create.ts create mode 100644 packages/commands/tests/managed-agent-create.test.ts diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index f5cff6b69..68b3784ef 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -152,6 +152,7 @@ import { managedAgentSessionEvents, managedAgentSkillList, managedAgentCapabilities, + managedAgentAgentCreate, managedAgentAgentList, managedAgentAgentGet, managedAgentAgentSearch, @@ -350,6 +351,7 @@ export const commands: Record<string, AnyCommand> = { "managed-agent session events": managedAgentSessionEvents, "managed-agent skill-list": managedAgentSkillList, "managed-agent capabilities": managedAgentCapabilities, + "managed-agent agent create": managedAgentAgentCreate, "managed-agent agent list": managedAgentAgentList, "managed-agent agent get": managedAgentAgentGet, "managed-agent agent search": managedAgentAgentSearch, diff --git a/packages/commands/src/commands/managed-agent/_engine/credentials.ts b/packages/commands/src/commands/managed-agent/_engine/credentials.ts index 3dbfbaaea..63489f589 100644 --- a/packages/commands/src/commands/managed-agent/_engine/credentials.ts +++ b/packages/commands/src/commands/managed-agent/_engine/credentials.ts @@ -170,11 +170,16 @@ export function normalizeInterpolatedProviderBlocks(providers: Record<string, un * 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. + * every declared provider by default. A scoped operation may pass its exact + * provider list so unrelated credentials cannot block it; 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; diff --git a/packages/commands/src/commands/managed-agent/_engine/file-state-manager.ts b/packages/commands/src/commands/managed-agent/_engine/file-state-manager.ts index 72a8fd066..8e1aff2b7 100644 --- a/packages/commands/src/commands/managed-agent/_engine/file-state-manager.ts +++ b/packages/commands/src/commands/managed-agent/_engine/file-state-manager.ts @@ -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); } diff --git a/packages/commands/src/commands/managed-agent/agent-create.ts b/packages/commands/src/commands/managed-agent/agent-create.ts new file mode 100644 index 000000000..6af87cdf4 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/agent-create.ts @@ -0,0 +1,443 @@ +import { randomUUID } from "node:crypto"; +import { readFile, rename, stat, unlink, writeFile } from "node:fs/promises"; +import { basename, dirname, resolve } from "node:path"; +import { + buildAgentDecl, + type IStateManager, + LocalFileStateBackend, + planAgentResourcesWithStateBackend, + type ResolvedProjectConfig, + type ResourceAddress, + resolveProjectConfigFromObject, + syncAgentResourcesWithStateBackend, +} 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, resolveAgentProjectConfig } from "./_engine/config-loader.ts"; +import { assertProviderCredentials } from "./_engine/credentials.ts"; +import { withStdoutProtected } from "./_engine/console-capture.ts"; +import { withAgentErrors } from "./_engine/errors.ts"; +import { createFileStateScope } from "./_engine/file-state-manager.ts"; +import { installSdkTransport } from "./_engine/transport.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 描述" }, + }, + provider: { + type: "string", + valueHint: "<name>", + description: { + "en-US": "Target provider; inferred when the config has one effective provider", + "zh-CN": "目标 Provider;配置只有一个有效 Provider 时自动推断", + }, + }, + environment: { + type: "string", + valueHint: "<name>", + description: { + "en-US": "Existing environment key from agents.yaml", + "zh-CN": "agents.yaml 中已有的 Environment key", + }, + }, + vault: { + type: "string", + valueHint: "<name>", + description: { + "en-US": "Existing vault key from agents.yaml", + "zh-CN": "agents.yaml 中已有的 Vault key", + }, + }, + skill: { + type: "array", + valueHint: "<name>", + description: { + "en-US": "Existing custom Skill key from agents.yaml (repeatable)", + "zh-CN": "agents.yaml 中已有的自定义 Skill key(可重复)", + }, + }, + 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"]; + +interface AgentKeySelection { + key: string; + reusedPending: 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 normalizeAgentKey(displayName: string): string { + const normalized = displayName + .normalize("NFKC") + .trim() + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, "-") + .replace(/^-+|-+$/g, ""); + return normalized || "agent"; +} + +export function selectAgentKey(options: { + displayName: string; + provider: string; + agents: Record<string, BuiltAgentDecl>; + candidate: BuiltAgentDecl; + state: IStateManager; +}): AgentKeySelection { + const tracked = new Set( + options.state + .listResources() + .filter( + (resource) => + resource.address.provider === options.provider && + (resource.address.type === "agent" || resource.address.type === "template"), + ) + .map((resource) => resource.address.name), + ); + const candidateJson = canonicalJson(options.candidate); + for (const [key, declaration] of Object.entries(options.agents)) { + const effectiveName = declaration.name ?? key; + if (effectiveName !== options.displayName || tracked.has(key)) continue; + if (canonicalJson({ ...declaration, name: effectiveName }) === candidateJson) { + return { key, reusedPending: true }; + } + } + + const baseKey = normalizeAgentKey(options.displayName); + if (!(baseKey in options.agents)) return { key: baseKey, reusedPending: false }; + let suffix = 2; + while (`${baseKey}-${suffix}` in options.agents) suffix += 1; + return { key: `${baseKey}-${suffix}`, reusedPending: false }; +} + +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 Agent 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; + } +} + +function resolveTargetProvider( + config: ResolvedProjectConfig, + requested: string | undefined, +): string { + if (requested === "all") { + throw new BailianError("--provider all is not valid for Agent create.", ExitCode.USAGE); + } + if (requested) { + if (requested in config.providers) return requested; + throw new BailianError( + `Provider '${requested}' is not configured in agents.yaml.`, + ExitCode.USAGE, + ); + } + const defaultProvider = config.defaults?.provider; + if (defaultProvider && defaultProvider !== "all") return defaultProvider; + const configuredProviders = Object.keys(config.providers); + if (configuredProviders.length === 1) return configuredProviders[0]!; + throw new BailianError( + "Agent create cannot infer one target provider.", + ExitCode.USAGE, + "Pass --provider <name> when defaults.provider is 'all' or multiple providers are configured.", + ); +} + +function rootAddress( + selectedAddresses: ResourceAddress[] | undefined, + agentKey: string, +): ResourceAddress | undefined { + return selectedAddresses?.find( + (address) => + address.name === agentKey && (address.type === "agent" || address.type === "template"), + ); +} + +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>] [--provider <name>] [--environment <name>] [--vault <name>] [--skill <name>...] [--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 --environment dev --skill search --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。", + }, + ], + 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."; + return undefined; + }, + async run(ctx) { + const { flags, settings } = ctx; + const format = detectOutputFormat(settings.output); + const file = flags.file ?? "agents.yaml"; + const sourceBeforeLoad = await readFile(resolve(file), "utf8").catch((error) => { + const code = (error as NodeJS.ErrnoException).code; + if (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(ctx, 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 = resolveTargetProvider(loaded.config, flags.provider); + if (!settings.dryRun) assertProviderCredentials(loaded.config.providers, [provider]); + installSdkTransport(ctx); + + const rawAgent = buildAgentDecl(undefined, { + name: flags.name.trim(), + description: flags.description, + model: flags.model, + instructions: flags.instructions, + provider, + environment: flags.environment, + vault: flags.vault, + builtinTools: flags.tool, + skills: flags.skill?.map((skillName) => ({ kind: "custom", name: skillName })), + }).agent; + const temporaryKey = "__bailian_cli_agent_create_candidate__"; + const resolvedCandidate = await withAgentErrors(() => + resolveProjectConfigFromObject( + { + ...loaded.config, + agents: { ...loaded.config.agents, [temporaryKey]: rawAgent }, + }, + { projectName: loaded.projectName, basePath: dirname(loaded.configPath) }, + ), + ); + const candidateAgent = resolvedCandidate.config.agents![temporaryKey]! as BuiltAgentDecl; + const stateBackend = new LocalFileStateBackend({ configPath: loaded.configPath }); + const stateScope = createFileStateScope(loaded.configPath, loaded.projectName); + const keySelection = await stateBackend.read(stateScope, (state) => + selectAgentKey({ + displayName: flags.name.trim(), + provider, + agents: (loaded.config.agents ?? {}) as Record<string, BuiltAgentDecl>, + candidate: candidateAgent, + state, + }), + ); + const agentKey = keySelection.key; + const candidateConfig = structuredClone(loaded.config); + candidateConfig.agents = { ...candidateConfig.agents, [agentKey]: candidateAgent }; + candidateConfig._resolved = true; + + const document = parseDocument(source); + if (document.errors.length > 0) { + throw new BailianError( + `YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`, + ExitCode.USAGE, + ); + } + document.setIn(["agents", agentKey], rawAgent); + const nextSource = document.toString(); + const backendInput = { + projectName: loaded.projectName, + config: candidateConfig, + configPath: loaded.configPath, + providers: { [provider]: candidateConfig.providers[provider] }, + stateBackend, + stateScope, + }; + + if (settings.dryRun || !flags.yes) { + const planned = await withAgentErrors(() => + withStdoutProtected(() => + planAgentResourcesWithStateBackend(backendInput, agentKey, { + refresh: !settings.dryRun, + quiet: format === "json", + mode: "create-only", + }), + ), + ); + const readyToCreate = !planned.diagnostics.some( + (diagnostic) => diagnostic.severity === "error", + ); + const result = { + agent: { key: agentKey, name: flags.name.trim(), provider }, + config_file: loaded.configPath, + yaml_written: false, + reused_pending: keySelection.reusedPending, + requires_confirmation: !settings.dryRun, + ready_to_create: readyToCreate, + actions: planned.actions, + diagnostics: planned.diagnostics, + }; + if (format === "json") { + emitResult(result, format); + } else { + emitBare(`Generated YAML key: ${agentKey}`); + for (const diagnostic of planned.diagnostics) { + emitBare(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`); + } + for (const action of planned.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(loaded.configPath, source, nextSource); + const run = await withAgentErrors(() => + withStdoutProtected(() => + syncAgentResourcesWithStateBackend(backendInput, agentKey, { + refresh: true, + quiet: format === "json", + mode: "create-only", + policy: "block", + }), + ), + ); + const remoteId = await stateBackend.read(stateScope, (state) => { + const selectedRoot = rootAddress( + run.actions.map((action) => action.address), + agentKey, + ); + return selectedRoot ? state.getResource(selectedRoot)?.remote_id : undefined; + }); + const result = { + agent: { key: agentKey, name: flags.name.trim(), provider, remote_id: remoteId }, + config_file: loaded.configPath, + yaml_written: true, + reused_pending: keySelection.reusedPending, + status: run.status, + actions: run.actions, + diagnostics: run.diagnostics, + results: run.results, + error: run.error, + }; + if (format === "json") emitResult(result, format); + else { + emitBare(`Wrote ${loaded.configPath} with Agent key '${agentKey}'.`); + if (run.status === "completed") emitBare(`Created Agent '${flags.name.trim()}'.`); + else emitBare(`Scoped create ${run.status}: ${run.error ?? "unknown error"}`); + } + if (run.status !== "completed") { + throw new BailianError( + run.error ?? "Scoped Agent create failed.", + ExitCode.GENERAL, + "The YAML declaration was kept. Fix the related dependency or provider error, then re-run the same create command.", + ); + } + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 6ec38d97e..a3655122a 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -149,6 +149,7 @@ export { default as managedAgentSessionSend } from "./commands/managed-agent/ses 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 managedAgentCapabilities } from "./commands/managed-agent/capabilities.ts"; +export { default as managedAgentAgentCreate } from "./commands/managed-agent/agent-create.ts"; export { managedAgentAgentGet, managedAgentAgentList, diff --git a/packages/commands/tests/credentials-bridge.test.ts b/packages/commands/tests/credentials-bridge.test.ts index 3a1fb982d..dde12cfaf 100644 --- a/packages/commands/tests/credentials-bridge.test.ts +++ b/packages/commands/tests/credentials-bridge.test.ts @@ -166,6 +166,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 { diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index 6bb31e620..f082b5a0a 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -1,5 +1,10 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vite-plus/test"; +import { parse } from "yaml"; import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts"; import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts"; @@ -9,6 +14,7 @@ const AGENTS_DEPLOYMENT_INVALID_YAML = join( "managed-agent", "agents-deployment-invalid.yaml", ); +const AGENTS_YAML = join(e2eFixturesDir, "managed-agent", "agents.yaml"); const DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES = [ "bailian.deployment.initial_events.message_required", @@ -132,6 +138,125 @@ describe("e2e: managed-agent", () => { expect(stderr).toMatch(/--file|--provider|--yes/i); }); + test("managed-agent agent create --help 展示声明和确认参数", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "agent", + "create", + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--name|--model|--instructions|--skill|--yes/i); + expect(stderr).not.toMatch(/--key/i); + }); + + test("managed-agent agent create 缺少 --name 时退出为用法错误 (2)", async () => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "agent", + "create", + "--model", + "qwen3.8-max", + "--instructions", + "help", + "--quiet", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--name|Missing required/i); + }); + + test("managed-agent agent create 默认只预览,不写 YAML", async () => { + const sourceBefore = await readFile(AGENTS_YAML, "utf8"); + const { stdout, stderr, exitCode } = await runCommandE2e( + MANAGED_AGENT_ROUTES, + [ + "managed-agent", + "agent", + "create", + "--name", + "Create Confirm", + "--model", + "qwen3.8-max", + "--instructions", + "Preview before create", + "--file", + AGENTS_YAML, + "--output", + "json", + ], + { DASHSCOPE_API_KEY: "sk-e2e-agent-create" }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + agent?: { key?: string; name?: string }; + yaml_written?: boolean; + requires_confirmation?: boolean; + ready_to_create?: boolean; + }>(stdout); + expect(data.agent).toEqual( + expect.objectContaining({ key: "create-confirm", name: "Create Confirm" }), + ); + expect(data.yaml_written).toBe(false); + expect(data.requires_confirmation).toBe(true); + expect(data.ready_to_create).toBe(true); + expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore); + }); + + test("managed-agent agent create 远端失败后保留 YAML,重试复用 key", async () => { + const directory = await mkdtemp(join(tmpdir(), "bl-agent-create-e2e-")); + const configPath = join(directory, "agents.yaml"); + await writeFile(configPath, await readFile(AGENTS_YAML, "utf8"), "utf8"); + const requestBodies: Array<Record<string, unknown>> = []; + const server = http.createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + const body = Buffer.concat(chunks).toString("utf8"); + if (body) requestBodies.push(JSON.parse(body) as Record<string, unknown>); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ message: "intentional create failure" })); + }); + }); + await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen)); + const address = server.address() as AddressInfo; + const args = [ + "managed-agent", + "agent", + "create", + "--name", + "Retry Agent", + "--model", + "qwen3.8-max", + "--instructions", + "Retry safely", + "--file", + configPath, + "--yes", + "--output", + "json", + ]; + const env = { + DASHSCOPE_API_KEY: "sk-e2e-agent-create", + BAILIAN_BASE_URL: `http://127.0.0.1:${address.port}/api/v1/agentstudio`, + }; + try { + for (let attempt = 0; attempt < 2; attempt += 1) { + const result = await runCommandE2e(MANAGED_AGENT_ROUTES, args, env); + expect(result.exitCode, result.stderr).toBe(1); + } + const config = parse(await readFile(configPath, "utf8")) as { + agents: Record<string, { name?: string }>; + }; + expect(config.agents["retry-agent"]?.name).toBe("Retry Agent"); + expect(config.agents["retry-agent-2"]).toBeUndefined(); + expect(requestBodies).toHaveLength(2); + expect(requestBodies.every((body) => body.name === "Retry Agent")).toBe(true); + } finally { + await new Promise<void>((resolveClose) => server.close(() => resolveClose())); + await rm(directory, { recursive: true, force: true }); + } + }); + test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => { const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", @@ -311,6 +436,44 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => expect(data.would_apply?.provider).toBe("all"); }); + test("agent create --dry-run 自动生成 key 且不改 YAML", async () => { + const sourceBefore = await readFile(AGENTS_YAML, "utf8"); + const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "agent", + "create", + "--dry-run", + "--name", + "Create Preview", + "--model", + "qwen3.8-max", + "--instructions", + "Preview only", + "--file", + AGENTS_YAML, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + agent?: { key?: string; name?: string }; + yaml_written?: boolean; + actions?: Array<{ action?: string; address?: { name?: string } }>; + }>(stdout); + expect(data.agent).toEqual( + expect.objectContaining({ key: "create-preview", name: "Create Preview" }), + ); + expect(data.yaml_written).toBe(false); + expect(data.actions).toContainEqual( + expect.objectContaining({ + action: "create", + address: expect.objectContaining({ name: "create-preview" }), + }), + ); + expect(data.actions?.every((action) => action.address?.name === "create-preview")).toBe(true); + expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore); + }); + test("destroy --dry-run 仅输出计划", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 5104072a9..ab962b2cb 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -198,6 +198,7 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = { "managed-agent session events": "managedAgentSessionEvents", "managed-agent skill-list": "managedAgentSkillList", "managed-agent capabilities": "managedAgentCapabilities", + "managed-agent agent create": "managedAgentAgentCreate", "managed-agent agent list": "managedAgentAgentList", "managed-agent agent get": "managedAgentAgentGet", "managed-agent agent search": "managedAgentAgentSearch", diff --git a/packages/commands/tests/managed-agent-create.test.ts b/packages/commands/tests/managed-agent-create.test.ts new file mode 100644 index 000000000..edbe39fd3 --- /dev/null +++ b/packages/commands/tests/managed-agent-create.test.ts @@ -0,0 +1,94 @@ +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 { + normalizeAgentKey, + replaceConfigAtomically, + selectAgentKey, +} from "../src/commands/managed-agent/agent-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("同名已跟踪 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("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 Agent create was being prepared/); + expect(await readFile(configPath, "utf8")).toBe("version: new\n"); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index cf5005930..69b55f361 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -40,6 +40,19 @@ API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault 5. Destroy bl managed-agent destroy --yes # only after user confirmation ``` +## Scoped single-Agent create + +`bl managed-agent agent create`仍然先把声明写入 `agents.yaml`,再通过 SDK 的定向 Plan/Apply 创建远端资源, +不是绕过 State 的命令式 API 调用: + +- 用户只提供 Agent `name`;CLI 自动生成稳定的 YAML 逻辑 key,同名 Agent 用递增后缀并存。 +- 默认只预览自动 key 和定向计划;`--dry-run` 完全离线,只有显式 `--yes` 才写 YAML 并创建远端 Agent。 +- 定向流程只刷新目标 Agent 及其传递依赖;无关资源不检测 Drift、不产生 action,也不阻塞。 +- 目标 Agent 必须是 `create`,相关依赖必须已经处于 `no-op`;项目级 Drift 和删除仍由全量 `plan/apply` 处理。 +- 远端创建失败时保留 YAML 声明;修复相关依赖或 Provider 错误后,重复相同命令会复用待创建 key。 + +具体 flags、usage 和 examples 以 `reference/` 或 `bl managed-agent agent create --help` 为准。 + ## Deployment as IaC Deployment 与 Agent 一样声明在 `agents.yaml` 中,并复用同一条 `validate → plan → apply → destroy` IaC 链路; diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index 46f2a8721..07f109f2b 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -9,69 +9,70 @@ Use this index for the skill-scoped quick index and global flags. ## Quick index -| Command | Authentication | Description | Detail | -| --------------------------------------- | -------------- | ------------------------------------------------------------- | ------------------------------------ | -| `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 capabilities` | No Auth | Show operation-level Managed Agents API capabilities | [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 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 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 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 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 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) | +| 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 capabilities` | No Auth | Show operation-level Managed Agents API capabilities | [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 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 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 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 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 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` | `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `capabilities`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `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 download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) | +| Group | Commands | Reference | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | +| `managed-agent` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `capabilities`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `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 download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) | ## Global flags diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index 7836606b2..93600bc71 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -7,66 +7,111 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| --------------------------------------- | -------------- | ------------------------------------------------------------- | -| `bl managed-agent agent get` | API Key | Get a Managed Agent | -| `bl managed-agent agent list` | API Key | List Managed Agents | -| `bl managed-agent agent search` | API Key | Search Managed Agents | -| `bl managed-agent agent versions` | API Key | List Managed Agent versions | -| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | -| `bl managed-agent capabilities` | No Auth | Show operation-level Managed Agents API capabilities | -| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | -| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | -| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | -| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | -| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | -| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | -| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | -| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | -| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | -| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | -| `bl managed-agent environment list` | API Key | List Managed Agent environments | -| `bl managed-agent environment search` | API Key | Search Managed Agent environments | -| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | -| `bl managed-agent file download` | API Key | Download Managed Agent file content | -| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | -| `bl managed-agent file list` | API Key | List Managed Agent files | -| `bl managed-agent file search` | API Key | Search Managed Agent files | -| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | -| `bl managed-agent init` | No Auth | Create a new agents.yaml template | -| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | -| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | -| `bl managed-agent session create` | API Key | Create a new session for an agent | -| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | -| `bl managed-agent session delete` | API Key | Delete a session | -| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | -| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | -| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | -| `bl managed-agent session events` | API Key | List events for a Managed Agent session | -| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | -| `bl managed-agent session get` | API Key | Get details of a session | -| `bl managed-agent session list` | API Key | List sessions from the provider | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | -| `bl managed-agent session search` | API Key | Search Managed Agent sessions | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | -| `bl managed-agent session update` | API Key | Update a Managed Agent session | -| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | -| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | -| `bl managed-agent skill list` | API Key | List Managed Agent skills | -| `bl managed-agent skill search` | API Key | Search Managed Agent skills | -| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | -| `bl managed-agent skill-list` | API Key | List Managed Agent skills | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | -| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | -| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | -| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | -| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | -| `bl managed-agent vault list` | API Key | List Managed Agent vaults | -| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | +| Command | Authentication | Description | +| --------------------------------------- | -------------- | ------------------------------------------------------------------- | +| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | +| `bl managed-agent agent get` | API Key | Get a Managed Agent | +| `bl managed-agent agent list` | API Key | List Managed Agents | +| `bl managed-agent agent search` | API Key | Search Managed Agents | +| `bl managed-agent agent versions` | API Key | List Managed Agent versions | +| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | +| `bl managed-agent capabilities` | No Auth | Show operation-level Managed Agents API capabilities | +| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | +| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | +| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | +| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | +| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | +| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | +| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | +| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | +| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | +| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | +| `bl managed-agent environment list` | API Key | List Managed Agent environments | +| `bl managed-agent environment search` | API Key | Search Managed Agent environments | +| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | +| `bl managed-agent file download` | API Key | Download Managed Agent file content | +| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | +| `bl managed-agent file list` | API Key | List Managed Agent files | +| `bl managed-agent file search` | API Key | Search Managed Agent files | +| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | +| `bl managed-agent init` | No Auth | Create a new agents.yaml template | +| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | +| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | +| `bl managed-agent session create` | API Key | Create a new session for an agent | +| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | +| `bl managed-agent session delete` | API Key | Delete a session | +| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | +| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | +| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | +| `bl managed-agent session events` | API Key | List events for a Managed Agent session | +| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | +| `bl managed-agent session get` | API Key | Get details of a session | +| `bl managed-agent session list` | API Key | List sessions from the provider | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | +| `bl managed-agent session search` | API Key | Search Managed Agent sessions | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | +| `bl managed-agent session update` | API Key | Update a Managed Agent session | +| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | +| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | +| `bl managed-agent skill list` | API Key | List Managed Agent skills | +| `bl managed-agent skill search` | API Key | Search Managed Agent skills | +| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | +| `bl managed-agent skill-list` | API Key | List Managed Agent skills | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | +| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | +| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | +| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | +| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | +| `bl managed-agent vault list` | API Key | List Managed Agent vaults | +| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | ## Command details +### `bl managed-agent agent create` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent agent create` | +| **Description** | Declare and create one Managed Agent through an isolated YAML apply | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent agent create --name <name> --model <model> --instructions <text\|path> [--description <text>] [--provider <name>] [--environment <name>] [--vault <name>] [--skill <name>...] [--tool <name>...] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------------- | ------ | -------- | -------------------------------------------------------------------- | +| `--name <name>` | string | yes | Remote Agent display name; the YAML key is generated automatically | +| `--model <model>` | string | yes | Model ID | +| `--instructions <text\|path>` | string | yes | Inline instructions or a ./, ../, or absolute file path | +| `--description <text>` | string | no | Agent description | +| `--provider <name>` | string | no | Target provider; inferred when the config has one effective provider | +| `--environment <name>` | string | no | Existing environment key from agents.yaml | +| `--vault <name>` | string | no | Existing vault key from agents.yaml | +| `--skill <name>` | array | no | Existing custom Skill key from agents.yaml (repeatable) | +| `--tool <name>` | array | no | Builtin tool name (repeatable) | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and run the scoped remote create | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- Without --yes, previews the generated YAML key and scoped plan. --dry-run stays offline. Unrelated resources are not refreshed or drift-checked. + +#### Examples + +```bash +bl managed-agent agent create --name assistant --model qwen3.8-max --instructions "You are helpful." +``` + +```bash +bl managed-agent agent create --name assistant --model qwen3.8-max --instructions ./prompts/assistant.md --environment dev --skill search --yes +``` + ### `bl managed-agent agent get` | Field | Value | From 47e9bcc634f54f5d0094f85457276926680b0bfd Mon Sep 17 00:00:00 2001 From: chenanran555 <car534511@alibaba-inc.com> Date: Thu, 27 Aug 2026 17:01:27 +0800 Subject: [PATCH 3/4] feat(managed-agent): add scoped resource create commands --- packages/cli/src/commands.ts | 10 + .../managed-agent/_engine/scoped-create.ts | 447 ++++++++++++++ .../src/commands/managed-agent/agent.ts | 236 -------- .../commands/managed-agent/agent/_shared.ts | 52 ++ .../{agent-create.ts => agent/create.ts} | 205 ++----- .../src/commands/managed-agent/agent/get.ts | 45 ++ .../src/commands/managed-agent/agent/list.ts | 53 ++ .../commands/managed-agent/agent/search.ts | 59 ++ .../commands/managed-agent/agent/versions.ts | 52 ++ .../src/commands/managed-agent/deployment.ts | 471 --------------- .../managed-agent/deployment/_set-paused.ts | 56 ++ .../managed-agent/deployment/_shared.ts | 160 +++++ .../managed-agent/deployment/create.ts | 288 +++++++++ .../commands/managed-agent/deployment/get.ts | 38 ++ .../commands/managed-agent/deployment/list.ts | 57 ++ .../managed-agent/deployment/pause.ts | 3 + .../commands/managed-agent/deployment/run.ts | 57 ++ .../managed-agent/deployment/runs/get.ts | 38 ++ .../managed-agent/deployment/runs/list.ts | 55 ++ .../managed-agent/deployment/search.ts | 67 +++ .../managed-agent/deployment/unpause.ts | 3 + .../src/commands/managed-agent/environment.ts | 174 ------ .../managed-agent/environment/_shared.ts | 41 ++ .../managed-agent/environment/create.ts | 157 +++++ .../commands/managed-agent/environment/get.ts | 41 ++ .../managed-agent/environment/list.ts | 53 ++ .../managed-agent/environment/search.ts | 68 +++ .../src/commands/managed-agent/skill-list.ts | 2 +- .../src/commands/managed-agent/skill.ts | 363 ------------ .../commands/managed-agent/skill/_shared.ts | 87 +++ .../commands/managed-agent/skill/create.ts | 124 ++++ .../commands/managed-agent/skill/download.ts | 49 ++ .../src/commands/managed-agent/skill/get.ts | 36 ++ .../src/commands/managed-agent/skill/list.ts | 94 +++ .../commands/managed-agent/skill/search.ts | 73 +++ .../commands/managed-agent/skill/versions.ts | 55 ++ .../src/commands/managed-agent/vault.ts | 165 ------ .../commands/managed-agent/vault/_shared.ts | 41 ++ .../commands/managed-agent/vault/create.ts | 108 ++++ .../managed-agent/vault/credential/create.ts | 344 +++++++++++ .../src/commands/managed-agent/vault/get.ts | 35 ++ .../src/commands/managed-agent/vault/list.ts | 53 ++ .../commands/managed-agent/vault/search.ts | 59 ++ packages/commands/src/index.ts | 61 +- .../tests/e2e/managed-agent.e2e.test.ts | 546 +++++++++++++++++- packages/commands/tests/e2e/topic-routes.ts | 5 + .../tests/managed-agent-create.test.ts | 19 +- skills/bailian-managed-agent/SKILL.md | 81 ++- .../bailian-managed-agent/reference/index.md | 127 ++-- .../reference/managed-agent.md | 336 +++++++++-- 50 files changed, 4096 insertions(+), 1753 deletions(-) create mode 100644 packages/commands/src/commands/managed-agent/_engine/scoped-create.ts delete mode 100644 packages/commands/src/commands/managed-agent/agent.ts create mode 100644 packages/commands/src/commands/managed-agent/agent/_shared.ts rename packages/commands/src/commands/managed-agent/{agent-create.ts => agent/create.ts} (58%) create mode 100644 packages/commands/src/commands/managed-agent/agent/get.ts create mode 100644 packages/commands/src/commands/managed-agent/agent/list.ts create mode 100644 packages/commands/src/commands/managed-agent/agent/search.ts create mode 100644 packages/commands/src/commands/managed-agent/agent/versions.ts delete mode 100644 packages/commands/src/commands/managed-agent/deployment.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/_set-paused.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/_shared.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/create.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/get.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/list.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/pause.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/run.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/runs/get.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/runs/list.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/search.ts create mode 100644 packages/commands/src/commands/managed-agent/deployment/unpause.ts delete mode 100644 packages/commands/src/commands/managed-agent/environment.ts create mode 100644 packages/commands/src/commands/managed-agent/environment/_shared.ts create mode 100644 packages/commands/src/commands/managed-agent/environment/create.ts create mode 100644 packages/commands/src/commands/managed-agent/environment/get.ts create mode 100644 packages/commands/src/commands/managed-agent/environment/list.ts create mode 100644 packages/commands/src/commands/managed-agent/environment/search.ts delete mode 100644 packages/commands/src/commands/managed-agent/skill.ts create mode 100644 packages/commands/src/commands/managed-agent/skill/_shared.ts create mode 100644 packages/commands/src/commands/managed-agent/skill/create.ts create mode 100644 packages/commands/src/commands/managed-agent/skill/download.ts create mode 100644 packages/commands/src/commands/managed-agent/skill/get.ts create mode 100644 packages/commands/src/commands/managed-agent/skill/list.ts create mode 100644 packages/commands/src/commands/managed-agent/skill/search.ts create mode 100644 packages/commands/src/commands/managed-agent/skill/versions.ts delete mode 100644 packages/commands/src/commands/managed-agent/vault.ts create mode 100644 packages/commands/src/commands/managed-agent/vault/_shared.ts create mode 100644 packages/commands/src/commands/managed-agent/vault/create.ts create mode 100644 packages/commands/src/commands/managed-agent/vault/credential/create.ts create mode 100644 packages/commands/src/commands/managed-agent/vault/get.ts create mode 100644 packages/commands/src/commands/managed-agent/vault/list.ts create mode 100644 packages/commands/src/commands/managed-agent/vault/search.ts diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 68b3784ef..26bc75ef1 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -153,6 +153,11 @@ import { managedAgentSkillList, managedAgentCapabilities, managedAgentAgentCreate, + managedAgentEnvironmentCreate, + managedAgentSkillCreate, + managedAgentVaultCreate, + managedAgentVaultCredentialCreate, + managedAgentDeploymentCreate, managedAgentAgentList, managedAgentAgentGet, managedAgentAgentSearch, @@ -356,17 +361,22 @@ export const commands: Record<string, AnyCommand> = { "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, diff --git a/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts b/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts new file mode 100644 index 000000000..5b78c634e --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/scoped-create.ts @@ -0,0 +1,447 @@ +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 { 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, + requestedProvider: string | undefined, +): 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 = resolveTargetProvider(loaded.config, requestedProvider); + 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 function resolveTargetProvider( + config: ResolvedProjectConfig, + requested: string | undefined, +): string { + if (requested === "all") { + throw new BailianError( + "--provider all is not valid for a single-resource create.", + ExitCode.USAGE, + ); + } + if (requested) { + if (requested in config.providers) return requested; + throw new BailianError( + `Provider '${requested}' is not configured in agents.yaml.`, + ExitCode.USAGE, + ); + } + const defaultProvider = config.defaults?.provider; + if (defaultProvider && defaultProvider !== "all") return defaultProvider; + const configuredProviders = Object.keys(config.providers); + if (configuredProviders.length === 1) return configuredProviders[0]!; + throw new BailianError( + "Cannot infer one target provider for this create command.", + ExitCode.USAGE, + "Pass --provider <name> when defaults.provider is 'all' or multiple providers are configured.", + ); +} + +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 retainedDeclarationError(error, input.resourceType); + } + 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)}`); + } +} + +function retainedDeclarationError(error: unknown, resourceType: string): BailianError { + if (error instanceof BailianError) { + return new BailianError( + error.message, + error.exitCode, + "The YAML declaration was kept. Fix the related dependency or provider error, then re-run the same create command.", + { api: error.api, rawResponse: error.rawResponse, cause: error.cause }, + ); + } + return new BailianError( + error instanceof Error ? error.message : String(error), + ExitCode.GENERAL, + `The ${resourceType} YAML declaration was kept; re-run the same create command after fixing the error.`, + ); +} diff --git a/packages/commands/src/commands/managed-agent/agent.ts b/packages/commands/src/commands/managed-agent/agent.ts deleted file mode 100644 index 9289dee94..000000000 --- a/packages/commands/src/commands/managed-agent/agent.ts +++ /dev/null @@ -1,236 +0,0 @@ -import type { CloudAgent } from "@openagentpack/sdk"; -import { getRemoteAgent, listRemoteAgents, listRemoteAgentVersions } 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, - INCLUDE_ARCHIVED_FLAG, - 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 { fetchAllPages } from "./_engine/pagination.ts"; - -const AGENT_LIST_FLAGS = { - ...API_TARGET_FLAGS, - ...CURSOR_FLAGS, - ...INCLUDE_ARCHIVED_FLAG, -}; - -const AGENT_SEARCH_FLAGS = { - ...API_TARGET_FLAGS, - limit: CURSOR_FLAGS.limit, - ...SEARCH_FLAGS, - ...INCLUDE_ARCHIVED_FLAG, -}; - -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; - -const AGENT_VERSIONS_FLAGS = { - ...API_TARGET_FLAGS, - ...CURSOR_FLAGS, - agentId: AGENT_GET_FLAGS.agentId, -}; - -function agentRows(agents: CloudAgent[]): string[][] { - return agents.map((agent) => [ - agent.id, - displayValue(agent.name), - displayValue(agent.version), - displayValue(agent.type), - displayValue(agent.updated_at), - ]); -} - -export const managedAgentAgentList = 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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentAgentGet = 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: ctx.flags.provider, - 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)}`); - }, -}); - -export const managedAgentAgentSearch = 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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentAgentVersions = 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: ctx.flags.provider, - 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.", - }); - }, -}); diff --git a/packages/commands/src/commands/managed-agent/agent/_shared.ts b/packages/commands/src/commands/managed-agent/agent/_shared.ts new file mode 100644 index 000000000..a8fb0639f --- /dev/null +++ b/packages/commands/src/commands/managed-agent/agent/_shared.ts @@ -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), + ]); +} diff --git a/packages/commands/src/commands/managed-agent/agent-create.ts b/packages/commands/src/commands/managed-agent/agent/create.ts similarity index 58% rename from packages/commands/src/commands/managed-agent/agent-create.ts rename to packages/commands/src/commands/managed-agent/agent/create.ts index 6af87cdf4..571e2aec4 100644 --- a/packages/commands/src/commands/managed-agent/agent-create.ts +++ b/packages/commands/src/commands/managed-agent/agent/create.ts @@ -1,14 +1,8 @@ -import { randomUUID } from "node:crypto"; -import { readFile, rename, stat, unlink, writeFile } from "node:fs/promises"; -import { basename, dirname, resolve } from "node:path"; import { buildAgentDecl, type IStateManager, - LocalFileStateBackend, planAgentResourcesWithStateBackend, - type ResolvedProjectConfig, type ResourceAddress, - resolveProjectConfigFromObject, syncAgentResourcesWithStateBackend, } from "@openagentpack/sdk"; import { @@ -20,13 +14,19 @@ import { } 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, resolveAgentProjectConfig } from "./_engine/config-loader.ts"; -import { assertProviderCredentials } from "./_engine/credentials.ts"; -import { withStdoutProtected } from "./_engine/console-capture.ts"; -import { withAgentErrors } from "./_engine/errors.ts"; -import { createFileStateScope } from "./_engine/file-state-manager.ts"; -import { installSdkTransport } from "./_engine/transport.ts"; +import { formatResourceLabel } from "../_engine/address-utils.ts"; +import { CREDENTIALS_NOTE } from "../_engine/config-loader.ts"; +import { withStdoutProtected } from "../_engine/console-capture.ts"; +import { withAgentErrors } from "../_engine/errors.ts"; +import { + loadScopedCreateProject, + normalizeResourceKey, + replaceConfigAtomically, + resolveCandidateDeclaration, + selectResourceKey, +} from "../_engine/scoped-create.ts"; + +export { replaceConfigAtomically } from "../_engine/scoped-create.ts"; const CREATE_FLAGS = { name: { @@ -122,28 +122,8 @@ interface AgentKeySelection { reusedPending: 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 normalizeAgentKey(displayName: string): string { - const normalized = displayName - .normalize("NFKC") - .trim() - .toLowerCase() - .replace(/[^\p{L}\p{N}]+/gu, "-") - .replace(/^-+|-+$/g, ""); - return normalized || "agent"; + return normalizeResourceKey(displayName, "agent"); } export function selectAgentKey(options: { @@ -153,80 +133,17 @@ export function selectAgentKey(options: { candidate: BuiltAgentDecl; state: IStateManager; }): AgentKeySelection { - const tracked = new Set( - options.state - .listResources() - .filter( - (resource) => - resource.address.provider === options.provider && - (resource.address.type === "agent" || resource.address.type === "template"), - ) - .map((resource) => resource.address.name), - ); - const candidateJson = canonicalJson(options.candidate); - for (const [key, declaration] of Object.entries(options.agents)) { - const effectiveName = declaration.name ?? key; - if (effectiveName !== options.displayName || tracked.has(key)) continue; - if (canonicalJson({ ...declaration, name: effectiveName }) === candidateJson) { - return { key, reusedPending: true }; - } - } - - const baseKey = normalizeAgentKey(options.displayName); - if (!(baseKey in options.agents)) return { key: baseKey, reusedPending: false }; - let suffix = 2; - while (`${baseKey}-${suffix}` in options.agents) suffix += 1; - return { key: `${baseKey}-${suffix}`, reusedPending: false }; -} - -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 Agent 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; - } -} - -function resolveTargetProvider( - config: ResolvedProjectConfig, - requested: string | undefined, -): string { - if (requested === "all") { - throw new BailianError("--provider all is not valid for Agent create.", ExitCode.USAGE); - } - if (requested) { - if (requested in config.providers) return requested; - throw new BailianError( - `Provider '${requested}' is not configured in agents.yaml.`, - ExitCode.USAGE, - ); - } - const defaultProvider = config.defaults?.provider; - if (defaultProvider && defaultProvider !== "all") return defaultProvider; - const configuredProviders = Object.keys(config.providers); - if (configuredProviders.length === 1) return configuredProviders[0]!; - throw new BailianError( - "Agent create cannot infer one target provider.", - ExitCode.USAGE, - "Pass --provider <name> when defaults.provider is 'all' or multiple providers are configured.", - ); + 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 rootAddress( @@ -271,32 +188,8 @@ export default defineCommand({ const { flags, settings } = ctx; const format = detectOutputFormat(settings.output); const file = flags.file ?? "agents.yaml"; - const sourceBeforeLoad = await readFile(resolve(file), "utf8").catch((error) => { - const code = (error as NodeJS.ErrnoException).code; - if (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(ctx, 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 = resolveTargetProvider(loaded.config, flags.provider); - if (!settings.dryRun) assertProviderCredentials(loaded.config.providers, [provider]); - installSdkTransport(ctx); + const project = await loadScopedCreateProject(ctx, file, flags.provider); + const provider = project.provider; const rawAgent = buildAgentDecl(undefined, { name: flags.name.trim(), @@ -309,34 +202,26 @@ export default defineCommand({ builtinTools: flags.tool, skills: flags.skill?.map((skillName) => ({ kind: "custom", name: skillName })), }).agent; - const temporaryKey = "__bailian_cli_agent_create_candidate__"; - const resolvedCandidate = await withAgentErrors(() => - resolveProjectConfigFromObject( - { - ...loaded.config, - agents: { ...loaded.config.agents, [temporaryKey]: rawAgent }, - }, - { projectName: loaded.projectName, basePath: dirname(loaded.configPath) }, - ), - ); - const candidateAgent = resolvedCandidate.config.agents![temporaryKey]! as BuiltAgentDecl; - const stateBackend = new LocalFileStateBackend({ configPath: loaded.configPath }); - const stateScope = createFileStateScope(loaded.configPath, loaded.projectName); - const keySelection = await stateBackend.read(stateScope, (state) => + const candidateAgent = (await resolveCandidateDeclaration({ + project, + 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: (loaded.config.agents ?? {}) as Record<string, BuiltAgentDecl>, + agents: (project.config.agents ?? {}) as Record<string, BuiltAgentDecl>, candidate: candidateAgent, state, }), ); const agentKey = keySelection.key; - const candidateConfig = structuredClone(loaded.config); + const candidateConfig = structuredClone(project.config); candidateConfig.agents = { ...candidateConfig.agents, [agentKey]: candidateAgent }; candidateConfig._resolved = true; - const document = parseDocument(source); + const document = parseDocument(project.source); if (document.errors.length > 0) { throw new BailianError( `YAML parse error: ${document.errors.map((error) => error.message).join("; ")}`, @@ -346,12 +231,12 @@ export default defineCommand({ document.setIn(["agents", agentKey], rawAgent); const nextSource = document.toString(); const backendInput = { - projectName: loaded.projectName, + projectName: project.projectName, config: candidateConfig, - configPath: loaded.configPath, + configPath: project.configPath, providers: { [provider]: candidateConfig.providers[provider] }, - stateBackend, - stateScope, + stateBackend: project.stateBackend, + stateScope: project.stateScope, }; if (settings.dryRun || !flags.yes) { @@ -369,7 +254,7 @@ export default defineCommand({ ); const result = { agent: { key: agentKey, name: flags.name.trim(), provider }, - config_file: loaded.configPath, + config_file: project.configPath, yaml_written: false, reused_pending: keySelection.reusedPending, requires_confirmation: !settings.dryRun, @@ -397,7 +282,7 @@ export default defineCommand({ return; } - await replaceConfigAtomically(loaded.configPath, source, nextSource); + await replaceConfigAtomically(project.configPath, project.source, nextSource); const run = await withAgentErrors(() => withStdoutProtected(() => syncAgentResourcesWithStateBackend(backendInput, agentKey, { @@ -408,7 +293,7 @@ export default defineCommand({ }), ), ); - const remoteId = await stateBackend.read(stateScope, (state) => { + const remoteId = await project.stateBackend.read(project.stateScope, (state) => { const selectedRoot = rootAddress( run.actions.map((action) => action.address), agentKey, @@ -417,7 +302,7 @@ export default defineCommand({ }); const result = { agent: { key: agentKey, name: flags.name.trim(), provider, remote_id: remoteId }, - config_file: loaded.configPath, + config_file: project.configPath, yaml_written: true, reused_pending: keySelection.reusedPending, status: run.status, @@ -428,7 +313,7 @@ export default defineCommand({ }; if (format === "json") emitResult(result, format); else { - emitBare(`Wrote ${loaded.configPath} with Agent key '${agentKey}'.`); + emitBare(`Wrote ${project.configPath} with Agent key '${agentKey}'.`); if (run.status === "completed") emitBare(`Created Agent '${flags.name.trim()}'.`); else emitBare(`Scoped create ${run.status}: ${run.error ?? "unknown error"}`); } diff --git a/packages/commands/src/commands/managed-agent/agent/get.ts b/packages/commands/src/commands/managed-agent/agent/get.ts new file mode 100644 index 000000000..bed98c70d --- /dev/null +++ b/packages/commands/src/commands/managed-agent/agent/get.ts @@ -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: ctx.flags.provider, + 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)}`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/agent/list.ts b/packages/commands/src/commands/managed-agent/agent/list.ts new file mode 100644 index 000000000..638df7977 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/agent/list.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/agent/search.ts b/packages/commands/src/commands/managed-agent/agent/search.ts new file mode 100644 index 000000000..555181c1a --- /dev/null +++ b/packages/commands/src/commands/managed-agent/agent/search.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/agent/versions.ts b/packages/commands/src/commands/managed-agent/agent/versions.ts new file mode 100644 index 000000000..e4f3d80d5 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/agent/versions.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment.ts b/packages/commands/src/commands/managed-agent/deployment.ts deleted file mode 100644 index f74cc2262..000000000 --- a/packages/commands/src/commands/managed-agent/deployment.ts +++ /dev/null @@ -1,471 +0,0 @@ -import type { DeploymentInfo, DeploymentRunInfo, ProjectRuntimeContext } from "@openagentpack/sdk"; -import { - getRemoteDeployment, - getRemoteDeploymentRun, - listRemoteDeploymentRuns, - listRemoteDeployments, - runRemoteDeployment, - setRemoteDeploymentPaused, - UserError, -} 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, - INCLUDE_ARCHIVED_FLAG, - SEARCH_FLAGS, - 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"; - -const DEPLOYMENT_ID_FLAG = { - deploymentId: { - type: "string", - valueHint: "<id>", - required: true, - description: { "en-US": "Deployment ID", "zh-CN": "Deployment ID" }, - }, -} as const; - -const 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; - -const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...LIST_FILTER_FLAGS }; -const GET_FLAGS = { ...API_TARGET_FLAGS, ...DEPLOYMENT_ID_FLAG }; -const SEARCH_RESOURCE_FLAGS = { - ...API_TARGET_FLAGS, - ...CURSOR_FLAGS, - query: SEARCH_FLAGS.query, - ...LIST_FILTER_FLAGS, -}; -const RUN_LIST_FLAGS = { ...API_TARGET_FLAGS, ...DEPLOYMENT_ID_FLAG, ...CURSOR_FLAGS }; -const RUN_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; -const 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; -const RUN_ACTION_FLAGS = { - ...ACTION_TARGET_FLAGS, - yes: { - type: "switch", - description: { "en-US": "Confirm deployment run", "zh-CN": "确认运行 Deployment" }, - }, -} as const; - -function validateActionTarget(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; -} - -function deploymentRows(deployments: DeploymentInfo[]): string[][] { - return deployments.map((deployment) => [ - displayValue(deployment.id), - deployment.status, - displayValue(deployment.schedule?.expression), - displayValue(deployment.paused_reason?.type), - ]); -} - -function runRows(runs: DeploymentRunInfo[]): string[][] { - return runs.map((run) => [ - run.id, - displayValue(run.deployment_id), - displayValue(run.session_id), - displayValue(run.status), - displayValue(run.created_at), - ]); -} - -async function resolveDeploymentTarget( - runtime: ProjectRuntimeContext, - options: { provider?: string; deployment?: string; deploymentId?: string }, -): Promise<string> { - if (options.deploymentId) return options.deploymentId; - const configuredProviders = Array.from(runtime.providers.keys()); - const provider = - options.provider ?? - (configuredProviders.length === 1 - ? configuredProviders[0] - : (() => { - throw new UserError("Multiple providers configured. Use --provider to specify one."); - })()); - const state = runtime.state.getResource({ - provider, - 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; -} - -export const managedAgentDeploymentList = 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: 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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentDeploymentGet = defineCommand({ - description: { - "en-US": "Get a Managed Agent deployment", - "zh-CN": "获取托管 Agent Deployment 详情", - }, - auth: "apiKey", - usageArgs: "--deployment-id <id>", - flags: 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: ctx.flags.provider, - }); - }), - ); - 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)}`); - } - }, -}); - -export const managedAgentDeploymentSearch = defineCommand({ - description: { - "en-US": "Search Managed Agent deployments", - "zh-CN": "搜索托管 Agent Deployment", - }, - auth: "apiKey", - usageArgs: "--query <text> [--limit <n>] [--page <cursor>] [--all]", - flags: SEARCH_RESOURCE_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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentDeploymentRunsList = 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: RUN_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: ctx.flags.provider, - 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: runRows(result.items), - hasMore: result.hasMore, - nextPage: result.nextPage, - emptyMessage: "No deployment runs found.", - }); - }, -}); - -export const managedAgentDeploymentRunsGet = defineCommand({ - description: { - "en-US": "Get a Managed Agent deployment run", - "zh-CN": "获取托管 Agent Deployment Run 详情", - }, - auth: "apiKey", - usageArgs: "--run-id <id>", - flags: RUN_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: ctx.flags.provider }); - }), - ); - 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)}`); - } - }, -}); - -export const managedAgentDeploymentRun = defineCommand({ - description: { - "en-US": "Run a Managed Agent deployment now", - "zh-CN": "立即运行托管 Agent Deployment", - }, - auth: "apiKey", - usageArgs: "(--deployment <name> | --deployment-id <id>) --yes", - flags: RUN_ACTION_FLAGS, - exampleArgs: ["--deployment daily-report --dry-run", "--deployment-id dep_abc --yes"], - notes: CREDENTIALS_NOTE, - validate: validateActionTarget, - 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; - } - if (!ctx.flags.yes) { - throw new BailianError( - "Refusing to run the deployment without confirmation.", - ExitCode.USAGE, - "Re-run with --yes or preview with --dry-run.", - ); - } - 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: ctx.flags.provider }), - }; - }), - ); - if (format === "json") - emitResult({ deployment_id: result.deploymentId, ...result.run }, format); - else emitBare(`Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}`); - }, -}); - -function createPauseCommand(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", - usageArgs: "(--deployment <name> | --deployment-id <id>)", - flags: ACTION_TARGET_FLAGS, - exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc`], - notes: CREDENTIALS_NOTE, - validate: validateActionTarget, - 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: ctx.flags.provider, - }), - }; - }), - ); - if (format === "json") { - emitResult({ deployment_id: result.deploymentId, deployment: result.deployment }, format); - } else { - emitBare(`Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.`); - } - }, - }); -} - -export const managedAgentDeploymentPause = createPauseCommand(true); -export const managedAgentDeploymentUnpause = createPauseCommand(false); diff --git a/packages/commands/src/commands/managed-agent/deployment/_set-paused.ts b/packages/commands/src/commands/managed-agent/deployment/_set-paused.ts new file mode 100644 index 000000000..4e3b4b68a --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/_set-paused.ts @@ -0,0 +1,56 @@ +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", + usageArgs: "(--deployment <name> | --deployment-id <id>)", + flags: DEPLOYMENT_ACTION_TARGET_FLAGS, + exampleArgs: [`--deployment daily-report --dry-run`, `--deployment-id dep_abc`], + 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: ctx.flags.provider, + }), + }; + }), + ); + if (format === "json") { + emitResult({ deployment_id: result.deploymentId, deployment: result.deployment }, format); + } else { + emitBare(`Deployment ${result.deploymentId} ${paused ? "paused" : "unpaused"}.`); + } + }, + }); +} diff --git a/packages/commands/src/commands/managed-agent/deployment/_shared.ts b/packages/commands/src/commands/managed-agent/deployment/_shared.ts new file mode 100644 index 000000000..a641d104c --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/_shared.ts @@ -0,0 +1,160 @@ +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 const DEPLOYMENT_RUN_ACTION_FLAGS = { + ...DEPLOYMENT_ACTION_TARGET_FLAGS, + yes: { + type: "switch", + description: { "en-US": "Confirm deployment run", "zh-CN": "确认运行 Deployment" }, + }, +} 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: { provider?: string; deployment?: string; deploymentId?: string }, +): Promise<string> { + if (options.deploymentId) return options.deploymentId; + const configuredProviders = Array.from(runtime.providers.keys()); + const provider = + options.provider ?? + (configuredProviders.length === 1 + ? configuredProviders[0] + : (() => { + throw new UserError("Multiple providers configured. Use --provider to specify one."); + })()); + const state = runtime.state.getResource({ + provider, + 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; +} diff --git a/packages/commands/src/commands/managed-agent/deployment/create.ts b/packages/commands/src/commands/managed-agent/deployment/create.ts new file mode 100644 index 000000000..c0446d39a --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/create.ts @@ -0,0 +1,288 @@ +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 条目(可重复)", + }, + }, + provider: { + type: "string", + valueHint: "<name>", + description: { + "en-US": "Target provider; inferred when unambiguous", + "zh-CN": "目标 Provider;可唯一确定时自动推断", + }, + }, + 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>...] [--provider <name>] [--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"}\' --message "Run" --yes', + "zh-CN": + '--name Daily --agent assistant --event \'{"type":"system.message","content":"保持简洁"}\' --message "执行" --yes', + }, + ], + notes: [ + ...CREDENTIALS_NOTE, + ...SCOPED_CREATE_NOTE, + { + "en-US": + "Initial Events must contain 1-50 user.message/system.message entries. --resource accepts only File Resources in this release.", + "zh-CN": + "Initial Events 必须包含 1–50 条 user.message/system.message;本期 --resource 仅接受 File Resource。", + }, + ], + 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 "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", + ctx.flags.provider, + ); + 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); + } +} diff --git a/packages/commands/src/commands/managed-agent/deployment/get.ts b/packages/commands/src/commands/managed-agent/deployment/get.ts new file mode 100644 index 000000000..59f244a52 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/get.ts @@ -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: ctx.flags.provider, + }); + }), + ); + 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)}`); + } + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment/list.ts b/packages/commands/src/commands/managed-agent/deployment/list.ts new file mode 100644 index 000000000..7c898c9d3 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/list.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment/pause.ts b/packages/commands/src/commands/managed-agent/deployment/pause.ts new file mode 100644 index 000000000..2b639d92c --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/pause.ts @@ -0,0 +1,3 @@ +import { createSetDeploymentPausedCommand } from "./_set-paused.ts"; + +export default createSetDeploymentPausedCommand(true); diff --git a/packages/commands/src/commands/managed-agent/deployment/run.ts b/packages/commands/src/commands/managed-agent/deployment/run.ts new file mode 100644 index 000000000..a6ebd7210 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/run.ts @@ -0,0 +1,57 @@ +import { runRemoteDeployment } from "@openagentpack/sdk"; +import { BailianError, defineCommand, detectOutputFormat, ExitCode } 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_RUN_ACTION_FLAGS, + resolveDeploymentTarget, + validateDeploymentActionTarget, +} from "./_shared.ts"; + +export default defineCommand({ + description: { + "en-US": "Run a Managed Agent deployment now", + "zh-CN": "立即运行托管 Agent Deployment", + }, + auth: "apiKey", + usageArgs: "(--deployment <name> | --deployment-id <id>) --yes", + flags: DEPLOYMENT_RUN_ACTION_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; + } + if (!ctx.flags.yes) { + throw new BailianError( + "Refusing to run the deployment without confirmation.", + ExitCode.USAGE, + "Re-run with --yes or preview with --dry-run.", + ); + } + 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: ctx.flags.provider }), + }; + }), + ); + if (format === "json") + emitResult({ deployment_id: result.deploymentId, ...result.run }, format); + else emitBare(`Deployment ${result.deploymentId} started. Run: ${result.run.run_id ?? "-"}`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment/runs/get.ts b/packages/commands/src/commands/managed-agent/deployment/runs/get.ts new file mode 100644 index 000000000..59ea3146b --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/runs/get.ts @@ -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: ctx.flags.provider }); + }), + ); + 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)}`); + } + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment/runs/list.ts b/packages/commands/src/commands/managed-agent/deployment/runs/list.ts new file mode 100644 index 000000000..face4cbf5 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/runs/list.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment/search.ts b/packages/commands/src/commands/managed-agent/deployment/search.ts new file mode 100644 index 000000000..d7c286148 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/search.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/deployment/unpause.ts b/packages/commands/src/commands/managed-agent/deployment/unpause.ts new file mode 100644 index 000000000..ac36bf29f --- /dev/null +++ b/packages/commands/src/commands/managed-agent/deployment/unpause.ts @@ -0,0 +1,3 @@ +import { createSetDeploymentPausedCommand } from "./_set-paused.ts"; + +export default createSetDeploymentPausedCommand(false); diff --git a/packages/commands/src/commands/managed-agent/environment.ts b/packages/commands/src/commands/managed-agent/environment.ts deleted file mode 100644 index 8ef2caa4d..000000000 --- a/packages/commands/src/commands/managed-agent/environment.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { CloudEnvironment } from "@openagentpack/sdk"; -import { getRemoteEnvironment, listRemoteEnvironments } 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, - INCLUDE_ARCHIVED_FLAG, - 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 { fetchAllPages } from "./_engine/pagination.ts"; - -const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...INCLUDE_ARCHIVED_FLAG }; -const SEARCH_RESOURCE_FLAGS = { - ...API_TARGET_FLAGS, - limit: CURSOR_FLAGS.limit, - ...SEARCH_FLAGS, - ...INCLUDE_ARCHIVED_FLAG, -}; -const GET_FLAGS = { - ...API_TARGET_FLAGS, - environmentId: { - type: "string", - valueHint: "<id>", - required: true, - description: { "en-US": "Environment ID", "zh-CN": "Environment ID" }, - }, -} as const; - -function environmentRows(environments: CloudEnvironment[]): string[][] { - return environments.map((environment) => [ - environment.id, - displayValue(environment.name), - displayValue(environment.scope), - displayValue(environment.version), - displayValue(environment.updated_at), - ]); -} - -export const managedAgentEnvironmentList = defineCommand({ - description: { "en-US": "List Managed Agent environments", "zh-CN": "列出托管 Agent 环境" }, - auth: "apiKey", - usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]", - flags: 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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentEnvironmentGet = defineCommand({ - description: { "en-US": "Get a Managed Agent environment", "zh-CN": "获取托管 Agent 环境详情" }, - auth: "apiKey", - usageArgs: "--environment-id <id>", - flags: 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: ctx.flags.provider, - }); - }), - ); - 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)}`); - }, -}); - -export const managedAgentEnvironmentSearch = defineCommand({ - description: { "en-US": "Search Managed Agent environments", "zh-CN": "搜索托管 Agent 环境" }, - auth: "apiKey", - usageArgs: "--query <text> [--limit <n>] [--page-limit <n>] [--include-archived]", - flags: SEARCH_RESOURCE_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: ctx.flags.provider, - 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.", - }); - }, -}); diff --git a/packages/commands/src/commands/managed-agent/environment/_shared.ts b/packages/commands/src/commands/managed-agent/environment/_shared.ts new file mode 100644 index 000000000..8c3bfc9ab --- /dev/null +++ b/packages/commands/src/commands/managed-agent/environment/_shared.ts @@ -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), + ]); +} diff --git a/packages/commands/src/commands/managed-agent/environment/create.ts b/packages/commands/src/commands/managed-agent/environment/create.ts new file mode 100644 index 000000000..813890aff --- /dev/null +++ b/packages/commands/src/commands/managed-agent/environment/create.ts @@ -0,0 +1,157 @@ +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"), + provider: providerFlag(), + 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>...] [--provider <name>] [--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", + ctx.flags.provider, + ); + 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 providerFlag() { + return { + type: "string" as const, + valueHint: "<name>", + description: { + "en-US": "Target provider; inferred when unambiguous", + "zh-CN": "目标 Provider;可唯一确定时自动推断", + }, + }; +} + +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 并执行定向远端创建", + }, + }; +} diff --git a/packages/commands/src/commands/managed-agent/environment/get.ts b/packages/commands/src/commands/managed-agent/environment/get.ts new file mode 100644 index 000000000..53de6a98f --- /dev/null +++ b/packages/commands/src/commands/managed-agent/environment/get.ts @@ -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: ctx.flags.provider, + }); + }), + ); + 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)}`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/environment/list.ts b/packages/commands/src/commands/managed-agent/environment/list.ts new file mode 100644 index 000000000..b143f7a5d --- /dev/null +++ b/packages/commands/src/commands/managed-agent/environment/list.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/environment/search.ts b/packages/commands/src/commands/managed-agent/environment/search.ts new file mode 100644 index 000000000..aa1a7fedc --- /dev/null +++ b/packages/commands/src/commands/managed-agent/environment/search.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/skill-list.ts b/packages/commands/src/commands/managed-agent/skill-list.ts index ee42b96a4..8e6a19fd5 100644 --- a/packages/commands/src/commands/managed-agent/skill-list.ts +++ b/packages/commands/src/commands/managed-agent/skill-list.ts @@ -1,2 +1,2 @@ // Backward-compatible alias for the API-oriented `managed-agent skill list` command. -export { managedAgentSkillList as default } from "./skill.ts"; +export { default } from "./skill/list.ts"; diff --git a/packages/commands/src/commands/managed-agent/skill.ts b/packages/commands/src/commands/managed-agent/skill.ts deleted file mode 100644 index 7a8b90a40..000000000 --- a/packages/commands/src/commands/managed-agent/skill.ts +++ /dev/null @@ -1,363 +0,0 @@ -import type { ProviderSkillInfo, SkillVersionInfo } from "@openagentpack/sdk"; -import { - downloadRemoteSkill, - getRemoteSkill, - listRemoteSkills, - listRemoteSkillVersions, -} 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 { writeOutputFile } from "./_engine/output-file.ts"; -import { fetchAllPages } from "./_engine/pagination.ts"; - -const SKILL_SOURCES = ["custom", "official", "all"] as const; -type SkillSource = (typeof SKILL_SOURCES)[number]; - -const SOURCE_FLAG = { - source: { - type: "string", - valueHint: "<source>", - choices: SKILL_SOURCES, - description: { - "en-US": "Skill catalog: custom (default), official, or all", - "zh-CN": "Skill Catalog:custom(默认)、official 或 all", - }, - }, -} as const; - -const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SOURCE_FLAG }; -const SEARCH_RESOURCE_FLAGS = { - ...API_TARGET_FLAGS, - limit: CURSOR_FLAGS.limit, - ...SEARCH_FLAGS, - ...SOURCE_FLAG, -}; -const GET_FLAGS = { - ...API_TARGET_FLAGS, - skillId: { - type: "string", - valueHint: "<id>", - required: true, - description: { "en-US": "Skill ID", "zh-CN": "Skill ID" }, - }, -} as const; -const VERSIONS_FLAGS = { - ...GET_FLAGS, - ...CURSOR_FLAGS, -}; -const DOWNLOAD_FLAGS = { - ...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; - -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), - ]); -} - -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), - ]); -} - -async function listOneCatalog( - runtime: Parameters<typeof listRemoteSkills>[0], - source: Exclude<SkillSource, "all">, - options: { provider?: string; limit?: number; page?: string; all?: boolean }, -) { - return fetchAllPages( - async (page) => { - const response = await listRemoteSkills(runtime, { - provider: options.provider, - source, - limit: options.limit, - page, - }); - return { items: response.data, hasMore: response.has_more, nextPage: response.next_page }; - }, - options.all, - options.page, - ); -} - -export const managedAgentSkillList = 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: 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, { - provider: ctx.flags.provider, - limit: ctx.flags.limit, - page: ctx.flags.page, - all: ctx.flags.all, - }); - } - const [custom, official] = await Promise.all([ - listOneCatalog(runtime, "custom", { - provider: ctx.flags.provider, - limit: ctx.flags.limit, - all: ctx.flags.all, - }), - listOneCatalog(runtime, "official", { - provider: ctx.flags.provider, - limit: ctx.flags.limit, - all: ctx.flags.all, - }), - ]); - return { - items: [...custom.items, ...official.items], - hasMore: custom.hasMore || official.hasMore, - nextPage: undefined, - }; - }), - ); - 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.", - }); - }, -}); - -export const managedAgentSkillGet = defineCommand({ - description: { "en-US": "Get a Managed Agent skill", "zh-CN": "获取托管 Agent Skill 详情" }, - auth: "apiKey", - usageArgs: "--skill-id <id>", - flags: 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: ctx.flags.provider }); - }), - ); - 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)}`); - }, -}); - -export const managedAgentSkillSearch = 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: SEARCH_RESOURCE_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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentSkillVersions = 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: 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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentSkillDownload = 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: 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: ctx.flags.provider, - }); - }), - ); - 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}`); - }, -}); diff --git a/packages/commands/src/commands/managed-agent/skill/_shared.ts b/packages/commands/src/commands/managed-agent/skill/_shared.ts new file mode 100644 index 000000000..b952fe536 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill/_shared.ts @@ -0,0 +1,87 @@ +import type { ProviderSkillInfo, SkillVersionInfo } from "@openagentpack/sdk"; +import { + API_TARGET_FLAGS, + CURSOR_FLAGS, + displayValue, + SEARCH_FLAGS, +} from "../_engine/api-helpers.ts"; + +export const SKILL_SOURCES = ["custom", "official", "all"] as const; +export type SkillSource = (typeof SKILL_SOURCES)[number]; + +const SOURCE_FLAG = { + source: { + type: "string", + valueHint: "<source>", + choices: SKILL_SOURCES, + description: { + "en-US": "Skill catalog: custom (default), official, or all", + "zh-CN": "Skill Catalog:custom(默认)、official 或 all", + }, + }, +} as const; + +export const SKILL_LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...SOURCE_FLAG }; + +export const SKILL_SEARCH_FLAGS = { + ...API_TARGET_FLAGS, + limit: CURSOR_FLAGS.limit, + ...SEARCH_FLAGS, + ...SOURCE_FLAG, +}; + +export const SKILL_GET_FLAGS = { + ...API_TARGET_FLAGS, + skillId: { + type: "string", + valueHint: "<id>", + required: true, + description: { "en-US": "Skill ID", "zh-CN": "Skill ID" }, + }, +} as const; + +export const SKILL_VERSIONS_FLAGS = { + ...SKILL_GET_FLAGS, + ...CURSOR_FLAGS, +}; + +export const SKILL_DOWNLOAD_FLAGS = { + ...SKILL_GET_FLAGS, + skillVersion: { + type: "string", + valueHint: "<version>", + required: true, + description: { "en-US": "Skill version", "zh-CN": "Skill 版本" }, + }, + outputFile: { + type: "string", + valueHint: "<path>", + required: true, + description: { "en-US": "Destination ZIP path", "zh-CN": "目标 ZIP 路径" }, + }, + force: { + type: "switch", + description: { "en-US": "Overwrite an existing output file", "zh-CN": "覆盖已存在的输出文件" }, + }, +} as const; + +export function skillRows(skills: ProviderSkillInfo[]): string[][] { + return skills.map((skill) => [ + skill.id, + displayValue(skill.name), + skill.source, + skill.status, + displayValue(skill.latest_version), + displayValue(skill.updated_at ?? skill.created_at), + ]); +} + +export function versionRows(versions: SkillVersionInfo[]): string[][] { + return versions.map((version) => [ + displayValue(version.version), + displayValue(version.name), + displayValue(version.type), + displayValue(version.status), + displayValue(version.updated_at ?? version.created_at), + ]); +} diff --git a/packages/commands/src/commands/managed-agent/skill/create.ts b/packages/commands/src/commands/managed-agent/skill/create.ts new file mode 100644 index 000000000..2ad512530 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill/create.ts @@ -0,0 +1,124 @@ +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", + }, + }, + provider: { + type: "string", + valueHint: "<name>", + description: { + "en-US": "Target provider; inferred when unambiguous", + "zh-CN": "目标 Provider;可唯一确定时自动推断", + }, + }, + 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> [--provider <name>] [--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", + ctx.flags.provider, + ); + 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, + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/skill/download.ts b/packages/commands/src/commands/managed-agent/skill/download.ts new file mode 100644 index 000000000..109ab37b8 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill/download.ts @@ -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: ctx.flags.provider, + }); + }), + ); + 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}`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/skill/get.ts b/packages/commands/src/commands/managed-agent/skill/get.ts new file mode 100644 index 000000000..f8cd69f04 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill/get.ts @@ -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: ctx.flags.provider }); + }), + ); + 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)}`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/skill/list.ts b/packages/commands/src/commands/managed-agent/skill/list.ts new file mode 100644 index 000000000..3221ae624 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill/list.ts @@ -0,0 +1,94 @@ +import { listRemoteSkills } 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_LIST_FLAGS, skillRows, type SkillSource } from "./_shared.ts"; + +async function listOneCatalog( + runtime: Parameters<typeof listRemoteSkills>[0], + source: Exclude<SkillSource, "all">, + options: { provider?: string; limit?: number; page?: string; all?: boolean }, +) { + return fetchAllPages( + async (page) => { + const response = await listRemoteSkills(runtime, { + provider: options.provider, + 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, { + provider: ctx.flags.provider, + limit: ctx.flags.limit, + page: ctx.flags.page, + all: ctx.flags.all, + }); + } + const [custom, official] = await Promise.all([ + listOneCatalog(runtime, "custom", { + provider: ctx.flags.provider, + limit: ctx.flags.limit, + all: ctx.flags.all, + }), + listOneCatalog(runtime, "official", { + provider: ctx.flags.provider, + limit: ctx.flags.limit, + all: ctx.flags.all, + }), + ]); + return { + items: [...custom.items, ...official.items], + hasMore: custom.hasMore || official.hasMore, + nextPage: undefined, + }; + }), + ); + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/skill/search.ts b/packages/commands/src/commands/managed-agent/skill/search.ts new file mode 100644 index 000000000..c11158080 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill/search.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/skill/versions.ts b/packages/commands/src/commands/managed-agent/skill/versions.ts new file mode 100644 index 000000000..bd11d0e5f --- /dev/null +++ b/packages/commands/src/commands/managed-agent/skill/versions.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/vault.ts b/packages/commands/src/commands/managed-agent/vault.ts deleted file mode 100644 index 38e916f47..000000000 --- a/packages/commands/src/commands/managed-agent/vault.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { CloudVault } from "@openagentpack/sdk"; -import { getRemoteVault, listRemoteVaults } 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, - INCLUDE_ARCHIVED_FLAG, - 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 { fetchAllPages } from "./_engine/pagination.ts"; - -const LIST_FLAGS = { ...API_TARGET_FLAGS, ...CURSOR_FLAGS, ...INCLUDE_ARCHIVED_FLAG }; -const SEARCH_RESOURCE_FLAGS = { - ...API_TARGET_FLAGS, - limit: CURSOR_FLAGS.limit, - ...SEARCH_FLAGS, - ...INCLUDE_ARCHIVED_FLAG, -}; -const GET_FLAGS = { - ...API_TARGET_FLAGS, - vaultId: { - type: "string", - valueHint: "<id>", - required: true, - description: { "en-US": "Vault ID", "zh-CN": "Vault ID" }, - }, -} as const; - -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), - ]); -} - -export const managedAgentVaultList = defineCommand({ - description: { "en-US": "List Managed Agent vaults", "zh-CN": "列出托管 Agent Vault" }, - auth: "apiKey", - usageArgs: "[--limit <n>] [--page <cursor>] [--all] [--include-archived]", - flags: 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: ctx.flags.provider, - 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.", - }); - }, -}); - -export const managedAgentVaultGet = defineCommand({ - description: { "en-US": "Get a Managed Agent vault", "zh-CN": "获取托管 Agent Vault 详情" }, - auth: "apiKey", - usageArgs: "--vault-id <id>", - flags: 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: ctx.flags.provider }); - }), - ); - 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)}`); - }, -}); - -export const managedAgentVaultSearch = 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: SEARCH_RESOURCE_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: ctx.flags.provider, - 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.", - }); - }, -}); diff --git a/packages/commands/src/commands/managed-agent/vault/_shared.ts b/packages/commands/src/commands/managed-agent/vault/_shared.ts new file mode 100644 index 000000000..4383329dd --- /dev/null +++ b/packages/commands/src/commands/managed-agent/vault/_shared.ts @@ -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), + ]); +} diff --git a/packages/commands/src/commands/managed-agent/vault/create.ts b/packages/commands/src/commands/managed-agent/vault/create.ts new file mode 100644 index 000000000..bb1133a5b --- /dev/null +++ b/packages/commands/src/commands/managed-agent/vault/create.ts @@ -0,0 +1,108 @@ +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 条目(可重复)", + }, + }, + provider: { + type: "string", + valueHint: "<name>", + description: { + "en-US": "Target provider; inferred when unambiguous", + "zh-CN": "目标 Provider;可唯一确定时自动推断", + }, + }, + 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>...] [--provider <name>] [--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", + ctx.flags.provider, + ); + 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, + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/vault/credential/create.ts b/packages/commands/src/commands/managed-agent/vault/credential/create.ts new file mode 100644 index 000000000..146474471 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/vault/credential/create.ts @@ -0,0 +1,344 @@ +import { dirname } from "node:path"; +import { + 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 { 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 条目(可重复)", + }, + }, + provider: { + type: "string", + valueHint: "<name>", + description: { + "en-US": "Target provider; inferred when unambiguous", + "zh-CN": "目标 Provider;可唯一确定时自动推断", + }, + }, + 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>...] [--provider <name>] [--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); + const project = await loadScopedCreateProject( + ctx, + ctx.flags.file ?? "agents.yaml", + ctx.flags.provider, + ); + 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, + ); + } + const secretValue = process.env[ctx.flags.secretEnv]; + 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) { + const message = error instanceof Error ? error.message : String(error); + throw new BailianError( + message, + error instanceof BailianError ? error.exitCode : ExitCode.GENERAL, + "The YAML declaration was kept. Restore the same secret environment variable and re-run this command; an already-created matching Credential will be adopted.", + ); + } + 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)); +} diff --git a/packages/commands/src/commands/managed-agent/vault/get.ts b/packages/commands/src/commands/managed-agent/vault/get.ts new file mode 100644 index 000000000..a4763187e --- /dev/null +++ b/packages/commands/src/commands/managed-agent/vault/get.ts @@ -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: ctx.flags.provider }); + }), + ); + 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)}`); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/vault/list.ts b/packages/commands/src/commands/managed-agent/vault/list.ts new file mode 100644 index 000000000..95c237ef3 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/vault/list.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/commands/managed-agent/vault/search.ts b/packages/commands/src/commands/managed-agent/vault/search.ts new file mode 100644 index 000000000..2ade13496 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/vault/search.ts @@ -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: ctx.flags.provider, + 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.", + }); + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index a3655122a..4b296aed9 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -149,39 +149,34 @@ export { default as managedAgentSessionSend } from "./commands/managed-agent/ses 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 managedAgentCapabilities } from "./commands/managed-agent/capabilities.ts"; -export { default as managedAgentAgentCreate } from "./commands/managed-agent/agent-create.ts"; -export { - managedAgentAgentGet, - managedAgentAgentList, - managedAgentAgentSearch, - managedAgentAgentVersions, -} from "./commands/managed-agent/agent.ts"; -export { - managedAgentEnvironmentGet, - managedAgentEnvironmentList, - managedAgentEnvironmentSearch, -} from "./commands/managed-agent/environment.ts"; -export { - managedAgentSkillDownload, - managedAgentSkillGet, - managedAgentSkillSearch, - managedAgentSkillVersions, -} from "./commands/managed-agent/skill.ts"; -export { - managedAgentVaultGet, - managedAgentVaultList, - managedAgentVaultSearch, -} from "./commands/managed-agent/vault.ts"; -export { - managedAgentDeploymentGet, - managedAgentDeploymentList, - managedAgentDeploymentPause, - managedAgentDeploymentRun, - managedAgentDeploymentRunsGet, - managedAgentDeploymentRunsList, - managedAgentDeploymentSearch, - managedAgentDeploymentUnpause, -} from "./commands/managed-agent/deployment.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, diff --git a/packages/commands/tests/e2e/managed-agent.e2e.test.ts b/packages/commands/tests/e2e/managed-agent.e2e.test.ts index f082b5a0a..ff1a56060 100644 --- a/packages/commands/tests/e2e/managed-agent.e2e.test.ts +++ b/packages/commands/tests/e2e/managed-agent.e2e.test.ts @@ -2,7 +2,13 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import http from "node:http"; import type { AddressInfo } from "node:net"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { basename, dirname, join } from "node:path"; +import { + LocalFileStateBackend, + planProjectWithStateBackend, + resolveProjectConfig, + type ResourceAddress, +} from "@openagentpack/sdk"; import { describe, expect, test } from "vite-plus/test"; import { parse } from "yaml"; import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts"; @@ -22,6 +28,48 @@ const DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES = [ "bailian.deployment.file.mount_path.duplicate", ]; +async function seedTrackedResources( + configPath: string, + addresses: ResourceAddress[], +): Promise<void> { + const loaded = await resolveProjectConfig(configPath); + const stateBackend = new LocalFileStateBackend({ configPath }); + const stateScope = { projectId: loaded.projectName ?? basename(dirname(configPath)) }; + const backendInput = { + projectName: loaded.projectName, + config: loaded.config, + configPath: loaded.configPath, + providers: loaded.config.providers, + stateBackend, + stateScope, + }; + const planned = await planProjectWithStateBackend(backendInput, { + provider: "bailian", + refresh: false, + quiet: true, + }); + await stateBackend.write(stateScope, (state) => { + for (const address of addresses) { + const action = planned.plan.actions.find( + (entry) => + entry.address.provider === address.provider && + entry.address.type === address.type && + entry.address.name === address.name, + ); + const contentHash = (action?.after as { content_hash?: string } | undefined)?.content_hash; + if (action?.action !== "create" || !contentHash) { + throw new Error(`Cannot seed tracked resource ${address.type}.${address.name}.`); + } + state.setResource({ + address, + remote_id: `${address.type}_${address.name}_e2e`, + content_hash: contentHash, + desired_hash: contentHash, + }); + } + }); +} + /** * managed-agent:help / 缺参不依赖密钥;所有 mutation 命令的 --dry-run * 必须在构建 SDK runtime(凭证注入 / 联网 / 写盘)之前短路,因此同样不需要密钥。 @@ -165,6 +213,43 @@ describe("e2e: managed-agent", () => { expect(stderr).toMatch(/--name|Missing required/i); }); + test.each([ + ["environment create", ["environment", "create"], /--name|--pip|--yes/i], + ["skill create", ["skill", "create"], /--source|--yes/i], + ["vault create", ["vault", "create"], /--name|--yes/i], + [ + "vault credential create", + ["vault", "credential", "create"], + /--vault|--secret-name|--secret-env|--yes/i, + ], + ["deployment create", ["deployment", "create"], /--name|--agent|--message|--yes/i], + ])("managed-agent %s --help 展示资源参数", async (_label, commandArgs, expected) => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + ...commandArgs, + "--help", + ]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(expected); + expect(stderr).not.toMatch(/--key/i); + }); + + test.each([ + [["environment", "create"], /--name|Missing required/i], + [["skill", "create"], /--source|Missing required/i], + [["vault", "create"], /--name|Missing required/i], + [["vault", "credential", "create"], /--vault|Missing required/i], + [["deployment", "create"], /--name|Missing required/i], + ])("managed-agent %s 缺少必填参数时退出为用法错误 (2)", async (commandArgs, expected) => { + const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + ...commandArgs, + "--quiet", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(expected); + }); + test("managed-agent agent create 默认只预览,不写 YAML", async () => { const sourceBefore = await readFile(AGENTS_YAML, "utf8"); const { stdout, stderr, exitCode } = await runCommandE2e( @@ -257,6 +342,243 @@ describe("e2e: managed-agent", () => { } }); + test("managed-agent environment create 远端失败后保留 YAML,重试复用 key", async () => { + const directory = await mkdtemp(join(tmpdir(), "bl-environment-create-e2e-")); + const configPath = join(directory, "agents.yaml"); + await writeFile(configPath, await readFile(AGENTS_YAML, "utf8"), "utf8"); + const requestBodies: Array<Record<string, unknown>> = []; + const server = http.createServer((request, response) => { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + const body = Buffer.concat(chunks).toString("utf8"); + if (body) requestBodies.push(JSON.parse(body) as Record<string, unknown>); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ message: "intentional environment create failure" })); + }); + }); + await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen)); + const address = server.address() as AddressInfo; + const args = [ + "managed-agent", + "environment", + "create", + "--name", + "Retry Environment", + "--pip", + "pandas", + "--file", + configPath, + "--yes", + "--output", + "json", + ]; + const env = { + DASHSCOPE_API_KEY: "sk-e2e-environment-create", + BAILIAN_BASE_URL: `http://127.0.0.1:${address.port}/api/v1/agentstudio`, + }; + try { + for (let attempt = 0; attempt < 2; attempt += 1) { + const result = await runCommandE2e(MANAGED_AGENT_ROUTES, args, env); + expect(result.exitCode, result.stderr).toBe(1); + } + const config = parse(await readFile(configPath, "utf8")) as { + environments: Record<string, { name?: string }>; + }; + expect(config.environments["retry-environment"]?.name).toBe("Retry Environment"); + expect(config.environments["retry-environment-2"]).toBeUndefined(); + expect(requestBodies).toHaveLength(2); + expect(requestBodies.every((body) => body.name === "Retry Environment")).toBe(true); + } finally { + await new Promise<void>((resolveClose) => server.close(() => resolveClose())); + await rm(directory, { recursive: true, force: true }); + } + }); + + test("vault credential create --yes 缺少 Secret env 时不写 YAML", async () => { + const directory = await mkdtemp(join(tmpdir(), "bl-vault-credential-secret-e2e-")); + const configPath = join(directory, "agents.yaml"); + const configSource = `version: "1" +providers: + bailian: + api_key: \${DASHSCOPE_API_KEY} + base_url: \${BAILIAN_BASE_URL} +defaults: + provider: bailian +vaults: + production: + display_name: Production + credentials: [] +`; + await writeFile(configPath, configSource, "utf8"); + try { + const { stderr, exitCode } = await runCommandE2e( + MANAGED_AGENT_ROUTES, + [ + "managed-agent", + "vault", + "credential", + "create", + "--vault", + "production", + "--name", + "api-token", + "--secret-name", + "API_TOKEN", + "--secret-env", + "E2E_SECRET_THAT_MUST_NOT_EXIST", + "--file", + configPath, + "--yes", + ], + { DASHSCOPE_API_KEY: "sk-e2e-vault-credential" }, + ); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/E2E_SECRET_THAT_MUST_NOT_EXIST.*not set or is empty/i); + expect(await readFile(configPath, "utf8")).toBe(configSource); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + + test("vault credential create 远端失败后复用 YAML 声明且不泄露 Secret", async () => { + const directory = await mkdtemp(join(tmpdir(), "bl-vault-credential-retry-e2e-")); + const configPath = join(directory, "agents.yaml"); + const requestBodies: Array<Record<string, unknown>> = []; + const server = http.createServer((request, response) => { + const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1"); + if (request.method === "GET" && requestUrl.pathname === "/vaults/vault_production_e2e") { + response.writeHead(200, { "Content-Type": "application/json" }); + response.end( + JSON.stringify({ id: "vault_production_e2e", type: "vault", display_name: "Production" }), + ); + return; + } + if ( + request.method === "GET" && + requestUrl.pathname === "/vaults/vault_production_e2e/credentials" + ) { + response.writeHead(200, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ data: [], next_page: null })); + return; + } + if ( + request.method === "POST" && + requestUrl.pathname === "/vaults/vault_production_e2e/credentials" + ) { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + requestBodies.push(JSON.parse(Buffer.concat(chunks).toString("utf8"))); + response.writeHead(500, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ message: "intentional credential create failure" })); + }); + return; + } + response.writeHead(404, { "Content-Type": "application/json" }); + response.end(JSON.stringify({ message: "unexpected mock route" })); + }); + await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen)); + const address = server.address() as AddressInfo; + const configSource = `version: "1" +providers: + bailian: + api_key: test + base_url: http://127.0.0.1:${address.port} +defaults: + provider: bailian +vaults: + production: + display_name: Production + credentials: [] +`; + await writeFile(configPath, configSource, "utf8"); + const args = [ + "managed-agent", + "vault", + "credential", + "create", + "--vault", + "production", + "--name", + "api-token", + "--secret-name", + "API_TOKEN", + "--secret-env", + "E2E_RETRY_SECRET", + "--file", + configPath, + "--yes", + "--output", + "json", + ]; + const env = { + DASHSCOPE_API_KEY: "sk-e2e-vault-credential", + E2E_RETRY_SECRET: "credential-secret-must-not-leak", + }; + try { + await seedTrackedResources(configPath, [ + { type: "vault", name: "production", provider: "bailian" }, + ]); + for (let attempt = 0; attempt < 2; attempt += 1) { + const result = await runCommandE2e(MANAGED_AGENT_ROUTES, args, env); + expect(result.exitCode).toBe(1); + expect(`${result.stdout}\n${result.stderr}`).not.toContain( + "credential-secret-must-not-leak", + ); + } + const config = parse(await readFile(configPath, "utf8")) as { + vaults: { production: { credentials: Array<{ name?: string; secret_value?: string }> } }; + }; + expect(config.vaults.production.credentials).toEqual([ + expect.objectContaining({ + name: "api-token", + secret_value: "${E2E_RETRY_SECRET}", + }), + ]); + expect(requestBodies).toHaveLength(2); + } finally { + await new Promise<void>((resolveClose) => server.close(() => resolveClose())); + await rm(directory, { recursive: true, force: true }); + } + }); + + test("deployment create 拒绝非法 Event 与不完整 Schedule", async () => { + const invalidEvent = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "deployment", + "create", + "--dry-run", + "--name", + "Invalid Event", + "--agent", + "assistant", + "--event", + '{"type":"tool.call","content":"x"}', + "--file", + AGENTS_YAML, + ]); + expect(invalidEvent.exitCode).toBe(2); + expect(invalidEvent.stderr).toMatch(/user\.message or system\.message/i); + + const incompleteSchedule = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "deployment", + "create", + "--name", + "Invalid Schedule", + "--agent", + "assistant", + "--message", + "Run", + "--schedule", + "0 9 * * *", + "--quiet", + ]); + expect(incompleteSchedule.exitCode).toBe(2); + expect(incompleteSchedule.stderr).toMatch(/--schedule and --timezone/i); + }); + test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => { const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", @@ -347,8 +669,20 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => ]); expect(exitCode, stderr).toBe(0); const data = parseStdoutJson<{ - operations?: Record<string, { supported?: boolean; reason?: string }>; + operations?: Record<string, { supported?: boolean; auth?: string; reason?: string }>; }>(stdout); + for (const operation of [ + "agent.create", + "environment.create", + "skill.create", + "vault.create", + "vault.credential.create", + "deployment.create", + ]) { + expect(data.operations?.[operation]).toEqual( + expect.objectContaining({ supported: true, auth: "api_key" }), + ); + } expect(data.operations?.["session.event.list"]?.supported).toBe(true); expect(data.operations?.["session_thread.list"]?.supported).toBe(false); expect(data.operations?.["session_thread.list"]?.reason).toMatch(/no independent Thread/i); @@ -474,6 +808,214 @@ describe("e2e: managed-agent(--dry-run 短路,不联网不写盘)", () => expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore); }); + test.each([ + [ + "environment", + ["environment", "create", "--name", "Create Environment", "--pip", "pandas"], + "environment", + "create-environment", + ], + ["vault", ["vault", "create", "--name", "Create Vault"], "vault", "create-vault"], + ])("%s create --dry-run 只规划目标资源且不改 YAML", async (_label, commandArgs, type, key) => { + const sourceBefore = await readFile(AGENTS_YAML, "utf8"); + const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + ...commandArgs, + "--dry-run", + "--file", + AGENTS_YAML, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + resource?: { type?: string; key?: string }; + yaml_written?: boolean; + actions?: Array<{ action?: string; address?: { type?: string; name?: string } }>; + }>(stdout); + expect(data.resource).toEqual(expect.objectContaining({ type, key })); + expect(data.yaml_written).toBe(false); + expect(data.actions).toContainEqual( + expect.objectContaining({ + action: "create", + address: expect.objectContaining({ type, name: key }), + }), + ); + expect( + data.actions?.every( + (action) => action.address?.type === type && action.address?.name === key, + ), + ).toBe(true); + expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore); + }); + + test("skill create --dry-run 从 SKILL.md 取 name 且不改 YAML", async () => { + const directory = await mkdtemp(join(tmpdir(), "bl-skill-create-e2e-")); + const skillPath = join(directory, "SKILL.md"); + const sourceBefore = await readFile(AGENTS_YAML, "utf8"); + await writeFile( + skillPath, + "---\nname: create-skill\ndescription: E2E Skill\n---\n\n# Create Skill\n", + "utf8", + ); + try { + const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "skill", + "create", + "--dry-run", + "--source", + skillPath, + "--file", + AGENTS_YAML, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + resource?: { type?: string; key?: string; name?: string }; + yaml_written?: boolean; + actions?: Array<{ action?: string; address?: { type?: string; name?: string } }>; + }>(stdout); + expect(data.resource).toEqual( + expect.objectContaining({ type: "skill", key: "create-skill", name: "create-skill" }), + ); + expect(data.yaml_written).toBe(false); + expect(data.actions).toContainEqual( + expect.objectContaining({ + action: "create", + address: expect.objectContaining({ type: "skill", name: "create-skill" }), + }), + ); + expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + + test("deployment create --dry-run 只要求依赖 no-op,不检查无关资源", async () => { + const directory = await mkdtemp(join(tmpdir(), "bl-deployment-create-e2e-")); + const configPath = join(directory, "agents.yaml"); + const configSource = `version: "1" +providers: + bailian: + api_key: test + workspace_id: ws_e2e +defaults: + provider: bailian +agents: + assistant: + name: Assistant + model: qwen3.8-max + instructions: Help the user. + provider: bailian +`; + await writeFile(configPath, configSource, "utf8"); + try { + await seedTrackedResources(configPath, [ + { type: "agent", name: "assistant", provider: "bailian" }, + ]); + const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "deployment", + "create", + "--dry-run", + "--name", + "Daily Report", + "--agent", + "assistant", + "--message", + "Generate the report", + "--file", + configPath, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + resource?: { type?: string; key?: string }; + yaml_written?: boolean; + ready_to_create?: boolean; + actions?: Array<{ action?: string; address?: { type?: string; name?: string } }>; + }>(stdout); + expect(data.resource).toEqual( + expect.objectContaining({ type: "deployment", key: "daily-report" }), + ); + expect(data.yaml_written).toBe(false); + expect(data.ready_to_create).toBe(true); + expect(data.actions?.filter((action) => action.action !== "no-op")).toEqual([ + expect.objectContaining({ + action: "create", + address: expect.objectContaining({ type: "deployment", name: "daily-report" }), + }), + ]); + expect(await readFile(configPath, "utf8")).toBe(configSource); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + + test("vault credential create --dry-run 不读取远端、不要求 Secret、不写 YAML", async () => { + const directory = await mkdtemp(join(tmpdir(), "bl-vault-credential-create-e2e-")); + const configPath = join(directory, "agents.yaml"); + const configSource = `version: "1" +providers: + bailian: + api_key: test + workspace_id: ws_e2e +defaults: + provider: bailian +vaults: + production: + display_name: Production + credentials: [] +`; + await writeFile(configPath, configSource, "utf8"); + try { + await seedTrackedResources(configPath, [ + { type: "vault", name: "production", provider: "bailian" }, + ]); + const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ + "managed-agent", + "vault", + "credential", + "create", + "--dry-run", + "--vault", + "production", + "--name", + "api-token", + "--secret-name", + "API_TOKEN", + "--secret-env", + "E2E_SECRET_THAT_IS_NOT_SET", + "--file", + configPath, + "--output", + "json", + ]); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + credential?: { name?: string; secret_name?: string; secret_env?: string }; + yaml_written?: boolean; + remote_checked?: boolean; + reuse_remote?: boolean; + }>(stdout); + expect(data.credential).toEqual({ + name: "api-token", + secret_name: "API_TOKEN", + secret_env: "E2E_SECRET_THAT_IS_NOT_SET", + }); + expect(data.yaml_written).toBe(false); + expect(data.remote_checked).toBe(false); + expect(data.reuse_remote).toBe(false); + expect(stdout).not.toContain("__dry_run_secret__"); + expect(await readFile(configPath, "utf8")).toBe(configSource); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + test("destroy --dry-run 仅输出计划", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [ "managed-agent", diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index ab962b2cb..d9b4d6776 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -203,17 +203,22 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = { "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", diff --git a/packages/commands/tests/managed-agent-create.test.ts b/packages/commands/tests/managed-agent-create.test.ts index edbe39fd3..48d23a3a7 100644 --- a/packages/commands/tests/managed-agent-create.test.ts +++ b/packages/commands/tests/managed-agent-create.test.ts @@ -7,7 +7,11 @@ import { normalizeAgentKey, replaceConfigAtomically, selectAgentKey, -} from "../src/commands/managed-agent/agent-create.ts"; +} 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, { @@ -24,6 +28,17 @@ test("Agent key 从显示名生成并保留 Unicode", () => { 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({ @@ -86,7 +101,7 @@ test("YAML 替换校验原内容并保留文件权限", async () => { await expect( replaceConfigAtomically(configPath, "version: stale\n", "version: overwritten\n"), - ).rejects.toThrow(/changed while Agent create was being prepared/); + ).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 }); diff --git a/skills/bailian-managed-agent/SKILL.md b/skills/bailian-managed-agent/SKILL.md index 69b55f361..3efbf9931 100644 --- a/skills/bailian-managed-agent/SKILL.md +++ b/skills/bailian-managed-agent/SKILL.md @@ -20,14 +20,15 @@ description: >- ## Safety guardrail (the most important rule) -`apply` / `destroy` **mutate persistent remote resources** and only execute when `--yes` is passed: +`apply` / `destroy` and single-resource `create --yes` **mutate persistent remote resources**: 1. For `agents.yaml` resource changes, always run `bl managed-agent plan` first and show the diff to the user. -2. Only after explicit user confirmation, retry `apply` / `destroy` with `--yes`. -3. Never add `--yes` on your own initiative before the user has confirmed. +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. -API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault 的持久配置仍通过 -`agents.yaml → plan → apply` 管理;命令式写操作只覆盖 Session、Event、File 和 Deployment 运行时动作。 +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` 也需要先 `--dry-run`,确认后才传 `--yes`。 ## IaC lifecycle @@ -40,23 +41,49 @@ API-oriented commands do not replace IaC. Agent / Environment / Skill / Vault 5. Destroy bl managed-agent destroy --yes # only after user confirmation ``` -## Scoped single-Agent create +## Scoped single-resource create -`bl managed-agent agent create`仍然先把声明写入 `agents.yaml`,再通过 SDK 的定向 Plan/Apply 创建远端资源, -不是绕过 State 的命令式 API 调用: +以下命令都先构造 `agents.yaml` 声明,再通过 SDK 的定向 Plan/Apply 创建远端资源,不绕过 State: -- 用户只提供 Agent `name`;CLI 自动生成稳定的 YAML 逻辑 key,同名 Agent 用递增后缀并存。 -- 默认只预览自动 key 和定向计划;`--dry-run` 完全离线,只有显式 `--yes` 才写 YAML 并创建远端 Agent。 -- 定向流程只刷新目标 Agent 及其传递依赖;无关资源不检测 Drift、不产生 action,也不阻塞。 -- 目标 Agent 必须是 `create`,相关依赖必须已经处于 `no-op`;项目级 Drift 和删除仍由全量 `plan/apply` 处理。 +| 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。 +- 默认只预览自动 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/` 或 `bl managed-agent agent create --help` 为准。 +具体 flags、usage 和 examples 以 `reference/` 或对应命令的 `--help` 为准。 ## Deployment as IaC Deployment 与 Agent 一样声明在 `agents.yaml` 中,并复用同一条 `validate → plan → apply → destroy` IaC 链路; -CLI 不提供绕过 state 的命令式 Deployment CRUD。最小配置: +`deployment create` 可追加一条声明并走定向 Apply;CLI 不提供绕过 state 的 Deployment create/update/delete。最小配置: ```yaml deployments: @@ -89,19 +116,19 @@ Managed Agents 的子线程通过 Event 中的 `session_thread_id` 暴露;公 ## API-oriented resource commands -| Intent | Command family | -| -------------------------------------- | ------------------------------- | ------- | ------------------------------ | --------- | --------- | ------- | -| Check exact API support/auth/reason | `bl managed-agent capabilities` | -| Discover agents and versions | `agent list | get | search | versions` | -| Discover environments | `environment list | get | search` | -| Discover skills and download a version | `skill list | get | search | versions | download` | -| Inspect vault envelopes | `vault list | get | search` | -| Inspect deployments and run history | `deployment list | get | search`, `deployment runs list | get` | -| Run or pause deployments | `deployment run | pause | unpause` | -| Manage session metadata/lifecycle | `session list | get | search | update | archive | delete` | -| Work with raw events | `session event send | list | stream` | -| Diagnose/export a session | `session debug | export` | -| Work with files | `file upload | list | get | search | download | delete` | +| Intent | Command family | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Check exact API support/auth/reason | `bl managed-agent capabilities` | +| 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`。 diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index 07f109f2b..d019fdf6d 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -9,70 +9,75 @@ Use this index for the skill-scoped quick index and global flags. ## Quick index -| 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 capabilities` | No Auth | Show operation-level Managed Agents API capabilities | [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 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 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 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 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 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) | +| 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 capabilities` | No Auth | Show operation-level Managed Agents API capabilities | [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 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 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` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `capabilities`, `deployment get`, `deployment list`, `deployment pause`, `deployment run`, `deployment runs get`, `deployment runs list`, `deployment search`, `deployment unpause`, `destroy`, `environment get`, `environment list`, `environment search`, `file delete`, `file download`, `file get`, `file list`, `file search`, `file upload`, `init`, `plan`, `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 download`, `skill get`, `skill list`, `skill search`, `skill versions`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate`, `vault get`, `vault list`, `vault search` | [managed-agent.md](managed-agent.md) | +| Group | Commands | Reference | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ | +| `managed-agent` | `agent create`, `agent get`, `agent list`, `agent search`, `agent versions`, `apply`, `capabilities`, `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`, `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 diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index 93600bc71..25cc4fe84 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -7,64 +7,69 @@ Index: [index.md](index.md) ## Commands in this group -| Command | Authentication | Description | -| --------------------------------------- | -------------- | ------------------------------------------------------------------- | -| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | -| `bl managed-agent agent get` | API Key | Get a Managed Agent | -| `bl managed-agent agent list` | API Key | List Managed Agents | -| `bl managed-agent agent search` | API Key | Search Managed Agents | -| `bl managed-agent agent versions` | API Key | List Managed Agent versions | -| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | -| `bl managed-agent capabilities` | No Auth | Show operation-level Managed Agents API capabilities | -| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | -| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | -| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | -| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | -| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | -| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | -| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | -| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | -| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | -| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | -| `bl managed-agent environment list` | API Key | List Managed Agent environments | -| `bl managed-agent environment search` | API Key | Search Managed Agent environments | -| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | -| `bl managed-agent file download` | API Key | Download Managed Agent file content | -| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | -| `bl managed-agent file list` | API Key | List Managed Agent files | -| `bl managed-agent file search` | API Key | Search Managed Agent files | -| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | -| `bl managed-agent init` | No Auth | Create a new agents.yaml template | -| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | -| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | -| `bl managed-agent session create` | API Key | Create a new session for an agent | -| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | -| `bl managed-agent session delete` | API Key | Delete a session | -| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | -| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | -| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | -| `bl managed-agent session events` | API Key | List events for a Managed Agent session | -| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | -| `bl managed-agent session get` | API Key | Get details of a session | -| `bl managed-agent session list` | API Key | List sessions from the provider | -| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | -| `bl managed-agent session search` | API Key | Search Managed Agent sessions | -| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | -| `bl managed-agent session update` | API Key | Update a Managed Agent session | -| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | -| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | -| `bl managed-agent skill list` | API Key | List Managed Agent skills | -| `bl managed-agent skill search` | API Key | Search Managed Agent skills | -| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | -| `bl managed-agent skill-list` | API Key | List Managed Agent skills | -| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | -| `bl managed-agent state list` | No Auth | List resources tracked in agents state | -| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | -| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | -| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | -| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | -| `bl managed-agent vault list` | API Key | List Managed Agent vaults | -| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | +| Command | Authentication | Description | +| ------------------------------------------ | -------------- | ---------------------------------------------------------------------------- | +| `bl managed-agent agent create` | API Key | Declare and create one Managed Agent through an isolated YAML apply | +| `bl managed-agent agent get` | API Key | Get a Managed Agent | +| `bl managed-agent agent list` | API Key | List Managed Agents | +| `bl managed-agent agent search` | API Key | Search Managed Agents | +| `bl managed-agent agent versions` | API Key | List Managed Agent versions | +| `bl managed-agent apply` | API Key | Apply planned changes to create/update/delete agent resources | +| `bl managed-agent capabilities` | No Auth | Show operation-level Managed Agents API capabilities | +| `bl managed-agent deployment create` | API Key | Declare and create one Managed Agent Deployment through a scoped YAML apply | +| `bl managed-agent deployment get` | API Key | Get a Managed Agent deployment | +| `bl managed-agent deployment list` | API Key | List Managed Agent deployments | +| `bl managed-agent deployment pause` | API Key | Pause a Managed Agent deployment | +| `bl managed-agent deployment run` | API Key | Run a Managed Agent deployment now | +| `bl managed-agent deployment runs get` | API Key | Get a Managed Agent deployment run | +| `bl managed-agent deployment runs list` | API Key | List runs for a Managed Agent deployment | +| `bl managed-agent deployment search` | API Key | Search Managed Agent deployments | +| `bl managed-agent deployment unpause` | API Key | Unpause a Managed Agent deployment | +| `bl managed-agent destroy` | API Key | Destroy all managed agent resources tracked in state | +| `bl managed-agent environment create` | API Key | Declare and create one Managed Agent Environment through a scoped YAML apply | +| `bl managed-agent environment get` | API Key | Get a Managed Agent environment | +| `bl managed-agent environment list` | API Key | List Managed Agent environments | +| `bl managed-agent environment search` | API Key | Search Managed Agent environments | +| `bl managed-agent file delete` | API Key | Delete a Managed Agent file | +| `bl managed-agent file download` | API Key | Download Managed Agent file content | +| `bl managed-agent file get` | API Key | Get Managed Agent file metadata | +| `bl managed-agent file list` | API Key | List Managed Agent files | +| `bl managed-agent file search` | API Key | Search Managed Agent files | +| `bl managed-agent file upload` | API Key | Upload a Managed Agent file | +| `bl managed-agent init` | No Auth | Create a new agents.yaml template | +| `bl managed-agent plan` | API Key | Show what changes would be applied to agent infrastructure | +| `bl managed-agent session archive` | API Key | Archive a Managed Agent session | +| `bl managed-agent session create` | API Key | Create a new session for an agent | +| `bl managed-agent session debug` | API Key | Aggregate session diagnostics | +| `bl managed-agent session delete` | API Key | Delete a session | +| `bl managed-agent session event list` | API Key | List events for a Managed Agent session | +| `bl managed-agent session event send` | API Key | Send raw events to a Managed Agent session | +| `bl managed-agent session event stream` | API Key | Stream events from a Managed Agent session | +| `bl managed-agent session events` | API Key | List events for a Managed Agent session | +| `bl managed-agent session export` | API Key | Export session diagnostics as a ZIP | +| `bl managed-agent session get` | API Key | Get details of a session | +| `bl managed-agent session list` | API Key | List sessions from the provider | +| `bl managed-agent session run` | API Key | Create a session, send a message, and stream the response | +| `bl managed-agent session search` | API Key | Search Managed Agent sessions | +| `bl managed-agent session send` | API Key | Send a message to an existing session and stream the response | +| `bl managed-agent session update` | API Key | Update a Managed Agent session | +| `bl managed-agent skill create` | API Key | Declare and create one custom Managed Agent Skill from a local source | +| `bl managed-agent skill download` | API Key | Download a Managed Agent skill version | +| `bl managed-agent skill get` | API Key | Get a Managed Agent skill | +| `bl managed-agent skill list` | API Key | List Managed Agent skills | +| `bl managed-agent skill search` | API Key | Search Managed Agent skills | +| `bl managed-agent skill versions` | API Key | List Managed Agent skill versions | +| `bl managed-agent skill-list` | API Key | List Managed Agent skills | +| `bl managed-agent state import` | API Key | Import an existing remote resource into agents state | +| `bl managed-agent state list` | No Auth | List resources tracked in agents state | +| `bl managed-agent state rm` | No Auth | Remove a resource from state without destroying it remotely | +| `bl managed-agent state show` | No Auth | Show details of a resource in agents state | +| `bl managed-agent validate` | No Auth | Validate an agents.yaml configuration (offline) | +| `bl managed-agent vault create` | API Key | Declare and create one empty Managed Agent Vault through a scoped YAML apply | +| `bl managed-agent vault credential create` | API Key | Append and create one environment-variable Credential in a tracked Vault | +| `bl managed-agent vault get` | API Key | Get a Managed Agent vault | +| `bl managed-agent vault list` | API Key | List Managed Agent vaults | +| `bl managed-agent vault search` | API Key | Search Managed Agent vaults | ## Command details @@ -332,6 +337,55 @@ bl managed-agent capabilities bl managed-agent capabilities --provider bailian --output json ``` +### `bl managed-agent deployment create` + +| Field | Value | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent deployment create` | +| **Description** | Declare and create one Managed Agent Deployment through a scoped YAML apply | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent deployment create --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>...] [--provider <name>] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| -------------------------- | ------ | -------- | ----------------------------------------------------------------------- | +| `--name <name>` | string | yes | Remote Deployment display name; the YAML key is generated automatically | +| `--agent <yaml-key>` | string | yes | Existing Agent key from agents.yaml | +| `--agent-version <number>` | number | no | Agent version | +| `--environment <yaml-key>` | string | no | Existing Environment key from agents.yaml | +| `--vault <yaml-key>` | array | no | Existing Vault key from agents.yaml (repeatable) | +| `--message <text>` | array | no | Initial user message (repeatable) | +| `--event <json\|@path>` | array | no | Initial user.message or system.message JSON (repeatable) | +| `--resource <json\|@path>` | array | no | File Resource JSON with source or file_id (repeatable) | +| `--schedule <cron>` | string | no | Five-field cron expression | +| `--timezone <timezone>` | string | no | IANA schedule timezone | +| `--description <text>` | string | no | Deployment description | +| `--metadata <key=value>` | array | no | Metadata entry (repeatable) | +| `--provider <name>` | string | no | Target provider; inferred when unambiguous | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and run the scoped remote create | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- 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. +- Initial Events must contain 1-50 user.message/system.message entries. --resource accepts only File Resources in this release. + +#### Examples + +```bash +bl managed-agent deployment create --name Daily --agent assistant --message "Generate the report" +``` + +```bash +bl managed-agent deployment create --name Daily --agent assistant --event '{"type":"system.message","content":"Be concise"}' --message "Run" --yes +``` + ### `bl managed-agent deployment get` | Field | Value | @@ -662,6 +716,52 @@ bl managed-agent destroy --yes bl managed-agent destroy --yes --cascade ``` +### `bl managed-agent environment create` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent environment create` | +| **Description** | Declare and create one Managed Agent Environment through a scoped YAML apply | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent environment create --name <name> [--description <text>] [--metadata <key=value>...] [--apt <package>...] [--pip <package>...] [--npm <package>...] [--cargo <package>...] [--gem <package>...] [--go <package>...] [--provider <name>] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------ | ------ | -------- | ------------------------------------------------------------------------ | +| `--name <name>` | string | yes | Remote Environment display name; the YAML key is generated automatically | +| `--description <text>` | string | no | Environment description | +| `--metadata <key=value>` | array | no | Metadata entry (repeatable) | +| `--apt <package>` | array | no | APT package (repeatable) | +| `--pip <package>` | array | no | pip package (repeatable) | +| `--npm <package>` | array | no | npm package (repeatable) | +| `--cargo <package>` | array | no | Cargo package (repeatable) | +| `--gem <package>` | array | no | Ruby gem package (repeatable) | +| `--go <package>` | array | no | Go package (repeatable) | +| `--provider <name>` | string | no | Target provider; inferred when unambiguous | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and run the scoped remote create | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- 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. +- Creates a cloud Environment with unrestricted networking. Without --yes, only previews the generated YAML key and scoped plan. + +#### Examples + +```bash +bl managed-agent environment create --name Development +``` + +```bash +bl managed-agent environment create --name Development --pip pandas --npm typescript --metadata owner=platform --yes +``` + ### `bl managed-agent environment get` | Field | Value | @@ -1624,6 +1724,44 @@ bl managed-agent session update --session-id sess_abc --title 'investigation' bl managed-agent session update --session-id sess_abc --metadata @metadata.json ``` +### `bl managed-agent skill create` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent skill create` | +| **Description** | Declare and create one custom Managed Agent Skill from a local source | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent skill create --source <directory\|zip\|SKILL.md> [--provider <name>] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------------- | ------ | -------- | --------------------------------------------------------- | +| `--source <directory\|zip\|SKILL.md>` | string | yes | Local Skill directory, ZIP archive, or single SKILL.md | +| `--provider <name>` | string | no | Target provider; inferred when unambiguous | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and upload the Skill through the scoped create | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- 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. +- The YAML key is derived from SKILL.md frontmatter name. Remote URLs remain available through handwritten YAML plus full apply. + +#### Examples + +```bash +bl managed-agent skill create --source ./skills/code-review +``` + +```bash +bl managed-agent skill create --source ./skill.zip --yes +``` + ### `bl managed-agent skill download` | Field | Value | @@ -1993,6 +2131,88 @@ bl managed-agent validate bl managed-agent validate --file agents.yaml ``` +### `bl managed-agent vault create` + +| Field | Value | +| ------------------ | --------------------------------------------------------------------------------------------------------------------- | +| **Name** | `managed-agent vault create` | +| **Description** | Declare and create one empty Managed Agent Vault through a scoped YAML apply | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent vault create --name <name> [--metadata <key=value>...] [--provider <name>] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------ | ------ | -------- | ------------------------------------------------------------------ | +| `--name <name>` | string | yes | Remote Vault display name; the YAML key is generated automatically | +| `--metadata <key=value>` | array | no | Metadata entry (repeatable) | +| `--provider <name>` | string | no | Target provider; inferred when unambiguous | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and run the scoped remote create | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- 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. +- Creates an empty Vault. Add secrets later with `vault credential create`. + +#### Examples + +```bash +bl managed-agent vault create --name Production +``` + +```bash +bl managed-agent vault create --name Production --metadata owner=platform --yes +``` + +### `bl managed-agent vault credential create` + +| Field | Value | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Name** | `managed-agent vault credential create` | +| **Description** | Append and create one environment-variable Credential in a tracked Vault | +| **Authentication** | API Key | +| **Usage** | `bl managed-agent vault credential create --vault <yaml-key> --name <name> --secret-name <name> --secret-env <env-name> [--metadata <key=value>...] [--provider <name>] [--file <path>] [--yes]` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------- | ------ | -------- | ------------------------------------------------------ | +| `--vault <yaml-key>` | string | yes | Existing tracked Vault key from agents.yaml | +| `--name <name>` | string | yes | Credential display name | +| `--secret-name <name>` | string | yes | Environment variable name exposed to the Agent | +| `--secret-env <env-name>` | string | yes | Local environment variable containing the secret value | +| `--metadata <key=value>` | array | no | Metadata entry (repeatable) | +| `--provider <name>` | string | no | Target provider; inferred when unambiguous | +| `--file <path>` | string | no | Config file path (default: agents.yaml) | +| `--yes` | switch | no | Write YAML and create the remote Credential | +| `--api-key <key>` | string | no | API key | +| `--base-url <url>` | string | no | API base URL | + +#### Notes + +- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile). +- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json. +- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env. +- 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. +- --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. +- YAML stores only ${ENV_NAME}. Never commit .env; subsequent full apply runs must provide the same environment variable. + +#### Examples + +```bash +bl managed-agent vault credential create --vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN +``` + +```bash +bl managed-agent vault credential create --vault production --name api-token --secret-name API_TOKEN --secret-env PROD_API_TOKEN --yes +``` + ### `bl managed-agent vault get` | Field | Value | From 449eede2375eb6573ecac3c7c937e05e6e193372 Mon Sep 17 00:00:00 2001 From: chenanran555 <car534511@alibaba-inc.com> Date: Thu, 27 Aug 2026 20:47:51 +0800 Subject: [PATCH 4/4] fix(managed-agent): upgrade OpenAgentPack SDK to 0.4.0 beta --- packages/commands/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/commands/package.json b/packages/commands/package.json index 88eefb5d8..d3d165669 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -40,7 +40,7 @@ "check": "vp check" }, "dependencies": { - "@openagentpack/sdk": "0.3.2", + "@openagentpack/sdk": "0.4.0-beta-2472720-20260827", "bailian-cli-core": "workspace:*", "bailian-cli-runtime": "workspace:*", "boxen": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f4e769de..fb6fb22c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,8 +189,8 @@ importers: packages/commands: dependencies: '@openagentpack/sdk': - specifier: 0.3.2 - version: 0.3.2 + specifier: 0.4.0-beta-2472720-20260827 + version: 0.4.0-beta-2472720-20260827 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.4.0-beta-2472720-20260827': + resolution: {integrity: sha512-Ms9xeYKllgaeeBKvlb5O6rvSn3mHw2PWJ6SEY+08Vayti3Fi7yhRqb4vOGfC1L3+vQtl0qRf2Bqsr3b8dyx4zw==} 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.4.0-beta-2472720-20260827': dependencies: jszip: 3.10.1 yaml: 2.9.0