Agent-JAE/default-extensions/checkpoints.ts
jae a1b6e22c01 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

126 lines
4.3 KiB
TypeScript

import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
export default function (pi: ExtensionAPI) {
let checkpointCount = 0;
async function createCheckpoint(ctx: any, label: string): Promise<boolean> {
try {
// Check if in git repo
const statusResult = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
if (statusResult.code !== 0) return false;
// Only checkpoint if there are changes
if (!statusResult.stdout.trim()) return false;
// Stage all and create stash
await pi.exec("git", ["add", "-A"], { cwd: ctx.cwd });
checkpointCount++;
const tag = `jae-checkpoint-${checkpointCount}`;
const result = await pi.exec(
"git",
["stash", "push", "-m", `${tag}: ${label}`, "--include-untracked"],
{ cwd: ctx.cwd },
);
if (result.code === 0 && !result.stdout.includes("No local changes")) {
// Immediately pop to restore working state - the stash entry remains in reflog
await pi.exec("git", ["stash", "pop"], { cwd: ctx.cwd });
return true;
}
return false;
} catch {
return false;
}
}
// Auto-checkpoint on file mutations
pi.on("tool_execution_start", async (event, ctx) => {
const mutatingTools = ["write", "edit"];
if (mutatingTools.includes(event.toolName)) {
await createCheckpoint(ctx, `before ${event.toolName}`);
}
});
// Also checkpoint on bash commands that might modify files
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash") {
const cmd = (event.input as any)?.command || "";
const dangerousPatterns = ["rm ", "mv ", "cp ", "sed ", "chmod ", "chown ", "truncate", "dd "];
if (dangerousPatterns.some((p) => cmd.includes(p))) {
await createCheckpoint(ctx, `before bash: ${cmd.substring(0, 50)}`);
}
}
});
pi.registerCommand("undo", {
description: "Undo last file change by restoring from git stash checkpoint",
handler: async (_args, ctx) => {
try {
// List stashes to find jae checkpoints
const listResult = await pi.exec("git", ["stash", "list"], { cwd: ctx.cwd });
if (listResult.code !== 0) {
ctx.ui.notify("Not in a git repository.", "error");
return;
}
const stashes = listResult.stdout.trim().split("\n").filter(Boolean);
const jaeStashes = stashes.filter((s) => s.includes("jae-checkpoint-"));
if (jaeStashes.length === 0) {
ctx.ui.notify("No JAE checkpoints available to undo.", "warning");
return;
}
// Show what we're undoing
const latest = jaeStashes[0];
const confirmed = await ctx.ui.confirm(
"Undo Checkpoint",
`Restore to: ${latest}\n\nThis will discard current changes and restore the checkpoint.`,
);
if (!confirmed) {
ctx.ui.notify("Undo cancelled.", "info");
return;
}
// Hard reset and apply stash
const stashRef = latest.split(":")[0]; // e.g., "stash@{0}"
await pi.exec("git", ["checkout", "."], { cwd: ctx.cwd });
await pi.exec("git", ["clean", "-fd"], { cwd: ctx.cwd });
const applyResult = await pi.exec("git", ["stash", "apply", stashRef], { cwd: ctx.cwd });
if (applyResult.code === 0) {
ctx.ui.notify(`\u23EA Restored checkpoint: ${latest}`, "info");
} else {
ctx.ui.notify(`Failed to apply stash: ${applyResult.stderr}`, "error");
}
} catch (err) {
ctx.ui.notify(`Undo error: ${err}`, "error");
}
},
});
pi.registerCommand("checkpoints", {
description: "List available JAE checkpoints",
handler: async (_args, ctx) => {
const listResult = await pi.exec("git", ["stash", "list"], { cwd: ctx.cwd });
if (listResult.code !== 0) {
ctx.ui.notify("Not in a git repository.", "error");
return;
}
const stashes = listResult.stdout.trim().split("\n").filter(Boolean);
const jaeStashes = stashes.filter((s) => s.includes("jae-checkpoint-"));
if (jaeStashes.length === 0) {
ctx.ui.notify("No JAE checkpoints found.", "info");
return;
}
ctx.ui.notify(
`\u{1F4CC} JAE Checkpoints (${jaeStashes.length}):\n${jaeStashes.map((s) => ` ${s}`).join("\n")}`,
"info",
);
},
});
}