import type { ExtensionAPI, ExtensionContext } from "@jaeswift/jae-coding-agent"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; interface Stats { sessionsCount: number; totalPrompts: number; filesCreated: number; filesEdited: number; linesWritten: number; toolsUsed: number; commandsRun: number; bugsFixed: number; timeSpentMinutes: number; fileTypesEdited: Set; usedSwarm: boolean; usedAllVeniceSkills: boolean; longestSessionMinutes: number; shortestSessionMinutes: number; currentSessionStart: number; nightOwlSessions: number; } interface Achievement { id: string; name: string; description: string; icon: string; check: (stats: Stats) => boolean; } interface StatsData { sessionsCount: number; totalPrompts: number; filesCreated: number; filesEdited: number; linesWritten: number; toolsUsed: number; commandsRun: number; bugsFixed: number; timeSpentMinutes: number; fileTypesEdited: string[]; usedSwarm: boolean; usedAllVeniceSkills: boolean; longestSessionMinutes: number; shortestSessionMinutes: number; nightOwlSessions: number; unlockedAchievements: string[]; personalBests: Record; } export default function (pi: ExtensionAPI) { const configDir = join(process.env.HOME || "/root", ".jae"); const statsPath = join(configDir, "stats.json"); const VENICE_SKILLS = [ "venice-chat", "venice-image-gen", "venice-tts", "venice-video-generate", "venice-list-text-models", "venice-list-image-models", "venice-list-video-models", "venice-chat-benchmark", "venice-video-queue", "venice-video-quote", "venice-video-retrieve", ]; const veniceSkillsUsed = new Set(); const stats: Stats = { sessionsCount: 0, totalPrompts: 0, filesCreated: 0, filesEdited: 0, linesWritten: 0, toolsUsed: 0, commandsRun: 0, bugsFixed: 0, timeSpentMinutes: 0, fileTypesEdited: new Set(), usedSwarm: false, usedAllVeniceSkills: false, longestSessionMinutes: 0, shortestSessionMinutes: Infinity, currentSessionStart: Date.now(), nightOwlSessions: 0, }; let unlockedAchievements = new Set(); let personalBests: Record = {}; const ACHIEVEMENTS: Achievement[] = [ { id: "first_prompt", name: "First Prompt", description: "Sent your first prompt to JAE", icon: "\u{1F476}", check: (s) => s.totalPrompts >= 1, }, { id: "centurion", name: "100 Prompts", description: "Sent 100 prompts total", icon: "\u{1F4AF}", check: (s) => s.totalPrompts >= 100, }, { id: "file_creator", name: "File Creator", description: "Created 10 files", icon: "\u{1F4C4}", check: (s) => s.filesCreated >= 10, }, { id: "bug_squasher", name: "Bug Squasher", description: "Fixed 10 bugs", icon: "\u{1F41B}", check: (s) => s.bugsFixed >= 10, }, { id: "night_owl", name: "Night Owl", description: "Coded after midnight", icon: "\u{1F989}", check: (s) => s.nightOwlSessions >= 1, }, { id: "speed_demon", name: "Speed Demon", description: "Completed a session in under 5 minutes", icon: "\u26A1", check: (s) => s.shortestSessionMinutes > 0 && s.shortestSessionMinutes < 5, }, { id: "marathon", name: "Marathon", description: "Session lasting over 2 hours", icon: "\u{1F3C3}", check: (s) => s.longestSessionMinutes >= 120, }, { id: "swarm_master", name: "Swarm Master", description: "Used swarm mode", icon: "\u{1F41D}", check: (s) => s.usedSwarm, }, { id: "dragon_rider", name: "Dragon Rider", description: "Used all Venice AI skills", icon: "\u{1F409}", check: (s) => s.usedAllVeniceSkills, }, { id: "full_stack", name: "Full Stack", description: "Edited 5+ different file types", icon: "\u{1F4DA}", check: (s) => s.fileTypesEdited.size >= 5, }, { id: "tool_master", name: "Tool Master", description: "Used 50 tools in a session", icon: "\u{1F6E0}\uFE0F", check: (s) => s.toolsUsed >= 50, }, { id: "prolific_writer", name: "Prolific Writer", description: "Written 1000+ lines of code", icon: "\u270D\uFE0F", check: (s) => s.linesWritten >= 1000, }, ]; function loadStats(): void { try { if (existsSync(statsPath)) { const data: StatsData = JSON.parse(readFileSync(statsPath, "utf-8")); stats.sessionsCount = data.sessionsCount || 0; stats.totalPrompts = data.totalPrompts || 0; stats.filesCreated = data.filesCreated || 0; stats.filesEdited = data.filesEdited || 0; stats.linesWritten = data.linesWritten || 0; stats.toolsUsed = data.toolsUsed || 0; stats.commandsRun = data.commandsRun || 0; stats.bugsFixed = data.bugsFixed || 0; stats.timeSpentMinutes = data.timeSpentMinutes || 0; stats.fileTypesEdited = new Set(data.fileTypesEdited || []); stats.usedSwarm = data.usedSwarm || false; stats.usedAllVeniceSkills = data.usedAllVeniceSkills || false; stats.longestSessionMinutes = data.longestSessionMinutes || 0; stats.shortestSessionMinutes = data.shortestSessionMinutes || Infinity; stats.nightOwlSessions = data.nightOwlSessions || 0; unlockedAchievements = new Set(data.unlockedAchievements || []); personalBests = data.personalBests || {}; } } catch { /* ignore */ } } function saveStats(): void { try { if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true }); const data: StatsData = { sessionsCount: stats.sessionsCount, totalPrompts: stats.totalPrompts, filesCreated: stats.filesCreated, filesEdited: stats.filesEdited, linesWritten: stats.linesWritten, toolsUsed: stats.toolsUsed, commandsRun: stats.commandsRun, bugsFixed: stats.bugsFixed, timeSpentMinutes: stats.timeSpentMinutes, fileTypesEdited: Array.from(stats.fileTypesEdited), usedSwarm: stats.usedSwarm, usedAllVeniceSkills: stats.usedAllVeniceSkills, longestSessionMinutes: stats.longestSessionMinutes, shortestSessionMinutes: stats.shortestSessionMinutes === Infinity ? 0 : stats.shortestSessionMinutes, nightOwlSessions: stats.nightOwlSessions, unlockedAchievements: Array.from(unlockedAchievements), personalBests, }; writeFileSync(statsPath, JSON.stringify(data, null, 2), "utf-8"); } catch { /* ignore write errors */ } } function checkAchievements(ctx: ExtensionContext): void { for (const achievement of ACHIEVEMENTS) { if (!unlockedAchievements.has(achievement.id) && achievement.check(stats)) { unlockedAchievements.add(achievement.id); ctx.ui.notify( `${achievement.icon} Achievement Unlocked!\n\n` + ` ${achievement.name}\n` + ` ${achievement.description}\n\n` + `Total: ${unlockedAchievements.size}/${ACHIEVEMENTS.length} achievements`, "info", ); saveStats(); } } } function updatePersonalBests(): void { const sessionMinutes = Math.floor((Date.now() - stats.currentSessionStart) / 60000); if (sessionMinutes > (personalBests.longestSession || 0)) { personalBests.longestSession = sessionMinutes; } if (stats.totalPrompts > (personalBests.mostPrompts || 0)) { personalBests.mostPrompts = stats.totalPrompts; } if (stats.filesCreated > (personalBests.mostFilesCreated || 0)) { personalBests.mostFilesCreated = stats.filesCreated; } if (stats.toolsUsed > (personalBests.mostToolsUsed || 0)) { personalBests.mostToolsUsed = stats.toolsUsed; } if (stats.linesWritten > (personalBests.mostLinesWritten || 0)) { personalBests.mostLinesWritten = stats.linesWritten; } } function getFileExtension(filepath: string): string { const parts = filepath.split("."); return parts.length > 1 ? "." + parts[parts.length - 1] : ""; } function renderStatsDashboard(): string { const sessionMinutes = Math.floor((Date.now() - stats.currentSessionStart) / 60000); const totalHours = (stats.timeSpentMinutes / 60).toFixed(1); const bar = '\u2588'; const dim = '\u2591'; // Progress bars const promptProgress = Math.min(stats.totalPrompts / 100, 1); const promptBar = bar.repeat(Math.floor(promptProgress * 20)) + dim.repeat(20 - Math.floor(promptProgress * 20)); const fileProgress = Math.min(stats.filesCreated / 10, 1); const fileBar = bar.repeat(Math.floor(fileProgress * 20)) + dim.repeat(20 - Math.floor(fileProgress * 20)); const bugProgress = Math.min(stats.bugsFixed / 10, 1); const bugBar = bar.repeat(Math.floor(bugProgress * 20)) + dim.repeat(20 - Math.floor(bugProgress * 20)); const achievementProgress = unlockedAchievements.size / ACHIEVEMENTS.length; const achieveBar = bar.repeat(Math.floor(achievementProgress * 20)) + dim.repeat(20 - Math.floor(achievementProgress * 20)); const lines = [ `\u{250C}${'\u2500'.repeat(50)}\u{2510}`, `\u{2502} \u{1F4CA} JAE Coding Statistics Dashboard${' '.repeat(16)}\u{2502}`, `\u{251C}${'\u2500'.repeat(50)}\u{2524}`, `\u{2502} \u{2502}`, `\u{2502} \u{1F3AE} Sessions: ${String(stats.sessionsCount).padEnd(8)} \u23F1\uFE0F This: ${sessionMinutes}m${' '.repeat(Math.max(0, 12 - String(sessionMinutes).length))}\u{2502}`, `\u{2502} \u{1F4AC} Prompts: ${String(stats.totalPrompts).padEnd(8)} \u{1F4DD} Lines: ${String(stats.linesWritten).padEnd(10)}\u{2502}`, `\u{2502} \u{1F4C4} Created: ${String(stats.filesCreated).padEnd(8)} \u270F\uFE0F Edited: ${String(stats.filesEdited).padEnd(9)}\u{2502}`, `\u{2502} \u{1F6E0}\uFE0F Tools: ${String(stats.toolsUsed).padEnd(8)} \u{1F41B} Fixes: ${String(stats.bugsFixed).padEnd(10)}\u{2502}`, `\u{2502} \u23F0 Total time: ${totalHours}h${' '.repeat(Math.max(0, 35 - totalHours.length))}\u{2502}`, `\u{2502} \u{2502}`, `\u{251C}${'\u2500'.repeat(50)}\u{2524}`, `\u{2502} Progress to Achievements:${' '.repeat(24)}\u{2502}`, `\u{2502} Prompts [${promptBar}] ${stats.totalPrompts}/100${' '.repeat(Math.max(0, 6 - String(stats.totalPrompts).length))}\u{2502}`, `\u{2502} Files [${fileBar}] ${stats.filesCreated}/10${' '.repeat(Math.max(0, 7 - String(stats.filesCreated).length))}\u{2502}`, `\u{2502} Bugs [${bugBar}] ${stats.bugsFixed}/10${' '.repeat(Math.max(0, 7 - String(stats.bugsFixed).length))}\u{2502}`, `\u{2502} Achieve [${achieveBar}] ${unlockedAchievements.size}/${ACHIEVEMENTS.length}${' '.repeat(Math.max(0, 7 - String(unlockedAchievements.size).length))}\u{2502}`, `\u{2502} \u{2502}`, `\u{2502} \u{1F4C2} File types: ${Array.from(stats.fileTypesEdited).slice(0, 8).join(", ") || "none"}${' '.repeat(Math.max(0, 5))}\u{2502}`, `\u{2514}${'\u2500'.repeat(50)}\u{2518}`, ]; return lines.join("\n"); } function renderAchievements(): string { const lines: string[] = [ `\u{1F3C6} Achievements (${unlockedAchievements.size}/${ACHIEVEMENTS.length})`, `${'\u2500'.repeat(50)}`, "", ]; for (const a of ACHIEVEMENTS) { const unlocked = unlockedAchievements.has(a.id); const status = unlocked ? "\u2705" : "\u{1F512}"; const icon = unlocked ? a.icon : "\u2753"; lines.push(` ${status} ${icon} ${a.name}`); lines.push(` ${unlocked ? a.description : "????"}`); lines.push(""); } return lines.join("\n"); } function renderLeaderboard(): string { const lines: string[] = [ `\u{1F3C5} Personal Bests`, `${'\u2500'.repeat(40)}`, "", ]; const bests: [string, string, number][] = [ ["\u23F1\uFE0F", "Longest Session", personalBests.longestSession || 0], ["\u{1F4AC}", "Most Prompts", personalBests.mostPrompts || 0], ["\u{1F4C4}", "Most Files Created", personalBests.mostFilesCreated || 0], ["\u{1F6E0}\uFE0F", "Most Tools Used", personalBests.mostToolsUsed || 0], ["\u{1F4DD}", "Most Lines Written", personalBests.mostLinesWritten || 0], ]; for (const [icon, label, value] of bests) { const unit = label.includes("Session") ? " min" : ""; lines.push(` ${icon} ${label}: ${value}${unit}`); } return lines.join("\n"); } // Load stats on init loadStats(); // Track session start pi.on("session_start", async (_event, ctx) => { loadStats(); stats.sessionsCount++; stats.currentSessionStart = Date.now(); // Check for Night Owl const hour = new Date().getHours(); if (hour >= 0 && hour < 5) { stats.nightOwlSessions++; } saveStats(); checkAchievements(ctx); ctx.ui.setFooter( "gamification", `\u{1F3AE} S:${stats.sessionsCount} P:${stats.totalPrompts} F:${stats.filesCreated} \u{1F41B}:${stats.bugsFixed} \u{1F3C6}:${unlockedAchievements.size}/${ACHIEVEMENTS.length}`, ); }); // Track prompts pi.on("turn_start", async (_event, ctx) => { stats.totalPrompts++; saveStats(); checkAchievements(ctx); ctx.ui.setFooter( "gamification", `\u{1F3AE} S:${stats.sessionsCount} P:${stats.totalPrompts} F:${stats.filesCreated} \u{1F41B}:${stats.bugsFixed} \u{1F3C6}:${unlockedAchievements.size}/${ACHIEVEMENTS.length}`, ); }); // Track tool calls and file operations pi.on("tool_execution_end", async (event, ctx) => { stats.toolsUsed++; const toolName = event.toolName || ""; // Track file creates/edits if (["write", "write_file"].includes(toolName)) { stats.filesCreated++; const filePath = (event.input as any)?.path || (event.input as any)?.file_path || ""; if (filePath) { const ext = getFileExtension(filePath); if (ext) stats.fileTypesEdited.add(ext); } // Estimate lines written const content = (event.input as any)?.content || ""; if (typeof content === "string") { stats.linesWritten += content.split("\n").length; } } if (["edit", "edit_file"].includes(toolName)) { stats.filesEdited++; const filePath = (event.input as any)?.path || (event.input as any)?.file_path || ""; if (filePath) { const ext = getFileExtension(filePath); if (ext) stats.fileTypesEdited.add(ext); } } if (toolName === "bash" || toolName === "terminal") { stats.commandsRun++; } // Track swarm usage if (toolName === "swarm_task") { stats.usedSwarm = true; } // Track Venice skill usage if (VENICE_SKILLS.includes(toolName)) { veniceSkillsUsed.add(toolName); if (veniceSkillsUsed.size >= VENICE_SKILLS.length) { stats.usedAllVeniceSkills = true; } } saveStats(); checkAchievements(ctx); }); // Track bug fixes via agent messages pi.on("agent_end", async (event, ctx) => { // Check messages for bug fix indicators for (const message of event.messages) { if (message && typeof message === "object" && "content" in message) { const content = typeof (message as any).content === "string" ? (message as any).content : ""; const fixPatterns = ["fixed", "bug fix", "resolved", "patched", "corrected the error", "fixed the issue", "fixed the bug"]; const lowerContent = content.toLowerCase(); for (const pattern of fixPatterns) { if (lowerContent.includes(pattern)) { stats.bugsFixed++; break; } } } } // Update session time const sessionMinutes = Math.floor((Date.now() - stats.currentSessionStart) / 60000); stats.timeSpentMinutes += 1; // Increment per agent_end call if (sessionMinutes > stats.longestSessionMinutes) { stats.longestSessionMinutes = sessionMinutes; } if (sessionMinutes > 0 && sessionMinutes < stats.shortestSessionMinutes) { stats.shortestSessionMinutes = sessionMinutes; } updatePersonalBests(); saveStats(); checkAchievements(ctx); }); pi.registerCommand("stats", { description: "Show coding statistics dashboard", handler: async (_args, ctx) => { loadStats(); const dashboard = renderStatsDashboard(); ctx.ui.notify(dashboard, "info"); }, }); pi.registerCommand("achievements", { description: "Show unlocked achievements", handler: async (_args, ctx) => { loadStats(); const achievements = renderAchievements(); ctx.ui.notify(achievements, "info"); }, }); pi.registerCommand("leaderboard", { description: "Show personal bests", handler: async (_args, ctx) => { loadStats(); const leaderboard = renderLeaderboard(); ctx.ui.notify(leaderboard, "info"); }, }); }