495 lines
17 KiB
TypeScript
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");
|
|
},
|
|
});
|
|
}
|