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