import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { generateKeyPair } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; const generateKeyPairAsync = promisify(generateKeyPair); const DEFAULT_IDENTITY_DIR = ".zenbin"; const PRIVATE_JWK_NAME = "identity.private.jwk"; const PUBLIC_JWK_NAME = "identity.public.jwk"; async function fileExists(path: string): Promise { try { await readFile(path); return true; } catch { return false; } } async function ensureIdentity(cwd: string, force = false) { const dir = join(cwd, DEFAULT_IDENTITY_DIR); const privatePath = join(dir, PRIVATE_JWK_NAME); const publicPath = join(dir, PUBLIC_JWK_NAME); if (!force && (await fileExists(privatePath)) && (await fileExists(publicPath))) { return { privatePath, publicPath, created: false }; } await mkdir(dirname(privatePath), { recursive: true }); const { publicKey, privateKey } = (await generateKeyPairAsync("ed25519", {})) as { publicKey: import("node:crypto").KeyObject; privateKey: import("node:crypto").KeyObject; }; const privateJwk = privateKey.export({ format: "jwk" }); const publicJwk = publicKey.export({ format: "jwk" }); await writeFile(privatePath, `${JSON.stringify(privateJwk, null, 2)}\n`, { mode: 0o600 }); await writeFile(publicPath, `${JSON.stringify(publicJwk, null, 2)}\n`, { mode: 0o644 }); return { privatePath, publicPath, created: true }; } export default function zenbinExtension(pi: ExtensionAPI) { pi.registerCommand("zenbin-identity", { description: "Create or show the local ZenBin JWK identity files", handler: async (args, ctx) => { const force = args.trim() === "--force"; const result = await ensureIdentity(ctx.cwd, force); ctx.ui.notify( result.created ? `Created ZenBin identity: ${result.publicPath}` : `ZenBin identity already exists: ${result.publicPath}`, "info", ); }, }); pi.registerCommand("zenbin-publish", { description: "Load the ZenBin publishing workflow skill", handler: async (args, ctx) => { await ensureIdentity(ctx.cwd); pi.sendUserMessage(`/skill:zenbin-publishing ${args}`.trim()); }, }); pi.registerCommand("zenbin-brain", { description: "Load the ZenBin brain workflow skill", handler: async (args) => { pi.sendUserMessage(`/skill:zenbin-brain ${args}`.trim()); }, }); pi.registerCommand("zenbin-message", { description: "Load the ZenBin messaging workflow skill", handler: async (args, ctx) => { await ensureIdentity(ctx.cwd); pi.sendUserMessage(`/skill:zenbin-messaging ${args}`.trim()); }, }); pi.registerTool({ name: "zenbin_identity", label: "ZenBin Identity", description: "Create or locate the local ZenBin JWK identity files", promptSnippet: "Create or locate ZenBin private/public JWK identity files", promptGuidelines: [ "Use zenbin_identity before ZenBin publishing or messaging if no identity exists.", "Never print or expose the ZenBin private JWK contents unless the user explicitly asks.", ], parameters: Type.Object({ force: Type.Optional(Type.Boolean({ description: "Regenerate identity even if one exists" })), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const result = await ensureIdentity(ctx.cwd, params.force === true); const publicJwk = JSON.parse(await readFile(result.publicPath, "utf8")); return { content: [ { type: "text", text: `${result.created ? "Created" : "Found"} ZenBin identity. Public JWK is ready for registration.`, }, ], details: { privatePath: result.privatePath, publicPath: result.publicPath, publicJwk, created: result.created, }, }; }, }); }