import { createRequire } from 'node:module' /// Thin SQLite read-only wrapper over Node's built-in `node:sqlite` module (stable in /// Node 24, experimental in Node 22 / 23). Replaces the earlier `better-sqlite3` binding /// so the dependency graph no longer pulls in the deprecated `prebuild-install` package /// (issue #75). Works across Cursor and OpenCode session DBs, both of which we only read. const requireForSqlite = createRequire(import.meta.url) type Row = Record export type SqliteDatabase = { query(sql: string, params?: unknown[]): T[] close(): void } type DatabaseSyncCtor = new (path: string, options?: { readOnly?: boolean }) => { prepare(sql: string): { all(...params: unknown[]): Row[] } exec?(sql: string): void close(): void } let DatabaseSync: DatabaseSyncCtor | null = null let loadAttempted = false let loadError: string | null = null const textDecoder = new TextDecoder('utf-8', { fatal: false }) /// Safely decode a BLOB column (Uint8Array) to a UTF-8 string. Node's /// node:sqlite crashes with a V8 CHECK abort when a TEXT column contains /// invalid UTF-8 (common in Cursor chat blobs with truncated multi-byte /// chars). By selecting those columns as `CAST(... AS BLOB)` in SQL, we /// get a Uint8Array here and decode it in JS where bad bytes become the /// U+FFFD replacement character instead of aborting the process. export function blobToText(value: Uint8Array | string | null | undefined): string { if (value == null) return '' if (typeof value === 'string') return value return textDecoder.decode(value) } /// Lazily imports `node:sqlite`. On Node 22/23 it emits an ExperimentalWarning the first /// time the module is loaded; we silence that specific warning once so dashboards aren't /// preceded by a scary stderr line every run. Any other warnings (including future /// non-SQLite ones) are left untouched. function loadDriver(): boolean { if (loadAttempted) return DatabaseSync !== null loadAttempted = true const origEmit = process.emit.bind(process) let restored = false const restore = () => { if (restored) return restored = true process.emit = origEmit } // Node's `process.emit` signature is overloaded; we intercept the 'warning' channel // only and proxy everything else through unchanged. The `any` cast avoids chasing the // overload union which isn't worth its verbosity for a single-purpose shim. // eslint-disable-next-line @typescript-eslint/no-explicit-any process.emit = function patchedEmit(this: NodeJS.Process, event: string, ...args: any[]): boolean { if (event === 'warning') { const warning = args[0] as { name?: string; message?: string } | undefined if ( warning?.name === 'ExperimentalWarning' && typeof warning.message === 'string' && /SQLite/i.test(warning.message) ) { return false } } // eslint-disable-next-line @typescript-eslint/no-explicit-any return (origEmit as any).call(this, event, ...args) } as typeof process.emit try { const mod = requireForSqlite('node:sqlite') as { DatabaseSync: DatabaseSyncCtor } DatabaseSync = mod.DatabaseSync return true } catch (err) { const message = err instanceof Error ? err.message : String(err) loadError = 'SQLite-based providers (Cursor, OpenCode) need Node 22+ with the node:sqlite module.\n' + `Current Node: ${process.version}.\n` + 'Upgrade Node (https://nodejs.org) and run codeburn again.\n' + `(underlying error: ${message})` return false } finally { process.nextTick(restore) } } export function isSqliteAvailable(): boolean { return loadDriver() } export function getSqliteLoadError(): string { return loadError ?? 'SQLite driver not available' } export function isSqliteBusyError(err: unknown): boolean { const e = err as { code?: unknown; errcode?: unknown; errstr?: unknown; message?: unknown } | null const code = typeof e?.code === 'string' ? e.code : '' const errcode = typeof e?.errcode === 'number' ? e.errcode : null const message = [ typeof e?.message === 'string' ? e.message : '', typeof e?.errstr === 'string' ? e.errstr : '', ].join(' ') return ( errcode === 5 || errcode === 6 || code === 'SQLITE_BUSY' || code === 'SQLITE_LOCKED' || /\bSQLITE_(BUSY|LOCKED)\b|database (?:is |table is )?locked/i.test(message) ) } export function openDatabase(path: string): SqliteDatabase { if (!loadDriver() || DatabaseSync === null) { throw new Error(getSqliteLoadError()) } const db = new DatabaseSync(path, { readOnly: true }) try { db.exec?.('PRAGMA busy_timeout = 1000') } catch { // Best effort. Some Node sqlite builds may not expose exec on DatabaseSync. } return { query(sql: string, params: unknown[] = []): T[] { return db.prepare(sql).all(...params) as T[] }, close() { db.close() }, } }