import { StringEnum } from "@earendil-works/pi-ai"; import { withFileMutationQueue, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import type { DocxCommitRequest, DocxDiffRequest, DocxEditRequest, DocxInspectRequest, DocxReadRequest, DocxRenderRequest, DocxValidateRequest } from "./src/contracts.ts"; import { ARTIFACT_SCHEMA, DOCX_CONTRACT_VERSION } from "./src/contracts.ts"; import { CommitSchema, DiffSchema, EditSchema, InspectSchema, ReadSchema, RenderSchema, ValidateSchema } from "./src/schemas.ts"; import { DocumentService } from "./src/backends/document-service.ts"; import { boundedJsonResult, renderImageResult, visibleLimitFrom } from "./src/output.ts"; import { DocxError } from "./src/errors.ts"; import { docxDoctorReport, formatDoctorReport } from "./src/doctor.ts"; import { loadRevision } from "./src/core/workspace.ts"; import { compactRenderCall, compactRenderResult } from "./src/tools/render.ts"; const GUIDELINES = [ "Use docx_inspect before editing an unfamiliar Word document and retain sourceSha256 for preconditions.", "Use docx_read for focused semantic blocks and stable selectors; never use page numbers as edit selectors.", "Use docx_render when pagination, tables, images, or formatting matter; rendering never authorizes an edit.", "Use docx_edit with dryRun=true first, then dryRun=false to create a private staged revision; docx_edit never commits a destination.", "Use docx_validate and docx_diff before docx_commit; save to a new .docx unless the user explicitly requested source overwrite.", "Never use DOCX tools to execute macros, refresh fields/links, or mutate signed/macro-enabled files.", ]; function progress(onUpdate: ((result: { content: Array<{ type: "text"; text: string }>; details: Record }) => void) | undefined, text: string): void { onUpdate?.({ content: [{ type: "text", text }], details: {} }); } const renderers = (label: string) => ({ renderCall: compactRenderCall(label), renderResult: compactRenderResult }); export default function docxExtension(pi: ExtensionAPI): void { pi.registerTool({ name: "docx_inspect", label: "Inspect DOCX", description: "Inventory bounded DOCX semantics, stories, package parts, protected/active/signed content, external relationships, hashes, engines, and operation capabilities.", promptSnippet: "Inspect DOCX structure, features, integrity, security risks, engines, and capabilities before editing.", promptGuidelines: GUIDELINES, parameters: InspectSchema, ...renderers("Inspect DOCX"), async execute(_id, raw, signal, onUpdate, ctx) { const params = raw as DocxInspectRequest; progress(onUpdate, "Inspecting DOCX package, relationships, stories, and capabilities…"); return boundedJsonResult(await new DocumentService(ctx.cwd).inspect(params, signal), "docx-inspect", visibleLimitFrom(params)); } }); pi.registerTool({ name: "docx_read", label: "Read DOCX", description: "Read bounded semantic DOCX content by story, stable selector, or exact cross-run search without exposing raw OOXML.", promptSnippet: "Read focused DOCX stories, paragraphs, tables, bookmarks, controls, comments, or exact cross-run text.", promptGuidelines: GUIDELINES, parameters: ReadSchema, ...renderers("Read DOCX"), async execute(_id, raw, signal, onUpdate, ctx) { const params = raw as DocxReadRequest; progress(onUpdate, "Resolving stable selectors and reading focused semantic blocks…"); return boundedJsonResult(await new DocumentService(ctx.cwd).read(params, signal), "docx-read", visibleLimitFrom(params)); } }); pi.registerTool({ name: "docx_render", label: "Render DOCX", description: "Safely render selected DOCX pages through isolated ONLYOFFICE or LibreOffice PDF export and LiteParse/PDFium PNG screenshots, returning image blocks and fidelity metadata.", promptSnippet: "Render selected DOCX pages to PNG for visual inspection in TUI and WebUI.", promptGuidelines: GUIDELINES, parameters: RenderSchema, ...renderers("Render DOCX"), async execute(_id, raw, signal, onUpdate, ctx) { const params = raw as DocxRenderRequest; progress(onUpdate, "Rendering a private DOCX copy with macros and link updates disabled…"); return renderImageResult(await new DocumentService(ctx.cwd).render(params, signal)); } }); pi.registerTool({ name: "docx_edit", label: "Stage DOCX Edit", description: "Dry-run or transactionally apply versioned P1 DOCX operations to a private staged revision with exact preconditions and protected-part gates; never writes a destination.", promptSnippet: "Dry-run or stage transactional DOCX edits without changing source or destination files.", promptGuidelines: GUIDELINES, parameters: EditSchema, ...renderers("Stage DOCX Edit"), async execute(_id, raw, signal, onUpdate, ctx) { const params = { ...(raw as DocxEditRequest), schemaVersion: (raw as DocxEditRequest).schemaVersion ?? DOCX_CONTRACT_VERSION }; progress(onUpdate, params.dryRun === false ? "Applying operations to a private staged copy and validating it…" : "Resolving selectors and validating the edit plan without mutation…"); return boundedJsonResult(await new DocumentService(ctx.cwd).edit(params, signal), "docx-edit", visibleLimitFrom(params)); } }); pi.registerTool({ name: "docx_diff", label: "Diff DOCX", description: "Compare DOCX semantic text, run/paragraph formatting, tables, relationships, OOXML part hashes, and protected content with bounded output.", promptSnippet: "Diff DOCX semantics, formatting, relationships, package parts, and protected content.", promptGuidelines: GUIDELINES, parameters: DiffSchema, ...renderers("Diff DOCX"), async execute(_id, raw, signal, onUpdate, ctx) { const params = raw as DocxDiffRequest; progress(onUpdate, "Comparing semantic snapshots and OOXML part manifests…"); return boundedJsonResult(await new DocumentService(ctx.cwd).diff(params, signal), "docx-diff", visibleLimitFrom(params)); } }); pi.registerTool({ name: "docx_validate", label: "Validate DOCX", description: "Validate ZIP/package structure, content types, relationships, Open XML schema, independent reopen, preservation, semantics, and optional render gates.", promptSnippet: "Validate DOCX integrity, Open XML schema, preservation contracts, and optional page rendering.", promptGuidelines: GUIDELINES, parameters: ValidateSchema, ...renderers("Validate DOCX"), async execute(_id, raw, signal, onUpdate, ctx) { const params = raw as DocxValidateRequest; progress(onUpdate, "Running package, schema, reopen, preservation, and configured render gates…"); return boundedJsonResult(await new DocumentService(ctx.cwd).validate(params, signal), "docx-validate", visibleLimitFrom(params)); } }); pi.registerTool({ name: "docx_commit", label: "Commit DOCX Revision", description: "Commit one validated staged revision to a new .docx by default, or to an explicitly confirmed overwrite destination with queued hash checks, durable replacement, and recovery data.", promptSnippet: "Commit a validated staged DOCX revision with source/destination hash protection and atomic save-as behavior.", promptGuidelines: GUIDELINES, parameters: CommitSchema, ...renderers("Commit DOCX"), async execute(_id, raw, signal, onUpdate, ctx) { const params = raw as DocxCommitRequest; if (params.inPlace) { if (!ctx.hasUI || ctx.mode === "print" || ctx.mode === "json") throw new DocxError("PERMISSION_DENIED", "Source overwrite is refused without an interactive TUI/RPC confirmation."); const revision = await loadRevision(params.revisionId), confirmed = await ctx.ui.confirm("Overwrite original DOCX?", `Replace ${revision.sourcePath}? A timestamped recovery copy will be retained.`); if (!confirmed) throw new DocxError("PERMISSION_DENIED", "Source overwrite was not confirmed."); } progress(onUpdate, "Rechecking source, destination, staged bytes, and validation inside the destination mutation queue…"); return boundedJsonResult(await new DocumentService(ctx.cwd, withFileMutationQueue).commit(params, signal), "docx-commit"); } }); pi.registerCommand("docx-doctor", { description: "Report DOCX sidecar, .NET, ONLYOFFICE/LibreOffice, LiteParse, platform, and workspace diagnostics.", handler: async (_args, ctx) => { const report = await docxDoctorReport(); ctx.ui.notify(formatDoctorReport(report), report.ok ? "info" : "warning"); } }); pi.on("tool_call", (event) => { if (event.toolName !== "edit" && event.toolName !== "write") return; const input = event.input as { path?: unknown }; if (typeof input.path === "string" && /\.(?:docx|docm|dotx|dotm)$/i.test(input.path.replace(/^@+/, ""))) return { block: true, reason: `Built-in ${event.toolName} is text-only and cannot safely mutate OOXML. Use docx_inspect/docx_edit/docx_commit.` }; }); } export { DocumentService } from "./src/backends/document-service.ts"; export { OpenXmlSidecar } from "./src/backends/openxml-sidecar.ts"; export { OoxmlPackage } from "./src/ooxml/package.ts"; export { DOCX_CONTRACT_VERSION, ARTIFACT_SCHEMA } from "./src/contracts.ts"; export const DocxBackendSchema = StringEnum(["openxml-sidecar", "typescript-reader", "libreoffice-renderer", "liteparse-reader"] as const); export const DocxContractSchema = Type.Literal(DOCX_CONTRACT_VERSION);