gogcli-mcp-docs 2.2.0 → 2.4.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.2.0"
10
+ "version": "2.4.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.2.0",
18
+ "version": "2.4.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.2.0",
4
+ "version": "2.4.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
@@ -31313,7 +31313,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31313
31313
  );
31314
31314
 
31315
31315
  // ../gogcli-mcp/src/server.ts
31316
- var VERSION = true ? "2.2.0" : "0.0.0";
31316
+ var VERSION = true ? "2.4.0" : "0.0.0";
31317
31317
  function createServer(options) {
31318
31318
  return new McpServer({
31319
31319
  name: options?.name ?? "gogcli",
@@ -31543,15 +31543,19 @@ function registerExtraDocsTools(server2) {
31543
31543
  text: external_exports.string().optional().describe("Text content to write"),
31544
31544
  file: external_exports.string().optional().describe("Path to a file whose content to use"),
31545
31545
  index: external_exports.number().optional().describe("Character index to insert at"),
31546
+ replaceRange: external_exports.string().optional().describe('Replace a UTF-16 Docs API range START:END (e.g. "25:40") instead of inserting at index. The replacement text/file content overwrites the range.'),
31547
+ markdown: external_exports.boolean().optional().describe("Convert markdown in the text/file to Google Docs formatting (headings, bold, lists, etc.) instead of inserting it literally."),
31546
31548
  tabId: external_exports.string().optional().describe("Tab ID for multi-tab docs"),
31547
31549
  pageless: external_exports.boolean().optional().describe("Set document to pageless format"),
31548
31550
  account: accountParam
31549
31551
  }
31550
- }, async ({ docId, text, file: file2, index, tabId, pageless, account }) => {
31552
+ }, async ({ docId, text, file: file2, index, replaceRange, markdown, tabId, pageless, account }) => {
31551
31553
  const args = ["docs", "update", docId];
31552
31554
  if (text) args.push(`--text=${text}`);
31553
31555
  if (file2) args.push(`--file=${file2}`);
31554
31556
  if (index !== void 0) args.push(`--index=${index}`);
31557
+ if (replaceRange) args.push(`--replace-range=${replaceRange}`);
31558
+ if (markdown) args.push("--markdown");
31555
31559
  if (tabId) args.push(`--tab-id=${tabId}`);
31556
31560
  if (pageless) args.push("--pageless");
31557
31561
  return runOrDiagnose(args, { account });
@@ -31663,11 +31667,25 @@ function registerExtraDocsTools(server2) {
31663
31667
  inputSchema: {
31664
31668
  docId: external_exports.string().describe("Doc ID (from the URL)"),
31665
31669
  layout: external_exports.enum(["pageless", "pages"]).optional().describe("Page layout (default: pageless)"),
31670
+ pageSize: external_exports.enum(["A4", "A5", "Letter", "Legal", "Tabloid"]).optional().describe("Named page size preset (only applies in paged layout)"),
31671
+ pageWidth: external_exports.string().optional().describe('Page width (points by default; supports pt, in, cm, mm \u2014 e.g. "8.5in")'),
31672
+ pageHeight: external_exports.string().optional().describe("Page height (points by default; supports pt, in, cm, mm)"),
31673
+ marginTop: external_exports.string().optional().describe("Top page margin (points by default; supports pt, in, cm, mm)"),
31674
+ marginBottom: external_exports.string().optional().describe("Bottom page margin (points by default; supports pt, in, cm, mm)"),
31675
+ marginLeft: external_exports.string().optional().describe("Left page margin (points by default; supports pt, in, cm, mm)"),
31676
+ marginRight: external_exports.string().optional().describe("Right page margin (points by default; supports pt, in, cm, mm)"),
31666
31677
  account: accountParam
31667
31678
  }
31668
- }, async ({ docId, layout, account }) => {
31679
+ }, async ({ docId, layout, pageSize, pageWidth, pageHeight, marginTop, marginBottom, marginLeft, marginRight, account }) => {
31669
31680
  const args = ["docs", "page-layout", docId];
31670
31681
  if (layout) args.push(`--layout=${layout}`);
31682
+ if (pageSize) args.push(`--page-size=${pageSize}`);
31683
+ if (pageWidth) args.push(`--page-width=${pageWidth}`);
31684
+ if (pageHeight) args.push(`--page-height=${pageHeight}`);
31685
+ if (marginTop) args.push(`--margin-top=${marginTop}`);
31686
+ if (marginBottom) args.push(`--margin-bottom=${marginBottom}`);
31687
+ if (marginLeft) args.push(`--margin-left=${marginLeft}`);
31688
+ if (marginRight) args.push(`--margin-right=${marginRight}`);
31671
31689
  return runOrDiagnose(args, { account });
31672
31690
  });
31673
31691
  server2.registerTool("gog_docs_insert_table", {
@@ -31691,6 +31709,179 @@ function registerExtraDocsTools(server2) {
31691
31709
  if (tab) args.push(`--tab=${tab}`);
31692
31710
  return runOrDiagnose(args, { account });
31693
31711
  });
31712
+ server2.registerTool("gog_docs_cell_update", {
31713
+ description: "Replace (or append to) the content of a single table cell, addressed by table / row / column \u2014 non-destructive to the rest of the table, unlike index-based edits that shift on every change. Provide content inline or read it from contentFile. Note: row/col/tableIndex are 1-based here; the sibling gog_docs_cell_style uses 0-based addressing.",
31714
+ annotations: { destructiveHint: true },
31715
+ inputSchema: {
31716
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31717
+ row: external_exports.number().int().describe("1-based row number"),
31718
+ col: external_exports.number().int().describe("1-based column number"),
31719
+ content: external_exports.string().optional().describe("Replacement content (omit when using contentFile)"),
31720
+ contentFile: external_exports.string().optional().describe("Read replacement content from a file instead of content"),
31721
+ append: external_exports.boolean().optional().describe("Append inside the cell instead of replacing existing cell content"),
31722
+ format: external_exports.enum(["markdown", "plain"]).optional().describe("Content format (default: markdown)"),
31723
+ tableIndex: external_exports.number().int().optional().describe("1-based table index in document order; negative counts from the end (default: 1)"),
31724
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31725
+ account: accountParam
31726
+ }
31727
+ }, async ({ docId, row, col, content, contentFile, append, format, tableIndex, tab, account }) => {
31728
+ const args = ["docs", "cell-update", docId, `--row=${row}`, `--col=${col}`];
31729
+ if (content !== void 0) args.push(`--content=${content}`);
31730
+ if (contentFile) args.push(`--content-file=${contentFile}`);
31731
+ if (append) args.push("--append");
31732
+ if (format) args.push(`--format=${format}`);
31733
+ if (tableIndex !== void 0) args.push(`--table-index=${tableIndex}`);
31734
+ if (tab) args.push(`--tab=${tab}`);
31735
+ return runOrDiagnose(args, { account });
31736
+ });
31737
+ server2.registerTool("gog_docs_cell_style", {
31738
+ description: "Apply background color and/or inline text styling (bold, italic, underline, colors) to one or more table cells, addressed by 0-based row/column with optional spans. Sibling to gog_docs_cell_update, which changes cell content rather than styling. Note: row/col/tableIndex are 0-based here; gog_docs_cell_update uses 1-based addressing.",
31739
+ annotations: { destructiveHint: true },
31740
+ inputSchema: {
31741
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31742
+ row: external_exports.number().int().describe("0-based row number"),
31743
+ col: external_exports.number().int().describe("0-based column number"),
31744
+ rowSpan: external_exports.number().int().optional().describe("Number of rows to style (default: 1)"),
31745
+ colSpan: external_exports.number().int().optional().describe("Number of columns to style (default: 1)"),
31746
+ backgroundColor: external_exports.string().optional().describe("Cell background color as #RRGGBB or #RGB"),
31747
+ textColor: external_exports.string().optional().describe("Text color as #RRGGBB or #RGB"),
31748
+ bold: external_exports.boolean().optional().describe("Set cell text bold"),
31749
+ italic: external_exports.boolean().optional().describe("Set cell text italic"),
31750
+ underline: external_exports.boolean().optional().describe("Set cell text underline"),
31751
+ tableIndex: external_exports.number().int().optional().describe("0-based table index in document order (default: 0)"),
31752
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31753
+ account: accountParam
31754
+ }
31755
+ }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, account }) => {
31756
+ const args = ["docs", "cell-style", docId, `--row=${row}`, `--col=${col}`];
31757
+ if (rowSpan !== void 0) args.push(`--row-span=${rowSpan}`);
31758
+ if (colSpan !== void 0) args.push(`--col-span=${colSpan}`);
31759
+ if (backgroundColor) args.push(`--background-color=${backgroundColor}`);
31760
+ if (textColor) args.push(`--text-color=${textColor}`);
31761
+ if (bold) args.push("--bold");
31762
+ if (italic) args.push("--italic");
31763
+ if (underline) args.push("--underline");
31764
+ if (tableIndex !== void 0) args.push(`--table-index=${tableIndex}`);
31765
+ if (tab) args.push(`--tab=${tab}`);
31766
+ return runOrDiagnose(args, { account });
31767
+ });
31768
+ server2.registerTool("gog_docs_insert_image", {
31769
+ description: "Upload a local image (PNG, JPEG, or GIF) and insert it into a Google Doc. The image is uploaded to Drive, temporarily shared so Docs can fetch it, inserted, then the public permission is revoked. Replaces placeholder text (at) or appends at end-of-doc.",
31770
+ annotations: { destructiveHint: true },
31771
+ inputSchema: {
31772
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31773
+ file: external_exports.string().describe("Local PNG, JPEG, or GIF image to upload and insert"),
31774
+ at: external_exports.string().optional().describe('Placeholder text to replace, or "end" to append (default: end)'),
31775
+ width: external_exports.number().optional().describe("Image width in points (default: 468)"),
31776
+ height: external_exports.number().optional().describe("Image height in points (optional; width-only preserves aspect ratio)"),
31777
+ name: external_exports.string().optional().describe("Override the uploaded Drive filename"),
31778
+ parent: external_exports.string().optional().describe("Drive folder ID for the uploaded image"),
31779
+ onRestricted: external_exports.enum(["error", "link"]).optional().describe("If public sharing is blocked: error out, or fall back to a linked image (default: error)"),
31780
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31781
+ account: accountParam
31782
+ }
31783
+ }, async ({ docId, file: file2, at, width, height, name, parent, onRestricted, tab, account }) => {
31784
+ const args = ["docs", "insert-image", docId, `--file=${file2}`];
31785
+ if (at) args.push(`--at=${at}`);
31786
+ if (width !== void 0) args.push(`--width=${width}`);
31787
+ if (height !== void 0) args.push(`--height=${height}`);
31788
+ if (name) args.push(`--name=${name}`);
31789
+ if (parent) args.push(`--parent=${parent}`);
31790
+ if (onRestricted) args.push(`--on-restricted=${onRestricted}`);
31791
+ if (tab) args.push(`--tab=${tab}`);
31792
+ return runOrDiagnose(args, { account });
31793
+ });
31794
+ server2.registerTool("gog_docs_insert_person", {
31795
+ description: "Insert a native Google Docs person smart chip (the interactive @-mention chip) for an email address, at a character index or end-of-doc.",
31796
+ annotations: { destructiveHint: true },
31797
+ inputSchema: {
31798
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31799
+ email: external_exports.string().describe("Email address for the person chip"),
31800
+ index: external_exports.number().int().optional().describe("Character index to insert at. Omit or use atEnd for end-of-doc."),
31801
+ atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
31802
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31803
+ account: accountParam
31804
+ }
31805
+ }, async ({ docId, email: email3, index, atEnd, tab, account }) => {
31806
+ const args = ["docs", "insert-person", docId, `--email=${email3}`];
31807
+ if (index !== void 0) args.push(`--index=${index}`);
31808
+ if (atEnd) args.push("--at-end");
31809
+ if (tab) args.push(`--tab=${tab}`);
31810
+ return runOrDiagnose(args, { account });
31811
+ });
31812
+ server2.registerTool("gog_docs_insert_date_chip", {
31813
+ description: "Insert a native Google Docs date smart chip, at a character index or end-of-doc.",
31814
+ annotations: { destructiveHint: true },
31815
+ inputSchema: {
31816
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31817
+ date: external_exports.string().optional().describe("Date to insert as YYYY-MM-DD (default: today)"),
31818
+ format: external_exports.enum(["abbreviated", "full", "iso"]).optional().describe("Date display format (default: abbreviated)"),
31819
+ index: external_exports.number().int().optional().describe("Character index to insert at. Omit or use atEnd for end-of-doc."),
31820
+ atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
31821
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31822
+ account: accountParam
31823
+ }
31824
+ }, async ({ docId, date: date5, format, index, atEnd, tab, account }) => {
31825
+ const args = ["docs", "insert-date-chip", docId];
31826
+ if (date5) args.push(`--date=${date5}`);
31827
+ if (format) args.push(`--format=${format}`);
31828
+ if (index !== void 0) args.push(`--index=${index}`);
31829
+ if (atEnd) args.push("--at-end");
31830
+ if (tab) args.push(`--tab=${tab}`);
31831
+ return runOrDiagnose(args, { account });
31832
+ });
31833
+ server2.registerTool("gog_docs_add_tab", {
31834
+ description: "Add a tab to a Google Doc. Tabs partition a doc into independently-addressable sections (multi-tab docs). Optionally set the title, zero-based position, parent tab (for nesting), and an emoji icon.",
31835
+ inputSchema: {
31836
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31837
+ title: external_exports.string().optional().describe("User-visible tab title"),
31838
+ index: external_exports.number().int().optional().describe("Zero-based tab position within the parent"),
31839
+ parentTab: external_exports.string().optional().describe("Parent tab title or ID to nest this tab under"),
31840
+ iconEmoji: external_exports.string().optional().describe("Emoji icon for the tab"),
31841
+ account: accountParam
31842
+ }
31843
+ }, async ({ docId, title, index, parentTab, iconEmoji, account }) => {
31844
+ const args = ["docs", "add-tab", docId];
31845
+ if (title) args.push(`--title=${title}`);
31846
+ if (index !== void 0) args.push(`--index=${index}`);
31847
+ if (parentTab) args.push(`--parent-tab=${parentTab}`);
31848
+ if (iconEmoji) args.push(`--icon-emoji=${iconEmoji}`);
31849
+ return runOrDiagnose(args, { account });
31850
+ });
31851
+ server2.registerTool("gog_docs_rename_tab", {
31852
+ description: "Rename a tab in a Google Doc. Identify the existing tab by title or ID and give it a new title.",
31853
+ inputSchema: {
31854
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31855
+ tab: external_exports.string().describe("Existing tab title or ID to rename"),
31856
+ title: external_exports.string().describe("New user-visible tab title"),
31857
+ account: accountParam
31858
+ }
31859
+ }, async ({ docId, tab, title, account }) => {
31860
+ const args = ["docs", "rename-tab", docId, `--tab=${tab}`, `--title=${title}`];
31861
+ return runOrDiagnose(args, { account });
31862
+ });
31863
+ server2.registerTool("gog_docs_delete_tab", {
31864
+ description: "Delete a tab (and its content) from a Google Doc. Identify the tab by title or ID. This permanently removes the tab and everything in it.",
31865
+ annotations: { destructiveHint: true },
31866
+ inputSchema: {
31867
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31868
+ tab: external_exports.string().describe("Existing tab title or ID to delete"),
31869
+ account: accountParam
31870
+ }
31871
+ }, async ({ docId, tab, account }) => {
31872
+ const args = ["docs", "delete-tab", docId, `--tab=${tab}`];
31873
+ return runOrDiagnose(args, { account });
31874
+ });
31875
+ server2.registerTool("gog_docs_clear", {
31876
+ description: "Clear all content from a Google Doc, leaving an empty document. The doc itself is preserved (same ID/URL); only its body content is removed. To delete the whole doc instead, use gog_docs_trash.",
31877
+ annotations: { destructiveHint: true },
31878
+ inputSchema: {
31879
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31880
+ account: accountParam
31881
+ }
31882
+ }, async ({ docId, account }) => {
31883
+ return runOrDiagnose(["docs", "clear", docId], { account });
31884
+ });
31694
31885
  }
31695
31886
 
31696
31887
  // src/index.ts
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.2.0",
6
+ "version": "2.4.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",
@@ -127,6 +127,22 @@
127
127
  "name": "gog_docs_list_tabs",
128
128
  "description": "List all tabs in a Google Doc."
129
129
  },
130
+ {
131
+ "name": "gog_docs_add_tab",
132
+ "description": "Add a tab to a Google Doc (title, position, parent, emoji icon)."
133
+ },
134
+ {
135
+ "name": "gog_docs_rename_tab",
136
+ "description": "Rename a tab in a Google Doc."
137
+ },
138
+ {
139
+ "name": "gog_docs_delete_tab",
140
+ "description": "Delete a tab and its content from a Google Doc."
141
+ },
142
+ {
143
+ "name": "gog_docs_clear",
144
+ "description": "Clear all content from a Google Doc, leaving an empty document."
145
+ },
130
146
  {
131
147
  "name": "gog_docs_sed",
132
148
  "description": "Stream-edit a Google Doc with sed-like regex expressions (s/find/replace/ syntax)."
@@ -190,6 +206,26 @@
190
206
  {
191
207
  "name": "gog_docs_insert_table",
192
208
  "description": "Insert a native Google Docs table with given dimensions, optionally populated from a JSON 2D array."
209
+ },
210
+ {
211
+ "name": "gog_docs_cell_update",
212
+ "description": "Replace or append the content of a single table cell, addressed by table/row/column."
213
+ },
214
+ {
215
+ "name": "gog_docs_cell_style",
216
+ "description": "Apply background color and inline text styling to one or more table cells."
217
+ },
218
+ {
219
+ "name": "gog_docs_insert_image",
220
+ "description": "Upload a local image and insert it into a Google Doc."
221
+ },
222
+ {
223
+ "name": "gog_docs_insert_person",
224
+ "description": "Insert a native Google Docs person smart chip for an email address."
225
+ },
226
+ {
227
+ "name": "gog_docs_insert_date_chip",
228
+ "description": "Insert a native Google Docs date smart chip."
193
229
  }
194
230
  ],
195
231
  "compatibility": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-docs",
3
- "version": "2.2.0",
3
+ "version": "2.4.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.2.0",
10
+ "version": "2.4.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-docs",
15
- "version": "2.2.0",
15
+ "version": "2.4.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -253,15 +253,19 @@ export function registerExtraDocsTools(server: McpServer): void {
253
253
  text: z.string().optional().describe('Text content to write'),
254
254
  file: z.string().optional().describe('Path to a file whose content to use'),
255
255
  index: z.number().optional().describe('Character index to insert at'),
256
+ replaceRange: z.string().optional().describe('Replace a UTF-16 Docs API range START:END (e.g. "25:40") instead of inserting at index. The replacement text/file content overwrites the range.'),
257
+ markdown: z.boolean().optional().describe('Convert markdown in the text/file to Google Docs formatting (headings, bold, lists, etc.) instead of inserting it literally.'),
256
258
  tabId: z.string().optional().describe('Tab ID for multi-tab docs'),
257
259
  pageless: z.boolean().optional().describe('Set document to pageless format'),
258
260
  account: accountParam,
259
261
  },
260
- }, async ({ docId, text, file, index, tabId, pageless, account }) => {
262
+ }, async ({ docId, text, file, index, replaceRange, markdown, tabId, pageless, account }) => {
261
263
  const args = ['docs', 'update', docId];
262
264
  if (text) args.push(`--text=${text}`);
263
265
  if (file) args.push(`--file=${file}`);
264
266
  if (index !== undefined) args.push(`--index=${index}`);
267
+ if (replaceRange) args.push(`--replace-range=${replaceRange}`);
268
+ if (markdown) args.push('--markdown');
265
269
  if (tabId) args.push(`--tab-id=${tabId}`);
266
270
  if (pageless) args.push('--pageless');
267
271
  return runOrDiagnose(args, { account });
@@ -385,11 +389,25 @@ export function registerExtraDocsTools(server: McpServer): void {
385
389
  inputSchema: {
386
390
  docId: z.string().describe('Doc ID (from the URL)'),
387
391
  layout: z.enum(['pageless', 'pages']).optional().describe('Page layout (default: pageless)'),
392
+ pageSize: z.enum(['A4', 'A5', 'Letter', 'Legal', 'Tabloid']).optional().describe('Named page size preset (only applies in paged layout)'),
393
+ pageWidth: z.string().optional().describe('Page width (points by default; supports pt, in, cm, mm — e.g. "8.5in")'),
394
+ pageHeight: z.string().optional().describe('Page height (points by default; supports pt, in, cm, mm)'),
395
+ marginTop: z.string().optional().describe('Top page margin (points by default; supports pt, in, cm, mm)'),
396
+ marginBottom: z.string().optional().describe('Bottom page margin (points by default; supports pt, in, cm, mm)'),
397
+ marginLeft: z.string().optional().describe('Left page margin (points by default; supports pt, in, cm, mm)'),
398
+ marginRight: z.string().optional().describe('Right page margin (points by default; supports pt, in, cm, mm)'),
388
399
  account: accountParam,
389
400
  },
390
- }, async ({ docId, layout, account }) => {
401
+ }, async ({ docId, layout, pageSize, pageWidth, pageHeight, marginTop, marginBottom, marginLeft, marginRight, account }) => {
391
402
  const args = ['docs', 'page-layout', docId];
392
403
  if (layout) args.push(`--layout=${layout}`);
404
+ if (pageSize) args.push(`--page-size=${pageSize}`);
405
+ if (pageWidth) args.push(`--page-width=${pageWidth}`);
406
+ if (pageHeight) args.push(`--page-height=${pageHeight}`);
407
+ if (marginTop) args.push(`--margin-top=${marginTop}`);
408
+ if (marginBottom) args.push(`--margin-bottom=${marginBottom}`);
409
+ if (marginLeft) args.push(`--margin-left=${marginLeft}`);
410
+ if (marginRight) args.push(`--margin-right=${marginRight}`);
393
411
  return runOrDiagnose(args, { account });
394
412
  });
395
413
 
@@ -414,4 +432,186 @@ export function registerExtraDocsTools(server: McpServer): void {
414
432
  if (tab) args.push(`--tab=${tab}`);
415
433
  return runOrDiagnose(args, { account });
416
434
  });
435
+
436
+ server.registerTool('gog_docs_cell_update', {
437
+ description: 'Replace (or append to) the content of a single table cell, addressed by table / row / column — non-destructive to the rest of the table, unlike index-based edits that shift on every change. Provide content inline or read it from contentFile. Note: row/col/tableIndex are 1-based here; the sibling gog_docs_cell_style uses 0-based addressing.',
438
+ annotations: { destructiveHint: true },
439
+ inputSchema: {
440
+ docId: z.string().describe('Doc ID (from the URL)'),
441
+ row: z.number().int().describe('1-based row number'),
442
+ col: z.number().int().describe('1-based column number'),
443
+ content: z.string().optional().describe('Replacement content (omit when using contentFile)'),
444
+ contentFile: z.string().optional().describe('Read replacement content from a file instead of content'),
445
+ append: z.boolean().optional().describe('Append inside the cell instead of replacing existing cell content'),
446
+ format: z.enum(['markdown', 'plain']).optional().describe('Content format (default: markdown)'),
447
+ tableIndex: z.number().int().optional().describe('1-based table index in document order; negative counts from the end (default: 1)'),
448
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
449
+ account: accountParam,
450
+ },
451
+ }, async ({ docId, row, col, content, contentFile, append, format, tableIndex, tab, account }) => {
452
+ const args = ['docs', 'cell-update', docId, `--row=${row}`, `--col=${col}`];
453
+ if (content !== undefined) args.push(`--content=${content}`);
454
+ if (contentFile) args.push(`--content-file=${contentFile}`);
455
+ if (append) args.push('--append');
456
+ if (format) args.push(`--format=${format}`);
457
+ if (tableIndex !== undefined) args.push(`--table-index=${tableIndex}`);
458
+ if (tab) args.push(`--tab=${tab}`);
459
+ return runOrDiagnose(args, { account });
460
+ });
461
+
462
+ server.registerTool('gog_docs_cell_style', {
463
+ description: 'Apply background color and/or inline text styling (bold, italic, underline, colors) to one or more table cells, addressed by 0-based row/column with optional spans. Sibling to gog_docs_cell_update, which changes cell content rather than styling. Note: row/col/tableIndex are 0-based here; gog_docs_cell_update uses 1-based addressing.',
464
+ annotations: { destructiveHint: true },
465
+ inputSchema: {
466
+ docId: z.string().describe('Doc ID (from the URL)'),
467
+ row: z.number().int().describe('0-based row number'),
468
+ col: z.number().int().describe('0-based column number'),
469
+ rowSpan: z.number().int().optional().describe('Number of rows to style (default: 1)'),
470
+ colSpan: z.number().int().optional().describe('Number of columns to style (default: 1)'),
471
+ backgroundColor: z.string().optional().describe('Cell background color as #RRGGBB or #RGB'),
472
+ textColor: z.string().optional().describe('Text color as #RRGGBB or #RGB'),
473
+ bold: z.boolean().optional().describe('Set cell text bold'),
474
+ italic: z.boolean().optional().describe('Set cell text italic'),
475
+ underline: z.boolean().optional().describe('Set cell text underline'),
476
+ tableIndex: z.number().int().optional().describe('0-based table index in document order (default: 0)'),
477
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
478
+ account: accountParam,
479
+ },
480
+ }, async ({ docId, row, col, rowSpan, colSpan, backgroundColor, textColor, bold, italic, underline, tableIndex, tab, account }) => {
481
+ const args = ['docs', 'cell-style', docId, `--row=${row}`, `--col=${col}`];
482
+ if (rowSpan !== undefined) args.push(`--row-span=${rowSpan}`);
483
+ if (colSpan !== undefined) args.push(`--col-span=${colSpan}`);
484
+ if (backgroundColor) args.push(`--background-color=${backgroundColor}`);
485
+ if (textColor) args.push(`--text-color=${textColor}`);
486
+ if (bold) args.push('--bold');
487
+ if (italic) args.push('--italic');
488
+ if (underline) args.push('--underline');
489
+ if (tableIndex !== undefined) args.push(`--table-index=${tableIndex}`);
490
+ if (tab) args.push(`--tab=${tab}`);
491
+ return runOrDiagnose(args, { account });
492
+ });
493
+
494
+ server.registerTool('gog_docs_insert_image', {
495
+ description: 'Upload a local image (PNG, JPEG, or GIF) and insert it into a Google Doc. The image is uploaded to Drive, temporarily shared so Docs can fetch it, inserted, then the public permission is revoked. Replaces placeholder text (at) or appends at end-of-doc.',
496
+ annotations: { destructiveHint: true },
497
+ inputSchema: {
498
+ docId: z.string().describe('Doc ID (from the URL)'),
499
+ file: z.string().describe('Local PNG, JPEG, or GIF image to upload and insert'),
500
+ at: z.string().optional().describe('Placeholder text to replace, or "end" to append (default: end)'),
501
+ width: z.number().optional().describe('Image width in points (default: 468)'),
502
+ height: z.number().optional().describe('Image height in points (optional; width-only preserves aspect ratio)'),
503
+ name: z.string().optional().describe('Override the uploaded Drive filename'),
504
+ parent: z.string().optional().describe('Drive folder ID for the uploaded image'),
505
+ onRestricted: z.enum(['error', 'link']).optional().describe('If public sharing is blocked: error out, or fall back to a linked image (default: error)'),
506
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
507
+ account: accountParam,
508
+ },
509
+ }, async ({ docId, file, at, width, height, name, parent, onRestricted, tab, account }) => {
510
+ const args = ['docs', 'insert-image', docId, `--file=${file}`];
511
+ if (at) args.push(`--at=${at}`);
512
+ if (width !== undefined) args.push(`--width=${width}`);
513
+ if (height !== undefined) args.push(`--height=${height}`);
514
+ if (name) args.push(`--name=${name}`);
515
+ if (parent) args.push(`--parent=${parent}`);
516
+ if (onRestricted) args.push(`--on-restricted=${onRestricted}`);
517
+ if (tab) args.push(`--tab=${tab}`);
518
+ return runOrDiagnose(args, { account });
519
+ });
520
+
521
+ server.registerTool('gog_docs_insert_person', {
522
+ description: 'Insert a native Google Docs person smart chip (the interactive @-mention chip) for an email address, at a character index or end-of-doc.',
523
+ annotations: { destructiveHint: true },
524
+ inputSchema: {
525
+ docId: z.string().describe('Doc ID (from the URL)'),
526
+ email: z.string().describe('Email address for the person chip'),
527
+ index: z.number().int().optional().describe('Character index to insert at. Omit or use atEnd for end-of-doc.'),
528
+ atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
529
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
530
+ account: accountParam,
531
+ },
532
+ }, async ({ docId, email, index, atEnd, tab, account }) => {
533
+ const args = ['docs', 'insert-person', docId, `--email=${email}`];
534
+ if (index !== undefined) args.push(`--index=${index}`);
535
+ if (atEnd) args.push('--at-end');
536
+ if (tab) args.push(`--tab=${tab}`);
537
+ return runOrDiagnose(args, { account });
538
+ });
539
+
540
+ server.registerTool('gog_docs_insert_date_chip', {
541
+ description: 'Insert a native Google Docs date smart chip, at a character index or end-of-doc.',
542
+ annotations: { destructiveHint: true },
543
+ inputSchema: {
544
+ docId: z.string().describe('Doc ID (from the URL)'),
545
+ date: z.string().optional().describe('Date to insert as YYYY-MM-DD (default: today)'),
546
+ format: z.enum(['abbreviated', 'full', 'iso']).optional().describe('Date display format (default: abbreviated)'),
547
+ index: z.number().int().optional().describe('Character index to insert at. Omit or use atEnd for end-of-doc.'),
548
+ atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
549
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
550
+ account: accountParam,
551
+ },
552
+ }, async ({ docId, date, format, index, atEnd, tab, account }) => {
553
+ const args = ['docs', 'insert-date-chip', docId];
554
+ if (date) args.push(`--date=${date}`);
555
+ if (format) args.push(`--format=${format}`);
556
+ if (index !== undefined) args.push(`--index=${index}`);
557
+ if (atEnd) args.push('--at-end');
558
+ if (tab) args.push(`--tab=${tab}`);
559
+ return runOrDiagnose(args, { account });
560
+ });
561
+
562
+ server.registerTool('gog_docs_add_tab', {
563
+ description: 'Add a tab to a Google Doc. Tabs partition a doc into independently-addressable sections (multi-tab docs). Optionally set the title, zero-based position, parent tab (for nesting), and an emoji icon.',
564
+ inputSchema: {
565
+ docId: z.string().describe('Doc ID (from the URL)'),
566
+ title: z.string().optional().describe('User-visible tab title'),
567
+ index: z.number().int().optional().describe('Zero-based tab position within the parent'),
568
+ parentTab: z.string().optional().describe('Parent tab title or ID to nest this tab under'),
569
+ iconEmoji: z.string().optional().describe('Emoji icon for the tab'),
570
+ account: accountParam,
571
+ },
572
+ }, async ({ docId, title, index, parentTab, iconEmoji, account }) => {
573
+ const args = ['docs', 'add-tab', docId];
574
+ if (title) args.push(`--title=${title}`);
575
+ if (index !== undefined) args.push(`--index=${index}`);
576
+ if (parentTab) args.push(`--parent-tab=${parentTab}`);
577
+ if (iconEmoji) args.push(`--icon-emoji=${iconEmoji}`);
578
+ return runOrDiagnose(args, { account });
579
+ });
580
+
581
+ server.registerTool('gog_docs_rename_tab', {
582
+ description: 'Rename a tab in a Google Doc. Identify the existing tab by title or ID and give it a new title.',
583
+ inputSchema: {
584
+ docId: z.string().describe('Doc ID (from the URL)'),
585
+ tab: z.string().describe('Existing tab title or ID to rename'),
586
+ title: z.string().describe('New user-visible tab title'),
587
+ account: accountParam,
588
+ },
589
+ }, async ({ docId, tab, title, account }) => {
590
+ const args = ['docs', 'rename-tab', docId, `--tab=${tab}`, `--title=${title}`];
591
+ return runOrDiagnose(args, { account });
592
+ });
593
+
594
+ server.registerTool('gog_docs_delete_tab', {
595
+ description: 'Delete a tab (and its content) from a Google Doc. Identify the tab by title or ID. This permanently removes the tab and everything in it.',
596
+ annotations: { destructiveHint: true },
597
+ inputSchema: {
598
+ docId: z.string().describe('Doc ID (from the URL)'),
599
+ tab: z.string().describe('Existing tab title or ID to delete'),
600
+ account: accountParam,
601
+ },
602
+ }, async ({ docId, tab, account }) => {
603
+ const args = ['docs', 'delete-tab', docId, `--tab=${tab}`];
604
+ return runOrDiagnose(args, { account });
605
+ });
606
+
607
+ server.registerTool('gog_docs_clear', {
608
+ description: 'Clear all content from a Google Doc, leaving an empty document. The doc itself is preserved (same ID/URL); only its body content is removed. To delete the whole doc instead, use gog_docs_trash.',
609
+ annotations: { destructiveHint: true },
610
+ inputSchema: {
611
+ docId: z.string().describe('Doc ID (from the URL)'),
612
+ account: accountParam,
613
+ },
614
+ }, async ({ docId, account }) => {
615
+ return runOrDiagnose(['docs', 'clear', docId], { account });
616
+ });
417
617
  }
@@ -378,6 +378,36 @@ describe('gog_docs_update', () => {
378
378
  await handlers.get('gog_docs_update')!({ docId: 'abc' });
379
379
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'update', 'abc'], { account: undefined });
380
380
  });
381
+
382
+ it('includes --replace-range when provided', async () => {
383
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
384
+ const handlers = setupHandlers();
385
+ await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'new', replaceRange: '25:40' });
386
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
387
+ ['docs', 'update', 'abc', '--text=new', '--replace-range=25:40'],
388
+ { account: undefined },
389
+ );
390
+ });
391
+
392
+ it('includes --markdown when true', async () => {
393
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
394
+ const handlers = setupHandlers();
395
+ await handlers.get('gog_docs_update')!({ docId: 'abc', text: '# Heading', markdown: true });
396
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
397
+ ['docs', 'update', 'abc', '--text=# Heading', '--markdown'],
398
+ { account: undefined },
399
+ );
400
+ });
401
+
402
+ it('omits --markdown when false', async () => {
403
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
404
+ const handlers = setupHandlers();
405
+ await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'hi', markdown: false });
406
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
407
+ ['docs', 'update', 'abc', '--text=hi'],
408
+ { account: undefined },
409
+ );
410
+ });
381
411
  });
382
412
 
383
413
  // --- Dedicated comment tools ---
@@ -741,6 +771,160 @@ describe('gog_docs_page_layout', () => {
741
771
  await handlers.get('gog_docs_page_layout')!({ docId: 'd1' });
742
772
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'page-layout', 'd1'], { account: undefined });
743
773
  });
774
+
775
+ it('passes --page-size, page dimensions and all margins', async () => {
776
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
777
+ const handlers = setupHandlers();
778
+ await handlers.get('gog_docs_page_layout')!({
779
+ docId: 'd1', layout: 'pages', pageSize: 'A4', pageWidth: '8.5in', pageHeight: '11in',
780
+ marginTop: '1in', marginBottom: '1in', marginLeft: '0.75in', marginRight: '0.75in',
781
+ });
782
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
783
+ ['docs', 'page-layout', 'd1', '--layout=pages', '--page-size=A4', '--page-width=8.5in',
784
+ '--page-height=11in', '--margin-top=1in', '--margin-bottom=1in', '--margin-left=0.75in', '--margin-right=0.75in'],
785
+ { account: undefined },
786
+ );
787
+ });
788
+ });
789
+
790
+ describe('gog_docs_cell_update', () => {
791
+ it('passes required --row and --col', async () => {
792
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
793
+ const handlers = setupHandlers();
794
+ await handlers.get('gog_docs_cell_update')!({ docId: 'd1', row: 2, col: 3, content: 'hi' });
795
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
796
+ ['docs', 'cell-update', 'd1', '--row=2', '--col=3', '--content=hi'],
797
+ { account: undefined },
798
+ );
799
+ });
800
+
801
+ it('supports empty-string content, --content-file, --append, --format, --table-index and --tab', async () => {
802
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
803
+ const handlers = setupHandlers();
804
+ await handlers.get('gog_docs_cell_update')!({
805
+ docId: 'd1', row: 1, col: 1, content: '', contentFile: '/tmp/c.md', append: true,
806
+ format: 'plain', tableIndex: -1, tab: 'Tab2',
807
+ });
808
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
809
+ ['docs', 'cell-update', 'd1', '--row=1', '--col=1', '--content=', '--content-file=/tmp/c.md',
810
+ '--append', '--format=plain', '--table-index=-1', '--tab=Tab2'],
811
+ { account: undefined },
812
+ );
813
+ });
814
+
815
+ it('omits content when not provided and forwards account', async () => {
816
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
817
+ const handlers = setupHandlers();
818
+ await handlers.get('gog_docs_cell_update')!({ docId: 'd1', row: 1, col: 2, account: 'a@b.com' });
819
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
820
+ ['docs', 'cell-update', 'd1', '--row=1', '--col=2'],
821
+ { account: 'a@b.com' },
822
+ );
823
+ });
824
+ });
825
+
826
+ describe('gog_docs_cell_style', () => {
827
+ it('passes required --row and --col', async () => {
828
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
829
+ const handlers = setupHandlers();
830
+ await handlers.get('gog_docs_cell_style')!({ docId: 'd1', row: 0, col: 0 });
831
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
832
+ ['docs', 'cell-style', 'd1', '--row=0', '--col=0'],
833
+ { account: undefined },
834
+ );
835
+ });
836
+
837
+ it('passes spans, colors and text styling', async () => {
838
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
839
+ const handlers = setupHandlers();
840
+ await handlers.get('gog_docs_cell_style')!({
841
+ docId: 'd1', row: 1, col: 2, rowSpan: 2, colSpan: 3, backgroundColor: '#eee',
842
+ textColor: '#111', bold: true, italic: true, underline: true, tableIndex: 1, tab: 'T',
843
+ });
844
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
845
+ ['docs', 'cell-style', 'd1', '--row=1', '--col=2', '--row-span=2', '--col-span=3',
846
+ '--background-color=#eee', '--text-color=#111', '--bold', '--italic', '--underline',
847
+ '--table-index=1', '--tab=T'],
848
+ { account: undefined },
849
+ );
850
+ });
851
+
852
+ it('omits text styling flags when false', async () => {
853
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
854
+ const handlers = setupHandlers();
855
+ await handlers.get('gog_docs_cell_style')!({ docId: 'd1', row: 0, col: 0, bold: false, italic: false, underline: false });
856
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
857
+ ['docs', 'cell-style', 'd1', '--row=0', '--col=0'],
858
+ { account: undefined },
859
+ );
860
+ });
861
+ });
862
+
863
+ describe('gog_docs_insert_image', () => {
864
+ it('passes required --file and defaults', async () => {
865
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
866
+ const handlers = setupHandlers();
867
+ await handlers.get('gog_docs_insert_image')!({ docId: 'd1', file: '/tmp/pic.png' });
868
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
869
+ ['docs', 'insert-image', 'd1', '--file=/tmp/pic.png'],
870
+ { account: undefined },
871
+ );
872
+ });
873
+
874
+ it('passes placement, sizing, naming and restriction handling', async () => {
875
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
876
+ const handlers = setupHandlers();
877
+ await handlers.get('gog_docs_insert_image')!({
878
+ docId: 'd1', file: '/tmp/pic.png', at: '{{logo}}', width: 200, height: 100,
879
+ name: 'logo.png', parent: 'folder1', onRestricted: 'link', tab: 'T',
880
+ });
881
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
882
+ ['docs', 'insert-image', 'd1', '--file=/tmp/pic.png', '--at={{logo}}', '--width=200',
883
+ '--height=100', '--name=logo.png', '--parent=folder1', '--on-restricted=link', '--tab=T'],
884
+ { account: undefined },
885
+ );
886
+ });
887
+ });
888
+
889
+ describe('gog_docs_insert_person', () => {
890
+ it('passes required --email', async () => {
891
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
892
+ const handlers = setupHandlers();
893
+ await handlers.get('gog_docs_insert_person')!({ docId: 'd1', email: 'a@b.com' });
894
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
895
+ ['docs', 'insert-person', 'd1', '--email=a@b.com'],
896
+ { account: undefined },
897
+ );
898
+ });
899
+
900
+ it('passes --index, --at-end and --tab', async () => {
901
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
902
+ const handlers = setupHandlers();
903
+ await handlers.get('gog_docs_insert_person')!({ docId: 'd1', email: 'a@b.com', index: 0, atEnd: true, tab: 'T' });
904
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
905
+ ['docs', 'insert-person', 'd1', '--email=a@b.com', '--index=0', '--at-end', '--tab=T'],
906
+ { account: undefined },
907
+ );
908
+ });
909
+ });
910
+
911
+ describe('gog_docs_insert_date_chip', () => {
912
+ it('omits all optional flags by default', async () => {
913
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
914
+ const handlers = setupHandlers();
915
+ await handlers.get('gog_docs_insert_date_chip')!({ docId: 'd1' });
916
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'insert-date-chip', 'd1'], { account: undefined });
917
+ });
918
+
919
+ it('passes --date, --format, --index and --at-end', async () => {
920
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
921
+ const handlers = setupHandlers();
922
+ await handlers.get('gog_docs_insert_date_chip')!({ docId: 'd1', date: '2026-06-01', format: 'iso', index: 5, atEnd: true });
923
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
924
+ ['docs', 'insert-date-chip', 'd1', '--date=2026-06-01', '--format=iso', '--index=5', '--at-end'],
925
+ { account: undefined },
926
+ );
927
+ });
744
928
  });
745
929
 
746
930
  describe('gog_docs_insert_table', () => {
@@ -799,3 +983,107 @@ describe('gog_docs_comments_reopen', () => {
799
983
  );
800
984
  });
801
985
  });
986
+
987
+ // --- gog_docs_add_tab ---
988
+
989
+ describe('gog_docs_add_tab', () => {
990
+ it('calls runOrDiagnose with only docId when no flags', async () => {
991
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
992
+ const handlers = setupHandlers();
993
+ await handlers.get('gog_docs_add_tab')!({ docId: 'abc' });
994
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'add-tab', 'abc'], { account: undefined });
995
+ });
996
+
997
+ it('includes all flags when provided', async () => {
998
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
999
+ const handlers = setupHandlers();
1000
+ await handlers.get('gog_docs_add_tab')!({
1001
+ docId: 'abc',
1002
+ title: 'Notes',
1003
+ index: 2,
1004
+ parentTab: 'Intro',
1005
+ iconEmoji: '📌',
1006
+ account: 'a@b.com',
1007
+ });
1008
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1009
+ ['docs', 'add-tab', 'abc', '--title=Notes', '--index=2', '--parent-tab=Intro', '--icon-emoji=📌'],
1010
+ { account: 'a@b.com' },
1011
+ );
1012
+ });
1013
+
1014
+ it('includes --index when zero', async () => {
1015
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1016
+ const handlers = setupHandlers();
1017
+ await handlers.get('gog_docs_add_tab')!({ docId: 'abc', index: 0 });
1018
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1019
+ ['docs', 'add-tab', 'abc', '--index=0'],
1020
+ { account: undefined },
1021
+ );
1022
+ });
1023
+ });
1024
+
1025
+ // --- gog_docs_rename_tab ---
1026
+
1027
+ describe('gog_docs_rename_tab', () => {
1028
+ it('calls runOrDiagnose with tab and title', async () => {
1029
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1030
+ const handlers = setupHandlers();
1031
+ await handlers.get('gog_docs_rename_tab')!({ docId: 'abc', tab: 'Old', title: 'New' });
1032
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1033
+ ['docs', 'rename-tab', 'abc', '--tab=Old', '--title=New'],
1034
+ { account: undefined },
1035
+ );
1036
+ });
1037
+
1038
+ it('forwards account override', async () => {
1039
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1040
+ const handlers = setupHandlers();
1041
+ await handlers.get('gog_docs_rename_tab')!({ docId: 'abc', tab: 't1', title: 'New', account: 'a@b.com' });
1042
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1043
+ ['docs', 'rename-tab', 'abc', '--tab=t1', '--title=New'],
1044
+ { account: 'a@b.com' },
1045
+ );
1046
+ });
1047
+ });
1048
+
1049
+ // --- gog_docs_delete_tab ---
1050
+
1051
+ describe('gog_docs_delete_tab', () => {
1052
+ it('calls runOrDiagnose with tab', async () => {
1053
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1054
+ const handlers = setupHandlers();
1055
+ await handlers.get('gog_docs_delete_tab')!({ docId: 'abc', tab: 'Old' });
1056
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1057
+ ['docs', 'delete-tab', 'abc', '--tab=Old'],
1058
+ { account: undefined },
1059
+ );
1060
+ });
1061
+
1062
+ it('forwards account override', async () => {
1063
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1064
+ const handlers = setupHandlers();
1065
+ await handlers.get('gog_docs_delete_tab')!({ docId: 'abc', tab: 't1', account: 'a@b.com' });
1066
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1067
+ ['docs', 'delete-tab', 'abc', '--tab=t1'],
1068
+ { account: 'a@b.com' },
1069
+ );
1070
+ });
1071
+ });
1072
+
1073
+ // --- gog_docs_clear ---
1074
+
1075
+ describe('gog_docs_clear', () => {
1076
+ it('calls runOrDiagnose with docId', async () => {
1077
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1078
+ const handlers = setupHandlers();
1079
+ await handlers.get('gog_docs_clear')!({ docId: 'abc' });
1080
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'clear', 'abc'], { account: undefined });
1081
+ });
1082
+
1083
+ it('forwards account override', async () => {
1084
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1085
+ const handlers = setupHandlers();
1086
+ await handlers.get('gog_docs_clear')!({ docId: 'abc', account: 'a@b.com' });
1087
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'clear', 'abc'], { account: 'a@b.com' });
1088
+ });
1089
+ });