const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '..', 'data'); const DB_PATH = path.join(DATA_DIR, 'studyos.db'); if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } let _sqlDb = null; function saveToDisk() { if (!_sqlDb) return; const data = _sqlDb.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); } function flatParams(params) { if (params.length === 1 && Array.isArray(params[0])) return params[0]; return params; } function createWrapper(sqlDb) { return { prepare(sql) { return { run(...params) { const p = flatParams(params); sqlDb.run(sql, p); const rowidResult = sqlDb.exec('SELECT last_insert_rowid()'); const lastInsertRowid = rowidResult.length > 0 ? rowidResult[0].values[0][0] : 0; const changes = sqlDb.getRowsModified(); saveToDisk(); return { changes, lastInsertRowid }; }, get(...params) { const p = flatParams(params); let stmt = null; try { stmt = sqlDb.prepare(sql); stmt.bind(p); let result = null; if (stmt.step()) result = stmt.getAsObject(); return result; } finally { if (stmt) stmt.free(); } }, all(...params) { const p = flatParams(params); let stmt = null; try { stmt = sqlDb.prepare(sql); stmt.bind(p); const results = []; while (stmt.step()) results.push(stmt.getAsObject()); return results; } finally { if (stmt) stmt.free(); } }, }; }, exec(sql) { sqlDb.exec(sql); saveToDisk(); }, pragma(sql) { sqlDb.exec('PRAGMA ' + sql + ';'); }, }; } const db = createWrapper(null); async function initDB() { const SQL = await import('sql.js'); const initSqlJs = SQL.default; const sqlModule = await initSqlJs(); let sqlDb; if (fs.existsSync(DB_PATH)) { const buffer = fs.readFileSync(DB_PATH); sqlDb = new sqlModule.Database(buffer); } else { sqlDb = new sqlModule.Database(); } _sqlDb = sqlDb; const real = createWrapper(sqlDb); db.prepare = real.prepare; db.exec = real.exec; db.pragma = real.pragma; db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS models ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, api_base TEXT NOT NULL, api_key TEXT NOT NULL DEFAULT '', provider TEXT NOT NULL CHECK(provider IN ('openai', 'anthropic')), is_default_main INTEGER NOT NULL DEFAULT 0, is_default_fork INTEGER NOT NULL DEFAULT 0, is_default_exam INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN ('main', 'fork')), parent_id INTEGER REFERENCES conversations(id) ON DELETE SET NULL, model_id INTEGER REFERENCES models(id) ON DELETE SET NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'context_merge')), content TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS pdfs ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, original_name TEXT NOT NULL, content_markdown TEXT NOT NULL DEFAULT '', pages INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), reorder_index INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS progress ( id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL UNIQUE, exercises_done INTEGER NOT NULL DEFAULT 0, exercises_correct INTEGER NOT NULL DEFAULT 0, last_session TEXT, notes TEXT NOT NULL DEFAULT '[]' ); CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content_markdown TEXT NOT NULL DEFAULT '', tags TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT NOT NULL DEFAULT '' ); `); const modelCount = db.prepare('SELECT COUNT(*) as count FROM models').get(); if (!modelCount || modelCount.count === 0) { db.prepare(` INSERT INTO models (name, api_base, api_key, provider, is_default_main) VALUES (?, ?, ?, ?, ?) `).run('claude-sonnet-4', 'https://api.anthropic.com', '', 'anthropic', 1); } const vlmConfig = db.prepare("SELECT value FROM config WHERE key = ?").get('vlm_endpoint'); if (!vlmConfig) { db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run('vlm_endpoint', 'http://localhost:8080/vlm'); } return db; } module.exports = db; module.exports.initDB = initDB;