@bendyline/docblocks-cli 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +651 -116
  2. package/package.json +7 -7
package/dist/bin.js CHANGED
@@ -869,6 +869,9 @@ async function withMaterializationLock(target, operation) {
869
869
  if (materializationTails.get(key) === tail) materializationTails.delete(key);
870
870
  }
871
871
  }
872
+ function authorityError(message, hint) {
873
+ return Object.assign(new Error(message), { code: "unauthorized-path", hint });
874
+ }
872
875
  var DEFAULT_MAX_INPUT_FILE_BYTES, MCP_FILE_AUTHORITY_LIMITS, MAX_PATH_CHARACTERS, MATERIALIZATION_WRITE_CHUNK_BYTES, HASH_READ_CHUNK_BYTES, materializationTails, McpFileAuthority;
873
876
  var init_authority = __esm({
874
877
  "src/mcp/authority.ts"() {
@@ -947,7 +950,12 @@ var init_authority = __esm({
947
950
  /** Resolve a root-relative read without ever treating the alias as new authority. */
948
951
  async authorizeRootRead(rootId, workspacePath) {
949
952
  const root = this.readRoots.find((candidate2) => candidate2.id === rootId);
950
- if (!root) throw new Error("Unknown or unreadable MCP root");
953
+ if (!root) {
954
+ throw authorityError(
955
+ "Unknown or unreadable MCP root",
956
+ "Call list_roots and copy a returned read-enabled root id. If no roots are listed, use an inline markdown or bundle source, or restart the server with --allow-read."
957
+ );
958
+ }
951
959
  const candidate = path7.join(root.physical, ...parseRootRelativePath(workspacePath));
952
960
  const physical = await realpath4(candidate).catch(() => null);
953
961
  if (!physical || !isPathInside(root.physical, physical)) {
@@ -958,7 +966,12 @@ var init_authority = __esm({
958
966
  /** Resolve a root-relative write without ever treating the alias as new authority. */
959
967
  async authorizeRootWrite(rootId, workspacePath) {
960
968
  const root = this.writeRoots.find((candidate2) => candidate2.id === rootId);
961
- if (!root) throw new Error("Unknown or unwritable MCP root");
969
+ if (!root) {
970
+ throw authorityError(
971
+ "Unknown or unwritable MCP root",
972
+ "Call list_roots and copy a returned write-enabled root id. If none is writable, keep the result as a session artifact or restart the server with --allow-write."
973
+ );
974
+ }
962
975
  const candidate = path7.join(root.physical, ...parseRootRelativePath(workspacePath));
963
976
  const authorized = await this.authorizeWrite(candidate);
964
977
  const physicalParent = await realpath4(path7.dirname(authorized));
@@ -1537,7 +1550,10 @@ var init_document_service = __esm({
1537
1550
  const { parseMarkdown: parseMarkdown2 } = await import("@bendyline/squisq/markdown");
1538
1551
  const { markdownToDoc: markdownToDoc2 } = await import("@bendyline/squisq/doc");
1539
1552
  const markdownDoc = parseMarkdown2(markdown);
1540
- const doc = markdownToDoc2(markdownDoc);
1553
+ const doc = markdownToDoc2(markdownDoc, {
1554
+ defaultTemplate: "content",
1555
+ autoTemplates: false
1556
+ });
1541
1557
  const assets = await this.assetSummaries(container, signal);
1542
1558
  throwIfAborted5(signal);
1543
1559
  return {
@@ -1776,7 +1792,8 @@ async function inspectPreparedDocument(_documents, prepared, maximumBlocks = DEF
1776
1792
  ...validation.diagnostics.map(
1777
1793
  (diagnostic) => mapDocDiagnostic(diagnostic, prepared.sourceFormat)
1778
1794
  ),
1779
- ...missingAltTextDiagnostics(prepared.markdownDoc, prepared.sourceFormat)
1795
+ ...missingAltTextDiagnostics(prepared.markdownDoc, prepared.sourceFormat),
1796
+ ...await authoringDiagnostics(prepared, "inspect", prepared.sourceFormat, signal)
1780
1797
  ];
1781
1798
  let wordCount = 0;
1782
1799
  for (let index = 0; index < analyzed.length; index += 1) {
@@ -1833,6 +1850,12 @@ async function validatePreparedDocument(_documents, prepared, targetFormat, sign
1833
1850
  prepared.markdownDoc,
1834
1851
  normalizedTargetFormat ?? prepared.sourceFormat
1835
1852
  ),
1853
+ ...await authoringDiagnostics(
1854
+ prepared,
1855
+ "validate",
1856
+ normalizedTargetFormat ?? prepared.sourceFormat,
1857
+ signal
1858
+ ),
1836
1859
  ...normalizedTargetFormat ? await targetPreflight(prepared, normalizedTargetFormat, signal) : []
1837
1860
  ];
1838
1861
  await yieldForCancellation(signal);
@@ -1849,6 +1872,111 @@ async function validatePreparedDocument(_documents, prepared, targetFormat, sign
1849
1872
  diagnostics: unique
1850
1873
  };
1851
1874
  }
1875
+ async function authoringDiagnostics(prepared, stage, format, signal) {
1876
+ throwIfAborted5(signal);
1877
+ const {
1878
+ TEMPLATE_AUTHORING_METADATA,
1879
+ flattenBlocks,
1880
+ getBlockBodyText,
1881
+ materializeBlockLayers,
1882
+ resolveTemplateName,
1883
+ resolveThemeForDoc
1884
+ } = await import("@bendyline/squisq/doc");
1885
+ const blocks = flattenBlocks(prepared.doc.blocks);
1886
+ const analyzedBlocks = blocks.slice(0, MCP_WIRE_LIMITS5.arrayEntries);
1887
+ const theme = resolveThemeForDoc(prepared.doc);
1888
+ const diagnostics = [];
1889
+ for (let index = 0; index < analyzedBlocks.length; index += 1) {
1890
+ const checkpoint = cancellationCheckpoint(signal, index);
1891
+ if (checkpoint) await checkpoint;
1892
+ const block = analyzedBlocks[index];
1893
+ throwIfAborted5(signal);
1894
+ if (block.standaloneAnnotation) {
1895
+ diagnostics.push({
1896
+ code: "standalone-template-block",
1897
+ severity: "warning",
1898
+ stage,
1899
+ format,
1900
+ count: 1,
1901
+ message: `Block "${block.id}" was created by a standalone template annotation and has no heading.`,
1902
+ remediation: "Move the annotation onto the intended heading, for example `# Heading {[content]}`, unless the additional heading-less block is deliberate.",
1903
+ retryable: false,
1904
+ location: {
1905
+ kind: "block",
1906
+ blockId: toWireIdentifier(block.id, "block"),
1907
+ nodeType: "block"
1908
+ }
1909
+ });
1910
+ }
1911
+ if (!block.template) continue;
1912
+ const templateId = resolveTemplateName(block.template);
1913
+ const behavior = TEMPLATE_AUTHORING_METADATA[templateId];
1914
+ if (!behavior) continue;
1915
+ const body = getBlockBodyText(block).trim();
1916
+ if (!body) continue;
1917
+ if (behavior.bodyPolicy === "ignored") {
1918
+ diagnostics.push({
1919
+ code: "template-body-not-rendered",
1920
+ severity: "warning",
1921
+ stage,
1922
+ format,
1923
+ count: 1,
1924
+ message: `Template "${templateId}" does not render the body of block "${block.title ?? block.id}".`,
1925
+ remediation: "Use the content template to preserve the complete body, or move this prose into a separate content block before visual optimization.",
1926
+ retryable: false,
1927
+ location: {
1928
+ kind: "block",
1929
+ blockId: toWireIdentifier(block.id, "block"),
1930
+ nodeType: "block"
1931
+ }
1932
+ });
1933
+ continue;
1934
+ }
1935
+ if (behavior.bodyPolicy !== "complete") continue;
1936
+ const materialized = materializeBlockLayers(block, {
1937
+ theme,
1938
+ customTemplates: prepared.doc.customTemplates,
1939
+ persistentLayers: false,
1940
+ blockIndex: index,
1941
+ totalBlocks: analyzedBlocks.length
1942
+ });
1943
+ const renderedText = materialized.layers.filter((layer) => layer.type === "text").map((layer) => layer.content.text).join("\n");
1944
+ if (!normalizedText(renderedText).includes(normalizedText(body))) {
1945
+ diagnostics.push({
1946
+ code: "rendered-content-omitted",
1947
+ severity: "error",
1948
+ stage,
1949
+ format,
1950
+ count: 1,
1951
+ message: `The complete body of block "${block.title ?? block.id}" is not present in its rendered text layers.`,
1952
+ remediation: "Keep the content template and repair its inputs, then validate and preview again before conversion.",
1953
+ retryable: false,
1954
+ location: {
1955
+ kind: "block",
1956
+ blockId: toWireIdentifier(block.id, "block"),
1957
+ nodeType: "block"
1958
+ }
1959
+ });
1960
+ }
1961
+ }
1962
+ if (analyzedBlocks.length < blocks.length) {
1963
+ diagnostics.push({
1964
+ code: "authoring-analysis-truncated",
1965
+ severity: "info",
1966
+ stage,
1967
+ format,
1968
+ count: blocks.length - analyzedBlocks.length,
1969
+ message: `Authoring diagnostics analyzed the first ${analyzedBlocks.length} blocks.`,
1970
+ remediation: "Split the document before requesting exhaustive per-block authoring diagnostics.",
1971
+ retryable: false,
1972
+ location: null
1973
+ });
1974
+ }
1975
+ return diagnostics;
1976
+ }
1977
+ function normalizedText(value) {
1978
+ return value.normalize("NFKC").replace(/\s+/gu, " ").trim().toLocaleLowerCase("en-US");
1979
+ }
1852
1980
  async function comparePreparedDocuments(documents, left, right, signal) {
1853
1981
  throwIfAborted5(signal);
1854
1982
  await yieldForCancellation(signal);
@@ -2728,6 +2856,7 @@ async function previewPreparedDocument(artifacts, prepared, request, signal, pro
2728
2856
  diagnostics: boundDiagnostics(
2729
2857
  [
2730
2858
  ...prepared.diagnostics,
2859
+ ...await authoringDiagnostics(prepared, "render", prepared.sourceFormat, signal),
2731
2860
  ...rendered.diagnostics ?? [],
2732
2861
  ...previewBasis === "reconstructed-import" ? [reconstructedPreviewDiagnostic(prepared.sourceFormat)] : []
2733
2862
  ],
@@ -3149,10 +3278,13 @@ async function prepareRenderedDocument(prepared, options, signal) {
3149
3278
  }
3150
3279
  };
3151
3280
  registry.register(captureDefinition);
3281
+ const canonicalMarkdown = options.autoTemplates === true ? prepared.markdownDoc : (await import("@bendyline/squisq/doc")).docToMarkdown(prepared.doc, {
3282
+ defaultTemplate: "sectionHeader"
3283
+ });
3152
3284
  const result = await convert(
3153
3285
  {
3154
3286
  kind: "markdown",
3155
- markdown: prepared.markdownDoc,
3287
+ markdown: canonicalMarkdown,
3156
3288
  container: prepared.container,
3157
3289
  baseName: prepared.baseName
3158
3290
  },
@@ -4029,13 +4161,258 @@ var init_progress = __esm({
4029
4161
  }
4030
4162
  });
4031
4163
 
4164
+ // src/mcp/revision-service.ts
4165
+ import { createHash as createHash7 } from "crypto";
4166
+ import {
4167
+ DOCBLOCKS_MCP_WIRE_VERSION as DOCBLOCKS_MCP_WIRE_VERSION5,
4168
+ MCP_WIRE_LIMITS as MCP_WIRE_LIMITS7,
4169
+ parseDocumentRevisionResult
4170
+ } from "@bendyline/docblocks/mcp";
4171
+ async function reviseDocumentArtifact(artifacts, documents, request, signal) {
4172
+ throwIfAborted5(signal);
4173
+ const parentArtifact = await artifacts.get(request.artifactUri, signal);
4174
+ if (parentArtifact.sha256 !== request.expectedSha256) {
4175
+ throw revisionError(
4176
+ "conflict",
4177
+ `Revision precondition failed: expected ${request.expectedSha256}, but the parent artifact is ${parentArtifact.sha256}.`,
4178
+ `Use expectedSha256 "${parentArtifact.sha256}" only if this is still the intended parent artifact.`
4179
+ );
4180
+ }
4181
+ if (parentArtifact.format !== "dbk") {
4182
+ throw revisionError(
4183
+ "invalid-revision-source",
4184
+ `Artifact-native block revision requires a DBK parent, not ${parentArtifact.format}.`,
4185
+ "Stage Markdown and assets with create_document_bundle, or convert the source to DBK first."
4186
+ );
4187
+ }
4188
+ if (new Set(request.edits.map((edit) => edit.blockId)).size !== request.edits.length) {
4189
+ throw revisionError(
4190
+ "duplicate-block-edit",
4191
+ "Each block id may be revised at most once per request.",
4192
+ "Combine changes to the same block into one replacement fragment."
4193
+ );
4194
+ }
4195
+ const totalReplacementCharacters = request.edits.reduce(
4196
+ (sum, edit) => sum + edit.markdown.length,
4197
+ 0
4198
+ );
4199
+ if (totalReplacementCharacters > MCP_WIRE_LIMITS7.documentCharacters) {
4200
+ throw revisionError(
4201
+ "revision-too-large",
4202
+ "Combined replacement Markdown exceeds the MCP document character limit.",
4203
+ "Use fewer or smaller block replacements."
4204
+ );
4205
+ }
4206
+ const prepared = await documents.prepare({ kind: "artifact", uri: parentArtifact.uri }, signal);
4207
+ const { flattenBlocks } = await import("@bendyline/squisq/doc");
4208
+ const blocks = flattenBlocks(prepared.doc.blocks);
4209
+ const ranges = await computeBlockSourceRanges(blocks, prepared.markdown.length, signal);
4210
+ const blockRecords = indexEditableBlocks(blocks, ranges);
4211
+ const replacements = [];
4212
+ for (let index = 0; index < request.edits.length; index += 1) {
4213
+ throwIfAborted5(signal);
4214
+ const edit = request.edits[index];
4215
+ const record = blockRecords.get(edit.blockId);
4216
+ if (!record) {
4217
+ throw revisionError(
4218
+ "block-not-found",
4219
+ `Block "${edit.blockId}" is not an editable heading block in the parent artifact.`,
4220
+ "Call inspect_document on the exact parent artifact and use a returned block id with a non-null sourceRange."
4221
+ );
4222
+ }
4223
+ const replacementMarkdown = await canonicalReplacement(
4224
+ edit.markdown,
4225
+ record.block,
4226
+ edit.blockId
4227
+ );
4228
+ const before = prepared.markdown.slice(record.start, record.end);
4229
+ if (replacementMarkdown === before) {
4230
+ throw revisionError(
4231
+ "no-op-block-edit",
4232
+ `Replacement for block "${edit.blockId}" does not change its heading or body.`,
4233
+ "Omit no-op block edits from the revision request."
4234
+ );
4235
+ }
4236
+ replacements.push({
4237
+ blockId: edit.blockId,
4238
+ start: record.start,
4239
+ end: record.end,
4240
+ markdown: replacementMarkdown,
4241
+ applied: {
4242
+ kind: "replace_block",
4243
+ blockId: edit.blockId,
4244
+ beforeSha256: sha256(Buffer.from(before, "utf8")),
4245
+ afterSha256: sha256(Buffer.from(replacementMarkdown, "utf8"))
4246
+ }
4247
+ });
4248
+ if ((index + 1) % 16 === 0) await yieldForCancellation2(signal);
4249
+ }
4250
+ const revisedMarkdown = applyReplacements(prepared.markdown, replacements);
4251
+ if (revisedMarkdown.length > MCP_WIRE_LIMITS7.documentCharacters) {
4252
+ throw revisionError(
4253
+ "revision-too-large",
4254
+ "The revised Markdown exceeds the MCP document character limit.",
4255
+ "Use smaller block replacements or split the document before revising it."
4256
+ );
4257
+ }
4258
+ if (revisedMarkdown === prepared.markdown) {
4259
+ throw revisionError(
4260
+ "no-op-revision",
4261
+ "The requested block replacements do not change the document.",
4262
+ "Submit only replacements that change heading or body Markdown."
4263
+ );
4264
+ }
4265
+ const [{ parseMarkdown: parseMarkdown2 }, { markdownToDoc: markdownToDoc2, flattenBlocks: flattenRevisedBlocks }] = await Promise.all([import("@bendyline/squisq/markdown"), import("@bendyline/squisq/doc")]);
4266
+ const markdownDoc = parseMarkdown2(revisedMarkdown);
4267
+ const doc = markdownToDoc2(markdownDoc, { defaultTemplate: "content", autoTemplates: false });
4268
+ const revisedIds = new Set(
4269
+ flattenRevisedBlocks(doc.blocks).map((block) => toWireIdentifier(block.id, "block"))
4270
+ );
4271
+ for (const replacement of replacements) {
4272
+ if (!revisedIds.has(replacement.blockId)) {
4273
+ throw revisionError(
4274
+ "unstable-block-id",
4275
+ `Replacement for block "${replacement.blockId}" did not preserve its stable id.`,
4276
+ "Keep the replacement heading id unchanged and retry from the parent artifact."
4277
+ );
4278
+ }
4279
+ }
4280
+ throwIfAborted5(signal);
4281
+ await prepared.container.writeDocument(revisedMarkdown);
4282
+ const revisionSourceSha256 = createHash7("sha256").update("docblocks-artifact-revision-v1\0", "utf8").update(parentArtifact.sha256, "utf8").update("\0", "utf8").update(revisedMarkdown, "utf8").digest("hex");
4283
+ const [conversion] = await convertPreparedDocument(
4284
+ artifacts,
4285
+ {
4286
+ ...prepared,
4287
+ markdown: revisedMarkdown,
4288
+ markdownDoc,
4289
+ doc,
4290
+ sourceSha256: revisionSourceSha256
4291
+ },
4292
+ { targets: [{ format: "dbk", fidelity: "semantic" }] },
4293
+ signal
4294
+ );
4295
+ if (!conversion) throw new Error("Document revision produced no DBK artifact");
4296
+ const result = {
4297
+ version: DOCBLOCKS_MCP_WIRE_VERSION5,
4298
+ kind: "revision",
4299
+ parentArtifact,
4300
+ artifact: conversion.artifact,
4301
+ edits: replacements.map((replacement) => replacement.applied),
4302
+ diagnostics: conversion.diagnostics
4303
+ };
4304
+ const parsed = parseDocumentRevisionResult(result);
4305
+ if (!parsed) throw new Error("Document revision produced an invalid MCP result");
4306
+ return parsed;
4307
+ }
4308
+ function indexEditableBlocks(blocks, ranges) {
4309
+ const indexed = /* @__PURE__ */ new Map();
4310
+ const ambiguous = /* @__PURE__ */ new Set();
4311
+ for (let index = 0; index < blocks.length; index += 1) {
4312
+ const block = blocks[index];
4313
+ const range = ranges[index];
4314
+ if (!block || !range || !block.sourceHeading) continue;
4315
+ const wireId = toWireIdentifier(block.id, "block");
4316
+ if (indexed.has(wireId)) {
4317
+ indexed.delete(wireId);
4318
+ ambiguous.add(wireId);
4319
+ } else if (!ambiguous.has(wireId)) {
4320
+ indexed.set(wireId, { block, start: range.start, end: range.end });
4321
+ }
4322
+ }
4323
+ return indexed;
4324
+ }
4325
+ async function canonicalReplacement(markdown, target, wireBlockId) {
4326
+ const { parseMarkdown: parseMarkdown2, stringifyMarkdown } = await import("@bendyline/squisq/markdown");
4327
+ const fragment = parseMarkdown2(markdown);
4328
+ if (fragment.frontmatter && Object.keys(fragment.frontmatter).length > 0) {
4329
+ throw revisionError(
4330
+ "invalid-block-replacement",
4331
+ `Replacement for block "${wireBlockId}" cannot contain frontmatter.`,
4332
+ "Provide one heading and its direct body only."
4333
+ );
4334
+ }
4335
+ const headings = fragment.children.filter(
4336
+ (node) => node.type === "heading"
4337
+ );
4338
+ const heading = headings[0];
4339
+ if (!heading || fragment.children[0] !== heading || headings.length !== 1) {
4340
+ throw revisionError(
4341
+ "invalid-block-replacement",
4342
+ `Replacement for block "${wireBlockId}" must begin with exactly one heading and cannot contain nested headings.`,
4343
+ "Use a complete fragment such as `# Heading {[content]}\n\nReplacement body`."
4344
+ );
4345
+ }
4346
+ const sourceHeading = target.sourceHeading;
4347
+ if (!sourceHeading) throw new Error("Editable replacement target has no source heading");
4348
+ if (heading.depth !== sourceHeading.depth) {
4349
+ throw revisionError(
4350
+ "invalid-block-replacement",
4351
+ `Replacement for block "${wireBlockId}" must keep heading depth ${sourceHeading.depth}.`,
4352
+ `Begin the replacement with ${"#".repeat(sourceHeading.depth)} followed by the heading text.`
4353
+ );
4354
+ }
4355
+ if (heading.attributes?.id && heading.attributes.id !== target.id) {
4356
+ throw revisionError(
4357
+ "block-id-change-not-allowed",
4358
+ `Replacement for block "${wireBlockId}" cannot change its id to "${heading.attributes.id}".`,
4359
+ `Omit the explicit id or keep {#${target.id}}.`
4360
+ );
4361
+ }
4362
+ const sourceAttributes = sourceHeading.attributes;
4363
+ const replacementAttributes = heading.attributes;
4364
+ const attributes = {
4365
+ ...sourceAttributes,
4366
+ ...replacementAttributes,
4367
+ id: target.id,
4368
+ ...replacementAttributes?.classes === void 0 && sourceAttributes?.classes ? { classes: sourceAttributes.classes } : {},
4369
+ ...replacementAttributes?.params === void 0 && sourceAttributes?.params ? { params: sourceAttributes.params } : {},
4370
+ ...replacementAttributes?.blockMeta === void 0 && sourceAttributes?.blockMeta ? { blockMeta: sourceAttributes.blockMeta } : {},
4371
+ ...replacementAttributes?.metadata === void 0 && sourceAttributes?.metadata ? { metadata: sourceAttributes.metadata } : {}
4372
+ };
4373
+ const pinnedHeading = {
4374
+ ...heading,
4375
+ attributes,
4376
+ templateAnnotation: heading.templateAnnotation ?? sourceHeading.templateAnnotation
4377
+ };
4378
+ return stringifyMarkdown({
4379
+ type: "document",
4380
+ children: [pinnedHeading, ...fragment.children.slice(1)]
4381
+ });
4382
+ }
4383
+ function applyReplacements(source, replacements) {
4384
+ let revised = source;
4385
+ const descending = [...replacements].sort((left, right) => right.start - left.start);
4386
+ for (const replacement of descending) {
4387
+ revised = revised.slice(0, replacement.start) + replacement.markdown + revised.slice(replacement.end);
4388
+ }
4389
+ return revised;
4390
+ }
4391
+ function revisionError(code, message, hint) {
4392
+ return Object.assign(new Error(message), { code, hint, retryable: false });
4393
+ }
4394
+ async function yieldForCancellation2(signal) {
4395
+ await new Promise((resolve4) => setImmediate(resolve4));
4396
+ throwIfAborted5(signal);
4397
+ }
4398
+ var init_revision_service = __esm({
4399
+ "src/mcp/revision-service.ts"() {
4400
+ "use strict";
4401
+ init_conversion_service();
4402
+ init_document_service();
4403
+ init_intelligence();
4404
+ init_output_bounds();
4405
+ }
4406
+ });
4407
+
4032
4408
  // src/mcp/agentic-tools.ts
4033
4409
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4034
4410
  import { z } from "zod";
4035
4411
  import {
4036
- MCP_WIRE_LIMITS as MCP_WIRE_LIMITS7,
4412
+ MCP_WIRE_LIMITS as MCP_WIRE_LIMITS8,
4037
4413
  parseComparisonResult,
4038
4414
  parseConversionResult as parseConversionResult2,
4415
+ parseDocumentRevisionResult as parseDocumentRevisionResult2,
4039
4416
  parseDocumentSource,
4040
4417
  parseInspectionResult,
4041
4418
  parseMaterializationOptions,
@@ -4047,6 +4424,7 @@ import {
4047
4424
  bundleDocumentSourceSchema,
4048
4425
  DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS,
4049
4426
  documentSourceSchema,
4427
+ documentRevisionRequestSchema,
4050
4428
  formatInputSchema,
4051
4429
  materializationOptionsSchema
4052
4430
  } from "@bendyline/docblocks/mcp/zod";
@@ -4142,7 +4520,7 @@ function registerAgenticTools(server, context) {
4142
4520
  server.registerTool(
4143
4521
  "create_document_bundle",
4144
4522
  {
4145
- description: "Package Markdown and authority-scoped assets into an immutable DBK artifact with asset metadata.",
4523
+ description: "Stage authored Markdown and authority-scoped assets as an immutable DBK artifact. Use the returned artifact URI directly with validate_document, preview_document, and convert_document; no local Markdown file or invented root id is required.",
4146
4524
  inputSchema: z.object({ source: bundleDocumentSourceSchema }).strict(),
4147
4525
  outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.create_document_bundle,
4148
4526
  annotations: ARTIFACT_CREATING
@@ -4174,6 +4552,38 @@ function registerAgenticTools(server, context) {
4174
4552
  }
4175
4553
  }
4176
4554
  );
4555
+ server.registerTool(
4556
+ "revise_document",
4557
+ {
4558
+ description: "Replace selected heading blocks in an immutable DBK artifact without resending the full document. Requires the parent artifact SHA-256, preserves block ids and child blocks, and returns a new DBK artifact without mutating the parent.",
4559
+ inputSchema: documentRevisionRequestSchema,
4560
+ outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.revise_document,
4561
+ annotations: ARTIFACT_CREATING
4562
+ },
4563
+ async (request, extra) => {
4564
+ try {
4565
+ return await context.runOperation(extra.signal, async (operationSignal) => {
4566
+ const documents = new DocumentService(await context.authority, context.artifacts);
4567
+ const result = await reviseDocumentArtifact(
4568
+ context.artifacts,
4569
+ documents,
4570
+ request,
4571
+ operationSignal
4572
+ );
4573
+ requireWire(parseDocumentRevisionResult2(result), "document revision");
4574
+ return {
4575
+ content: [
4576
+ { type: "text", text: JSON.stringify(result) },
4577
+ artifactLink(result.artifact)
4578
+ ],
4579
+ structuredContent: asStructuredContent(successResult(result))
4580
+ };
4581
+ });
4582
+ } catch (caught) {
4583
+ return errorResult(caught, "transform", "dbk", extra.signal);
4584
+ }
4585
+ }
4586
+ );
4177
4587
  server.registerTool(
4178
4588
  "save_artifact",
4179
4589
  {
@@ -4422,43 +4832,30 @@ function registerAuthoringGuideResource(server) {
4422
4832
  "docblocks://authoring-guide",
4423
4833
  {
4424
4834
  description: "Compact linked-Squisq authoring vocabulary, artifact workflow, fidelity modes, templates, themes, and transform styles.",
4425
- mimeType: "application/json"
4835
+ mimeType: "application/json",
4836
+ annotations: { audience: ["assistant"], priority: 1 }
4426
4837
  },
4427
4838
  async () => {
4428
- const [docModule, schemaModule, transformModule] = await Promise.all([
4429
- import("@bendyline/squisq/doc"),
4839
+ const [templates, schemaModule, transformModule] = await Promise.all([
4840
+ authoringTemplateCatalog(),
4430
4841
  import("@bendyline/squisq/schemas"),
4431
4842
  import("@bendyline/squisq/transform")
4432
4843
  ]);
4433
- const templates = docModule.getAvailableTemplates().slice(0, MCP_WIRE_LIMITS7.arrayEntries).map((id) => ({
4434
- id: requireWireIdentifier(id, "template id"),
4435
- label: boundWireText(
4436
- docModule.TEMPLATE_METADATA[id]?.label ?? id,
4437
- MCP_WIRE_LIMITS7.labelCharacters,
4438
- id
4439
- ),
4440
- description: boundWireText(docModule.TEMPLATE_METADATA[id]?.description ?? ""),
4441
- inputs: (docModule.TEMPLATE_INPUT_DESCRIPTORS[id] ?? []).slice(0, MCP_WIRE_LIMITS7.arrayEntries).map((input) => ({
4442
- key: requireWireIdentifier(input.key, "template input key"),
4443
- required: input.required ?? false,
4444
- values: boundWireTextArray(
4445
- input.values ?? [],
4446
- MCP_WIRE_LIMITS7.arrayEntries,
4447
- MCP_WIRE_LIMITS7.labelCharacters
4448
- ),
4449
- hint: boundNullableWireText(input.valueHint, MCP_WIRE_LIMITS7.labelCharacters)
4450
- }))
4451
- }));
4452
4844
  const guide = {
4453
- version: 1,
4845
+ version: 3,
4454
4846
  workflow: [
4455
- "list_roots and select an authority-scoped source",
4456
- "inspect_document and validate_document",
4457
- "convert_document to one or more immutable artifacts",
4847
+ "author complete Markdown with annotations bound to headings; ordinary headings default to content",
4848
+ "keep Markdown inline or stage Markdown plus assets with create_document_bundle",
4849
+ "inspect_document and validate_document before visual optimization",
4850
+ "for a staged DBK, use revise_document with inspected block ids and the exact parent SHA-256 for targeted repairs",
4458
4851
  "preview_document and inspect previewBasis before treating images as source renders, reconstructed imports, or native extraction",
4852
+ "replace selected content blocks with compatible visual templates and preview again",
4853
+ "convert_document to one or more immutable artifacts",
4459
4854
  "save_artifact only when a durable file is required"
4460
4855
  ],
4461
- markdownAnnotation: '{[templateId key="value"]}',
4856
+ markdownAnnotation: '# Heading {[templateId key="value"]}',
4857
+ standaloneAnnotation: '{[templateId key="value"]}',
4858
+ standaloneWarning: "A standalone annotation creates an additional heading-less block. Bind the annotation to a heading unless that extra block is deliberate.",
4462
4859
  fidelity: {
4463
4860
  semantic: "Prioritize meaning and portability.",
4464
4861
  "editable-native": "Produce editable native structures with target-specific diagnostics.",
@@ -4468,14 +4865,14 @@ function registerAuthoringGuideResource(server) {
4468
4865
  fidelityByFormat: MCP_FORMAT_FIDELITIES,
4469
4866
  formats: boundedFormatCapabilities(),
4470
4867
  templates,
4471
- themes: schemaModule.getThemeSummaries().slice(0, MCP_WIRE_LIMITS7.arrayEntries).map((theme) => ({
4868
+ themes: schemaModule.getThemeSummaries().slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((theme) => ({
4472
4869
  id: requireWireIdentifier(theme.id, "theme id"),
4473
- name: boundWireText(theme.name, MCP_WIRE_LIMITS7.labelCharacters, theme.id),
4870
+ name: boundWireText(theme.name, MCP_WIRE_LIMITS8.labelCharacters, theme.id),
4474
4871
  description: boundWireText(theme.description ?? "")
4475
4872
  })),
4476
- transformStyles: transformModule.getTransformStyleSummaries().slice(0, MCP_WIRE_LIMITS7.arrayEntries).map((style) => ({
4873
+ transformStyles: transformModule.getTransformStyleSummaries().slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((style) => ({
4477
4874
  id: requireWireIdentifier(style.id, "transform style id"),
4478
- name: boundWireText(style.name, MCP_WIRE_LIMITS7.labelCharacters, style.id),
4875
+ name: boundWireText(style.name, MCP_WIRE_LIMITS8.labelCharacters, style.id),
4479
4876
  description: boundWireText(style.description)
4480
4877
  }))
4481
4878
  };
@@ -4492,6 +4889,115 @@ function registerAuthoringGuideResource(server) {
4492
4889
  );
4493
4890
  }
4494
4891
  function registerTemplateTools(server, context) {
4892
+ server.registerTool(
4893
+ "get_authoring_context",
4894
+ {
4895
+ description: "Get the complete linked-Squisq authoring vocabulary, canonical heading syntax, content-retention behavior, workflow, and optional block recommendations in one call.",
4896
+ inputSchema: z.object({
4897
+ targetFormat: formatInputSchema.optional(),
4898
+ goal: z.enum(["content-first", "visual-polish"]).optional(),
4899
+ source: documentSourceSchema.optional(),
4900
+ maxBlocks: z.number().int().min(1).max(100).optional()
4901
+ }).strict(),
4902
+ outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.get_authoring_context,
4903
+ annotations: READ_ONLY
4904
+ },
4905
+ async ({ targetFormat, goal: requestedGoal, source, maxBlocks }, extra) => {
4906
+ try {
4907
+ return await context.runOperation(extra.signal, async (operationSignal) => {
4908
+ const goal = requestedGoal ?? "content-first";
4909
+ const normalizedTarget = targetFormat?.toLowerCase() ?? null;
4910
+ const formats = boundedFormatCapabilities();
4911
+ if (normalizedTarget) {
4912
+ const target = formats.find((format) => format.id === normalizedTarget);
4913
+ if (!target) throw new Error(`Unknown target format "${normalizedTarget}"`);
4914
+ if (!target.export.supported) {
4915
+ throw new Error(`Format "${normalizedTarget}" is not export-capable`);
4916
+ }
4917
+ }
4918
+ const [templates, schemaModule, transformModule] = await Promise.all([
4919
+ authoringTemplateCatalog(),
4920
+ import("@bendyline/squisq/schemas"),
4921
+ import("@bendyline/squisq/transform")
4922
+ ]);
4923
+ let recommendations = [];
4924
+ let totalBlocks = null;
4925
+ let truncated = false;
4926
+ if (source) {
4927
+ const documents = new DocumentService(await context.authority, context.artifacts);
4928
+ const prepared = await documents.prepare(
4929
+ requireDocumentSource(source),
4930
+ operationSignal
4931
+ );
4932
+ const [{ flattenBlocks }, recommendModule] = await Promise.all([
4933
+ import("@bendyline/squisq/doc"),
4934
+ import("@bendyline/squisq/recommend")
4935
+ ]);
4936
+ const blocks = flattenBlocks(prepared.doc.blocks);
4937
+ const limit = maxBlocks ?? 50;
4938
+ recommendations = blocks.slice(0, limit).map((block) => {
4939
+ const profile = recommendModule.profileBlockContents(block.contents ?? []);
4940
+ const visual = recommendModule.recommendTemplatesForBlock(
4941
+ profile,
4942
+ templates.map((template) => template.id)
4943
+ ).recommended;
4944
+ const recommendedTemplateIds = goal === "content-first" ? ["content", ...visual.filter((id) => id !== "content")] : visual;
4945
+ return {
4946
+ blockId: toWireIdentifier(block.id, "block"),
4947
+ title: boundNullableWireText(block.title, MCP_WIRE_LIMITS8.labelCharacters),
4948
+ profile: {
4949
+ ...profile,
4950
+ hasAsciiDiagram: profile.hasAsciiDiagram ?? false,
4951
+ hasTimeline: profile.hasTimeline ?? false,
4952
+ hasTree: profile.hasTree ?? false
4953
+ },
4954
+ recommendedTemplateIds: recommendedTemplateIds.slice(0, 256).map((id) => requireWireIdentifier(id, "recommended template id"))
4955
+ };
4956
+ });
4957
+ totalBlocks = blocks.length;
4958
+ truncated = recommendations.length < blocks.length;
4959
+ }
4960
+ return textAndStructured({
4961
+ goal,
4962
+ targetFormat: normalizedTarget,
4963
+ defaultTemplateId: "content",
4964
+ defaultFidelity: authoringDefaultFidelity(normalizedTarget, goal),
4965
+ workflow: [
4966
+ "Author complete content with heading-bound annotations; ordinary headings default to content.",
4967
+ "Keep Markdown inline or stage Markdown plus assets with create_document_bundle.",
4968
+ "Validate the source and repair content-retention, accessibility, and target diagnostics.",
4969
+ "For a staged DBK, use revise_document with inspected block ids and the exact parent SHA-256 for targeted repairs.",
4970
+ "Preview the bounded visual items and repair overflow before changing layouts.",
4971
+ "For visual polish, replace selected content blocks with compatible recommended templates and preview again.",
4972
+ "Convert to immutable artifacts, inspect conversion reports, and save only final durable outputs."
4973
+ ],
4974
+ syntax: {
4975
+ headingAnnotation: "# Heading {[content]}",
4976
+ standaloneAnnotation: "{[content]}",
4977
+ standaloneWarning: "A standalone annotation creates an additional heading-less block. Bind it to a heading unless that extra block is deliberate."
4978
+ },
4979
+ formats,
4980
+ templates,
4981
+ themes: schemaModule.getThemeSummaries().slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((theme) => ({
4982
+ id: requireWireIdentifier(theme.id, "theme id"),
4983
+ name: boundWireText(theme.name, MCP_WIRE_LIMITS8.labelCharacters, theme.id),
4984
+ description: boundWireText(theme.description ?? "")
4985
+ })),
4986
+ transformStyles: transformModule.getTransformStyleSummaries().slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((style) => ({
4987
+ id: requireWireIdentifier(style.id, "transform style id"),
4988
+ name: boundWireText(style.name, MCP_WIRE_LIMITS8.labelCharacters, style.id),
4989
+ description: boundWireText(style.description)
4990
+ })),
4991
+ recommendations,
4992
+ totalBlocks,
4993
+ truncated
4994
+ });
4995
+ });
4996
+ } catch (caught) {
4997
+ return errorResult(caught, "inspect", null, extra.signal);
4998
+ }
4999
+ }
5000
+ );
4495
5001
  server.registerTool(
4496
5002
  "list_templates",
4497
5003
  {
@@ -4502,15 +5008,10 @@ function registerTemplateTools(server, context) {
4502
5008
  },
4503
5009
  async () => {
4504
5010
  try {
4505
- const { getAvailableTemplates, TEMPLATE_METADATA } = await import("@bendyline/squisq/doc");
4506
- const templates = getAvailableTemplates().slice(0, MCP_WIRE_LIMITS7.arrayEntries).map((id) => ({
4507
- id: requireWireIdentifier(id, "template id"),
4508
- label: boundWireText(
4509
- TEMPLATE_METADATA[id]?.label ?? id,
4510
- MCP_WIRE_LIMITS7.labelCharacters,
4511
- id
4512
- ),
4513
- description: boundWireText(TEMPLATE_METADATA[id]?.description ?? "")
5011
+ const templates = (await authoringTemplateCatalog()).map(({ id, label, description }) => ({
5012
+ id,
5013
+ label,
5014
+ description
4514
5015
  }));
4515
5016
  return textAndStructured({ templates });
4516
5017
  } catch (caught) {
@@ -4528,41 +5029,21 @@ function registerTemplateTools(server, context) {
4528
5029
  },
4529
5030
  async ({ templateId }) => {
4530
5031
  try {
4531
- const { getAvailableTemplates, TEMPLATE_INPUT_DESCRIPTORS, TEMPLATE_METADATA } = await import("@bendyline/squisq/doc");
4532
- if (!getAvailableTemplates().includes(templateId)) {
5032
+ const described = (await authoringTemplateCatalog()).find(
5033
+ (template2) => template2.id === templateId
5034
+ );
5035
+ if (!described) {
4533
5036
  return errorResult(new Error(`Unknown template "${templateId}"`), "validate");
4534
5037
  }
4535
- const inputs = (TEMPLATE_INPUT_DESCRIPTORS[templateId] ?? []).slice(0, MCP_WIRE_LIMITS7.arrayEntries).map((input) => ({
4536
- key: requireWireIdentifier(input.key, "template input key"),
4537
- description: boundWireText(input.description),
4538
- type: boundWireText(
4539
- input.coerce ?? "string",
4540
- MCP_WIRE_LIMITS7.labelCharacters,
4541
- "string"
4542
- ),
4543
- required: input.required ?? false,
4544
- values: boundWireTextArray(
4545
- input.values ?? [],
4546
- MCP_WIRE_LIMITS7.arrayEntries,
4547
- MCP_WIRE_LIMITS7.labelCharacters
4548
- ),
4549
- valueHint: boundNullableWireText(input.valueHint, MCP_WIRE_LIMITS7.labelCharacters)
4550
- }));
4551
- const metadata = TEMPLATE_METADATA[templateId];
4552
5038
  const template = {
4553
- id: requireWireIdentifier(templateId, "template id"),
4554
- label: boundWireText(
4555
- metadata?.label ?? templateId,
4556
- MCP_WIRE_LIMITS7.labelCharacters,
4557
- templateId
4558
- ),
4559
- description: boundWireText(metadata?.description ?? ""),
4560
- inputs
5039
+ id: described.id,
5040
+ label: described.label,
5041
+ description: described.description,
5042
+ inputs: described.inputs
4561
5043
  };
4562
- const required = inputs.filter((input) => input.required).map((input) => `${input.key}="\u2026"`).join(" ");
4563
5044
  return textAndStructured({
4564
5045
  template,
4565
- annotationExample: boundWireText(`{[${templateId}${required ? ` ${required}` : ""}]}`)
5046
+ annotationExample: described.annotationExample
4566
5047
  });
4567
5048
  } catch (caught) {
4568
5049
  return errorResult(caught, "inspect");
@@ -4600,7 +5081,7 @@ function registerTemplateTools(server, context) {
4600
5081
  const profile = recommendModule.profileBlockContents(block.contents ?? []);
4601
5082
  return {
4602
5083
  blockId: toWireIdentifier(block.id, "block"),
4603
- title: boundNullableWireText(block.title, MCP_WIRE_LIMITS7.labelCharacters),
5084
+ title: boundNullableWireText(block.title, MCP_WIRE_LIMITS8.labelCharacters),
4604
5085
  profile: {
4605
5086
  ...profile,
4606
5087
  hasAsciiDiagram: profile.hasAsciiDiagram ?? false,
@@ -4699,12 +5180,12 @@ function registerThemeTools(server, context) {
4699
5180
  layouts: (inferred.layouts ?? []).slice(0, 1e3).map((layout, index) => ({
4700
5181
  id: boundWireText(
4701
5182
  layout.name,
4702
- MCP_WIRE_LIMITS7.labelCharacters,
5183
+ MCP_WIRE_LIMITS8.labelCharacters,
4703
5184
  `layout-${index + 1}`
4704
5185
  ),
4705
5186
  name: boundWireText(
4706
5187
  layout.label,
4707
- MCP_WIRE_LIMITS7.labelCharacters,
5188
+ MCP_WIRE_LIMITS8.labelCharacters,
4708
5189
  `Layout ${index + 1}`
4709
5190
  ),
4710
5191
  description: boundWireText(layout.description ?? "")
@@ -4749,21 +5230,21 @@ function registerThemeTools(server, context) {
4749
5230
  layouts: result.layouts.slice(0, 2048).map((layout, index) => ({
4750
5231
  layoutPath: boundWireText(
4751
5232
  layout.layoutPath,
4752
- MCP_WIRE_LIMITS7.pathCharacters,
5233
+ MCP_WIRE_LIMITS8.pathCharacters,
4753
5234
  `ppt/slideLayouts/slideLayout${index + 1}.xml`
4754
5235
  ),
4755
5236
  name: boundWireText(
4756
5237
  layout.name,
4757
- MCP_WIRE_LIMITS7.labelCharacters,
5238
+ MCP_WIRE_LIMITS8.labelCharacters,
4758
5239
  `Layout ${index + 1}`
4759
5240
  ),
4760
- masterName: boundNullableWireText(layout.masterName, MCP_WIRE_LIMITS7.labelCharacters),
4761
- type: boundNullableWireText(layout.typeAttr, MCP_WIRE_LIMITS7.labelCharacters),
5241
+ masterName: boundNullableWireText(layout.masterName, MCP_WIRE_LIMITS8.labelCharacters),
5242
+ type: boundNullableWireText(layout.typeAttr, MCP_WIRE_LIMITS8.labelCharacters),
4762
5243
  slideCount: layout.slideCount,
4763
5244
  verdict: layout.verdict,
4764
5245
  templateId: boundNullableWireText(
4765
5246
  layout.builtinTemplate ?? layout.customTemplate?.name,
4766
- MCP_WIRE_LIMITS7.labelCharacters
5247
+ MCP_WIRE_LIMITS8.labelCharacters
4767
5248
  ),
4768
5249
  notes: boundWireTextArray(layout.notes ?? [], 1e3)
4769
5250
  }))
@@ -4863,7 +5344,7 @@ function registerThemeTools(server, context) {
4863
5344
  const payload = {
4864
5345
  result,
4865
5346
  theme: summarizeTheme(inferred.theme),
4866
- layoutIds: (inferred.layouts ?? []).slice(0, 1e3).map((layout) => boundWireText(layout.name, MCP_WIRE_LIMITS7.labelCharacters)),
5347
+ layoutIds: (inferred.layouts ?? []).slice(0, 1e3).map((layout) => boundWireText(layout.name, MCP_WIRE_LIMITS8.labelCharacters)),
4867
5348
  warnings: boundWireWarnings(inferred.warnings)
4868
5349
  };
4869
5350
  return {
@@ -4938,34 +5419,80 @@ function summarizeTheme(theme) {
4938
5419
  const id = requireWireIdentifier(theme.id, "theme id");
4939
5420
  return {
4940
5421
  id,
4941
- name: boundWireText(theme.name, MCP_WIRE_LIMITS7.labelCharacters, id),
5422
+ name: boundWireText(theme.name, MCP_WIRE_LIMITS8.labelCharacters, id),
4942
5423
  description: boundWireText(theme.description ?? ""),
4943
5424
  colors: {
4944
- primary: boundWireText(theme.colors.primary, MCP_WIRE_LIMITS7.labelCharacters, "#000000"),
4945
- secondary: boundWireText(theme.colors.secondary, MCP_WIRE_LIMITS7.labelCharacters, "#000000"),
5425
+ primary: boundWireText(theme.colors.primary, MCP_WIRE_LIMITS8.labelCharacters, "#000000"),
5426
+ secondary: boundWireText(theme.colors.secondary, MCP_WIRE_LIMITS8.labelCharacters, "#000000"),
4946
5427
  background: boundWireText(
4947
5428
  theme.colors.background,
4948
- MCP_WIRE_LIMITS7.labelCharacters,
5429
+ MCP_WIRE_LIMITS8.labelCharacters,
4949
5430
  "#ffffff"
4950
5431
  ),
4951
- text: boundWireText(theme.colors.text, MCP_WIRE_LIMITS7.labelCharacters, "#000000"),
4952
- highlight: boundWireText(theme.colors.highlight, MCP_WIRE_LIMITS7.labelCharacters, "#000000")
5432
+ text: boundWireText(theme.colors.text, MCP_WIRE_LIMITS8.labelCharacters, "#000000"),
5433
+ highlight: boundWireText(theme.colors.highlight, MCP_WIRE_LIMITS8.labelCharacters, "#000000")
4953
5434
  },
4954
5435
  bodyFont: boundWireText(JSON.stringify(theme.typography.bodyFont) ?? ""),
4955
5436
  titleFont: boundWireText(JSON.stringify(theme.typography.titleFont) ?? "")
4956
5437
  };
4957
5438
  }
5439
+ async function authoringTemplateCatalog() {
5440
+ const {
5441
+ getAvailableTemplates,
5442
+ TEMPLATE_AUTHORING_METADATA,
5443
+ TEMPLATE_INPUT_DESCRIPTORS,
5444
+ TEMPLATE_METADATA
5445
+ } = await import("@bendyline/squisq/doc");
5446
+ const authoring = TEMPLATE_AUTHORING_METADATA;
5447
+ return getAvailableTemplates().slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((id) => {
5448
+ const metadata = TEMPLATE_METADATA[id];
5449
+ const behavior = authoring[id];
5450
+ if (!behavior) throw new Error(`Template "${id}" has no authoring metadata`);
5451
+ const inputs = (TEMPLATE_INPUT_DESCRIPTORS[id] ?? []).slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((input) => ({
5452
+ key: requireWireIdentifier(input.key, "template input key"),
5453
+ description: boundWireText(input.description),
5454
+ type: boundWireText(input.coerce ?? "string", MCP_WIRE_LIMITS8.labelCharacters, "string"),
5455
+ required: input.required ?? false,
5456
+ values: boundWireTextArray(
5457
+ input.values ?? [],
5458
+ MCP_WIRE_LIMITS8.arrayEntries,
5459
+ MCP_WIRE_LIMITS8.labelCharacters
5460
+ ),
5461
+ valueHint: boundNullableWireText(input.valueHint, MCP_WIRE_LIMITS8.labelCharacters)
5462
+ }));
5463
+ const required = inputs.filter((input) => input.required).map((input) => `${input.key}="\u2026"`).join(" ");
5464
+ return {
5465
+ id: requireWireIdentifier(id, "template id"),
5466
+ label: boundWireText(metadata?.label ?? id, MCP_WIRE_LIMITS8.labelCharacters, id),
5467
+ description: boundWireText(metadata?.description ?? ""),
5468
+ inputs,
5469
+ ...behavior,
5470
+ annotationExample: boundWireText(`# Heading {[${id}${required ? ` ${required}` : ""}]}`)
5471
+ };
5472
+ });
5473
+ }
5474
+ function authoringDefaultFidelity(targetFormat, goal) {
5475
+ if (!targetFormat) return null;
5476
+ if (targetFormat === "mp4" || targetFormat === "gif") return "rendered-fidelity";
5477
+ if (goal === "visual-polish" && (targetFormat === "pptx" || targetFormat === "pdf")) {
5478
+ return "rendered-fidelity";
5479
+ }
5480
+ if (targetFormat === "docx" || targetFormat === "pptx" || targetFormat === "xlsx") {
5481
+ return "editable-native";
5482
+ }
5483
+ return "semantic";
5484
+ }
4958
5485
  function boundedFormatCapabilities() {
4959
5486
  return MCP_FORMAT_CAPABILITIES.slice(0, 64).map((format) => {
4960
5487
  const id = requireWireIdentifier(format.id, "format id");
4961
- if (id.length > MCP_WIRE_LIMITS7.formatCharacters || !MCP_OUTPUT_PATTERNS.format.test(id)) {
5488
+ if (id.length > MCP_WIRE_LIMITS8.formatCharacters || !MCP_OUTPUT_PATTERNS.format.test(id)) {
4962
5489
  throw new Error("Linked Squisq returned an invalid MCP format id");
4963
5490
  }
4964
5491
  return {
4965
5492
  id,
4966
- label: boundWireText(format.label, MCP_WIRE_LIMITS7.labelCharacters, id),
5493
+ label: boundWireText(format.label, MCP_WIRE_LIMITS8.labelCharacters, id),
4967
5494
  mimeType: format.mimeType,
4968
- extensions: boundWireTextArray(format.extensions, 64, MCP_WIRE_LIMITS7.labelCharacters),
5495
+ extensions: boundWireTextArray(format.extensions, 64, MCP_WIRE_LIMITS8.labelCharacters),
4969
5496
  import: { ...format.import },
4970
5497
  export: { ...format.export }
4971
5498
  };
@@ -4981,6 +5508,7 @@ var init_agentic_tools = __esm({
4981
5508
  init_preview_service();
4982
5509
  init_error_result();
4983
5510
  init_progress();
5511
+ init_revision_service();
4984
5512
  init_output_bounds();
4985
5513
  READ_ONLY = {
4986
5514
  readOnlyHint: true,
@@ -5000,7 +5528,7 @@ var init_agentic_tools = __esm({
5000
5528
  idempotentHint: false,
5001
5529
  openWorldHint: false
5002
5530
  };
5003
- identifierSchema = z.string().min(1).max(MCP_WIRE_LIMITS7.identifierCharacters).regex(MCP_OUTPUT_PATTERNS.identifier);
5531
+ identifierSchema = z.string().min(1).max(MCP_WIRE_LIMITS8.identifierCharacters).regex(MCP_OUTPUT_PATTERNS.identifier);
5004
5532
  fidelitySchemas = {
5005
5533
  editable: z.enum(MCP_FORMAT_FIDELITIES.docx),
5006
5534
  pdf: z.enum(MCP_FORMAT_FIDELITIES.pdf),
@@ -5113,7 +5641,7 @@ var init_agentic_tools = __esm({
5113
5641
 
5114
5642
  // src/mcp/discovery-tools.ts
5115
5643
  import { z as z2 } from "zod";
5116
- import { MCP_WIRE_LIMITS as MCP_WIRE_LIMITS8 } from "@bendyline/docblocks/mcp";
5644
+ import { MCP_WIRE_LIMITS as MCP_WIRE_LIMITS9 } from "@bendyline/docblocks/mcp";
5117
5645
  import { DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS as DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS2 } from "@bendyline/docblocks/mcp/zod";
5118
5646
  function registerDiscoveryTools(server) {
5119
5647
  server.registerTool(
@@ -5148,9 +5676,9 @@ function registerDiscoveryTools(server) {
5148
5676
  try {
5149
5677
  const { getThemeSummaries } = await import("@bendyline/squisq/schemas");
5150
5678
  const result = {
5151
- themes: getThemeSummaries().slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((theme) => ({
5679
+ themes: getThemeSummaries().slice(0, MCP_WIRE_LIMITS9.arrayEntries).map((theme) => ({
5152
5680
  id: requireWireIdentifier(theme.id, "theme id"),
5153
- name: boundWireText(theme.name, MCP_WIRE_LIMITS8.labelCharacters, theme.id),
5681
+ name: boundWireText(theme.name, MCP_WIRE_LIMITS9.labelCharacters, theme.id),
5154
5682
  description: boundWireText(theme.description ?? "")
5155
5683
  }))
5156
5684
  };
@@ -5175,9 +5703,9 @@ function registerDiscoveryTools(server) {
5175
5703
  try {
5176
5704
  const { getTransformStyleSummaries } = await import("@bendyline/squisq/transform");
5177
5705
  const result = {
5178
- styles: getTransformStyleSummaries().slice(0, MCP_WIRE_LIMITS8.arrayEntries).map((style) => ({
5706
+ styles: getTransformStyleSummaries().slice(0, MCP_WIRE_LIMITS9.arrayEntries).map((style) => ({
5179
5707
  id: requireWireIdentifier(style.id, "transform style id"),
5180
- name: boundWireText(style.name, MCP_WIRE_LIMITS8.labelCharacters, style.id),
5708
+ name: boundWireText(style.name, MCP_WIRE_LIMITS9.labelCharacters, style.id),
5181
5709
  description: boundWireText(style.description)
5182
5710
  }))
5183
5711
  };
@@ -5211,16 +5739,16 @@ function registerDiscoveryTools(server) {
5211
5739
  function serializableCapabilities() {
5212
5740
  return MCP_FORMAT_CAPABILITIES.slice(0, 64).map((format) => ({
5213
5741
  id: requireWireFormat(format.id),
5214
- label: boundWireText(format.label, MCP_WIRE_LIMITS8.labelCharacters, format.id),
5742
+ label: boundWireText(format.label, MCP_WIRE_LIMITS9.labelCharacters, format.id),
5215
5743
  mimeType: format.mimeType,
5216
- extensions: boundWireTextArray(format.extensions, 64, MCP_WIRE_LIMITS8.labelCharacters),
5744
+ extensions: boundWireTextArray(format.extensions, 64, MCP_WIRE_LIMITS9.labelCharacters),
5217
5745
  import: { ...format.import },
5218
5746
  export: { ...format.export }
5219
5747
  }));
5220
5748
  }
5221
5749
  function requireWireFormat(value) {
5222
5750
  requireWireIdentifier(value, "format id");
5223
- if (value.length > MCP_WIRE_LIMITS8.formatCharacters || !MCP_OUTPUT_PATTERNS.format.test(value)) {
5751
+ if (value.length > MCP_WIRE_LIMITS9.formatCharacters || !MCP_OUTPUT_PATTERNS.format.test(value)) {
5224
5752
  throw new Error("Linked Squisq returned an invalid MCP format id");
5225
5753
  }
5226
5754
  return value;
@@ -5345,19 +5873,21 @@ function registerAuthoringPrompts(server) {
5345
5873
  function presentationPrompt(topic, style, theme, template) {
5346
5874
  return `Create a presentation about: ${topic}
5347
5875
 
5348
- 1. Use list_formats, list_themes, list_transform_styles, and list_templates for current linked-Squisq capabilities.
5349
- 2. Author focused Markdown sections with accessible alt text. Prefer style "${style ?? "choose from list_transform_styles"}", theme "${theme ?? "choose from list_themes"}", and template "${template ?? "choose per block"}".
5350
- 3. Call validate_document with targetFormat "pptx" and repair actionable diagnostics.
5351
- 4. Call convert_document with a pptx target. Choose editable-native for editable Office structures, rendered-fidelity for exact Squisq visuals, or hybrid for rendered visuals plus semantic retention.
5352
- 5. Call preview_document and inspect the bounded slide images.
5353
- 6. Use get_conversion_report for provenance and save_artifact only when a durable file is required.`;
5876
+ 1. Call get_authoring_context with targetFormat "pptx" and goal "content-first" for the complete linked-Squisq vocabulary and workflow in one response.
5877
+ 2. Author complete Markdown sections with accessible alt text. Put annotations on headings, for example \`# Heading {[content]}\`; a standalone \`{[template]}\` creates an extra block. Prefer style "${style ?? "choose from the returned transform styles"}", theme "${theme ?? "choose from the returned themes"}", and template "${template ?? "content until visual optimization"}".
5878
+ 3. Keep the draft inline or stage Markdown plus assets with create_document_bundle. Do not write a temporary local Markdown file or invent a root id.
5879
+ 4. Call validate_document with targetFormat "pptx" and repair actionable diagnostics before optimizing layouts.
5880
+ 5. Call preview_document and inspect every bounded slide image, especially overflow and body-retention diagnostics.
5881
+ 6. If a staged DBK needs targeted repairs, call inspect_document for block IDs and revise_document with the artifact's exact SHA-256 instead of resending the full Markdown. Only after content coverage is acceptable, replace selected \`content\` blocks with more visual recommended templates and preview again.
5882
+ 7. Call convert_document with a pptx target. Choose editable-native for editable Office structures, rendered-fidelity for exact Squisq visuals, or hybrid for rendered visuals plus semantic retention.
5883
+ 8. Use get_conversion_report for provenance and save_artifact only when a durable file is required.`;
5354
5884
  }
5355
5885
  function videoPrompt(topic, orientation, theme, template) {
5356
5886
  return `Create a video about: ${topic}
5357
5887
 
5358
- 1. Use list_formats, list_themes, list_transform_styles, and list_templates for current linked-Squisq capabilities.
5359
- 2. Author concise Markdown for a ${orientation ?? "landscape"} animated sequence using theme "${theme ?? "choose from list_themes"}" and template "${template ?? "choose per block"}".
5360
- 3. Call validate_document with targetFormat "mp4" and repair actionable diagnostics.
5888
+ 1. Call get_authoring_context with targetFormat "mp4" for the complete linked-Squisq vocabulary and workflow.
5889
+ 2. Author concise Markdown for a ${orientation ?? "landscape"} animated sequence using theme "${theme ?? "choose from the returned themes"}" and template "${template ?? "content until visual optimization"}". Bind annotations to headings.
5890
+ 3. If the draft is staged, use inspect_document plus revise_document for targeted repairs instead of resending the full Markdown. Call validate_document with targetFormat "mp4" and repair actionable diagnostics.
5361
5891
  4. Call preview_document to review representative frames.
5362
5892
  5. Call convert_document with an mp4 target and orientation "${orientation ?? "landscape"}"; monitor progress and honor cancellation.
5363
5893
  6. Use get_conversion_report for provenance and save_artifact only when a durable file is required.`;
@@ -5366,9 +5896,9 @@ function documentPrompt(topic, format, theme, template) {
5366
5896
  const target = format ?? "pdf";
5367
5897
  return `Create a professional document about: ${topic}
5368
5898
 
5369
- 1. Use list_formats, list_themes, list_transform_styles, and list_templates for current linked-Squisq capabilities.
5370
- 2. Author structured Markdown using theme "${theme ?? "choose from list_themes"}" and template "${template ?? "choose per block"}".
5371
- 3. Call validate_document with targetFormat "${target}" and repair actionable diagnostics.
5899
+ 1. Call get_authoring_context with targetFormat "${target}" for the complete linked-Squisq vocabulary and workflow.
5900
+ 2. Author structured Markdown using theme "${theme ?? "choose from the returned themes"}" and template "${template ?? "content until visual optimization"}". Bind annotations to headings.
5901
+ 3. If the draft is staged, use inspect_document plus revise_document for targeted repairs instead of resending the full Markdown. Call validate_document with targetFormat "${target}" and repair actionable diagnostics.
5372
5902
  4. Call convert_document with a ${target} target to create an immutable artifact.
5373
5903
  5. Call preview_document and review the bounded page images.
5374
5904
  6. Use get_conversion_report for provenance and save_artifact only when a durable file is required.`;
@@ -5410,7 +5940,12 @@ function createMcpServer(options = {}) {
5410
5940
  options.shutdownDrainTimeoutMs ?? DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS
5411
5941
  );
5412
5942
  const artifacts = new ArtifactStore(options);
5413
- const server = new McpServer({ name: "docblocks", version: getPackageVersion() });
5943
+ const server = new McpServer(
5944
+ { name: "docblocks", version: getPackageVersion() },
5945
+ {
5946
+ instructions: "Start with get_authoring_context for one-call format, theme, transform, template, syntax, and workflow guidance. Author template annotations on headings, for example `# Heading {[content]}`; standalone `{[template]}` creates an extra heading-less block. DocBlocks MCP defaults ordinary headings to the loss-averse `content` template. Keep authored work in an inline source or stage Markdown plus assets with create_document_bundle. For targeted repairs to a staged DBK, inspect its block ids and call revise_document with the artifact's exact SHA-256 instead of resending the full Markdown. Then validate, preview, convert, and only save the final artifact when durable output is required. Never invent root ids: call list_roots and use an id exactly as returned."
5947
+ }
5948
+ );
5414
5949
  registerAgenticTools(server, {
5415
5950
  authority,
5416
5951
  artifacts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/docblocks-cli",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "Build, preview, convert, render, inspect, and automate documents",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -46,12 +46,12 @@
46
46
  "typecheck": "tsc --noEmit"
47
47
  },
48
48
  "dependencies": {
49
- "@bendyline/docblocks": "2.1.1",
50
- "@bendyline/squisq": "2.3.1",
51
- "@bendyline/squisq-cli": "2.3.1",
52
- "@bendyline/squisq-formats": "2.3.1",
53
- "@bendyline/squisq-react": "2.3.1",
54
- "@bendyline/squisq-video": "2.2.1",
49
+ "@bendyline/docblocks": "2.1.2",
50
+ "@bendyline/squisq": "2.3.2",
51
+ "@bendyline/squisq-cli": "2.3.2",
52
+ "@bendyline/squisq-formats": "2.3.2",
53
+ "@bendyline/squisq-react": "2.3.2",
54
+ "@bendyline/squisq-video": "2.2.2",
55
55
  "@modelcontextprotocol/sdk": "1.29.0",
56
56
  "commander": "13.1.0",
57
57
  "jszip": "3.10.1",