UNPKG

@botpress/adk-cli

Version:

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

935 lines (928 loc) 32.1 kB
// @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 };