Agent-JAE/default-extensions/project-dna.ts
jae 4c2f22bf3e
Some checks are pending
CI / build-check-test (push) Waiting to run
feat: add 16 default extensions for Agent JAE
New extensions shipping with Agent JAE:
- jae-rules: Per-project .jae/rules.md injection
- cost-tracker: Token/cost tracking with budget limits
- project-dna: Auto-analyze project fingerprint
- teach-mode: Pedagogical explanation mode
- bookmarks: File bookmark management
- replay: Session recording and asciicast export
- checkpoints: Git-based undo/time-travel
- pr-review: Autonomous PR diff review
- auto-docs: Auto-generate README and docs
- screenshot-context: Vision analysis for screenshots
- deploy: One-command deploy (Docker/Vercel/rsync/static)
- pair-programming: Real-time file watching with feedback
- dashboard: Terminal HUD with live stats
- swarm: Multi-agent parallel task execution
- skill-marketplace: Search/install/publish skills
- widget-api-demo: TUI widget API demonstration
2026-03-23 20:14:41 +01:00

139 lines
5.5 KiB
TypeScript

import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
export default function (pi: ExtensionAPI) {
pi.on("before_agent_start", async (_event, ctx) => {
const dnaPath = join(ctx.cwd, ".jae", "project-dna.md");
if (existsSync(dnaPath)) {
const dna = readFileSync(dnaPath, "utf-8").trim();
if (dna.length > 0) {
return { systemPrompt: `\n\n# Project DNA (auto-generated fingerprint)\n\n${dna}` };
}
}
});
pi.on("session_start", async (_event, ctx) => {
const dnaPath = join(ctx.cwd, ".jae", "project-dna.md");
if (!existsSync(dnaPath)) {
ctx.ui.notify("\u{1F9EC} No project DNA found. Run /dna to auto-analyze your project.", "info");
}
});
pi.registerCommand("dna", {
description: "Auto-analyze project and generate .jae/project-dna.md fingerprint",
handler: async (args, ctx) => {
const cwd = ctx.cwd;
const jaeDir = join(cwd, ".jae");
const dnaPath = join(jaeDir, "project-dna.md");
if (args.trim() === "show" && existsSync(dnaPath)) {
const dna = readFileSync(dnaPath, "utf-8");
ctx.ui.notify(`\u{1F9EC} Project DNA:\n${dna.substring(0, 1500)}`, "info");
return;
}
ctx.ui.notify("\u{1F50D} Analyzing project...", "info");
const sections: string[] = ["# Project DNA\n"];
// Tech stack detection
const checks = [
{ file: "package.json", label: "Node.js" },
{ file: "Cargo.toml", label: "Rust" },
{ file: "go.mod", label: "Go" },
{ file: "pyproject.toml", label: "Python (pyproject)" },
{ file: "requirements.txt", label: "Python (pip)" },
{ file: "Gemfile", label: "Ruby" },
{ file: "pom.xml", label: "Java (Maven)" },
{ file: "build.gradle", label: "Java/Kotlin (Gradle)" },
{ file: "docker-compose.yml", label: "Docker Compose" },
{ file: "Dockerfile", label: "Docker" },
{ file: ".github/workflows", label: "GitHub Actions" },
{ file: "tsconfig.json", label: "TypeScript" },
{ file: ".eslintrc.json", label: "ESLint" },
{ file: "vitest.config.ts", label: "Vitest" },
{ file: "jest.config.js", label: "Jest" },
{ file: "tailwind.config.js", label: "Tailwind CSS" },
{ file: "next.config.js", label: "Next.js" },
{ file: "vite.config.ts", label: "Vite" },
];
const detected: string[] = [];
for (const c of checks) {
if (existsSync(join(cwd, c.file))) detected.push(c.label);
}
sections.push(`## Tech Stack\n${detected.length > 0 ? detected.map((d) => `- ${d}`).join("\n") : "- Unknown"}\n`);
// Package.json details
const pkgPath = join(cwd, "package.json");
if (existsSync(pkgPath)) {
try {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
sections.push(
`## Package Info\n- Name: ${pkg.name || "unknown"}\n- Version: ${pkg.version || "unknown"}\n- Description: ${pkg.description || "none"}\n`,
);
if (pkg.dependencies) {
const deps = Object.keys(pkg.dependencies).slice(0, 20);
sections.push(`## Dependencies (top 20)\n${deps.map((d) => `- ${d}`).join("\n")}\n`);
}
if (pkg.scripts) {
const scripts = Object.entries(pkg.scripts).slice(0, 15) as [string, string][];
sections.push(`## Scripts\n${scripts.map(([k, v]) => `- \`${k}\`: ${v}`).join("\n")}\n`);
}
} catch {
/* ignore parse errors */
}
}
// Git info
try {
const gitResult = await pi.exec("git", ["log", "--oneline", "-10"], { cwd });
if (gitResult.code === 0 && gitResult.stdout.trim()) {
sections.push(`## Recent Git History\n\`\`\`\n${gitResult.stdout.trim()}\n\`\`\`\n`);
}
const branchResult = await pi.exec("git", ["branch", "--show-current"], { cwd });
if (branchResult.code === 0 && branchResult.stdout.trim()) {
sections.push(`## Current Branch: ${branchResult.stdout.trim()}\n`);
}
} catch {
/* not a git repo */
}
// File stats
try {
const findResult = await pi.exec(
"find",
[".", "-maxdepth", "3", "-type", "f", "-not", "-path", "*/node_modules/*", "-not", "-path", "*/.git/*", "-not", "-path", "*/dist/*", "-not", "-path", "*/.jae/*"],
{ cwd },
);
if (findResult.code === 0) {
const files = findResult.stdout.trim().split("\n").filter(Boolean);
sections.push(`## File Count: ${files.length}\n`);
const extCounts: Record<string, number> = {};
for (const f of files) {
const ext = f.includes(".") ? (f.split(".").pop() || "none") : "(no ext)";
extCounts[ext] = (extCounts[ext] || 0) + 1;
}
const sorted = Object.entries(extCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 15);
sections.push(`## File Types\n${sorted.map(([ext, count]) => `- .${ext}: ${count}`).join("\n")}\n`);
}
} catch {
/* ignore */
}
sections.push(`\n---\nGenerated: ${new Date().toISOString()}\n`);
const dnaContent = sections.join("\n");
if (!existsSync(jaeDir)) mkdirSync(jaeDir, { recursive: true });
writeFileSync(dnaPath, dnaContent, "utf-8");
ctx.ui.notify(
`\u{1F9EC} Project DNA generated (${dnaContent.length} chars). Saved to .jae/project-dna.md`,
"info",
);
},
});
}