gogcli-mcp-docs 2.4.1 → 2.6.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.4.1"
10
+ "version": "2.6.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.4.1",
18
+ "version": "2.6.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.4.1",
4
+ "version": "2.6.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
@@ -31105,6 +31105,11 @@ var paginationParams = {
31105
31105
  page: external_exports.string().optional().describe("Page token"),
31106
31106
  all: external_exports.boolean().optional().describe("Fetch all pages")
31107
31107
  };
31108
+ function pushPaginationFlags(args, p) {
31109
+ if (p.max !== void 0) args.push(`--max=${p.max}`);
31110
+ if (p.page) args.push(`--page=${p.page}`);
31111
+ if (p.all) args.push("--all");
31112
+ }
31108
31113
  function registerRunTool(server2, options) {
31109
31114
  const { service, examples, omitAccount = false, note } = options;
31110
31115
  const baseDescription = `Run any gog ${service} subcommand not covered by the other tools. Run \`gog ${service} --help\` for the full list of subcommands, or \`gog ${service} <subcommand> --help\` for flags on a specific subcommand.`;
@@ -31313,7 +31318,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31313
31318
  );
31314
31319
 
31315
31320
  // ../gogcli-mcp/src/server.ts
31316
- var VERSION = true ? "2.4.1" : "0.0.0";
31321
+ var VERSION = true ? "2.6.0" : "0.0.0";
31317
31322
  function createServer(options) {
31318
31323
  return new McpServer({
31319
31324
  name: options?.name ?? "gogcli",
@@ -31341,13 +31346,22 @@ function registerExtraDocsTools(server2) {
31341
31346
  annotations: { destructiveHint: true },
31342
31347
  inputSchema: {
31343
31348
  docId: external_exports.string().describe("Doc ID (from the URL)"),
31344
- start: external_exports.number().describe("Start index (character position, 1-based)"),
31345
- end: external_exports.number().describe("End index (character position, exclusive)"),
31349
+ start: external_exports.number().optional().describe("Start index (character position, 1-based). Required unless `at` is set."),
31350
+ end: external_exports.number().optional().describe("End index (character position, exclusive). Required unless `at` is set."),
31351
+ at: external_exports.string().optional().describe("Anchor by literal text and delete that matched range, instead of supplying start/end indices."),
31352
+ occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31353
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31346
31354
  tabId: external_exports.string().optional().describe("Tab ID to delete content from (for multi-tab docs)"),
31347
31355
  account: accountParam
31348
31356
  }
31349
- }, async ({ docId, start, end, tabId, account }) => {
31350
- const args = ["docs", "delete", `--start=${start}`, `--end=${end}`, docId];
31357
+ }, async ({ docId, start, end, at, occurrence, matchCase, tabId, account }) => {
31358
+ const args = ["docs", "delete"];
31359
+ if (start !== void 0) args.push(`--start=${start}`);
31360
+ if (end !== void 0) args.push(`--end=${end}`);
31361
+ args.push(docId);
31362
+ if (at) args.push(`--at=${at}`);
31363
+ if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31364
+ if (matchCase) args.push("--match-case");
31351
31365
  if (tabId) args.push(`--tab-id=${tabId}`);
31352
31366
  return runOrDiagnose(args, { account });
31353
31367
  });
@@ -31418,6 +31432,9 @@ function registerExtraDocsTools(server2) {
31418
31432
  noUnderline: external_exports.boolean().optional().describe("Clear underline"),
31419
31433
  strikethrough: external_exports.boolean().optional().describe("Set strikethrough"),
31420
31434
  noStrikethrough: external_exports.boolean().optional().describe("Clear strikethrough"),
31435
+ code: external_exports.boolean().optional().describe("Apply code style (Courier New monospace + grey background)"),
31436
+ link: external_exports.string().optional().describe("Set a hyperlink on the matched text. Accepts http://, https://, mailto:, #bookmarkId, or #heading-slug."),
31437
+ noLink: external_exports.boolean().optional().describe("Clear any hyperlink on the matched text"),
31421
31438
  alignment: external_exports.enum(["left", "center", "right", "justify", "start", "end", "justified"]).optional().describe("Paragraph alignment"),
31422
31439
  lineSpacing: external_exports.number().optional().describe("Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)"),
31423
31440
  headingLevel: external_exports.number().int().optional().describe("Set paragraph named style to HEADING_1..HEADING_6 (shortcut for namedStyle=HEADING_N)"),
@@ -31443,6 +31460,9 @@ function registerExtraDocsTools(server2) {
31443
31460
  if (a.noUnderline) argv.push("--no-underline");
31444
31461
  if (a.strikethrough) argv.push("--strikethrough");
31445
31462
  if (a.noStrikethrough) argv.push("--no-strikethrough");
31463
+ if (a.code) argv.push("--code");
31464
+ if (a.link) argv.push(`--link=${a.link}`);
31465
+ if (a.noLink) argv.push("--no-link");
31446
31466
  if (a.alignment) argv.push(`--alignment=${a.alignment}`);
31447
31467
  if (a.lineSpacing !== void 0) argv.push(`--line-spacing=${a.lineSpacing}`);
31448
31468
  if (a.headingLevel !== void 0) argv.push(`--heading-level=${a.headingLevel}`);
@@ -31472,14 +31492,20 @@ function registerExtraDocsTools(server2) {
31472
31492
  content: external_exports.string().optional().describe("Text content to insert"),
31473
31493
  index: external_exports.number().optional().describe("Character index to insert at (1-based; default: 1 = start of doc). Prefer gog_docs_append when you want to add at the end."),
31474
31494
  file: external_exports.string().optional().describe("Path to a file whose content to insert"),
31495
+ at: external_exports.string().optional().describe("Anchor by literal text and insert at the start of the matched range, instead of computing an index."),
31496
+ occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31497
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31475
31498
  tabId: external_exports.string().optional().describe("Tab ID to insert into (for multi-tab docs)"),
31476
31499
  account: accountParam
31477
31500
  }
31478
- }, async ({ docId, content, index, file: file2, tabId, account }) => {
31501
+ }, async ({ docId, content, index, file: file2, at, occurrence, matchCase, tabId, account }) => {
31479
31502
  const args = ["docs", "insert", docId];
31480
31503
  if (content) args.push(content);
31481
31504
  if (index !== void 0) args.push(`--index=${index}`);
31482
31505
  if (file2) args.push(`--file=${file2}`);
31506
+ if (at) args.push(`--at=${at}`);
31507
+ if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31508
+ if (matchCase) args.push("--match-case");
31483
31509
  if (tabId) args.push(`--tab-id=${tabId}`);
31484
31510
  return runOrDiagnose(args, { account });
31485
31511
  });
@@ -31545,17 +31571,23 @@ function registerExtraDocsTools(server2) {
31545
31571
  index: external_exports.number().optional().describe("Character index to insert at"),
31546
31572
  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
31573
  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."),
31574
+ at: external_exports.string().optional().describe("Anchor by literal text and replace that matched range, instead of supplying an index or replaceRange."),
31575
+ occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31576
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31548
31577
  tabId: external_exports.string().optional().describe("Tab ID for multi-tab docs"),
31549
31578
  pageless: external_exports.boolean().optional().describe("Set document to pageless format"),
31550
31579
  account: accountParam
31551
31580
  }
31552
- }, async ({ docId, text, file: file2, index, replaceRange, markdown, tabId, pageless, account }) => {
31581
+ }, async ({ docId, text, file: file2, index, replaceRange, markdown, at, occurrence, matchCase, tabId, pageless, account }) => {
31553
31582
  const args = ["docs", "update", docId];
31554
31583
  if (text) args.push(`--text=${text}`);
31555
31584
  if (file2) args.push(`--file=${file2}`);
31556
31585
  if (index !== void 0) args.push(`--index=${index}`);
31557
31586
  if (replaceRange) args.push(`--replace-range=${replaceRange}`);
31558
31587
  if (markdown) args.push("--markdown");
31588
+ if (at) args.push(`--at=${at}`);
31589
+ if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31590
+ if (matchCase) args.push("--match-case");
31559
31591
  if (tabId) args.push(`--tab-id=${tabId}`);
31560
31592
  if (pageless) args.push("--pageless");
31561
31593
  return runOrDiagnose(args, { account });
@@ -31566,11 +31598,15 @@ function registerExtraDocsTools(server2) {
31566
31598
  inputSchema: {
31567
31599
  docId: external_exports.string().describe("Doc ID (from the URL)"),
31568
31600
  includeResolved: external_exports.boolean().optional().describe("Include resolved comments (default: false, open only)"),
31601
+ since: external_exports.string().optional().describe("Only return comments modified at or after this RFC3339 timestamp (e.g. 2026-06-01T00:00:00Z)"),
31602
+ ...paginationParams,
31569
31603
  account: accountParam
31570
31604
  }
31571
- }, async ({ docId, includeResolved, account }) => {
31605
+ }, async ({ docId, includeResolved, since, max, page, all, account }) => {
31572
31606
  const args = ["docs", "comments", "list", docId];
31573
31607
  if (includeResolved) args.push("--include-resolved");
31608
+ if (since) args.push(`--since=${since}`);
31609
+ pushPaginationFlags(args, { max, page, all });
31574
31610
  return runOrDiagnose(args, { account });
31575
31611
  });
31576
31612
  server2.registerTool("gog_docs_comments_get", {
@@ -31644,6 +31680,69 @@ function registerExtraDocsTools(server2) {
31644
31680
  }, async ({ docId, commentId, account }) => {
31645
31681
  return runOrDiagnose(["docs", "comments", "reopen", docId, commentId], { account });
31646
31682
  });
31683
+ server2.registerTool("gog_docs_comments_locate", {
31684
+ description: "Locate a comment's anchor in a Google Doc \u2014 resolves the comment's quoted text to its current Docs API index range, or reports the comment as orphaned if the quote can no longer be found (e.g. the anchored text was edited away). Read-only; useful before an index-based edit near a comment.",
31685
+ annotations: { readOnlyHint: true },
31686
+ inputSchema: {
31687
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31688
+ commentId: external_exports.string().describe("Comment ID to locate"),
31689
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive matching of the comment quote"),
31690
+ normalizeWhitespace: external_exports.boolean().optional().describe("Collapse whitespace while matching the comment quote"),
31691
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31692
+ account: accountParam
31693
+ }
31694
+ }, async ({ docId, commentId, matchCase, normalizeWhitespace, tab, account }) => {
31695
+ const args = ["docs", "comments", "locate", docId, commentId];
31696
+ if (matchCase) args.push("--match-case");
31697
+ if (normalizeWhitespace) args.push("--normalize-whitespace");
31698
+ if (tab) args.push(`--tab=${tab}`);
31699
+ return runOrDiagnose(args, { account });
31700
+ });
31701
+ server2.registerTool("gog_docs_find_range", {
31702
+ description: "Map literal text in a Google Doc to its Docs API UTF-16 index range(s). Read-only helper for computing the start/end indices that index-based tools (gog_docs_delete, gog_docs_update --replace-range) need. Returns the first match by default; use occurrence to pick a specific one or all to return every match.",
31703
+ annotations: { readOnlyHint: true },
31704
+ inputSchema: {
31705
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31706
+ text: external_exports.string().describe("Literal text to locate"),
31707
+ occurrence: external_exports.number().int().optional().describe("Return the Nth occurrence (1-based; default: first)"),
31708
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive matching"),
31709
+ normalizeWhitespace: external_exports.boolean().optional().describe("Collapse whitespace while matching"),
31710
+ all: external_exports.boolean().optional().describe("Return all matches instead of just one"),
31711
+ failEmpty: external_exports.boolean().optional().describe("Treat no matches as an error instead of returning an empty result"),
31712
+ tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31713
+ account: accountParam
31714
+ }
31715
+ }, async ({ docId, text, occurrence, matchCase, normalizeWhitespace, all, failEmpty, tab, account }) => {
31716
+ const args = ["docs", "find-range", docId, text];
31717
+ if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31718
+ if (matchCase) args.push("--match-case");
31719
+ if (normalizeWhitespace) args.push("--normalize-whitespace");
31720
+ if (all) args.push("--all");
31721
+ if (failEmpty) args.push("--fail-empty");
31722
+ if (tab) args.push(`--tab=${tab}`);
31723
+ return runOrDiagnose(args, { account });
31724
+ });
31725
+ server2.registerTool("gog_docs_table_column_width", {
31726
+ description: "Set a fixed width (in points) for a table column, or reset columns to Docs-managed even distribution. Target the table by 1-based index in document order (negative counts from the end) and the column by 1-based number. Pass evenlyDistributed without col to reset every column in the table.",
31727
+ annotations: { destructiveHint: true },
31728
+ inputSchema: {
31729
+ docId: external_exports.string().describe("Doc ID (from the URL)"),
31730
+ col: external_exports.number().int().optional().describe("1-based column number. Omit with evenlyDistributed to reset all columns."),
31731
+ width: external_exports.number().optional().describe("Fixed column width in points (minimum 5pt). Mutually exclusive with evenlyDistributed."),
31732
+ evenlyDistributed: external_exports.boolean().optional().describe("Reset the selected column (or all columns when col is omitted) to Docs-managed equal width."),
31733
+ tableIndex: external_exports.number().int().optional().describe("1-based table index in document order; negative counts from the end (default: 1)"),
31734
+ tab: external_exports.string().optional().describe("Target tab title or ID"),
31735
+ account: accountParam
31736
+ }
31737
+ }, async ({ docId, col, width, evenlyDistributed, tableIndex, tab, account }) => {
31738
+ const args = ["docs", "table-column-width", docId];
31739
+ if (col !== void 0) args.push(`--col=${col}`);
31740
+ if (width !== void 0) args.push(`--width=${width}`);
31741
+ if (evenlyDistributed) args.push("--evenly-distributed");
31742
+ if (tableIndex !== void 0) args.push(`--table-index=${tableIndex}`);
31743
+ if (tab) args.push(`--tab=${tab}`);
31744
+ return runOrDiagnose(args, { account });
31745
+ });
31647
31746
  server2.registerTool("gog_docs_insert_page_break", {
31648
31747
  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.",
31649
31748
  annotations: { destructiveHint: true },
@@ -31651,13 +31750,19 @@ function registerExtraDocsTools(server2) {
31651
31750
  docId: external_exports.string().describe("Doc ID (from the URL)"),
31652
31751
  index: external_exports.number().int().optional().describe("Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc."),
31653
31752
  atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
31753
+ at: external_exports.string().optional().describe("Anchor by literal text and insert the page break at the start of the matched range, instead of an index."),
31754
+ occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31755
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31654
31756
  tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31655
31757
  account: accountParam
31656
31758
  }
31657
- }, async ({ docId, index, atEnd, tab, account }) => {
31759
+ }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, account }) => {
31658
31760
  const args = ["docs", "insert-page-break", docId];
31659
31761
  if (index !== void 0) args.push(`--index=${index}`);
31660
31762
  if (atEnd) args.push("--at-end");
31763
+ if (at) args.push(`--at=${at}`);
31764
+ if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31765
+ if (matchCase) args.push("--match-case");
31661
31766
  if (tab) args.push(`--tab=${tab}`);
31662
31767
  return runOrDiagnose(args, { account });
31663
31768
  });
@@ -31799,13 +31904,19 @@ function registerExtraDocsTools(server2) {
31799
31904
  email: external_exports.string().describe("Email address for the person chip"),
31800
31905
  index: external_exports.number().int().optional().describe("Character index to insert at. Omit or use atEnd for end-of-doc."),
31801
31906
  atEnd: external_exports.boolean().optional().describe("Insert at end-of-doc/tab (mutually exclusive with index)"),
31907
+ at: external_exports.string().optional().describe("Anchor by literal text, delete the match, and insert the person chip there, instead of an index."),
31908
+ occurrence: external_exports.number().int().optional().describe("Use the Nth `at` match (1-based; required when `at` is ambiguous)"),
31909
+ matchCase: external_exports.boolean().optional().describe("Case-sensitive `at` matching"),
31802
31910
  tab: external_exports.string().optional().describe("Target a specific tab by title or ID"),
31803
31911
  account: accountParam
31804
31912
  }
31805
- }, async ({ docId, email: email3, index, atEnd, tab, account }) => {
31913
+ }, async ({ docId, email: email3, index, atEnd, at, occurrence, matchCase, tab, account }) => {
31806
31914
  const args = ["docs", "insert-person", docId, `--email=${email3}`];
31807
31915
  if (index !== void 0) args.push(`--index=${index}`);
31808
31916
  if (atEnd) args.push("--at-end");
31917
+ if (at) args.push(`--at=${at}`);
31918
+ if (occurrence !== void 0) args.push(`--occurrence=${occurrence}`);
31919
+ if (matchCase) args.push("--match-case");
31809
31920
  if (tab) args.push(`--tab=${tab}`);
31810
31921
  return runOrDiagnose(args, { account });
31811
31922
  });
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.4.1",
6
+ "version": "2.6.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",
@@ -195,6 +195,18 @@
195
195
  "name": "gog_docs_comments_reopen",
196
196
  "description": "Reopen a previously resolved comment."
197
197
  },
198
+ {
199
+ "name": "gog_docs_comments_locate",
200
+ "description": "Resolve a comment's quoted anchor to its current Docs API index range, or report it as orphaned."
201
+ },
202
+ {
203
+ "name": "gog_docs_find_range",
204
+ "description": "Map literal text to its Docs API UTF-16 index range(s) for index-based edits."
205
+ },
206
+ {
207
+ "name": "gog_docs_table_column_width",
208
+ "description": "Set a fixed table column width in points, or reset columns to Docs-managed even distribution."
209
+ },
198
210
  {
199
211
  "name": "gog_docs_insert_page_break",
200
212
  "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.4.1",
3
+ "version": "2.6.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.4.1",
10
+ "version": "2.6.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-docs",
15
- "version": "2.4.1",
15
+ "version": "2.6.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose } from '../../../gogcli-mcp/src/lib.js';
3
+ import { accountParam, runOrDiagnose, paginationParams, pushPaginationFlags } from '../../../gogcli-mcp/src/lib.js';
4
4
 
5
5
  export function registerExtraDocsTools(server: McpServer): void {
6
6
  server.registerTool('gog_docs_copy', {
@@ -22,13 +22,22 @@ export function registerExtraDocsTools(server: McpServer): void {
22
22
  annotations: { destructiveHint: true },
23
23
  inputSchema: {
24
24
  docId: z.string().describe('Doc ID (from the URL)'),
25
- start: z.number().describe('Start index (character position, 1-based)'),
26
- end: z.number().describe('End index (character position, exclusive)'),
25
+ start: z.number().optional().describe('Start index (character position, 1-based). Required unless `at` is set.'),
26
+ end: z.number().optional().describe('End index (character position, exclusive). Required unless `at` is set.'),
27
+ at: z.string().optional().describe('Anchor by literal text and delete that matched range, instead of supplying start/end indices.'),
28
+ occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
29
+ matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
27
30
  tabId: z.string().optional().describe('Tab ID to delete content from (for multi-tab docs)'),
28
31
  account: accountParam,
29
32
  },
30
- }, async ({ docId, start, end, tabId, account }) => {
31
- const args = ['docs', 'delete', `--start=${start}`, `--end=${end}`, docId];
33
+ }, async ({ docId, start, end, at, occurrence, matchCase, tabId, account }) => {
34
+ const args = ['docs', 'delete'];
35
+ if (start !== undefined) args.push(`--start=${start}`);
36
+ if (end !== undefined) args.push(`--end=${end}`);
37
+ args.push(docId);
38
+ if (at) args.push(`--at=${at}`);
39
+ if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
40
+ if (matchCase) args.push('--match-case');
32
41
  if (tabId) args.push(`--tab-id=${tabId}`);
33
42
  return runOrDiagnose(args, { account });
34
43
  });
@@ -103,6 +112,9 @@ export function registerExtraDocsTools(server: McpServer): void {
103
112
  noUnderline: z.boolean().optional().describe('Clear underline'),
104
113
  strikethrough: z.boolean().optional().describe('Set strikethrough'),
105
114
  noStrikethrough: z.boolean().optional().describe('Clear strikethrough'),
115
+ code: z.boolean().optional().describe('Apply code style (Courier New monospace + grey background)'),
116
+ link: z.string().optional().describe('Set a hyperlink on the matched text. Accepts http://, https://, mailto:, #bookmarkId, or #heading-slug.'),
117
+ noLink: z.boolean().optional().describe('Clear any hyperlink on the matched text'),
106
118
  alignment: z.enum(['left', 'center', 'right', 'justify', 'start', 'end', 'justified']).optional().describe('Paragraph alignment'),
107
119
  lineSpacing: z.number().optional().describe('Line spacing percentage (e.g. 100 for single, 150 for 1.5x, 200 for double)'),
108
120
  headingLevel: z.number().int().optional().describe('Set paragraph named style to HEADING_1..HEADING_6 (shortcut for namedStyle=HEADING_N)'),
@@ -124,6 +136,8 @@ export function registerExtraDocsTools(server: McpServer): void {
124
136
  italic?: boolean; noItalic?: boolean;
125
137
  underline?: boolean; noUnderline?: boolean;
126
138
  strikethrough?: boolean; noStrikethrough?: boolean;
139
+ code?: boolean;
140
+ link?: string; noLink?: boolean;
127
141
  alignment?: string;
128
142
  lineSpacing?: number;
129
143
  headingLevel?: number;
@@ -147,6 +161,9 @@ export function registerExtraDocsTools(server: McpServer): void {
147
161
  if (a.noUnderline) argv.push('--no-underline');
148
162
  if (a.strikethrough) argv.push('--strikethrough');
149
163
  if (a.noStrikethrough) argv.push('--no-strikethrough');
164
+ if (a.code) argv.push('--code');
165
+ if (a.link) argv.push(`--link=${a.link}`);
166
+ if (a.noLink) argv.push('--no-link');
150
167
  if (a.alignment) argv.push(`--alignment=${a.alignment}`);
151
168
  if (a.lineSpacing !== undefined) argv.push(`--line-spacing=${a.lineSpacing}`);
152
169
  if (a.headingLevel !== undefined) argv.push(`--heading-level=${a.headingLevel}`);
@@ -178,14 +195,20 @@ export function registerExtraDocsTools(server: McpServer): void {
178
195
  content: z.string().optional().describe('Text content to insert'),
179
196
  index: z.number().optional().describe('Character index to insert at (1-based; default: 1 = start of doc). Prefer gog_docs_append when you want to add at the end.'),
180
197
  file: z.string().optional().describe('Path to a file whose content to insert'),
198
+ at: z.string().optional().describe('Anchor by literal text and insert at the start of the matched range, instead of computing an index.'),
199
+ occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
200
+ matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
181
201
  tabId: z.string().optional().describe('Tab ID to insert into (for multi-tab docs)'),
182
202
  account: accountParam,
183
203
  },
184
- }, async ({ docId, content, index, file, tabId, account }) => {
204
+ }, async ({ docId, content, index, file, at, occurrence, matchCase, tabId, account }) => {
185
205
  const args = ['docs', 'insert', docId];
186
206
  if (content) args.push(content);
187
207
  if (index !== undefined) args.push(`--index=${index}`);
188
208
  if (file) args.push(`--file=${file}`);
209
+ if (at) args.push(`--at=${at}`);
210
+ if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
211
+ if (matchCase) args.push('--match-case');
189
212
  if (tabId) args.push(`--tab-id=${tabId}`);
190
213
  return runOrDiagnose(args, { account });
191
214
  });
@@ -255,17 +278,23 @@ export function registerExtraDocsTools(server: McpServer): void {
255
278
  index: z.number().optional().describe('Character index to insert at'),
256
279
  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
280
  markdown: z.boolean().optional().describe('Convert markdown in the text/file to Google Docs formatting (headings, bold, lists, etc.) instead of inserting it literally.'),
281
+ at: z.string().optional().describe('Anchor by literal text and replace that matched range, instead of supplying an index or replaceRange.'),
282
+ occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
283
+ matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
258
284
  tabId: z.string().optional().describe('Tab ID for multi-tab docs'),
259
285
  pageless: z.boolean().optional().describe('Set document to pageless format'),
260
286
  account: accountParam,
261
287
  },
262
- }, async ({ docId, text, file, index, replaceRange, markdown, tabId, pageless, account }) => {
288
+ }, async ({ docId, text, file, index, replaceRange, markdown, at, occurrence, matchCase, tabId, pageless, account }) => {
263
289
  const args = ['docs', 'update', docId];
264
290
  if (text) args.push(`--text=${text}`);
265
291
  if (file) args.push(`--file=${file}`);
266
292
  if (index !== undefined) args.push(`--index=${index}`);
267
293
  if (replaceRange) args.push(`--replace-range=${replaceRange}`);
268
294
  if (markdown) args.push('--markdown');
295
+ if (at) args.push(`--at=${at}`);
296
+ if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
297
+ if (matchCase) args.push('--match-case');
269
298
  if (tabId) args.push(`--tab-id=${tabId}`);
270
299
  if (pageless) args.push('--pageless');
271
300
  return runOrDiagnose(args, { account });
@@ -279,11 +308,15 @@ export function registerExtraDocsTools(server: McpServer): void {
279
308
  inputSchema: {
280
309
  docId: z.string().describe('Doc ID (from the URL)'),
281
310
  includeResolved: z.boolean().optional().describe('Include resolved comments (default: false, open only)'),
311
+ since: z.string().optional().describe('Only return comments modified at or after this RFC3339 timestamp (e.g. 2026-06-01T00:00:00Z)'),
312
+ ...paginationParams,
282
313
  account: accountParam,
283
314
  },
284
- }, async ({ docId, includeResolved, account }) => {
315
+ }, async ({ docId, includeResolved, since, max, page, all, account }) => {
285
316
  const args = ['docs', 'comments', 'list', docId];
286
317
  if (includeResolved) args.push('--include-resolved');
318
+ if (since) args.push(`--since=${since}`);
319
+ pushPaginationFlags(args, { max, page, all });
287
320
  return runOrDiagnose(args, { account });
288
321
  });
289
322
 
@@ -365,6 +398,72 @@ export function registerExtraDocsTools(server: McpServer): void {
365
398
  return runOrDiagnose(['docs', 'comments', 'reopen', docId, commentId], { account });
366
399
  });
367
400
 
401
+ server.registerTool('gog_docs_comments_locate', {
402
+ description: 'Locate a comment\'s anchor in a Google Doc — resolves the comment\'s quoted text to its current Docs API index range, or reports the comment as orphaned if the quote can no longer be found (e.g. the anchored text was edited away). Read-only; useful before an index-based edit near a comment.',
403
+ annotations: { readOnlyHint: true },
404
+ inputSchema: {
405
+ docId: z.string().describe('Doc ID (from the URL)'),
406
+ commentId: z.string().describe('Comment ID to locate'),
407
+ matchCase: z.boolean().optional().describe('Case-sensitive matching of the comment quote'),
408
+ normalizeWhitespace: z.boolean().optional().describe('Collapse whitespace while matching the comment quote'),
409
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
410
+ account: accountParam,
411
+ },
412
+ }, async ({ docId, commentId, matchCase, normalizeWhitespace, tab, account }) => {
413
+ const args = ['docs', 'comments', 'locate', docId, commentId];
414
+ if (matchCase) args.push('--match-case');
415
+ if (normalizeWhitespace) args.push('--normalize-whitespace');
416
+ if (tab) args.push(`--tab=${tab}`);
417
+ return runOrDiagnose(args, { account });
418
+ });
419
+
420
+ server.registerTool('gog_docs_find_range', {
421
+ description: 'Map literal text in a Google Doc to its Docs API UTF-16 index range(s). Read-only helper for computing the start/end indices that index-based tools (gog_docs_delete, gog_docs_update --replace-range) need. Returns the first match by default; use occurrence to pick a specific one or all to return every match.',
422
+ annotations: { readOnlyHint: true },
423
+ inputSchema: {
424
+ docId: z.string().describe('Doc ID (from the URL)'),
425
+ text: z.string().describe('Literal text to locate'),
426
+ occurrence: z.number().int().optional().describe('Return the Nth occurrence (1-based; default: first)'),
427
+ matchCase: z.boolean().optional().describe('Case-sensitive matching'),
428
+ normalizeWhitespace: z.boolean().optional().describe('Collapse whitespace while matching'),
429
+ all: z.boolean().optional().describe('Return all matches instead of just one'),
430
+ failEmpty: z.boolean().optional().describe('Treat no matches as an error instead of returning an empty result'),
431
+ tab: z.string().optional().describe('Target a specific tab by title or ID'),
432
+ account: accountParam,
433
+ },
434
+ }, async ({ docId, text, occurrence, matchCase, normalizeWhitespace, all, failEmpty, tab, account }) => {
435
+ const args = ['docs', 'find-range', docId, text];
436
+ if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
437
+ if (matchCase) args.push('--match-case');
438
+ if (normalizeWhitespace) args.push('--normalize-whitespace');
439
+ if (all) args.push('--all');
440
+ if (failEmpty) args.push('--fail-empty');
441
+ if (tab) args.push(`--tab=${tab}`);
442
+ return runOrDiagnose(args, { account });
443
+ });
444
+
445
+ server.registerTool('gog_docs_table_column_width', {
446
+ description: 'Set a fixed width (in points) for a table column, or reset columns to Docs-managed even distribution. Target the table by 1-based index in document order (negative counts from the end) and the column by 1-based number. Pass evenlyDistributed without col to reset every column in the table.',
447
+ annotations: { destructiveHint: true },
448
+ inputSchema: {
449
+ docId: z.string().describe('Doc ID (from the URL)'),
450
+ col: z.number().int().optional().describe('1-based column number. Omit with evenlyDistributed to reset all columns.'),
451
+ width: z.number().optional().describe('Fixed column width in points (minimum 5pt). Mutually exclusive with evenlyDistributed.'),
452
+ evenlyDistributed: z.boolean().optional().describe('Reset the selected column (or all columns when col is omitted) to Docs-managed equal width.'),
453
+ tableIndex: z.number().int().optional().describe('1-based table index in document order; negative counts from the end (default: 1)'),
454
+ tab: z.string().optional().describe('Target tab title or ID'),
455
+ account: accountParam,
456
+ },
457
+ }, async ({ docId, col, width, evenlyDistributed, tableIndex, tab, account }) => {
458
+ const args = ['docs', 'table-column-width', docId];
459
+ if (col !== undefined) args.push(`--col=${col}`);
460
+ if (width !== undefined) args.push(`--width=${width}`);
461
+ if (evenlyDistributed) args.push('--evenly-distributed');
462
+ if (tableIndex !== undefined) args.push(`--table-index=${tableIndex}`);
463
+ if (tab) args.push(`--tab=${tab}`);
464
+ return runOrDiagnose(args, { account });
465
+ });
466
+
368
467
  server.registerTool('gog_docs_insert_page_break', {
369
468
  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.',
370
469
  annotations: { destructiveHint: true },
@@ -372,13 +471,19 @@ export function registerExtraDocsTools(server: McpServer): void {
372
471
  docId: z.string().describe('Doc ID (from the URL)'),
373
472
  index: z.number().int().optional().describe('Character index to insert at (1 = beginning). Omit or use atEnd for end-of-doc.'),
374
473
  atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
474
+ at: z.string().optional().describe('Anchor by literal text and insert the page break at the start of the matched range, instead of an index.'),
475
+ occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
476
+ matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
375
477
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
376
478
  account: accountParam,
377
479
  },
378
- }, async ({ docId, index, atEnd, tab, account }) => {
480
+ }, async ({ docId, index, atEnd, at, occurrence, matchCase, tab, account }) => {
379
481
  const args = ['docs', 'insert-page-break', docId];
380
482
  if (index !== undefined) args.push(`--index=${index}`);
381
483
  if (atEnd) args.push('--at-end');
484
+ if (at) args.push(`--at=${at}`);
485
+ if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
486
+ if (matchCase) args.push('--match-case');
382
487
  if (tab) args.push(`--tab=${tab}`);
383
488
  return runOrDiagnose(args, { account });
384
489
  });
@@ -526,13 +631,19 @@ export function registerExtraDocsTools(server: McpServer): void {
526
631
  email: z.string().describe('Email address for the person chip'),
527
632
  index: z.number().int().optional().describe('Character index to insert at. Omit or use atEnd for end-of-doc.'),
528
633
  atEnd: z.boolean().optional().describe('Insert at end-of-doc/tab (mutually exclusive with index)'),
634
+ at: z.string().optional().describe('Anchor by literal text, delete the match, and insert the person chip there, instead of an index.'),
635
+ occurrence: z.number().int().optional().describe('Use the Nth `at` match (1-based; required when `at` is ambiguous)'),
636
+ matchCase: z.boolean().optional().describe('Case-sensitive `at` matching'),
529
637
  tab: z.string().optional().describe('Target a specific tab by title or ID'),
530
638
  account: accountParam,
531
639
  },
532
- }, async ({ docId, email, index, atEnd, tab, account }) => {
640
+ }, async ({ docId, email, index, atEnd, at, occurrence, matchCase, tab, account }) => {
533
641
  const args = ['docs', 'insert-person', docId, `--email=${email}`];
534
642
  if (index !== undefined) args.push(`--index=${index}`);
535
643
  if (atEnd) args.push('--at-end');
644
+ if (at) args.push(`--at=${at}`);
645
+ if (occurrence !== undefined) args.push(`--occurrence=${occurrence}`);
646
+ if (matchCase) args.push('--match-case');
536
647
  if (tab) args.push(`--tab=${tab}`);
537
648
  return runOrDiagnose(args, { account });
538
649
  });
@@ -63,6 +63,17 @@ describe('gog_docs_delete', () => {
63
63
  );
64
64
  });
65
65
 
66
+ // gog 0.23.0
67
+ it('anchors by text with --at/--occurrence/--match-case', async () => {
68
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
69
+ const handlers = setupHandlers();
70
+ await handlers.get('gog_docs_delete')!({ docId: 'abc', at: 'TODO', occurrence: 2, matchCase: true });
71
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
72
+ ['docs', 'delete', 'abc', '--at=TODO', '--occurrence=2', '--match-case'],
73
+ { account: undefined },
74
+ );
75
+ });
76
+
66
77
  it('includes --tab-id when provided', async () => {
67
78
  vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
68
79
  const handlers = setupHandlers();
@@ -179,6 +190,17 @@ describe('gog_docs_insert', () => {
179
190
  );
180
191
  });
181
192
 
193
+ // gog 0.23.0
194
+ it('anchors by text with --at/--occurrence/--match-case', async () => {
195
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
196
+ const handlers = setupHandlers();
197
+ await handlers.get('gog_docs_insert')!({ docId: 'abc', content: 'X', at: 'HERE', occurrence: 3, matchCase: true });
198
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
199
+ ['docs', 'insert', 'abc', 'X', '--at=HERE', '--occurrence=3', '--match-case'],
200
+ { account: undefined },
201
+ );
202
+ });
203
+
182
204
  it('includes --index when provided', async () => {
183
205
  vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
184
206
  const handlers = setupHandlers();
@@ -312,6 +334,17 @@ describe('gog_docs_update', () => {
312
334
  );
313
335
  });
314
336
 
337
+ // gog 0.23.0
338
+ it('anchors by text with --at/--occurrence/--match-case', async () => {
339
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
340
+ const handlers = setupHandlers();
341
+ await handlers.get('gog_docs_update')!({ docId: 'abc', text: 'New', at: 'OLD', occurrence: 1, matchCase: true });
342
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
343
+ ['docs', 'update', 'abc', '--text=New', '--at=OLD', '--occurrence=1', '--match-case'],
344
+ { account: undefined },
345
+ );
346
+ });
347
+
315
348
  it('includes --file when provided', async () => {
316
349
  vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
317
350
  const handlers = setupHandlers();
@@ -446,6 +479,94 @@ describe('gog_docs_comments_list', () => {
446
479
  { account: 'other@gmail.com' },
447
480
  );
448
481
  });
482
+
483
+ // gog 0.22.0 adds --since; pagination flags were already supported by gog.
484
+ it('includes --since and pagination flags when set', async () => {
485
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
486
+ const handlers = setupHandlers();
487
+ await handlers.get('gog_docs_comments_list')!({
488
+ docId: 'abc', includeResolved: true, since: '2026-06-01T00:00:00Z', max: 10, page: 'tok', all: true,
489
+ });
490
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
491
+ ['docs', 'comments', 'list', 'abc', '--include-resolved', '--since=2026-06-01T00:00:00Z', '--max=10', '--page=tok', '--all'],
492
+ { account: undefined },
493
+ );
494
+ });
495
+ });
496
+
497
+ describe('gog_docs_table_column_width', () => {
498
+ it('sets a fixed column width', async () => {
499
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
500
+ const handlers = setupHandlers();
501
+ await handlers.get('gog_docs_table_column_width')!({
502
+ docId: 'd1', col: 2, width: 120, tableIndex: 1, tab: 'Body',
503
+ });
504
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
505
+ ['docs', 'table-column-width', 'd1', '--col=2', '--width=120', '--table-index=1', '--tab=Body'],
506
+ { account: undefined },
507
+ );
508
+ });
509
+
510
+ it('resets all columns to even distribution', async () => {
511
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
512
+ const handlers = setupHandlers();
513
+ await handlers.get('gog_docs_table_column_width')!({ docId: 'd1', evenlyDistributed: true });
514
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
515
+ ['docs', 'table-column-width', 'd1', '--evenly-distributed'],
516
+ { account: undefined },
517
+ );
518
+ });
519
+
520
+ it('forwards account override', async () => {
521
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
522
+ const handlers = setupHandlers();
523
+ await handlers.get('gog_docs_table_column_width')!({ docId: 'd1', col: 1, width: 80, account: 'other@gmail.com' });
524
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
525
+ ['docs', 'table-column-width', 'd1', '--col=1', '--width=80'],
526
+ { account: 'other@gmail.com' },
527
+ );
528
+ });
529
+ });
530
+
531
+ // gog 0.23.0
532
+ describe('gog_docs_find_range', () => {
533
+ it('maps matched text to index ranges', async () => {
534
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
535
+ const handlers = setupHandlers();
536
+ await handlers.get('gog_docs_find_range')!({ docId: 'd1', text: 'hello' });
537
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'find-range', 'd1', 'hello'], { account: undefined });
538
+ });
539
+
540
+ it('passes all match flags', async () => {
541
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
542
+ const handlers = setupHandlers();
543
+ await handlers.get('gog_docs_find_range')!({
544
+ docId: 'd1', text: 'hello', occurrence: 2, matchCase: true, normalizeWhitespace: true, all: true, failEmpty: true, tab: 'T',
545
+ });
546
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
547
+ ['docs', 'find-range', 'd1', 'hello', '--occurrence=2', '--match-case', '--normalize-whitespace', '--all', '--fail-empty', '--tab=T'],
548
+ { account: undefined },
549
+ );
550
+ });
551
+ });
552
+
553
+ describe('gog_docs_comments_locate', () => {
554
+ it('locates a comment by id', async () => {
555
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
556
+ const handlers = setupHandlers();
557
+ await handlers.get('gog_docs_comments_locate')!({ docId: 'd1', commentId: 'c1' });
558
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['docs', 'comments', 'locate', 'd1', 'c1'], { account: undefined });
559
+ });
560
+
561
+ it('passes match flags', async () => {
562
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
563
+ const handlers = setupHandlers();
564
+ await handlers.get('gog_docs_comments_locate')!({ docId: 'd1', commentId: 'c1', matchCase: true, normalizeWhitespace: true, tab: 'T' });
565
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
566
+ ['docs', 'comments', 'locate', 'd1', 'c1', '--match-case', '--normalize-whitespace', '--tab=T'],
567
+ { account: undefined },
568
+ );
569
+ });
449
570
  });
450
571
 
451
572
  describe('gog_docs_comments_get', () => {
@@ -718,6 +839,38 @@ describe('gog_docs_format', () => {
718
839
  { account: undefined },
719
840
  );
720
841
  });
842
+
843
+ // gog 0.22.0
844
+ it('passes --code when code is true', async () => {
845
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
846
+ const handlers = setupHandlers();
847
+ await handlers.get('gog_docs_format')!({ docId: 'd1', match: 'snippet', code: true });
848
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
849
+ ['docs', 'format', 'd1', '--match=snippet', '--code'],
850
+ { account: undefined },
851
+ );
852
+ });
853
+
854
+ // gog 0.23.0
855
+ it('sets a hyperlink with --link', async () => {
856
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
857
+ const handlers = setupHandlers();
858
+ await handlers.get('gog_docs_format')!({ docId: 'd1', match: 'see docs', link: 'https://example.com' });
859
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
860
+ ['docs', 'format', 'd1', '--match=see docs', '--link=https://example.com'],
861
+ { account: undefined },
862
+ );
863
+ });
864
+
865
+ it('clears a hyperlink with --no-link', async () => {
866
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
867
+ const handlers = setupHandlers();
868
+ await handlers.get('gog_docs_format')!({ docId: 'd1', match: 'linked', noLink: true });
869
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
870
+ ['docs', 'format', 'd1', '--match=linked', '--no-link'],
871
+ { account: undefined },
872
+ );
873
+ });
721
874
  });
722
875
 
723
876
  // --- gog 0.18.0 new tools ---
@@ -733,6 +886,17 @@ describe('gog_docs_insert_page_break', () => {
733
886
  );
734
887
  });
735
888
 
889
+ // gog 0.23.0
890
+ it('anchors by text with --at/--occurrence/--match-case', async () => {
891
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
892
+ const handlers = setupHandlers();
893
+ await handlers.get('gog_docs_insert_page_break')!({ docId: 'd1', at: 'Chapter 2', occurrence: 1, matchCase: true });
894
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
895
+ ['docs', 'insert-page-break', 'd1', '--at=Chapter 2', '--occurrence=1', '--match-case'],
896
+ { account: undefined },
897
+ );
898
+ });
899
+
736
900
  it('uses --at-end when atEnd is true', async () => {
737
901
  vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
738
902
  const handlers = setupHandlers();
@@ -897,6 +1061,17 @@ describe('gog_docs_insert_person', () => {
897
1061
  );
898
1062
  });
899
1063
 
1064
+ // gog 0.23.0
1065
+ it('anchors by text with --at/--occurrence/--match-case', async () => {
1066
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
1067
+ const handlers = setupHandlers();
1068
+ await handlers.get('gog_docs_insert_person')!({ docId: 'd1', email: 'a@b.com', at: '@alice', occurrence: 1, matchCase: true });
1069
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1070
+ ['docs', 'insert-person', 'd1', '--email=a@b.com', '--at=@alice', '--occurrence=1', '--match-case'],
1071
+ { account: undefined },
1072
+ );
1073
+ });
1074
+
900
1075
  it('passes --index, --at-end and --tab', async () => {
901
1076
  vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
902
1077
  const handlers = setupHandlers();