import { Database } from "bun:sqlite"; import type { IOpenCodeReader, OpenCodeSession, OpenCodeMessage, OpenCodeMessageWindow, OpenCodeAgent, } from "./types.js"; import { BUILTIN_AGENTS } from "./types.js"; export class SqliteReader implements IOpenCodeReader { private db: Database; constructor(dbPath: string) { this.db = new Database(dbPath, { readonly: true }); } async getSessionsUpdatedSince(since: Date): Promise { const sinceMs = since.getTime(); const rows = this.db .query( ` SELECT id, title, directory, project_id, parent_id, time_created, time_updated FROM session WHERE time_updated > ?1 AND parent_id IS NULL ORDER BY time_updated DESC ` ) .all(sinceMs) as Array<{ id: string; title: string; directory: string; project_id: string; parent_id: string | null; time_created: number; time_updated: number; }>; return rows.map((row) => ({ id: row.id, title: row.title, directory: row.directory, projectId: row.project_id, parentId: row.parent_id ?? undefined, time: { created: row.time_created, updated: row.time_updated, }, })); } async getSessionsInRange(from: Date, to: Date): Promise { const fromMs = from.getTime(); const toMs = to.getTime(); const rows = this.db .query( ` SELECT id, title, directory, project_id, parent_id, time_created, time_updated FROM session WHERE time_updated > ?1 AND time_updated <= ?2 AND parent_id IS NULL ORDER BY time_updated ASC ` ) .all(fromMs, toMs) as Array<{ id: string; title: string; directory: string; project_id: string; parent_id: string | null; time_created: number; time_updated: number; }>; return rows.map((row) => ({ id: row.id, title: row.title, directory: row.directory, projectId: row.project_id, parentId: row.parent_id ?? undefined, time: { created: row.time_created, updated: row.time_updated, }, })); } async getMessagesForSession( sessionId: string, since?: Date ): Promise { const sinceMs = since?.getTime() ?? 0; const messages = this.db .query( ` SELECT id, session_id, time_created, data FROM message WHERE session_id = ?1 AND time_created > ?2 ORDER BY time_created ASC ` ) .all(sessionId, sinceMs) as Array<{ id: string; session_id: string; time_created: number; data: string; }>; const result: OpenCodeMessage[] = []; for (const msg of messages) { const msgData = JSON.parse(msg.data) as { role: "user" | "assistant"; agent?: string }; const content = this.getMessageContent(msg.id); if (!content) continue; result.push({ id: msg.id, sessionId: msg.session_id, role: msgData.role, agent: (msgData.agent || "build").toLowerCase(), content, timestamp: new Date(msg.time_created).toISOString(), }); } return result; } private getMessageContent(messageId: string): string | null { const parts = this.db .query( ` SELECT data, time_created FROM part WHERE message_id = ?1 ORDER BY time_created ASC ` ) .all(messageId) as Array<{ data: string; time_created: number }>; const textParts: string[] = []; for (const part of parts) { const partData = JSON.parse(part.data) as { type: string; synthetic?: boolean; text?: string; }; if (partData.type !== "text") continue; if (partData.synthetic === true) continue; if (!partData.text) continue; textParts.push(partData.text); } return textParts.length > 0 ? textParts.join("\n\n") : null; } async getMessageById(messageId: string, before = 0, after = 0): Promise { const row = this.db .query(`SELECT session_id FROM message WHERE id = ?1 LIMIT 1`) .get(messageId) as { session_id: string } | null; if (!row) return null; const sessionRow = this.db .query(`SELECT id, title, directory, project_id, parent_id, time_created, time_updated FROM session WHERE id = ?1 LIMIT 1`) .get(row.session_id) as { id: string; title: string; directory: string; project_id: string; parent_id: string | null; time_created: number; time_updated: number } | null; if (!sessionRow) return null; const session: OpenCodeSession = { id: sessionRow.id, title: sessionRow.title, directory: sessionRow.directory, projectId: sessionRow.project_id, parentId: sessionRow.parent_id ?? undefined, time: { created: sessionRow.time_created, updated: sessionRow.time_updated }, }; const allMessages = await this.getMessagesForSession(row.session_id); const idx = allMessages.findIndex(m => m.id === messageId); if (idx === -1) return null; return { message: allMessages[idx], before: allMessages.slice(Math.max(0, idx - before), idx), after: allMessages.slice(idx + 1, idx + 1 + after), session, }; } async getAgentInfo(agentName: string): Promise { const normalized = agentName.toLowerCase(); if (BUILTIN_AGENTS[normalized]) { return BUILTIN_AGENTS[normalized]; } return { name: agentName, description: "OpenCode coding agent" }; } async getAllUniqueAgents(sessionId: string): Promise { const messages = await this.getMessagesForSession(sessionId); return [...new Set(messages.map((m) => m.agent))]; } async getFirstAgent(sessionId: string): Promise { const row = this.db .query( ` SELECT data FROM message WHERE session_id = ?1 ORDER BY time_created ASC LIMIT 1 ` ) .get(sessionId) as { data: string } | null; if (!row) return null; const msgData = JSON.parse(row.data) as { agent?: string }; return (msgData.agent || "build").toLowerCase(); } close(): void { this.db.close(); } }