AkurAI Build
Menu

popagent

public

Latest 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();