Agent-JAE/default-extensions/replay.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

127 lines
3.9 KiB
TypeScript

import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
interface ReplayEvent {
time: number;
type: string;
text: string;
}
export default function (pi: ExtensionAPI) {
let recording = false;
let events: ReplayEvent[] = [];
let startTime = 0;
pi.on("message_end", async (event, ctx) => {
if (!recording) return;
const msg = event.message;
if (!msg) return;
let text = "";
if ("content" in msg && typeof msg.content === "string") {
text = msg.content;
} else if ("content" in msg && Array.isArray(msg.content)) {
text = (msg.content as any[])
.filter((b: any) => b.type === "text")
.map((b: any) => b.text)
.join("");
}
if (text) {
events.push({
time: (Date.now() - startTime) / 1000,
type: (msg as any).role || "unknown",
text,
});
}
});
pi.on("tool_execution_end", async (event, _ctx) => {
if (!recording) return;
const resultText =
typeof event.result === "string"
? event.result
: JSON.stringify(event.result).substring(0, 500);
events.push({
time: (Date.now() - startTime) / 1000,
type: `tool:${event.toolName}`,
text: resultText,
});
});
pi.registerCommand("replay", {
description: "Session replay: /replay start | /replay stop | /replay export [file.cast]",
handler: async (args, ctx) => {
const parts = args.trim().split(/\s+/);
const sub = parts[0] || "status";
if (sub === "start") {
if (recording) {
ctx.ui.notify("Already recording.", "warning");
return;
}
recording = true;
events = [];
startTime = Date.now();
ctx.ui.setStatus("replay", "\u{1F534} REC");
ctx.ui.notify("\u{1F3AC} Replay recording started.", "info");
return;
}
if (sub === "stop") {
if (!recording) {
ctx.ui.notify("Not recording.", "warning");
return;
}
recording = false;
ctx.ui.setStatus("replay", undefined);
ctx.ui.notify(`\u{1F3AC} Recording stopped. ${events.length} events captured. Use /replay export to save.`, "info");
return;
}
if (sub === "export") {
if (events.length === 0) {
ctx.ui.notify("No events recorded. Start a recording first.", "warning");
return;
}
const filename = parts[1] || `jae-replay-${Date.now()}.cast`;
const outPath = join(ctx.cwd, filename);
// asciicast v2 format
const header = JSON.stringify({
version: 2,
width: 120,
height: 40,
timestamp: Math.floor(startTime / 1000),
title: "JAE Session Replay",
env: { TERM: "xterm-256color" },
});
const lines = [header];
for (const ev of events) {
const prefix = ev.type === "assistant" ? "\x1b[36m" : ev.type.startsWith("tool:") ? "\x1b[33m" : "\x1b[37m";
const label = ev.type === "assistant" ? "JAE" : ev.type === "user" ? "YOU" : ev.type;
const displayText = `${prefix}[${label}]\x1b[0m ${ev.text}\r\n`;
lines.push(JSON.stringify([ev.time, "o", displayText]));
}
writeFileSync(outPath, lines.join("\n") + "\n", "utf-8");
ctx.ui.notify(`\u{1F3AC} Exported ${events.length} events to ${outPath}\nPlay with: asciinema play ${filename}`, "info");
return;
}
if (sub === "status") {
ctx.ui.notify(
recording
? `\u{1F534} Recording in progress: ${events.length} events (${((Date.now() - startTime) / 1000).toFixed(0)}s)`
: `\u{1F3AC} Not recording. ${events.length} events in buffer.`,
"info",
);
return;
}
ctx.ui.notify("Usage: /replay start | stop | export [file.cast] | status", "info");
},
});
}