agentcrumbs
Version:
Debug mode for any agent.
145 lines • 4.11 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
const DEFAULT_BASE = path.join(os.homedir(), ".agentcrumbs");
const DEFAULT_FILE = "crumbs.jsonl";
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB before rotation
export class CrumbStore {
filePath;
fd;
constructor(dir) {
const storeDir = dir ?? DEFAULT_BASE;
if (!fs.existsSync(storeDir)) {
fs.mkdirSync(storeDir, { recursive: true });
}
this.filePath = path.join(storeDir, DEFAULT_FILE);
}
static forApp(app, baseDir) {
const base = baseDir ?? DEFAULT_BASE;
return new CrumbStore(path.join(base, app));
}
static listApps(baseDir) {
const base = baseDir ?? DEFAULT_BASE;
try {
const entries = fs.readdirSync(base, { withFileTypes: true });
return entries
.filter((e) => e.isDirectory())
.map((e) => e.name)
.sort();
}
catch {
return [];
}
}
static readAllApps(baseDir) {
const apps = CrumbStore.listApps(baseDir);
const allCrumbs = [];
for (const app of apps) {
const store = CrumbStore.forApp(app, baseDir);
allCrumbs.push(...store.readAll());
}
// Also read legacy flat file if it exists
const base = baseDir ?? DEFAULT_BASE;
const legacyPath = path.join(base, DEFAULT_FILE);
try {
if (fs.existsSync(legacyPath)) {
const legacy = new CrumbStore(base);
const legacyCrumbs = legacy.readAll();
// Tag legacy crumbs missing app field
for (const crumb of legacyCrumbs) {
if (!crumb.app)
crumb.app = "unknown";
allCrumbs.push(crumb);
}
}
}
catch {
// ignore
}
allCrumbs.sort((a, b) => a.ts.localeCompare(b.ts));
return allCrumbs;
}
ensureFd() {
if (this.fd === undefined) {
this.fd = fs.openSync(this.filePath, "a");
}
return this.fd;
}
append(crumb) {
const line = JSON.stringify(crumb) + "\n";
fs.writeSync(this.ensureFd(), line);
this.maybeRotate();
}
appendRaw(line) {
fs.writeSync(this.ensureFd(), line.endsWith("\n") ? line : line + "\n");
this.maybeRotate();
}
maybeRotate() {
try {
const stat = fs.fstatSync(this.ensureFd());
if (stat.size > MAX_FILE_SIZE) {
this.rotate();
}
}
catch {
// ignore
}
}
rotate() {
if (this.fd !== undefined) {
fs.closeSync(this.fd);
this.fd = undefined;
}
const rotated = this.filePath + ".1";
try {
if (fs.existsSync(rotated))
fs.unlinkSync(rotated);
fs.renameSync(this.filePath, rotated);
}
catch {
// ignore rotation errors
}
}
getFilePath() {
return this.filePath;
}
readAll() {
try {
const content = fs.readFileSync(this.filePath, "utf-8");
return content
.split("\n")
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line);
}
catch {
return null;
}
})
.filter((c) => c !== null);
}
catch {
return [];
}
}
clear() {
if (this.fd !== undefined) {
fs.closeSync(this.fd);
this.fd = undefined;
}
try {
fs.writeFileSync(this.filePath, "");
}
catch {
// ignore
}
}
close() {
if (this.fd !== undefined) {
fs.closeSync(this.fd);
this.fd = undefined;
}
}
}
//# sourceMappingURL=store.js.map