gogcli-mcp-docs 2.7.1 → 2.8.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
10
- "version": "2.7.1"
10
+ "version": "2.8.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli (Docs)",
16
16
  "source": "./",
17
17
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
18
- "version": "2.7.1",
18
+ "version": "2.8.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp-docs",
3
3
  "displayName": "gogcli (Docs)",
4
- "version": "2.7.1",
4
+ "version": "2.8.0",
5
5
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31277,12 +31277,14 @@ function registerDocsTools(server2) {
31277
31277
  text: external_exports.string().describe("Text content to write"),
31278
31278
  append: external_exports.boolean().optional().describe("Append to existing content instead of replacing (default: false)"),
31279
31279
  checkOrphans: external_exports.boolean().optional().describe("Block the write (exit code 11) if it would orphan an open comment \u2014 i.e. remove the text the comment is anchored to. Recommended for replacement writes on commented docs."),
31280
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin in gogcli-mcp-docs) instead of applying it \u2014 nothing changes in the doc until the batch is submitted."),
31280
31281
  account: accountParam
31281
31282
  }
31282
- }, async ({ docId, text, append, checkOrphans, account }) => {
31283
+ }, async ({ docId, text, append, checkOrphans, batch, account }) => {
31283
31284
  const args = ["docs", "write", docId, `--text=${text}`];
31284
31285
  if (append) args.push("--append");
31285
31286
  if (checkOrphans) args.push("--check-orphans");
31287
+ if (batch) args.push(`--batch=${batch}`);
31286
31288
  return runOrDiagnose(args, { account });
31287
31289
  });
31288
31290
  server2.registerTool("gog_docs_find_replace", {
@@ -31320,7 +31322,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31320
31322
  );
31321
31323
 
31322
31324
  // ../gogcli-mcp/src/server.ts
31323
- var VERSION = true ? "2.7.1" : "0.0.0";
31325
+ var VERSION = true ? "2.8.0" : "0.0.0";
31324
31326
  function createServer(options) {
31325
31327
  return new McpServer({
31326
31328
  name: options?.name ?? "gogcli",
@@ -31354,9 +31356,10 @@ function registerExtraDocsTools(server2) {
31354
31356
  occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31355
31357
  matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31356
31358
  tabId: external_exports.string().optional().describe("Tab ID to delete content from (for multi-tab docs)"),
31359
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
31357
31360
  account: accountParam
31358
31361
  }
31359
- }, async ({ docId, start, end, at, occurrence, matchCase, tabId, account }) => {
31362
+ }, async ({ docId, start, end, at, occurrence, matchCase, tabId, batch, account }) => {
31360
31363
  const args = ["docs", "delete"];
31361
31364
  if (start !== void 0) args.push(`--start=${start}`);
31362
31365
  if (end !== void 0) args.push(`--end=${end}`);
@@ -31365,6 +31368,7 @@ function registerExtraDocsTools(server2) {
31365
31368
  if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31366
31369
  if (matchCase) args.push("--match-case");
31367
31370
  if (tabId) args.push(`--tab-id=${tabId}`);
31371
+ if (batch) args.push(`--batch=${batch}`);
31368
31372
  return runOrDiagnose(args, { account });
31369
31373
  });
31370
31374
  server2.registerTool("gog_docs_trash", {
@@ -31444,6 +31448,7 @@ function registerExtraDocsTools(server2) {
31444
31448
  lineSpacing: external_exports.number().optional().describe("Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)"),
31445
31449
  headingLevel: external_exports.number().int().optional().describe("Set paragraph named style to HEADING_1..HEADING_6 (shortcut for namedStyle=HEADING_N)"),
31446
31450
  namedStyle: external_exports.enum(["NORMAL_TEXT", "TITLE", "SUBTITLE", "HEADING_1", "HEADING_2", "HEADING_3", "HEADING_4", "HEADING_5", "HEADING_6"]).optional().describe("Set paragraph named style explicitly"),
31451
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
31447
31452
  account: accountParam
31448
31453
  }
31449
31454
  }, async (args) => {
@@ -31472,6 +31477,7 @@ function registerExtraDocsTools(server2) {
31472
31477
  if (a.lineSpacing !== void 0) argv.push(`--line-spacing=${a.lineSpacing}`);
31473
31478
  if (a.headingLevel !== void 0) argv.push(`--heading-level=${a.headingLevel}`);
31474
31479
  if (a.namedStyle) argv.push(`--named-style=${a.namedStyle}`);
31480
+ if (a.batch) argv.push(`--batch=${a.batch}`);
31475
31481
  return runOrDiagnose(argv, { account: a.account });
31476
31482
  });
31477
31483
  server2.registerTool("gog_docs_export", {
@@ -31501,9 +31507,10 @@ function registerExtraDocsTools(server2) {
31501
31507
  occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31502
31508
  matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31503
31509
  tabId: external_exports.string().optional().describe("Tab ID to insert into (for multi-tab docs)"),
31510
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
31504
31511
  account: accountParam
31505
31512
  }
31506
- }, async ({ docId, content, index, file: file2, at, occurrence, matchCase, tabId, account }) => {
31513
+ }, async ({ docId, content, index, file: file2, at, occurrence, matchCase, tabId, batch, account }) => {
31507
31514
  const args = ["docs", "insert", docId];
31508
31515
  if (content) args.push(content);
31509
31516
  if (index !== void 0) args.push(`--index=${index}`);
@@ -31512,6 +31519,7 @@ function registerExtraDocsTools(server2) {
31512
31519
  if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31513
31520
  if (matchCase) args.push("--match-case");
31514
31521
  if (tabId) args.push(`--tab-id=${tabId}`);
31522
+ if (batch) args.push(`--batch=${batch}`);
31515
31523
  return runOrDiagnose(args, { account });
31516
31524
  });
31517
31525
  server2.registerTool("gog_docs_append", {
@@ -31581,9 +31589,10 @@ function registerExtraDocsTools(server2) {
31581
31589
  matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31582
31590
  tabId: external_exports.string().optional().describe("Tab ID for multi-tab docs"),
31583
31591
  pageless: external_exports.boolean().optional().describe("Set document to pageless format"),
31592
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
31584
31593
  account: accountParam
31585
31594
  }
31586
- }, async ({ docId, text, file: file2, index, replaceRange, markdown, at, occurrence, matchCase, tabId, pageless, account }) => {
31595
+ }, async ({ docId, text, file: file2, index, replaceRange, markdown, at, occurrence, matchCase, tabId, pageless, batch, account }) => {
31587
31596
  const args = ["docs", "update", docId];
31588
31597
  if (text) args.push(`--text=${text}`);
31589
31598
  if (file2) args.push(`--file=${file2}`);
@@ -31595,6 +31604,7 @@ function registerExtraDocsTools(server2) {
31595
31604
  if (matchCase) args.push("--match-case");
31596
31605
  if (tabId) args.push(`--tab-id=${tabId}`);
31597
31606
  if (pageless) args.push("--pageless");
31607
+ if (batch) args.push(`--batch=${batch}`);
31598
31608
  return runOrDiagnose(args, { account });
31599
31609
  });
31600
31610
  server2.registerTool("gog_docs_comments_list", {
@@ -31727,6 +31737,74 @@ function registerExtraDocsTools(server2) {
31727
31737
  if (tab) args.push(`--tab=${tab}`);
31728
31738
  return runOrDiagnose(args, { account });
31729
31739
  });
31740
+ server2.registerTool("gog_batch_begin", {
31741
+ description: "Open a persisted, revision-locked request batch for a Google Doc. Subsequent docs mutation tools called with batch=<batchId> append their requests locally instead of applying them; gog_batch_end submits everything atomically. Returns the batchId.",
31742
+ inputSchema: {
31743
+ docId: external_exports.string().describe("Google Doc ID the batch is locked to"),
31744
+ name: external_exports.string().optional().describe("Optional batch label"),
31745
+ account: accountParam
31746
+ }
31747
+ }, async ({ docId, name, account }) => {
31748
+ const args = ["batch", "begin", `--doc=${docId}`];
31749
+ if (name) args.push(`--name=${name}`);
31750
+ return runOrDiagnose(args, { account });
31751
+ });
31752
+ server2.registerTool("gog_batch_end", {
31753
+ description: "Submit a persisted batch: applies every composed request to the doc in one atomic batchUpdate against the locked revision. Atomic by default \u2014 set autoSplit to submit >500-request batches as ordered chunks (non-atomic), or continueOnError to retry individually after an atomic validation failure, retaining failures in the batch.",
31754
+ annotations: { destructiveHint: true },
31755
+ inputSchema: {
31756
+ batchId: external_exports.string().describe("Batch ID (from gog_batch_begin)"),
31757
+ autoSplit: external_exports.boolean().optional().describe("Submit batches over 500 requests as ordered chunks (non-atomic)"),
31758
+ continueOnError: external_exports.boolean().optional().describe("After an atomic validation failure, submit requests individually and retain failures"),
31759
+ account: accountParam
31760
+ }
31761
+ }, async ({ batchId, autoSplit, continueOnError, account }) => {
31762
+ const args = ["batch", "end", batchId];
31763
+ if (autoSplit) args.push("--auto-split");
31764
+ if (continueOnError) args.push("--continue-on-error");
31765
+ return runOrDiagnose(args, { account });
31766
+ });
31767
+ server2.registerTool("gog_batch_abort", {
31768
+ description: "Discard a persisted batch and its composed requests without applying anything to the doc.",
31769
+ annotations: { destructiveHint: true },
31770
+ inputSchema: {
31771
+ batchId: external_exports.string().describe("Batch ID to discard"),
31772
+ account: accountParam
31773
+ }
31774
+ }, async ({ batchId, account }) => {
31775
+ return runOrDiagnose(["batch", "abort", batchId], { account });
31776
+ });
31777
+ server2.registerTool("gog_batch_list", {
31778
+ description: "List persisted Docs request batches (id, doc, label, request count, status).",
31779
+ annotations: { readOnlyHint: true },
31780
+ inputSchema: {
31781
+ account: accountParam
31782
+ }
31783
+ }, async ({ account }) => {
31784
+ return runOrDiagnose(["batch", "list"], { account });
31785
+ });
31786
+ server2.registerTool("gog_batch_show", {
31787
+ description: "Show one persisted batch: its doc, locked revision, and the composed requests awaiting submission.",
31788
+ annotations: { readOnlyHint: true },
31789
+ inputSchema: {
31790
+ batchId: external_exports.string().describe("Batch ID"),
31791
+ account: accountParam
31792
+ }
31793
+ }, async ({ batchId, account }) => {
31794
+ return runOrDiagnose(["batch", "show", batchId], { account });
31795
+ });
31796
+ server2.registerTool("gog_batch_prune", {
31797
+ description: "Delete stale persisted batches (not updated within olderThan).",
31798
+ annotations: { destructiveHint: true },
31799
+ inputSchema: {
31800
+ olderThan: external_exports.string().optional().describe("Delete batches not updated within this duration (e.g. 72h, 7d)"),
31801
+ account: accountParam
31802
+ }
31803
+ }, async ({ olderThan, account }) => {
31804
+ const args = ["batch", "prune"];
31805
+ if (olderThan) args.push(`--older-than=${olderThan}`);
31806
+ return runOrDiagnose(args, { account });
31807
+ });
31730
31808
  server2.registerTool("gog_docs_table_column_width", {
31731
31809
  description: "Set a fixed width (in points) for a table column, or reset columns to Docs-managed even distribution. Target the table by 1-based index in document order (negative counts from the end) and the column by 1-based number. Pass evenlyDistributed without col to reset every column in the table.",
31732
31810
  annotations: { destructiveHint: true },
@@ -31737,15 +31815,17 @@ function registerExtraDocsTools(server2) {
31737
31815
  evenlyDistributed: external_exports.boolean().optional().describe("Reset the selected column (or all columns when col is omitted) to Docs-managed equal width."),
31738
31816
  tableIndex: external_exports.number().int().optional().describe("1-based table index in document order; negative counts from the end (default: 1)"),
31739
31817
  tab: external_exports.string().optional().describe("Target tab title or ID"),
31818
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
31740
31819
  account: accountParam
31741
31820
  }
31742
- }, async ({ docId, col, width, evenlyDistributed, tableIndex, tab, account }) => {
31821
+ }, async ({ docId, col, width, evenlyDistributed, tableIndex, tab, batch, account }) => {
31743
31822
  const args = ["docs", "table-column-width", docId];
31744
31823
  if (col !== void 0) args.push(`--col=${col}`);
31745
31824
  if (width !== void 0) args.push(`--width=${width}`);
31746
31825
  if (evenlyDistributed) args.push("--evenly-distributed");
31747
31826
  if (tableIndex !== void 0) args.push(`--table-index=${tableIndex}`);
31748
31827
  if (tab) args.push(`--tab=${tab}`);
31828
+ if (batch) args.push(`--batch=${batch}`);
31749
31829
  return runOrDiagnose(args, { account });
31750
31830
  });
31751
31831
  const tableSelectorParam = external_exports.string().optional().describe("Table selector: 1-based index (negative from the end), exact first-cell text, * for the only table, or text:VALUE for numeric/syntax-looking first-cell text (default: 1)");
@@ -31983,9 +32063,10 @@ function registerExtraDocsTools(server2) {
31983
32063
  occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31984
32064
  matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31985
32065
  tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
32066
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
31986
32067
  account: accountParam
31987
32068
  }
31988
- }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, account }) => {
32069
+ }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
31989
32070
  const args = ["docs", "insert-page-break", docId];
31990
32071
  if (index !== void 0) args.push(`--index=${index}`);
31991
32072
  if (atEnd) args.push("--at-end");
@@ -31993,6 +32074,7 @@ function registerExtraDocsTools(server2) {
31993
32074
  if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31994
32075
  if (matchCase) args.push("--match-case");
31995
32076
  if (tab) args.push(`--tab=${tab}`);
32077
+ if (batch) args.push(`--batch=${batch}`);
31996
32078
  return runOrDiagnose(args, { account });
31997
32079
  });
31998
32080
  server2.registerTool("gog_docs_page_layout", {
@@ -32084,9 +32166,10 @@ function registerExtraDocsTools(server2) {
32084
32166
  underline: external_exports.boolean().optional().describe("Set cell text underline"),
32085
32167
  tableIndex: external_exports.number().int().optional().describe("0-based table index in document order (default: 0)"),
32086
32168
  tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
32169
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
32087
32170
  account: accountParam
32088
32171
  }
32089
- }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, account }) => {
32172
+ }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, batch, account }) => {
32090
32173
  const args = ["docs", "cell-style", docId, `--row=${row}`, `--col=${col}`];
32091
32174
  if (rowSpan !== void 0) args.push(`--row-span=${rowSpan}`);
32092
32175
  if (colSpan !== void 0) args.push(`--col-span=${colSpan}`);
@@ -32097,6 +32180,7 @@ function registerExtraDocsTools(server2) {
32097
32180
  if (underline) args.push("--underline");
32098
32181
  if (tableIndex !== void 0) args.push(`--table-index=${tableIndex}`);
32099
32182
  if (tab) args.push(`--tab=${tab}`);
32183
+ if (batch) args.push(`--batch=${batch}`);
32100
32184
  return runOrDiagnose(args, { account });
32101
32185
  });
32102
32186
  server2.registerTool("gog_docs_insert_image", {
@@ -32140,9 +32224,10 @@ function registerExtraDocsTools(server2) {
32140
32224
  occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
32141
32225
  matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
32142
32226
  tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
32227
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
32143
32228
  account: accountParam
32144
32229
  }
32145
- }, async ({ docId, email: email3, index, atEnd, at, occurrence, matchCase, tab, account }) => {
32230
+ }, async ({ docId, email: email3, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
32146
32231
  const args = ["docs", "insert-person", docId, `--email=${email3}`];
32147
32232
  if (index !== void 0) args.push(`--index=${index}`);
32148
32233
  if (atEnd) args.push("--at-end");
@@ -32150,6 +32235,7 @@ function registerExtraDocsTools(server2) {
32150
32235
  if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
32151
32236
  if (matchCase) args.push("--match-case");
32152
32237
  if (tab) args.push(`--tab=${tab}`);
32238
+ if (batch) args.push(`--batch=${batch}`);
32153
32239
  return runOrDiagnose(args, { account });
32154
32240
  });
32155
32241
  server2.registerTool("gog_docs_insert_date_chip", {
@@ -32162,15 +32248,17 @@ function registerExtraDocsTools(server2) {
32162
32248
  index: external_exports.number().int().optional().describe("Character index to insert at. Omit or use atEnd for end-of-doc."),
32163
32249
  atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
32164
32250
  tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
32251
+ batch: external_exports.string().optional().describe("Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it \u2014 nothing changes in the doc until gog_batch_end submits the batch."),
32165
32252
  account: accountParam
32166
32253
  }
32167
- }, async ({ docId, date: date5, format, index, atEnd, tab, account }) => {
32254
+ }, async ({ docId, date: date5, format, index, atEnd, tab, batch, account }) => {
32168
32255
  const args = ["docs", "insert-date-chip", docId];
32169
32256
  if (date5) args.push(`--date=${date5}`);
32170
32257
  if (format) args.push(`--format=${format}`);
32171
32258
  if (index !== void 0) args.push(`--index=${index}`);
32172
32259
  if (atEnd) args.push("--at-end");
32173
32260
  if (tab) args.push(`--tab=${tab}`);
32261
+ if (batch) args.push(`--batch=${batch}`);
32174
32262
  return runOrDiagnose(args, { account });
32175
32263
  });
32176
32264
  server2.registerTool("gog_docs_add_tab", {
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-docs",
5
5
  "display_name": "gogcli (Docs)",
6
- "version": "2.7.1",
6
+ "version": "2.8.0",
7
7
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -203,6 +203,30 @@
203
203
  "name": "gog_docs_find_range",
204
204
  "description": "Map literal text to its Docs API UTF-16 index range(s) for index-based edits."
205
205
  },
206
+ {
207
+ "name": "gog_batch_begin",
208
+ "description": "Open a persisted, revision-locked request batch for a Doc; compose mutations with batch=<id>."
209
+ },
210
+ {
211
+ "name": "gog_batch_end",
212
+ "description": "Submit a batch atomically (or with explicit split / partial-recovery modes)."
213
+ },
214
+ {
215
+ "name": "gog_batch_abort",
216
+ "description": "Discard a batch without applying anything."
217
+ },
218
+ {
219
+ "name": "gog_batch_list",
220
+ "description": "List persisted request batches."
221
+ },
222
+ {
223
+ "name": "gog_batch_show",
224
+ "description": "Show a batch's doc, locked revision, and composed requests."
225
+ },
226
+ {
227
+ "name": "gog_batch_prune",
228
+ "description": "Delete stale batches not updated within a duration."
229
+ },
206
230
  {
207
231
  "name": "gog_docs_table_column_width",
208
232
  "description": "Set a fixed table column width in points, or reset columns to Docs-managed even distribution."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-docs",
3
- "version": "2.7.1",
3
+ "version": "2.8.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-docs",
5
5
  "description": "Extended Google Docs MCP server via gogcli — all base tools plus full Docs support",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-docs"
9
9
  },
10
- "version": "2.7.1",
10
+ "version": "2.8.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-docs",
15
- "version": "2.7.1",
15
+ "version": "2.8.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -28,9 +28,10 @@ export function registerExtraDocsTools(server: McpServer): void {
28
28
  occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
29
29
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
30
30
  tabId: z.string().optional().describe('Tab ID to delete content from (for multi-tab docs)'),
31
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
31
32
  account: accountParam,
32
33
  },
33
- }, async ({ docId, start, end, at, occurrence, matchCase, tabId, account }) => {
34
+ }, async ({ docId, start, end, at, occurrence, matchCase, tabId, batch, account }) => {
34
35
  const args = ['docs', 'delete'];
35
36
  if (start !== undefined) args.push(`--start=${start}`);
36
37
  if (end !== undefined) args.push(`--end=${end}`);
@@ -39,6 +40,7 @@ export function registerExtraDocsTools(server: McpServer): void {
39
40
  if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
40
41
  if (matchCase) args.push('--match-case');
41
42
  if (tabId) args.push(`--tab-id=${tabId}`);
43
+ if (batch) args.push(`--batch=${batch}`);
42
44
  return runOrDiagnose(args, { account });
43
45
  });
44
46
 
@@ -122,6 +124,7 @@ export function registerExtraDocsTools(server: McpServer): void {
122
124
  lineSpacing: z.number().optional().describe('Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)'),
123
125
  headingLevel: z.number().int().optional().describe('Set paragraph named style to HEADING_1..HEADING_6 (shortcut for namedStyle=HEADING_N)'),
124
126
  namedStyle: z.enum(['NORMAL_TEXT', 'TITLE', 'SUBTITLE', 'HEADING_1', 'HEADING_2', 'HEADING_3', 'HEADING_4', 'HEADING_5', 'HEADING_6']).optional().describe('Set paragraph named style explicitly'),
127
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
125
128
  account: accountParam,
126
129
  },
127
130
  }, async (args) => {
@@ -142,6 +145,7 @@ export function registerExtraDocsTools(server: McpServer): void {
142
145
  code?: boolean;
143
146
  link?: string; noLink?: boolean;
144
147
  alignment?: string;
148
+ batch?: string;
145
149
  lineSpacing?: number;
146
150
  headingLevel?: number;
147
151
  namedStyle?: string;
@@ -171,6 +175,7 @@ export function registerExtraDocsTools(server: McpServer): void {
171
175
  if (a.lineSpacing !== undefined) argv.push(`--line-spacing=${a.lineSpacing}`);
172
176
  if (a.headingLevel !== undefined) argv.push(`--heading-level=${a.headingLevel}`);
173
177
  if (a.namedStyle) argv.push(`--named-style=${a.namedStyle}`);
178
+ if (a.batch) argv.push(`--batch=${a.batch}`);
174
179
  return runOrDiagnose(argv, { account: a.account });
175
180
  });
176
181
 
@@ -202,9 +207,10 @@ export function registerExtraDocsTools(server: McpServer): void {
202
207
  occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
203
208
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
204
209
  tabId: z.string().optional().describe('Tab ID to insert into (for multi-tab docs)'),
210
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
205
211
  account: accountParam,
206
212
  },
207
- }, async ({ docId, content, index, file, at, occurrence, matchCase, tabId, account }) => {
213
+ }, async ({ docId, content, index, file, at, occurrence, matchCase, tabId, batch, account }) => {
208
214
  const args = ['docs', 'insert', docId];
209
215
  if (content) args.push(content);
210
216
  if (index !== undefined) args.push(`--index=${index}`);
@@ -213,6 +219,7 @@ export function registerExtraDocsTools(server: McpServer): void {
213
219
  if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
214
220
  if (matchCase) args.push('--match-case');
215
221
  if (tabId) args.push(`--tab-id=${tabId}`);
222
+ if (batch) args.push(`--batch=${batch}`);
216
223
  return runOrDiagnose(args, { account });
217
224
  });
218
225
 
@@ -286,9 +293,10 @@ export function registerExtraDocsTools(server: McpServer): void {
286
293
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
287
294
  tabId: z.string().optional().describe('Tab ID for multi-tab docs'),
288
295
  pageless: z.boolean().optional().describe('Set document to pageless format'),
296
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
289
297
  account: accountParam,
290
298
  },
291
- }, async ({ docId, text, file, index, replaceRange, markdown, at, occurrence, matchCase, tabId, pageless, account }) => {
299
+ }, async ({ docId, text, file, index, replaceRange, markdown, at, occurrence, matchCase, tabId, pageless, batch, account }) => {
292
300
  const args = ['docs', 'update', docId];
293
301
  if (text) args.push(`--text=${text}`);
294
302
  if (file) args.push(`--file=${file}`);
@@ -300,6 +308,7 @@ export function registerExtraDocsTools(server: McpServer): void {
300
308
  if (matchCase) args.push('--match-case');
301
309
  if (tabId) args.push(`--tab-id=${tabId}`);
302
310
  if (pageless) args.push('--pageless');
311
+ if (batch) args.push(`--batch=${batch}`);
303
312
  return runOrDiagnose(args, { account });
304
313
  });
305
314
 
@@ -445,6 +454,83 @@ export function registerExtraDocsTools(server: McpServer): void {
445
454
  return runOrDiagnose(args, { account });
446
455
  });
447
456
 
457
+ // Persisted, revision-locked Docs request batches (gog 0.25): begin a batch,
458
+ // run docs mutation tools with batch=<id> to compose requests locally, then
459
+ // end to submit them atomically against the locked revision.
460
+ server.registerTool('gog_batch_begin', {
461
+ description: 'Open a persisted, revision-locked request batch for a Google Doc. Subsequent docs mutation tools called with batch=<batchId> append their requests locally instead of applying them; gog_batch_end submits everything atomically. Returns the batchId.',
462
+ inputSchema: {
463
+ docId: z.string().describe('Google Doc ID the batch is locked to'),
464
+ name: z.string().optional().describe('Optional batch label'),
465
+ account: accountParam,
466
+ },
467
+ }, async ({ docId, name, account }) => {
468
+ const args = ['batch', 'begin', `--doc=${docId}`];
469
+ if (name) args.push(`--name=${name}`);
470
+ return runOrDiagnose(args, { account });
471
+ });
472
+
473
+ server.registerTool('gog_batch_end', {
474
+ description: 'Submit a persisted batch: applies every composed request to the doc in one atomic batchUpdate against the locked revision. Atomic by default — set autoSplit to submit >500-request batches as ordered chunks (non-atomic), or continueOnError to retry individually after an atomic validation failure, retaining failures in the batch.',
475
+ annotations: { destructiveHint: true },
476
+ inputSchema: {
477
+ batchId: z.string().describe('Batch ID (from gog_batch_begin)'),
478
+ autoSplit: z.boolean().optional().describe('Submit batches over 500 requests as ordered chunks (non-atomic)'),
479
+ continueOnError: z.boolean().optional().describe('After an atomic validation failure, submit requests individually and retain failures'),
480
+ account: accountParam,
481
+ },
482
+ }, async ({ batchId, autoSplit, continueOnError, account }) => {
483
+ const args = ['batch', 'end', batchId];
484
+ if (autoSplit) args.push('--auto-split');
485
+ if (continueOnError) args.push('--continue-on-error');
486
+ return runOrDiagnose(args, { account });
487
+ });
488
+
489
+ server.registerTool('gog_batch_abort', {
490
+ description: 'Discard a persisted batch and its composed requests without applying anything to the doc.',
491
+ annotations: { destructiveHint: true },
492
+ inputSchema: {
493
+ batchId: z.string().describe('Batch ID to discard'),
494
+ account: accountParam,
495
+ },
496
+ }, async ({ batchId, account }) => {
497
+ return runOrDiagnose(['batch', 'abort', batchId], { account });
498
+ });
499
+
500
+ server.registerTool('gog_batch_list', {
501
+ description: 'List persisted Docs request batches (id, doc, label, request count, status).',
502
+ annotations: { readOnlyHint: true },
503
+ inputSchema: {
504
+ account: accountParam,
505
+ },
506
+ }, async ({ account }) => {
507
+ return runOrDiagnose(['batch', 'list'], { account });
508
+ });
509
+
510
+ server.registerTool('gog_batch_show', {
511
+ description: 'Show one persisted batch: its doc, locked revision, and the composed requests awaiting submission.',
512
+ annotations: { readOnlyHint: true },
513
+ inputSchema: {
514
+ batchId: z.string().describe('Batch ID'),
515
+ account: accountParam,
516
+ },
517
+ }, async ({ batchId, account }) => {
518
+ return runOrDiagnose(['batch', 'show', batchId], { account });
519
+ });
520
+
521
+ server.registerTool('gog_batch_prune', {
522
+ description: 'Delete stale persisted batches (not updated within olderThan).',
523
+ annotations: { destructiveHint: true },
524
+ inputSchema: {
525
+ olderThan: z.string().optional().describe('Delete batches not updated within this duration (e.g. 72h, 7d)'),
526
+ account: accountParam,
527
+ },
528
+ }, async ({ olderThan, account }) => {
529
+ const args = ['batch', 'prune'];
530
+ if (olderThan) args.push(`--older-than=${olderThan}`);
531
+ return runOrDiagnose(args, { account });
532
+ });
533
+
448
534
  server.registerTool('gog_docs_table_column_width', {
449
535
  description: 'Set a fixed width (in points) for a table column, or reset columns to Docs-managed even distribution. Target the table by 1-based index in document order (negative counts from the end) and the column by 1-based number. Pass evenlyDistributed without col to reset every column in the table.',
450
536
  annotations: { destructiveHint: true },
@@ -455,15 +541,17 @@ export function registerExtraDocsTools(server: McpServer): void {
455
541
  evenlyDistributed: z.boolean().optional().describe('Reset the selected column (or all columns when col is omitted) to Docs-managed equal width.'),
456
542
  tableIndex: z.number().int().optional().describe('1-based table index in document order; negative counts from the end (default: 1)'),
457
543
  tab: z.string().optional().describe('Target tab title or ID'),
544
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
458
545
  account: accountParam,
459
546
  },
460
- }, async ({ docId, col, width, evenlyDistributed, tableIndex, tab, account }) => {
547
+ }, async ({ docId, col, width, evenlyDistributed, tableIndex, tab, batch, account }) => {
461
548
  const args = ['docs', 'table-column-width', docId];
462
549
  if (col !== undefined) args.push(`--col=${col}`);
463
550
  if (width !== undefined) args.push(`--width=${width}`);
464
551
  if (evenlyDistributed) args.push('--evenly-distributed');
465
552
  if (tableIndex !== undefined) args.push(`--table-index=${tableIndex}`);
466
553
  if (tab) args.push(`--tab=${tab}`);
554
+ if (batch) args.push(`--batch=${batch}`);
467
555
  return runOrDiagnose(args, { account });
468
556
  });
469
557
 
@@ -719,9 +807,10 @@ export function registerExtraDocsTools(server: McpServer): void {
719
807
  occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
720
808
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
721
809
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
810
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
722
811
  account: accountParam,
723
812
  },
724
- }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, account }) => {
813
+ }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
725
814
  const args = ['docs', 'insert-page-break', docId];
726
815
  if (index !== undefined) args.push(`--index=${index}`);
727
816
  if (atEnd) args.push('--at-end');
@@ -729,6 +818,7 @@ export function registerExtraDocsTools(server: McpServer): void {
729
818
  if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
730
819
  if (matchCase) args.push('--match-case');
731
820
  if (tab) args.push(`--tab=${tab}`);
821
+ if (batch) args.push(`--batch=${batch}`);
732
822
  return runOrDiagnose(args, { account });
733
823
  });
734
824
 
@@ -824,9 +914,10 @@ export function registerExtraDocsTools(server: McpServer): void {
824
914
  underline: z.boolean().optional().describe('Set cell text underline'),
825
915
  tableIndex: z.number().int().optional().describe('0-based table index in document order (default: 0)'),
826
916
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
917
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
827
918
  account: accountParam,
828
919
  },
829
- }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, account }) => {
920
+ }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, batch, account }) => {
830
921
  const args = ['docs', 'cell-style', docId, `--row=${row}`, `--col=${col}`];
831
922
  if (rowSpan !== undefined) args.push(`--row-span=${rowSpan}`);
832
923
  if (colSpan !== undefined) args.push(`--col-span=${colSpan}`);
@@ -837,6 +928,7 @@ export function registerExtraDocsTools(server: McpServer): void {
837
928
  if (underline) args.push('--underline');
838
929
  if (tableIndex !== undefined) args.push(`--table-index=${tableIndex}`);
839
930
  if (tab) args.push(`--tab=${tab}`);
931
+ if (batch) args.push(`--batch=${batch}`);
840
932
  return runOrDiagnose(args, { account });
841
933
  });
842
934
 
@@ -882,9 +974,10 @@ export function registerExtraDocsTools(server: McpServer): void {
882
974
  occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
883
975
  matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
884
976
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
977
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
885
978
  account: accountParam,
886
979
  },
887
- }, async ({ docId, email, index, atEnd, at, occurrence, matchCase, tab, account }) => {
980
+ }, async ({ docId, email, index, atEnd, at, occurrence, matchCase, tab, batch, account }) => {
888
981
  const args = ['docs', 'insert-person', docId, `--email=${email}`];
889
982
  if (index !== undefined) args.push(`--index=${index}`);
890
983
  if (atEnd) args.push('--at-end');
@@ -892,6 +985,7 @@ export function registerExtraDocsTools(server: McpServer): void {
892
985
  if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
893
986
  if (matchCase) args.push('--match-case');
894
987
  if (tab) args.push(`--tab=${tab}`);
988
+ if (batch) args.push(`--batch=${batch}`);
895
989
  return runOrDiagnose(args, { account });
896
990
  });
897
991
 
@@ -905,15 +999,17 @@ export function registerExtraDocsTools(server: McpServer): void {
905
999
  index: z.number().int().optional().describe('Character index to insert at. Omit or use atEnd for end-of-doc.'),
906
1000
  atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
907
1001
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
1002
+ batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin) instead of applying it — nothing changes in the doc until gog_batch_end submits the batch.'),
908
1003
  account: accountParam,
909
1004
  },
910
- }, async ({ docId, date, format, index, atEnd, tab, account }) => {
1005
+ }, async ({ docId, date, format, index, atEnd, tab, batch, account }) => {
911
1006
  const args = ['docs', 'insert-date-chip', docId];
912
1007
  if (date) args.push(`--date=${date}`);
913
1008
  if (format) args.push(`--format=${format}`);
914
1009
  if (index !== undefined) args.push(`--index=${index}`);
915
1010
  if (atEnd) args.push('--at-end');
916
1011
  if (tab) args.push(`--tab=${tab}`);
1012
+ if (batch) args.push(`--batch=${batch}`);
917
1013
  return runOrDiagnose(args, { account });
918
1014
  });
919
1015
 
@@ -1482,3 +1482,163 @@ describe('gog_docs_read json tab targeting', () => {
1482
1482
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'raw', 'd1', '--pretty', '--all-tabs'], { account: undefined });
1483
1483
  });
1484
1484
  });
1485
+
1486
+ // --- gog 0.25.0: persisted Docs request batches ---
1487
+
1488
+ describe('batch lifecycle tools', () => {
1489
+ it('gog_batch_begin opens a batch for a doc', async () => {
1490
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1491
+ const handlers = setupHandlers();
1492
+ await handlers.get('gog_batch_begin')!({ docId: 'd1', name: 'big-edit' });
1493
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1494
+ ['batch', 'begin', '--doc=d1', '--name=big-edit'],
1495
+ { account: undefined },
1496
+ );
1497
+ });
1498
+
1499
+ it('gog_batch_begin opens a batch without a name', async () => {
1500
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1501
+ const handlers = setupHandlers();
1502
+ await handlers.get('gog_batch_begin')!({ docId: 'd1' });
1503
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1504
+ ['batch', 'begin', '--doc=d1'],
1505
+ { account: undefined },
1506
+ );
1507
+ });
1508
+
1509
+ it('gog_batch_end submits with split/recovery modes', async () => {
1510
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1511
+ const handlers = setupHandlers();
1512
+ await handlers.get('gog_batch_end')!({ batchId: 'b1', autoSplit: true, continueOnError: true });
1513
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1514
+ ['batch', 'end', 'b1', '--auto-split', '--continue-on-error'],
1515
+ { account: undefined },
1516
+ );
1517
+ });
1518
+
1519
+ it('gog_batch_end submits atomically with no options', async () => {
1520
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1521
+ const handlers = setupHandlers();
1522
+ await handlers.get('gog_batch_end')!({ batchId: 'b1' });
1523
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1524
+ ['batch', 'end', 'b1'],
1525
+ { account: undefined },
1526
+ );
1527
+ });
1528
+
1529
+ it('gog_batch_abort discards a batch', async () => {
1530
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1531
+ const handlers = setupHandlers();
1532
+ await handlers.get('gog_batch_abort')!({ batchId: 'b1' });
1533
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'abort', 'b1'], { account: undefined });
1534
+ });
1535
+
1536
+ it('gog_batch_list / gog_batch_show read batch state', async () => {
1537
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1538
+ const handlers = setupHandlers();
1539
+ await handlers.get('gog_batch_list')!({});
1540
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'list'], { account: undefined });
1541
+ await handlers.get('gog_batch_show')!({ batchId: 'b1' });
1542
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'show', 'b1'], { account: undefined });
1543
+ });
1544
+
1545
+ it('gog_batch_prune deletes stale batches', async () => {
1546
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1547
+ const handlers = setupHandlers();
1548
+ await handlers.get('gog_batch_prune')!({ olderThan: '72h' });
1549
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'prune', '--older-than=72h'], { account: undefined });
1550
+ });
1551
+
1552
+ it('gog_batch_prune without a duration prunes with no filter', async () => {
1553
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1554
+ const handlers = setupHandlers();
1555
+ await handlers.get('gog_batch_prune')!({});
1556
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'prune'], { account: undefined });
1557
+ });
1558
+ });
1559
+
1560
+ describe('batch param on docs mutation tools (gog 0.25.0)', () => {
1561
+ // Each wrapped docs command that gained --batch appends to a persisted
1562
+ // batch instead of submitting when batch is supplied.
1563
+ const cases: Array<{ tool: string; input: Record<string, unknown>; expected: string[] }> = [
1564
+ { tool: 'gog_docs_insert', input: { docId: 'd1', content: 'X', batch: 'b1' },
1565
+ expected: ['docs', 'insert', 'd1', 'X', '--batch=b1'] },
1566
+ { tool: 'gog_docs_delete', input: { docId: 'd1', start: 1, end: 5, batch: 'b1' },
1567
+ expected: ['docs', 'delete', '--start=1', '--end=5', 'd1', '--batch=b1'] },
1568
+ { tool: 'gog_docs_update', input: { docId: 'd1', text: 'T', batch: 'b1' },
1569
+ expected: ['docs', 'update', 'd1', '--text=T', '--batch=b1'] },
1570
+ { tool: 'gog_docs_format', input: { docId: 'd1', match: 'm', bold: true, batch: 'b1' },
1571
+ expected: ['docs', 'format', 'd1', '--match=m', '--bold', '--batch=b1'] },
1572
+ { tool: 'gog_docs_insert_page_break', input: { docId: 'd1', atEnd: true, batch: 'b1' },
1573
+ expected: ['docs', 'insert-page-break', 'd1', '--at-end', '--batch=b1'] },
1574
+ { tool: 'gog_docs_insert_person', input: { docId: 'd1', email: 'a@b.com', batch: 'b1' },
1575
+ expected: ['docs', 'insert-person', 'd1', '--email=a@b.com', '--batch=b1'] },
1576
+ { tool: 'gog_docs_insert_date_chip', input: { docId: 'd1', batch: 'b1' },
1577
+ expected: ['docs', 'insert-date-chip', 'd1', '--batch=b1'] },
1578
+ { tool: 'gog_docs_table_column_width', input: { docId: 'd1', col: 1, width: 80, batch: 'b1' },
1579
+ expected: ['docs', 'table-column-width', 'd1', '--col=1', '--width=80', '--batch=b1'] },
1580
+ { tool: 'gog_docs_cell_style', input: { docId: 'd1', row: 0, col: 0, bold: true, batch: 'b1' },
1581
+ expected: ['docs', 'cell-style', 'd1', '--row=0', '--col=0', '--bold', '--batch=b1'] },
1582
+ ];
1583
+
1584
+ for (const { tool, input, expected } of cases) {
1585
+ it(`${tool} appends to a batch via --batch`, async () => {
1586
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1587
+ const handlers = setupHandlers();
1588
+ await handlers.get(tool)!(input);
1589
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(expected, { account: undefined });
1590
+ });
1591
+ }
1592
+ });
1593
+
1594
+ // Truthy --tab/--table branches the original tests missed (exposed by the
1595
+ // vitest 4.1.8 V8 remapping change — these flags were untested on these tools).
1596
+ describe('tab/table selector branch coverage', () => {
1597
+ const cases: Array<{ tool: string; input: Record<string, unknown>; expected: string[] }> = [
1598
+ { tool: 'gog_docs_table_row_delete', input: { docId: 'd1', row: 1, tab: 'T' },
1599
+ expected: ['docs', 'table-row', 'delete', 'd1', '--row=1', '--tab=T'] },
1600
+ { tool: 'gog_docs_table_column_delete', input: { docId: 'd1', col: 1, table: '2', tab: 'T' },
1601
+ expected: ['docs', 'table-column', 'delete', 'd1', '--col=1', '--table=2', '--tab=T'] },
1602
+ { tool: 'gog_docs_table_merge', input: { docId: 'd1', range: '1,1:2,2', tab: 'T' },
1603
+ expected: ['docs', 'table-merge', 'd1', '--range=1,1:2,2', '--tab=T'] },
1604
+ { tool: 'gog_docs_table_unmerge', input: { docId: 'd1', cell: '1,1', table: 'Budget' },
1605
+ expected: ['docs', 'table-unmerge', 'd1', '--cell=1,1', '--table=Budget'] },
1606
+ { tool: 'gog_docs_named_range_list', input: { docId: 'd1', tab: 'T' },
1607
+ expected: ['docs', 'named-range', 'list', 'd1', '--tab=T'] },
1608
+ { tool: 'gog_docs_named_range_delete', input: { docId: 'd1', nameOrId: 'nr', tab: 'T' },
1609
+ expected: ['docs', 'named-range', 'delete', 'd1', 'nr', '--tab=T'] },
1610
+ { tool: 'gog_docs_images_list', input: { docId: 'd1', tab: 'T' },
1611
+ expected: ['docs', 'images', 'list', 'd1', '--tab=T'] },
1612
+ { tool: 'gog_docs_headings_list', input: { docId: 'd1', tab: 'T' },
1613
+ expected: ['docs', 'headings', 'list', 'd1', '--tab=T'] },
1614
+ { tool: 'gog_docs_insert_date_chip', input: { docId: 'd1', tab: 'T' },
1615
+ expected: ['docs', 'insert-date-chip', 'd1', '--tab=T'] },
1616
+ ];
1617
+ for (const { tool, input, expected } of cases) {
1618
+ it(`${tool} passes its tab/table selectors`, async () => {
1619
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1620
+ const handlers = setupHandlers();
1621
+ await handlers.get(tool)!(input);
1622
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(expected, { account: undefined });
1623
+ });
1624
+ }
1625
+ });
1626
+
1627
+ describe('bare-call branch coverage (no optional flags)', () => {
1628
+ const cases: Array<{ tool: string; input: Record<string, unknown>; expected: string[] }> = [
1629
+ { tool: 'gog_docs_table_column_insert', input: { docId: 'd1' },
1630
+ expected: ['docs', 'table-column', 'insert', 'd1'] },
1631
+ { tool: 'gog_docs_tables_list', input: { docId: 'd1' },
1632
+ expected: ['docs', 'tables', 'list', 'd1'] },
1633
+ { tool: 'gog_docs_paragraphs_list', input: { docId: 'd1' },
1634
+ expected: ['docs', 'paragraphs', 'list', 'd1'] },
1635
+ ];
1636
+ for (const { tool, input, expected } of cases) {
1637
+ it(`${tool} with no optional flags`, async () => {
1638
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1639
+ const handlers = setupHandlers();
1640
+ await handlers.get(tool)!(input);
1641
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(expected, { account: undefined });
1642
+ });
1643
+ }
1644
+ });