import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import type { UpdateDocParams, DocgraphToolDetails } from "../types.js"; import { readFileSafe, writeFileSafe, parseMetadata, renderMetadata, isInitialized, contentText, } from "../utils.js"; const VALID_FIELDS = [ "purpose", "audience", "dependsOn", "referencedBy", "body", ] as const; export function registerDocgraphUpdate(pi: ExtensionAPI): void { pi.registerTool({ name: "docgraph_update", label: "Docgraph Update", description: "Update a specific field in a managed documentation file. Fields: purpose, audience (Human|AI|Both), dependsOn (comma-separated repo-root-relative paths), referencedBy (comma-separated repo-root-relative paths), body (appended or replaced).", parameters: Type.Object({ path: Type.String({ description: "Document path to update", }), field: Type.String({ description: `Field to update: ${VALID_FIELDS.join(", ")}`, }), value: Type.String({ description: "New value. For dependsOn/referencedBy, provide comma-separated canonical repo-root-relative paths (e.g. 'docs/SPEC.md'); hrefs are rendered relative to the document. For body, the new body content.", }), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { const { path, field, value } = params as UpdateDocParams; if (!isInitialized(ctx)) { return { content: [ { type: "text", text: "Documentation not initialized. Run `docgraph_init` first.", }, ], details: { action: "update", error: "not_initialized", state: { initialized: false, schemaVersion: 1 }, } as DocgraphToolDetails, }; } if (!VALID_FIELDS.includes(field as (typeof VALID_FIELDS)[number])) { return { content: [ { type: "text", text: `Invalid field: ${field}. Valid fields: ${VALID_FIELDS.join(", ")}`, }, ], details: { action: "update", error: "invalid_field", state: { initialized: true, schemaVersion: 1 }, } as DocgraphToolDetails, }; } const raw = readFileSafe(path, ctx.cwd); if (raw === null) { return { content: [ { type: "text", text: `Document not found: ${path}`, }, ], details: { action: "update", error: "not_found", path, state: { initialized: true, schemaVersion: 1 }, } as DocgraphToolDetails & { path: string }, }; } const metadata = parseMetadata(raw, path); const today = new Date().toISOString().slice(0, 10); // Apply the field update to metadata switch (field) { case "purpose": metadata.purpose = value; break; case "audience": { const v = value.toLowerCase(); if (v.includes("both")) metadata.audience = "Both"; else if (v.includes("ai")) metadata.audience = "AI"; else metadata.audience = "Human"; break; } case "dependsOn": metadata.dependsOn = value .split(",") .map((s) => s.trim()) .filter(Boolean); break; case "referencedBy": metadata.referencedBy = value .split(",") .map((s) => s.trim()) .filter(Boolean); break; } metadata.lastUpdated = today; // Rebuild the document if (field === "body") { // Replace the entire body (everything after metadata block) const newMeta = renderMetadata(metadata, path); const title = raw.split("\n")[0] ?? ""; const newContent = `${title}\n\n${newMeta}\n\n${value}`; const ok = writeFileSafe(path, newContent, ctx.cwd); return { content: [ { type: "text", text: ok ? `Updated body of \`${path}\`` : `Failed to write ${path}`, }, ], details: { action: "update", path, field, success: ok, state: { initialized: true, schemaVersion: 1 }, } as DocgraphToolDetails & { path: string; field: string; success: boolean; }, }; } // For metadata-only updates, rewrite just the metadata block const lines = raw.split("\n"); // Find metadata block bounds let metaStart = -1; let metaEnd = -1; for (let i = 0; i < lines.length; i++) { if (lines[i]!.startsWith("> **") && metaStart === -1) { metaStart = i; } if ( metaStart !== -1 && metaEnd === -1 && !lines[i]!.startsWith(">") && i > metaStart ) { metaEnd = i; break; } } if (metaEnd === -1) metaEnd = lines.length; const newMeta = renderMetadata(metadata, path); const before = lines.slice(0, metaStart); const after = lines.slice(metaEnd); const newContent = [...before, newMeta, ...after].join("\n"); const ok = writeFileSafe(path, newContent, ctx.cwd); return { content: [ { type: "text", text: ok ? `Updated \`${field}\` in \`${path}\`` : `Failed to write ${path}`, }, ], details: { action: "update", path, field, success: ok, state: { initialized: true, schemaVersion: 1 }, } as DocgraphToolDetails & { path: string; field: string; success: boolean; }, }; }, renderCall(args, theme) { return new Text( theme.fg("toolTitle", theme.bold("docgraph-update ")) + theme.fg("muted", `${args.path}:${args.field}`), 0, 0, ); }, renderResult(result, _opts, theme) { const text = contentText(result.content?.[0]); return new Text( theme.fg("muted", text), 0, 0, ); }, }); }