Files
2026-07-13 12:20:32 +08:00

140 lines
4.1 KiB
JavaScript

const express = require("express");
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const PORT = process.env.PORT || 8090;
const AGENT_USER = process.env.AGENT_USER || "agent";
const MEMORY_DIR =
process.env.MEMORY_DIR ||
`/home/${AGENT_USER}/.pi/agent/pi-hermes-memory`;
const MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
const USER_FILE = path.join(MEMORY_DIR, "USER.md");
const SESSIONS_DB = path.join(MEMORY_DIR, "sessions.db");
const DELIMITER = "§";
const CHAR_LIMIT = 5000;
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, "public")));
function readEntries(filePath) {
try {
const raw = fs.readFileSync(filePath, "utf-8").trim();
if (!raw) return [];
return raw.split(DELIMITER).map((e) => e.trim()).filter(Boolean);
} catch {
return [];
}
}
function writeEntries(filePath, entries) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(filePath, entries.join(` ${DELIMITER} `) + "\n");
}
function getFileStats(filePath) {
try {
const raw = fs.readFileSync(filePath, "utf-8");
return { chars: raw.length, exists: true };
} catch {
return { chars: 0, exists: false };
}
}
// --- API Routes ---
app.get("/api/memories", (_req, res) => {
res.json({ entries: readEntries(MEMORY_FILE) });
});
app.get("/api/user", (_req, res) => {
res.json({ entries: readEntries(USER_FILE) });
});
app.post("/api/memories", (req, res) => {
const { entry } = req.body;
if (!entry || !entry.trim()) return res.status(400).json({ error: "Empty entry" });
const entries = readEntries(MEMORY_FILE);
entries.push(entry.trim());
const combined = entries.join(` ${DELIMITER} `);
if (combined.length > CHAR_LIMIT) {
return res.status(400).json({ error: `Would exceed ${CHAR_LIMIT} char limit` });
}
writeEntries(MEMORY_FILE, entries);
res.json({ entries });
});
app.delete("/api/memories/:index", (req, res) => {
const entries = readEntries(MEMORY_FILE);
const idx = parseInt(req.params.index, 10);
if (idx < 0 || idx >= entries.length) return res.status(404).json({ error: "Not found" });
entries.splice(idx, 1);
writeEntries(MEMORY_FILE, entries);
res.json({ entries });
});
app.post("/api/user", (req, res) => {
const { entry } = req.body;
if (!entry || !entry.trim()) return res.status(400).json({ error: "Empty entry" });
const entries = readEntries(USER_FILE);
entries.push(entry.trim());
const combined = entries.join(` ${DELIMITER} `);
if (combined.length > CHAR_LIMIT) {
return res.status(400).json({ error: `Would exceed ${CHAR_LIMIT} char limit` });
}
writeEntries(USER_FILE, entries);
res.json({ entries });
});
app.delete("/api/user/:index", (req, res) => {
const entries = readEntries(USER_FILE);
const idx = parseInt(req.params.index, 10);
if (idx < 0 || idx >= entries.length) return res.status(404).json({ error: "Not found" });
entries.splice(idx, 1);
writeEntries(USER_FILE, entries);
res.json({ entries });
});
app.post("/api/review", (_req, res) => {
const child = spawn("pi", ["-p", "--no-session", "Review the recent conversation and save any durable memories, corrections, or lessons learned."], {
stdio: "ignore",
detached: true,
env: { ...process.env, HOME: `/home/${AGENT_USER}` },
});
child.unref();
res.json({ status: "review_triggered" });
});
app.get("/api/stats", (_req, res) => {
const memStats = getFileStats(MEMORY_FILE);
const userStats = getFileStats(USER_FILE);
let dbSize = 0;
try {
dbSize = fs.statSync(SESSIONS_DB).size;
} catch {}
res.json({
memory: {
chars: memStats.chars,
limit: CHAR_LIMIT,
entries: readEntries(MEMORY_FILE).length,
exists: memStats.exists,
},
user: {
chars: userStats.chars,
limit: CHAR_LIMIT,
entries: readEntries(USER_FILE).length,
exists: userStats.exists,
},
sessionsDbBytes: dbSize,
});
});
app.listen(PORT, () => {
console.log(`[memory-manager] listening on http://localhost:${PORT}`);
});