Agent-JAE/default-extensions/gamification.ts

495 lines
17 KiB
TypeScript

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<string>;
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<string, number>;
}
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<string>();
const stats: Stats = {
sessionsCount: 0,
totalPrompts: 0,
filesCreated: 0,
filesEdited: 0,
linesWritten: 0,
toolsUsed: 0,
commandsRun: 0,
bugsFixed: 0,
timeSpentMinutes: 0,
fileTypesEdited: new Set<string>(),
usedSwarm: false,
usedAllVeniceSkills: false,
longestSessionMinutes: 0,
shortestSessionMinutes: Infinity,
currentSessionStart: Date.now(),
nightOwlSessions: 0,
};
let unlockedAchievements = new Set<string>();
let personalBests: Record<string, number> = {};
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");
},
});
}