import { dirname, resolve } from "node:path"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "typebox"; import { scheduleReindex } from "./indexing.js"; import { createKnowledgeDocument, writeKnowledgeDocumentFile } from "./knowledge-document.js"; import { appendEvent, rebuildMetadataLight } from "./metadata.js"; import type { Runtime } from "./runtime.js"; import { type VaultPaths, fmtDate, resolveVaultPaths } from "./utils.js"; import { assertWritableVault, inspectWritableVault } from "./vault-format.js"; // ─── Public API ──────────────────────────────────────── export interface RetroResult { slug: string; sourcePagePath: string; } /** * Save an atomic insight into the wiki as a single markdown file. * * Unlike wiki_capture_source (which creates a full source packet with * manifest.json, extracted.md, and attachments), this is a lightweight * path for quick knowledge capture — one file, one call. * * The 4-layer pipeline (raw → source pages → canonical pages → metadata) * is still available via wiki_capture_source → wiki_ingest for deep research. */ function insightPath(paths: VaultPaths, slug: string): string { if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug) || slug === "index" || slug === "log") { throw new Error(`Invalid insight slug: ${slug}`); } const directory = resolve(paths.wiki, "sources"); const target = resolve(directory, `${slug}.md`); if (dirname(target) !== directory) throw new Error(`Invalid insight slug: ${slug}`); return target; } export function saveInsight( paths: VaultPaths, slug: string, title: string, body: string, category?: string, opts?: { rebuild?: boolean }, ): RetroResult { assertWritableVault(paths); const today = fmtDate(); const sourcePagePath = insightPath(paths, slug); const pageBody = `# ${title} ${body} ${category ? `*Category: ${category}*` : ""} --- *Captured: ${today}* ## Related _Add links to related pages._`; const doc = createKnowledgeDocument( `sources/${slug}.md`, { type: "source", title, slug, status: "insight", created: today, updated: today, ...(category ? { category } : {}), }, pageBody, ); writeKnowledgeDocumentFile(sourcePagePath, doc); // Log event appendEvent(paths, { kind: "retro", slug, title, category: category || "uncategorized", }); // Rebuild metadata so the insight is immediately searchable. The wiki_retro // tool passes { rebuild: false } and schedules a non-blocking reindex instead. if (opts?.rebuild !== false) rebuildMetadataLight(paths); return { slug, sourcePagePath }; } // ─── Tool Registration ────────────────────────────────── /** * Register the `wiki_retro` tool. * The model calls this to save an atomic insight from a completed task. * Inspired by the memex_retro pattern. */ export function registerWikiRetro(pi: ExtensionAPI, runtime?: Runtime): void { pi.registerTool({ name: "wiki_retro", label: "Wiki Retro", description: "Save an atomic insight from a completed task into the wiki. " + "Creates a source packet and source page. The insight will be " + "surfaced automatically by wiki_recall in future sessions.", promptSnippet: "Save atomic insights from completed tasks into the wiki", promptGuidelines: [ "Use wiki_retro at the END of every meaningful task to save what you learned.", "Write atomic insights — one insight per call. Use multiple calls for multiple insights.", "The insight will be auto-surfaced by wiki_recall in future sessions.", ], parameters: Type.Object({ slug: Type.String({ description: "Unique kebab-case identifier (e.g. 'jwt-revocation-pattern'). Used for lookups.", }), title: Type.String({ description: "Short descriptive title (60 chars max). Noun phrase, not a sentence.", }), body: Type.String({ description: "Markdown body with [[wikilinks]] to related wiki pages. Explain what was learned.", }), category: Type.Optional( Type.String({ description: "Optional category (e.g. frontend, architecture, devops, bugfix, design)", }), ), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const paths = resolveVaultPaths(ctx.cwd ?? process.cwd()); const vaultCheck = inspectWritableVault(paths); if (!vaultCheck.ok) { return { content: [ { type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}`, }, ], details: { error: vaultCheck.diagnostics[0].code, diagnostics: vaultCheck.diagnostics, } as Record, isError: true, }; } let result: RetroResult; try { result = saveInsight(paths, params.slug, params.title, params.body, params.category, { rebuild: !runtime, }); } catch (error: unknown) { if ((error as Error).message.startsWith("Invalid insight slug:")) { return { content: [{ type: "text", text: (error as Error).message }], details: { error: "invalid_insight_slug" } as Record, isError: true, }; } throw error; } if (runtime) { scheduleReindex(runtime, { hasUI: ctx.hasUI, ui: ctx.ui }, paths); } return { content: [ { type: "text", text: [ `🧠 **Insight saved**: ${params.title}`, "", `- Page: \`${result.sourcePagePath}\``, "", "This insight will be auto-surfaced by wiki_recall in future sessions.", ].join("\n"), }, ], details: { slug: params.slug, title: params.title, category: params.category || null, } as Record, }; }, }); }