@dieulc/pi-office-protocol 0.2.0 → 0.3.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.
@@ -0,0 +1,1342 @@
1
+ /**
2
+ * Office op catalog — the single source of truth for every operation the Pi
3
+ * agent can drive through the office bridge.
4
+ *
5
+ * Both sides derive from this file:
6
+ *
7
+ * - The Pi extension (`@dieulc/pi-office-bridge`) registers one
8
+ * `office_<host>_<op>` tool per entry.
9
+ * - The task-pane add-in builds its bridge op registry (`ALL_BRIDGE_OPS`)
10
+ * from the same op ids and delegates execution to the shared local tool
11
+ * implementations. Word/PowerPoint local tools import the parameter
12
+ * schemas from here, so bridge and local mode can never drift.
13
+ *
14
+ * The catalog is **server-authoritative**: a pane may only *narrow* the set of
15
+ * ops it advertises in `hello` — it never supplies tool names, schemas, or
16
+ * descriptions. This keeps the loopback WebSocket surface safe against a rogue
17
+ * local page trying to inject tool definitions into the agent.
18
+ */
19
+
20
+ import { Type, type TLiteral, type TSchema, type TUnion } from "typebox";
21
+ import type { OfficeHostApp } from "./protocol.js";
22
+
23
+ export interface OfficeCatalogEntry {
24
+ /** Host app that can execute this op. */
25
+ host: OfficeHostApp;
26
+ /** Payload op id, namespaced by host: "excel.read_range". */
27
+ op: string;
28
+ /** Pi-registered tool name, e.g. "office_excel_read_range". */
29
+ name: string;
30
+ /** Short UI/command label. */
31
+ label: string;
32
+ /** Long description shown to the LLM. */
33
+ description: string;
34
+ /** One-line description listed under "Available tools". */
35
+ promptSnippet?: string;
36
+ /** Tool-specific guideline bullets (pi appends them flat; each must name the tool). */
37
+ promptGuidelines?: string[];
38
+ /** TypeBox schema for the tool's arguments. */
39
+ parameters: TSchema;
40
+ /** Catalog version this entry was introduced in. */
41
+ since: number;
42
+ /** True when the op was exposed by the legacy 0.2.x bridge (v1 op set). */
43
+ legacyV1?: boolean;
44
+ }
45
+
46
+ /** Increment when entries or schemas change incompatibly. */
47
+ export const CATALOG_VERSION = 1;
48
+
49
+ /** Office apps we expose tools for (map host → tool prefix). */
50
+ export const HOST_APP_LABEL: Record<OfficeHostApp, string> = {
51
+ excel: "Excel",
52
+ word: "Word",
53
+ powerpoint: "PowerPoint",
54
+ };
55
+
56
+ /** String enum helper (protocol must not depend on pi-ai's StringEnum). */
57
+ export function catalogEnum<T extends readonly string[]>(
58
+ values: T,
59
+ options?: { description?: string },
60
+ ): TUnion<[TLiteral<T[number]>]> {
61
+ // SAFETY: the runtime array holds one TLiteral per input value; the tuple
62
+ // cast only affects the static type so `Static` preserves the literal union.
63
+ const schemas = values.map((v) => Type.Literal(v)) as unknown as [
64
+ TLiteral<T[number]>,
65
+ ];
66
+ return Type.Union(schemas, options);
67
+ }
68
+
69
+ export function officeToolName(host: OfficeHostApp, op: string): string {
70
+ return `office_${host}_${op}`;
71
+ }
72
+
73
+ /* ── Shared parameter fragments ─────────────────────────────────────── */
74
+
75
+ const WORD_ALIGNMENT = catalogEnum(
76
+ ["Left", "Centered", "Right", "Justified"] as const,
77
+ {
78
+ description:
79
+ 'Paragraph alignment. Word values: "Left", "Centered" (note the capital C), "Right", "Justified".',
80
+ },
81
+ );
82
+
83
+ const PPT_ALIGNMENT = catalogEnum(
84
+ ["Left", "Center", "Right", "Justify"] as const,
85
+ {
86
+ description:
87
+ 'Horizontal alignment. PowerPoint values: "Left", "Center", "Right", "Justify".',
88
+ },
89
+ );
90
+
91
+ const WORD_FONT_PROPS = {
92
+ bold: Type.Optional(
93
+ Type.Boolean({ description: "Set bold (true) or unbold (false)." }),
94
+ ),
95
+ italic: Type.Optional(
96
+ Type.Boolean({ description: "Set italic (true) or unitalicize (false)." }),
97
+ ),
98
+ underline: Type.Optional(
99
+ Type.Boolean({ description: "Underline the text (single underline)." }),
100
+ ),
101
+ size: Type.Optional(
102
+ Type.Number({ description: "Font size in points (e.g. 16)." }),
103
+ ),
104
+ name: Type.Optional(
105
+ Type.String({ description: 'Font name (e.g. "Times New Roman").' }),
106
+ ),
107
+ color: Type.Optional(
108
+ Type.String({ description: 'Font color as #RRGGBB (e.g. "#000000").' }),
109
+ ),
110
+ };
111
+
112
+ const WORD_PARAGRAPH_PROPS = {
113
+ alignment: Type.Optional(WORD_ALIGNMENT),
114
+ style: Type.Optional(
115
+ Type.String({
116
+ description:
117
+ 'Paragraph style name, e.g. "Heading 1", "Title", "Normal". Style names are locale-sensitive; a fallback to direct formatting is applied when the style cannot be verified.',
118
+ }),
119
+ ),
120
+ spaceBefore: Type.Optional(
121
+ Type.Number({ description: "Space before the paragraph, in points." }),
122
+ ),
123
+ spaceAfter: Type.Optional(
124
+ Type.Number({ description: "Space after the paragraph, in points." }),
125
+ ),
126
+ lineSpacing: Type.Optional(
127
+ Type.Number({ description: "Line spacing, in points." }),
128
+ ),
129
+ firstLineIndent: Type.Optional(
130
+ Type.Number({ description: "First-line indent, in points." }),
131
+ ),
132
+ leftIndent: Type.Optional(
133
+ Type.Number({ description: "Left indent, in points." }),
134
+ ),
135
+ };
136
+
137
+ const PPT_SLIDE_INDEX = Type.Integer({
138
+ minimum: 1,
139
+ description: "1-based slide index.",
140
+ });
141
+
142
+ const PPT_TEXT_FORMAT_PROPS = {
143
+ bold: Type.Optional(
144
+ Type.Boolean({ description: "Set bold (true) or unbold (false)." }),
145
+ ),
146
+ italic: Type.Optional(
147
+ Type.Boolean({ description: "Set italic (true) or unitalicize (false)." }),
148
+ ),
149
+ underline: Type.Optional(
150
+ Type.Boolean({ description: "Underline the text (single underline)." }),
151
+ ),
152
+ fontSize: Type.Optional(
153
+ Type.Number({ description: "Font size in points (e.g. 24)." }),
154
+ ),
155
+ fontName: Type.Optional(
156
+ Type.String({ description: 'Font name (e.g. "Arial").' }),
157
+ ),
158
+ fontColor: Type.Optional(
159
+ Type.String({ description: 'Font color as #RRGGBB (e.g. "#333333").' }),
160
+ ),
161
+ alignment: Type.Optional(PPT_ALIGNMENT),
162
+ };
163
+
164
+ /* ── Word schemas (imported by the add-in's local Word tools) ────────── */
165
+
166
+ export const WORD_GET_OVERVIEW_PARAMETERS = Type.Object({});
167
+
168
+ export const WORD_READ_DOCUMENT_PARAMETERS = Type.Object({
169
+ scope: Type.Optional(
170
+ catalogEnum(["all", "selection"] as const, {
171
+ description:
172
+ '"all": whole document. "selection": currently selected text only.',
173
+ }),
174
+ ),
175
+ maxChars: Type.Optional(
176
+ Type.Integer({
177
+ minimum: 100,
178
+ maximum: 200000,
179
+ description: "Cap on characters returned (default 20000).",
180
+ }),
181
+ ),
182
+ });
183
+
184
+ export const WORD_INSERT_TEXT_PARAMETERS = Type.Object({
185
+ text: Type.String({
186
+ description: "Text to insert. Newlines create separate paragraphs.",
187
+ }),
188
+ location: Type.Optional(
189
+ catalogEnum(["start", "end", "replace_selection"] as const, {
190
+ description:
191
+ '"end" (default) appends to the document. "replace_selection" overwrites the selection.',
192
+ }),
193
+ ),
194
+ format: Type.Optional(
195
+ catalogEnum(["text", "markdown"] as const, {
196
+ description:
197
+ '"text" (default): plain text with newline→paragraph. "markdown": parse a markdown subset.',
198
+ }),
199
+ ),
200
+ ...WORD_FONT_PROPS,
201
+ ...WORD_PARAGRAPH_PROPS,
202
+ });
203
+
204
+ export const WORD_REPLACE_TEXT_PARAMETERS = Type.Object({
205
+ find: Type.String({ description: "Literal text to find." }),
206
+ replace: Type.String({ description: "Replacement text." }),
207
+ matchCase: Type.Optional(
208
+ Type.Boolean({ description: "Case-sensitive match (default false)." }),
209
+ ),
210
+ });
211
+
212
+ export const WORD_FORMAT_RANGE_PARAMETERS = Type.Object({
213
+ text: Type.String({
214
+ description:
215
+ "Literal text to find and format. Every occurrence is formatted.",
216
+ }),
217
+ matchCase: Type.Optional(
218
+ Type.Boolean({ description: "Case-sensitive search (default false)." }),
219
+ ),
220
+ ...WORD_FONT_PROPS,
221
+ ...WORD_PARAGRAPH_PROPS,
222
+ });
223
+
224
+ export const WORD_INSERT_BLOCKS_PARAMETERS = Type.Object({
225
+ location: Type.Optional(
226
+ catalogEnum(["start", "end", "replace_selection"] as const, {
227
+ description:
228
+ '"end" (default) appends to the document. "replace_selection" overwrites the selection.',
229
+ }),
230
+ ),
231
+ blocks: Type.Array(
232
+ Type.Object({
233
+ text: Type.String({ description: "Text content of the block." }),
234
+ type: Type.Optional(
235
+ catalogEnum(
236
+ ["paragraph", "heading", "listItem", "pageBreak"] as const,
237
+ {
238
+ description:
239
+ 'Block type: "paragraph" (default), "heading" (use with level), "listItem", or "pageBreak" (text is ignored).',
240
+ },
241
+ ),
242
+ ),
243
+ level: Type.Optional(
244
+ Type.Integer({
245
+ minimum: 1,
246
+ maximum: 6,
247
+ description: "Heading level 1-6 (for type: heading).",
248
+ }),
249
+ ),
250
+ listType: Type.Optional(
251
+ catalogEnum(["bullet", "number"] as const, {
252
+ description:
253
+ 'List marker style for type: listItem ("bullet" or "number").',
254
+ }),
255
+ ),
256
+ ...WORD_FONT_PROPS,
257
+ ...WORD_PARAGRAPH_PROPS,
258
+ }),
259
+ { description: "Ordered list of blocks to insert." },
260
+ ),
261
+ });
262
+
263
+ export const WORD_INSERT_TABLE_PARAMETERS = Type.Object({
264
+ cells: Type.Array(Type.Array(Type.String()), {
265
+ description: "2D array of cell values (rows × cols).",
266
+ }),
267
+ headerRow: Type.Optional(
268
+ Type.Boolean({
269
+ description: "Bold the first row as a header. Default: false.",
270
+ }),
271
+ ),
272
+ style: Type.Optional(
273
+ Type.String({
274
+ description:
275
+ 'Table style name, e.g. "Grid Table 4 - Accent 1". Locale-sensitive; degraded gracefully.',
276
+ }),
277
+ ),
278
+ alignment: Type.Optional(WORD_ALIGNMENT),
279
+ location: Type.Optional(
280
+ catalogEnum(["start", "end"] as const, {
281
+ description:
282
+ '"end" (default) appends to the document. "start" inserts at the beginning.',
283
+ }),
284
+ ),
285
+ });
286
+
287
+ export const WORD_INSERT_PAGE_BREAK_PARAMETERS = Type.Object({
288
+ location: Type.Optional(
289
+ catalogEnum(["start", "end"] as const, {
290
+ description:
291
+ '"end" (default) appends to the document. "start" inserts at the beginning.',
292
+ }),
293
+ ),
294
+ });
295
+
296
+ export const WORD_INSERT_IMAGE_PARAMETERS = Type.Object({
297
+ base64: Type.String({
298
+ description:
299
+ "Base64-encoded image, optionally prefixed with data:<mime>;base64,",
300
+ }),
301
+ width: Type.Optional(
302
+ Type.Number({ description: "Width in points. Default: keep source size." }),
303
+ ),
304
+ height: Type.Optional(
305
+ Type.Number({
306
+ description: "Height in points. Default: keep source size.",
307
+ }),
308
+ ),
309
+ alignment: Type.Optional(WORD_ALIGNMENT),
310
+ location: Type.Optional(
311
+ catalogEnum(["start", "end"] as const, {
312
+ description:
313
+ '"end" (default) appends to the document. "start" inserts at the beginning.',
314
+ }),
315
+ ),
316
+ });
317
+
318
+ export const WORD_INSERT_HYPERLINK_PARAMETERS = Type.Object({
319
+ text: Type.String({ description: "Display text for the hyperlink." }),
320
+ url: Type.String({ description: "Target URL (e.g. https://example.com)." }),
321
+ screenTip: Type.Optional(
322
+ Type.String({ description: "Optional hover tooltip." }),
323
+ ),
324
+ location: Type.Optional(
325
+ catalogEnum(["start", "end"] as const, {
326
+ description:
327
+ '"end" (default) appends to the document. "start" inserts at the beginning.',
328
+ }),
329
+ ),
330
+ });
331
+
332
+ /* ── PowerPoint schemas ──────────────────────────────────────────────── */
333
+
334
+ export const POWERPOINT_GET_OVERVIEW_PARAMETERS = Type.Object({});
335
+
336
+ export const POWERPOINT_READ_SLIDE_PARAMETERS = Type.Object({
337
+ slideIndex: PPT_SLIDE_INDEX,
338
+ });
339
+
340
+ export const POWERPOINT_ADD_SLIDE_PARAMETERS = Type.Object({});
341
+
342
+ export const POWERPOINT_ADD_TEXT_BOX_PARAMETERS = Type.Object({
343
+ slideIndex: PPT_SLIDE_INDEX,
344
+ text: Type.String({ description: "Text box content." }),
345
+ x: Type.Optional(
346
+ Type.Number({ description: "Left edge in points (default centered)." }),
347
+ ),
348
+ y: Type.Optional(
349
+ Type.Number({ description: "Top edge in points (default centered)." }),
350
+ ),
351
+ width: Type.Optional(
352
+ Type.Number({ description: "Width in points (default 400)." }),
353
+ ),
354
+ height: Type.Optional(
355
+ Type.Number({ description: "Height in points (default 60)." }),
356
+ ),
357
+ ...PPT_TEXT_FORMAT_PROPS,
358
+ });
359
+
360
+ export const POWERPOINT_FORMAT_SLIDE_PARAMETERS = Type.Object({
361
+ slideIndex: PPT_SLIDE_INDEX,
362
+ ...PPT_TEXT_FORMAT_PROPS,
363
+ });
364
+
365
+ /* ── Excel schemas ──────────────────────────────────────────────────── */
366
+
367
+ const EXCEL_READ_RANGE_PARAMETERS = Type.Object({
368
+ range: Type.String({
369
+ description:
370
+ 'Cell range in A1 notation, e.g. "A1:D10" or "Sheet2!A1:B5". ' +
371
+ "Uses the active sheet when no sheet is specified.",
372
+ }),
373
+ mode: Type.Optional(
374
+ catalogEnum(["compact", "csv", "detailed"] as const, {
375
+ description:
376
+ '"compact" (default): markdown table. "csv": raw values. "detailed": with formulas/formats.',
377
+ }),
378
+ ),
379
+ });
380
+
381
+ const EXCEL_WRITE_CELLS_PARAMETERS = Type.Object({
382
+ start_cell: Type.String({
383
+ description: 'Top-left cell to write from, e.g. "A1" or "Sheet2!B3".',
384
+ }),
385
+ values: Type.Array(Type.Array(Type.Any()), {
386
+ description: "2D array of cell values (rows × cols).",
387
+ }),
388
+ allow_overwrite: Type.Optional(
389
+ Type.Boolean({
390
+ description:
391
+ "Set to true to overwrite existing data. Default: false. " +
392
+ "If false and the target range contains values or formulas, the write is blocked.",
393
+ }),
394
+ ),
395
+ });
396
+
397
+ const EXCEL_FILL_FORMULA_PARAMETERS = Type.Object({
398
+ range: Type.String({
399
+ description: 'Target range, e.g. "B2:B20" or "Sheet1!C3:F20".',
400
+ }),
401
+ formula: Type.String({
402
+ description: 'Formula starting with "=", e.g. "=SUM(B2:B10)".',
403
+ }),
404
+ allow_overwrite: Type.Optional(
405
+ Type.Boolean({
406
+ description:
407
+ "Set to true to overwrite existing data. Default: false. " +
408
+ "If false and the target range contains data, the fill is blocked.",
409
+ }),
410
+ ),
411
+ });
412
+
413
+ const EXCEL_SEARCH_WORKBOOK_PARAMETERS = Type.Object({
414
+ query: Type.String({
415
+ description:
416
+ 'Search term. For formula search, use references like "Sheet1!" to find cross-sheet links.',
417
+ }),
418
+ search_formulas: Type.Optional(
419
+ Type.Boolean({
420
+ description:
421
+ "If true, search in formula text instead of values. " +
422
+ 'Useful for finding cross-sheet references (e.g. query "Inputs!").',
423
+ }),
424
+ ),
425
+ use_regex: Type.Optional(
426
+ Type.Boolean({
427
+ description:
428
+ "If true, treat the query as a regular expression (case-insensitive).",
429
+ }),
430
+ ),
431
+ offset: Type.Optional(
432
+ Type.Number({
433
+ description: "Skip the first N matches (pagination). Default: 0.",
434
+ }),
435
+ ),
436
+ sheet: Type.Optional(
437
+ Type.String({
438
+ description:
439
+ "Restrict search to this sheet. If omitted, searches all sheets.",
440
+ }),
441
+ ),
442
+ max_results: Type.Optional(
443
+ Type.Number({
444
+ description: "Maximum number of results to return. Default: 20.",
445
+ }),
446
+ ),
447
+ context_rows: Type.Optional(
448
+ Type.Number({
449
+ description:
450
+ "Number of rows above and below each match to include as context. Default: 0 (no context).",
451
+ }),
452
+ ),
453
+ });
454
+
455
+ const EXCEL_MODIFY_STRUCTURE_PARAMETERS = Type.Object({
456
+ action: catalogEnum(
457
+ [
458
+ "insert_rows",
459
+ "delete_rows",
460
+ "insert_columns",
461
+ "delete_columns",
462
+ "add_sheet",
463
+ "delete_sheet",
464
+ "rename_sheet",
465
+ "duplicate_sheet",
466
+ "hide_sheet",
467
+ "unhide_sheet",
468
+ ] as const,
469
+ { description: "The structural modification to perform." },
470
+ ),
471
+ sheet: Type.Optional(
472
+ Type.String({
473
+ description:
474
+ "Target sheet name. Required for sheet operations and row/column operations on a specific sheet. " +
475
+ "If omitted for row/column ops, uses the active sheet.",
476
+ }),
477
+ ),
478
+ position: Type.Optional(
479
+ Type.Number({
480
+ description:
481
+ "For insert_rows/delete_rows: the 1-indexed row number. " +
482
+ "For insert_columns/delete_columns: the 1-indexed column number. " +
483
+ "For add_sheet: the 0-indexed position to insert the new sheet.",
484
+ }),
485
+ ),
486
+ count: Type.Optional(
487
+ Type.Number({
488
+ description: "Number of rows or columns to insert/delete. Default: 1.",
489
+ }),
490
+ ),
491
+ new_name: Type.Optional(
492
+ Type.String({
493
+ description:
494
+ "New name for rename_sheet or add_sheet. Also used for duplicate_sheet target name.",
495
+ }),
496
+ ),
497
+ });
498
+
499
+ const EXCEL_FORMAT_CELLS_PARAMETERS = Type.Object({
500
+ range: Type.String({
501
+ description:
502
+ 'Range to format, e.g. "A1:D1", "Sheet2!B3:B20". Supports comma/semicolon-separated ranges on the same sheet.',
503
+ }),
504
+ style: Type.Optional(
505
+ Type.Union([Type.String(), Type.Array(Type.String())], {
506
+ description:
507
+ "Named style(s) to apply. Compose as array (left-to-right). " +
508
+ 'Format: "number", "integer", "currency", "percent", "ratio", "text". ' +
509
+ 'Structural: "header", "total-row", "subtotal", "input", "blank-section".',
510
+ }),
511
+ ),
512
+ bold: Type.Optional(Type.Boolean({ description: "Set bold." })),
513
+ italic: Type.Optional(Type.Boolean({ description: "Set italic." })),
514
+ underline: Type.Optional(Type.Boolean({ description: "Set underline." })),
515
+ font_color: Type.Optional(
516
+ Type.String({ description: 'Font color as hex, e.g. "#0000FF" for blue.' }),
517
+ ),
518
+ font_size: Type.Optional(
519
+ Type.Number({ description: "Font size in points." }),
520
+ ),
521
+ font_name: Type.Optional(
522
+ Type.String({ description: 'Font name, e.g. "Arial", "Calibri".' }),
523
+ ),
524
+ fill_color: Type.Optional(
525
+ Type.String({
526
+ description: 'Background fill color as hex, e.g. "#FFFF00" for yellow.',
527
+ }),
528
+ ),
529
+ number_format: Type.Optional(
530
+ Type.String({
531
+ description:
532
+ 'Preset name ("number", "integer", "currency", "percent", "ratio", "text") ' +
533
+ "or raw Excel format string. Overrides style's number format.",
534
+ }),
535
+ ),
536
+ number_format_dp: Type.Optional(
537
+ Type.Number({
538
+ description: "Override decimal places for a number format preset.",
539
+ }),
540
+ ),
541
+ currency_symbol: Type.Optional(
542
+ Type.String({
543
+ description:
544
+ 'Override currency symbol, e.g. "£", "€". Only with currency preset.',
545
+ }),
546
+ ),
547
+ horizontal_alignment: Type.Optional(
548
+ Type.String({ description: '"Left", "Center", "Right", or "General".' }),
549
+ ),
550
+ vertical_alignment: Type.Optional(
551
+ Type.String({ description: '"Top", "Center", "Bottom".' }),
552
+ ),
553
+ wrap_text: Type.Optional(
554
+ Type.Boolean({ description: "Enable text wrapping." }),
555
+ ),
556
+ column_width: Type.Optional(
557
+ Type.Number({
558
+ description:
559
+ "Set column width in Excel character-width units (assumes Arial 10).",
560
+ }),
561
+ ),
562
+ row_height: Type.Optional(
563
+ Type.Number({ description: "Set row height in points." }),
564
+ ),
565
+ auto_fit: Type.Optional(
566
+ Type.Boolean({
567
+ description: "Auto-fit column widths to content. Default: false.",
568
+ }),
569
+ ),
570
+ borders: Type.Optional(
571
+ catalogEnum(["thin", "medium", "thick", "none"] as const, {
572
+ description:
573
+ "Border weight for ALL edges (shorthand). Individual edge params override this.",
574
+ }),
575
+ ),
576
+ border_top: Type.Optional(
577
+ catalogEnum(["thin", "medium", "thick", "none"] as const, {
578
+ description: "Top border weight.",
579
+ }),
580
+ ),
581
+ border_bottom: Type.Optional(
582
+ catalogEnum(["thin", "medium", "thick", "none"] as const, {
583
+ description: "Bottom border weight.",
584
+ }),
585
+ ),
586
+ border_left: Type.Optional(
587
+ catalogEnum(["thin", "medium", "thick", "none"] as const, {
588
+ description: "Left border weight.",
589
+ }),
590
+ ),
591
+ border_right: Type.Optional(
592
+ catalogEnum(["thin", "medium", "thick", "none"] as const, {
593
+ description: "Right border weight.",
594
+ }),
595
+ ),
596
+ border_color: Type.Optional(
597
+ Type.String({
598
+ description:
599
+ 'Hex color for borders (e.g. "#000000"). Applies to all borders set in this call.',
600
+ }),
601
+ ),
602
+ merge: Type.Optional(
603
+ Type.Boolean({ description: "Merge the range into a single cell." }),
604
+ ),
605
+ });
606
+
607
+ const EXCEL_CONDITIONAL_FORMAT_PARAMETERS = Type.Object({
608
+ action: Type.Union([Type.Literal("add"), Type.Literal("clear")], {
609
+ description:
610
+ '"add" to create a rule, "clear" to remove all rules in the range.',
611
+ }),
612
+ range: Type.String({
613
+ description: 'Target range, e.g. "A1:D10" or "Sheet2!B2:B50".',
614
+ }),
615
+ type: Type.Optional(
616
+ Type.Union([Type.Literal("formula"), Type.Literal("cell_value")], {
617
+ description: 'Rule type for "add": "formula" or "cell_value".',
618
+ }),
619
+ ),
620
+ formula: Type.Optional(
621
+ Type.String({
622
+ description: 'Custom formula for "formula" rules, e.g. "=A1>0".',
623
+ }),
624
+ ),
625
+ operator: Type.Optional(
626
+ catalogEnum(
627
+ [
628
+ "Between",
629
+ "NotBetween",
630
+ "EqualTo",
631
+ "NotEqualTo",
632
+ "GreaterThan",
633
+ "LessThan",
634
+ "GreaterThanOrEqual",
635
+ "LessThanOrEqual",
636
+ ] as const,
637
+ { description: "Cell value operator (required for cell_value rules)." },
638
+ ),
639
+ ),
640
+ value: Type.Optional(
641
+ Type.Union([Type.String(), Type.Number()], {
642
+ description:
643
+ 'Cell value comparison target (required for cell_value rules). Use numbers or formulas like "=$B$2".',
644
+ }),
645
+ ),
646
+ value2: Type.Optional(
647
+ Type.Union([Type.String(), Type.Number()], {
648
+ description: "Second value for Between/NotBetween operators (optional).",
649
+ }),
650
+ ),
651
+ fill_color: Type.Optional(
652
+ Type.String({ description: 'Fill color hex, e.g. "#FFFDE0".' }),
653
+ ),
654
+ font_color: Type.Optional(
655
+ Type.String({ description: 'Font color hex, e.g. "#000000".' }),
656
+ ),
657
+ bold: Type.Optional(Type.Boolean({ description: "Bold text." })),
658
+ italic: Type.Optional(Type.Boolean({ description: "Italic text." })),
659
+ underline: Type.Optional(Type.Boolean({ description: "Underline text." })),
660
+ stop_if_true: Type.Optional(
661
+ Type.Boolean({ description: "Stop evaluating later rules if true." }),
662
+ ),
663
+ });
664
+
665
+ const EXCEL_CHARTS_PARAMETERS = Type.Object({
666
+ action: catalogEnum(
667
+ ["list", "create", "update", "delete", "get_image"] as const,
668
+ {
669
+ description:
670
+ "Chart operation: list, create, update, delete, or get_image.",
671
+ },
672
+ ),
673
+ sheet: Type.Optional(
674
+ Type.String({
675
+ description:
676
+ "Worksheet name. For list, limits output to one sheet. For create/update, defaults to the active sheet or the source range sheet.",
677
+ }),
678
+ ),
679
+ name: Type.Optional(
680
+ Type.String({
681
+ description:
682
+ "Chart name. Required for update, delete, and get_image. Optional assigned name for create.",
683
+ }),
684
+ ),
685
+ new_name: Type.Optional(
686
+ Type.String({ description: "New chart name for update." }),
687
+ ),
688
+ source_range: Type.Optional(
689
+ Type.String({
690
+ description:
691
+ "Source data range for create/update, e.g. `Sheet1!A1:B12` or `A1:B12` relative to sheet.",
692
+ }),
693
+ ),
694
+ chart_type: Type.Optional(
695
+ catalogEnum(
696
+ [
697
+ "column",
698
+ "column_stacked",
699
+ "column_stacked_100",
700
+ "bar",
701
+ "bar_stacked",
702
+ "bar_stacked_100",
703
+ "line",
704
+ "line_markers",
705
+ "area",
706
+ "area_stacked",
707
+ "pie",
708
+ "doughnut",
709
+ "scatter",
710
+ "scatter_lines",
711
+ "scatter_smooth",
712
+ "radar",
713
+ ] as const,
714
+ { description: "Chart type (friendly name, e.g. column_stacked)." },
715
+ ),
716
+ ),
717
+ series_by: Type.Optional(
718
+ catalogEnum(["auto", "columns", "rows"] as const, {
719
+ description:
720
+ "How source rows/columns become series: auto, columns, or rows.",
721
+ }),
722
+ ),
723
+ title: Type.Optional(
724
+ Type.String({ description: "Chart title. Empty string hides the title." }),
725
+ ),
726
+ legend_position: Type.Optional(
727
+ catalogEnum(["none", "right", "left", "top", "bottom"] as const, {
728
+ description: "Legend position, or none to hide the legend.",
729
+ }),
730
+ ),
731
+ x_axis_title: Type.Optional(
732
+ Type.String({
733
+ description: "Category/X axis title. Empty string hides it.",
734
+ }),
735
+ ),
736
+ y_axis_title: Type.Optional(
737
+ Type.String({ description: "Value/Y axis title. Empty string hides it." }),
738
+ ),
739
+ position: Type.Optional(
740
+ Type.String({
741
+ description: "Anchor range for chart placement, e.g. `D2:J18`.",
742
+ }),
743
+ ),
744
+ width: Type.Optional(
745
+ Type.Number({
746
+ description:
747
+ "Image width in pixels for get_image. Defaults to 600; capped at 1200.",
748
+ }),
749
+ ),
750
+ });
751
+
752
+ const EXCEL_TRACE_DEPENDENCIES_PARAMETERS = Type.Object({
753
+ cell: Type.String({
754
+ description:
755
+ 'Cell to trace, e.g. "D10", "Sheet2!F5". Must be a single cell, not a range.',
756
+ }),
757
+ mode: Type.Optional(
758
+ catalogEnum(["precedents", "dependents"] as const, {
759
+ description:
760
+ "Trace direction: precedents (upstream) or dependents (downstream). Default: precedents.",
761
+ }),
762
+ ),
763
+ depth: Type.Optional(
764
+ Type.Number({
765
+ description:
766
+ "How many levels of dependencies to trace. Default: 2. Max: 5.",
767
+ }),
768
+ ),
769
+ });
770
+
771
+ const EXCEL_EXPLAIN_FORMULA_PARAMETERS = Type.Object({
772
+ cell: Type.String({
773
+ description: 'Single formula cell to explain, e.g. "D10" or "Sheet2!F5".',
774
+ }),
775
+ max_references: Type.Optional(
776
+ Type.Number({
777
+ description:
778
+ "Max number of direct references to preview. Default: 8. Max: 20.",
779
+ }),
780
+ ),
781
+ });
782
+
783
+ const EXCEL_VIEW_SETTINGS_PARAMETERS = Type.Object({
784
+ action: catalogEnum(
785
+ [
786
+ "get",
787
+ "show_gridlines",
788
+ "hide_gridlines",
789
+ "show_headings",
790
+ "hide_headings",
791
+ "freeze_rows",
792
+ "freeze_columns",
793
+ "freeze_at",
794
+ "unfreeze",
795
+ "set_tab_color",
796
+ "hide_sheet",
797
+ "show_sheet",
798
+ "very_hide_sheet",
799
+ "set_standard_width",
800
+ "activate",
801
+ ] as const,
802
+ { description: "The view setting to read or change." },
803
+ ),
804
+ sheet: Type.Optional(
805
+ Type.String({
806
+ description:
807
+ "Target sheet name. Defaults to the active sheet for most actions. " +
808
+ "Required for hide/show/very_hide and activate.",
809
+ }),
810
+ ),
811
+ count: Type.Optional(
812
+ Type.Number({
813
+ description:
814
+ "Number of rows or columns to freeze. Required for freeze_rows/freeze_columns.",
815
+ }),
816
+ ),
817
+ range: Type.Optional(
818
+ Type.String({
819
+ description:
820
+ 'Cell range for freeze_at (e.g. "B3"). Everything above and to the left of this cell will be frozen.',
821
+ }),
822
+ ),
823
+ color: Type.Optional(
824
+ Type.String({
825
+ description:
826
+ 'Tab color in #RRGGBB format (e.g. "#FF6600"). Use "" to clear.',
827
+ }),
828
+ ),
829
+ width: Type.Optional(
830
+ Type.Number({
831
+ description:
832
+ "Standard (default) column width for the worksheet, in Excel character-width units.",
833
+ }),
834
+ ),
835
+ });
836
+
837
+ const EXCEL_COMMENTS_PARAMETERS = Type.Object({
838
+ action: catalogEnum(
839
+ ["read", "add", "update", "reply", "delete", "resolve", "reopen"] as const,
840
+ {
841
+ description:
842
+ "Comment operation: read (list comments in range), add (new comment on cell), " +
843
+ "update (edit existing comment text), reply (add threaded reply), " +
844
+ "delete (remove comment + replies), resolve/reopen (toggle thread status).",
845
+ },
846
+ ),
847
+ range: Type.String({
848
+ description:
849
+ 'Target cell or range in A1 notation, e.g. "A1", "B2:D10", "Sheet2!A1". ' +
850
+ "Range supported for read; other actions require a single cell.",
851
+ }),
852
+ content: Type.Optional(
853
+ Type.String({
854
+ description: "Comment text. Required for add, update, and reply actions.",
855
+ }),
856
+ ),
857
+ });
858
+
859
+ const EXCEL_WORKBOOK_HISTORY_PARAMETERS = Type.Object({
860
+ action: Type.Optional(
861
+ catalogEnum(["list", "restore", "delete", "clear"] as const, {
862
+ description:
863
+ "Operation to run. list (default): show recent backups; " +
864
+ "restore: revert one backup; delete: remove one backup; clear: remove all backups for current workbook.",
865
+ }),
866
+ ),
867
+ snapshot_id: Type.Optional(
868
+ Type.String({
869
+ description:
870
+ "Backup id for restore/delete. If omitted, the latest backup is used.",
871
+ }),
872
+ ),
873
+ limit: Type.Optional(
874
+ Type.Integer({
875
+ minimum: 1,
876
+ maximum: 50,
877
+ description: "Max backups to list (list action only). Default: 10.",
878
+ }),
879
+ ),
880
+ });
881
+
882
+ /* ── Catalog entries ────────────────────────────────────────────────── */
883
+
884
+ export const OFFICE_CATALOG: readonly OfficeCatalogEntry[] = [
885
+ /* ── Excel ─────────────────────────────────────────────────────── */
886
+ {
887
+ host: "excel",
888
+ op: "get_overview",
889
+ name: officeToolName("excel", "get_overview"),
890
+ label: "Excel Workbook Overview",
891
+ description:
892
+ "Read a compact overview of the attached Excel workbook: sheet names, used ranges, " +
893
+ "table names, and named ranges. Call this first before any range operation.",
894
+ promptSnippet: "Outline the attached Excel workbook",
895
+ promptGuidelines: [
896
+ "Call office_excel_get_overview before office_excel_read_range to learn the workbook structure.",
897
+ ],
898
+ parameters: Type.Object({
899
+ sheet: Type.Optional(
900
+ Type.String({
901
+ description:
902
+ "If provided, return detailed info for this specific sheet " +
903
+ "(dimensions, headers, tables, named ranges, objects, and a data preview). " +
904
+ "If omitted, return the workbook-level overview.",
905
+ }),
906
+ ),
907
+ }),
908
+ since: 1,
909
+ legacyV1: true,
910
+ },
911
+ {
912
+ host: "excel",
913
+ op: "read_range",
914
+ name: officeToolName("excel", "read_range"),
915
+ label: "Excel Read Range",
916
+ description:
917
+ "Read cell values (and optionally formulas/formatting) from a range in the attached Excel workbook.",
918
+ promptSnippet: "Read cells from the attached Excel workbook",
919
+ parameters: EXCEL_READ_RANGE_PARAMETERS,
920
+ since: 1,
921
+ legacyV1: true,
922
+ },
923
+ {
924
+ host: "excel",
925
+ op: "write_cells",
926
+ name: officeToolName("excel", "write_cells"),
927
+ label: "Excel Write Cells",
928
+ description:
929
+ "Write a 2D array of values into the attached Excel workbook, starting at a top-left cell. " +
930
+ "values[row][col]; the array is written down and to the right from start_cell.",
931
+ promptSnippet: "Write values/formulas into the attached Excel workbook",
932
+ promptGuidelines: [
933
+ "Prefer office_excel_write_cells in a single batched call instead of many small edits.",
934
+ "Always verify with office_excel_read_range after office_excel_write_cells when the change is user-visible.",
935
+ ],
936
+ parameters: EXCEL_WRITE_CELLS_PARAMETERS,
937
+ since: 1,
938
+ legacyV1: true,
939
+ },
940
+ {
941
+ host: "excel",
942
+ op: "fill_formula",
943
+ name: officeToolName("excel", "fill_formula"),
944
+ label: "Excel Fill Formula",
945
+ description:
946
+ "Write a formula into a single contiguous range of the attached Excel workbook. " +
947
+ "Relative references adjust as the formula fills.",
948
+ promptSnippet: "Fill a formula across an Excel range",
949
+ parameters: EXCEL_FILL_FORMULA_PARAMETERS,
950
+ since: 1,
951
+ legacyV1: true,
952
+ },
953
+ {
954
+ host: "excel",
955
+ op: "search_workbook",
956
+ name: officeToolName("excel", "search_workbook"),
957
+ label: "Excel Search Workbook",
958
+ description:
959
+ "Search for text, values, or formulas across the attached Excel workbook. " +
960
+ "Returns matching cells with sheet name, address, value, and formula. " +
961
+ "Use to find data, locate cells by label, or trace cross-sheet references.",
962
+ promptSnippet:
963
+ "Search for text/values/formulas in the attached Excel workbook",
964
+ parameters: EXCEL_SEARCH_WORKBOOK_PARAMETERS,
965
+ since: 1,
966
+ },
967
+ {
968
+ host: "excel",
969
+ op: "modify_structure",
970
+ name: officeToolName("excel", "modify_structure"),
971
+ label: "Excel Modify Structure",
972
+ description:
973
+ "Modify the workbook structure of the attached Excel workbook: insert/delete rows and columns, " +
974
+ "add/delete/rename/duplicate/hide/unhide sheets. Be careful with deletions — there is no undo.",
975
+ promptSnippet:
976
+ "Insert/delete rows, columns, or sheets in the attached Excel workbook",
977
+ parameters: EXCEL_MODIFY_STRUCTURE_PARAMETERS,
978
+ since: 1,
979
+ },
980
+ {
981
+ host: "excel",
982
+ op: "format_cells",
983
+ name: officeToolName("excel", "format_cells"),
984
+ label: "Excel Format Cells",
985
+ description:
986
+ "Apply formatting to a range of cells in the attached Excel workbook (supports comma-separated ranges on one sheet). " +
987
+ 'Use named styles for common patterns: style: "currency" or style: ["currency", "total-row"]. ' +
988
+ "Individual params (bold, fill_color, etc.) override style properties. " +
989
+ "Does NOT modify cell values — use office_excel_write_cells for that.",
990
+ promptSnippet:
991
+ "Format cells (font, fill, borders, alignment, number format) in the attached Excel workbook",
992
+ parameters: EXCEL_FORMAT_CELLS_PARAMETERS,
993
+ since: 1,
994
+ },
995
+ {
996
+ host: "excel",
997
+ op: "conditional_format",
998
+ name: officeToolName("excel", "conditional_format"),
999
+ label: "Excel Conditional Format",
1000
+ description:
1001
+ "Add or clear conditional formatting rules in the attached Excel workbook. " +
1002
+ "Supports custom formula and cell value rules.",
1003
+ promptSnippet:
1004
+ "Add or clear conditional formatting rules in the attached Excel workbook",
1005
+ parameters: EXCEL_CONDITIONAL_FORMAT_PARAMETERS,
1006
+ since: 1,
1007
+ },
1008
+ {
1009
+ host: "excel",
1010
+ op: "charts",
1011
+ name: officeToolName("excel", "charts"),
1012
+ label: "Excel Charts",
1013
+ description:
1014
+ "List, create, update, delete, and capture images of charts in the attached Excel workbook.",
1015
+ promptSnippet:
1016
+ "List/create/update/delete charts in the attached Excel workbook",
1017
+ parameters: EXCEL_CHARTS_PARAMETERS,
1018
+ since: 1,
1019
+ },
1020
+ {
1021
+ host: "excel",
1022
+ op: "trace_dependencies",
1023
+ name: officeToolName("excel", "trace_dependencies"),
1024
+ label: "Excel Trace Dependencies",
1025
+ description:
1026
+ "Return formula lineage for a cell in the attached Excel workbook: precedents (upstream) or dependents (downstream).",
1027
+ promptSnippet: "Trace formula precedents/dependents of an Excel cell",
1028
+ parameters: EXCEL_TRACE_DEPENDENCIES_PARAMETERS,
1029
+ since: 1,
1030
+ },
1031
+ {
1032
+ host: "excel",
1033
+ op: "explain_formula",
1034
+ name: officeToolName("excel", "explain_formula"),
1035
+ label: "Excel Explain Formula",
1036
+ description:
1037
+ "Explain a formula cell in the attached Excel workbook in plain language, including direct input references and current values.",
1038
+ promptSnippet: "Explain what an Excel formula cell does",
1039
+ parameters: EXCEL_EXPLAIN_FORMULA_PARAMETERS,
1040
+ since: 1,
1041
+ },
1042
+ {
1043
+ host: "excel",
1044
+ op: "view_settings",
1045
+ name: officeToolName("excel", "view_settings"),
1046
+ label: "Excel View Settings",
1047
+ description:
1048
+ "Read or change worksheet view/navigation settings in the attached Excel workbook: gridlines, row/column headings, " +
1049
+ "freeze panes, tab color, sheet visibility, sheet activation, and standard width.",
1050
+ promptSnippet:
1051
+ "Change Excel view settings (gridlines, freeze panes, tab color, visibility)",
1052
+ parameters: EXCEL_VIEW_SETTINGS_PARAMETERS,
1053
+ since: 1,
1054
+ },
1055
+ {
1056
+ host: "excel",
1057
+ op: "comments",
1058
+ name: officeToolName("excel", "comments"),
1059
+ label: "Excel Comments",
1060
+ description:
1061
+ "Read, add, update, reply, delete, resolve, and reopen cell comments in the attached Excel workbook.",
1062
+ promptSnippet: "Read or manage Excel cell comments and reply threads",
1063
+ parameters: EXCEL_COMMENTS_PARAMETERS,
1064
+ since: 1,
1065
+ },
1066
+ {
1067
+ host: "excel",
1068
+ op: "workbook_history",
1069
+ name: officeToolName("excel", "workbook_history"),
1070
+ label: "Excel Workbook History",
1071
+ description:
1072
+ "List, restore, and manage automatic workbook backups created before edits in the attached Excel workbook.",
1073
+ promptSnippet:
1074
+ "List/restore automatic backups of the attached Excel workbook",
1075
+ parameters: EXCEL_WORKBOOK_HISTORY_PARAMETERS,
1076
+ since: 1,
1077
+ },
1078
+
1079
+ /* ── Word ──────────────────────────────────────────────────────── */
1080
+ {
1081
+ host: "word",
1082
+ op: "get_overview",
1083
+ name: officeToolName("word", "get_overview"),
1084
+ label: "Word Document Overview",
1085
+ description:
1086
+ "Read a compact overview of the attached Word document: heading outline (with text), paragraph count, " +
1087
+ "table count, and word count. Call this first before editing.",
1088
+ promptSnippet: "Outline the attached Word document",
1089
+ promptGuidelines: [
1090
+ "Call office_word_get_overview before office_word_insert_text or office_word_replace_text.",
1091
+ ],
1092
+ parameters: WORD_GET_OVERVIEW_PARAMETERS,
1093
+ since: 1,
1094
+ legacyV1: true,
1095
+ },
1096
+ {
1097
+ host: "word",
1098
+ op: "read_document",
1099
+ name: officeToolName("word", "read_document"),
1100
+ label: "Word Read Document",
1101
+ description:
1102
+ "Read text from the attached Word document: the whole body or the current selection. " +
1103
+ "Paragraphs are preserved (one line per paragraph) with heading/list markers.",
1104
+ promptSnippet: "Read the attached Word document (or selection)",
1105
+ parameters: WORD_READ_DOCUMENT_PARAMETERS,
1106
+ since: 1,
1107
+ legacyV1: true,
1108
+ },
1109
+ {
1110
+ host: "word",
1111
+ op: "insert_text",
1112
+ name: officeToolName("word", "insert_text"),
1113
+ label: "Word Insert Text",
1114
+ description:
1115
+ "Insert text into the attached Word document — at the start, at the end (default), " +
1116
+ "or replacing the current selection. Multi-line text becomes separate paragraphs. " +
1117
+ "Optionally format the inserted text in the same call: font (bold, italic, underline, size, name, color), " +
1118
+ 'paragraph alignment ("Left"/"Centered"/"Right"/"Justified"), style names (e.g. "Heading 1", "Title"), ' +
1119
+ 'spacing, and indents. Use format: "markdown" to insert a markdown document (headings, bold/italic, bullets). ' +
1120
+ "Formatting is fully supported — never tell the user it is not.",
1121
+ promptSnippet: "Insert (and format) text into the attached Word document",
1122
+ promptGuidelines: [
1123
+ 'For a formatted document use office_word_insert_blocks (precise per-block control) or office_word_insert_text with format: "markdown".',
1124
+ "Never emit HTML for Word documents; use office_word_insert_text / office_word_insert_blocks instead.",
1125
+ ],
1126
+ parameters: WORD_INSERT_TEXT_PARAMETERS,
1127
+ since: 1,
1128
+ legacyV1: true,
1129
+ },
1130
+ {
1131
+ host: "word",
1132
+ op: "replace_text",
1133
+ name: officeToolName("word", "replace_text"),
1134
+ label: "Word Replace Text",
1135
+ description:
1136
+ "Find and replace literal text in the attached Word document. Returns how many occurrences were replaced.",
1137
+ promptSnippet:
1138
+ "Find and replace literal text in the attached Word document",
1139
+ parameters: WORD_REPLACE_TEXT_PARAMETERS,
1140
+ since: 1,
1141
+ legacyV1: true,
1142
+ },
1143
+ {
1144
+ host: "word",
1145
+ op: "format_range",
1146
+ name: officeToolName("word", "format_range"),
1147
+ label: "Word Format Range",
1148
+ description:
1149
+ "Find text by literal content in the attached Word document and apply formatting to every match: " +
1150
+ "bold, italic, underline, font size (points), font name, color (#RRGGBB), paragraph alignment, " +
1151
+ 'paragraph style (e.g. "Heading 1"), spacing, and indents. ' +
1152
+ "Use this to bold/size/center existing content — e.g. format a document title. Formatting is fully supported.",
1153
+ promptSnippet: "Format existing Word text (bold, size, alignment, style)",
1154
+ parameters: WORD_FORMAT_RANGE_PARAMETERS,
1155
+ since: 1,
1156
+ },
1157
+ {
1158
+ host: "word",
1159
+ op: "insert_blocks",
1160
+ name: officeToolName("word", "insert_blocks"),
1161
+ label: "Word Insert Blocks",
1162
+ description:
1163
+ "Assemble a structured formatted document in the attached Word document from blocks: " +
1164
+ "paragraphs, headings (level 1-6), bullet/numbered list items, and page breaks. " +
1165
+ "Each block can carry its own font (bold/italic/underline/size/name/color), " +
1166
+ "paragraph alignment, style name, spacing and indents. Use this for a complete formatted document " +
1167
+ "in one call — e.g. a title, then body paragraphs, then a right-aligned signature block.",
1168
+ promptSnippet:
1169
+ "Insert a fully formatted document (title, headings, lists, alignment) into Word",
1170
+ promptGuidelines: [
1171
+ "Prefer office_word_insert_blocks over many small inserts when composing a formatted document.",
1172
+ 'Align office_word_insert_blocks signatures/dates right (alignment: "Right") and keep heading levels ≤ 6.',
1173
+ "Never emit HTML for Word documents; use office_word_insert_blocks / office_word_insert_text instead.",
1174
+ ],
1175
+ parameters: WORD_INSERT_BLOCKS_PARAMETERS,
1176
+ since: 1,
1177
+ },
1178
+ {
1179
+ host: "word",
1180
+ op: "insert_table",
1181
+ name: officeToolName("word", "insert_table"),
1182
+ label: "Word Insert Table",
1183
+ description:
1184
+ "Insert a table into the attached Word document with values[row][col]. " +
1185
+ "Optionally bold the header row, apply a table style/alignment.",
1186
+ promptSnippet: "Insert a table into the attached Word document",
1187
+ parameters: WORD_INSERT_TABLE_PARAMETERS,
1188
+ since: 1,
1189
+ },
1190
+ {
1191
+ host: "word",
1192
+ op: "insert_page_break",
1193
+ name: officeToolName("word", "insert_page_break"),
1194
+ label: "Word Insert Page Break",
1195
+ description:
1196
+ "Insert a page break at the start or end of the attached Word document.",
1197
+ promptSnippet: "Insert a page break in the attached Word document",
1198
+ parameters: WORD_INSERT_PAGE_BREAK_PARAMETERS,
1199
+ since: 1,
1200
+ },
1201
+ {
1202
+ host: "word",
1203
+ op: "insert_image",
1204
+ name: officeToolName("word", "insert_image"),
1205
+ label: "Word Insert Image",
1206
+ description:
1207
+ "Insert an inline image into the attached Word document from a base64 string (or a data URL). " +
1208
+ "Max decoded size ~5 MB. To embed a local file, encode it first: on macOS/Linux run `base64 -w0 <file>` " +
1209
+ "in bash and pass the output; on Windows run `certutil -encode <file> tmp.b64` and read the file.",
1210
+ promptSnippet: "Insert an image (base64) into the attached Word document",
1211
+ parameters: WORD_INSERT_IMAGE_PARAMETERS,
1212
+ since: 1,
1213
+ },
1214
+ {
1215
+ host: "word",
1216
+ op: "insert_hyperlink",
1217
+ name: officeToolName("word", "insert_hyperlink"),
1218
+ label: "Word Insert Hyperlink",
1219
+ description:
1220
+ "Insert a clickable hyperlink with display text into the attached Word document.",
1221
+ promptSnippet: "Insert a hyperlink into the attached Word document",
1222
+ parameters: WORD_INSERT_HYPERLINK_PARAMETERS,
1223
+ since: 1,
1224
+ },
1225
+
1226
+ /* ── PowerPoint ─────────────────────────────────────────────────── */
1227
+ {
1228
+ host: "powerpoint",
1229
+ op: "get_overview",
1230
+ name: officeToolName("powerpoint", "get_overview"),
1231
+ label: "PowerPoint Overview",
1232
+ description:
1233
+ "Read a compact overview of the attached presentation: slide count, each slide's title and " +
1234
+ "shape count. Call this first before any slide operation.",
1235
+ promptSnippet: "Outline the attached PowerPoint presentation",
1236
+ promptGuidelines: [
1237
+ "Call office_powerpoint_get_overview before office_powerpoint_read_slide or office_powerpoint_add_slide.",
1238
+ ],
1239
+ parameters: POWERPOINT_GET_OVERVIEW_PARAMETERS,
1240
+ since: 1,
1241
+ legacyV1: true,
1242
+ },
1243
+ {
1244
+ host: "powerpoint",
1245
+ op: "read_slide",
1246
+ name: officeToolName("powerpoint", "read_slide"),
1247
+ label: "PowerPoint Read Slide",
1248
+ description:
1249
+ "Read all text content of one slide in the attached presentation (shapes, text frames, notes).",
1250
+ promptSnippet: "Read the text of a PowerPoint slide",
1251
+ parameters: POWERPOINT_READ_SLIDE_PARAMETERS,
1252
+ since: 1,
1253
+ legacyV1: true,
1254
+ },
1255
+ {
1256
+ host: "powerpoint",
1257
+ op: "add_slide",
1258
+ name: officeToolName("powerpoint", "add_slide"),
1259
+ label: "PowerPoint Add Slide",
1260
+ description:
1261
+ "Append a new slide to the attached presentation and navigate to it. Uses the default layout.",
1262
+ promptSnippet: "Append a slide to the attached PowerPoint presentation",
1263
+ parameters: POWERPOINT_ADD_SLIDE_PARAMETERS,
1264
+ since: 1,
1265
+ legacyV1: true,
1266
+ },
1267
+ {
1268
+ host: "powerpoint",
1269
+ op: "add_text_box",
1270
+ name: officeToolName("powerpoint", "add_text_box"),
1271
+ label: "PowerPoint Add Text Box",
1272
+ description:
1273
+ "Add a text box with the given text to a slide. Coordinates/geometry are in points. " +
1274
+ "Supports formatting in the same call (bold, italic, underline, fontSize/fontName/fontColor, " +
1275
+ 'alignment "Left"/"Center"/"Right"/"Justify") — always pass formatting for titles and headings.',
1276
+ promptSnippet: "Add a (formatted) text box to a PowerPoint slide",
1277
+ parameters: POWERPOINT_ADD_TEXT_BOX_PARAMETERS,
1278
+ since: 1,
1279
+ legacyV1: true,
1280
+ },
1281
+ {
1282
+ host: "powerpoint",
1283
+ op: "format_slide",
1284
+ name: officeToolName("powerpoint", "format_slide"),
1285
+ label: "PowerPoint Format Slide",
1286
+ description:
1287
+ "Apply formatting (bold, italic, underline, fontSize in points, fontName, fontColor #RRGGBB, " +
1288
+ 'alignment "Left"/"Center"/"Right"/"Justify") to every text box on a slide of the attached ' +
1289
+ "PowerPoint presentation. Use this to restyle existing content, e.g. make all text on a slide bold and centered.",
1290
+ promptSnippet: "Restyle all text boxes on a PowerPoint slide",
1291
+ parameters: POWERPOINT_FORMAT_SLIDE_PARAMETERS,
1292
+ since: 1,
1293
+ },
1294
+ ];
1295
+
1296
+ /** Index by "<host>.<op>" for fast lookup. */
1297
+ export const OFFICE_CATALOG_BY_OP: ReadonlyMap<string, OfficeCatalogEntry> =
1298
+ new Map(OFFICE_CATALOG.map((entry) => [`${entry.host}.${entry.op}`, entry]));
1299
+
1300
+ /** All Pi tool names in the catalog ("office_<host>_<op>"). */
1301
+ export const OFFICE_TOOL_NAMES: readonly string[] = OFFICE_CATALOG.map(
1302
+ (entry) => entry.name,
1303
+ );
1304
+
1305
+ /** Entries for one host. */
1306
+ export function catalogForHost(
1307
+ host: OfficeHostApp,
1308
+ ): readonly OfficeCatalogEntry[] {
1309
+ return OFFICE_CATALOG.filter((entry) => entry.host === host);
1310
+ }
1311
+
1312
+ /** The office host this tool name drives, or null when unknown. */
1313
+ export function hostForToolName(name: string): OfficeHostApp | null {
1314
+ const entry = OFFICE_CATALOG.find((candidate) => candidate.name === name);
1315
+ return entry?.host ?? null;
1316
+ }
1317
+
1318
+ /**
1319
+ * The op set exposed by the legacy 0.2.x bridge. Panes that do not advertise
1320
+ * `ops` are treated as legacy and only these ops are activated for them.
1321
+ */
1322
+ export const LEGACY_V1_OPS: readonly string[] = OFFICE_CATALOG.flatMap(
1323
+ (entry) => (entry.legacyV1 === true ? [`${entry.host}.${entry.op}`] : []),
1324
+ );
1325
+
1326
+ /** Op ids for one host — i.e. `<host>.<op>` strings. */
1327
+ export function catalogOpIdsForHost(host: OfficeHostApp): readonly string[] {
1328
+ return catalogForHost(host).map((entry) => `${entry.host}.${entry.op}`);
1329
+ }
1330
+
1331
+ /**
1332
+ * Resolve a `<host>.<op>` id to its shared parameter schema. Throws for
1333
+ * unknown ids so local tools import the single source of truth and drift is
1334
+ * impossible.
1335
+ */
1336
+ export function catalogSchemaFor(opId: string): TSchema {
1337
+ const entry = OFFICE_CATALOG_BY_OP.get(opId);
1338
+ if (!entry) {
1339
+ throw new Error(`office catalog: no schema for op "${opId}"`);
1340
+ }
1341
+ return entry.parameters;
1342
+ }