import os from 'node:os'; import path from 'node:path'; import { sessionsDb } from '@/modules/database/index.js'; import { buildLookupMap, extractFirstValidJsonlData, findFilesRecursivelyCreatedAfter, normalizeSessionName, readFileTimestamps, } from '@/shared/utils.js'; import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js'; type ParsedSession = { sessionId: string; projectPath: string; sessionName?: string; }; /** * Session indexer for Codex transcript artifacts. */ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer { private readonly provider = 'codex' as const; private readonly codexHome = path.join(os.homedir(), '.codex'); /** * Scans ~/.codex/sessions and upserts discovered sessions into DB. */ async synchronize(since?: Date): Promise { const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name'); const files = await findFilesRecursivelyCreatedAfter( path.join(this.codexHome, 'sessions'), '.jsonl', since ?? null ); let processed = 0; for (const filePath of files) { const parsed = await this.processSessionFile(filePath, nameMap); if (!parsed) { continue; } const timestamps = await readFileTimestamps(filePath); sessionsDb.createSession( parsed.sessionId, this.provider, parsed.projectPath, parsed.sessionName, timestamps.createdAt, timestamps.updatedAt, filePath ); processed += 1; } return processed; } /** * Parses and upserts one Codex session JSONL file. */ async synchronizeFile(filePath: string): Promise { if (!filePath.endsWith('.jsonl')) { return null; } const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name'); const parsed = await this.processSessionFile(filePath, nameMap); if (!parsed) { return null; } const timestamps = await readFileTimestamps(filePath); return sessionsDb.createSession( parsed.sessionId, this.provider, parsed.projectPath, parsed.sessionName, timestamps.createdAt, timestamps.updatedAt, filePath ); } /** * Extracts session metadata from one Codex JSONL session file. */ private async processSessionFile( filePath: string, nameMap: Map ): Promise { return extractFirstValidJsonlData(filePath, (rawData) => { const data = rawData as Record; const payload = data.payload as Record | undefined; const sessionId = typeof payload?.id === 'string' ? payload.id : undefined; const projectPath = typeof payload?.cwd === 'string' ? payload.cwd : undefined; if (!sessionId || !projectPath) { return null; } return { sessionId, projectPath, sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Codex Session'), }; }); } }