Menu
popagent
publicLatest change 7f0ff66d6d9fb6468416c58bee46bd3d08169501 - Checkpoint browser channels and memory work by AkurAI Build
import { mkdir, readdir, rm, stat } from "node:fs/promises";
import { basename, join, resolve } from "node:path";
export type BrowserRecording = { name: string; size: number; createdAt: string };
export class BrowserRecordingStore {
readonly root: string;
constructor(root = join(process.env.POPAGENT_DATA_DIR ?? join(process.cwd(), "data"), "browser-recordings"), private readonly now = Date.now) {
this.root = resolve(root);
}
async list(): Promise<BrowserRecording[]> {
await mkdir(this.root, { recursive: true, mode: 0o700 });
const entries = await readdir(this.root, { withFileTypes: true });
const files = await Promise.all(entries
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".avi"))
.map(async (entry) => {
const info = await stat(join(this.root, entry.name));
return { name: entry.name, size: info.size, createdAt: info.mtime.toISOString(), mtime: info.mtimeMs };
}));
return files.sort((left, right) => right.mtime - left.mtime).map(({ mtime: _mtime, ...file }) => file);
}
async pathFor(name: string): Promise<string> {
if (!name || basename(name) !== name || !name.toLowerCase().endsWith(".avi")) throw new Error("Invalid recording name");
const path = resolve(this.root, name);
if (!path.startsWith(`${this.root}/`)) throw new Error("Invalid recording path");
const info = await stat(path).catch(() => undefined);
if (!info?.isFile()) throw new Error("Recording not found");
return path;
}
async delete(name: string): Promise<boolean> {
try {
await rm(await this.pathFor(name));
return true;
} catch (error) {
if (error instanceof Error && error.message === "Recording not found") return false;
throw error;
}
}
async cleanup(settings: { retentionDays: number; maxFiles: number }): Promise<number> {
const files = await this.list();
const cutoff = this.now() - settings.retentionDays * 86_400_000;
const remove = files.filter((file, index) => Date.parse(file.createdAt) < cutoff || index >= settings.maxFiles);
await Promise.all(remove.map((file) => this.delete(file.name)));
return remove.length;
}
}
export const browserRecordings = new BrowserRecordingStore();