UNPKG

@botpress/adk-cli

Version:

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

425 lines (422 loc) 12.2 kB
// @bun import { parseFilterTokens } from "./chunk-wk6ynbab.js"; import { SqliteLogStore, filterLogEntries, findLatestLogFile } from "./chunk-ssjw4jwy.js"; import { findMatchingAdkDevConsole } from "./chunk-8ck49k1q.js"; import { ensureJsonOnlyFormat } from "./chunk-7sfagm12.js"; import"./chunk-ndxsgd72.js"; import"./chunk-3ahwp6fe.js"; import"./chunk-sgj6770p.js"; import { findAgentRootOrFail } from "./chunk-kk3h6qaj.js"; import { createCliLogger, source_default } from "./chunk-gzwt1qdr.js"; import"./chunk-nxy2ya5r.js"; import"./chunk-wzj4dc7n.js"; import { AdkError } 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-logs.ts import { existsSync as existsSync2, statSync } from "fs"; import { join as join2 } from "path"; // src/logs/log-follower.ts var POLL_INTERVAL_MS = 1000; function createLogFollower(store, query = {}) { let lastId = store.getMaxId(); let stopped = false; let resolveNext = null; const buffer = []; let intervalId = null; function poll() { if (stopped) return; const rows = store.getLogsAfter(lastId, query); for (const row of rows) { if (row.id > lastId) lastId = row.id; if (resolveNext) { const resolve = resolveNext; resolveNext = null; resolve({ value: row, done: false }); } else { buffer.push(row); } } } function stop() { stopped = true; if (intervalId) { clearInterval(intervalId); intervalId = null; } if (resolveNext) { const resolve = resolveNext; resolveNext = null; resolve({ value: undefined, done: true }); } } const entries = { [Symbol.asyncIterator]() { if (!intervalId && !stopped) { intervalId = setInterval(poll, POLL_INTERVAL_MS); intervalId.unref?.(); } return { next() { if (buffer.length > 0) { return Promise.resolve({ value: buffer.shift(), done: false }); } if (stopped) { return Promise.resolve({ value: undefined, done: true }); } return new Promise((resolve) => { resolveNext = resolve; }); } }; } }; return { entries, stop }; } // src/logs/log-formatter.ts var LEVEL_LABELS = { error: "ERROR", warning: "WARN ", info: "INFO ", raw: "RAW " }; function levelColor(level) { switch (level) { case "error": return source_default.red; case "warning": return source_default.yellow; case "info": return source_default.cyan; default: return source_default.gray; } } function formatLogEntry(entry, _opts) { const ts = new Date(entry.timestamp); const timeStr = ts.toLocaleTimeString("en-US", { hour12: false }); const label = LEVEL_LABELS[entry.level] || entry.level.toUpperCase().padEnd(5); const colorize = levelColor(entry.level); return `${source_default.dim(`[${timeStr}]`)} ${colorize(label)} ${entry.message}`; } // src/logs/log-summary.ts import { existsSync } from "fs"; import { join } from "path"; var ALL_LOG_LIMIT = 5000; function normalizeHealthStatus(status) { if (status === "running") return "ready"; if (status === "ready" || status === "building" || status === "error") return status; return "unknown"; } function isError(entry) { return entry.level === "error"; } function isWarning(entry) { return entry.level === "warning"; } function getEntryFindingLevel(entry) { if (isError(entry)) return "error"; if (isWarning(entry)) return "warning"; return null; } function parseBuildDuration(message) { const match = message.match(/(\d+(?:\.\d+)?)ms/); if (!match) return; const duration = Number.parseFloat(match[1]); return Number.isFinite(duration) ? duration : undefined; } function detectBuildStatus(entries) { for (let index = entries.length - 1;index >= 0; index--) { const entry = entries[index]; const message = entry.message.toLowerCase(); if (message.includes("bot bundled") || message.includes("typings generated") || message.includes("dev bot ready") || message.includes("listening on")) { return { status: "ready", lastBuild: { timestamp: entry.timestamp, duration: parseBuildDuration(entry.message), message: entry.message } }; } if (message.includes("bundling") || message.includes("generating") || message.includes("compiling")) { return { status: "building", lastBuild: { timestamp: entry.timestamp, message: entry.message } }; } if (isError(entry)) { return { status: "error", lastBuild: { timestamp: entry.timestamp, message: entry.message } }; } } return { status: "unknown" }; } function mergeBuildStatus(healthStatus, logStatus) { if (healthStatus === "error") return "error"; if (healthStatus === "building") return logStatus === "error" ? "error" : "building"; if (healthStatus === "ready") return logStatus === "error" ? "error" : "ready"; return logStatus; } function serializeEntry(entry) { return { timestamp: entry.timestamp, type: entry.type, level: entry.level, message: entry.message, ...entry.spanId ? { spanId: entry.spanId } : {}, ...entry.traceId ? { traceId: entry.traceId } : {} }; } function dedupeFindings(entries) { const findings = new Map; for (const entry of entries) { const level = getEntryFindingLevel(entry); if (!level) { continue; } const message = entry.message.trim(); const key = `${level}:${message}`; const existing = findings.get(key); if (existing) { existing.count += 1; existing.lastSeenAt = entry.timestamp; continue; } findings.set(key, { level, message, count: 1, firstSeenAt: entry.timestamp, lastSeenAt: entry.timestamp }); } return [...findings.values()]; } async function buildLogSummary(options, dependencies = {}) { const findAdkDevConsole = dependencies.findAdkDevConsole ?? findMatchingAdkDevConsole; const logFile = options.logFile ?? findLatestLogFile(options.agentRoot); const storeDbPath = join(options.agentRoot, ".adk", "bot", "logs", "logs.db"); let allEntries = []; if (existsSync(storeDbPath)) { const store = new SqliteLogStore(options.agentRoot); allEntries = store.getRecentLogs(ALL_LOG_LIMIT); store.close(); } const logs = filterLogEntries(allEntries, { ...options.level ? { level: options.level } : {}, ...options.since ? { since: options.since } : {}, limit: options.limit ?? 50 }).map(serializeEntry); const findings = dedupeFindings(allEntries); const errors = findings.filter((finding) => finding.level === "error"); const warnings = findings.filter((finding) => finding.level === "warning"); const logBuild = detectBuildStatus(allEntries); const adkDevConsole = await findAdkDevConsole(options.agentRoot); const healthStatus = normalizeHealthStatus(adkDevConsole?.health.status); return { logFile, health: { running: adkDevConsole !== null, status: healthStatus, port: adkDevConsole?.port ?? null, url: adkDevConsole?.url ?? null, agentPath: adkDevConsole?.health.agentPath ?? null, adkVersion: adkDevConsole?.health.adkVersion ?? null, startTime: adkDevConsole?.health.startTime ?? null, uptime: adkDevConsole?.health.uptime ?? null }, buildStatus: mergeBuildStatus(healthStatus, logBuild.status), ...logBuild.lastBuild ? { lastBuild: logBuild.lastBuild } : {}, counts: { totalEntries: allEntries.length, errorCount: errors.length, warningCount: warnings.length, findingCount: findings.length }, findings, errors, warnings, logs }; } // src/commands/adk-logs.ts function logStoreDbPath(agentRoot) { return join2(agentRoot, ".adk", "bot", "logs", "logs.db"); } function isDevServerLikelyRunning(agentRoot) { const dbPath = logStoreDbPath(agentRoot); let newest = 0; for (const candidate of [dbPath, `${dbPath}-wal`]) { try { newest = Math.max(newest, statSync(candidate).mtime.getTime()); } catch {} } return newest > 0 && Date.now() - newest < 30000; } function toResultJson(entry) { const { raw: _raw, id: _id, ...rest } = entry; return rest; } async function adkLogs(tokens, options) { const logger = createCliLogger({ format: options?.format }); if (options.summary) { if (options.follow) { throw new AdkError({ code: "INVALID_FLAGS", message: "The --summary flag cannot be used with --follow.", expected: true }); } if (options.format !== "json") { throw new AdkError({ code: "INVALID_FLAGS", message: "The --summary flag requires --format json.", expected: true }); } } else { ensureJsonOnlyFormat(options.format); } const agentRoot = await findAgentRootOrFail(process.cwd()); const { options: parsed, errors } = parseFilterTokens(tokens); if (errors.length > 0) { throw new AdkError({ code: "INVALID_FLAGS", message: errors.map((e) => e.suggestion ? `${e.message} (${e.suggestion})` : e.message).join("; "), expected: true }); } if (options.summary) { const summary = await buildLogSummary({ agentRoot, level: parsed.level, since: parsed.since, limit: parsed.limit }); logger.info("Log summary").result(summary); return; } if (!existsSync2(logStoreDbPath(agentRoot))) { logger.warn("No logs found. Run `adk dev` to start generating logs."); process.exit(0); } const isFollow = options.follow ?? false; const serverRunning = isDevServerLikelyRunning(agentRoot); if (!serverRunning) { if (isFollow) { logger.warn("Dev server is not running. Nothing to follow."); process.exit(0); } logger.warn(`Dev server is not running. Showing logs from last session. `); } const store = new SqliteLogStore(agentRoot); const query = { level: parsed.level, since: parsed.since }; if (isFollow) { const cap = parsed.limit; let printed = 0; const printEntry = (entry) => { if (cap && printed >= cap) return; logger.info(formatLogEntry(entry)).result(toResultJson(entry)); printed++; }; const existing = store.queryLogs({ ...query, limit: cap ?? 999999 }); for (const entry of existing) { printEntry(entry); } if (cap && printed >= cap) { store.close(); return; } const handle = createLogFollower(store, query); const cleanup = () => { handle.stop(); store.close(); process.exit(0); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); for await (const entry of handle.entries) { printEntry(entry); if (cap && printed >= cap) { cleanup(); break; } } } else { const entries = store.queryLogs({ ...query, limit: parsed.limit ?? 50 }); store.close(); if (entries.length === 0) { logger.info("No log entries match the given filters."); return; } for (const entry of entries) { logger.info(formatLogEntry(entry)).result(toResultJson(entry)); } } } export { adkLogs };