UNPKG

@botpress/adk-cli

Version:

Command-line interface for the Botpress Agent Development Kit (ADK)

612 lines (609 loc) 21.5 kB
// @bun import { JSON_ONLY_FORMAT_ERROR, ensureJsonOnlyFormat } from "./chunk-7sfagm12.js"; import { DEV_SERVER_DOWN_ERROR, getDevServerConfig, getDevServerWorkflow, listDevServerWorkflows } from "./chunk-ndxsgd72.js"; import { createCliLogger } from "./chunk-gzwt1qdr.js"; import"./chunk-nxy2ya5r.js"; import"./chunk-wzj4dc7n.js"; import { getProjectClient } from "./chunk-p0hjqn4r.js"; import"./chunk-np5wcwfv.js"; import"./chunk-dq2xpa24.js"; import"./chunk-6w0knnta.js"; import"./chunk-40x04ckt.js"; import"./chunk-t76d8fxx.js"; import"./chunk-nh2akp42.js"; import"./chunk-0fdvzjbh.js"; import"./chunk-2a5b6azq.js"; import"./chunk-vay209b5.js"; import"./chunk-3xrpxgq4.js"; import"./chunk-rfm3jr1m.js"; import"./chunk-w346ejn9.js"; import"./chunk-knvm2anf.js"; import"./chunk-65h5trb5.js"; import"./chunk-s2akeqpw.js"; import"./chunk-6771vrjp.js"; import"./chunk-g8mm42v1.js"; import"./chunk-50hzjdck.js"; import"./chunk-nn2jb0x0.js"; import"./chunk-v8xvth6j.js"; import"./chunk-kkk13rcb.js"; import"./chunk-ytpp1kam.js"; import"./chunk-na956zz3.js"; import"./chunk-f4bw8q7c.js"; import"./chunk-0v8vgrns.js"; import"./chunk-54qt5g7m.js"; import"./chunk-dhs2bg35.js"; // src/commands/adk-workflows.ts import { fstatSync } from "fs"; var DEFAULT_WAIT_TIMEOUT = "60s"; var DEFAULT_POLL_INTERVAL_MS = 1000; var PAYLOAD_NOT_OBJECT_ERROR = `Payload must be a JSON object (e.g. '{"key":"value"}').`; class WorkflowCommandError extends Error { exitCode; json; constructor(message, options = {}) { super(message); this.name = "WorkflowCommandError"; this.exitCode = options.exitCode ?? 1; this.json = options.json; } } function ensureWorkflowJsonOnlyFormat(format) { try { ensureJsonOnlyFormat(format); } catch { throw new WorkflowCommandError(JSON_ONLY_FORMAT_ERROR); } } function formatScalar(value) { if (value === null) { return "null"; } switch (typeof value) { case "string": return value; case "number": return String(value); case "boolean": return value ? "true" : "false"; default: return String(value); } } function appendYamlLikeValue(lines, key, value, indent) { const prefix = " ".repeat(indent); const keyLabel = `${key}:`; if (Array.isArray(value)) { if (value.length === 0) { lines.push(`${prefix}${keyLabel} []`); return; } lines.push(`${prefix}${keyLabel}`); for (const item of value) { appendYamlLikeListItem(lines, item, indent + 2); } return; } if (value && typeof value === "object") { const entries = Object.entries(value); if (entries.length === 0) { lines.push(`${prefix}${keyLabel} {}`); return; } lines.push(`${prefix}${keyLabel}`); for (const [childKey, childValue] of entries) { appendYamlLikeValue(lines, childKey, childValue, indent + 2); } return; } lines.push(`${prefix}${keyLabel} ${formatScalar(value)}`); } function appendYamlLikeListItem(lines, value, indent) { const prefix = " ".repeat(indent); if (Array.isArray(value)) { if (value.length === 0) { lines.push(`${prefix}- []`); return; } lines.push(`${prefix}-`); for (const item of value) { appendYamlLikeListItem(lines, item, indent + 2); } return; } if (value && typeof value === "object") { const entries = Object.entries(value); if (entries.length === 0) { lines.push(`${prefix}- {}`); return; } const [firstKey, firstValue] = entries[0]; if (Array.isArray(firstValue) || firstValue && typeof firstValue === "object") { lines.push(`${prefix}- ${firstKey}:`); if (Array.isArray(firstValue)) { for (const item of firstValue) { appendYamlLikeListItem(lines, item, indent + 4); } } else { for (const [childKey, childValue] of Object.entries(firstValue)) { appendYamlLikeValue(lines, childKey, childValue, indent + 4); } } } else { lines.push(`${prefix}- ${firstKey}: ${formatScalar(firstValue)}`); } for (const [childKey, childValue] of entries.slice(1)) { appendYamlLikeValue(lines, childKey, childValue, indent + 2); } return; } lines.push(`${prefix}- ${formatScalar(value)}`); } function formatWorkflowForText(workflow, options = {}) { const lines = []; appendYamlLikeValue(lines, "name", workflow.name, 0); appendYamlLikeValue(lines, "description", workflow.description ?? "\u2014", 0); appendYamlLikeValue(lines, "schedule", workflow.schedule ?? "manual", 0); appendYamlLikeValue(lines, "timeout", workflow.timeout ?? "\u2014", 0); if (options.includeInputSchema) { lines.push("inputSchema:"); for (const line of JSON.stringify(workflow.inputSchema, null, 2).split(` `)) { lines.push(` ${line}`); } } return lines.join(` `); } async function readPipedStdin(stdin) { if (!stdinHasPipedData(stdin)) { return; } let raw = ""; for await (const chunk of stdin) { raw += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"); } return raw.trim() === "" ? undefined : raw; } function stdinHasPipedData(stdin) { if (stdin.isTTY) { return false; } const fd = "fd" in stdin && typeof stdin.fd === "number" ? stdin.fd : undefined; if (fd !== undefined) { try { const stat = fstatSync(fd); if (stat.isFIFO() || stat.isFile() || stat.isSocket()) { return true; } if (stat.isCharacterDevice()) { return false; } } catch {} } return true; } function isPlainObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } async function resolvePayload(payloadArg, stdin) { const rawPayload = payloadArg ?? await readPipedStdin(stdin); if (rawPayload === undefined) { return; } let parsed; try { parsed = JSON.parse(rawPayload); } catch (error) { throw new WorkflowCommandError(`Invalid JSON payload: ${error instanceof Error ? error.message : String(error)}`); } if (!isPlainObject(parsed)) { throw new WorkflowCommandError(PAYLOAD_NOT_OBJECT_ERROR); } return parsed; } function getRequiredInputShape(inputSchema) { const required = Array.isArray(inputSchema.required) ? inputSchema.required.filter((field) => typeof field === "string") : []; return required.length > 0 ? { required } : null; } function parseDuration(duration) { const label = duration ?? DEFAULT_WAIT_TIMEOUT; const match = label.match(/^(\d+)(ms|s|m|h)$/); if (!match) { throw new WorkflowCommandError("Invalid timeout. Use a duration like 500ms, 30s, 1m, or 5m."); } const value = Number(match[1]); const unit = match[2]; const multiplier = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60000 : 3600000; return { ms: value * multiplier, label }; } function isTerminalStatus(status) { return status === "completed" || status === "failed" || status === "cancelled" || status === "timedout"; } function resolveDuration(workflow, startedAt, endedAt) { if (typeof workflow.duration === "number") { return workflow.duration; } const finishedAt = workflow.endedAt ?? workflow.updatedAt; if (workflow.createdAt && finishedAt) { const started = Date.parse(workflow.createdAt); const ended = Date.parse(finishedAt); if (Number.isFinite(started) && Number.isFinite(ended)) { return Math.max(0, ended - started); } } return Math.max(0, endedAt - startedAt); } function buildWorkflowWaitOutput(workflowId, name, workflow, startedAt, endedAt) { const output = { workflowId, workflow: name, status: workflow.status, duration: resolveDuration(workflow, startedAt, endedAt) }; if (workflow.output !== undefined) { output.output = workflow.output; } return output; } async function getClientFromDevServer() { const config = await getDevServerConfig(); const credentials = config.credentials ?? {}; const token = typeof credentials.token === "string" ? credentials.token : undefined; const botId = typeof credentials.devBotId === "string" ? credentials.devBotId : undefined; const apiUrl = typeof credentials.apiUrl === "string" ? credentials.apiUrl : undefined; const workspaceId = typeof credentials.workspaceId === "string" ? credentials.workspaceId : undefined; if (!token || !botId) { throw new WorkflowCommandError("Could not get credentials from dev server. Ensure the bot is properly linked."); } return getProjectClient({ credentials: { token, apiUrl: apiUrl || "https://api.botpress.cloud", ...workspaceId ? { workspaceId } : {} }, botId }); } async function resolveStatePayload(client, payload) { if (!payload || typeof payload !== "object") return payload; const p = payload; if (p.location?.type === "state" && "value" in p) return p.value; if (p.location?.type !== "file" || !p.location.key) return payload; try { const { file } = await client.getFile({ id: p.location.key }); const res = await fetch(file.url); if (!res.ok) { throw new Error(`HTTP ${res.status}${res.statusText ? ` ${res.statusText}` : ""}`); } return await res.json(); } catch (error) { throw new WorkflowCommandError(`Failed to resolve file-swapped state payload (key=${p.location.key}): ${error instanceof Error ? error.message : String(error)}`); } } async function fetchWorkflowStateDoc(client, workflowId, name) { const response = await client.getState({ type: "workflow", id: workflowId, name }); const resolved = await resolveStatePayload(client, response.state.payload); return { updatedAt: response.state.updatedAt, payload: resolved }; } function isRecord(value) { return typeof value === "object" && value !== null; } function isMissingStateDocError(error) { if (!isRecord(error)) return false; const response = isRecord(error.response) ? error.response : undefined; const code = error.code ?? error.status ?? error.statusCode ?? response?.status; const type = typeof error.type === "string" ? error.type : undefined; const name = typeof error.name === "string" ? error.name : undefined; return code === 404 || code === "404" || type === "ResourceNotFound" || name === "ResourceNotFoundError"; } function formatErrorMessage(error) { return error instanceof Error ? error.message : String(error); } async function fetchWorkflowStateDocForRuns(client, workflowId, name) { try { return { doc: await fetchWorkflowStateDoc(client, workflowId, name) }; } catch (error) { if (isMissingStateDocError(error)) { return { doc: null }; } return { doc: null, error: formatErrorMessage(error) }; } } async function adkWorkflowsList(options = {}) { ensureWorkflowJsonOnlyFormat(options.format); const logger = createCliLogger({ format: options.format }); const workflows = await listDevServerWorkflows(); if (workflows.length === 0) { logger.info("No workflows found.").result({ workflows: [], count: 0 }); return; } logger.info(workflows.map((workflow) => formatWorkflowForText(workflow)).join(` `)).result({ workflows: workflows.map((workflow) => ({ name: workflow.name, description: workflow.description, hasSchedule: workflow.hasSchedule, hasInput: workflow.hasInput, schedule: workflow.schedule, timeout: workflow.timeout })), count: workflows.length }); } async function adkWorkflowsInspect(name, options = {}) { ensureWorkflowJsonOnlyFormat(options.format); const logger = createCliLogger({ format: options.format }); const workflow = await getDevServerWorkflow(name); if (!workflow) { throw new WorkflowCommandError(`Workflow '${name}' not found.`); } logger.info(formatWorkflowForText(workflow, { includeInputSchema: true })).result({ name: workflow.name, description: workflow.description, hasSchedule: workflow.hasSchedule, hasInput: workflow.hasInput, schedule: workflow.schedule, timeout: workflow.timeout, inputSchema: workflow.inputSchema }); } async function adkWorkflowsRun(name, payloadArg, options = {}) { ensureWorkflowJsonOnlyFormat(options.format); const logger = createCliLogger({ format: "json" }); const timeout = options.timeout === undefined ? undefined : parseDuration(options.timeout); const workflow = await getDevServerWorkflow(name); if (!workflow) { throw new WorkflowCommandError(`Workflow '${name}' not found.`); } const requiredInputShape = getRequiredInputShape(workflow.inputSchema); const stdin = options.stdin ?? process.stdin; if (payloadArg === undefined && requiredInputShape && !stdinHasPipedData(stdin)) { throw new WorkflowCommandError(`Workflow '${name}' requires input. Run \`adk workflows inspect ${name}\` to see the schema.`, { json: { error: `Workflow '${name}' requires input. Run \`adk workflows inspect ${name}\` to see the schema.`, inputSchema: requiredInputShape } }); } const payload = await resolvePayload(payloadArg, stdin); if (payload === undefined && requiredInputShape) { throw new WorkflowCommandError(`Workflow '${name}' requires input. Run \`adk workflows inspect ${name}\` to see the schema.`, { json: { error: `Workflow '${name}' requires input. Run \`adk workflows inspect ${name}\` to see the schema.`, inputSchema: requiredInputShape } }); } const client = await getClientFromDevServer(); const now = options.now ?? (() => Date.now()); const startedAt = now(); const createWorkflowPayload = { name, status: "pending", input: payload ?? {} }; if (typeof workflow.timeout === "number") { createWorkflowPayload.timeoutAt = new Date(startedAt + workflow.timeout).toISOString(); } const shouldWait = options.wait || options.timeout !== undefined; if (!shouldWait) { const created2 = await client.createWorkflow(createWorkflowPayload); const workflowId2 = created2.workflow.id; logger.info(`Workflow '${name}' started (${workflowId2})`).result({ workflowId: workflowId2, workflow: name, status: "started" }); return; } const waitTimeout = timeout ?? parseDuration(undefined); const created = await client.createWorkflow(createWorkflowPayload); const workflowId = created.workflow.id; const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))); const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS; const deadline = startedAt + waitTimeout.ms; let consecutivePollErrors = 0; while (now() < deadline) { let current; try { const response = await client.getWorkflow({ id: workflowId }); current = response.workflow; consecutivePollErrors = 0; } catch (error) { consecutivePollErrors += 1; if (consecutivePollErrors >= 3) { throw new WorkflowCommandError(`Failed to poll workflow status: ${error instanceof Error ? error.message : String(error)}`); } current = undefined; } if (current && isTerminalStatus(current.status)) { const output = buildWorkflowWaitOutput(workflowId, name, current, startedAt, now()); if (current.status !== "completed") { throw new WorkflowCommandError(`Workflow ended with status '${current.status}'.`, { json: output }); } logger.info(`Workflow '${name}' ${current.status} (${workflowId})`).result(output); return; } await sleep(pollIntervalMs); } throw new WorkflowCommandError(`Workflow did not complete within ${waitTimeout.label}.`, { json: { workflowId, workflow: name, status: "timeout", message: `Workflow did not complete within ${waitTimeout.label}. Check adk traces workflow=${name} for details.` } }); } var VALID_STATUSES = [ "pending", "in_progress", "failed", "completed", "listening", "paused", "timedout", "cancelled" ]; function parseRunsTokens(tokens) { const filters = {}; for (const token of tokens) { const eq = token.indexOf("="); const key = eq === -1 ? token : token.slice(0, eq); const value = eq === -1 ? undefined : token.slice(eq + 1); if (!key || value === undefined) { throw new WorkflowCommandError(`Invalid filter '${token}'. Expected key=value (e.g. name=onboarding, status=failed, limit=20).`); } if (key === "name") { filters.name = value; } else if (key === "status") { const statuses = value.split(",").map((s) => s.trim()); for (const s of statuses) { if (!VALID_STATUSES.includes(s)) { throw new WorkflowCommandError(`Invalid status '${s}'. Valid: ${VALID_STATUSES.join(", ")}.`); } } filters.statuses = statuses; } else if (key === "limit") { const n = Number(value); if (!Number.isFinite(n) || n <= 0) { throw new WorkflowCommandError(`Invalid limit '${value}'. Must be a positive integer.`); } filters.limit = Math.floor(n); } else if (key === "nextToken") { filters.nextToken = value; } else { throw new WorkflowCommandError(`Unknown filter '${key}'. Supported: name, status, limit, nextToken.`); } } return filters; } function isWorkflowInstanceId(value) { return /^wrkflow_/i.test(value); } async function listWorkflowRuns(client, filters) { const workflows = []; let nextToken = filters.nextToken; let locallyTruncated = false; do { const remaining = filters.limit === undefined ? undefined : filters.limit - workflows.length; if (remaining !== undefined && remaining <= 0) break; const response = await client.listWorkflows({ ...filters.name ? { name: filters.name } : {}, ...filters.statuses ? { statuses: filters.statuses } : {}, ...nextToken ? { nextToken } : {} }); const pageItems = remaining === undefined ? response.workflows : response.workflows.slice(0, remaining); locallyTruncated = remaining !== undefined && response.workflows.length > remaining; workflows.push(...pageItems); nextToken = response.meta?.nextToken; if (filters.limit === undefined || locallyTruncated) { break; } } while (nextToken); return { workflows, ...nextToken && !locallyTruncated ? { nextToken } : {} }; } async function adkWorkflowsRuns(workflowIdOrTokens, options = {}) { ensureWorkflowJsonOnlyFormat(options.format); const logger = createCliLogger({ format: options.format }); const isShowMode = typeof workflowIdOrTokens === "string" && isWorkflowInstanceId(workflowIdOrTokens); const client = await getClientFromDevServer(); if (isShowMode) { const workflowId = workflowIdOrTokens; const [wfResponse, stateResult, stepsResult] = await Promise.all([ client.getWorkflow({ id: workflowId }), fetchWorkflowStateDocForRuns(client, workflowId, "workflowState"), fetchWorkflowStateDocForRuns(client, workflowId, "workflowSteps") ]); const wf = wfResponse.workflow; const stateDoc = stateResult.doc; const stepsDoc = stepsResult.doc; const lines = []; appendYamlLikeValue(lines, "workflowId", wf.id, 0); appendYamlLikeValue(lines, "name", wf.name, 0); appendYamlLikeValue(lines, "status", wf.status, 0); appendYamlLikeValue(lines, "createdAt", wf.createdAt, 0); appendYamlLikeValue(lines, "updatedAt", wf.updatedAt, 0); if (wf.input !== undefined) appendYamlLikeValue(lines, "input", wf.input, 0); if (wf.output !== undefined) appendYamlLikeValue(lines, "output", wf.output, 0); if (stateDoc) { appendYamlLikeValue(lines, "workflowState.updatedAt", stateDoc.updatedAt, 0); appendYamlLikeValue(lines, "workflowState.payload", stateDoc.payload, 0); } if (stepsDoc) { appendYamlLikeValue(lines, "workflowSteps.updatedAt", stepsDoc.updatedAt, 0); appendYamlLikeValue(lines, "workflowSteps.payload", stepsDoc.payload, 0); } if (stateResult.error) appendYamlLikeValue(lines, "workflowState.error", stateResult.error, 0); if (stepsResult.error) appendYamlLikeValue(lines, "workflowSteps.error", stepsResult.error, 0); logger.info(lines.join(` `)).result({ workflowId: wf.id, workflow: wf, workflowState: stateDoc, workflowSteps: stepsDoc, ...stateResult.error ? { workflowStateError: stateResult.error } : {}, ...stepsResult.error ? { workflowStepsError: stepsResult.error } : {} }); return; } const tokens = Array.isArray(workflowIdOrTokens) ? workflowIdOrTokens : workflowIdOrTokens ? [workflowIdOrTokens] : []; const filters = parseRunsTokens(tokens); const response = await listWorkflowRuns(client, filters); const items = response.workflows; if (items.length === 0) { logger.info("No workflow runs found.").result({ workflows: [], count: 0 }); return; } const textLines = items.map((wf) => `${wf.id} ${wf.name.padEnd(24)} ${wf.status.padEnd(12)} ${wf.createdAt}`); logger.info(textLines.join(` `)).result({ workflows: items.map((wf) => ({ id: wf.id, name: wf.name, status: wf.status, createdAt: wf.createdAt, updatedAt: wf.updatedAt })), count: items.length, ...response.nextToken ? { nextToken: response.nextToken } : {} }); } export { isWorkflowInstanceId, adkWorkflowsRuns, adkWorkflowsRun, adkWorkflowsList, adkWorkflowsInspect, WorkflowCommandError, PAYLOAD_NOT_OBJECT_ERROR, JSON_ONLY_FORMAT_ERROR, DEV_SERVER_DOWN_ERROR };