@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
935 lines (928 loc) • 32.1 kB
JavaScript
// @bun
import {
ensureChatIntegrationReady
} from "./chunk-rvevacmq.js";
import {
displayWorkspaceInfo
} from "./chunk-5fy9y5qt.js";
import {
JSON_ONLY_FORMAT_ERROR,
ensureJsonOnlyFormat
} from "./chunk-7sfagm12.js";
import {
DEV_SERVER_DOWN_ERROR,
buildDevServerHeaders,
getDevServerUrl,
resolveAgentRoot
} from "./chunk-ndxsgd72.js";
import {
resolveCommandContext
} from "./chunk-3ahwp6fe.js";
import {
findAgentRootOrFail
} from "./chunk-kk3h6qaj.js";
import {
createCliLogger
} from "./chunk-gzwt1qdr.js";
import"./chunk-nxy2ya5r.js";
import"./chunk-wzj4dc7n.js";
import {
AdkError,
BpChatCommand,
getChatClient
} 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 {
Uk
} from "./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 {
__require
} from "./chunk-dhs2bg35.js";
// src/commands/adk-chat.ts
import readline from "readline";
// ../evals/dist/client.js
var defaultLogger = console;
var AdkError2 = class extends Error {
static __IS_ADK_BASE_ERROR = true;
code;
expected;
details;
suggestion;
constructor(opts) {
super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
this.name = this.constructor.name;
this.code = opts.code;
this.expected = opts.expected ?? false;
if (opts.details !== undefined) {
this.details = opts.details;
}
if (opts.suggestion !== undefined) {
this.suggestion = opts.suggestion;
}
}
};
var EvalRunnerError = class extends AdkError2 {
};
async function assertChatChannelBound(devServerUrl, headers = {}, logger = defaultLogger) {
let conversations = [];
try {
const res = await fetch(`${devServerUrl}/api/agent`, { headers });
if (!res.ok)
return;
const agent = await res.json();
conversations = agent.conversations ?? [];
} catch (err) {
logger?.warn(`Skipping chat.channel binding pre-flight \u2014 could not query ${devServerUrl}/api/agent: ${err instanceof Error ? err.message : String(err)}`);
return;
}
if (conversations.length === 0)
return;
const matchesChatChannel = (ch) => {
if (!ch)
return false;
if (ch === "*" || ch === "chat.channel")
return true;
return Array.isArray(ch) && (ch.includes("chat.channel") || ch.includes("*"));
};
const bound = conversations.some((c) => matchesChatChannel(c.channel));
if (bound)
return;
throw new EvalRunnerError({
code: "CHAT_CHANNEL_UNBOUND",
message: [
"No Conversation is bound to `chat.channel` \u2014 messages via `@botpress/chat` will time out silently.",
"",
"To fix, bind a Conversation in `src/conversations/` to `chat.channel` (or `*`):",
" channel: 'chat.channel', // explicit",
" channel: '*', // wildcard \u2014 matches every channel",
" channel: ['chat.channel', ...], // array form",
"",
"The `chat` integration is how evals and `adk chat --single` send messages programmatically."
].join(`
`),
expected: true
});
}
// src/utils/chat-single.ts
var DEV_SERVER_HEALTH_TIMEOUT_MS = 2000;
var DEV_SERVER_REQUEST_TIMEOUT_MS = 5000;
var RESPONSE_POLL_INTERVAL_MS = 100;
var CHAT_IDLE_TIMEOUT_MS = 2000;
var RELOAD_POLL_INTERVAL_MS = 400;
var MAX_RELOAD_RESENDS = 20;
var DEFAULT_CHAT_SINGLE_TIMEOUT = "60s";
var INVALID_CHAT_TIMEOUT_ERROR = "Invalid timeout. Use a duration like 500ms, 30s, 1m, or 5m.";
function isForbiddenError(error) {
if (error instanceof Error && /\bforbidden\b/i.test(error.message)) {
return true;
}
if (error != null && typeof error === "object" && "status" in error && error.status === 403) {
return true;
}
return false;
}
class SingleMessageError extends Error {
code;
conversationId;
botId;
cause;
constructor(code, message, options = {}) {
super(message);
this.name = "SingleMessageError";
this.code = code;
this.conversationId = options.conversationId;
this.botId = options.botId;
this.cause = options.cause;
}
}
function parseChatSingleTimeout(duration) {
const label = duration ?? DEFAULT_CHAT_SINGLE_TIMEOUT;
const match = label.match(/^(\d+)(ms|s|m|h)$/);
if (!match) {
throw new SingleMessageError("invalid_timeout", INVALID_CHAT_TIMEOUT_ERROR);
}
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 createSingleMessageTimeoutError(label, conversationId) {
return new SingleMessageError("timeout", `Bot did not respond within ${label}. Check adk logs error for details.`, {
conversationId
});
}
function extractBotResponse(payload) {
if (!payload || typeof payload !== "object") {
return { type: "unknown" };
}
const data = payload;
return {
type: typeof data.type === "string" ? data.type : "unknown",
text: typeof data.text === "string" ? data.text : undefined,
payload: data
};
}
async function assertDevServerRunning(baseUrl, headers) {
let response;
try {
response = await fetch(`${baseUrl}/api/health`, {
headers,
signal: AbortSignal.timeout(DEV_SERVER_HEALTH_TIMEOUT_MS)
});
} catch (error) {
throw new SingleMessageError("dev_server_down", DEV_SERVER_DOWN_ERROR, { cause: error });
}
if (!response.ok) {
throw new SingleMessageError("dev_server_down", DEV_SERVER_DOWN_ERROR);
}
}
async function fetchDevServerHealth(baseUrl, headers) {
try {
const response = await fetch(`${baseUrl}/api/health`, {
headers,
signal: AbortSignal.timeout(DEV_SERVER_HEALTH_TIMEOUT_MS)
});
if (!response.ok) {
return null;
}
const data = await response.json();
return data.status === "building" || data.status === "error" ? data.status : "ready";
} catch {
return null;
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function createReloadingTimeoutError(label, conversationId) {
return new SingleMessageError("reloading", `Bot did not respond within ${label}: the dev server was still reloading. Wait for the rebuild to settle and retry.`, { conversationId });
}
async function waitForDevServerReady(gate) {
const interval = gate.pollIntervalMs ?? RELOAD_POLL_INTERVAL_MS;
for (;; ) {
const status = await gate.fetchHealth();
if (status === null || status === "ready") {
return;
}
if (status === "error") {
throw new SingleMessageError("request_failed", "Dev server reported a build error. Check `adk dev` logs and fix the build, then retry.", { conversationId: gate.conversationId });
}
if (Date.now() >= gate.deadlineAt) {
throw createReloadingTimeoutError(gate.timeoutLabel, gate.conversationId);
}
await delay(interval);
}
}
function startReloadWatcher(gate) {
const controller = new AbortController;
const interval = gate.pollIntervalMs ?? RELOAD_POLL_INTERVAL_MS;
let stopped = false;
let timer;
const tick = async () => {
if (stopped) {
return;
}
const status = await gate.fetchHealth().catch(() => null);
if (stopped) {
return;
}
if (status === "building" || status === "error") {
stopped = true;
controller.abort();
return;
}
timer = setTimeout(tick, interval);
};
timer = setTimeout(tick, interval);
return {
signal: controller.signal,
stop: () => {
stopped = true;
if (timer) {
clearTimeout(timer);
}
}
};
}
async function sendWithReadinessGate(send, gate) {
const maxResends = gate.maxResends ?? MAX_RELOAD_RESENDS;
let resends = 0;
for (;; ) {
await waitForDevServerReady(gate);
const remainingMs = gate.deadlineAt - Date.now();
if (remainingMs <= 0) {
throw createReloadingTimeoutError(gate.timeoutLabel, gate.conversationId);
}
const watcher = startReloadWatcher(gate);
try {
return await send({ ms: remainingMs, label: gate.timeoutLabel }, watcher.signal);
} catch (error) {
if (error instanceof SingleMessageError && error.code === "reloading") {
if (resends >= maxResends) {
throw createReloadingTimeoutError(gate.timeoutLabel, gate.conversationId);
}
resends += 1;
continue;
}
throw error;
} finally {
watcher.stop();
}
}
}
var DEV_SERVER_RECOVERY_HINT = "Verify `adk dev` is running on this port, restart it, or check for another local app/proxy on the same port.";
function sanitizeBodyPreview(raw) {
const cleaned = raw.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").trim();
return cleaned.length > 120 ? `${cleaned.slice(0, 120)}\u2026` : cleaned;
}
async function parseJsonOrThrow(response, pathname) {
const contentType = response.headers?.get?.("content-type") ?? "unknown";
let bodyText;
if (typeof response.text === "function") {
try {
bodyText = await response.text();
} catch {}
}
if (bodyText !== undefined) {
try {
return JSON.parse(bodyText);
} catch {
const preview = sanitizeBodyPreview(bodyText);
const previewSuffix = preview ? ` Body preview: "${preview}".` : "";
throw new SingleMessageError("request_failed", `Dev server returned non-JSON from ${pathname} (status ${response.status}, content-type: ${contentType}).${previewSuffix} ${DEV_SERVER_RECOVERY_HINT}`);
}
}
try {
return await response.json();
} catch {
throw new SingleMessageError("request_failed", `Dev server returned non-JSON from ${pathname} (status ${response.status}, content-type: ${contentType}). ${DEV_SERVER_RECOVERY_HINT}`);
}
}
async function fetchJson(baseUrl, pathname, headers, timeoutMs = DEV_SERVER_REQUEST_TIMEOUT_MS) {
const response = await fetch(`${baseUrl}${pathname}`, {
headers,
signal: AbortSignal.timeout(timeoutMs)
});
if (!response.ok) {
throw new SingleMessageError("request_failed", `Failed to fetch ${pathname} from dev server (${response.status} ${response.statusText}). ${DEV_SERVER_RECOVERY_HINT}`);
}
return parseJsonOrThrow(response, pathname);
}
async function lookupUserToken(baseUrl, botId, conversationId, headers) {
try {
const params = new URLSearchParams({ botId, conversationId });
const data = await fetchJson(baseUrl, `/api/conversations/user-token?${params.toString()}`, headers, DEV_SERVER_HEALTH_TIMEOUT_MS);
return data.userToken ?? null;
} catch {
return null;
}
}
async function persistUserToken(baseUrl, botId, conversationId, userToken, headers) {
try {
await fetch(`${baseUrl}/api/conversations/user-token`, {
method: "POST",
headers: { "Content-Type": "application/json", ...headers },
body: JSON.stringify({ botId, conversationId, userToken }),
signal: AbortSignal.timeout(DEV_SERVER_HEALTH_TIMEOUT_MS)
});
} catch {}
}
async function fetchConversationHistory(conversationId, devServerUrl, cwd = process.cwd()) {
const baseUrl = devServerUrl ?? getDevServerUrl();
const headers = buildDevServerHeaders(resolveAgentRoot(cwd));
const config = await getDevServerConfig(baseUrl, headers);
const credentials = config.credentials ?? {};
const token = typeof credentials.token === "string" ? credentials.token : undefined;
const botId = typeof credentials.devBotId === "string" ? credentials.devBotId : undefined;
if (!token || !botId) {
return [];
}
const apiUrl = typeof credentials.apiUrl === "string" ? credentials.apiUrl : undefined;
const client = new Uk({ token, botId, apiUrl });
try {
const { messages } = await client.listMessages({ conversationId });
const sorted = [...messages].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
return sorted.map((msg) => ({
direction: msg.direction,
text: typeof msg.payload?.text === "string" ? msg.payload.text : ""
})).filter((msg) => msg.text.length > 0);
} catch {
return [];
}
}
async function getDevServerConfig(baseUrl, headers) {
await assertDevServerRunning(baseUrl, headers);
return fetchJson(baseUrl, "/api/config", headers);
}
async function discoverAvailableIntegrations(baseUrl, headers) {
const config = await getDevServerConfig(baseUrl, headers);
const credentials = config.credentials ?? {};
const token = typeof credentials.token === "string" ? credentials.token : undefined;
const botId = typeof credentials.devBotId === "string" ? credentials.devBotId : undefined;
if (!token || !botId) {
throw new SingleMessageError("missing_credentials", "Could not get credentials from dev server. Ensure the bot is properly linked.");
}
const apiUrl = typeof credentials.apiUrl === "string" ? credentials.apiUrl : undefined;
const client = new Uk({
token,
botId,
apiUrl
});
const { bot } = await client.getBot({ id: botId });
const integrations = Object.values(bot.integrations ?? {});
const webchat = integrations.find((integration) => integration.name === "webchat");
const chat = integrations.find((integration) => integration.name === "chat");
return {
available: {
webchat: webchat?.webhookId ? { webhookId: webchat.webhookId } : null,
chat: chat?.webhookId ? { webhookId: chat.webhookId } : null
},
botId
};
}
function resolveChannelAttempts(available, requestedChannel) {
if (requestedChannel === "auto") {
const attempts = [];
if (available.chat) {
attempts.push({ channel: "chat", webhookId: available.chat.webhookId });
}
if (available.webchat) {
attempts.push({ channel: "webchat", webhookId: available.webchat.webhookId });
}
if (attempts.length > 0) {
return attempts;
}
throw new SingleMessageError("no_integration", 'No chat or webchat integration found. Install one via "adk integrations add chat" or "adk integrations add webchat".');
}
if (requestedChannel === "chat") {
if (!available.chat) {
throw new SingleMessageError("no_integration", 'Chat integration not found. Install via "adk integrations add chat" or use channel: "webchat".');
}
return [{ channel: "chat", webhookId: available.chat.webhookId }];
}
if (!available.webchat) {
throw new SingleMessageError("no_integration", 'Webchat integration not found. Install via "adk integrations add webchat" or use channel: "chat".');
}
return [{ channel: "webchat", webhookId: available.webchat.webhookId }];
}
function normalizeSingleMessageError(error, conversationId, botId) {
if (error instanceof SingleMessageError) {
if (botId && !error.botId) {
return new SingleMessageError(error.code, error.message, {
conversationId: error.conversationId,
botId,
cause: error
});
}
return error;
}
if (isForbiddenError(error) && conversationId) {
return new SingleMessageError("request_failed", "Forbidden: the conversation ID and user token do not match. The dev server may have been restarted since this conversation was started.", { cause: error, conversationId, botId });
}
return new SingleMessageError("request_failed", `Failed to send message: ${error instanceof Error ? error.message : String(error)}`, {
cause: error,
conversationId,
botId
});
}
function shouldRetryWithWebchat(requestedChannel, attempts, attempt, error) {
return requestedChannel === "auto" && attempt.channel === "chat" && error.code === "missing_dependency" && attempts.some((candidate) => candidate.channel === "webchat");
}
function createFallbackContinuationError(conversationId, cause) {
return new SingleMessageError("request_failed", `Could not continue conversation ${conversationId} after falling back to webchat. This conversation likely started on the chat channel and requires a working @botpress/chat runtime. Retry where @botpress/chat is available, or start a new conversation without conversationId.`, { cause, conversationId });
}
async function sendViaWebchat(message, webhookId, options) {
let createClient;
let createUser;
try {
const webchatModule = await import("./chunk-spek9c3w.js");
createClient = webchatModule.createClient;
createUser = webchatModule.createUser;
} catch (error) {
throw new SingleMessageError("missing_dependency", "Webchat client not available. Install @botpress/webchat-client to use this feature.", { cause: error });
}
const userKey = options.userKey ?? (await createUser({ clientId: webhookId })).key;
const client = createClient({ userKey, clientId: webhookId });
let conversationId = options.conversationId;
const responses = [];
let resolved = false;
let sentMessageId = null;
if (!conversationId) {
const createdConversation = await client.createConversation();
conversationId = createdConversation.conversation.id;
}
const on = client.listenConversation({ conversationId });
const unsubscribers = [];
try {
unsubscribers.push(on("message_created", (event) => {
if (resolved) {
return;
}
if (sentMessageId && event.id === sentMessageId) {
return;
}
responses.push(extractBotResponse(event.payload));
}));
unsubscribers.push(on("message_status_changed", ({ newStatus, message: event }) => {
if (resolved) {
return;
}
if (sentMessageId && event.id === sentMessageId && newStatus === "processed") {
resolved = true;
}
}));
const createdMessage = await client.createMessage({
conversationId,
payload: { type: "text", text: message }
});
sentMessageId = createdMessage.message.id;
const startedAt = Date.now();
await new Promise((resolve, reject) => {
const checkInterval = setInterval(() => {
if (resolved) {
clearInterval(checkInterval);
resolve();
return;
}
if (options.reloadSignal?.aborted && responses.length === 0) {
clearInterval(checkInterval);
reject(new SingleMessageError("reloading", "Dev server reload detected while awaiting a reply.", {
conversationId
}));
return;
}
if (Date.now() - startedAt > options.timeout.ms) {
clearInterval(checkInterval);
if (responses.length > 0) {
resolve();
return;
}
reject(createSingleMessageTimeoutError(options.timeout.label, conversationId));
}
}, RESPONSE_POLL_INTERVAL_MS);
});
return {
conversationId,
userKey,
channel: "webchat",
responses
};
} finally {
for (const unsubscribe of unsubscribers) {
try {
unsubscribe();
} catch {}
}
if (typeof client.disconnect === "function") {
try {
client.disconnect();
} catch {}
}
}
}
async function sendViaChat(message, webhookId, options) {
let ChatClient;
try {
ChatClient = getChatClient();
} catch (error) {
throw new SingleMessageError("missing_dependency", "Chat client not available. Install @botpress/chat to use this feature.", {
cause: error
});
}
const client = await ChatClient.connect({ webhookId, userKey: options.userKey });
let conversationId = options.conversationId;
const responses = [];
let resolved = false;
let lastBotMessageAt = 0;
if (!conversationId) {
const createdConversation = await client.createConversation({});
conversationId = createdConversation.conversation.id;
}
const listener = await client.listenConversation({ id: conversationId });
try {
listener.on("message_created", (event) => {
if (resolved || !event.isBot) {
return;
}
responses.push(extractBotResponse(event.payload));
lastBotMessageAt = Date.now();
});
listener.on("event_created", (event) => {
if (event.payload?.done) {
resolved = true;
}
});
await client.createMessage({
conversationId,
payload: { type: "text", text: message }
});
const startedAt = Date.now();
await new Promise((resolve, reject) => {
const checkInterval = setInterval(() => {
const now = Date.now();
if (resolved) {
clearInterval(checkInterval);
resolve();
return;
}
if (responses.length > 0 && lastBotMessageAt > 0 && now - lastBotMessageAt > CHAT_IDLE_TIMEOUT_MS) {
clearInterval(checkInterval);
resolve();
return;
}
if (options.reloadSignal?.aborted && responses.length === 0) {
clearInterval(checkInterval);
reject(new SingleMessageError("reloading", "Dev server reload detected while awaiting a reply.", {
conversationId
}));
return;
}
if (now - startedAt > options.timeout.ms) {
clearInterval(checkInterval);
if (responses.length > 0) {
resolve();
return;
}
reject(createSingleMessageTimeoutError(options.timeout.label, conversationId));
}
}, RESPONSE_POLL_INTERVAL_MS);
});
return {
conversationId,
userKey: client.user.key,
channel: "chat",
responses
};
} finally {
if (typeof listener.disconnect === "function") {
try {
listener.disconnect();
} catch {}
}
if (typeof client.disconnect === "function") {
try {
client.disconnect();
} catch {}
}
}
}
async function sendSingleMessage(options) {
const timeout = options.timeout ?? parseChatSingleTimeout(undefined);
const baseUrl = options.devServerUrl ?? getDevServerUrl();
const headers = buildDevServerHeaders(resolveAgentRoot(options.cwd));
let available;
let botId;
try {
const discovery = await discoverAvailableIntegrations(baseUrl, headers);
available = discovery.available;
botId = discovery.botId;
} catch (error) {
throw normalizeSingleMessageError(error, options.conversationId);
}
const requestedChannel = options.channel ?? "auto";
const attempts = resolveChannelAttempts(available, requestedChannel);
let userKey;
if (options.conversationId) {
const token = await lookupUserToken(baseUrl, botId, options.conversationId, headers);
if (!token) {
throw new SingleMessageError("missing_user_key", "Could not look up user token for this conversation. Ensure the dev server (adk dev) is running and that the conversation was started in this session.");
}
userKey = token;
}
let retriedFromMissingChatDependency = false;
const deadlineAt = Date.now() + timeout.ms;
const fetchHealth = () => fetchDevServerHealth(baseUrl, headers);
for (const attempt of attempts) {
try {
const result = await sendWithReadinessGate((attemptTimeout, reloadSignal) => attempt.channel === "webchat" ? sendViaWebchat(options.message, attempt.webhookId, {
conversationId: options.conversationId,
userKey,
timeout: attemptTimeout,
reloadSignal
}) : sendViaChat(options.message, attempt.webhookId, {
conversationId: options.conversationId,
userKey,
timeout: attemptTimeout,
cwd: options.cwd,
reloadSignal
}), { fetchHealth, deadlineAt, timeoutLabel: timeout.label, conversationId: options.conversationId });
if (result.userKey) {
persistUserToken(baseUrl, botId, result.conversationId, result.userKey, headers).catch(() => {});
}
const { userKey: _stripped, ...publicResult } = result;
return publicResult;
} catch (error) {
const normalizedError = normalizeSingleMessageError(error, options.conversationId, botId);
if (shouldRetryWithWebchat(requestedChannel, attempts, attempt, normalizedError)) {
retriedFromMissingChatDependency = true;
continue;
}
if (retriedFromMissingChatDependency && attempt.channel === "webchat" && options.conversationId && isForbiddenError(error)) {
throw createFallbackContinuationError(options.conversationId, error);
}
throw normalizedError;
}
}
throw new SingleMessageError("request_failed", "Failed to send message: no available channel succeeded.");
}
// src/commands/adk-chat.ts
class AdkChatCommandError extends Error {
exitCode;
json;
constructor(message, options = {}) {
super(message);
this.name = "AdkChatCommandError";
this.exitCode = options.exitCode ?? 1;
this.json = options.json;
}
}
function createBpChatCommand(options) {
return new BpChatCommand(options);
}
var defaultDeps = {
displayWorkspaceInfo,
findAgentRoot: findAgentRootOrFail,
ensureChatIntegrationReady,
assertChatChannelBound,
resolveCommandContext,
createBpChatCommand,
sendSingleMessage,
fetchConversationHistory
};
var logger = createCliLogger();
async function resolveChatContext(startPath, deps) {
return deps.resolveCommandContext({
cwd: startPath,
target: "dev",
require: ["project", "credentials", "workspace", "bot"]
});
}
function ensureChatJsonOnlyFormat(format) {
try {
ensureJsonOnlyFormat(format);
} catch {
throw new AdkChatCommandError(JSON_ONLY_FORMAT_ERROR);
}
}
function formatResponse(response) {
if (response.type === "text" && response.text) {
return response.text;
}
if (response.payload) {
return JSON.stringify(response.payload, null, 2);
}
return `[${response.type}]`;
}
function toJsonErrorPayload(error) {
if (error instanceof SingleMessageError) {
return {
error: error.message,
...error.conversationId ? { conversationId: error.conversationId } : {}
};
}
if (error instanceof Error) {
return { error: error.message };
}
return { error: String(error) };
}
async function runSingleChat(options, deps) {
ensureChatJsonOnlyFormat(options.format);
const singleLogger = createCliLogger({ format: options.format });
const isJson = options.format === "json";
try {
const startPath = options.startPath ?? process.cwd();
const context = await resolveChatContext(startPath, deps);
const project = context.project;
const devId = context.botId;
const promptForInstall = isJson ? async () => {
throw new AdkError({
code: "CHAT_START_FAILED",
message: [
"The `chat` integration is not installed on this bot \u2014 `adk chat --single` requires it.",
"",
"To fix:",
" 1. Install it: adk integrations add chat",
" 2. Redeploy: adk dev (or: adk deploy)"
].join(`
`),
expected: true
});
} : options.promptForInstall;
await deps.ensureChatIntegrationReady(project.path, devId, {
promptForInstall,
project,
client: context.client
});
await deps.assertChatChannelBound(getDevServerUrl(), buildDevServerHeaders(project.path));
const timeout = parseChatSingleTimeout(options.timeout);
const result = await deps.sendSingleMessage({
message: options.single,
conversationId: options.conversationId,
timeout,
cwd: context.agentRoot
});
const rendered = result.responses.map(formatResponse).join(`
`).trim();
singleLogger.info(rendered.length > 0 ? rendered : "No response").result(result);
return { shouldExitAfterSuccess: true };
} catch (error) {
const json = options.format === "json" ? toJsonErrorPayload(error) : undefined;
const message = error instanceof Error ? error.message : String(error);
throw new AdkChatCommandError(message, { json });
}
}
function printBotResponses(result) {
for (const response of result.responses) {
const text = formatResponse(response);
if (text.length > 0) {
logger.info(`\uD83E\uDD16 ${text}`);
}
}
}
async function runInteractiveChatRepl(options, deps) {
const timeout = parseChatSingleTimeout(options.timeout);
const startPath = options.startPath ?? process.cwd();
const agentRoot = await deps.findAgentRoot(startPath);
let conversationId = options.conversationId;
logger.stdout("\x1B[2J\x1B[3J\x1B[H");
logger.info("Botpress Chat");
if (conversationId) {
logger.info(`Continuing conversation ${conversationId}`, "gray");
}
logger.info('Type "exit" or press ESC key to quit', "gray");
if (conversationId) {
try {
const history = await deps.fetchConversationHistory(conversationId, undefined, agentRoot);
if (history.length > 0) {
logger.newline();
for (const msg of history) {
if (msg.direction === "incoming") {
logger.info(`\uD83D\uDC64 ${msg.text}`, "gray");
} else {
logger.info(`\uD83E\uDD16 ${msg.text}`);
}
}
}
} catch (err) {
logger.debug(`Conversation history fetch failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
let closed = false;
rl.once("close", () => {
closed = true;
});
if (process.stdin.isTTY) {
process.stdin.on("keypress", (_ch, key) => {
if (key?.name === "escape") {
closed = true;
rl.close();
}
});
}
const prompt = () => new Promise((resolve) => {
if (closed) {
resolve(null);
return;
}
rl.question(">> ", (answer) => resolve(answer));
rl.once("close", () => resolve(null));
});
try {
while (true) {
const input = await prompt();
if (input === null || input.trim() === "exit") {
break;
}
const trimmed = input.trim();
if (trimmed.length === 0) {
continue;
}
logger.stdout("\x1B[A\x1B[2K");
logger.info(`\uD83D\uDC64 ${trimmed}`, "gray");
try {
const result = await deps.sendSingleMessage({
message: trimmed,
conversationId,
timeout,
cwd: agentRoot
});
conversationId = result.conversationId;
printBotResponses(result);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
logger.error(`Error: ${msg}`);
}
}
} finally {
rl.close();
}
}
async function runBpChat(options, deps) {
const startPath = options.startPath ?? process.cwd();
logger.info("\uD83E\uDD16 Starting chat with your agent...", "blue");
const context = await resolveChatContext(startPath, deps);
await deps.displayWorkspaceInfo({ context });
const project = context.project;
const devId = context.botId;
logger.debug(`Using bot ID: ${devId}`);
await deps.ensureChatIntegrationReady(project.path, devId, {
promptForInstall: options.promptForInstall,
project,
client: context.client
});
await deps.assertChatChannelBound(getDevServerUrl(), buildDevServerHeaders(project.path));
const credentials = context.credentials;
const chatCommand = deps.createBpChatCommand({
botId: devId,
workspaceId: context.workspaceId,
credentials: {
token: credentials.token,
apiUrl: credentials.apiUrl
},
projectPath: context.agentRoot
});
await chatCommand.run();
}
async function adkChat(options = {}, deps = defaultDeps) {
if (options.single !== undefined) {
return runSingleChat(options, deps);
}
if (options.conversationId) {
await runInteractiveChatRepl(options, deps);
return;
}
await runBpChat(options, deps);
}
export {
adkChat,
JSON_ONLY_FORMAT_ERROR,
AdkChatCommandError
};