@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
79 lines • 2.87 kB
JavaScript
import { randomBytes } from 'node:crypto';
import { chmod, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
export function defaultStatePath() {
if (process.env.POPPY_AGENT_STATE)
return process.env.POPPY_AGENT_STATE;
const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
return join(base, 'poppy-agent', 'state.json');
}
/**
* The local state file. It holds private keys and Account Tokens, so it is written with mode
* 0600 (directory 0700) and atomically (temp file + rename).
*/
export class StateStore {
path;
data;
persist;
constructor(path, data, persist) {
this.path = path;
this.data = data;
this.persist = persist;
}
/**
* A state kept by the caller instead of a file — a server keeping each user's Companies in its
* own database. `persist` receives the whole state on every save (serialized, like the file).
*/
static memory(data = { version: 1, profiles: {} }, persist) {
data.profiles ??= {};
return new StateStore('', data, persist);
}
static async open(path = defaultStatePath()) {
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
let data = { version: 1, profiles: {} };
try {
const raw = await readFile(path, 'utf8');
data = JSON.parse(raw);
const st = await stat(path);
if ((st.mode & 0o077) !== 0)
await chmod(path, 0o600);
}
catch (e) {
if (e.code !== 'ENOENT')
throw e;
}
data.profiles ??= {};
return new StateStore(path, data);
}
/** Directory next to the state file, used for the Direct Sign-In callback relay. */
get dir() {
return dirname(this.path);
}
profile(name) {
this.data.profiles[name] ??= { companies: {} };
return this.data.profiles[name];
}
#saving = Promise.resolve();
/**
* Writes the state atomically (tmp file + rename). Saves are serialized: a conversation stream
* and the prompt can save at the same time, and two writes sharing one tmp file made the second
* `rename` fail with ENOENT.
*/
save() {
const run = this.#saving.then(() => this.#write());
this.#saving = run.catch(() => { });
return run;
}
async #write() {
if (this.path === '') {
await this.persist?.(this.data);
return;
}
const tmp = `${this.path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
await writeFile(tmp, `${JSON.stringify(this.data, null, 2)}\n`, { mode: 0o600 });
await chmod(tmp, 0o600);
await rename(tmp, this.path);
}
}
//# sourceMappingURL=store.js.map