focus-productivity-cli
Version:
An ADHD-friendly productivity CLI tool built to run inside Warp terminal
370 lines (328 loc) • 11.2 kB
JavaScript
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;