UNPKG

focus-productivity-cli

Version:

An ADHD-friendly productivity CLI tool built to run inside Warp terminal

370 lines (328 loc) 11.2 kB
const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const os = require('os'); class Gamification { constructor() { this.dbPath = path.join(os.homedir(), '.focuscli', 'focus.db'); this.achievements = this.getAchievements(); } getAchievements() { return [ { id: 'first_task', name: 'Getting Started', description: 'Complete your first task', icon: '🌱', condition: 'tasks_completed >= 1', xp_reward: 50 }, { id: 'task_streak_3', name: 'On a Roll', description: 'Complete 3 tasks in a row', icon: '🔥', condition: 'tasks_completed >= 3', xp_reward: 100 }, { id: 'task_streak_10', name: 'Productivity Machine', description: 'Complete 10 tasks', icon: '⚡', condition: 'tasks_completed >= 10', xp_reward: 250 }, { id: 'focus_master', name: 'Focus Master', description: 'Complete a 25+ minute focus session', icon: '🧠', condition: 'long_focus_session', xp_reward: 150 }, { id: 'daily_warrior', name: 'Daily Warrior', description: 'Complete at least one task every day for 3 days', icon: '🏆', condition: 'daily_streak >= 3', xp_reward: 200 }, { id: 'centurion', name: 'Centurion', description: 'Earn 1000 total XP', icon: '💎', condition: 'total_xp >= 1000', xp_reward: 500 }, { id: 'night_owl', name: 'Night Owl', description: 'Complete a task after 9 PM', icon: '🦉', condition: 'late_night_task', xp_reward: 75 }, { id: 'early_bird', name: 'Early Bird', description: 'Complete a task before 7 AM', icon: '🐦', condition: 'early_morning_task', xp_reward: 75 } ]; } async awardXP(amount) { return new Promise((resolve, reject) => { const db = new sqlite3.Database(this.dbPath); db.serialize(() => { // Update XP and check for level up db.run(` UPDATE user_stats SET total_xp = total_xp + ?, last_activity_date = date('now') WHERE id = 1 `, [amount], function(err) { if (err) { reject(err); return; } // Get updated stats to calculate level db.get('SELECT total_xp FROM user_stats WHERE id = 1', (err, row) => { if (err) { reject(err); return; } const newLevel = Math.floor(row.total_xp / 100) + 1; // 100 XP per level // Update level if necessary db.run('UPDATE user_stats SET level = ? WHERE id = 1', [newLevel], (err) => { db.close(); if (err) { reject(err); return; } resolve(amount); }); }); }); }); }); } async updateStreak() { return new Promise((resolve, reject) => { const db = new sqlite3.Database(this.dbPath); db.serialize(() => { db.get('SELECT current_streak, best_streak, last_activity_date FROM user_stats WHERE id = 1', (err, row) => { if (err) { reject(err); return; } const today = new Date().toISOString().split('T')[0]; const lastActivity = row.last_activity_date; let newStreak = row.current_streak; let newBestStreak = row.best_streak; if (lastActivity !== today) { // Check if it's consecutive days const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]; if (lastActivity === yesterday) { // Continue streak newStreak = row.current_streak + 1; } else if (lastActivity !== today) { // Reset streak (missed a day) newStreak = 1; } // Update best streak if necessary if (newStreak > newBestStreak) { newBestStreak = newStreak; } db.run(` UPDATE user_stats SET current_streak = ?, best_streak = ?, last_activity_date = ? WHERE id = 1 `, [newStreak, newBestStreak, today], function(err) { db.close(); if (err) { reject(err); return; } resolve({ currentStreak: newStreak, bestStreak: newBestStreak }); }); } else { db.close(); resolve({ currentStreak: newStreak, bestStreak: newBestStreak }); } }); }); }); } async checkAchievements() { return new Promise((resolve, reject) => { const db = new sqlite3.Database(this.dbPath); db.serialize(() => { // Get current stats db.all(` SELECT us.total_xp, us.current_streak, us.best_streak, us.level, COUNT(t.id) as tasks_completed, SUM(CASE WHEN t.completed = TRUE AND date(t.completed_at) = date('now') THEN 1 ELSE 0 END) as tasks_completed_today, MAX(fs.duration) as longest_focus_session FROM user_stats us LEFT JOIN tasks t ON t.completed = TRUE LEFT JOIN focus_sessions fs ON fs.completed = TRUE WHERE us.id = 1 `, (err, rows) => { if (err) { reject(err); return; } const stats = rows[0]; const newAchievements = []; // Check each achievement this.achievements.forEach(achievement => { const condition = this.evaluateAchievementCondition(achievement, stats); if (condition) { // Check if already unlocked db.get(` SELECT id FROM achievements WHERE achievement_id = ? `, [achievement.id], (err, existingAchievement) => { if (err) { // Table might not exist yet, create it db.run(` CREATE TABLE IF NOT EXISTS achievements ( id INTEGER PRIMARY KEY AUTOINCREMENT, achievement_id TEXT UNIQUE, unlocked_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `, () => { // Try again this.unlockAchievement(achievement, newAchievements); }); } else if (!existingAchievement) { this.unlockAchievement(achievement, newAchievements); } }); } }); // Wait a bit for all async operations to complete setTimeout(() => { db.close(); resolve(newAchievements); }, 100); }); }); }); } unlockAchievement(achievement, newAchievements) { const db = new sqlite3.Database(this.dbPath); db.run(` INSERT OR IGNORE INTO achievements (achievement_id) VALUES (?) `, [achievement.id], function(err) { if (err) return; if (this.changes > 0) { newAchievements.push(achievement); // Award XP for the achievement db.run('UPDATE user_stats SET total_xp = total_xp + ? WHERE id = 1', [achievement.xp_reward]); } db.close(); }); } evaluateAchievementCondition(achievement, stats) { switch (achievement.condition) { case 'tasks_completed >= 1': return stats.tasks_completed >= 1; case 'tasks_completed >= 3': return stats.tasks_completed >= 3; case 'tasks_completed >= 10': return stats.tasks_completed >= 10; case 'total_xp >= 1000': return stats.total_xp >= 1000; case 'daily_streak >= 3': return stats.current_streak >= 3; case 'long_focus_session': return stats.longest_focus_session >= 25; default: return false; } } async getUserStats() { return new Promise((resolve, reject) => { const db = new sqlite3.Database(this.dbPath); db.all(` SELECT us.*, COUNT(t.id) as total_tasks, SUM(CASE WHEN t.completed = TRUE THEN 1 ELSE 0 END) as completed_tasks, SUM(CASE WHEN t.completed = TRUE AND date(t.completed_at) = date('now') THEN 1 ELSE 0 END) as completed_today, COUNT(fs.id) as total_focus_sessions, SUM(fs.duration) as total_focus_time FROM user_stats us LEFT JOIN tasks t ON 1=1 LEFT JOIN focus_sessions fs ON fs.completed = TRUE WHERE us.id = 1 GROUP BY us.id `, (err, rows) => { db.close(); if (err) { reject(err); return; } resolve(rows[0] || { total_xp: 0, current_streak: 0, best_streak: 0, level: 1, total_tasks: 0, completed_tasks: 0, completed_today: 0, total_focus_sessions: 0, total_focus_time: 0 }); }); }); } async getUnlockedAchievements() { return new Promise((resolve, reject) => { const db = new sqlite3.Database(this.dbPath); db.all(` SELECT a.achievement_id, a.unlocked_at FROM achievements a ORDER BY a.unlocked_at DESC `, (err, rows) => { db.close(); if (err) { resolve([]); // Return empty array if table doesn't exist yet return; } const unlockedAchievements = rows.map(row => { const achievement = this.achievements.find(a => a.id === row.achievement_id); return { ...achievement, unlockedAt: row.unlocked_at }; }).filter(a => a !== undefined); resolve(unlockedAchievements); }); }); } getLevelProgress(xp) { const currentLevel = Math.floor(xp / 100) + 1; const xpInCurrentLevel = xp % 100; const xpForNextLevel = 100 - xpInCurrentLevel; return { currentLevel, xpInCurrentLevel, xpForNextLevel, progressPercent: Math.floor((xpInCurrentLevel / 100) * 100) }; } } module.exports = Gamification;