UNPKG

rawi

Version:

Rawi (راوي) is the developer-friendly AI CLI that brings the power of 11 major AI providers directly to your terminal. With seamless shell integration, persistent conversations, and 200+ specialized prompt templates, Rawi transforms your command line into

1 lines 36.3 kB
{"version":3,"sources":["/home/mkabumattar/work/withrawi/rawi/dist/chunk-LF24LEAH.cjs","../src/core/database/adapter.ts"],"names":["DatabaseAdapter","#client","#dbPath","getDatabaseFilePath","#ensureConfigDir","#ensureDatabaseFileExists","dbUrl"],"mappings":"AAAA;AACA,wDAAwC,wDAAgD,wDAAyC,wBCM1H,4BACe,wCACK,4BACA,IAadA,CAAAA,CAAN,KAAsB,CAC3BC,CAAAA,CAAAA,CACAC,CAAAA,CAAAA,CAEA,WAAA,CAAA,CAAc,CACZ,GAAI,CACF,IAAA,CAAKA,CAAAA,CAAAA,CAAUC,iCAAAA,CAAoB,CAEnC,IAAA,CAAKC,CAAAA,CAAAA,CAAiB,CAAA,CAEtB,IAAA,CAAKC,CAAAA,CAAAA,CAA0B,CAAA,CAE/B,IAAMC,CAAAA,CAAQ,CAAA,KAAA,EAAQ,IAAA,CAAKJ,CAAAA,CAAO,CAAA,CAAA;AAsFe;AAAA;AA4B/C,QAAA;AAkN2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBA,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBA,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBE,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,UAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,UAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASoC,2CAAA;AACC,4CAAA;AAAA;AAAA;AAAA;AAAA;AAgBrC,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAKA,UAAA;AAAA;AAAA;AAakB,UAAA;AAAA;AAAA;AAoCnB,QAAA;AAwCtB;AAAA;AAmCqC,IAAA;AA6CxC;AAAA;AAuBG,6CAAA;AAAA;AAAA;AAAA;AAkCR,IAAA;AAQQ;AAAA;AAAA;AA0CS,IAAA;AA2BF;AAAA;AAAA;AAAA;AAAA;AAUD,IAAA;AAAA;AAAA;AAcE,MAAA;AAAA;AAAA;AAOD,IAAA;AAAA;AAAA;AAmBF,MAAA;AAAA;AAAA;AAOD,IAAA;AAAA;AAAA;AAmBK,MAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,IAAA;AAAA;AAAA;AAAA;AAAA;AASD,IAAA;AAAA;AAAA;AA8Bb,MAAA;AAIE;AAAA;AA2CI,MAAA;AAEgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAyBzB,IAAA;AA2CoB,sCAAA;AAuClB,yCAAA;ADp5B4a","file":"/home/mkabumattar/work/withrawi/rawi/dist/chunk-LF24LEAH.cjs","sourcesContent":[null,"import {\n accessSync,\n constants,\n existsSync,\n mkdirSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport {dirname} from 'node:path';\nimport {createClient} from '@libsql/client';\nimport {v4 as uuidv4} from 'uuid';\nimport {\n type ChatHistoryOptions,\n type ChatMessage,\n type ChatSession,\n DEFAULT_SESSION_TITLE_LENGTH,\n debugLog,\n getConfigDir,\n type HistoryStats,\n type SupportedProvider,\n} from '../shared/index.js';\nimport {getDatabaseFilePath} from './config/paths.js';\n\nexport class DatabaseAdapter {\n #client;\n #dbPath: string;\n\n constructor() {\n try {\n this.#dbPath = getDatabaseFilePath();\n\n this.#ensureConfigDir();\n\n this.#ensureDatabaseFileExists();\n\n const dbUrl = `file:${this.#dbPath}`;\n this.#client = createClient({\n url: dbUrl,\n });\n\n debugLog('Database adapter initialized with URL:', dbUrl);\n } catch (error) {\n console.error('Error initializing database adapter:', error);\n throw new Error(\n `Failed to initialize database adapter: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n }\n\n #ensureConfigDir(): void {\n try {\n const configDir = getConfigDir();\n debugLog('Ensuring config directory exists:', configDir);\n\n if (!existsSync(configDir)) {\n debugLog('Config directory not found, creating it');\n mkdirSync(configDir, {recursive: true});\n debugLog('Config directory created successfully');\n } else {\n debugLog('Config directory already exists');\n }\n } catch (error) {\n console.error('Error ensuring config directory exists:', error);\n throw new Error(\n `Failed to create config directory: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n }\n\n #ensureDatabaseFileExists(): void {\n try {\n const dbDir = dirname(this.#dbPath);\n if (!existsSync(dbDir)) {\n debugLog(`Database directory not found, creating it: ${dbDir}`);\n mkdirSync(dbDir, {recursive: true});\n }\n\n if (!existsSync(this.#dbPath)) {\n debugLog('Database file not found, creating it:', this.#dbPath);\n\n writeFileSync(this.#dbPath, '');\n\n if (!existsSync(this.#dbPath)) {\n throw new Error(`Failed to create database file at ${this.#dbPath}`);\n }\n\n debugLog('Empty database file created successfully');\n } else {\n debugLog('Database file already exists');\n\n try {\n accessSync(this.#dbPath, constants.W_OK);\n debugLog('Database file is writable');\n } catch (accessError) {\n console.error(\n 'Database file exists but is not writable!',\n accessError,\n );\n throw new Error(`Database file at ${this.#dbPath} is not writable`);\n }\n }\n } catch (error) {\n console.error('Error ensuring database file exists:', error);\n throw new Error(\n `Failed to create or access database file: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n }\n\n public async ensureDatabaseInitialized(): Promise<void> {\n let attempt = 1;\n const maxAttempts = 3;\n\n while (attempt <= maxAttempts) {\n try {\n debugLog(`Checking if database schema exists (attempt ${attempt})...`);\n\n this.#ensureConfigDir();\n\n this.#ensureDatabaseFileExists();\n\n const tableResult = await this.#client.execute(`\n SELECT count(*) as table_count FROM sqlite_master \n WHERE type='table' AND name IN ('sessions', 'messages');\n `);\n\n const tableCount = Number(tableResult.rows[0].table_count);\n\n if (tableCount === 2) {\n debugLog('Database schema verified, both tables exist');\n\n try {\n await this.#client.execute('SELECT COUNT(*) FROM sessions');\n await this.#client.execute('SELECT COUNT(*) FROM messages');\n debugLog('Database tables are accessible');\n return;\n } catch (queryError) {\n console.error(\n 'Tables exist but querying them failed:',\n queryError instanceof Error\n ? queryError.message\n : 'Unknown error',\n );\n }\n }\n\n debugLog(\n `Found only ${tableCount} of 2 required tables, initializing schema...`,\n );\n await this.#initialize();\n return;\n } catch (error) {\n debugLog(`Error checking database schema (attempt ${attempt})`);\n\n if (error instanceof Error) {\n debugLog('Error details:', error.message);\n\n const isMissingTable =\n error.message.includes('no such table') ||\n error.message.includes('unable to open database file');\n\n if (isMissingTable) {\n debugLog(\n 'Database tables missing or database file issues. Initializing schema...',\n );\n\n if (attempt === maxAttempts) {\n try {\n debugLog(\n 'Last attempt - recreating database file from scratch',\n );\n\n if (this.#client) {\n try {\n await this.#client.close();\n } catch (e: unknown) {\n debugLog(\n 'Error closing client:',\n e instanceof Error ? e.message : 'Unknown error',\n );\n }\n }\n\n try {\n if (existsSync(this.#dbPath)) {\n unlinkSync(this.#dbPath);\n debugLog('Deleted existing database file');\n }\n } catch (fsError) {\n console.error(\n 'Error deleting database file:',\n fsError instanceof Error\n ? fsError.message\n : 'Unknown error',\n );\n }\n\n this.#ensureDatabaseFileExists();\n\n this.#client = createClient({\n url: `file:${this.#dbPath}`,\n });\n\n debugLog(\n 'Reconnected to fresh database. Attempting initialization...',\n );\n await this.#initialize();\n return;\n } catch (recreateError) {\n console.error(\n 'Failed to recreate and initialize database:',\n recreateError instanceof Error\n ? recreateError.message\n : 'Unknown error',\n );\n throw new Error(\n `Database recreation failed after ${maxAttempts} attempts`,\n );\n }\n } else {\n try {\n if (this.#client) {\n try {\n await this.#client.close();\n } catch (e: unknown) {\n debugLog(\n 'Error closing client:',\n e instanceof Error ? e.message : 'Unknown error',\n );\n }\n }\n\n this.#ensureDatabaseFileExists();\n\n this.#client = createClient({\n url: `file:${this.#dbPath}`,\n });\n\n debugLog(\n 'Reconnected to database. Attempting initialization...',\n );\n await this.#initialize();\n return;\n } catch (reconnectError) {\n console.error(\n 'Failed to reconnect and initialize database:',\n reconnectError instanceof Error\n ? reconnectError.message\n : 'Unknown error',\n );\n }\n }\n } else {\n console.error(\n 'Unexpected database error, not related to missing tables:',\n error,\n );\n }\n } else {\n debugLog(\n 'Unknown error type detected during initialization:',\n typeof error,\n );\n try {\n await this.#initialize();\n return;\n } catch (initError) {\n console.error(\n 'Failed to initialize after unknown error:',\n initError instanceof Error ? initError.message : 'Unknown error',\n );\n }\n }\n }\n\n attempt++;\n\n if (attempt <= maxAttempts) {\n const delay = 500 * attempt;\n debugLog(`Waiting ${delay}ms before attempt ${attempt}...`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n\n throw new Error(\n `Failed to initialize database after ${maxAttempts} attempts`,\n );\n }\n\n async #initialize(): Promise<void> {\n let retryCount = 0;\n const maxRetries = 3;\n\n while (retryCount < maxRetries) {\n try {\n this.#ensureConfigDir();\n\n this.#ensureDatabaseFileExists();\n\n try {\n await this.#client.execute('PRAGMA quick_check;');\n debugLog('Database connection verified');\n } catch (connError: unknown) {\n console.error(\n 'Database connection test failed:',\n connError instanceof Error ? connError.message : 'Unknown error',\n );\n\n try {\n if (this.#client) {\n try {\n await this.#client.close();\n } catch {}\n }\n\n this.#client = createClient({\n url: `file:${this.#dbPath}`,\n });\n\n debugLog('Database client recreated');\n } catch (recreateError: unknown) {\n console.error(\n 'Failed to recreate database client:',\n recreateError instanceof Error\n ? recreateError.message\n : 'Unknown error',\n );\n throw new Error('Cannot connect to database');\n }\n }\n\n debugLog(\n `Creating database schema step by step (attempt ${retryCount + 1})...`,\n );\n\n try {\n await this.#client.execute('PRAGMA journal_mode = WAL;');\n } catch (error: unknown) {\n const pragmaError =\n error instanceof Error ? error.message : 'Unknown error';\n debugLog('Warning: Failed to set journal_mode pragma:', pragmaError);\n }\n\n try {\n await this.#client.execute('PRAGMA synchronous = NORMAL;');\n } catch (error: unknown) {\n const pragmaError =\n error instanceof Error ? error.message : 'Unknown error';\n debugLog('Warning: Failed to set synchronous pragma:', pragmaError);\n }\n\n try {\n await this.#client.execute('PRAGMA foreign_keys = ON;');\n } catch (error: unknown) {\n const pragmaError =\n error instanceof Error ? error.message : 'Unknown error';\n debugLog('Warning: Failed to set foreign_keys pragma:', pragmaError);\n }\n\n await this.#client.execute(`\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n profile TEXT NOT NULL,\n type TEXT NOT NULL DEFAULT 'ask',\n title TEXT,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'paused', 'pending', 'completed', 'failed')),\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n last_accessed_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n message_count INTEGER NOT NULL DEFAULT 0,\n query TEXT,\n files_processed TEXT,\n content_filtered INTEGER DEFAULT 0,\n conversation_context TEXT,\n max_messages INTEGER,\n is_private INTEGER DEFAULT 0,\n tags TEXT\n );\n `);\n\n debugLog('Created sessions table');\n\n await this.#client.execute(`\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'assistant', 'system')),\n content TEXT NOT NULL,\n timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n message_order INTEGER NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n temperature REAL,\n max_tokens INTEGER,\n processing_time REAL,\n token_usage TEXT,\n metadata TEXT,\n parent_message_id TEXT,\n is_edited INTEGER DEFAULT 0,\n edit_history TEXT,\n reactions TEXT,\n FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE\n );\n `);\n\n debugLog('Created messages table');\n\n await this.#client.execute(`\n CREATE TABLE IF NOT EXISTS act_templates (\n id TEXT PRIMARY KEY,\n label TEXT NOT NULL,\n category TEXT NOT NULL,\n description TEXT NOT NULL,\n template TEXT NOT NULL,\n is_built_in INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n `);\n\n debugLog('Created act_templates table');\n\n try {\n await this.#client.execute(`\n CREATE TRIGGER IF NOT EXISTS update_session_message_count_insert\n AFTER INSERT ON messages\n BEGIN\n UPDATE sessions \n SET message_count = (\n SELECT COUNT(*) FROM messages WHERE session_id = NEW.session_id\n ),\n updated_at = strftime('%s', 'now')\n WHERE id = NEW.session_id;\n END;\n `);\n\n await this.#client.execute(`\n CREATE TRIGGER IF NOT EXISTS update_session_message_count_delete\n AFTER DELETE ON messages\n BEGIN\n UPDATE sessions \n SET message_count = (\n SELECT COUNT(*) FROM messages WHERE session_id = OLD.session_id\n ),\n updated_at = strftime('%s', 'now')\n WHERE id = OLD.session_id;\n END;\n `);\n\n await this.#client.execute(`\n CREATE TRIGGER IF NOT EXISTS generate_session_title\n AFTER INSERT ON messages\n WHEN NEW.role = 'user' AND (\n SELECT title FROM sessions WHERE id = NEW.session_id\n ) IS NULL\n BEGIN\n UPDATE sessions \n SET title = CASE \n WHEN length(NEW.content) > ${DEFAULT_SESSION_TITLE_LENGTH} \n THEN substr(NEW.content, 1, ${DEFAULT_SESSION_TITLE_LENGTH}) || '...'\n ELSE NEW.content\n END\n WHERE id = NEW.session_id;\n END;\n `);\n\n debugLog('Created triggers');\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : 'Unknown error';\n debugLog('Warning: Failed to create triggers:', errorMessage);\n debugLog('Continuing with basic functionality');\n }\n\n try {\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_sessions_profile \n ON sessions (profile);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_sessions_created_at \n ON sessions (created_at DESC);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_sessions_type \n ON sessions (type);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_sessions_status \n ON sessions (status);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_messages_session_id \n ON messages (session_id);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_messages_timestamp \n ON messages (timestamp DESC);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_messages_provider \n ON messages (provider);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_messages_content_fts \n ON messages (content);\n `);\n\n await this.#client.execute(`\n CREATE INDEX IF NOT EXISTS idx_messages_order \n ON messages (session_id, message_order);\n `);\n\n debugLog('Created indexes');\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : 'Unknown error';\n debugLog('Warning: Failed to create indexes:', errorMessage);\n debugLog('Continuing with basic functionality');\n }\n\n const tableResult = await this.#client.execute(`\n SELECT count(*) as table_count FROM sqlite_master \n WHERE type='table' AND name IN ('sessions', 'messages');\n `);\n\n const tableCount = Number(tableResult.rows[0].table_count);\n\n if (tableCount !== 2) {\n throw new Error(`Expected 2 tables but found ${tableCount}`);\n }\n\n debugLog('Database schema initialized successfully');\n return;\n } catch (error) {\n console.error(\n `Error initializing database schema (attempt ${retryCount + 1}):`,\n error,\n );\n retryCount++;\n\n if (retryCount < maxRetries) {\n const delay = 500 * retryCount;\n debugLog(`Waiting ${delay}ms before retry...`);\n await new Promise((resolve) => setTimeout(resolve, delay));\n }\n }\n }\n\n throw new Error(\n `Failed to initialize database after ${maxRetries} attempts`,\n );\n }\n\n async createSession(\n profile: string,\n title?: string,\n type: 'ask' | 'chat' | 'exec' = 'ask',\n ): Promise<string> {\n await this.ensureDatabaseInitialized();\n\n const sessionId = uuidv4();\n\n await this.#client.execute({\n sql: 'INSERT INTO sessions (id, profile, title, type) VALUES (?, ?, ?, ?)',\n args: [sessionId, profile, title || null, type],\n });\n\n return sessionId;\n }\n\n async getSession(sessionId: string): Promise<ChatSession | null> {\n await this.ensureDatabaseInitialized();\n const result = await this.#client.execute({\n sql: 'SELECT * FROM sessions WHERE id = ?',\n args: [sessionId],\n });\n\n if (result.rows.length === 0) {\n return null;\n }\n\n const row = result.rows[0];\n return {\n id: row.id as string,\n profile: row.profile as string,\n title: (row.title as string) ?? undefined,\n createdAt: new Date(Number(row.created_at) * 1000).toISOString(),\n updatedAt: new Date(Number(row.updated_at) * 1000).toISOString(),\n messageCount: Number(row.message_count),\n };\n }\n\n async getSessions(options: ChatHistoryOptions = {}): Promise<ChatSession[]> {\n await this.ensureDatabaseInitialized();\n const {profile, limit = 10, fromDate, toDate, type} = options;\n\n let sql = `\n SELECT * FROM sessions\n WHERE 1=1\n `;\n const args: any[] = [];\n\n if (profile) {\n sql += ' AND profile = ?';\n args.push(profile);\n }\n\n if (type) {\n sql += ' AND type = ?';\n args.push(type);\n }\n\n if (fromDate) {\n sql += ' AND created_at >= ?';\n args.push(Math.floor(new Date(fromDate).getTime() / 1000));\n }\n\n if (toDate) {\n sql += ' AND created_at <= ?';\n args.push(Math.floor(new Date(toDate).getTime() / 1000));\n }\n\n sql += ' ORDER BY updated_at DESC LIMIT ?';\n args.push(limit);\n\n const result = await this.#client.execute({sql, args});\n\n return result.rows.map((row) => ({\n id: row.id as string,\n profile: row.profile as string,\n title: (row.title as string) ?? undefined,\n createdAt: new Date(Number(row.created_at) * 1000).toISOString(),\n updatedAt: new Date(Number(row.updated_at) * 1000).toISOString(),\n messageCount: Number(row.message_count),\n }));\n }\n\n async deleteSession(sessionId: string): Promise<boolean> {\n await this.ensureDatabaseInitialized();\n const result = await this.#client.execute({\n sql: 'DELETE FROM sessions WHERE id = ?',\n args: [sessionId],\n });\n\n return result.rowsAffected > 0;\n }\n\n async updateSessionTitle(sessionId: string, title: string): Promise<boolean> {\n const result = await this.#client.execute({\n sql: `UPDATE sessions SET title = ?, updated_at = strftime('%s', 'now') WHERE id = ?`,\n args: [title, sessionId],\n });\n\n return result.rowsAffected > 0;\n }\n\n async addMessage(\n sessionId: string,\n role: 'user' | 'assistant' | 'system',\n content: string,\n provider: string,\n model: string,\n temperature?: number,\n maxTokens?: number,\n metadata?: any,\n ): Promise<string> {\n await this.ensureDatabaseInitialized();\n const messageId = uuidv4();\n\n const orderResult = await this.#client.execute({\n sql: 'SELECT COALESCE(MAX(message_order), 0) + 1 as next_order FROM messages WHERE session_id = ?',\n args: [sessionId],\n });\n const messageOrder = Number(orderResult.rows[0].next_order);\n\n await this.#client.execute({\n sql: `INSERT INTO messages (\n id, session_id, role, content, provider, model, \n message_order, temperature, max_tokens, metadata\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n args: [\n messageId,\n sessionId,\n role,\n content,\n provider,\n model,\n messageOrder,\n temperature || null,\n maxTokens || null,\n metadata ? JSON.stringify(metadata) : null,\n ],\n });\n\n return messageId;\n }\n\n async getMessages(sessionId: string, limit?: number): Promise<ChatMessage[]> {\n await this.ensureDatabaseInitialized();\n let sql = `\n SELECT * FROM messages \n WHERE session_id = ? \n ORDER BY message_order ASC\n `;\n const args: any[] = [sessionId];\n\n if (limit) {\n sql += ' LIMIT ?';\n args.push(limit);\n }\n\n const result = await this.#client.execute({sql, args});\n\n return result.rows.map((row) => ({\n id: row.id as string,\n sessionId: row.session_id as string,\n role: row.role as 'user' | 'assistant' | 'system',\n content: row.content as string,\n timestamp: new Date(Number(row.timestamp) * 1000).toISOString(),\n provider: row.provider as SupportedProvider,\n model: row.model as string,\n temperature: row.temperature as number | undefined,\n maxTokens: row.max_tokens as number | undefined,\n metadata: row.metadata ? JSON.parse(row.metadata as string) : null,\n }));\n }\n\n async searchMessages(\n options: ChatHistoryOptions = {},\n ): Promise<ChatMessage[]> {\n await this.ensureDatabaseInitialized();\n const {\n profile,\n search,\n limit = 10,\n fromDate,\n toDate,\n provider,\n model,\n } = options;\n\n let sql = `\n SELECT m.* FROM messages m\n JOIN sessions s ON m.session_id = s.id\n WHERE 1=1\n `;\n const args: any[] = [];\n\n if (profile) {\n sql += ' AND s.profile = ?';\n args.push(profile);\n }\n\n if (search) {\n sql += ' AND m.content LIKE ?';\n args.push(`%${search}%`);\n }\n\n if (provider) {\n sql += ' AND m.provider = ?';\n args.push(provider);\n }\n\n if (model) {\n sql += ' AND m.model = ?';\n args.push(model);\n }\n\n if (fromDate) {\n sql += ' AND m.timestamp >= ?';\n args.push(Math.floor(new Date(fromDate).getTime() / 1000));\n }\n\n if (toDate) {\n sql += ' AND m.timestamp <= ?';\n args.push(Math.floor(new Date(toDate).getTime() / 1000));\n }\n\n sql += ' ORDER BY m.timestamp DESC LIMIT ?';\n args.push(limit);\n\n const result = await this.#client.execute({sql, args});\n\n return result.rows.map((row) => ({\n id: row.id as string,\n sessionId: row.session_id as string,\n role: row.role as 'user' | 'assistant' | 'system',\n content: row.content as string,\n timestamp: new Date(Number(row.timestamp) * 1000).toISOString(),\n provider: row.provider as SupportedProvider,\n model: row.model as string,\n temperature: row.temperature as number | undefined,\n maxTokens: row.max_tokens as number | undefined,\n metadata: row.metadata ? JSON.parse(row.metadata as string) : null,\n }));\n }\n\n async getStats(profile?: string): Promise<HistoryStats> {\n await this.ensureDatabaseInitialized();\n let sql = 'SELECT COUNT(*) as count FROM sessions';\n const args: any[] = [];\n\n if (profile) {\n sql += ' WHERE profile = ?';\n args.push(profile);\n }\n\n const sessionResult = await this.#client.execute({sql, args});\n const totalSessions = Number(sessionResult.rows[0].count);\n\n let messageSql = `\n SELECT \n COUNT(*) as total_count,\n SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END) as user_count,\n SUM(CASE WHEN role = 'assistant' THEN 1 ELSE 0 END) as assistant_count\n FROM messages\n `;\n const messageArgs: any[] = [];\n\n if (profile) {\n messageSql += `\n JOIN sessions ON messages.session_id = sessions.id\n WHERE sessions.profile = ?\n `;\n messageArgs.push(profile);\n }\n\n const messageResult = await this.#client.execute({\n sql: messageSql,\n args: messageArgs,\n });\n\n const totalMessages = Number(messageResult.rows[0].total_count);\n\n let providerSql = `\n SELECT provider, COUNT(*) as count\n FROM messages\n `;\n const providerArgs: any[] = [];\n\n if (profile) {\n providerSql += `\n JOIN sessions ON messages.session_id = sessions.id\n WHERE sessions.profile = ?\n `;\n providerArgs.push(profile);\n }\n\n providerSql += ' GROUP BY provider ORDER BY count DESC';\n\n const providerResult = await this.#client.execute({\n sql: providerSql,\n args: providerArgs,\n });\n\n const messagesByProvider: Record<string, number> = {};\n providerResult.rows.forEach((row) => {\n messagesByProvider[row.provider as string] = Number(row.count);\n });\n\n let modelSql = `\n SELECT model, COUNT(*) as count\n FROM messages\n `;\n const modelArgs: any[] = [];\n\n if (profile) {\n modelSql += `\n JOIN sessions ON messages.session_id = sessions.id\n WHERE sessions.profile = ?\n `;\n modelArgs.push(profile);\n }\n\n modelSql += ' GROUP BY model ORDER BY count DESC';\n\n const modelResult = await this.#client.execute({\n sql: modelSql,\n args: modelArgs,\n });\n\n const messagesByModel: Record<string, number> = {};\n modelResult.rows.forEach((row) => {\n messagesByModel[row.model as string] = Number(row.count);\n });\n\n const profileSql = `\n SELECT sessions.profile, COUNT(*) as count\n FROM messages\n JOIN sessions ON messages.session_id = sessions.id\n GROUP BY sessions.profile\n ORDER BY count DESC\n `;\n\n const profileResult = await this.#client.execute({sql: profileSql});\n\n const messagesByProfile: Record<string, number> = {};\n profileResult.rows.forEach((row) => {\n messagesByProfile[row.profile as string] = Number(row.count);\n });\n\n let timeRangeSql = `\n SELECT \n MIN(timestamp) as oldest,\n MAX(timestamp) as newest\n FROM messages\n `;\n const timeRangeArgs: any[] = [];\n\n if (profile) {\n timeRangeSql += `\n JOIN sessions ON messages.session_id = sessions.id\n WHERE sessions.profile = ?\n `;\n timeRangeArgs.push(profile);\n }\n\n const timeRangeResult = await this.#client.execute({\n sql: timeRangeSql,\n args: timeRangeArgs,\n });\n\n return {\n totalSessions,\n totalMessages,\n messagesByProvider,\n messagesByModel,\n messagesByProfile,\n oldestMessage: timeRangeResult.rows[0].oldest\n ? new Date(Number(timeRangeResult.rows[0].oldest) * 1000).toISOString()\n : undefined,\n newestMessage: timeRangeResult.rows[0].newest\n ? new Date(Number(timeRangeResult.rows[0].newest) * 1000).toISOString()\n : undefined,\n };\n }\n\n async deleteOldSessions(profile: string, days: number): Promise<number> {\n await this.ensureDatabaseInitialized();\n const date = new Date();\n date.setDate(date.getDate() - days);\n const cutoffTimestamp = Math.floor(date.getTime() / 1000);\n\n const result = await this.#client.execute({\n sql: `\n DELETE FROM sessions \n WHERE profile = ? AND created_at < ?\n `,\n args: [profile, cutoffTimestamp],\n });\n\n return result.rowsAffected;\n }\n\n async vacuum(): Promise<void> {\n await this.ensureDatabaseInitialized();\n await this.#client.execute({sql: 'VACUUM'});\n }\n\n async exportChatHistory(options: ChatHistoryOptions = {}): Promise<{\n sessions: ChatSession[];\n messages: Record<string, ChatMessage[]>;\n stats: HistoryStats;\n }> {\n await this.ensureDatabaseInitialized();\n const {profile} = options;\n\n const sessions = await this.getSessions({\n profile,\n limit: 1000,\n });\n\n const messages: Record<string, ChatMessage[]> = {};\n for (const session of sessions) {\n messages[session.id] = await this.getMessages(session.id);\n }\n\n const stats = await this.getStats(profile);\n\n return {\n sessions,\n messages,\n stats,\n };\n }\n\n private async ensureActTemplatesTable(): Promise<void> {\n await this.ensureDatabaseInitialized();\n\n await this.#client.execute(`\n CREATE TABLE IF NOT EXISTS act_templates (\n id TEXT PRIMARY KEY,\n label TEXT NOT NULL,\n category TEXT NOT NULL,\n description TEXT NOT NULL,\n template TEXT NOT NULL,\n is_built_in INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n );\n `);\n }\n\n async createActTemplate(template: {\n id: string;\n label: string;\n category: string;\n description: string;\n template: string;\n }): Promise<void> {\n await this.ensureActTemplatesTable();\n\n const now = Math.floor(Date.now() / 1000);\n await this.#client.execute(\n `INSERT INTO act_templates (id, label, category, description, template, is_built_in, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n template.id,\n template.label,\n template.category,\n template.description,\n template.template,\n 0,\n now,\n now,\n ],\n );\n }\n\n async updateActTemplate(\n id: string,\n updates: Partial<{\n label: string;\n category: string;\n description: string;\n template: string;\n }>,\n ): Promise<boolean> {\n await this.ensureActTemplatesTable();\n\n const setClause = [];\n const values = [];\n\n if (updates.label !== undefined) {\n setClause.push('label = ?');\n values.push(updates.label);\n }\n if (updates.category !== undefined) {\n setClause.push('category = ?');\n values.push(updates.category);\n }\n if (updates.description !== undefined) {\n setClause.push('description = ?');\n values.push(updates.description);\n }\n if (updates.template !== undefined) {\n setClause.push('template = ?');\n values.push(updates.template);\n }\n\n if (setClause.length === 0) {\n return false;\n }\n\n setClause.push('updated_at = ?');\n values.push(Math.floor(Date.now() / 1000));\n values.push(id);\n const result = await this.#client.execute(\n `UPDATE act_templates SET ${setClause.join(', ')} \n WHERE id = ? AND is_built_in = 0`,\n values,\n );\n\n return result.rowsAffected > 0;\n }\n\n async deleteActTemplate(id: string): Promise<boolean> {\n await this.ensureActTemplatesTable();\n const result = await this.#client.execute(\n 'DELETE FROM act_templates WHERE id = ? AND is_built_in = 0',\n [id],\n );\n\n return result.rowsAffected > 0;\n }\n\n async getActTemplate(id: string): Promise<any | null> {\n await this.ensureActTemplatesTable();\n const result = await this.#client.execute(\n 'SELECT * FROM act_templates WHERE id = ? LIMIT 1',\n [id],\n );\n\n return result.rows.length > 0 ? result.rows[0] : null;\n }\n\n async listActTemplates(customOnly = false): Promise<any[]> {\n await this.ensureActTemplatesTable();\n const whereClause = customOnly ? 'WHERE is_built_in = 0' : '';\n\n const result = await this.#client.execute(\n `SELECT * FROM act_templates ${whereClause} ORDER BY created_at DESC`,\n [],\n );\n\n return result.rows;\n }\n\n async templateExists(id: string): Promise<boolean> {\n await this.ensureActTemplatesTable();\n const result = await this.#client.execute(\n 'SELECT 1 FROM act_templates WHERE id = ? LIMIT 1',\n [id],\n );\n\n return result.rows.length > 0;\n }\n\n async close(): Promise<void> {\n await this.#client.close();\n }\n}\n"]}