@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
612 lines (609 loc) • 21.5 kB
JavaScript
// @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
};