import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { ReviewWriteResult } from "./core.ts"; const EXTENSION_NAME = "security-review-writer"; const TOOL_NAME = "security_review_writer"; type PlainTextComponent = { setText(value: string): void; render(width: number): string[]; invalidate(): void; }; function plainText(initial: string): PlainTextComponent { let value = initial; return { setText(next) { value = next; }, render(width) { return value.split("\n").map((line) => line.slice(0, Math.max(0, width))); }, invalidate() {}, }; } function component(previous: unknown): PlainTextComponent { return previous !== null && typeof previous === "object" && "setText" in previous ? previous as PlainTextComponent : plainText(""); } function redactedPathShape(path: string): { kind: "absolute" | "relative"; length: number } { return { kind: /^([A-Za-z]:[\\/]|[\\/])/.test(path) ? "absolute" : "relative", length: path.length }; } function errorCode(error: unknown): string { return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "UNEXPECTED"; } async function emit(ctx: ExtensionContext, event: string, durationMs: number, context: unknown): Promise { const { dynamicExtensionRevision, logDynamicExtensionEvent } = await import("./debug.ts"); await logDynamicExtensionEvent({ name: EXTENSION_NAME, revision: dynamicExtensionRevision(import.meta.url), ctx, event, durationMs, context, }).catch(() => undefined); } export default function dynamicExtension(pi: ExtensionAPI) { pi.registerTool({ name: TOOL_NAME, label: "Security Review Writer", description: "Atomically create or overwrite one UTF-8 text file beneath the current project's ai-workspace/security-reviews directory. Relative paths are review-root-relative; absolute paths must already be beneath that root.", parameters: { type: "object", properties: { path: { type: "string", description: "Review-root-relative or contained absolute target file path." }, text: { type: "string", description: "Complete UTF-8 text content to write atomically." }, }, required: ["path", "text"], additionalProperties: false, }, async execute(_id, params, signal, _onUpdate, ctx) { const started = performance.now(); try { const { writeSecurityReview } = await import("./core.ts"); const result = await writeSecurityReview({ cwd: ctx.cwd, path: params.path, text: params.text, signal }); await emit(ctx, "write_complete", performance.now() - started, { relativePath: result.relativePath, bytes: result.bytes, replaced: result.replaced, }); return { content: [{ type: "text" as const, text: `${result.replaced ? "Overwrote" : "Created"} ${result.relativePath} atomically (${result.bytes} UTF-8 bytes).`, }], details: result, }; } catch (error) { if (signal?.aborted || (error instanceof Error && error.name === "AbortError")) throw error; await emit(ctx, "write_failed", performance.now() - started, { path: redactedPathShape(params.path), code: errorCode(error), }); throw error; } }, renderCall(args, _theme, context) { const text = component(context.lastComponent); text.setText(`${TOOL_NAME} ${args.path}`); return text; }, renderResult(result, { expanded, isPartial }, _theme, context) { const text = component(context.lastComponent); if (isPartial) { text.setText("Writing review…"); return text; } const details = result.details as ReviewWriteResult | undefined; if (details?.ok) { let rendered = details.replaced ? "✓ Review overwritten" : "✓ Review created"; if (expanded) rendered += `\n${details.relativePath} · ${details.bytes} UTF-8 bytes · atomic rename`; text.setText(rendered); return text; } text.setText("Review write failed"); return text; }, }); pi.on("session_start", () => { const active = pi.getActiveTools(); if (!active.includes(TOOL_NAME)) pi.setActiveTools([...active, TOOL_NAME]); }); }