google-tools-mcp 1.2.11 → 2.0.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.
Files changed (97) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +555 -276
  3. package/dist/cachedToolsList.js +52 -52
  4. package/dist/googleDocsApiHelpers.js +76 -12
  5. package/dist/helpers.js +572 -252
  6. package/dist/httpAuth.js +296 -0
  7. package/dist/index.js +162 -11
  8. package/dist/logger.js +87 -87
  9. package/dist/markdown-transformer/docsToMarkdown.js +222 -21
  10. package/dist/markdown-transformer/index.js +19 -9
  11. package/dist/markdown-transformer/markdownToDocs.js +458 -28
  12. package/dist/readTracker.js +136 -17
  13. package/dist/sessionContext.js +37 -0
  14. package/dist/setup.js +187 -9
  15. package/dist/tools/calendar/getBusy.js +64 -64
  16. package/dist/tools/calendar/getEvents.js +141 -141
  17. package/dist/tools/calendar/getFree.js +225 -225
  18. package/dist/tools/calendar/index.js +19 -19
  19. package/dist/tools/calendar/listCalendars.js +38 -38
  20. package/dist/tools/calendar/listRecurringInstances.js +83 -83
  21. package/dist/tools/calendar/manageCalendar.js +121 -121
  22. package/dist/tools/calendar/manageEvent.js +258 -258
  23. package/dist/tools/calendar/moveEvent.js +66 -66
  24. package/dist/tools/docs/addTab.js +10 -12
  25. package/dist/tools/docs/appendToGoogleDoc.js +9 -4
  26. package/dist/tools/docs/deleteRange.js +9 -4
  27. package/dist/tools/docs/findAndReplace.js +9 -4
  28. package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
  29. package/dist/tools/docs/formatting/getFormatting.js +171 -171
  30. package/dist/tools/docs/insertImage.js +15 -2
  31. package/dist/tools/docs/insertPageBreak.js +4 -1
  32. package/dist/tools/docs/insertTable.js +4 -1
  33. package/dist/tools/docs/insertTableWithData.js +4 -1
  34. package/dist/tools/docs/modifyText.js +14 -4
  35. package/dist/tools/docs/modifyText.test.js +84 -84
  36. package/dist/tools/docs/readGoogleDoc.js +71 -11
  37. package/dist/tools/docs/renameTab.js +12 -14
  38. package/dist/tools/drafts.js +165 -165
  39. package/dist/tools/drive/createDocument.js +13 -1
  40. package/dist/tools/drive/downloadFile.js +268 -268
  41. package/dist/tools/drive/getFileInfo.js +48 -48
  42. package/dist/tools/drive/getFilePath.js +50 -50
  43. package/dist/tools/drive/listDriveFiles.js +112 -112
  44. package/dist/tools/drive/listSharedDrives.js +52 -52
  45. package/dist/tools/drive/listSharedWithMe.js +97 -97
  46. package/dist/tools/drive/uploadFile.js +111 -111
  47. package/dist/tools/extras/index.js +9 -9
  48. package/dist/tools/extras/readDriveFile.js +212 -206
  49. package/dist/tools/extras/readFile.js +84 -84
  50. package/dist/tools/extras/searchFileContents.js +81 -81
  51. package/dist/tools/forms/batchUpdateForm.js +81 -81
  52. package/dist/tools/forms/createForm.js +54 -54
  53. package/dist/tools/forms/getForm.js +118 -118
  54. package/dist/tools/forms/getFormResponse.js +45 -45
  55. package/dist/tools/forms/index.js +15 -15
  56. package/dist/tools/forms/listFormResponses.js +56 -56
  57. package/dist/tools/forms/setPublishSettings.js +59 -59
  58. package/dist/tools/gmail/drafts.js +165 -165
  59. package/dist/tools/gmail/labels.js +54 -83
  60. package/dist/tools/gmail/messages.js +444 -441
  61. package/dist/tools/gmail/settings.js +358 -528
  62. package/dist/tools/gmail/threads.js +286 -164
  63. package/dist/tools/index.js +511 -473
  64. package/dist/tools/labels.js +103 -103
  65. package/dist/tools/legacyAliases.js +426 -0
  66. package/dist/tools/maps/directions.js +72 -0
  67. package/dist/tools/maps/geocode.js +16 -0
  68. package/dist/tools/maps/index.js +15 -0
  69. package/dist/tools/maps/mapsClient.js +83 -0
  70. package/dist/tools/maps/placeDetails.js +16 -0
  71. package/dist/tools/maps/reverseGeocode.js +19 -0
  72. package/dist/tools/maps/searchNearby.js +54 -0
  73. package/dist/tools/maps/searchPlaces.js +24 -0
  74. package/dist/tools/messages.js +448 -448
  75. package/dist/tools/settings.js +528 -528
  76. package/dist/tools/slides/createPresentation.js +104 -104
  77. package/dist/tools/slides/createShape.js +92 -92
  78. package/dist/tools/slides/createTextBox.js +82 -82
  79. package/dist/tools/slides/deleteSlide.js +30 -30
  80. package/dist/tools/slides/duplicateSlide.js +37 -37
  81. package/dist/tools/slides/exportThumbnail.js +42 -42
  82. package/dist/tools/slides/formatParagraph.js +72 -72
  83. package/dist/tools/slides/formatText.js +84 -84
  84. package/dist/tools/slides/getPresentation.js +87 -87
  85. package/dist/tools/slides/index.js +33 -33
  86. package/dist/tools/slides/reorderSlides.js +41 -41
  87. package/dist/tools/slides/replaceAllText.js +46 -46
  88. package/dist/tools/slides/setBackground.js +58 -58
  89. package/dist/tools/slides/speakerNotes.js +102 -102
  90. package/dist/tools/slides/styleShape.js +111 -111
  91. package/dist/tools/slides/updatePresentation.js +128 -128
  92. package/dist/tools/threads.js +145 -145
  93. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +31 -7
  94. package/dist/tools/utils/replaceDocumentWithMarkdown.js +72 -9
  95. package/dist/updateCheck.js +202 -0
  96. package/dist/workspace.js +122 -0
  97. package/package.json +82 -80
@@ -4,6 +4,134 @@
4
4
  * When these are detected on a text run, we render backtick code in markdown.
5
5
  */
6
6
  const CODE_FONT_FAMILIES = new Set(['Roboto Mono', 'Courier New', 'Consolas', 'monospace']);
7
+ /**
8
+ * Inspects the body content that replaceDocumentWithMarkdown will delete and
9
+ * re-insert, and reports anything docsJsonToMarkdown cannot represent at all.
10
+ * Returns an array of human-readable warning strings (empty if the content
11
+ * converts losslessly). Call before replaceDocumentWithMarkdown to warn the AI
12
+ * about what a body replacement will permanently lose.
13
+ *
14
+ * Checked: inline/positioned images and footnote references, neither of which
15
+ * has any markdown representation in either direction of this converter; a
16
+ * generated table of contents; and, generically, every OTHER Docs API
17
+ * `ParagraphElement` variant that `extractFormattedText()` does not render.
18
+ *
19
+ * `extractFormattedText()` only ever renders `textRun` (see below). The Docs
20
+ * API `ParagraphElement` union also defines `autoText`, `pageBreak`,
21
+ * `columnBreak`, `horizontalRule`, `equation`, `person`, `richLink`, and
22
+ * `dateElement` (https://developers.google.com/workspace/docs/api/reference/rest/v1/documents#ParagraphElement).
23
+ * None of those has a markdown representation, so a body replacement deletes
24
+ * them permanently and the current markdown output never mentions they were
25
+ * there. Rather than enumerate that list (and silently miss whatever variant
26
+ * Google adds next), this scanner is deny-by-default: any paragraph-element
27
+ * key other than `startIndex`/`endIndex` (position metadata) and the three
28
+ * keys explicitly handled elsewhere in this function (`textRun`, rendered;
29
+ * `inlineObjectElement`/`footnoteReference`, already counted above) is
30
+ * treated as an unhandled content variant and reported by name.
31
+ *
32
+ * NOT checked (intentionally): custom text/highlight colors and non-default
33
+ * paragraph alignment. Those round-trip losslessly through the rich-markdown
34
+ * HTML extensions this converter emits by default (`<span style="color:...">`,
35
+ * `<p align="...">`, table alignment markers) and their inverse parsing in
36
+ * markdownToDocs.js, so warning about them here would be inaccurate.
37
+ *
38
+ * IMPORTANT — accuracy: every warning is derived from `bodyContent` itself,
39
+ * i.e. the exact body (a specific tab's body in tab mode, the document body
40
+ * otherwise) that the replacement mutates. This deliberately does NOT inspect
41
+ * document-level `headers`/`footers`, which are separate document segments a
42
+ * body-content replacement does not delete, nor the global `inlineObjects`/
43
+ * `footnotes` maps, which can include content belonging to OTHER tabs that a
44
+ * scoped replacement never touches.
45
+ *
46
+ * @param {Array} bodyContent structural elements of the body being replaced
47
+ * (e.g. `contentSource.body.content`).
48
+ * @returns {string[]} warnings
49
+ */
50
+ // ParagraphElement keys this converter already accounts for: startIndex/
51
+ // endIndex are position metadata (not content); textRun is rendered by
52
+ // extractFormattedText(); inlineObjectElement/footnoteReference are counted
53
+ // as their own dedicated warnings below. Any other key on a ParagraphElement
54
+ // is, by the Docs API's own union contract, an unhandled content-bearing
55
+ // variant (autoText, pageBreak, columnBreak, horizontalRule, equation,
56
+ // person, richLink, dateElement, or a future addition) that extractFormattedText
57
+ // silently drops.
58
+ const HANDLED_PARAGRAPH_ELEMENT_KEYS = new Set([
59
+ 'startIndex',
60
+ 'endIndex',
61
+ 'textRun',
62
+ 'inlineObjectElement',
63
+ 'footnoteReference',
64
+ ]);
65
+ export function checkMarkdownFidelity(bodyContent) {
66
+ const warnings = [];
67
+ let imageCount = 0;
68
+ let footnoteCount = 0;
69
+ let tocCount = 0;
70
+ const unhandledElementCounts = {};
71
+ function scanParagraphElements(elements) {
72
+ for (const pe of elements) {
73
+ // Inline images embedded in the body flow — deleted with the body.
74
+ if (pe.inlineObjectElement) {
75
+ imageCount++;
76
+ }
77
+ // Footnote references live in the body; deleting the body removes them
78
+ // (and Docs then drops the orphaned footnote).
79
+ if (pe.footnoteReference) {
80
+ footnoteCount++;
81
+ }
82
+ // Deny-by-default: anything that isn't a key we explicitly handle is an
83
+ // unhandled ParagraphElement variant. Grouped and named below rather
84
+ // than warning per element.
85
+ for (const key of Object.keys(pe)) {
86
+ if (!HANDLED_PARAGRAPH_ELEMENT_KEYS.has(key)) {
87
+ unhandledElementCounts[key] = (unhandledElementCounts[key] ?? 0) + 1;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ function scanBodyContent(content) {
93
+ for (const element of content) {
94
+ if (element.paragraph) {
95
+ // Positioned (floating) images anchored to this paragraph are also
96
+ // removed when the paragraph is deleted.
97
+ if (Array.isArray(element.paragraph.positionedObjectIds)) {
98
+ imageCount += element.paragraph.positionedObjectIds.length;
99
+ }
100
+ scanParagraphElements(element.paragraph.elements ?? []);
101
+ }
102
+ else if (element.table) {
103
+ for (const row of (element.table.tableRows ?? [])) {
104
+ for (const cell of (row.tableCells ?? [])) {
105
+ scanBodyContent(cell.content ?? []);
106
+ }
107
+ }
108
+ }
109
+ // A generated table of contents sits in the body, so the replacement
110
+ // deletes it, and markdown has no way to express one, so the importer
111
+ // cannot put it back. Without this the whole round trip drops the TOC
112
+ // with nothing said about it.
113
+ else if (element.tableOfContents) {
114
+ tocCount++;
115
+ }
116
+ }
117
+ }
118
+ scanBodyContent(bodyContent ?? []);
119
+ if (imageCount > 0) {
120
+ warnings.push(`${imageCount} image(s) — will be removed`);
121
+ }
122
+ if (footnoteCount > 0) {
123
+ warnings.push(`${footnoteCount} footnote(s) — will be removed`);
124
+ }
125
+ if (tocCount > 0) {
126
+ warnings.push(`${tocCount} table(s) of contents — will be removed (markdown cannot express a generated TOC; reinsert it in Docs afterward)`);
127
+ }
128
+ const unhandledKeys = Object.keys(unhandledElementCounts).sort();
129
+ if (unhandledKeys.length > 0) {
130
+ const parts = unhandledKeys.map((key) => `${unhandledElementCounts[key]} ${key}`).join(', ');
131
+ warnings.push(`${parts} — unsupported content type(s) with no markdown representation; will be removed`);
132
+ }
133
+ return warnings;
134
+ }
7
135
  // --- Main Conversion ---
8
136
  /**
9
137
  * Converts Google Docs JSON structure to a markdown string.
@@ -15,34 +143,43 @@ const CODE_FONT_FAMILIES = new Set(['Roboto Mono', 'Courier New', 'Consolas', 'm
15
143
  * underline, links, code), ordered & unordered lists with nesting, tables,
16
144
  * and section breaks.
17
145
  */
18
- export function docsJsonToMarkdown(docData) {
146
+ export function docsJsonToMarkdown(docData, options = {}) {
19
147
  const body = docData.body;
20
148
  if (!body?.content) {
21
149
  return '';
22
150
  }
23
151
  const lists = docData.lists ?? {};
152
+ const conversionOptions = {
153
+ richMarkdown: options.plainMarkdown ? false : options.richMarkdown ?? true,
154
+ };
24
155
  let markdown = '';
25
156
  for (const element of body.content) {
26
157
  if (element.paragraph) {
27
- markdown += convertParagraph(element.paragraph, lists);
158
+ markdown += convertParagraph(element.paragraph, lists, conversionOptions);
28
159
  }
29
160
  else if (element.table) {
30
- markdown += convertTable(element.table);
161
+ markdown += convertTable(element.table, conversionOptions);
31
162
  }
32
163
  else if (element.sectionBreak) {
164
+ if (isInitialDocumentSectionBreak(element)) {
165
+ continue;
166
+ }
33
167
  markdown += '\n---\n\n';
34
168
  }
35
169
  }
36
170
  return markdown.trim();
37
171
  }
172
+ function isInitialDocumentSectionBreak(element) {
173
+ return element.endIndex === 1 && element.startIndex === undefined;
174
+ }
38
175
  // --- Paragraph Conversion ---
39
- function convertParagraph(paragraph, lists) {
176
+ function convertParagraph(paragraph, lists, options) {
40
177
  // 1. Determine paragraph type
41
178
  const headingLevel = getHeadingLevel(paragraph);
42
179
  const listInfo = getListInfo(paragraph, lists);
43
180
  // 2. Extract text content with inline formatting
44
181
  const elements = paragraph.elements ?? [];
45
- const text = extractFormattedText(elements);
182
+ const text = extractFormattedText(elements, options);
46
183
  // 3. Format based on type
47
184
  if (headingLevel && text.trim()) {
48
185
  const hashes = '#'.repeat(Math.min(headingLevel, 6));
@@ -54,7 +191,15 @@ function convertParagraph(paragraph, lists) {
54
191
  return `${indent}${marker} ${text.trim()}\n`;
55
192
  }
56
193
  if (text.trim()) {
57
- return `${text.trim()}\n\n`;
194
+ const trimmed = text.trim();
195
+ if (options.richMarkdown && isBlockquoteParagraph(paragraph)) {
196
+ return `<blockquote>${trimmed}</blockquote>\n\n`;
197
+ }
198
+ const alignment = paragraphAlignmentToHtml(paragraph.paragraphStyle?.alignment);
199
+ if (options.richMarkdown && alignment) {
200
+ return `<p align="${alignment}">${trimmed}</p>\n\n`;
201
+ }
202
+ return `${trimmed}\n\n`;
58
203
  }
59
204
  return '\n';
60
205
  }
@@ -91,16 +236,16 @@ function getListInfo(paragraph, lists) {
91
236
  return { ordered, nestingLevel };
92
237
  }
93
238
  // --- Text Run Conversion ---
94
- function extractFormattedText(elements) {
239
+ function extractFormattedText(elements, options) {
95
240
  let result = '';
96
241
  for (const element of elements) {
97
242
  if (element.textRun) {
98
- result += convertTextRun(element.textRun);
243
+ result += convertTextRun(element.textRun, options);
99
244
  }
100
245
  }
101
246
  return result;
102
247
  }
103
- function convertTextRun(textRun) {
248
+ function convertTextRun(textRun, options) {
104
249
  let text = textRun.content ?? '';
105
250
  const style = textRun.textStyle;
106
251
  if (!style)
@@ -134,20 +279,72 @@ function convertTextRun(textRun) {
134
279
  if (style.strikethrough) {
135
280
  formatted = `~~${formatted}~~`;
136
281
  }
137
- if (style.underline && !style.link) {
282
+ if (options.richMarkdown && style.underline && !style.link) {
138
283
  formatted = `<u>${formatted}</u>`;
139
284
  }
285
+ if (options.richMarkdown) {
286
+ formatted = applyRichTextStyle(formatted, style);
287
+ }
140
288
  if (style.link?.url) {
141
289
  formatted = `[${formatted}](${style.link.url})`;
142
290
  }
143
291
  return formatted + (trailingNewline ? '\n' : '');
144
292
  }
293
+ function applyRichTextStyle(text, style) {
294
+ const styles = [];
295
+ const fg = rgbColorToHex(style.foregroundColor?.color?.rgbColor);
296
+ const bg = rgbColorToHex(style.backgroundColor?.color?.rgbColor);
297
+ const fontSize = style.fontSize?.magnitude;
298
+ const fontFamily = style.weightedFontFamily?.fontFamily;
299
+ if (fg)
300
+ styles.push(`color:${fg}`);
301
+ if (bg)
302
+ styles.push(`background-color:${bg}`);
303
+ if (typeof fontSize === 'number')
304
+ styles.push(`font-size:${fontSize}pt`);
305
+ if (fontFamily && !CODE_FONT_FAMILIES.has(fontFamily))
306
+ styles.push(`font-family:${escapeHtmlAttr(fontFamily)}`);
307
+ if (styles.length === 0)
308
+ return text;
309
+ return `<span style="${styles.join(';')}">${text}</span>`;
310
+ }
145
311
  function isCodeStyled(style) {
146
312
  const fontFamily = style.weightedFontFamily?.fontFamily;
147
313
  return typeof fontFamily === 'string' && CODE_FONT_FAMILIES.has(fontFamily);
148
314
  }
315
+ function isBlockquoteParagraph(paragraph) {
316
+ const style = paragraph.paragraphStyle;
317
+ return Boolean(style?.borderLeft ||
318
+ style?.indentStart?.magnitude >= 30 ||
319
+ style?.indentFirstLine?.magnitude >= 30);
320
+ }
321
+ function paragraphAlignmentToHtml(alignment) {
322
+ switch (alignment) {
323
+ case 'CENTER':
324
+ return 'center';
325
+ case 'END':
326
+ case 'RIGHT':
327
+ return 'right';
328
+ case 'JUSTIFIED':
329
+ return 'justify';
330
+ default:
331
+ return null;
332
+ }
333
+ }
334
+ function rgbColorToHex(rgb) {
335
+ if (!rgb)
336
+ return null;
337
+ const toHex = (value) => {
338
+ const normalized = Math.max(0, Math.min(255, Math.round((value ?? 0) * 255)));
339
+ return normalized.toString(16).padStart(2, '0');
340
+ };
341
+ return `#${toHex(rgb.red)}${toHex(rgb.green)}${toHex(rgb.blue)}`;
342
+ }
343
+ function escapeHtmlAttr(value) {
344
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
345
+ }
149
346
  // --- Table Conversion ---
150
- function convertTable(table) {
347
+ function convertTable(table, options) {
151
348
  if (!table.tableRows || table.tableRows.length === 0) {
152
349
  return '';
153
350
  }
@@ -162,15 +359,15 @@ function convertTable(table) {
162
359
  continue;
163
360
  let rowText = '|';
164
361
  for (const cell of row.tableCells) {
165
- const cellText = extractCellText(cell);
362
+ const cellText = extractCellText(cell, options);
166
363
  rowText += ` ${cellText} |`;
167
364
  }
168
365
  markdown += rowText + '\n';
169
366
  // Add header separator after the first row
170
367
  if (isFirstRow) {
171
368
  let separator = '|';
172
- for (let i = 0; i < row.tableCells.length; i++) {
173
- separator += ' --- |';
369
+ for (const cell of row.tableCells) {
370
+ separator += ` ${tableAlignmentMarker(cell)} |`;
174
371
  }
175
372
  markdown += separator + '\n';
176
373
  isFirstRow = false;
@@ -242,18 +439,22 @@ function convertCodeBlockTable(table) {
242
439
  }
243
440
  return '\n```\n' + codeText + '\n```\n\n';
244
441
  }
245
- function extractCellText(cell) {
442
+ function tableAlignmentMarker(cell) {
443
+ const alignment = paragraphAlignmentToHtml(cell.content?.[0]?.paragraph?.paragraphStyle?.alignment);
444
+ if (alignment === 'center')
445
+ return ':---:';
446
+ if (alignment === 'right')
447
+ return '---:';
448
+ return '---';
449
+ }
450
+ function extractCellText(cell, options) {
246
451
  let text = '';
247
452
  if (!cell.content)
248
453
  return text;
249
454
  for (const element of cell.content) {
250
455
  if (element.paragraph?.elements) {
251
- for (const pe of element.paragraph.elements) {
252
- if (pe.textRun?.content) {
253
- text += pe.textRun.content.replace(/\n/g, ' ').trim();
254
- }
255
- }
456
+ text += extractFormattedText(element.paragraph.elements, options).replace(/\n/g, ' ');
256
457
  }
257
458
  }
258
- return text;
459
+ return text.trim().replace(/\|/g, '\\|');
259
460
  }
@@ -12,24 +12,31 @@
12
12
  import { docsJsonToMarkdown } from './docsToMarkdown.js';
13
13
  import { convertMarkdownToRequests } from './markdownToDocs.js';
14
14
  import { executeBatchUpdateWithSplitting, findTabById } from '../googleDocsApiHelpers.js';
15
- export { docsJsonToMarkdown } from './docsToMarkdown.js';
15
+ export { docsJsonToMarkdown, checkMarkdownFidelity } from './docsToMarkdown.js';
16
16
  /** Formats InsertMarkdownResult into a concise human-readable debug summary. */
17
17
  export function formatInsertResult(result) {
18
18
  const lines = [];
19
+ if (result.warnings?.length) {
20
+ lines.push('WARNINGS (content dropped):');
21
+ for (const warning of result.warnings) {
22
+ lines.push(` - ${warning}`);
23
+ }
24
+ lines.push('');
25
+ }
19
26
  lines.push(`Markdown insert completed in ${result.totalElapsedMs}ms`);
20
27
  lines.push(` Parse: ${result.parseElapsedMs}ms`);
21
28
  lines.push(` Requests: ${result.totalRequests} total (${Object.entries(result.requestsByType)
22
29
  .map(([k, v]) => `${v} ${k}`)
23
30
  .join(', ')})`);
24
31
  lines.push(` API calls: ${result.batchUpdate.totalApiCalls} batchUpdate calls in ${result.batchUpdate.totalElapsedMs}ms`);
25
- const { phases } = result.batchUpdate;
26
- if (phases.delete.requests > 0) {
32
+ const phases = result.batchUpdate.phases;
33
+ if (phases?.delete?.requests > 0) {
27
34
  lines.push(` Delete phase: ${phases.delete.requests} requests, ${phases.delete.apiCalls} calls, ${phases.delete.elapsedMs}ms`);
28
35
  }
29
- if (phases.insert.requests > 0) {
36
+ if (phases?.insert?.requests > 0) {
30
37
  lines.push(` Insert phase: ${phases.insert.requests} requests, ${phases.insert.apiCalls} calls, ${phases.insert.elapsedMs}ms`);
31
38
  }
32
- if (phases.format.requests > 0) {
39
+ if (phases?.format?.requests > 0) {
33
40
  lines.push(` Format phase: ${phases.format.requests} requests, ${phases.format.apiCalls} calls, ${phases.format.elapsedMs}ms`);
34
41
  }
35
42
  return lines.join('\n');
@@ -61,12 +68,12 @@ export async function extractMarkdown(docs, documentId, options) {
61
68
  return docsJsonToMarkdown({
62
69
  body: targetTab.documentTab.body,
63
70
  lists: targetTab.documentTab.lists,
64
- });
71
+ }, options);
65
72
  }
66
73
  return docsJsonToMarkdown({
67
74
  body: res.data.body,
68
75
  lists: res.data.lists,
69
- });
76
+ }, options);
70
77
  }
71
78
  // --- insertMarkdown ---
72
79
  /**
@@ -85,6 +92,7 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
85
92
  const overallStart = performance.now();
86
93
  const startIndex = options?.startIndex ?? 1;
87
94
  const tabId = options?.tabId;
95
+ const writeControl = options?.writeControl;
88
96
  // Fetch the document's default text style so we can explicitly set
89
97
  // foreground color on inserted text (fixes issue #14 — text without
90
98
  // explicit color shows "no color selected" in the Docs color picker).
@@ -109,7 +117,7 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
109
117
  ...(options?.firstHeadingAsTitle && { firstHeadingAsTitle: true }),
110
118
  ...(defaultForegroundColor && { defaultForegroundColor }),
111
119
  };
112
- const requests = convertMarkdownToRequests(markdown, startIndex, tabId, conversionOptions);
120
+ const { requests, warnings } = convertMarkdownToRequests(markdown, startIndex, tabId, conversionOptions);
113
121
  const parseElapsedMs = Math.round(performance.now() - parseStart);
114
122
  // Count requests by type
115
123
  const requestsByType = {};
@@ -119,6 +127,7 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
119
127
  }
120
128
  if (requests.length === 0) {
121
129
  return {
130
+ warnings,
122
131
  totalRequests: 0,
123
132
  requestsByType,
124
133
  parseElapsedMs,
@@ -135,8 +144,9 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
135
144
  totalElapsedMs: Math.round(performance.now() - overallStart),
136
145
  };
137
146
  }
138
- const batchUpdate = await executeBatchUpdateWithSplitting(docs, documentId, requests);
147
+ const batchUpdate = await executeBatchUpdateWithSplitting(docs, documentId, requests, undefined, writeControl);
139
148
  return {
149
+ warnings,
140
150
  totalRequests: requests.length,
141
151
  requestsByType,
142
152
  parseElapsedMs,