Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317#!/usr/bin/env node// memory/codex-sync.mjs// Imports recent Codex user/assistant messages from local Codex session logs.
import { existsSync } from "fs";import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises";import { homedir } from "os";import { basename, join } from "path";
import { commitEvent } from "./index.mjs";
const CODEX_SYNC_CURSOR_VERSION = 1;
function asBool(value, fallback = false) { if (value == null || value === "") return fallback; return /^(1|true|yes|on)$/i.test(String(value));}
function nowIso() { return new Date().toISOString();}
function parseArgs(argv) { const args = { _: [] }; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { args._.push(token); continue; }
const key = token.slice(2); const next = argv[i + 1]; if (next && !next.startsWith("--")) { args[key] = next; i += 1; } else { args[key] = true; } } return args;}
function toInt(value, fallback) { const n = Number(value); return Number.isFinite(n) ? n : fallback;}
function resolveMemoryHome() { if (process.env.AGENT_MEMORY_HOME) return process.env.AGENT_MEMORY_HOME; return join(homedir(), ".ac-agent-memory");}
function resolveCodexHome() { if (process.env.CODEX_HOME) return process.env.CODEX_HOME; return join(homedir(), ".codex");}
async function walkJsonlFiles(rootDir, out = []) { if (!existsSync(rootDir)) return out; const entries = await readdir(rootDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(rootDir, entry.name); if (entry.isDirectory()) { await walkJsonlFiles(fullPath, out); continue; } if (entry.isFile() && entry.name.endsWith(".jsonl")) { out.push(fullPath); } } return out;}
async function getLatestSessionFiles(codexHome, limit = 3) { const sessionsDir = join(codexHome, "sessions"); const files = await walkJsonlFiles(sessionsDir, []); const withTimes = []; for (const file of files) { try { const fileStat = await stat(file); withTimes.push({ file, mtimeMs: fileStat.mtimeMs, }); } catch { // Ignore unreadable files. } }
withTimes.sort((a, b) => b.mtimeMs - a.mtimeMs); return withTimes.slice(0, Math.max(1, limit)).map((entry) => entry.file);}
function sanitizeSessionId(sessionId) { return String(sessionId || "").replace(/[^a-zA-Z0-9._:-]/g, "-");}
function extractCodexSessionId(filePath) { const name = basename(filePath, ".jsonl"); const match = name.match( /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i ); if (match?.[1]) return `codex:${match[1]}`; return `codex:${name}`;}
function isBoilerplateMessage(text) { if (!text) return true; if (text.includes("AGENTS.md instructions for /workspaces/aesthetic-computer")) { return true; } if (text.includes("<permissions instructions>")) { return true; } return false;}
function truncateText(text, maxChars) { if (text.length <= maxChars) return text; return `${text.slice(0, maxChars)}\n\n[truncated by codex-sync]`;}
function cursorFilePath() { return join(resolveMemoryHome(), "imports", "codex-sync-cursor.json");}
async function loadCursor() { const path = cursorFilePath(); if (!existsSync(path)) { return { version: CODEX_SYNC_CURSOR_VERSION, files: {} }; } try { const parsed = JSON.parse(await readFile(path, "utf8")); if (!parsed || typeof parsed !== "object") { return { version: CODEX_SYNC_CURSOR_VERSION, files: {} }; } if (!parsed.files || typeof parsed.files !== "object") { parsed.files = {}; } return parsed; } catch { return { version: CODEX_SYNC_CURSOR_VERSION, files: {} }; }}
async function saveCursor(cursor) { const path = cursorFilePath(); await mkdir(join(resolveMemoryHome(), "imports"), { recursive: true }); await writeFile(path, `${JSON.stringify(cursor, null, 2)}\n`, "utf8");}
function parseJsonLine(line) { try { return JSON.parse(line); } catch { return null; }}
function messageFromRecord(record, includeAssistant) { if (record?.type !== "event_msg") return null; const payload = record.payload || {}; if (!payload?.type) return null;
if (payload.type === "user_message") { const text = typeof payload.message === "string" ? payload.message : ""; if (!text.trim()) return null; return { role: "user", text, metadata: { media_count: (Array.isArray(payload.images) ? payload.images.length : 0) + (Array.isArray(payload.local_images) ? payload.local_images.length : 0), }, }; }
if (includeAssistant && payload.type === "agent_message") { const text = typeof payload.message === "string" ? payload.message : ""; if (!text.trim()) return null; return { role: "assistant", text, metadata: {}, }; }
return null;}
export async function syncCodexSessions(options = {}) { const codexHome = options.codexHome || resolveCodexHome(); const includeAssistant = asBool( process.env.AGENT_MEMORY_CODEX_INCLUDE_ASSISTANT, false ); const maxSessions = Math.max(1, toInt(options.maxSessions, 3)); const maxEvents = Math.max(1, toInt(options.maxEvents, 120)); const maxChars = Math.max(500, toInt(options.maxChars, 20000));
if (!existsSync(codexHome)) { return { synced_events: 0, scanned_sessions: 0, skipped: "codex-home-missing", codex_home: codexHome, }; }
const sessionFiles = await getLatestSessionFiles(codexHome, maxSessions); if (sessionFiles.length === 0) { return { synced_events: 0, scanned_sessions: 0, skipped: "no-codex-sessions", codex_home: codexHome, }; }
const cursor = await loadCursor(); let syncedEvents = 0; let scannedSessions = 0; let reachedEventCap = false;
for (const file of sessionFiles) { scannedSessions += 1; const raw = await readFile(file, "utf8"); const lines = raw .split("\n") .map((line) => line.trim()) .filter(Boolean);
const existing = cursor.files[file] || {}; const startIndex = Math.min(toInt(existing.lines, 0), lines.length); const pending = lines.slice(startIndex); const sessionId = sanitizeSessionId(extractCodexSessionId(file)); let consumedLines = startIndex;
for (const line of pending) { if (syncedEvents >= maxEvents) { reachedEventCap = true; break; } consumedLines += 1;
const record = parseJsonLine(line); if (!record) continue;
const message = messageFromRecord(record, includeAssistant); if (!message) continue; if (isBoilerplateMessage(message.text)) continue;
const text = truncateText(message.text, maxChars);
await commitEvent({ sessionId, provider: "codex", role: message.role, source: "codex-sync", project: process.env.AGENT_MEMORY_PROJECT || "aesthetic-computer", model: null, text, context: { codex_file: file, codex_timestamp: record.timestamp || null, }, metadata: { codex_sync: true, imported_at: nowIso(), ...(message.metadata || {}), }, title: "Codex Session", });
syncedEvents += 1; }
cursor.files[file] = { lines: consumedLines, updated_at: nowIso(), };
if (reachedEventCap) { break; } }
await saveCursor(cursor);
return { synced_events: syncedEvents, scanned_sessions: scannedSessions, codex_home: codexHome, include_assistant: includeAssistant, };}
async function main() { const args = parseArgs(process.argv.slice(2)); const result = await syncCodexSessions({ maxSessions: args["max-sessions"], maxEvents: args["max-events"], maxChars: args["max-chars"], codexHome: args["codex-home"], }); console.log(JSON.stringify(result, null, 2));}
if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error(`codex-sync: ${error.message}`); process.exit(1); });}