gogcli-mcp-docs 2.6.1 → 2.7.1

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.6.1"
10
+ "version": "2.7.1"
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.6.1",
18
+ "version": "2.7.1",
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.6.1",
4
+ "version": "2.7.1",
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -31276,11 +31276,13 @@ function registerDocsTools(server2) {
31276
31276
  docId: external_exports.string().describe("Doc ID (from the URL)"),
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
+ 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."),
31279
31280
  account: accountParam
31280
31281
  }
31281
- }, async ({ docId, text, append, account }) => {
31282
+ }, async ({ docId, text, append, checkOrphans, account }) => {
31282
31283
  const args = ["docs", "write", docId, `--text=${text}`];
31283
31284
  if (append) args.push("--append");
31285
+ if (checkOrphans) args.push("--check-orphans");
31284
31286
  return runOrDiagnose(args, { account });
31285
31287
  });
31286
31288
  server2.registerTool("gog_docs_find_replace", {
@@ -31318,7 +31320,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31318
31320
  );
31319
31321
 
31320
31322
  // ../gogcli-mcp/src/server.ts
31321
- var VERSION = true ? "2.6.1" : "0.0.0";
31323
+ var VERSION = true ? "2.7.1" : "0.0.0";
31322
31324
  function createServer(options) {
31323
31325
  return new McpServer({
31324
31326
  name: options?.name ?? "gogcli",
@@ -31396,14 +31398,17 @@ function registerExtraDocsTools(server2) {
31396
31398
  inputSchema: {
31397
31399
  docId: external_exports.string().describe("Doc ID (from the URL)"),
31398
31400
  format: external_exports.enum(["text", "json"]).optional().describe("Output format (default: text)"),
31399
- tab: external_exports.string().optional().describe("Target tab title or ID (text mode only)"),
31400
- allTabs: external_exports.boolean().optional().describe("Show all tabs with headers (text mode only)"),
31401
+ tab: external_exports.string().optional().describe("Target tab title or ID. In json mode, returns that one tab in the legacy top-level Document shape."),
31402
+ allTabs: external_exports.boolean().optional().describe("Show all tabs. In json mode, returns the canonical Document response with all tab content populated."),
31401
31403
  maxBytes: external_exports.number().optional().describe("Max bytes to read in text mode (0 = unlimited; default 2000000)"),
31402
31404
  account: accountParam
31403
31405
  }
31404
31406
  }, async ({ docId, format, tab, allTabs, maxBytes, account }) => {
31405
31407
  if (format === "json") {
31406
- return runOrDiagnose(["docs", "raw", docId, "--pretty"], { account });
31408
+ const args2 = ["docs", "raw", docId, "--pretty"];
31409
+ if (tab) args2.push(`--tab=${tab}`);
31410
+ if (allTabs) args2.push("--all-tabs");
31411
+ return runOrDiagnose(args2, { account });
31407
31412
  }
31408
31413
  const args = ["docs", "cat", docId];
31409
31414
  if (tab) args.push(`--tab=${tab}`);
@@ -31743,6 +31748,230 @@ function registerExtraDocsTools(server2) {
31743
31748
  if (tab) args.push(`--tab=${tab}`);
31744
31749
  return runOrDiagnose(args, { account });
31745
31750
  });
31751
+ 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)");
31752
+ server2.registerTool("gog_docs_table_row_insert", {
31753
+ description: 'Insert a row into a native Google Docs table, optionally populated from a JSON string array. Inserts before the 1-based position given by `at` (negative counts from the end; "end" appends \u2014 the default).',
31754
+ inputSchema: {
31755
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31756
+ table: tableSelectorParam,
31757
+ at: external_exports.string().optional().describe('Insert before this 1-based row, a negative index from the end, or "end" to append (default: end)'),
31758
+ valuesJson: external_exports.string().optional().describe(`JSON string array with the new row's cell values, e.g. ["Name","Qty"]`),
31759
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31760
+ account: accountParam
31761
+ }
31762
+ }, async ({ docId, table, at, valuesJson, tab, account }) => {
31763
+ const args = ["docs", "table-row", "insert", docId];
31764
+ if (table) args.push(`--table=${table}`);
31765
+ if (at) args.push(`--at=${at}`);
31766
+ if (valuesJson) args.push(`--values-json=${valuesJson}`);
31767
+ if (tab) args.push(`--tab=${tab}`);
31768
+ return runOrDiagnose(args, { account });
31769
+ });
31770
+ server2.registerTool("gog_docs_table_row_delete", {
31771
+ description: "Delete a row from a native Google Docs table by 1-based row number (negative counts from the end). The row's cell content is lost.",
31772
+ annotations: { destructiveHint: true },
31773
+ inputSchema: {
31774
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31775
+ row: external_exports.number().int().describe("1-based row number; negative indexes count from the end"),
31776
+ table: tableSelectorParam,
31777
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31778
+ account: accountParam
31779
+ }
31780
+ }, async ({ docId, row, table, tab, account }) => {
31781
+ const args = ["docs", "table-row", "delete", docId, `--row=${row}`];
31782
+ if (table) args.push(`--table=${table}`);
31783
+ if (tab) args.push(`--tab=${tab}`);
31784
+ return runOrDiagnose(args, { account });
31785
+ });
31786
+ server2.registerTool("gog_docs_table_column_insert", {
31787
+ description: 'Insert a column into a native Google Docs table. Inserts before the 1-based position given by `at` (negative counts from the end; "end" appends \u2014 the default).',
31788
+ inputSchema: {
31789
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31790
+ table: tableSelectorParam,
31791
+ at: external_exports.string().optional().describe('Insert before this 1-based column, a negative index from the end, or "end" to append (default: end)'),
31792
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31793
+ account: accountParam
31794
+ }
31795
+ }, async ({ docId, table, at, tab, account }) => {
31796
+ const args = ["docs", "table-column", "insert", docId];
31797
+ if (at) args.push(`--at=${at}`);
31798
+ if (table) args.push(`--table=${table}`);
31799
+ if (tab) args.push(`--tab=${tab}`);
31800
+ return runOrDiagnose(args, { account });
31801
+ });
31802
+ server2.registerTool("gog_docs_table_column_delete", {
31803
+ description: "Delete a column from a native Google Docs table by 1-based column number (negative counts from the end). The column's cell content is lost.",
31804
+ annotations: { destructiveHint: true },
31805
+ inputSchema: {
31806
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31807
+ col: external_exports.number().int().describe("1-based column number; negative indexes count from the end"),
31808
+ table: tableSelectorParam,
31809
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31810
+ account: accountParam
31811
+ }
31812
+ }, async ({ docId, col, table, tab, account }) => {
31813
+ const args = ["docs", "table-column", "delete", docId, `--col=${col}`];
31814
+ if (table) args.push(`--table=${table}`);
31815
+ if (tab) args.push(`--tab=${tab}`);
31816
+ return runOrDiagnose(args, { account });
31817
+ });
31818
+ server2.registerTool("gog_docs_table_merge", {
31819
+ description: "Merge a rectangular cell range in a native Google Docs table. Content of non-first cells in the range is absorbed/discarded by the merge \u2014 use gog_docs_table_unmerge to split back.",
31820
+ annotations: { destructiveHint: true },
31821
+ inputSchema: {
31822
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31823
+ range: external_exports.string().describe('1-based cell range r1,c1:r2,c2 (e.g. "1,1:2,3")'),
31824
+ table: tableSelectorParam,
31825
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31826
+ account: accountParam
31827
+ }
31828
+ }, async ({ docId, range, table, tab, account }) => {
31829
+ const args = ["docs", "table-merge", docId, `--range=${range}`];
31830
+ if (table) args.push(`--table=${table}`);
31831
+ if (tab) args.push(`--tab=${tab}`);
31832
+ return runOrDiagnose(args, { account });
31833
+ });
31834
+ server2.registerTool("gog_docs_table_unmerge", {
31835
+ description: "Unmerge (split) the merged region containing a given cell in a native Google Docs table.",
31836
+ inputSchema: {
31837
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31838
+ cell: external_exports.string().describe('1-based cell r,c inside the merged region (e.g. "1,1")'),
31839
+ table: tableSelectorParam,
31840
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31841
+ account: accountParam
31842
+ }
31843
+ }, async ({ docId, cell, table, tab, account }) => {
31844
+ const args = ["docs", "table-unmerge", docId, `--cell=${cell}`];
31845
+ if (table) args.push(`--table=${table}`);
31846
+ if (tab) args.push(`--tab=${tab}`);
31847
+ return runOrDiagnose(args, { account });
31848
+ });
31849
+ server2.registerTool("gog_docs_named_range_create", {
31850
+ description: "Create a named range \u2014 a durable, tab-aware anchor over a span of document text that survives subsequent edits (unlike raw indices). Anchor by literal text (`at`) or explicit UTF-16 start/end indices. Pair with gog_docs_named_range_replace for repeatable templated updates.",
31851
+ inputSchema: {
31852
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31853
+ name: external_exports.string().describe("Unique named range name"),
31854
+ at: external_exports.string().optional().describe("Create the range around this literal matched text, instead of start/end indices"),
31855
+ occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31856
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31857
+ start: external_exports.number().int().optional().describe("Range start UTF-16 index (inclusive). Required with end unless `at` is set."),
31858
+ end: external_exports.number().int().optional().describe("Range end UTF-16 index (exclusive). Required with start unless `at` is set."),
31859
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31860
+ account: accountParam
31861
+ }
31862
+ }, async ({ docId, name, at, occurrence, matchCase, start, end, tab, account }) => {
31863
+ const args = ["docs", "named-range", "create", docId, `--name=${name}`];
31864
+ if (at) args.push(`--at=${at}`);
31865
+ if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31866
+ if (matchCase) args.push("--match-case");
31867
+ if (start !== void 0) args.push(`--start=${start}`);
31868
+ if (end !== void 0) args.push(`--end=${end}`);
31869
+ if (tab) args.push(`--tab=${tab}`);
31870
+ return runOrDiagnose(args, { account });
31871
+ });
31872
+ server2.registerTool("gog_docs_named_range_list", {
31873
+ description: "List named ranges in a Google Doc, optionally filtered by name.",
31874
+ annotations: { readOnlyHint: true },
31875
+ inputSchema: {
31876
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31877
+ name: external_exports.string().optional().describe("Only return ranges with this name"),
31878
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31879
+ account: accountParam
31880
+ }
31881
+ }, async ({ docId, name, tab, account }) => {
31882
+ const args = ["docs", "named-range", "list", docId];
31883
+ if (name) args.push(`--name=${name}`);
31884
+ if (tab) args.push(`--tab=${tab}`);
31885
+ return runOrDiagnose(args, { account });
31886
+ });
31887
+ server2.registerTool("gog_docs_named_range_delete", {
31888
+ description: "Delete a named range (the anchor only \u2014 the underlying document text is untouched).",
31889
+ annotations: { destructiveHint: true },
31890
+ inputSchema: {
31891
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31892
+ nameOrId: external_exports.string().describe("Named range name or ID"),
31893
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31894
+ account: accountParam
31895
+ }
31896
+ }, async ({ docId, nameOrId, tab, account }) => {
31897
+ const args = ["docs", "named-range", "delete", docId, nameOrId];
31898
+ if (tab) args.push(`--tab=${tab}`);
31899
+ return runOrDiagnose(args, { account });
31900
+ });
31901
+ server2.registerTool("gog_docs_named_range_replace", {
31902
+ description: "Replace the text inside a named range with new content (inline or from a file), keeping the anchor for future updates \u2014 the templated-update workhorse.",
31903
+ annotations: { destructiveHint: true },
31904
+ inputSchema: {
31905
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31906
+ nameOrId: external_exports.string().describe("Named range name or ID"),
31907
+ text: external_exports.string().optional().describe("Replacement text"),
31908
+ file: external_exports.string().optional().describe("Path to a file with the replacement text"),
31909
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31910
+ account: accountParam
31911
+ }
31912
+ }, async ({ docId, nameOrId, text, file: file2, tab, account }) => {
31913
+ const args = ["docs", "named-range", "replace", docId, nameOrId];
31914
+ if (text !== void 0) args.push(`--text=${text}`);
31915
+ if (file2) args.push(`--file=${file2}`);
31916
+ if (tab) args.push(`--tab=${tab}`);
31917
+ return runOrDiagnose(args, { account });
31918
+ });
31919
+ server2.registerTool("gog_docs_tables_list", {
31920
+ description: "Enumerate the native tables in a Google Doc (dimensions, position) \u2014 the index/first-cell-text it reports feeds the `table` selector on the table-row/column/merge tools.",
31921
+ annotations: { readOnlyHint: true },
31922
+ inputSchema: {
31923
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31924
+ tab: external_exports.string().optional().describe("Tab title or ID (omit for default)"),
31925
+ account: accountParam
31926
+ }
31927
+ }, async ({ docId, tab, account }) => {
31928
+ const args = ["docs", "tables", "list", docId];
31929
+ if (tab) args.push(`--tab=${tab}`);
31930
+ return runOrDiagnose(args, { account });
31931
+ });
31932
+ server2.registerTool("gog_docs_images_list", {
31933
+ description: "Enumerate inline/positioned images in a Google Doc.",
31934
+ annotations: { readOnlyHint: true },
31935
+ inputSchema: {
31936
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31937
+ tab: external_exports.string().optional().describe("Tab title or ID (omit for default)"),
31938
+ account: accountParam
31939
+ }
31940
+ }, async ({ docId, tab, account }) => {
31941
+ const args = ["docs", "images", "list", docId];
31942
+ if (tab) args.push(`--tab=${tab}`);
31943
+ return runOrDiagnose(args, { account });
31944
+ });
31945
+ server2.registerTool("gog_docs_headings_list", {
31946
+ description: "Enumerate headings in a Google Doc (a lightweight outline view), optionally filtered to one heading level.",
31947
+ annotations: { readOnlyHint: true },
31948
+ inputSchema: {
31949
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31950
+ level: external_exports.number().int().optional().describe("Only return this heading level (1-6)"),
31951
+ tab: external_exports.string().optional().describe("Tab title or ID (omit for default)"),
31952
+ account: accountParam
31953
+ }
31954
+ }, async ({ docId, level, tab, account }) => {
31955
+ const args = ["docs", "headings", "list", docId];
31956
+ if (level !== void 0) args.push(`--level=${level}`);
31957
+ if (tab) args.push(`--tab=${tab}`);
31958
+ return runOrDiagnose(args, { account });
31959
+ });
31960
+ server2.registerTool("gog_docs_paragraphs_list", {
31961
+ description: "Enumerate paragraphs in a Google Doc with emptiness, text-run ranges, styles, and links \u2014 richer than gog_docs_structure when you need per-run detail. Optionally filter to one named style.",
31962
+ annotations: { readOnlyHint: true },
31963
+ inputSchema: {
31964
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31965
+ style: external_exports.string().optional().describe("Only return this named style (e.g. NORMAL_TEXT or HEADING_2)"),
31966
+ tab: external_exports.string().optional().describe("Tab title or ID (omit for default)"),
31967
+ account: accountParam
31968
+ }
31969
+ }, async ({ docId, style, tab, account }) => {
31970
+ const args = ["docs", "paragraphs", "list", docId];
31971
+ if (style) args.push(`--style=${style}`);
31972
+ if (tab) args.push(`--tab=${tab}`);
31973
+ return runOrDiagnose(args, { account });
31974
+ });
31746
31975
  server2.registerTool("gog_docs_insert_page_break", {
31747
31976
  description: "Insert a Google Docs page break via InsertPageBreakRequest \u2014 the only path for multi-page deliverables (markdown has no page-break construct). Specify `index` for a precise character position, or `atEnd` for end-of-doc.",
31748
31977
  annotations: { destructiveHint: true },
@@ -31871,11 +32100,12 @@ function registerExtraDocsTools(server2) {
31871
32100
  return runOrDiagnose(args, { account });
31872
32101
  });
31873
32102
  server2.registerTool("gog_docs_insert_image", {
31874
- 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.",
32103
+ description: "Insert an image into a Google Doc from a local file or a public HTTPS URL. file: uploaded to Drive, temporarily shared so Docs can fetch it, inserted, then the public permission is revoked. url: inserted directly with no Drive upload or temporary sharing. Replaces placeholder text (at) or appends at end-of-doc.",
31875
32104
  annotations: { destructiveHint: true },
31876
32105
  inputSchema: {
31877
32106
  docId: external_exports.string().describe("Doc ID (from the URL)"),
31878
- file: external_exports.string().describe("Local PNG, JPEG, or GIF image to upload and insert"),
32107
+ file: external_exports.string().optional().describe("Local PNG, JPEG, or GIF image to upload and insert (exactly one of file or url)"),
32108
+ url: external_exports.string().optional().describe("Public HTTPS image URL to insert directly \u2014 no Drive upload or temporary public sharing (exactly one of file or url)"),
31879
32109
  at: external_exports.string().optional().describe('Placeholder text to replace, or "end" to append (default: end)'),
31880
32110
  width: external_exports.number().optional().describe("Image width in points (default: 468)"),
31881
32111
  height: external_exports.number().optional().describe("Image height in points (optional; width-only preserves aspect ratio)"),
@@ -31885,8 +32115,10 @@ function registerExtraDocsTools(server2) {
31885
32115
  tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31886
32116
  account: accountParam
31887
32117
  }
31888
- }, async ({ docId, file: file2, at, width, height, name, parent, onRestricted, tab, account }) => {
31889
- const args = ["docs", "insert-image", docId, `--file=${file2}`];
32118
+ }, async ({ docId, file: file2, url: url2, at, width, height, name, parent, onRestricted, tab, account }) => {
32119
+ const args = ["docs", "insert-image", docId];
32120
+ if (file2) args.push(`--file=${file2}`);
32121
+ if (url2) args.push(`--url=${url2}`);
31890
32122
  if (at) args.push(`--at=${at}`);
31891
32123
  if (width !== void 0) args.push(`--width=${width}`);
31892
32124
  if (height !== void 0) args.push(`--height=${height}`);
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.6.1",
6
+ "version": "2.7.1",
7
7
  "description": "Extended Google Docs for Claude via gogcli — auth + full Docs and comments support",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -207,6 +207,62 @@
207
207
  "name": "gog_docs_table_column_width",
208
208
  "description": "Set a fixed table column width in points, or reset columns to Docs-managed even distribution."
209
209
  },
210
+ {
211
+ "name": "gog_docs_table_row_insert",
212
+ "description": "Insert a table row, optionally populated from a JSON string array."
213
+ },
214
+ {
215
+ "name": "gog_docs_table_row_delete",
216
+ "description": "Delete a table row by 1-based number."
217
+ },
218
+ {
219
+ "name": "gog_docs_table_column_insert",
220
+ "description": "Insert a table column before a 1-based position or at the end."
221
+ },
222
+ {
223
+ "name": "gog_docs_table_column_delete",
224
+ "description": "Delete a table column by 1-based number."
225
+ },
226
+ {
227
+ "name": "gog_docs_table_merge",
228
+ "description": "Merge a rectangular cell range in a table."
229
+ },
230
+ {
231
+ "name": "gog_docs_table_unmerge",
232
+ "description": "Unmerge (split) the merged region containing a cell."
233
+ },
234
+ {
235
+ "name": "gog_docs_named_range_create",
236
+ "description": "Create a named range — a durable anchor over document text, by matched text or indices."
237
+ },
238
+ {
239
+ "name": "gog_docs_named_range_list",
240
+ "description": "List named ranges, optionally filtered by name."
241
+ },
242
+ {
243
+ "name": "gog_docs_named_range_delete",
244
+ "description": "Delete a named range anchor (the text is untouched)."
245
+ },
246
+ {
247
+ "name": "gog_docs_named_range_replace",
248
+ "description": "Replace the text inside a named range, keeping the anchor for future updates."
249
+ },
250
+ {
251
+ "name": "gog_docs_tables_list",
252
+ "description": "Enumerate native tables (dimensions, position) — feeds the table selector on the table tools."
253
+ },
254
+ {
255
+ "name": "gog_docs_images_list",
256
+ "description": "Enumerate inline/positioned images."
257
+ },
258
+ {
259
+ "name": "gog_docs_headings_list",
260
+ "description": "Enumerate headings (outline view), optionally one level."
261
+ },
262
+ {
263
+ "name": "gog_docs_paragraphs_list",
264
+ "description": "Enumerate paragraphs with text-run ranges, styles, and links."
265
+ },
210
266
  {
211
267
  "name": "gog_docs_insert_page_break",
212
268
  "description": "Insert a Google Docs page break at a given index or at end-of-doc — the only path for multi-page deliverables."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-docs",
3
- "version": "2.6.1",
3
+ "version": "2.7.1",
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.6.1",
10
+ "version": "2.7.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-docs",
15
- "version": "2.6.1",
15
+ "version": "2.7.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -75,14 +75,17 @@ export function registerExtraDocsTools(server: McpServer): void {
75
75
  inputSchema: {
76
76
  docId: z.string().describe('Doc ID (from the URL)'),
77
77
  format: z.enum(['text', 'json']).optional().describe('Output format (default: text)'),
78
- tab: z.string().optional().describe('Target tab title or ID (text mode only)'),
79
- allTabs: z.boolean().optional().describe('Show all tabs with headers (text mode only)'),
78
+ tab: z.string().optional().describe('Target tab title or ID. In json mode, returns that one tab in the legacy top-level Document shape.'),
79
+ allTabs: z.boolean().optional().describe('Show all tabs. In json mode, returns the canonical Document response with all tab content populated.'),
80
80
  maxBytes: z.number().optional().describe('Max bytes to read in text mode (0 = unlimited; default 2000000)'),
81
81
  account: accountParam,
82
82
  },
83
83
  }, async ({ docId, format, tab, allTabs, maxBytes, account }) => {
84
84
  if (format === 'json') {
85
- return runOrDiagnose(['docs', 'raw', docId, '--pretty'], { account });
85
+ const args = ['docs', 'raw', docId, '--pretty'];
86
+ if (tab) args.push(`--tab=${tab}`);
87
+ if (allTabs) args.push('--all-tabs');
88
+ return runOrDiagnose(args, { account });
86
89
  }
87
90
  const args = ['docs', 'cat', docId];
88
91
  if (tab) args.push(`--tab=${tab}`);
@@ -464,6 +467,247 @@ export function registerExtraDocsTools(server: McpServer): void {
464
467
  return runOrDiagnose(args, { account });
465
468
  });
466
469
 
470
+ // Shared describe for the --table selector used by the table-row/column/merge
471
+ // tools (a richer addressing scheme than the older tableIndex param).
472
+ const tableSelectorParam = z.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)');
473
+
474
+ server.registerTool('gog_docs_table_row_insert', {
475
+ description: 'Insert a row into a native Google Docs table, optionally populated from a JSON string array. Inserts before the 1-based position given by `at` (negative counts from the end; "end" appends — the default).',
476
+ inputSchema: {
477
+ docId: z.string().describe('Doc ID (from the URL)'),
478
+ table: tableSelectorParam,
479
+ at: z.string().optional().describe('Insert before this 1-based row, a negative index from the end, or "end" to append (default: end)'),
480
+ valuesJson: z.string().optional().describe('JSON string array with the new row\'s cell values, e.g. ["Name","Qty"]'),
481
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
482
+ account: accountParam,
483
+ },
484
+ }, async ({ docId, table, at, valuesJson, tab, account }) => {
485
+ const args = ['docs', 'table-row', 'insert', docId];
486
+ if (table) args.push(`--table=${table}`);
487
+ if (at) args.push(`--at=${at}`);
488
+ if (valuesJson) args.push(`--values-json=${valuesJson}`);
489
+ if (tab) args.push(`--tab=${tab}`);
490
+ return runOrDiagnose(args, { account });
491
+ });
492
+
493
+ server.registerTool('gog_docs_table_row_delete', {
494
+ description: 'Delete a row from a native Google Docs table by 1-based row number (negative counts from the end). The row\'s cell content is lost.',
495
+ annotations: { destructiveHint: true },
496
+ inputSchema: {
497
+ docId: z.string().describe('Doc ID (from the URL)'),
498
+ row: z.number().int().describe('1-based row number; negative indexes count from the end'),
499
+ table: tableSelectorParam,
500
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
501
+ account: accountParam,
502
+ },
503
+ }, async ({ docId, row, table, tab, account }) => {
504
+ const args = ['docs', 'table-row', 'delete', docId, `--row=${row}`];
505
+ if (table) args.push(`--table=${table}`);
506
+ if (tab) args.push(`--tab=${tab}`);
507
+ return runOrDiagnose(args, { account });
508
+ });
509
+
510
+ server.registerTool('gog_docs_table_column_insert', {
511
+ description: 'Insert a column into a native Google Docs table. Inserts before the 1-based position given by `at` (negative counts from the end; "end" appends — the default).',
512
+ inputSchema: {
513
+ docId: z.string().describe('Doc ID (from the URL)'),
514
+ table: tableSelectorParam,
515
+ at: z.string().optional().describe('Insert before this 1-based column, a negative index from the end, or "end" to append (default: end)'),
516
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
517
+ account: accountParam,
518
+ },
519
+ }, async ({ docId, table, at, tab, account }) => {
520
+ const args = ['docs', 'table-column', 'insert', docId];
521
+ if (at) args.push(`--at=${at}`);
522
+ if (table) args.push(`--table=${table}`);
523
+ if (tab) args.push(`--tab=${tab}`);
524
+ return runOrDiagnose(args, { account });
525
+ });
526
+
527
+ server.registerTool('gog_docs_table_column_delete', {
528
+ description: 'Delete a column from a native Google Docs table by 1-based column number (negative counts from the end). The column\'s cell content is lost.',
529
+ annotations: { destructiveHint: true },
530
+ inputSchema: {
531
+ docId: z.string().describe('Doc ID (from the URL)'),
532
+ col: z.number().int().describe('1-based column number; negative indexes count from the end'),
533
+ table: tableSelectorParam,
534
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
535
+ account: accountParam,
536
+ },
537
+ }, async ({ docId, col, table, tab, account }) => {
538
+ const args = ['docs', 'table-column', 'delete', docId, `--col=${col}`];
539
+ if (table) args.push(`--table=${table}`);
540
+ if (tab) args.push(`--tab=${tab}`);
541
+ return runOrDiagnose(args, { account });
542
+ });
543
+
544
+ server.registerTool('gog_docs_table_merge', {
545
+ description: 'Merge a rectangular cell range in a native Google Docs table. Content of non-first cells in the range is absorbed/discarded by the merge — use gog_docs_table_unmerge to split back.',
546
+ annotations: { destructiveHint: true },
547
+ inputSchema: {
548
+ docId: z.string().describe('Doc ID (from the URL)'),
549
+ range: z.string().describe('1-based cell range r1,c1:r2,c2 (e.g. "1,1:2,3")'),
550
+ table: tableSelectorParam,
551
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
552
+ account: accountParam,
553
+ },
554
+ }, async ({ docId, range, table, tab, account }) => {
555
+ const args = ['docs', 'table-merge', docId, `--range=${range}`];
556
+ if (table) args.push(`--table=${table}`);
557
+ if (tab) args.push(`--tab=${tab}`);
558
+ return runOrDiagnose(args, { account });
559
+ });
560
+
561
+ server.registerTool('gog_docs_table_unmerge', {
562
+ description: 'Unmerge (split) the merged region containing a given cell in a native Google Docs table.',
563
+ inputSchema: {
564
+ docId: z.string().describe('Doc ID (from the URL)'),
565
+ cell: z.string().describe('1-based cell r,c inside the merged region (e.g. "1,1")'),
566
+ table: tableSelectorParam,
567
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
568
+ account: accountParam,
569
+ },
570
+ }, async ({ docId, cell, table, tab, account }) => {
571
+ const args = ['docs', 'table-unmerge', docId, `--cell=${cell}`];
572
+ if (table) args.push(`--table=${table}`);
573
+ if (tab) args.push(`--tab=${tab}`);
574
+ return runOrDiagnose(args, { account });
575
+ });
576
+
577
+ server.registerTool('gog_docs_named_range_create', {
578
+ description: 'Create a named range — a durable, tab-aware anchor over a span of document text that survives subsequent edits (unlike raw indices). Anchor by literal text (`at`) or explicit UTF-16 start/end indices. Pair with gog_docs_named_range_replace for repeatable templated updates.',
579
+ inputSchema: {
580
+ docId: z.string().describe('Doc ID (from the URL)'),
581
+ name: z.string().describe('Unique named range name'),
582
+ at: z.string().optional().describe('Create the range around this literal matched text, instead of start/end indices'),
583
+ occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
584
+ matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
585
+ start: z.number().int().optional().describe('Range start UTF-16 index (inclusive). Required with end unless `at` is set.'),
586
+ end: z.number().int().optional().describe('Range end UTF-16 index (exclusive). Required with start unless `at` is set.'),
587
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
588
+ account: accountParam,
589
+ },
590
+ }, async ({ docId, name, at, occurrence, matchCase, start, end, tab, account }) => {
591
+ const args = ['docs', 'named-range', 'create', docId, `--name=${name}`];
592
+ if (at) args.push(`--at=${at}`);
593
+ if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
594
+ if (matchCase) args.push('--match-case');
595
+ if (start !== undefined) args.push(`--start=${start}`);
596
+ if (end !== undefined) args.push(`--end=${end}`);
597
+ if (tab) args.push(`--tab=${tab}`);
598
+ return runOrDiagnose(args, { account });
599
+ });
600
+
601
+ server.registerTool('gog_docs_named_range_list', {
602
+ description: 'List named ranges in a Google Doc, optionally filtered by name.',
603
+ annotations: { readOnlyHint: true },
604
+ inputSchema: {
605
+ docId: z.string().describe('Doc ID (from the URL)'),
606
+ name: z.string().optional().describe('Only return ranges with this name'),
607
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
608
+ account: accountParam,
609
+ },
610
+ }, async ({ docId, name, tab, account }) => {
611
+ const args = ['docs', 'named-range', 'list', docId];
612
+ if (name) args.push(`--name=${name}`);
613
+ if (tab) args.push(`--tab=${tab}`);
614
+ return runOrDiagnose(args, { account });
615
+ });
616
+
617
+ server.registerTool('gog_docs_named_range_delete', {
618
+ description: 'Delete a named range (the anchor only — the underlying document text is untouched).',
619
+ annotations: { destructiveHint: true },
620
+ inputSchema: {
621
+ docId: z.string().describe('Doc ID (from the URL)'),
622
+ nameOrId: z.string().describe('Named range name or ID'),
623
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
624
+ account: accountParam,
625
+ },
626
+ }, async ({ docId, nameOrId, tab, account }) => {
627
+ const args = ['docs', 'named-range', 'delete', docId, nameOrId];
628
+ if (tab) args.push(`--tab=${tab}`);
629
+ return runOrDiagnose(args, { account });
630
+ });
631
+
632
+ server.registerTool('gog_docs_named_range_replace', {
633
+ description: 'Replace the text inside a named range with new content (inline or from a file), keeping the anchor for future updates — the templated-update workhorse.',
634
+ annotations: { destructiveHint: true },
635
+ inputSchema: {
636
+ docId: z.string().describe('Doc ID (from the URL)'),
637
+ nameOrId: z.string().describe('Named range name or ID'),
638
+ text: z.string().optional().describe('Replacement text'),
639
+ file: z.string().optional().describe('Path to a file with the replacement text'),
640
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
641
+ account: accountParam,
642
+ },
643
+ }, async ({ docId, nameOrId, text, file, tab, account }) => {
644
+ const args = ['docs', 'named-range', 'replace', docId, nameOrId];
645
+ if (text !== undefined) args.push(`--text=${text}`);
646
+ if (file) args.push(`--file=${file}`);
647
+ if (tab) args.push(`--tab=${tab}`);
648
+ return runOrDiagnose(args, { account });
649
+ });
650
+
651
+ server.registerTool('gog_docs_tables_list', {
652
+ description: 'Enumerate the native tables in a Google Doc (dimensions, position) — the index/first-cell-text it reports feeds the `table` selector on the table-row/column/merge tools.',
653
+ annotations: { readOnlyHint: true },
654
+ inputSchema: {
655
+ docId: z.string().describe('Doc ID (from the URL)'),
656
+ tab: z.string().optional().describe('Tab title or ID (omit for default)'),
657
+ account: accountParam,
658
+ },
659
+ }, async ({ docId, tab, account }) => {
660
+ const args = ['docs', 'tables', 'list', docId];
661
+ if (tab) args.push(`--tab=${tab}`);
662
+ return runOrDiagnose(args, { account });
663
+ });
664
+
665
+ server.registerTool('gog_docs_images_list', {
666
+ description: 'Enumerate inline/positioned images in a Google Doc.',
667
+ annotations: { readOnlyHint: true },
668
+ inputSchema: {
669
+ docId: z.string().describe('Doc ID (from the URL)'),
670
+ tab: z.string().optional().describe('Tab title or ID (omit for default)'),
671
+ account: accountParam,
672
+ },
673
+ }, async ({ docId, tab, account }) => {
674
+ const args = ['docs', 'images', 'list', docId];
675
+ if (tab) args.push(`--tab=${tab}`);
676
+ return runOrDiagnose(args, { account });
677
+ });
678
+
679
+ server.registerTool('gog_docs_headings_list', {
680
+ description: 'Enumerate headings in a Google Doc (a lightweight outline view), optionally filtered to one heading level.',
681
+ annotations: { readOnlyHint: true },
682
+ inputSchema: {
683
+ docId: z.string().describe('Doc ID (from the URL)'),
684
+ level: z.number().int().optional().describe('Only return this heading level (1-6)'),
685
+ tab: z.string().optional().describe('Tab title or ID (omit for default)'),
686
+ account: accountParam,
687
+ },
688
+ }, async ({ docId, level, tab, account }) => {
689
+ const args = ['docs', 'headings', 'list', docId];
690
+ if (level !== undefined) args.push(`--level=${level}`);
691
+ if (tab) args.push(`--tab=${tab}`);
692
+ return runOrDiagnose(args, { account });
693
+ });
694
+
695
+ server.registerTool('gog_docs_paragraphs_list', {
696
+ description: 'Enumerate paragraphs in a Google Doc with emptiness, text-run ranges, styles, and links — richer than gog_docs_structure when you need per-run detail. Optionally filter to one named style.',
697
+ annotations: { readOnlyHint: true },
698
+ inputSchema: {
699
+ docId: z.string().describe('Doc ID (from the URL)'),
700
+ style: z.string().optional().describe('Only return this named style (e.g. NORMAL_TEXT or HEADING_2)'),
701
+ tab: z.string().optional().describe('Tab title or ID (omit for default)'),
702
+ account: accountParam,
703
+ },
704
+ }, async ({ docId, style, tab, account }) => {
705
+ const args = ['docs', 'paragraphs', 'list', docId];
706
+ if (style) args.push(`--style=${style}`);
707
+ if (tab) args.push(`--tab=${tab}`);
708
+ return runOrDiagnose(args, { account });
709
+ });
710
+
467
711
  server.registerTool('gog_docs_insert_page_break', {
468
712
  description: 'Insert a Google Docs page break via InsertPageBreakRequest — the only path for multi-page deliverables (markdown has no page-break construct). Specify `index` for a precise character position, or `atEnd` for end-of-doc.',
469
713
  annotations: { destructiveHint: true },
@@ -597,11 +841,12 @@ export function registerExtraDocsTools(server: McpServer): void {
597
841
  });
598
842
 
599
843
  server.registerTool('gog_docs_insert_image', {
600
- 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.',
844
+ description: 'Insert an image into a Google Doc from a local file or a public HTTPS URL. file: uploaded to Drive, temporarily shared so Docs can fetch it, inserted, then the public permission is revoked. url: inserted directly with no Drive upload or temporary sharing. Replaces placeholder text (at) or appends at end-of-doc.',
601
845
  annotations: { destructiveHint: true },
602
846
  inputSchema: {
603
847
  docId: z.string().describe('Doc ID (from the URL)'),
604
- file: z.string().describe('Local PNG, JPEG, or GIF image to upload and insert'),
848
+ file: z.string().optional().describe('Local PNG, JPEG, or GIF image to upload and insert (exactly one of file or url)'),
849
+ url: z.string().optional().describe('Public HTTPS image URL to insert directly — no Drive upload or temporary public sharing (exactly one of file or url)'),
605
850
  at: z.string().optional().describe('Placeholder text to replace, or "end" to append (default: end)'),
606
851
  width: z.number().optional().describe('Image width in points (default: 468)'),
607
852
  height: z.number().optional().describe('Image height in points (optional; width-only preserves aspect ratio)'),
@@ -611,8 +856,10 @@ export function registerExtraDocsTools(server: McpServer): void {
611
856
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
612
857
  account: accountParam,
613
858
  },
614
- }, async ({ docId, file, at, width, height, name, parent, onRestricted, tab, account }) => {
615
- const args = ['docs', 'insert-image', docId, `--file=${file}`];
859
+ }, async ({ docId, file, url, at, width, height, name, parent, onRestricted, tab, account }) => {
860
+ const args = ['docs', 'insert-image', docId];
861
+ if (file) args.push(`--file=${file}`);
862
+ if (url) args.push(`--url=${url}`);
616
863
  if (at) args.push(`--at=${at}`);
617
864
  if (width !== undefined) args.push(`--width=${width}`);
618
865
  if (height !== undefined) args.push(`--height=${height}`);
@@ -1262,3 +1262,223 @@ describe('gog_docs_clear', () => {
1262
1262
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'clear', 'abc'], { account: 'a@b.com' });
1263
1263
  });
1264
1264
  });
1265
+
1266
+ // --- gog 0.24.0 ---
1267
+
1268
+ describe('gog_docs_table_row_insert', () => {
1269
+ it('appends a row with values to a selected table', async () => {
1270
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1271
+ const handlers = setupHandlers();
1272
+ await handlers.get('gog_docs_table_row_insert')!({
1273
+ docId: 'd1', table: '2', at: 'end', valuesJson: '["A","B"]', tab: 'T',
1274
+ });
1275
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1276
+ ['docs', 'table-row', 'insert', 'd1', '--table=2', '--at=end', '--values-json=["A","B"]', '--tab=T'],
1277
+ { account: undefined },
1278
+ );
1279
+ });
1280
+
1281
+ it('works with no optional flags', async () => {
1282
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1283
+ const handlers = setupHandlers();
1284
+ await handlers.get('gog_docs_table_row_insert')!({ docId: 'd1' });
1285
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'table-row', 'insert', 'd1'], { account: undefined });
1286
+ });
1287
+ });
1288
+
1289
+ describe('gog_docs_table_row_delete', () => {
1290
+ it('deletes a row by 1-based number', async () => {
1291
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1292
+ const handlers = setupHandlers();
1293
+ await handlers.get('gog_docs_table_row_delete')!({ docId: 'd1', row: -1, table: 'Budget' });
1294
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1295
+ ['docs', 'table-row', 'delete', 'd1', '--row=-1', '--table=Budget'],
1296
+ { account: undefined },
1297
+ );
1298
+ });
1299
+ });
1300
+
1301
+ describe('gog_docs_table_column_insert', () => {
1302
+ it('inserts a column before a 1-based index', async () => {
1303
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1304
+ const handlers = setupHandlers();
1305
+ await handlers.get('gog_docs_table_column_insert')!({ docId: 'd1', at: '3', table: '*', tab: 'T' });
1306
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1307
+ ['docs', 'table-column', 'insert', 'd1', '--at=3', '--table=*', '--tab=T'],
1308
+ { account: undefined },
1309
+ );
1310
+ });
1311
+ });
1312
+
1313
+ describe('gog_docs_table_column_delete', () => {
1314
+ it('deletes a column by 1-based number', async () => {
1315
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1316
+ const handlers = setupHandlers();
1317
+ await handlers.get('gog_docs_table_column_delete')!({ docId: 'd1', col: 2 });
1318
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1319
+ ['docs', 'table-column', 'delete', 'd1', '--col=2'],
1320
+ { account: undefined },
1321
+ );
1322
+ });
1323
+ });
1324
+
1325
+ describe('gog_docs_table_merge', () => {
1326
+ it('merges a 1-based cell range', async () => {
1327
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1328
+ const handlers = setupHandlers();
1329
+ await handlers.get('gog_docs_table_merge')!({ docId: 'd1', range: '1,1:2,3', table: '1' });
1330
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1331
+ ['docs', 'table-merge', 'd1', '--range=1,1:2,3', '--table=1'],
1332
+ { account: undefined },
1333
+ );
1334
+ });
1335
+ });
1336
+
1337
+ describe('gog_docs_table_unmerge', () => {
1338
+ it('unmerges the region containing a cell', async () => {
1339
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1340
+ const handlers = setupHandlers();
1341
+ await handlers.get('gog_docs_table_unmerge')!({ docId: 'd1', cell: '1,1', tab: 'T' });
1342
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1343
+ ['docs', 'table-unmerge', 'd1', '--cell=1,1', '--tab=T'],
1344
+ { account: undefined },
1345
+ );
1346
+ });
1347
+ });
1348
+
1349
+ describe('gog_docs_named_range_create', () => {
1350
+ it('creates a range around matched text', async () => {
1351
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1352
+ const handlers = setupHandlers();
1353
+ await handlers.get('gog_docs_named_range_create')!({
1354
+ docId: 'd1', name: 'intro', at: 'Introduction', occurrence: 1, matchCase: true,
1355
+ });
1356
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1357
+ ['docs', 'named-range', 'create', 'd1', '--name=intro', '--at=Introduction', '--occurrence=1', '--match-case'],
1358
+ { account: undefined },
1359
+ );
1360
+ });
1361
+
1362
+ it('creates a range from explicit indices', async () => {
1363
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1364
+ const handlers = setupHandlers();
1365
+ await handlers.get('gog_docs_named_range_create')!({ docId: 'd1', name: 'intro', start: 5, end: 20, tab: 'T' });
1366
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1367
+ ['docs', 'named-range', 'create', 'd1', '--name=intro', '--start=5', '--end=20', '--tab=T'],
1368
+ { account: undefined },
1369
+ );
1370
+ });
1371
+ });
1372
+
1373
+ describe('gog_docs_named_range_list', () => {
1374
+ it('lists named ranges, optionally filtered by name', async () => {
1375
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1376
+ const handlers = setupHandlers();
1377
+ await handlers.get('gog_docs_named_range_list')!({ docId: 'd1', name: 'intro' });
1378
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1379
+ ['docs', 'named-range', 'list', 'd1', '--name=intro'],
1380
+ { account: undefined },
1381
+ );
1382
+ });
1383
+ });
1384
+
1385
+ describe('gog_docs_named_range_delete', () => {
1386
+ it('deletes by name or id', async () => {
1387
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1388
+ const handlers = setupHandlers();
1389
+ await handlers.get('gog_docs_named_range_delete')!({ docId: 'd1', nameOrId: 'intro' });
1390
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1391
+ ['docs', 'named-range', 'delete', 'd1', 'intro'],
1392
+ { account: undefined },
1393
+ );
1394
+ });
1395
+ });
1396
+
1397
+ describe('gog_docs_named_range_replace', () => {
1398
+ it('replaces range content with text', async () => {
1399
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1400
+ const handlers = setupHandlers();
1401
+ await handlers.get('gog_docs_named_range_replace')!({ docId: 'd1', nameOrId: 'intro', text: 'New intro', tab: 'T' });
1402
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1403
+ ['docs', 'named-range', 'replace', 'd1', 'intro', '--text=New intro', '--tab=T'],
1404
+ { account: undefined },
1405
+ );
1406
+ });
1407
+
1408
+ it('replaces range content from a file', async () => {
1409
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1410
+ const handlers = setupHandlers();
1411
+ await handlers.get('gog_docs_named_range_replace')!({ docId: 'd1', nameOrId: 'kix.abc', file: '/tmp/intro.txt' });
1412
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1413
+ ['docs', 'named-range', 'replace', 'd1', 'kix.abc', '--file=/tmp/intro.txt'],
1414
+ { account: undefined },
1415
+ );
1416
+ });
1417
+ });
1418
+
1419
+ describe('gog_docs_tables_list', () => {
1420
+ it('enumerates tables, tab-aware', async () => {
1421
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1422
+ const handlers = setupHandlers();
1423
+ await handlers.get('gog_docs_tables_list')!({ docId: 'd1', tab: 'T' });
1424
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'tables', 'list', 'd1', '--tab=T'], { account: undefined });
1425
+ });
1426
+ });
1427
+
1428
+ describe('gog_docs_images_list', () => {
1429
+ it('enumerates images', async () => {
1430
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1431
+ const handlers = setupHandlers();
1432
+ await handlers.get('gog_docs_images_list')!({ docId: 'd1' });
1433
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'images', 'list', 'd1'], { account: undefined });
1434
+ });
1435
+ });
1436
+
1437
+ describe('gog_docs_headings_list', () => {
1438
+ it('enumerates headings with a level filter', async () => {
1439
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1440
+ const handlers = setupHandlers();
1441
+ await handlers.get('gog_docs_headings_list')!({ docId: 'd1', level: 2 });
1442
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'headings', 'list', 'd1', '--level=2'], { account: undefined });
1443
+ });
1444
+ });
1445
+
1446
+ describe('gog_docs_paragraphs_list', () => {
1447
+ it('enumerates paragraphs with a style filter', async () => {
1448
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1449
+ const handlers = setupHandlers();
1450
+ await handlers.get('gog_docs_paragraphs_list')!({ docId: 'd1', style: 'HEADING_2', tab: 'T' });
1451
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1452
+ ['docs', 'paragraphs', 'list', 'd1', '--style=HEADING_2', '--tab=T'],
1453
+ { account: undefined },
1454
+ );
1455
+ });
1456
+ });
1457
+
1458
+ describe('gog_docs_insert_image url mode', () => {
1459
+ it('inserts a public HTTPS image by url without file', async () => {
1460
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1461
+ const handlers = setupHandlers();
1462
+ await handlers.get('gog_docs_insert_image')!({ docId: 'd1', url: 'https://x.test/i.png', at: '{{logo}}' });
1463
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1464
+ ['docs', 'insert-image', 'd1', '--url=https://x.test/i.png', '--at={{logo}}'],
1465
+ { account: undefined },
1466
+ );
1467
+ });
1468
+ });
1469
+
1470
+ describe('gog_docs_read json tab targeting', () => {
1471
+ it('passes --tab through to docs raw in json mode', async () => {
1472
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1473
+ const handlers = setupHandlers();
1474
+ await handlers.get('gog_docs_read')!({ docId: 'd1', format: 'json', tab: 'T' });
1475
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'raw', 'd1', '--pretty', '--tab=T'], { account: undefined });
1476
+ });
1477
+
1478
+ it('passes --all-tabs through to docs raw in json mode', async () => {
1479
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1480
+ const handlers = setupHandlers();
1481
+ await handlers.get('gog_docs_read')!({ docId: 'd1', format: 'json', allTabs: true });
1482
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'raw', 'd1', '--pretty', '--all-tabs'], { account: undefined });
1483
+ });
1484
+ });