Add 6 new extensions: dependency-auditor, docker-manager, env-manager, theme-system, agent-to-agent, gamification

This commit is contained in:
jae 2026-03-24 02:53:36 +01:00
parent 36273543bd
commit a2c1c92872
6 changed files with 1975 additions and 0 deletions

View file

@ -0,0 +1,311 @@
import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
import { spawn, type ChildProcess } from "node:child_process";
import { Type } from "@sinclair/typebox";
interface AgentInstance {
id: string;
role: string;
systemPrompt: string;
status: "running" | "idle" | "stopped" | "error";
process: ChildProcess | null;
output: string;
createdAt: number;
lastMessage: string;
}
const ROLE_PROMPTS: Record<string, string> = {
frontend: "You are a frontend development specialist. Focus on UI/UX, React, CSS, accessibility, responsive design, and browser APIs. Write clean component code with proper state management.",
backend: "You are a backend development specialist. Focus on API design, databases, server architecture, authentication, and performance. Write robust server-side code with proper error handling.",
tester: "You are a QA and testing specialist. Focus on writing comprehensive tests: unit tests, integration tests, E2E tests. Identify edge cases, write test plans, and ensure code reliability.",
reviewer: "You are a senior code reviewer. Analyze code for bugs, security issues, performance problems, and style violations. Provide constructive feedback with specific suggestions.",
devops: "You are a DevOps and infrastructure specialist. Focus on CI/CD pipelines, Docker, Kubernetes, monitoring, and deployment automation. Write infrastructure as code.",
security: "You are a security specialist. Focus on vulnerability assessment, secure coding practices, authentication/authorization, encryption, and threat modeling.",
architect: "You are a software architect. Focus on system design, scalability patterns, technology selection, API contracts, and architectural decision records.",
docs: "You are a technical documentation specialist. Focus on writing clear README files, API docs, tutorials, inline comments, and architectural documentation.",
};
export default function (pi: ExtensionAPI) {
const agents = new Map<string, AgentInstance>();
let idCounter = 0;
function generateId(role: string): string {
idCounter++;
return `${role}-${idCounter}`;
}
function getSystemPrompt(role: string): string {
return ROLE_PROMPTS[role] || `You are a ${role} specialist. Focus on tasks related to ${role} and provide expert-level assistance.`;
}
async function spawnAgent(role: string, cwd: string): Promise<AgentInstance> {
const id = generateId(role);
const systemPrompt = getSystemPrompt(role);
const agent: AgentInstance = {
id,
role,
systemPrompt,
status: "idle",
process: null,
output: "",
createdAt: Date.now(),
lastMessage: "",
};
agents.set(id, agent);
return agent;
}
async function sendMessage(
agent: AgentInstance,
message: string,
cwd: string,
): Promise<string> {
return new Promise((resolve) => {
const timeout = 180_000; // 3 minutes
let output = "";
let resolved = false;
agent.status = "running";
agent.lastMessage = message;
const prompt = `[System: ${agent.systemPrompt}]\n\n${message}`;
try {
const child = spawn(
"jae",
["-p", prompt],
{
cwd,
stdio: ["ignore", "pipe", "pipe"],
timeout,
env: { ...process.env },
},
);
agent.process = child;
child.stdout?.on("data", (data: Buffer) => {
output += data.toString();
});
child.stderr?.on("data", (data: Buffer) => {
output += data.toString();
});
child.on("close", (code) => {
agent.process = null;
agent.status = code === 0 ? "idle" : "error";
agent.output = output.trim();
if (!resolved) {
resolved = true;
resolve(output.trim() || "(no output)");
}
});
child.on("error", (err) => {
agent.process = null;
agent.status = "error";
if (!resolved) {
resolved = true;
resolve(`Agent error: ${err.message}`);
}
});
setTimeout(() => {
if (!resolved) {
resolved = true;
try { child.kill("SIGTERM"); } catch {}
agent.status = "error";
resolve(output.trim() + "\n(timed out after 3 minutes)");
}
}, timeout);
} catch (err: any) {
agent.status = "error";
resolve(`Spawn error: ${err.message}`);
}
});
}
function killAgent(id: string): boolean {
const agent = agents.get(id);
if (!agent) return false;
if (agent.process) {
try { agent.process.kill("SIGTERM"); } catch {}
}
agent.status = "stopped";
agent.process = null;
agents.delete(id);
return true;
}
function formatAgentList(): string {
if (agents.size === 0) return "No active agents. Use /a2a spawn <role> to create one.";
const lines: string[] = [];
for (const [id, agent] of agents) {
const uptime = Math.floor((Date.now() - agent.createdAt) / 1000);
const uptimeStr = uptime > 60 ? `${Math.floor(uptime / 60)}m${uptime % 60}s` : `${uptime}s`;
const statusIcon = agent.status === "running" ? "\u{1F7E2}" : agent.status === "idle" ? "\u{1F535}" : agent.status === "error" ? "\u{1F534}" : "\u26AB";
lines.push(` ${statusIcon} ${id} [${agent.role}] — ${agent.status} — uptime: ${uptimeStr}`);
}
return lines.join("\n");
}
pi.registerTool({
name: "a2a_communicate",
label: "Agent-to-Agent Communication",
description: "Spawn specialized AI agents and communicate with them. Agents can be given roles like 'frontend', 'backend', 'tester', 'reviewer', 'devops', 'security', 'architect', or 'docs'.",
parameters: Type.Object({
action: Type.String({ description: "Action: 'spawn', 'send', 'broadcast', 'list', 'kill'" }),
role: Type.Optional(Type.String({ description: "Agent role for spawn (e.g., 'frontend', 'backend', 'tester')" })),
agent_id: Type.Optional(Type.String({ description: "Target agent ID for send/kill" })),
message: Type.Optional(Type.String({ description: "Message to send to agent(s)" })),
}),
execute: async (args, ctx) => {
const action = args.action.toLowerCase();
switch (action) {
case "spawn": {
const role = args.role || "general";
const agent = await spawnAgent(role, ctx.cwd);
return { output: `\u2705 Agent spawned: ${agent.id} [${role}]\nSystem prompt: ${agent.systemPrompt.substring(0, 100)}...` };
}
case "send": {
if (!args.agent_id) return { error: "Specify agent_id to send message to." };
if (!args.message) return { error: "Specify message to send." };
const agent = agents.get(args.agent_id);
if (!agent) return { error: `Agent '${args.agent_id}' not found. Use action 'list' to see agents.` };
if (agent.status === "running") return { error: `Agent '${args.agent_id}' is busy processing. Wait for completion.` };
const response = await sendMessage(agent, args.message, ctx.cwd);
return { output: `\u{1F4AC} Response from ${args.agent_id}:\n\n${response}` };
}
case "broadcast": {
if (!args.message) return { error: "Specify message to broadcast." };
if (agents.size === 0) return { error: "No active agents. Spawn some first." };
const idle = Array.from(agents.values()).filter((a) => a.status === "idle");
if (idle.length === 0) return { error: "All agents are busy. Wait for them to finish." };
const promises = idle.map(async (agent) => {
const response = await sendMessage(agent, args.message!, ctx.cwd);
return `### ${agent.id} [${agent.role}]:\n${response.substring(0, 500)}`;
});
const results = await Promise.all(promises);
return { output: `\u{1F4E2} Broadcast to ${idle.length} agents:\n\n${results.join("\n\n")}` };
}
case "list": {
return { output: `\u{1F916} Active Agents:\n${formatAgentList()}` };
}
case "kill": {
if (!args.agent_id) return { error: "Specify agent_id to kill." };
const success = killAgent(args.agent_id);
return { output: success ? `\u2705 Agent '${args.agent_id}' terminated.` : `\u274C Agent '${args.agent_id}' not found.` };
}
default:
return { error: `Unknown action '${action}'. Use: spawn, send, broadcast, list, kill` };
}
},
});
pi.registerCommand("a2a", {
description: "Agent-to-Agent: /a2a spawn <role> | list | send <id> <msg> | broadcast <msg> | kill <id>",
handler: async (args, ctx) => {
const parts = args.trim().split(/\s+/);
const subcommand = (parts[0] || "list").toLowerCase();
switch (subcommand) {
case "spawn": {
const role = parts[1] || "general";
const validRoles = [...Object.keys(ROLE_PROMPTS), "general"];
const agent = await spawnAgent(role, ctx.cwd);
ctx.ui.notify(
`\u{1F916} Agent Spawned!\n${'\u2500'.repeat(30)}\n\n` +
` ID: ${agent.id}\n` +
` Role: ${role}\n` +
` Prompt: ${agent.systemPrompt.substring(0, 80)}...\n\n` +
`Available roles: ${validRoles.join(", ")}\n` +
`Send messages: /a2a send ${agent.id} <message>`,
"info",
);
break;
}
case "list":
case "ls": {
ctx.ui.notify(`\u{1F916} Agent Registry\n${'\u2500'.repeat(30)}\n\n${formatAgentList()}`, "info");
break;
}
case "send": {
const agentId = parts[1];
const message = parts.slice(2).join(" ");
if (!agentId || !message) {
ctx.ui.notify("Usage: /a2a send <agent-id> <message>", "warning");
return;
}
const agent = agents.get(agentId);
if (!agent) {
ctx.ui.notify(`\u274C Agent '${agentId}' not found. Use /a2a list.`, "warning");
return;
}
if (agent.status === "running") {
ctx.ui.notify(`\u23F3 Agent '${agentId}' is busy. Wait for completion.`, "warning");
return;
}
ctx.ui.notify(`\u{1F4E4} Sending to ${agentId}...`, "info");
const response = await sendMessage(agent, message, ctx.cwd);
ctx.ui.notify(`\u{1F4AC} ${agentId} responded:\n\n${response.substring(0, 2000)}`, "info");
break;
}
case "broadcast": {
const message = parts.slice(1).join(" ");
if (!message) {
ctx.ui.notify("Usage: /a2a broadcast <message>", "warning");
return;
}
const idle = Array.from(agents.values()).filter((a) => a.status === "idle");
if (idle.length === 0) {
ctx.ui.notify("\u274C No idle agents. Spawn some first with /a2a spawn <role>.", "warning");
return;
}
ctx.ui.notify(`\u{1F4E2} Broadcasting to ${idle.length} agents...`, "info");
const promises = idle.map(async (agent) => {
const response = await sendMessage(agent, message, ctx.cwd);
return `\u{1F916} ${agent.id}:\n${response.substring(0, 500)}`;
});
const results = await Promise.all(promises);
ctx.ui.notify(`\u{1F4E2} Broadcast Results:\n${'\u2500'.repeat(30)}\n\n${results.join("\n\n")}`, "info");
break;
}
case "kill":
case "stop": {
const agentId = parts[1];
if (!agentId) {
ctx.ui.notify("Usage: /a2a kill <agent-id>", "warning");
return;
}
if (agentId === "all") {
const count = agents.size;
for (const id of Array.from(agents.keys())) killAgent(id);
ctx.ui.notify(`\u2705 Terminated ${count} agents.`, "info");
return;
}
const success = killAgent(agentId);
ctx.ui.notify(
success ? `\u2705 Agent '${agentId}' terminated.` : `\u274C Agent '${agentId}' not found.`,
success ? "info" : "warning",
);
break;
}
default:
ctx.ui.notify(
`\u{1F916} Agent-to-Agent Communication\n${'\u2500'.repeat(30)}\n\n` +
"Commands:\n" +
" /a2a spawn <role> - Spawn specialized agent\n" +
" /a2a list - List active agents\n" +
" /a2a send <id> <message> - Send message to agent\n" +
" /a2a broadcast <message> - Send to all idle agents\n" +
" /a2a kill <id|all> - Terminate agent(s)\n\n" +
`Roles: ${Object.keys(ROLE_PROMPTS).join(", ")}`,
"info",
);
}
},
});
}

View file

@ -0,0 +1,257 @@
import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";
import { Type } from "@sinclair/typebox";
interface AuditResult {
outdated: string[];
vulnerabilities: string[];
unused: string[];
missing: string[];
summary: string;
}
export default function (pi: ExtensionAPI) {
async function parsePackageJson(cwd: string): Promise<{ deps: Record<string, string>; devDeps: Record<string, string>; scripts: Record<string, string> } | null> {
const pkgPath = join(cwd, "package.json");
if (!existsSync(pkgPath)) return null;
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
return {
deps: pkg.dependencies || {},
devDeps: pkg.devDependencies || {},
scripts: pkg.scripts || {},
};
} catch {
return null;
}
}
async function runNpmAudit(cwd: string): Promise<string> {
try {
const result = await pi.exec("npm", ["audit", "--json"], { cwd });
if (result.stdout.trim()) {
try {
const audit = JSON.parse(result.stdout);
const meta = audit.metadata || {};
const vulns = meta.vulnerabilities || {};
const lines: string[] = [];
let totalVulns = 0;
for (const [severity, count] of Object.entries(vulns)) {
if (typeof count === "number" && count > 0) {
lines.push(` ${severity}: ${count}`);
totalVulns += count;
}
}
if (totalVulns === 0) return "\u2705 No known vulnerabilities found.";
return `\u26A0\uFE0F Found ${totalVulns} vulnerabilities:\n${lines.join("\n")}`;
} catch {
return result.stdout.substring(0, 1000);
}
}
return result.stderr.trim() || "No audit data available.";
} catch (err: any) {
return `Audit error: ${err.message}`;
}
}
async function runNpmOutdated(cwd: string): Promise<string[]> {
try {
const result = await pi.exec("npm", ["outdated", "--json"], { cwd });
if (result.stdout.trim()) {
try {
const outdated = JSON.parse(result.stdout);
const lines: string[] = [];
for (const [pkg, info] of Object.entries(outdated)) {
const i = info as any;
lines.push(` ${pkg}: ${i.current || "?"} \u2192 ${i.latest || "?"}`);
}
return lines;
} catch {
return [result.stdout.substring(0, 500)];
}
}
return [];
} catch {
return [];
}
}
async function findUnusedDeps(cwd: string, deps: Record<string, string>): Promise<string[]> {
const unused: string[] = [];
const depNames = Object.keys(deps);
if (depNames.length === 0) return unused;
for (const dep of depNames) {
try {
// Search for import/require of this dep in source files
const result = await pi.exec(
"grep",
["-r", "-l", "--include=*.ts", "--include=*.tsx", "--include=*.js", "--include=*.jsx", "--include=*.mjs", "--include=*.cjs", dep, "."],
{ cwd },
);
if (result.code !== 0 || !result.stdout.trim()) {
// Also check if referenced in config files
const configResult = await pi.exec(
"grep",
["-r", "-l", "--include=*.json", "--include=*.yaml", "--include=*.yml", "--include=*.toml", "--include=*.config.*", dep, "."],
{ cwd },
);
// Exclude package.json and lock files
const configFiles = (configResult.stdout || "").split("\n").filter(
(f) => f.trim() && !f.includes("package.json") && !f.includes("package-lock") && !f.includes("node_modules"),
);
if (configFiles.length === 0) {
unused.push(dep);
}
}
} catch {
// grep failed, skip
}
}
return unused;
}
async function fullAudit(cwd: string): Promise<AuditResult> {
const pkg = await parsePackageJson(cwd);
if (!pkg) {
return {
outdated: [],
vulnerabilities: ["No package.json found in current directory."],
unused: [],
missing: [],
summary: "\u274C No package.json found. Navigate to a Node.js project directory.",
};
}
const allDeps = { ...pkg.deps, ...pkg.devDeps };
const totalDeps = Object.keys(allDeps).length;
// Run checks in parallel
const [outdated, securityReport, unused] = await Promise.all([
runNpmOutdated(cwd),
runNpmAudit(cwd),
findUnusedDeps(cwd, pkg.deps),
]);
// Check for missing deps (referenced but not in package.json)
const missing: string[] = [];
try {
const result = await pi.exec(
"grep",
["-r", "-h", "-o", "--include=*.ts", "--include=*.tsx", "--include=*.js", "--include=*.jsx",
`from ['"][^./][^'"]*['"]`, "."],
{ cwd },
);
if (result.stdout) {
const imports = new Set(
result.stdout.split("\n")
.map((line) => {
const match = line.match(/from\s+['"]([^./][^'"]*)['"]/);
return match ? match[1].split("/")[0] : null;
})
.filter((name): name is string => name !== null && !name.startsWith("node:")),
);
for (const imp of imports) {
if (!allDeps[imp] && !imp.startsWith("@")) {
missing.push(imp);
}
}
}
} catch { /* ignore grep errors */ }
const summaryLines = [
`\u{1F4E6} Dependencies: ${totalDeps} total (${Object.keys(pkg.deps).length} prod, ${Object.keys(pkg.devDeps).length} dev)`,
`\u{1F504} Outdated: ${outdated.length > 0 ? outdated.length + " packages" : "all up to date \u2705"}`,
`\u{1F6E1}\uFE0F Security: ${securityReport}`,
`\u{1F5D1}\uFE0F Potentially unused: ${unused.length > 0 ? unused.join(", ") : "none detected \u2705"}`,
];
if (missing.length > 0) {
summaryLines.push(`\u26A0\uFE0F Possibly missing: ${missing.join(", ")}`);
}
return {
outdated,
vulnerabilities: [securityReport],
unused,
missing,
summary: summaryLines.join("\n"),
};
}
pi.registerTool({
name: "audit_dependencies",
label: "Audit Dependencies",
description: "Scan project dependencies for outdated packages, security vulnerabilities, and unused dependencies. Optionally auto-fix issues.",
parameters: Type.Object({
action: Type.Optional(Type.String({ description: "Action: 'scan' (default), 'fix', 'security'" })),
}),
execute: async (args, ctx) => {
const action = (args.action || "scan").toLowerCase();
if (action === "fix") {
const updateResult = await pi.exec("npm", ["update"], { cwd: ctx.cwd });
const auditFixResult = await pi.exec("npm", ["audit", "fix"], { cwd: ctx.cwd });
return {
output: `\u{1F527} Auto-fix results:\n\nnpm update:\n${updateResult.stdout || updateResult.stderr}\n\nnpm audit fix:\n${auditFixResult.stdout || auditFixResult.stderr}`,
};
}
if (action === "security") {
const report = await runNpmAudit(ctx.cwd);
return { output: `\u{1F6E1}\uFE0F Security Report:\n\n${report}` };
}
const result = await fullAudit(ctx.cwd);
return { output: result.summary };
},
});
pi.registerCommand("audit", {
description: "Audit dependencies: /audit [fix|security]",
handler: async (args, ctx) => {
const subcommand = args.trim().toLowerCase();
if (subcommand === "fix") {
ctx.ui.notify("\u{1F527} Running npm update and audit fix...", "info");
const updateResult = await pi.exec("npm", ["update"], { cwd: ctx.cwd });
const auditFixResult = await pi.exec("npm", ["audit", "fix"], { cwd: ctx.cwd });
ctx.ui.notify(
`\u{1F527} Fix Results:\n\nnpm update:\n${(updateResult.stdout || updateResult.stderr || "done").substring(0, 500)}\n\nnpm audit fix:\n${(auditFixResult.stdout || auditFixResult.stderr || "done").substring(0, 500)}`,
"info",
);
return;
}
if (subcommand === "security") {
ctx.ui.notify("\u{1F6E1}\uFE0F Running security audit...", "info");
const report = await runNpmAudit(ctx.cwd);
ctx.ui.notify(`\u{1F6E1}\uFE0F Security Report:\n\n${report}`, "info");
return;
}
ctx.ui.notify("\u{1F50D} Scanning dependencies...", "info");
const result = await fullAudit(ctx.cwd);
let output = `\u{1F4CB} Dependency Audit Report\n${'\u2500'.repeat(40)}\n\n${result.summary}`;
if (result.outdated.length > 0) {
output += `\n\n\u{1F504} Outdated Packages:\n${result.outdated.join("\n")}`;
}
if (result.unused.length > 0) {
output += `\n\n\u{1F5D1}\uFE0F Potentially Unused:\n ${result.unused.join(", ")}`;
output += `\n Run: npm uninstall ${result.unused.join(" ")}`;
}
if (result.missing.length > 0) {
output += `\n\n\u26A0\uFE0F Possibly Missing:\n ${result.missing.join(", ")}`;
output += `\n Run: npm install ${result.missing.join(" ")}`;
}
output += `\n\n\u{1F4A1} Tip: /audit fix to auto-update, /audit security for vuln details`;
ctx.ui.notify(output, "info");
},
});
}

View file

@ -0,0 +1,318 @@
import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
import { existsSync, readFileSync } from "node:fs";
import { join, basename } from "node:path";
import { Type } from "@sinclair/typebox";
export default function (pi: ExtensionAPI) {
async function dockerExec(args: string[], cwd: string): Promise<{ ok: boolean; output: string }> {
try {
const result = await pi.exec("docker", args, { cwd });
return {
ok: result.code === 0,
output: (result.stdout || "").trim() || (result.stderr || "").trim() || "(no output)",
};
} catch (err: any) {
return { ok: false, output: `Docker error: ${err.message}` };
}
}
async function getContainerStatus(cwd: string): Promise<string> {
const result = await dockerExec(
["ps", "--format", "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"],
cwd,
);
if (!result.ok) return `\u274C Docker not available: ${result.output}`;
if (!result.output.trim() || result.output.split("\n").length <= 1) {
return "\u{1F4AD} No running containers.";
}
return `\u{1F433} Running Containers:\n\n${result.output}`;
}
async function getContainerLogs(container: string, cwd: string, tail: number = 50): Promise<string> {
const result = await dockerExec(["logs", "--tail", String(tail), container], cwd);
if (!result.ok) return `\u274C Failed to get logs for '${container}': ${result.output}`;
return `\u{1F4DC} Logs for ${container} (last ${tail} lines):\n\n${result.output}`;
}
async function buildImage(cwd: string, tag?: string, dockerfile?: string): Promise<string> {
const imageTag = tag || basename(cwd).toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
const args = ["build", "-t", imageTag];
if (dockerfile) args.push("-f", dockerfile);
args.push(".");
const result = await dockerExec(args, cwd);
if (!result.ok) return `\u274C Build failed:\n${result.output.substring(0, 1000)}`;
// Get image info
const info = await dockerExec(["images", imageTag, "--format", "{{.Repository}}:{{.Tag}} {{.Size}} {{.CreatedAt}}"], cwd);
return `\u2705 Image built successfully: ${imageTag}\n${info.output}\n\nRun: docker run -d -p 3000:3000 ${imageTag}`;
}
async function analyzeProjectForDockerfile(cwd: string): Promise<string> {
const analysis: string[] = [];
// Detect project type
if (existsSync(join(cwd, "package.json"))) {
try {
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
if (deps["next"]) {
analysis.push("Detected: Next.js application");
analysis.push("Recommended: Multi-stage build with Node.js Alpine");
analysis.push(generateNodeDockerfile("next", pkg));
} else if (deps["vite"] || deps["react-scripts"]) {
analysis.push("Detected: SPA (React/Vite)");
analysis.push("Recommended: Multi-stage build with nginx");
analysis.push(generateSPADockerfile(pkg));
} else {
analysis.push("Detected: Node.js application");
analysis.push(generateNodeDockerfile("node", pkg));
}
} catch {
analysis.push("Could not parse package.json");
}
} else if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml"))) {
analysis.push("Detected: Python application");
analysis.push(generatePythonDockerfile(cwd));
} else if (existsSync(join(cwd, "go.mod"))) {
analysis.push("Detected: Go application");
analysis.push(generateGoDockerfile());
} else if (existsSync(join(cwd, "Cargo.toml"))) {
analysis.push("Detected: Rust application");
analysis.push("Recommended: Multi-stage build with rust:alpine");
} else {
analysis.push("Could not detect project type. Provide a Dockerfile manually.");
}
return analysis.join("\n\n");
}
function generateNodeDockerfile(type: string, pkg: any): string {
const startCmd = pkg.scripts?.start ? "npm start" : "node index.js";
if (type === "next") {
return `# Suggested Dockerfile for Next.js
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]`;
}
return `# Suggested Dockerfile for Node.js
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["${startCmd.split(" ")[0]}", ${startCmd.split(" ").slice(1).map((s: string) => `"${s}"`).join(", ")}]`;
}
function generateSPADockerfile(pkg: any): string {
const buildDir = pkg.scripts?.build?.includes("vite") ? "dist" : "build";
return `# Suggested Dockerfile for SPA
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/${buildDir} /usr/share/nginx/html
COPY <<EOF /etc/nginx/conf.d/default.conf
server {
listen 80;
root /usr/share/nginx/html;
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]`;
}
function generatePythonDockerfile(cwd: string): string {
const hasRequirements = existsSync(join(cwd, "requirements.txt"));
const hasPyproject = existsSync(join(cwd, "pyproject.toml"));
const installCmd = hasRequirements
? "COPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt"
: hasPyproject
? "COPY pyproject.toml .\nRUN pip install --no-cache-dir ."
: "# Add your dependency installation here";
return `# Suggested Dockerfile for Python
FROM python:3.12-slim
WORKDIR /app
${installCmd}
COPY . .
EXPOSE 8000
CMD ["python", "main.py"]`;
}
function generateGoDockerfile(): string {
return `# Suggested Dockerfile for Go
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server .
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]`;
}
pi.registerTool({
name: "docker_manage",
label: "Docker Manager",
description: "Manage Docker containers: list running containers, build images, view logs, or generate Dockerfiles from project analysis.",
parameters: Type.Object({
action: Type.String({ description: "Action: 'status', 'build', 'logs', 'generate', 'run', 'stop'" }),
container: Type.Optional(Type.String({ description: "Container name or ID (for logs/stop)" })),
tag: Type.Optional(Type.String({ description: "Image tag for build" })),
port: Type.Optional(Type.String({ description: "Port mapping for run (e.g., '3000:3000')" })),
}),
execute: async (args, ctx) => {
const action = args.action.toLowerCase();
switch (action) {
case "status":
case "ps":
case "list": {
const status = await getContainerStatus(ctx.cwd);
return { output: status };
}
case "build": {
if (!existsSync(join(ctx.cwd, "Dockerfile"))) {
return { error: "No Dockerfile found. Use action 'generate' to create one." };
}
const result = await buildImage(ctx.cwd, args.tag);
return { output: result };
}
case "logs": {
if (!args.container) return { error: "Specify a container name or ID." };
const logs = await getContainerLogs(args.container, ctx.cwd);
return { output: logs };
}
case "generate": {
const analysis = await analyzeProjectForDockerfile(ctx.cwd);
return { output: `\u{1F433} Dockerfile Generation:\n\n${analysis}` };
}
case "run": {
const tag = args.tag || basename(ctx.cwd).toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
const runArgs = ["run", "-d"];
if (args.port) runArgs.push("-p", args.port);
runArgs.push(tag);
const result = await dockerExec(runArgs, ctx.cwd);
return { output: result.ok ? `\u2705 Container started: ${result.output.substring(0, 12)}` : `\u274C ${result.output}` };
}
case "stop": {
if (!args.container) return { error: "Specify a container name or ID." };
const result = await dockerExec(["stop", args.container], ctx.cwd);
return { output: result.ok ? `\u2705 Stopped: ${args.container}` : `\u274C ${result.output}` };
}
default:
return { error: `Unknown action '${action}'. Use: status, build, logs, generate, run, stop` };
}
},
});
pi.registerCommand("docker", {
description: "Docker manager: /docker [build|logs <container>|generate|status]",
handler: async (args, ctx) => {
const parts = args.trim().split(/\s+/);
const subcommand = (parts[0] || "status").toLowerCase();
switch (subcommand) {
case "status":
case "ps":
case "": {
const status = await getContainerStatus(ctx.cwd);
ctx.ui.notify(status, "info");
break;
}
case "build": {
if (!existsSync(join(ctx.cwd, "Dockerfile"))) {
ctx.ui.notify("\u274C No Dockerfile found. Use /docker generate first.", "warning");
return;
}
ctx.ui.notify("\u{1F528} Building Docker image...", "info");
const tag = parts[1] || undefined;
const result = await buildImage(ctx.cwd, tag);
ctx.ui.notify(result, "info");
break;
}
case "logs": {
const container = parts[1];
if (!container) {
ctx.ui.notify("Usage: /docker logs <container-name-or-id>", "warning");
return;
}
const tail = parseInt(parts[2] || "50", 10);
const logs = await getContainerLogs(container, ctx.cwd, tail);
ctx.ui.notify(logs, "info");
break;
}
case "generate": {
ctx.ui.notify("\u{1F50D} Analyzing project for Dockerfile generation...", "info");
const analysis = await analyzeProjectForDockerfile(ctx.cwd);
ctx.ui.notify(`\u{1F433} Dockerfile Suggestion:\n\n${analysis}\n\n\u{1F4A1} Tip: Ask JAE to create the Dockerfile based on this analysis.`, "info");
break;
}
case "stop": {
const container = parts[1];
if (!container) {
ctx.ui.notify("Usage: /docker stop <container-name-or-id>", "warning");
return;
}
const result = await dockerExec(["stop", container], ctx.cwd);
ctx.ui.notify(result.ok ? `\u2705 Stopped: ${container}` : `\u274C ${result.output}`, "info");
break;
}
case "run": {
const tag = parts[1];
if (!tag) {
ctx.ui.notify("Usage: /docker run <image> [port-mapping]", "warning");
return;
}
const port = parts[2] || undefined;
const runArgs = ["run", "-d"];
if (port) runArgs.push("-p", port);
runArgs.push(tag);
const result = await dockerExec(runArgs, ctx.cwd);
ctx.ui.notify(result.ok ? `\u2705 Container started: ${result.output.substring(0, 12)}` : `\u274C ${result.output}`, "info");
break;
}
default:
ctx.ui.notify(
`\u{1F433} Docker Manager\n${'\u2500'.repeat(30)}\n\n` +
"Commands:\n" +
" /docker - Show running containers\n" +
" /docker build - Build image from Dockerfile\n" +
" /docker logs <id> - Show container logs\n" +
" /docker generate - Generate Dockerfile for project\n" +
" /docker run <img> - Run a container\n" +
" /docker stop <id> - Stop a container",
"info",
);
}
},
});
}

View file

@ -0,0 +1,295 @@
import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
import { join, relative, basename } from "node:path";
import { Type } from "@sinclair/typebox";
interface EnvFile {
name: string;
path: string;
vars: Map<string, string>;
}
export default function (pi: ExtensionAPI) {
function parseEnvFile(filePath: string): Map<string, string> {
const vars = new Map<string, string>();
if (!existsSync(filePath)) return vars;
try {
const content = readFileSync(filePath, "utf-8");
for (const line of content.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIndex = trimmed.indexOf("=");
if (eqIndex === -1) continue;
const key = trimmed.substring(0, eqIndex).trim();
let value = trimmed.substring(eqIndex + 1).trim();
// Strip quotes
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
vars.set(key, value);
}
} catch { /* ignore read errors */ }
return vars;
}
function findEnvFiles(cwd: string): EnvFile[] {
const envFiles: EnvFile[] = [];
function scanDir(dir: string, depth: number = 0): void {
if (depth > 3) return; // Don't go too deep
try {
const entries = readdirSync(dir);
for (const entry of entries) {
if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build") continue;
const fullPath = join(dir, entry);
try {
const stat = statSync(fullPath);
if (stat.isFile() && (entry.startsWith(".env") || entry.endsWith(".env"))) {
envFiles.push({
name: relative(cwd, fullPath) || entry,
path: fullPath,
vars: parseEnvFile(fullPath),
});
} else if (stat.isDirectory() && depth < 3) {
scanDir(fullPath, depth + 1);
}
} catch { /* skip inaccessible */ }
}
} catch { /* skip unreadable dirs */ }
}
scanDir(cwd);
return envFiles;
}
function maskValue(value: string): string {
if (value.length <= 4) return "****";
return value.substring(0, 2) + "*".repeat(Math.min(value.length - 4, 20)) + value.substring(value.length - 2);
}
function diffEnvFiles(fileA: EnvFile, fileB: EnvFile): string {
const allKeys = new Set([...fileA.vars.keys(), ...fileB.vars.keys()]);
const lines: string[] = [
`\u{1F504} Diff: ${fileA.name} vs ${fileB.name}`,
`${'\u2500'.repeat(50)}`,
];
let onlyA: string[] = [];
let onlyB: string[] = [];
let different: string[] = [];
let same = 0;
for (const key of Array.from(allKeys).sort()) {
const hasA = fileA.vars.has(key);
const hasB = fileB.vars.has(key);
if (hasA && !hasB) {
onlyA.push(key);
} else if (!hasA && hasB) {
onlyB.push(key);
} else if (hasA && hasB) {
const valA = fileA.vars.get(key)!;
const valB = fileB.vars.get(key)!;
if (valA !== valB) {
different.push(` ${key}: ${maskValue(valA)} \u2192 ${maskValue(valB)}`);
} else {
same++;
}
}
}
if (onlyA.length > 0) lines.push(`\n\u{1F7E2} Only in ${fileA.name}:\n ${onlyA.join(", ")}`);
if (onlyB.length > 0) lines.push(`\n\u{1F535} Only in ${fileB.name}:\n ${onlyB.join(", ")}`);
if (different.length > 0) lines.push(`\n\u{1F7E1} Different values:\n${different.join("\n")}`);
lines.push(`\n\u2705 ${same} variables identical`);
return lines.join("\n");
}
function checkMissingVars(envFiles: EnvFile[]): string {
if (envFiles.length < 2) return "Need at least 2 .env files to check for missing variables.";
// Collect all keys across all files
const allKeys = new Set<string>();
for (const f of envFiles) {
for (const key of f.vars.keys()) {
allKeys.add(key);
}
}
const lines: string[] = [
`\u{1F50D} Missing Variables Check`,
`${'\u2500'.repeat(50)}`,
`Files: ${envFiles.map((f) => f.name).join(", ")}`,
`Total unique keys: ${allKeys.size}`,
"",
];
let missingCount = 0;
for (const key of Array.from(allKeys).sort()) {
const presentIn: string[] = [];
const missingFrom: string[] = [];
for (const f of envFiles) {
if (f.vars.has(key)) presentIn.push(f.name);
else missingFrom.push(f.name);
}
if (missingFrom.length > 0) {
lines.push(` \u26A0\uFE0F ${key}: missing from ${missingFrom.join(", ")}`);
missingCount++;
}
}
if (missingCount === 0) {
lines.push("\u2705 All variables present in all env files!");
} else {
lines.push(`\n\u{1F4CA} ${missingCount} variables missing across environments`);
}
return lines.join("\n");
}
function syncEnvFiles(source: EnvFile, target: EnvFile): { added: string[]; content: string } {
const added: string[] = [];
const targetContent = existsSync(target.path) ? readFileSync(target.path, "utf-8") : "";
let newContent = targetContent;
for (const [key, value] of source.vars) {
if (!target.vars.has(key)) {
added.push(key);
const appendLine = `\n# Synced from ${source.name}\n${key}=${value}`;
newContent += appendLine;
}
}
return { added, content: newContent };
}
pi.registerTool({
name: "env_manage",
label: "Environment Manager",
description: "Manage .env files: list, diff, check for missing vars, or sync between environments.",
parameters: Type.Object({
action: Type.String({ description: "Action: 'list', 'diff', 'check', 'sync'" }),
source: Type.Optional(Type.String({ description: "Source env file name for diff/sync" })),
target: Type.Optional(Type.String({ description: "Target env file name for diff/sync" })),
}),
execute: async (args, ctx) => {
const envFiles = findEnvFiles(ctx.cwd);
const action = args.action.toLowerCase();
switch (action) {
case "list": {
if (envFiles.length === 0) return { output: "No .env files found in project." };
const list = envFiles
.map((f) => ` ${f.name}: ${f.vars.size} variables`)
.join("\n");
return { output: `\u{1F4C1} Environment Files:\n${list}` };
}
case "diff": {
if (envFiles.length < 2) return { error: "Need at least 2 .env files to diff." };
const source = args.source ? envFiles.find((f) => f.name.includes(args.source!)) : envFiles[0];
const target = args.target ? envFiles.find((f) => f.name.includes(args.target!)) : envFiles[1];
if (!source || !target) return { error: "Could not find specified env files." };
return { output: diffEnvFiles(source, target) };
}
case "check": {
return { output: checkMissingVars(envFiles) };
}
case "sync": {
if (!args.source || !args.target) return { error: "Specify source and target env files." };
const source = envFiles.find((f) => f.name.includes(args.source!));
const target = envFiles.find((f) => f.name.includes(args.target!));
if (!source || !target) return { error: "Could not find specified env files." };
const { added, content } = syncEnvFiles(source, target);
if (added.length === 0) return { output: "\u2705 Target already has all source variables." };
writeFileSync(target.path, content, "utf-8");
return { output: `\u2705 Synced ${added.length} variables to ${target.name}:\n ${added.join(", ")}` };
}
default:
return { error: `Unknown action '${action}'. Use: list, diff, check, sync` };
}
},
});
pi.registerCommand("env", {
description: "Manage env files: /env [diff|check|sync <source> <target>]",
handler: async (args, ctx) => {
const parts = args.trim().split(/\s+/);
const subcommand = (parts[0] || "list").toLowerCase();
const envFiles = findEnvFiles(ctx.cwd);
switch (subcommand) {
case "list":
case "": {
if (envFiles.length === 0) {
ctx.ui.notify("\u{1F4C1} No .env files found in this project.", "info");
return;
}
const list = envFiles
.map((f) => {
const keys = Array.from(f.vars.keys()).slice(0, 5).join(", ");
const more = f.vars.size > 5 ? ` +${f.vars.size - 5} more` : "";
return ` \u{1F4C4} ${f.name} (${f.vars.size} vars): ${keys}${more}`;
})
.join("\n");
ctx.ui.notify(`\u{1F4C1} Environment Files:\n\n${list}`, "info");
break;
}
case "diff": {
if (envFiles.length < 2) {
ctx.ui.notify("Need at least 2 .env files to diff.", "warning");
return;
}
const source = parts[1] ? envFiles.find((f) => f.name.includes(parts[1])) : envFiles[0];
const target = parts[2] ? envFiles.find((f) => f.name.includes(parts[2])) : envFiles[1];
if (!source || !target) {
ctx.ui.notify("Could not find specified env files. Use /env to list them.", "warning");
return;
}
ctx.ui.notify(diffEnvFiles(source, target), "info");
break;
}
case "check": {
ctx.ui.notify(checkMissingVars(envFiles), "info");
break;
}
case "sync": {
const sourceName = parts[1];
const targetName = parts[2];
if (!sourceName || !targetName) {
ctx.ui.notify("Usage: /env sync <source> <target>\n\nExample: /env sync .env.development .env.production", "warning");
return;
}
const source = envFiles.find((f) => f.name.includes(sourceName));
const target = envFiles.find((f) => f.name.includes(targetName));
if (!source) {
ctx.ui.notify(`Could not find env file matching '${sourceName}'.`, "warning");
return;
}
if (!target) {
ctx.ui.notify(`Could not find env file matching '${targetName}'.`, "warning");
return;
}
const { added, content } = syncEnvFiles(source, target);
if (added.length === 0) {
ctx.ui.notify("\u2705 Target already has all source variables. Nothing to sync.", "info");
return;
}
writeFileSync(target.path, content, "utf-8");
ctx.ui.notify(`\u2705 Synced ${added.length} missing variables to ${target.name}:\n ${added.join(", ")}`, "info");
break;
}
default:
ctx.ui.notify(
`\u{1F4C1} Environment Manager\n${'\u2500'.repeat(30)}\n\n` +
"Commands:\n" +
" /env - List all .env files\n" +
" /env diff [a] [b] - Compare two env files\n" +
" /env check - Find missing vars across files\n" +
" /env sync <s> <t> - Sync missing vars from source to target",
"info",
);
}
},
});
}

View file

@ -0,0 +1,495 @@
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");
},
});
}

View file

@ -0,0 +1,299 @@
import type { ExtensionAPI, ExtensionContext } from "@jaeswift/jae-coding-agent";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
interface ThemeColors {
primary: string;
secondary: string;
accent: string;
bg: string;
surface: string;
border: string;
text: string;
muted: string;
}
interface Theme {
name: string;
description: string;
colors: ThemeColors;
}
interface ThemeConfig {
active: string;
custom: Theme[];
}
export default function (pi: ExtensionAPI) {
const THEMES: Record<string, Theme> = {
dragon: {
name: "dragon",
description: "Default JAE theme — black with orange accents",
colors: {
primary: "#FF6B00",
secondary: "#FF8C38",
accent: "#FFA500",
bg: "#0a0a0a",
surface: "#111111",
border: "#1e1e1e",
text: "#f8f8f8",
muted: "#888888",
},
},
midnight: {
name: "midnight",
description: "Deep dark blue — calm and focused",
colors: {
primary: "#4A9EFF",
secondary: "#6BB5FF",
accent: "#80CAFF",
bg: "#0a0e1a",
surface: "#111827",
border: "#1e293b",
text: "#e2e8f0",
muted: "#64748b",
},
},
matrix: {
name: "matrix",
description: "Green on black — classic hacker aesthetic",
colors: {
primary: "#00FF41",
secondary: "#00CC33",
accent: "#33FF77",
bg: "#000000",
surface: "#0a0a0a",
border: "#0f3d0f",
text: "#00FF41",
muted: "#00802a",
},
},
frost: {
name: "frost",
description: "Light cool tones — easy on the eyes",
colors: {
primary: "#5B8DEF",
secondary: "#7AA8F2",
accent: "#98C1FF",
bg: "#0c1220",
surface: "#141e30",
border: "#1f2d42",
text: "#dce6f5",
muted: "#7b8da6",
},
},
sunset: {
name: "sunset",
description: "Warm oranges and reds — vibrant energy",
colors: {
primary: "#FF4500",
secondary: "#FF6347",
accent: "#FF8C00",
bg: "#0d0806",
surface: "#1a0f0a",
border: "#2d1810",
text: "#fce4d6",
muted: "#a0735c",
},
},
};
const configDir = join(process.env.HOME || "/root", ".jae");
const configPath = join(configDir, "theme.json");
function loadConfig(): ThemeConfig {
try {
if (existsSync(configPath)) {
return JSON.parse(readFileSync(configPath, "utf-8"));
}
} catch { /* ignore */ }
return { active: "dragon", custom: [] };
}
function saveConfig(config: ThemeConfig): void {
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
}
function getTheme(name: string, config: ThemeConfig): Theme | undefined {
if (THEMES[name]) return THEMES[name];
return config.custom.find((t) => t.name === name);
}
function getAllThemes(config: ThemeConfig): Theme[] {
return [...Object.values(THEMES), ...config.custom];
}
function applyTheme(theme: Theme, ctx: ExtensionContext): void {
const c = theme.colors;
ctx.ui.setStatus(
"theme",
`\u{1F3A8} ${theme.name}`,
);
// Apply theme colors via footer display
ctx.ui.setFooter(
"theme",
`\u{1F3A8} Theme: ${theme.name} | ${c.primary} ${c.secondary}`,
);
}
function formatThemePreview(theme: Theme): string {
const c = theme.colors;
return (
` \u{1F3A8} ${theme.name}${theme.description}\n` +
` Primary: ${c.primary} Secondary: ${c.secondary}\n` +
` Accent: ${c.accent} Background: ${c.bg}\n` +
` Surface: ${c.surface} Border: ${c.border}\n` +
` Text: ${c.text} Muted: ${c.muted}`
);
}
// Apply theme on session start
pi.on("session_start", async (_event, ctx) => {
const config = loadConfig();
const theme = getTheme(config.active, config) || THEMES.dragon;
applyTheme(theme, ctx);
});
pi.registerCommand("theme", {
description: "Manage themes: /theme [list|set <name>|create <name>|show]",
handler: async (args, ctx) => {
const parts = args.trim().split(/\s+/);
const subcommand = (parts[0] || "show").toLowerCase();
const config = loadConfig();
switch (subcommand) {
case "show":
case "": {
const theme = getTheme(config.active, config) || THEMES.dragon;
ctx.ui.notify(
`\u{1F3A8} Current Theme\n${'\u2500'.repeat(40)}\n\n${formatThemePreview(theme)}`,
"info",
);
break;
}
case "list":
case "ls": {
const all = getAllThemes(config);
const list = all
.map((t) => {
const active = t.name === config.active ? " \u2705 (active)" : "";
const isCustom = !THEMES[t.name] ? " [custom]" : "";
return ` \u{25CF} ${t.name}${active}${isCustom}${t.description}`;
})
.join("\n");
ctx.ui.notify(
`\u{1F3A8} Available Themes\n${'\u2500'.repeat(40)}\n\n${list}\n\nUse /theme set <name> to apply a theme.`,
"info",
);
break;
}
case "set":
case "apply": {
const name = parts[1];
if (!name) {
ctx.ui.notify("Usage: /theme set <name>\n\nUse /theme list to see available themes.", "warning");
return;
}
const theme = getTheme(name.toLowerCase(), config);
if (!theme) {
ctx.ui.notify(
`\u274C Theme '${name}' not found.\n\nAvailable: ${getAllThemes(config).map((t) => t.name).join(", ")}`,
"warning",
);
return;
}
config.active = theme.name;
saveConfig(config);
applyTheme(theme, ctx);
ctx.ui.notify(
`\u2705 Theme set to '${theme.name}'\n\n${formatThemePreview(theme)}`,
"info",
);
break;
}
case "create": {
const name = parts[1];
if (!name) {
ctx.ui.notify(
"Usage: /theme create <name>\n\n" +
"Creates a custom theme based on the current theme.\n" +
"Edit ~/.jae/theme.json to customize colors.",
"warning",
);
return;
}
const themeName = name.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
if (THEMES[themeName]) {
ctx.ui.notify(`\u274C Cannot override built-in theme '${themeName}'.`, "warning");
return;
}
const existing = config.custom.find((t) => t.name === themeName);
if (existing) {
ctx.ui.notify(`\u274C Custom theme '${themeName}' already exists. Edit ~/.jae/theme.json to modify.`, "warning");
return;
}
// Clone current theme as base
const base = getTheme(config.active, config) || THEMES.dragon;
const newTheme: Theme = {
name: themeName,
description: `Custom theme based on ${base.name}`,
colors: { ...base.colors },
};
config.custom.push(newTheme);
config.active = themeName;
saveConfig(config);
applyTheme(newTheme, ctx);
ctx.ui.notify(
`\u2705 Custom theme '${themeName}' created and activated!\n\n${formatThemePreview(newTheme)}\n\n\u{1F4DD} Edit colors in ~/.jae/theme.json`,
"info",
);
break;
}
case "reset": {
config.active = "dragon";
saveConfig(config);
applyTheme(THEMES.dragon, ctx);
ctx.ui.notify("\u2705 Theme reset to dragon (default).", "info");
break;
}
case "delete":
case "remove": {
const name = parts[1];
if (!name) {
ctx.ui.notify("Usage: /theme delete <name>", "warning");
return;
}
const themeName = name.toLowerCase();
if (THEMES[themeName]) {
ctx.ui.notify(`\u274C Cannot delete built-in theme '${themeName}'.`, "warning");
return;
}
const idx = config.custom.findIndex((t) => t.name === themeName);
if (idx === -1) {
ctx.ui.notify(`\u274C Custom theme '${themeName}' not found.`, "warning");
return;
}
config.custom.splice(idx, 1);
if (config.active === themeName) config.active = "dragon";
saveConfig(config);
ctx.ui.notify(`\u2705 Custom theme '${themeName}' deleted.`, "info");
break;
}
default:
ctx.ui.notify(
`\u{1F3A8} Theme System\n${'\u2500'.repeat(30)}\n\n` +
"Commands:\n" +
" /theme - Show current theme\n" +
" /theme list - List all themes\n" +
" /theme set <name> - Apply a theme\n" +
" /theme create <name>- Create custom theme\n" +
" /theme delete <name>- Delete custom theme\n" +
" /theme reset - Reset to dragon (default)",
"info",
);
}
},
});
}