@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
332 lines (325 loc) • 9.39 kB
JavaScript
// @bun
import {
createCliLogger
} from "./chunk-gzwt1qdr.js";
// src/utils/sqlite-log-store.ts
import { Database } from "bun:sqlite";
import { mkdirSync } from "fs";
import { join as join2 } from "path";
// src/logs/log-reader.ts
import { existsSync, readdirSync, statSync } from "fs";
import { join } from "path";
function normalizeLevel(type) {
const lower = type.toLowerCase();
if (lower === "error")
return "error";
if (lower === "warn" || lower === "warning")
return "warning";
if (lower === "info" || lower === "stdout" || lower === "stderr" || lower === "log")
return "info";
return lower;
}
function findLatestLogFile(agentRoot) {
const logsDir = join(agentRoot, ".adk", "logs");
if (!existsSync(logsDir))
return null;
try {
const files = readdirSync(logsDir).filter((f) => f.startsWith("adk-dev-") && f.endsWith(".log")).map((f) => ({
name: f,
path: join(logsDir, f),
mtime: statSync(join(logsDir, f)).mtime.getTime()
})).sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].path : null;
} catch {
return null;
}
}
function parseLine(line) {
if (!line.trim())
return null;
try {
const parsed = JSON.parse(line);
if (parsed.type === "worker_stats")
return null;
const msg = parsed.message || (Array.isArray(parsed.args) ? parsed.args.join(" ") : "") || "";
if (msg.includes("ExperimentalWarning") || msg.includes("punycode"))
return null;
return {
timestamp: parsed.timestamp || new Date().toISOString(),
type: parsed.type || "info",
level: normalizeLevel(parsed.type || parsed.level || "info"),
message: msg,
raw: line,
spanId: parsed.spanId,
traceId: parsed.traceId
};
} catch {
return {
timestamp: new Date().toISOString(),
type: "raw",
level: "raw",
message: line,
raw: line
};
}
}
function matchesLevel(entry, level) {
if (!level)
return true;
const l = entry.level;
if (level === "error")
return l === "error";
if (level === "warning")
return l === "error" || l === "warning";
if (level === "info")
return l === "error" || l === "warning" || l === "info";
return true;
}
function matchesSince(entry, since) {
if (!since)
return true;
const ts = new Date(entry.timestamp).getTime();
return !isNaN(ts) && ts >= since;
}
function filterLogEntries(entries, options) {
const limit = options.limit ?? 50;
return entries.filter((entry) => matchesLevel(entry, options.level) && matchesSince(entry, options.since)).slice(-limit);
}
// src/utils/sqlite-log-store.ts
var logger = createCliLogger({ tag: "sqlite-log-store" });
var FLUSH_DELAY_MS = 16;
var FLUSH_MAX_BATCH = 250;
var CHECKPOINT_INTERVAL_MS = 5000;
var SCHEMA = `
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts REAL NOT NULL,
timestamp TEXT NOT NULL,
type TEXT NOT NULL,
level TEXT NOT NULL,
message TEXT NOT NULL,
span_id TEXT,
trace_id TEXT,
raw TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs(ts);
CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
CREATE INDEX IF NOT EXISTS idx_logs_trace ON logs(trace_id) WHERE trace_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_logs_span ON logs(span_id) WHERE span_id IS NOT NULL;
`;
function levelsFor(level) {
if (level === "error")
return ["error"];
if (level === "warning")
return ["error", "warning"];
return ["error", "warning", "info"];
}
function rowToEntry(row) {
return {
id: row.id,
timestamp: row.timestamp,
type: row.type,
level: row.level,
message: row.message,
raw: row.raw,
spanId: row.span_id ?? undefined,
traceId: row.trace_id ?? undefined
};
}
class SqliteLogStore {
db = null;
dbPath;
logsDir;
insertStmt = null;
writeQueue = [];
flushTimer = null;
checkpointTimer = null;
constructor(agentPath) {
this.logsDir = agentPath ? join2(agentPath, ".adk", "bot", "logs") : process.env.LOG_DIR ?? "logs";
this.dbPath = join2(this.logsDir, "logs.db");
}
getDb() {
if (this.db)
return this.db;
mkdirSync(this.logsDir, { recursive: true });
try {
this.db = new Database(this.dbPath);
this.db.run("PRAGMA journal_mode=WAL");
this.db.run("PRAGMA synchronous=NORMAL");
this.db.run("PRAGMA busy_timeout=5000");
this.db.run("PRAGMA wal_autocheckpoint=0");
this.db.run(SCHEMA);
this.insertStmt = this.db.prepare(`
INSERT INTO logs (ts, timestamp, type, level, message, span_id, trace_id, raw)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
this.startCheckpointTimer();
return this.db;
} catch (err) {
logger.warn(`Failed to initialize database: ${err instanceof Error ? err.message : String(err)}`);
return null;
}
}
clear() {
this.writeQueue = [];
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
const db = this.getDb();
if (!db)
return;
db.run("DELETE FROM logs");
}
insertLine(line) {
const entry = parseLine(line);
if (!entry)
return;
this.insertEntry(entry);
}
insertEntry(entry) {
const db = this.getDb();
if (!db || !this.insertStmt)
return;
const parsedTs = Date.parse(entry.timestamp);
const ts = Number.isNaN(parsedTs) ? Date.now() : parsedTs;
this.insertStmt.run(ts, entry.timestamp, entry.type, entry.level, entry.message, entry.spanId ?? null, entry.traceId ?? null, entry.raw);
}
enqueueLine(line) {
const entry = parseLine(line);
if (!entry)
return;
this.writeQueue.push(entry);
this.scheduleFlush();
}
scheduleFlush() {
if (this.writeQueue.length >= FLUSH_MAX_BATCH) {
this.flush();
return;
}
if (this.flushTimer)
return;
this.flushTimer = setTimeout(() => {
this.flushTimer = null;
this.flush();
}, FLUSH_DELAY_MS);
this.flushTimer.unref?.();
}
flush() {
if (this.writeQueue.length === 0)
return;
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
const batch = this.writeQueue;
this.writeQueue = [];
const db = this.getDb();
if (!db)
return;
try {
db.transaction((entries) => {
for (const entry of entries) {
try {
this.insertEntry(entry);
} catch (err) {
logger.warn(`Failed to write log entry: ${err instanceof Error ? err.message : String(err)}`);
}
}
})(batch);
} catch (err) {
logger.warn(`Log batch flush failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
startCheckpointTimer() {
if (this.checkpointTimer)
return;
this.checkpointTimer = setInterval(() => {
try {
this.db?.run("PRAGMA wal_checkpoint(PASSIVE)");
} catch {}
}, CHECKPOINT_INTERVAL_MS);
this.checkpointTimer.unref?.();
}
getRecentLogs(limit = 50) {
return this.queryLogs({ limit });
}
queryLogs(query) {
this.flush();
const db = this.getDb();
if (!db)
return [];
const limit = query.limit ?? 50;
if (limit <= 0)
return [];
const where = [];
const params = [];
if (query.level) {
const levels = levelsFor(query.level);
where.push(`level IN (${levels.map(() => "?").join(",")})`);
params.push(...levels);
}
if (query.since != null) {
where.push("ts >= ?");
params.push(query.since);
}
const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
const rows = db.prepare(`SELECT * FROM (SELECT * FROM logs ${whereSql} ORDER BY id DESC LIMIT ?) ORDER BY id ASC`).all(...params, limit);
return rows.map(rowToEntry);
}
getMaxId() {
this.flush();
const db = this.getDb();
if (!db)
return 0;
const row = db.prepare("SELECT MAX(id) as maxId FROM logs").get();
return row?.maxId ?? 0;
}
getLogsAfter(afterId, query = {}) {
this.flush();
const db = this.getDb();
if (!db)
return [];
const where = ["id > ?"];
const params = [afterId];
if (query.level) {
const levels = levelsFor(query.level);
where.push(`level IN (${levels.map(() => "?").join(",")})`);
params.push(...levels);
}
if (query.since != null) {
where.push("ts >= ?");
params.push(query.since);
}
const rows = db.prepare(`SELECT * FROM logs WHERE ${where.join(" AND ")} ORDER BY id ASC`).all(...params);
return rows.map(rowToEntry);
}
getLogsByTrace(traceId) {
this.flush();
const db = this.getDb();
if (!db)
return [];
const rows = db.prepare("SELECT * FROM logs WHERE trace_id = ? ORDER BY id ASC").all(traceId);
return rows.map(rowToEntry);
}
close() {
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
if (this.checkpointTimer) {
clearInterval(this.checkpointTimer);
this.checkpointTimer = null;
}
try {
this.flush();
this.db?.run("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {}
try {
this.db?.close();
this.db = null;
this.insertStmt = null;
} catch {}
}
}
export { findLatestLogFile, filterLogEntries, SqliteLogStore };