@tiwater/office-mcp 0.20.3 → 0.21.0

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.
package/office/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { createHash, randomUUID } from 'node:crypto';
3
- import { createReadStream } from 'node:fs';
4
- import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, readFile, rename, rm, stat } from 'node:fs/promises';
5
4
  import path from 'node:path';
6
5
  import { isDeepStrictEqual } from 'node:util';
7
6
  import { McpServer } from '@modelcontextprotocol/server';
@@ -15,6 +14,13 @@ import {
15
14
  runJsonCandidateChain,
16
15
  withTempJsonFile,
17
16
  } from '../_shared/tool-runtime.mjs';
17
+ import {
18
+ deliverLargeJsonResult,
19
+ fileArtifact,
20
+ resultChannels,
21
+ returnedContentBudgetBytes,
22
+ writeJsonArtifact,
23
+ } from '../_shared/large-json-result.mjs';
18
24
 
19
25
  const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
20
26
  const inputContractManifest = JSON.parse(await readFile(
@@ -157,8 +163,30 @@ function fixedEditOutput(tool) {
157
163
  }).strict();
158
164
  }
159
165
 
160
- function artifactOutput(tool) {
161
- return z.object({ tool: z.literal(tool), runtime: runtimeIdentity, source: artifact, artifact }).strict();
166
+ function largeResultOutput(tool) {
167
+ return z.object({
168
+ tool: z.literal(tool),
169
+ runtime: runtimeIdentity,
170
+ sources: z.array(artifact).min(1).max(2),
171
+ returnContent: z.boolean(),
172
+ artifact: artifact.nullable(),
173
+ receipt: z.object({
174
+ contentBytes: z.number().int().nonnegative(),
175
+ contentReturned: z.boolean(),
176
+ contentWritten: z.boolean(),
177
+ }).strict(),
178
+ content: z.unknown().optional(),
179
+ }).strict();
180
+ }
181
+
182
+ function docxObservationOutput(tool) {
183
+ return z.object({
184
+ tool: z.literal(tool),
185
+ runtime: runtimeIdentity,
186
+ source: artifact,
187
+ returnContent: z.boolean(),
188
+ artifact: artifact.nullable(),
189
+ }).strict();
162
190
  }
163
191
 
164
192
  const docxAddress = z.object({
@@ -167,14 +195,14 @@ const docxAddress = z.object({
167
195
  }).strict();
168
196
 
169
197
  function docxInspectionOutput(tool) {
170
- return artifactOutput(tool).extend({
198
+ return docxObservationOutput(tool).extend({
171
199
  summary: z.object({
172
200
  tableCount: z.number().int().nonnegative(),
173
201
  openingParagraphs: z.array(z.object({
174
202
  address: docxAddress,
175
203
  textPreview: z.string().min(1).max(240),
176
204
  }).strict()).max(6),
177
- }).strict(),
205
+ }).strict().optional(),
178
206
  }).strict();
179
207
  }
180
208
 
@@ -211,14 +239,14 @@ const docxObservationReceipt = z.object({
211
239
  nextOffset: z.number().int().nonnegative().nullable(),
212
240
  }).strict();
213
241
 
214
- const docxListObjectsOutput = artifactOutput('docx_list_objects').extend({
242
+ const docxListObjectsOutput = docxObservationOutput('docx_list_objects').extend({
215
243
  schema: z.literal('tiwater.docx-observation-list/v1'),
216
244
  receipt: docxObservationReceipt,
217
- objects: z.array(docxObjectIdentity),
245
+ objects: z.array(docxObjectIdentity).optional(),
218
246
  runtime: runtimeIdentity,
219
247
  }).strict();
220
248
 
221
- const docxTableIndexOutput = artifactOutput('docx_table_index').extend({
249
+ const docxTableIndexOutput = docxObservationOutput('docx_table_index').extend({
222
250
  schema: z.literal('tiwater.docx-table-index/v1'),
223
251
  receipt: z.object({
224
252
  schema: z.literal('tiwater.docx-table-index-receipt/v1'),
@@ -244,10 +272,10 @@ const docxTableIndexOutput = artifactOutput('docx_table_index').extend({
244
272
  textPreview: z.string(),
245
273
  textLength: z.number().int().nonnegative(),
246
274
  }).strict().nullable(),
247
- }).strict()),
275
+ }).strict()).optional(),
248
276
  }).strict();
249
277
 
250
- const docxTableReadOutput = artifactOutput('docx_read_table').extend({
278
+ const docxTableReadOutput = docxObservationOutput('docx_read_table').extend({
251
279
  schema: z.literal('tiwater.docx-table-page/v1'),
252
280
  receipt: z.object({
253
281
  schema: z.literal('tiwater.docx-table-page-receipt/v1'),
@@ -255,7 +283,7 @@ const docxTableReadOutput = artifactOutput('docx_read_table').extend({
255
283
  returnedRowCount: z.number().int().nonnegative(),
256
284
  remaining: z.number().int().nonnegative(),
257
285
  nextOffset: z.number().int().nonnegative().nullable(),
258
- detailPageRetained: z.literal(true),
286
+ detailPageRetained: z.boolean(),
259
287
  narrowingRequired: z.boolean(),
260
288
  }).strict(),
261
289
  address: docxAddress,
@@ -290,10 +318,10 @@ const docxTableReadOutput = artifactOutput('docx_read_table').extend({
290
318
  text: z.string(),
291
319
  logicalText: z.string(),
292
320
  }).strict()),
293
- }).strict()),
321
+ }).strict()).optional(),
294
322
  }).strict();
295
323
 
296
- const docxReadObjectOutput = artifactOutput('docx_read_object').extend({
324
+ const docxReadObjectOutput = docxObservationOutput('docx_read_object').extend({
297
325
  receipt: z.object({
298
326
  schema: z.literal('tiwater.docx-read-object-receipt/v1'),
299
327
  observationCount: z.number().int().positive(),
@@ -301,13 +329,13 @@ const docxReadObjectOutput = artifactOutput('docx_read_object').extend({
301
329
  responseComplete: z.boolean(),
302
330
  narrowingRequired: z.boolean(),
303
331
  }).strict(),
304
- observations: z.array(docxObservationNode),
332
+ observations: z.array(docxObservationNode).optional(),
305
333
  }).strict();
306
334
 
307
335
  const tools = [
308
336
  {
309
337
  name: 'docx_inspect',
310
- description: 'Inspect one current DOCX and retain its complete machine observation at output. The response returns the observation artifact, table count, and up to six opening non-empty body paragraphs with their OpenXML addresses. Use list and read operations to traverse selected document objects in native structure order.',
338
+ description: 'Inspect one current DOCX. Set returnContent true to return the compact identity summary. Provide output to retain the complete machine observation and return its artifact receipt. These channels are independent and may be used together. At least one channel is required. Use list and read operations to traverse selected document objects in native structure order.',
311
339
  inputSchema: inputContract('docx_inspect'),
312
340
  outputSchema: docxInspectionOutput('docx_inspect'),
313
341
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -315,7 +343,7 @@ const tools = [
315
343
  },
316
344
  {
317
345
  name: 'docx_list_objects',
318
- description: 'Page through mixed nearest-child OpenXML objects when document order or paragraph relationships are required. The complete requested provider page is retained at output; the response returns a byte-bounded prefix. Continue with receipt.nextOffset and descend through a returned parent address. Do not use this tool to locate tables or read a whole document: use docx_table_index to locate tables, then docx_read_table for one selected table.',
346
+ description: 'Page through mixed nearest-child OpenXML objects only when document order or paragraph relationships are required. Set returnContent true to return a byte-bounded object page. Provide output to store the complete requested page and return its artifact receipt. These channels are independent and may be used together; at least one is required. Continue from receipt.nextOffset only when an unreturned object is needed for the current decision. Do not use this tool to locate tables or read a whole document: use docx_table_index to locate tables, then a narrow table or object read.',
319
347
  inputSchema: inputContract('docx_list_objects'),
320
348
  outputSchema: docxListObjectsOutput,
321
349
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -323,7 +351,7 @@ const tools = [
323
351
  },
324
352
  {
325
353
  name: 'docx_table_index',
326
- description: 'Locate tables in one current DOCX. Writes the complete index to output and returns a bounded page containing table address, shape, short text preview, and nearest non-empty paragraphs. Continue with receipt.nextOffset, then call docx_read_table for one selected address. It does not return full cell content or decide table semantics.',
354
+ description: 'Locate tables in one current DOCX without returning full cell content or deciding table semantics. Set returnContent true to return a bounded page of addresses, shapes, short previews, and nearest non-empty paragraphs. Provide output to store the complete index and return its artifact receipt. These channels are independent and may be used together; at least one is required. Continue from receipt.nextOffset only when an unreturned table is needed for the current decision, then read one selected native address.',
327
355
  inputSchema: inputContract('docx_table_index'),
328
356
  outputSchema: docxTableIndexOutput,
329
357
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -331,7 +359,7 @@ const tools = [
331
359
  },
332
360
  {
333
361
  name: 'docx_read_object',
334
- description: 'Read selected rows, cells, or paragraphs from one native DOCX and retain the complete result at output. A selected cell exposes its vertical-merge owner and logical text, so a continue cell keeps its physical identity while resolving the restart cell value. The response returns compact requested descendants when bounded; if receipt.narrowingRequired is true, request fewer observed addresses or descendants. Use docx_read_table for a table.',
362
+ description: 'Read explicitly selected rows, cells, or paragraphs from one native DOCX. Set returnContent true to return compact requested descendants; if receipt.narrowingRequired is true, request fewer addresses or descendant kinds. Provide output to store the complete selected observation and return its artifact receipt. These channels are independent and may be used together; at least one is required. A selected cell exposes its vertical-merge owner and logical text, so a continue cell keeps its physical identity while resolving the restart cell value. Use docx_read_table for a table range.',
335
363
  inputSchema: inputContract('docx_read_object'),
336
364
  outputSchema: docxReadObjectOutput,
337
365
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -339,7 +367,7 @@ const tools = [
339
367
  },
340
368
  {
341
369
  name: 'docx_read_table',
342
- description: 'Read exactly one table selected from docx_table_index by native OpenXML address. Each call retains full paragraph and text-node detail for exactly the returned row page at output; it never builds another whole-table data object. The machine response is the compact form of that page: each row and cell keeps its reusable native address, grid position and span, vertical-merge owner, physical text, and logical text. In a vertical merge, restart begins one logical cell and a continue cell is not an independent row value: logicalText resolves the restart cell value while text remains the physical cell value. A table observation is complete only when receipt.remaining is 0; process each page once in ascending receipt.nextOffset order. Use docx_read_object only when one selected object needs a narrower descendant view. The provider reports physical structure only; the Agent decides the template and business meaning.',
370
+ description: 'Read an explicit row range from exactly one table selected by native OpenXML address; it never builds another whole-table data object. Set returnContent true to return a byte-bounded compact row page. Provide output to store full paragraph and text-node detail for the selected row page and return its artifact receipt. These channels are independent and may be used together; at least one is required. Request only rows needed for the current decision; receipt.remaining is navigation information, not an obligation to read unused rows, and blank template rows need not be paged through. Each returned row and cell keeps its reusable native address, zero-based logical gridColumnStart, gridSpan, vertical-merge owner, physical text, and logical text. Match columns across rows by gridColumnStart; a tc[n] path or array position is only that row\'s physical cell ordinal and is not a column identity when gridSpan or gridBefore is present. In a vertical merge, restart begins one logical cell and a continue cell is not an independent row value: logicalText resolves the restart cell value while text remains the physical cell value. Use docx_read_object when one exact object needs a narrower descendant view. The provider reports physical structure only; the Agent decides template and business meaning.',
343
371
  inputSchema: inputContract('docx_read_table'),
344
372
  outputSchema: docxTableReadOutput,
345
373
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -375,7 +403,7 @@ const tools = [
375
403
  },
376
404
  {
377
405
  name: 'docx_merge_cells',
378
- description: 'Merge selected current DOCX cells when they form one closed rectangle.',
406
+ description: 'Merge selected current DOCX cells when they form one closed rectangle. A one-column, multi-row rectangle creates a vertical merge whose first cell is the restart owner and whose later cells are continuations. All selected cell content moves into the top-left owner, so the selected content must already be correct for that one logical cell.',
379
407
  inputSchema: inputContract('docx_merge_cells'),
380
408
  outputSchema: fixedEditOutput('docx_merge_cells'),
381
409
  handler: args => fixedEdit('docx_merge_cells', args),
@@ -403,30 +431,33 @@ const tools = [
403
431
  },
404
432
  {
405
433
  name: 'docx_compare',
406
- description: 'Compare two DOCX files and report package, metric, and style differences.',
434
+ description: 'Compare two DOCX files and report package, metric, and style differences. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
407
435
  inputSchema: inputContract('docx_compare'),
436
+ outputSchema: largeResultOutput('docx_compare'),
408
437
  annotations: { readOnlyHint: true, idempotentHint: true },
409
438
  handler: docxCompare,
410
439
  },
411
440
  {
412
441
  name: 'docx_export_json',
413
- description: 'Write a body-only DOCX JSON projection only when a downstream consumer explicitly requires that format. It is not a companion to docx_inspect and does not replace bounded list/find/read object selection.',
442
+ description: 'Produce a body-only DOCX JSON projection only when a downstream consumer explicitly requires that format. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required. This does not replace bounded list and read operations.',
414
443
  inputSchema: inputContract('docx_export_json'),
415
- outputSchema: artifactOutput('docx_export_json'),
444
+ outputSchema: largeResultOutput('docx_export_json'),
416
445
  annotations: { readOnlyHint: true, idempotentHint: true },
417
446
  handler: docxExportJson,
418
447
  },
419
448
  {
420
449
  name: 'docx_validate',
421
- description: 'Validate a current DOCX package against the published OpenXML contract.',
450
+ description: 'Validate a current DOCX package against the published OpenXML contract. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
422
451
  inputSchema: inputContract('docx_validate'),
452
+ outputSchema: largeResultOutput('docx_validate'),
423
453
  annotations: { readOnlyHint: true, idempotentHint: true },
424
454
  handler: docxValidate,
425
455
  },
426
456
  {
427
457
  name: 'docx_validate_font_policy',
428
- description: 'Validate current DOCX text against an explicit font policy.',
458
+ description: 'Validate current DOCX text against an explicit font policy. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
429
459
  inputSchema: inputContract('docx_validate_font_policy'),
460
+ outputSchema: largeResultOutput('docx_validate_font_policy'),
430
461
  annotations: { readOnlyHint: true, idempotentHint: true },
431
462
  handler: docxValidateFontPolicy,
432
463
  },
@@ -439,8 +470,9 @@ const tools = [
439
470
  },
440
471
  {
441
472
  name: 'docx_validate_toc_style_policy',
442
- description: 'Validate current DOCX table-of-contents paragraph styles against an explicit policy.',
473
+ description: 'Validate current DOCX table-of-contents paragraph styles against an explicit policy. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
443
474
  inputSchema: inputContract('docx_validate_toc_style_policy'),
475
+ outputSchema: largeResultOutput('docx_validate_toc_style_policy'),
444
476
  annotations: { readOnlyHint: true, idempotentHint: true },
445
477
  handler: docxValidateTocStylePolicy,
446
478
  },
@@ -485,41 +517,42 @@ const tools = [
485
517
  },
486
518
  {
487
519
  name: 'xlsx_inspect',
488
- description: 'Inspect a current XLSX workbook or legacy XLS workbook and write one JSON observation containing workbook structure, exported values, formulas, styles, merged ranges, and any published legacy-format conversion evidence.',
520
+ description: 'Inspect a current XLSX workbook or legacy XLS workbook, including workbook structure, exported values, formulas, styles, merged ranges, and published legacy-format conversion evidence. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
489
521
  inputSchema: inputContract('xlsx_inspect'),
490
- outputSchema: artifactOutput('xlsx_inspect'),
522
+ outputSchema: largeResultOutput('xlsx_inspect'),
491
523
  annotations: { readOnlyHint: true, idempotentHint: true },
492
524
  handler: xlsxInspect,
493
525
  },
494
526
  {
495
527
  name: 'xlsx_export_json',
496
- description: 'Export workbook sheet data from XLSX as structured JSON.',
528
+ description: 'Export workbook sheet data from XLSX as structured JSON. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
497
529
  inputSchema: inputContract('xlsx_export_json'),
498
- outputSchema: artifactOutput('xlsx_export_json'),
530
+ outputSchema: largeResultOutput('xlsx_export_json'),
499
531
  annotations: { readOnlyHint: true, idempotentHint: true },
500
532
  handler: xlsxExportJson,
501
533
  },
502
534
  ...fixedToolDefinitions(xlsxFixedTools),
503
535
  {
504
536
  name: 'xlsx_validate',
505
- description: 'Validate an XLSX workbook package and return Open XML validation evidence.',
537
+ description: 'Validate an XLSX workbook package and produce OpenXML validation evidence. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
506
538
  inputSchema: inputContract('xlsx_validate'),
539
+ outputSchema: largeResultOutput('xlsx_validate'),
507
540
  annotations: { readOnlyHint: true, idempotentHint: true },
508
541
  handler: xlsxValidate,
509
542
  },
510
543
  {
511
544
  name: 'pptx_inspect',
512
- description: 'Inspect a PPTX file and write one detailed JSON observation containing slides, masters, layouts, shapes, transforms, paragraphs, runs, and placeholders.',
545
+ description: 'Inspect a PPTX file, including slides, masters, layouts, shapes, transforms, paragraphs, runs, and placeholders. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
513
546
  inputSchema: inputContract('pptx_inspect'),
514
- outputSchema: artifactOutput('pptx_inspect'),
547
+ outputSchema: largeResultOutput('pptx_inspect'),
515
548
  annotations: { readOnlyHint: true, idempotentHint: true },
516
549
  handler: pptxInspect,
517
550
  },
518
551
  {
519
552
  name: 'pptx_export_json',
520
- description: 'Export PPTX slide text, notes, and placeholder hints to a new JSON artifact without returning the full presentation through MCP.',
553
+ description: 'Export PPTX slide text, notes, and placeholder hints as structured JSON. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
521
554
  inputSchema: inputContract('pptx_export_json'),
522
- outputSchema: artifactOutput('pptx_export_json'),
555
+ outputSchema: largeResultOutput('pptx_export_json'),
523
556
  annotations: { readOnlyHint: true, idempotentHint: true },
524
557
  handler: pptxExportJson,
525
558
  },
@@ -553,8 +586,9 @@ const tools = [
553
586
  },
554
587
  {
555
588
  name: 'pptx_validate',
556
- description: 'Validate a current PPTX package against the published OpenXML contract.',
589
+ description: 'Validate a current PPTX package against the published OpenXML contract. Set returnContent true to return the complete result when it fits the response limit. Provide output to write the complete result to a new JSON file. The two choices are independent and may be used together; at least one is required.',
557
590
  inputSchema: inputContract('pptx_validate'),
591
+ outputSchema: largeResultOutput('pptx_validate'),
558
592
  annotations: { readOnlyHint: true, idempotentHint: true },
559
593
  handler: pptxValidate,
560
594
  },
@@ -618,6 +652,24 @@ function compactDocxObjectIdentity(object) {
618
652
  };
619
653
  }
620
654
 
655
+ function compactTableIndexEntry(table) {
656
+ const paragraph = value => value === null ? null : {
657
+ address: value.address,
658
+ textPreview: value.textPreview.trim().replace(/\s+/gu, ' ').slice(0, 160),
659
+ textLength: value.textLength,
660
+ };
661
+ return {
662
+ address: table.address,
663
+ parentAddress: table.parentAddress,
664
+ rowCount: table.rowCount,
665
+ columnCount: table.columnCount,
666
+ textPreview: table.textPreview.trim().replace(/\s+/gu, ' ').slice(0, 240),
667
+ textLength: table.textLength,
668
+ precedingParagraph: paragraph(table.precedingParagraph),
669
+ followingParagraph: paragraph(table.followingParagraph),
670
+ };
671
+ }
672
+
621
673
  function compactDocxObservation(observation) {
622
674
  if (!observation?.object) throw new Error('docx-read-object-missing-observation');
623
675
  function compactNode(node) {
@@ -642,20 +694,24 @@ function compactDocxObservation(observation) {
642
694
 
643
695
  async function docxInspect(args) {
644
696
  const input = path.resolve(requireString(args.input, 'input'));
697
+ const delivery = resultChannels(args);
645
698
  const result = await runJsonCandidateChain(docxCandidates, ['inspect', input, '--json']);
646
699
  return {
647
700
  tool: 'docx_inspect',
648
701
  runtime: commandRuntime(result),
649
702
  source: await fileArtifact(input),
650
- artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
651
- ...compactDocxInspection(result.json),
703
+ returnContent: delivery.returnContent,
704
+ artifact: delivery.output === null
705
+ ? null
706
+ : await writeJsonArtifact(delivery.output, result.json),
707
+ ...(delivery.returnContent ? compactDocxInspection(result.json) : {}),
652
708
  };
653
709
  }
654
710
 
655
711
  async function docxObservation(tool, args) {
656
712
  const input = path.resolve(requireString(args.input, 'input'));
657
- const output = requireString(args.output, 'output');
658
- const { output: _output, ...request } = args;
713
+ const delivery = resultChannels(args);
714
+ const { output: _output, returnContent: _returnContent, ...request } = args;
659
715
  return withTempJsonFile(request, async requestPath => {
660
716
  const result = await runJsonCandidateChain(docxCandidates, [tool, requestPath]);
661
717
  const payload = { ...result.json, runtime: commandRuntime(result) };
@@ -663,22 +719,33 @@ async function docxObservation(tool, args) {
663
719
  tool,
664
720
  runtime: payload.runtime,
665
721
  source: await fileArtifact(input),
666
- ...(tool === 'docx_read_table' ? {} : {
667
- artifact: await writeJsonArtifact(output, result.json),
668
- }),
722
+ returnContent: delivery.returnContent,
723
+ artifact: null,
669
724
  };
670
725
  if (tool === 'docx_list_objects') {
671
726
  const totalCount = payload.receipt.totalCount;
672
727
  const offset = Math.min(args.offset ?? 0, totalCount);
728
+ const withArtifact = delivery.output === null ? retained : {
729
+ ...retained,
730
+ artifact: await writeJsonArtifact(delivery.output, result.json),
731
+ };
732
+ if (!delivery.returnContent) {
733
+ return {
734
+ ...withArtifact,
735
+ schema: payload.schema,
736
+ receipt: payload.receipt,
737
+ };
738
+ }
673
739
  const objects = [];
674
740
  for (const sourceObject of payload.objects.map(compactDocxObjectIdentity)) {
675
741
  const candidate = [...objects, sourceObject];
676
- if (objects.length > 0 && Buffer.byteLength(JSON.stringify(candidate)) > 6_500) break;
742
+ if (objects.length > 0
743
+ && Buffer.byteLength(JSON.stringify(candidate)) > returnedContentBudgetBytes) break;
677
744
  objects.push(sourceObject);
678
745
  }
679
746
  const nextOffset = offset + objects.length < totalCount ? offset + objects.length : null;
680
747
  return {
681
- ...retained,
748
+ ...withArtifact,
682
749
  schema: payload.schema,
683
750
  receipt: {
684
751
  schema: 'tiwater.docx-observation-receipt/v1',
@@ -693,17 +760,36 @@ async function docxObservation(tool, args) {
693
760
  }
694
761
  if (tool === 'docx_table_index') {
695
762
  const totalCount = payload.tables.length;
763
+ const withArtifact = delivery.output === null ? retained : {
764
+ ...retained,
765
+ artifact: await writeJsonArtifact(delivery.output, result.json),
766
+ };
767
+ if (!delivery.returnContent) {
768
+ return {
769
+ ...withArtifact,
770
+ schema: payload.schema,
771
+ receipt: {
772
+ schema: 'tiwater.docx-table-index-receipt/v1',
773
+ totalCount,
774
+ returnedCount: totalCount,
775
+ remaining: 0,
776
+ nextOffset: null,
777
+ },
778
+ };
779
+ }
696
780
  const offset = Math.min(args.offset ?? 0, totalCount);
697
781
  const requestedLimit = args.limit ?? totalCount;
698
782
  const tables = [];
699
- for (const table of payload.tables.slice(offset, offset + requestedLimit)) {
783
+ for (const sourceTable of payload.tables.slice(offset, offset + requestedLimit)) {
784
+ const table = compactTableIndexEntry(sourceTable);
700
785
  const candidate = [...tables, table];
701
- if (tables.length > 0 && Buffer.byteLength(JSON.stringify(candidate)) > 7_000) break;
786
+ if (tables.length > 0
787
+ && Buffer.byteLength(JSON.stringify(candidate)) > returnedContentBudgetBytes) break;
702
788
  tables.push(table);
703
789
  }
704
790
  const nextOffset = offset + tables.length < totalCount ? offset + tables.length : null;
705
791
  return {
706
- ...retained,
792
+ ...withArtifact,
707
793
  schema: payload.schema,
708
794
  receipt: {
709
795
  schema: 'tiwater.docx-table-index-receipt/v1',
@@ -719,9 +805,52 @@ async function docxObservation(tool, args) {
719
805
  const totalRowCount = payload.rows.length;
720
806
  const offset = Math.min(args.offset ?? 0, totalRowCount);
721
807
  const requestedLimit = args.limit ?? totalRowCount;
808
+ const selectedRows = payload.rows.slice(offset, offset + requestedLimit);
809
+ const selectedNextOffset = offset + selectedRows.length < totalRowCount
810
+ ? offset + selectedRows.length
811
+ : null;
812
+ const selectedPageReceipt = {
813
+ schema: 'tiwater.docx-table-page-receipt/v1',
814
+ totalRowCount,
815
+ returnedRowCount: selectedRows.length,
816
+ remaining: totalRowCount - offset - selectedRows.length,
817
+ nextOffset: selectedNextOffset,
818
+ };
819
+ const detailPage = {
820
+ schema: 'tiwater.docx-table-detail-page/v1',
821
+ address: payload.address,
822
+ rowCount: payload.rowCount,
823
+ columnCount: payload.columnCount,
824
+ gridColumns: payload.gridColumns,
825
+ precedingParagraph: payload.precedingParagraph,
826
+ followingParagraph: payload.followingParagraph,
827
+ receipt: selectedPageReceipt,
828
+ rows: selectedRows,
829
+ };
830
+ const withArtifact = delivery.output === null ? retained : {
831
+ ...retained,
832
+ artifact: await writeJsonArtifact(delivery.output, detailPage),
833
+ };
834
+ if (!delivery.returnContent) {
835
+ return {
836
+ ...withArtifact,
837
+ schema: 'tiwater.docx-table-page/v1',
838
+ address: payload.address,
839
+ rowCount: payload.rowCount,
840
+ columnCount: payload.columnCount,
841
+ gridColumns: payload.gridColumns,
842
+ precedingParagraph: payload.precedingParagraph,
843
+ followingParagraph: payload.followingParagraph,
844
+ receipt: {
845
+ ...selectedPageReceipt,
846
+ detailPageRetained: true,
847
+ narrowingRequired: false,
848
+ },
849
+ };
850
+ }
722
851
  const rows = [];
723
852
  let narrowingRequired = false;
724
- for (const sourceRow of payload.rows.slice(offset, offset + requestedLimit)) {
853
+ for (const sourceRow of selectedRows) {
725
854
  const row = {
726
855
  address: sourceRow.address,
727
856
  repeatHeader: sourceRow.repeatHeader,
@@ -739,7 +868,7 @@ async function docxObservation(tool, args) {
739
868
  })),
740
869
  };
741
870
  const candidate = [...rows, row];
742
- if (Buffer.byteLength(JSON.stringify(candidate)) > 6_500) {
871
+ if (Buffer.byteLength(JSON.stringify(candidate)) > returnedContentBudgetBytes) {
743
872
  narrowingRequired = rows.length === 0;
744
873
  break;
745
874
  }
@@ -755,20 +884,8 @@ async function docxObservation(tool, args) {
755
884
  remaining: totalRowCount - offset - rows.length,
756
885
  nextOffset,
757
886
  };
758
- const detailPage = {
759
- schema: 'tiwater.docx-table-detail-page/v1',
760
- address: payload.address,
761
- rowCount: payload.rowCount,
762
- columnCount: payload.columnCount,
763
- gridColumns: payload.gridColumns,
764
- precedingParagraph: payload.precedingParagraph,
765
- followingParagraph: payload.followingParagraph,
766
- receipt: pageReceipt,
767
- rows: payload.rows.slice(offset, offset + rows.length),
768
- };
769
887
  return {
770
- ...retained,
771
- artifact: await writeJsonArtifact(output, detailPage),
888
+ ...withArtifact,
772
889
  schema: 'tiwater.docx-table-page/v1',
773
890
  address: payload.address,
774
891
  rowCount: payload.rowCount,
@@ -778,7 +895,7 @@ async function docxObservation(tool, args) {
778
895
  followingParagraph: payload.followingParagraph,
779
896
  receipt: {
780
897
  ...pageReceipt,
781
- detailPageRetained: true,
898
+ detailPageRetained: delivery.output !== null,
782
899
  narrowingRequired,
783
900
  },
784
901
  rows,
@@ -790,46 +907,53 @@ async function docxObservation(tool, args) {
790
907
 
791
908
  async function docxReadObject(args) {
792
909
  const input = path.resolve(requireString(args.input, 'input'));
793
- const output = requireString(args.output, 'output');
794
- const { output: _output, ...request } = args;
910
+ const delivery = resultChannels(args);
911
+ const { output: _output, returnContent: _returnContent, ...request } = args;
795
912
  return withTempJsonFile(request, async requestPath => {
796
913
  const result = await runJsonCandidateChain(docxCandidates, ['docx_read_object', requestPath]);
797
914
  const observations = result.json.observations.map(compactDocxObservation);
798
- const responseComplete = Buffer.byteLength(JSON.stringify(observations)) <= 6_500;
915
+ const responseComplete = delivery.returnContent
916
+ && Buffer.byteLength(JSON.stringify(observations)) <= returnedContentBudgetBytes;
799
917
  return {
800
918
  tool: 'docx_read_object',
801
919
  runtime: commandRuntime(result),
802
920
  source: await fileArtifact(input),
803
- artifact: await writeJsonArtifact(output, result.json),
921
+ returnContent: delivery.returnContent,
922
+ artifact: delivery.output === null
923
+ ? null
924
+ : await writeJsonArtifact(delivery.output, result.json),
804
925
  receipt: {
805
926
  schema: 'tiwater.docx-read-object-receipt/v1',
806
927
  observationCount: observations.length,
807
928
  returnedCount: responseComplete ? observations.length : 0,
808
929
  responseComplete,
809
- narrowingRequired: !responseComplete,
930
+ narrowingRequired: delivery.returnContent && !responseComplete,
810
931
  },
811
- observations: responseComplete ? observations : [],
932
+ ...(responseComplete ? { observations } : {}),
812
933
  };
813
934
  });
814
935
  }
815
936
 
816
937
  async function docxValidate(args) {
817
- const result = await runJsonCandidateChain(docxCandidates, ['validate-openxml', requireString(args.input, 'input')], { allowedExitCodes: [0, 1] });
818
- return { tool: 'docx_validate', runtime: commandRuntime(result), result: result.json };
938
+ const input = path.resolve(requireString(args.input, 'input'));
939
+ const result = await runJsonCandidateChain(docxCandidates, ['validate-openxml', input], { allowedExitCodes: [0, 1] });
940
+ return deliverLargeJsonResult({ tool: 'docx_validate', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
819
941
  }
820
942
 
821
943
  async function docxValidateFontPolicy(args) {
822
944
  return withTempJsonFile(args.policy, async policyPath => {
823
- const result = await runJsonCandidateChain(docxCandidates, ['validate-font-policy', requireString(args.input, 'input'), policyPath], { allowedExitCodes: [0, 1] });
824
- return { tool: 'docx_validate_font_policy', runtime: commandRuntime(result), result: result.json };
945
+ const input = path.resolve(requireString(args.input, 'input'));
946
+ const result = await runJsonCandidateChain(docxCandidates, ['validate-font-policy', input, policyPath], { allowedExitCodes: [0, 1] });
947
+ return deliverLargeJsonResult({ tool: 'docx_validate_font_policy', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
825
948
  });
826
949
  }
827
950
 
828
951
  async function docxValidateTocStylePolicy(args) {
952
+ const input = path.resolve(requireString(args.input, 'input'));
829
953
  const result = await runJsonCandidateChain(docxCandidates, [
830
- 'validate-toc-style-policy', requireString(args.input, 'input'), String(args.italic), String(args.indentCharactersPerLevel),
954
+ 'validate-toc-style-policy', input, String(args.italic), String(args.indentCharactersPerLevel),
831
955
  ], { allowedExitCodes: [0, 1] });
832
- return { tool: 'docx_validate_toc_style_policy', runtime: commandRuntime(result), result: result.json };
956
+ return deliverLargeJsonResult({ tool: 'docx_validate_toc_style_policy', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
833
957
  }
834
958
 
835
959
  async function docxStripDirectFormatting(args) {
@@ -925,21 +1049,16 @@ async function fixedEdit(tool, args) {
925
1049
  }
926
1050
 
927
1051
  async function docxCompare(args) {
928
- const baseline = requireString(args.baseline, 'baseline');
929
- const updated = requireString(args.updated, 'updated');
1052
+ const baseline = path.resolve(requireString(args.baseline, 'baseline'));
1053
+ const updated = path.resolve(requireString(args.updated, 'updated'));
930
1054
  const result = await runJsonCandidateChain(docxCandidates, ['compare', baseline, updated, '--json']);
931
- return { tool: 'docx_compare', runtime: commandRuntime(result), report: result.json };
1055
+ return deliverLargeJsonResult({ tool: 'docx_compare', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [baseline, updated] });
932
1056
  }
933
1057
 
934
1058
  async function docxExportJson(args) {
935
1059
  const input = path.resolve(requireString(args.input, 'input'));
936
1060
  const result = await runJsonCandidateChain(docxCandidates, ['export-json', input]);
937
- return {
938
- tool: 'docx_export_json',
939
- runtime: commandRuntime(result),
940
- source: await fileArtifact(input),
941
- artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
942
- };
1061
+ return deliverLargeJsonResult({ tool: 'docx_export_json', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
943
1062
  }
944
1063
 
945
1064
  async function officeRenderPdf(args) {
@@ -1027,12 +1146,7 @@ function nativeRenderBackend(sourceFormat) {
1027
1146
  async function xlsxInspect(args) {
1028
1147
  const input = path.resolve(requireString(args.input, 'input'));
1029
1148
  const result = await runJsonCandidateChain(xlsxCandidates, ['inspect', input, '--json']);
1030
- return {
1031
- tool: 'xlsx_inspect',
1032
- runtime: commandRuntime(result),
1033
- source: await fileArtifact(input),
1034
- artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
1035
- };
1149
+ return deliverLargeJsonResult({ tool: 'xlsx_inspect', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1036
1150
  }
1037
1151
 
1038
1152
  async function xlsxExportJson(args) {
@@ -1042,45 +1156,31 @@ async function xlsxExportJson(args) {
1042
1156
  cmdArgs.push('--resolve-merged-cells');
1043
1157
  }
1044
1158
  const result = await runJsonCandidateChain(xlsxCandidates, cmdArgs);
1045
- return {
1046
- tool: 'xlsx_export_json',
1047
- runtime: commandRuntime(result),
1048
- source: await fileArtifact(input),
1049
- artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
1050
- };
1159
+ return deliverLargeJsonResult({ tool: 'xlsx_export_json', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1051
1160
  }
1052
1161
 
1053
1162
  async function xlsxValidate(args) {
1054
- const input = requireString(args.input, 'input');
1163
+ const input = path.resolve(requireString(args.input, 'input'));
1055
1164
  const result = await runJsonCandidateChain(xlsxCandidates, ['validate', input], { allowedExitCodes: [0, 1] });
1056
- return { tool: 'xlsx_validate', runtime: commandRuntime(result), result: result.json };
1165
+ return deliverLargeJsonResult({ tool: 'xlsx_validate', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1057
1166
  }
1058
1167
 
1059
1168
  async function pptxInspect(args) {
1060
1169
  const input = path.resolve(requireString(args.input, 'input'));
1061
1170
  const result = await runJsonCandidateChain(pptxCandidates, ['inspect', input, '--json']);
1062
- return {
1063
- tool: 'pptx_inspect',
1064
- runtime: commandRuntime(result),
1065
- source: await fileArtifact(input),
1066
- artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
1067
- };
1171
+ return deliverLargeJsonResult({ tool: 'pptx_inspect', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1068
1172
  }
1069
1173
 
1070
1174
  async function pptxExportJson(args) {
1071
1175
  const input = path.resolve(requireString(args.input, 'input'));
1072
1176
  const result = await runJsonCandidateChain(pptxCandidates, ['export-json', input]);
1073
- return {
1074
- tool: 'pptx_export_json',
1075
- runtime: commandRuntime(result),
1076
- source: await fileArtifact(input),
1077
- artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
1078
- };
1177
+ return deliverLargeJsonResult({ tool: 'pptx_export_json', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1079
1178
  }
1080
1179
 
1081
1180
  async function pptxValidate(args) {
1082
- const result = await runJsonCandidateChain(pptxCandidates, ['validate', requireString(args.input, 'input')], { allowedExitCodes: [0, 1] });
1083
- return { tool: 'pptx_validate', runtime: commandRuntime(result), result: result.json };
1181
+ const input = path.resolve(requireString(args.input, 'input'));
1182
+ const result = await runJsonCandidateChain(pptxCandidates, ['validate', input], { allowedExitCodes: [0, 1] });
1183
+ return deliverLargeJsonResult({ tool: 'pptx_validate', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
1084
1184
  }
1085
1185
 
1086
1186
  async function requireReturnedArtifact(returned, expectedPath, label) {
@@ -1093,31 +1193,6 @@ async function requireReturnedArtifact(returned, expectedPath, label) {
1093
1193
  }
1094
1194
  }
1095
1195
 
1096
- async function writeJsonArtifact(output, payload) {
1097
- const fullPath = path.resolve(output);
1098
- await mkdir(path.dirname(fullPath), { recursive: true });
1099
- const bytes = Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf8');
1100
- await writeFile(fullPath, bytes, { flag: 'wx' });
1101
- return {
1102
- path: fullPath,
1103
- sha256: createHash('sha256').update(bytes).digest('hex'),
1104
- bytes: bytes.length,
1105
- };
1106
- }
1107
-
1108
- async function fileArtifact(filePath) {
1109
- const hash = createHash('sha256');
1110
- for await (const chunk of createReadStream(filePath)) {
1111
- hash.update(chunk);
1112
- }
1113
- const file = await stat(filePath);
1114
- return {
1115
- path: path.resolve(filePath),
1116
- sha256: hash.digest('hex'),
1117
- bytes: file.size,
1118
- };
1119
- }
1120
-
1121
1196
  async function requireNewFile(filePath, label) {
1122
1197
  try {
1123
1198
  await stat(filePath);