@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
44 lines (43 loc) • 2.04 kB
JavaScript
import { appendFile, mkdir, readFile, rename, stat } from "node:fs/promises";
import path from "node:path";
import { bridgeRoot } from "./protocol.js";
/** Local bounded journal. No prompts, transcript bodies, credentials, or cloud upload. */
export class ActivityJournal {
pending = Promise.resolve();
previous = new Map();
record(server, panes) {
this.pending = this.pending.catch(() => { }).then(async () => {
const changes = [];
for (const pane of panes) {
if (!pane.agent)
continue;
const key = `${server}:${pane.pane_id}`;
const state = JSON.stringify([pane.agent, pane.agent_status, pane.terminal_id]);
if (this.previous.get(key) === state)
continue;
this.previous.set(key, state);
changes.push({ at: new Date().toISOString(), source: "herdr", server, workspace: pane.workspace_id,
tab: pane.tab_id, pane: pane.pane_id, provider: pane.agent, state: pane.agent_status, directory: pane.foreground_cwd || pane.cwd });
}
if (!changes.length)
return;
const root = bridgeRoot(), file = path.join(root, "activity.jsonl");
await mkdir(root, { recursive: true, mode: 0o700 });
if ((await stat(file).catch(() => null))?.size && (await stat(file)).size > 2_097_152)
await rename(file, file + ".previous");
await appendFile(file, changes.map(row => JSON.stringify(row)).join("\n") + "\n", { mode: 0o600 });
if (this.previous.size > 4096)
this.previous.clear();
});
return this.pending;
}
async recent() {
const text = await readFile(path.join(bridgeRoot(), "activity.jsonl"), "utf8").catch(() => "");
return text.trim().split("\n").slice(-500).flatMap(line => { try {
return [JSON.parse(line)];
}
catch {
return [];
} });
}
}