import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { savePlaybookDraft } from "./draft-save.js"; import { createRecordSession, markSkill, recordBranch, recordSessionToDefinition, renderRecordStatus, validatePlaybookId, } from "./record.js"; import { clearActiveRecordSession, loadActiveRecordSession, saveRecordSession, setActiveRecordSession, } from "./record-state.js"; import { normalizeSkillCommandName } from "./validation.js"; type RecordUi = { notify(message: string, level: "info" | "warning" | "error"): void; select?(title: string, options: string[]): Promise; confirm?(title: string, message: string): Promise; }; type RecordContext = { cwd: string; hasUI: boolean; ui?: RecordUi; }; export const RECORD_COMMANDS = [ ["record:start", "start recording an explicit skill flow"], ["record:mark", "mark explicit skill usage in the active recording"], ["record:branch", "record a branch outcome label"], ["record:stop", "stop recording and save a playbook draft"], ["record:status", "show active recording status"], ] as const; export function recordUsage(): string { return [ "Usage:", "/playbook:record:start [--name ]", "/playbook:record:mark []", "/playbook:record:branch [--to ]", "/playbook:record:stop", "/playbook:record:status", ].join("\n"); } export async function handleRecordCommand( pi: ExtensionAPI, command: string, args: string, ctx: RecordContext, ): Promise { const ui = ctx.hasUI ? ctx.ui : undefined; const tokens = tokenize(args); switch (command) { case "record": if (tokens.length === 0) { await showRecordStatus(ctx.cwd, ui); notify(ui, recordUsage(), "info"); return; } notify(ui, recordUsage(), "error"); return; case "record:start": await startRecording(tokens, ctx.cwd, ui); return; case "record:mark": await markRecordingSkill(pi, tokens, ctx.cwd, ui); return; case "record:branch": await branchRecording(tokens, ctx.cwd, ui); return; case "record:stop": await stopRecording(pi, ctx.cwd, ui); return; case "record:status": await showRecordStatus(ctx.cwd, ui); return; default: notify(ui, recordUsage(), "error"); } } async function startRecording(tokens: string[], cwd: string, ui: RecordUi | undefined): Promise { const playbookId = tokens[0]; if (!playbookId) { notify(ui, "Playbook id is required. Example: /playbook:record:start my-recorded-flow", "error"); return; } validatePlaybookId(playbookId); const nameFlag = tokens.indexOf("--name"); const playbookName = nameFlag >= 0 ? tokens.slice(nameFlag + 1).join(" ").trim() : titleCase(playbookId); if (nameFlag >= 0 && !playbookName) { notify(ui, "Display name is required after --name.", "error"); return; } const existing = await loadActiveRecordSession(cwd); if (existing) { notify(ui, `Recording already active (${existing.playbookId}). Run /playbook:record:stop first.`, "warning"); return; } const session = createRecordSession(playbookId, playbookName); await saveRecordSession(cwd, session); await setActiveRecordSession(cwd, session.sessionId); notify(ui, [`Started recording ${playbookId}.`, ...renderRecordStatus(session)].join("\n"), "info"); } async function markRecordingSkill( pi: ExtensionAPI, tokens: string[], cwd: string, ui: RecordUi | undefined, ): Promise { const session = await requireActiveSession(cwd, ui); if (!session) return; const availableSkills = getAvailableSkills(pi); let skillName = tokens[0]; if (!skillName) { const selected = await pickSkill(pi, ui); if (!selected) return; skillName = selected; } else { skillName = normalizeSkillCommandName(skillName); if (!availableSkills.has(skillName)) { notify(ui, `Unknown Agent Skill '${skillName}'.`, "error"); return; } } const updated = markSkill(session, skillName); await saveRecordSession(cwd, updated); notify(ui, [`Marked skill '${skillName}' on step '${updated.currentStepId}'.`, ...renderRecordStatus(updated)].join("\n"), "info"); } async function branchRecording(tokens: string[], cwd: string, ui: RecordUi | undefined): Promise { const session = await requireActiveSession(cwd, ui); if (!session) return; const outcome = tokens[0]; if (!outcome) { notify(ui, "Outcome label is required. Example: /playbook:record:branch ready-for-prd", "error"); return; } const toFlag = tokens.indexOf("--to"); const toStepId = toFlag >= 0 ? tokens[toFlag + 1] : undefined; const updated = recordBranch(session, outcome, toStepId); await saveRecordSession(cwd, updated); notify(ui, [`Recorded branch outcome '${outcome}'.`, ...renderRecordStatus(updated)].join("\n"), "info"); } async function stopRecording(pi: ExtensionAPI, cwd: string, ui: RecordUi | undefined): Promise { const session = await requireActiveSession(cwd, ui); if (!session) return; const definition = recordSessionToDefinition(session); const saved = await savePlaybookDraft(cwd, definition, getAvailableSkills(pi), ui, { sourceLabel: "Recorded" }); if (saved) { await clearActiveRecordSession(cwd); notify(ui, `Recording ${session.playbookId} converted to playbook draft.`, "info"); } } async function showRecordStatus(cwd: string, ui: RecordUi | undefined): Promise { const session = await loadActiveRecordSession(cwd); if (!session) { notify(ui, "No active recording. Start one with /playbook:record:start .", "info"); return; } notify(ui, renderRecordStatus(session).join("\n"), "info"); } async function requireActiveSession(cwd: string, ui: RecordUi | undefined) { const session = await loadActiveRecordSession(cwd); if (!session) { notify(ui, "No active recording. Start one with /playbook:record:start .", "error"); return undefined; } return session; } async function pickSkill(pi: ExtensionAPI, ui: RecordUi | undefined): Promise { const skills = [...getAvailableSkills(pi)].sort(); if (skills.length === 0) { notify(ui, "No Agent Skills are available to mark.", "error"); return undefined; } if (!ui?.select) { notify(ui, "Skill name is required when selection UI is unavailable. Example: /playbook:record:mark grill-with-docs", "error"); return undefined; } const selected = await ui.select("Mark which skill?", skills); return selected ? normalizeSkillCommandName(selected) : undefined; } function getAvailableSkills(pi: ExtensionAPI): ReadonlySet { const skills = pi.getCommands() .filter((command) => command.source === "skill") .map((command) => normalizeSkillCommandName(command.name)); return new Set(skills); } function tokenize(args: string): string[] { return args.trim().split(/\s+/).filter(Boolean); } function titleCase(slug: string): string { return slug .split("-") .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function notify(ui: RecordUi | undefined, message: string, level: "info" | "warning" | "error"): void { ui?.notify(message, level); }