UNPKG

virtue-tracker

Version:

Personal character development CLI tool for tracking virtues and philosophical alignment

227 lines 8.47 kB
import SQLite from 'better-sqlite3'; import { readFileSync } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; // Get current directory in a way that works with both Node.js and Jest const currentDir = process.cwd(); export class Database { db; constructor(dbPath = './virtue-tracker-data.db') { this.db = new SQLite(dbPath); this.db.pragma('journal_mode = WAL'); this.db.pragma('foreign_keys = ON'); this.initialize(); } initialize() { // Try multiple paths for schema.sql to handle different execution contexts let schemaPath = join(currentDir, 'src', 'database', 'schema.sql'); try { const schema = readFileSync(schemaPath, 'utf-8'); this.db.exec(schema); } catch { // Fallback for production/built environment try { schemaPath = join(currentDir, 'dist', 'database', 'schema.sql'); const schema = readFileSync(schemaPath, 'utf-8'); this.db.exec(schema); } catch (fallbackError) { throw new Error(`Could not find schema.sql at ${schemaPath}: ${fallbackError}`); } } } // Profile methods async createProfile(profile) { const id = randomUUID(); const stmt = this.db.prepare(` INSERT INTO profiles (id, name, philosophical_template, ascii_theme, color_scheme) VALUES (?, ?, ?, ?, ?) `); stmt.run(id, profile.name, profile.philosophical_template, profile.ascii_theme, profile.color_scheme); return this.getProfile(id); } getProfile(id) { const stmt = this.db.prepare('SELECT * FROM profiles WHERE id = ?'); return stmt.get(id); } getAllProfiles() { const stmt = this.db.prepare('SELECT * FROM profiles ORDER BY created_at DESC'); return stmt.all(); } hasProfiles() { const stmt = this.db.prepare('SELECT COUNT(*) as count FROM profiles'); const result = stmt.get(); return result.count > 0; } // Virtue methods async createVirtue(virtue) { const id = randomUUID(); const stmt = this.db.prepare(` INSERT INTO virtues (id, profile_id, name, definition, priority_weight, template_source, emoji) VALUES (?, ?, ?, ?, ?, ?, ?) `); stmt.run(id, virtue.profile_id, virtue.name, virtue.definition, virtue.priority_weight, virtue.template_source, virtue.emoji); return this.getVirtue(id); } getVirtue(id) { const stmt = this.db.prepare('SELECT * FROM virtues WHERE id = ?'); return stmt.get(id); } getVirtuesForProfile(profileId) { const stmt = this.db.prepare(` SELECT * FROM virtues WHERE profile_id = ? ORDER BY priority_weight DESC, created_at ASC `); return stmt.all(profileId); } // Daily entry methods async createDailyEntry(entry) { const id = randomUUID(); const stmt = this.db.prepare(` INSERT INTO daily_entries (id, profile_id, date, virtue_scores, notes, coherence_score, completion_time_seconds) VALUES (?, ?, ?, ?, ?, ?, ?) `); stmt.run(id, entry.profile_id, entry.date, entry.virtue_scores, entry.notes, entry.coherence_score, entry.completion_time_seconds); return this.getDailyEntry(id); } getDailyEntry(id) { const stmt = this.db.prepare('SELECT * FROM daily_entries WHERE id = ?'); return stmt.get(id); } getDailyEntryByDate(profileId, date) { const stmt = this.db.prepare('SELECT * FROM daily_entries WHERE profile_id = ? AND date = ?'); return stmt.get(profileId, date); } getEntriesForProfile(profileId, cutoffDateOrDays) { let query = 'SELECT * FROM daily_entries WHERE profile_id = ?'; const params = [profileId]; if (cutoffDateOrDays instanceof Date) { query += ' AND date >= ?'; params.push(cutoffDateOrDays.toISOString().split('T')[0]); query += ' ORDER BY date DESC'; } else if (typeof cutoffDateOrDays === 'number') { query += ' ORDER BY date DESC LIMIT ?'; params.push(cutoffDateOrDays); } else { query += ' ORDER BY date DESC'; } const stmt = this.db.prepare(query); const entries = stmt.all(...params); // Parse virtue_scores JSON for each entry return entries.map(entry => ({ ...entry, virtue_scores: typeof entry.virtue_scores === 'string' ? JSON.parse(entry.virtue_scores) : entry.virtue_scores })); } getLatestEntry(profileId) { const stmt = this.db.prepare(` SELECT * FROM daily_entries WHERE profile_id = ? ORDER BY date DESC LIMIT 1 `); return stmt.get(profileId); } // Session methods async createSession(session) { const id = randomUUID(); const stmt = this.db.prepare(` INSERT INTO sessions (id, profile_id, session_type, duration_seconds) VALUES (?, ?, ?, ?) `); stmt.run(id, session.profile_id, session.session_type, session.duration_seconds); return this.getSession(id); } getSession(id) { const stmt = this.db.prepare('SELECT * FROM sessions WHERE id = ?'); return stmt.get(id); } // Monthly deep-dive methods async createMonthlyDeepDive(deepDive) { const id = randomUUID(); const stmt = this.db.prepare(` INSERT INTO monthly_deep_dives (id, profile_id, month, analysis_data, intervention_plan) VALUES (?, ?, ?, ?, ?) `); stmt.run(id, deepDive.profile_id, deepDive.month, deepDive.analysis_data, deepDive.intervention_plan); return this.getMonthlyDeepDiveById(id); } getMonthlyDeepDiveById(id) { const stmt = this.db.prepare('SELECT * FROM monthly_deep_dives WHERE id = ?'); return stmt.get(id); } getMonthlyDeepDiveByMonth(profileId, month) { const stmt = this.db.prepare('SELECT * FROM monthly_deep_dives WHERE profile_id = ? AND month = ?'); return stmt.get(profileId, month); } // Streak calculation calculateCurrentStreak(profileId) { const entries = this.db.prepare(` SELECT date FROM daily_entries WHERE profile_id = ? ORDER BY date DESC `).all(profileId); if (entries.length === 0) return 0; let streak = 1; const today = new Date(); const lastDate = new Date(entries[0].date); // Check if last entry was today or yesterday const dayDiff = Math.floor((today.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24)); if (dayDiff > 1) return 0; // Count consecutive days for (let i = 1; i < entries.length; i++) { const currentDate = new Date(entries[i].date); const previousDate = new Date(entries[i - 1].date); const diff = Math.floor((previousDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24)); if (diff === 1) { streak++; } else { break; } } return streak; } // Average coherence calculation calculateAverageCoherence(profileId, days = 30) { const stmt = this.db.prepare(` SELECT AVG(coherence_score) as avg_coherence FROM daily_entries WHERE profile_id = ? ORDER BY date DESC LIMIT ? `); const result = stmt.get(profileId, days); return result.avg_coherence || 0; } // Additional monthly deep dive methods createMonthlyDeepDiveSimple(profileId, month, analysisData, interventionPlan) { const id = randomUUID(); const stmt = this.db.prepare(` INSERT INTO monthly_deep_dives (id, profile_id, month, analysis_data, intervention_plan, created_at) VALUES (?, ?, ?, ?, ?, datetime('now')) `); stmt.run(id, profileId, month, analysisData, interventionPlan); return id; } getAllMonthlyDeepDives(profileId) { const stmt = this.db.prepare(` SELECT * FROM monthly_deep_dives WHERE profile_id = ? ORDER BY month DESC `); return stmt.all(profileId); } close() { this.db.close(); } } //# sourceMappingURL=Database.js.map