virtue-tracker
Version:
Personal character development CLI tool for tracking virtues and philosophical alignment
227 lines • 8.47 kB
JavaScript
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