@triffon/google-docs-mcp 1.11.3-triffon

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 (165) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +567 -0
  3. package/dist/auth.js +327 -0
  4. package/dist/cachedToolsList.js +38 -0
  5. package/dist/clients.js +180 -0
  6. package/dist/config.js +9 -0
  7. package/dist/downloadProxy.js +82 -0
  8. package/dist/driveQueryUtils.js +7 -0
  9. package/dist/firestoreTokenStorage.js +32 -0
  10. package/dist/googleDocsApiHelpers.js +1257 -0
  11. package/dist/googleSheetsApiHelpers.js +849 -0
  12. package/dist/index.js +240 -0
  13. package/dist/landingPage.js +103 -0
  14. package/dist/logger.js +58 -0
  15. package/dist/markdown-transformer/docsToMarkdown.js +259 -0
  16. package/dist/markdown-transformer/index.js +128 -0
  17. package/dist/markdown-transformer/markdownToDocs.js +834 -0
  18. package/dist/remoteWrapper.js +72 -0
  19. package/dist/tools/calendar/createEvent.js +89 -0
  20. package/dist/tools/calendar/deleteEvent.js +49 -0
  21. package/dist/tools/calendar/helpers.js +19 -0
  22. package/dist/tools/calendar/index.js +12 -0
  23. package/dist/tools/calendar/listEvents.js +84 -0
  24. package/dist/tools/calendar/quickAddEvent.js +55 -0
  25. package/dist/tools/calendar/updateEvent.js +84 -0
  26. package/dist/tools/docs/addTab.js +84 -0
  27. package/dist/tools/docs/appendTableRows.js +89 -0
  28. package/dist/tools/docs/appendToGoogleDoc.js +85 -0
  29. package/dist/tools/docs/cloneTable.js +159 -0
  30. package/dist/tools/docs/comments/addComment.js +83 -0
  31. package/dist/tools/docs/comments/deleteComment.js +30 -0
  32. package/dist/tools/docs/comments/getComment.js +45 -0
  33. package/dist/tools/docs/comments/index.js +14 -0
  34. package/dist/tools/docs/comments/listComments.js +43 -0
  35. package/dist/tools/docs/comments/replyToComment.js +35 -0
  36. package/dist/tools/docs/comments/resolveComment.js +55 -0
  37. package/dist/tools/docs/deleteRange.js +61 -0
  38. package/dist/tools/docs/deleteTableRows.js +62 -0
  39. package/dist/tools/docs/findAndReplace.js +54 -0
  40. package/dist/tools/docs/findElement.js +43 -0
  41. package/dist/tools/docs/findSectionsByHeading.js +46 -0
  42. package/dist/tools/docs/formatting/applyParagraphStyle.js +83 -0
  43. package/dist/tools/docs/formatting/applyTextStyle.js +49 -0
  44. package/dist/tools/docs/formatting/batchApplyTextStyle.js +86 -0
  45. package/dist/tools/docs/formatting/index.js +16 -0
  46. package/dist/tools/docs/formatting/updateTableBorders.js +81 -0
  47. package/dist/tools/docs/formatting/updateTableCellStyle.js +85 -0
  48. package/dist/tools/docs/formatting/updateTableColumnWidth.js +51 -0
  49. package/dist/tools/docs/formatting/updateTableRowStyle.js +74 -0
  50. package/dist/tools/docs/getTableStructure.js +48 -0
  51. package/dist/tools/docs/index.js +66 -0
  52. package/dist/tools/docs/insertDateChip.js +82 -0
  53. package/dist/tools/docs/insertImage.js +112 -0
  54. package/dist/tools/docs/insertPageBreak.js +47 -0
  55. package/dist/tools/docs/insertPerson.js +53 -0
  56. package/dist/tools/docs/insertRichLink.js +58 -0
  57. package/dist/tools/docs/insertSectionBreak.js +60 -0
  58. package/dist/tools/docs/insertTable.js +42 -0
  59. package/dist/tools/docs/insertTableWithData.js +125 -0
  60. package/dist/tools/docs/insertText.js +49 -0
  61. package/dist/tools/docs/listDocumentTables.js +47 -0
  62. package/dist/tools/docs/listDocumentTabs.js +59 -0
  63. package/dist/tools/docs/listSmartChips.js +41 -0
  64. package/dist/tools/docs/modifyText.js +147 -0
  65. package/dist/tools/docs/readGoogleDoc.js +164 -0
  66. package/dist/tools/docs/renameTab.js +47 -0
  67. package/dist/tools/docs/replaceTableRowData.js +55 -0
  68. package/dist/tools/docs/smartChipHelpers.js +71 -0
  69. package/dist/tools/docs/structureHelpers.js +250 -0
  70. package/dist/tools/docs/tabFieldMasks.js +48 -0
  71. package/dist/tools/docs/tableRowDataHelpers.js +53 -0
  72. package/dist/tools/docs/updateSectionStyle.js +148 -0
  73. package/dist/tools/drive/copyFile.js +63 -0
  74. package/dist/tools/drive/createDocument.js +105 -0
  75. package/dist/tools/drive/createFolder.js +48 -0
  76. package/dist/tools/drive/createFromTemplate.js +82 -0
  77. package/dist/tools/drive/deleteFile.js +72 -0
  78. package/dist/tools/drive/downloadFile.js +266 -0
  79. package/dist/tools/drive/getDocumentInfo.js +48 -0
  80. package/dist/tools/drive/getFolderInfo.js +48 -0
  81. package/dist/tools/drive/index.js +34 -0
  82. package/dist/tools/drive/listDriveFiles.js +129 -0
  83. package/dist/tools/drive/listFolderContents.js +83 -0
  84. package/dist/tools/drive/listGoogleDocs.js +70 -0
  85. package/dist/tools/drive/moveFile.js +54 -0
  86. package/dist/tools/drive/renameFile.js +39 -0
  87. package/dist/tools/drive/savePathGuard.js +86 -0
  88. package/dist/tools/drive/searchDriveFiles.js +148 -0
  89. package/dist/tools/drive/searchGoogleDocs.js +77 -0
  90. package/dist/tools/drive/setFilePermission.js +68 -0
  91. package/dist/tools/gmail/createDraft.js +58 -0
  92. package/dist/tools/gmail/deleteDraft.js +37 -0
  93. package/dist/tools/gmail/getDraft.js +52 -0
  94. package/dist/tools/gmail/getMessage.js +92 -0
  95. package/dist/tools/gmail/helpers.js +113 -0
  96. package/dist/tools/gmail/index.js +28 -0
  97. package/dist/tools/gmail/listDrafts.js +74 -0
  98. package/dist/tools/gmail/listLabels.js +31 -0
  99. package/dist/tools/gmail/listMessages.js +87 -0
  100. package/dist/tools/gmail/modifyMessageLabels.js +54 -0
  101. package/dist/tools/gmail/sendDraft.js +42 -0
  102. package/dist/tools/gmail/sendEmail.js +56 -0
  103. package/dist/tools/gmail/trashMessage.js +37 -0
  104. package/dist/tools/gmail/triageInbox.js +147 -0
  105. package/dist/tools/gmail/updateDraft.js +60 -0
  106. package/dist/tools/index.js +64 -0
  107. package/dist/tools/script/appsScriptShared.js +96 -0
  108. package/dist/tools/script/createAppsScriptProject.js +74 -0
  109. package/dist/tools/script/getAppsScriptContent.js +56 -0
  110. package/dist/tools/script/index.js +14 -0
  111. package/dist/tools/script/updateAppsScriptContent.js +56 -0
  112. package/dist/tools/sheets/addConditionalFormatting.js +143 -0
  113. package/dist/tools/sheets/addSpreadsheetSheet.js +34 -0
  114. package/dist/tools/sheets/appendSpreadsheetRows.js +44 -0
  115. package/dist/tools/sheets/appendTableRows.js +51 -0
  116. package/dist/tools/sheets/autoResizeColumns.js +67 -0
  117. package/dist/tools/sheets/autoResizeRows.js +63 -0
  118. package/dist/tools/sheets/batchWrite.js +61 -0
  119. package/dist/tools/sheets/clearSpreadsheetRange.js +31 -0
  120. package/dist/tools/sheets/comments/commentAnchor.js +95 -0
  121. package/dist/tools/sheets/comments/createSheetsCellNote.js +33 -0
  122. package/dist/tools/sheets/comments/createSheetsComment.js +168 -0
  123. package/dist/tools/sheets/comments/deleteSheetsComment.js +32 -0
  124. package/dist/tools/sheets/comments/getSheetsComment.js +51 -0
  125. package/dist/tools/sheets/comments/index.js +16 -0
  126. package/dist/tools/sheets/comments/listSheetsComments.js +177 -0
  127. package/dist/tools/sheets/comments/replyToSheetsComment.js +37 -0
  128. package/dist/tools/sheets/comments/resolveSheetsComment.js +53 -0
  129. package/dist/tools/sheets/copyFormatting.js +59 -0
  130. package/dist/tools/sheets/copySheetTo.js +36 -0
  131. package/dist/tools/sheets/createSpreadsheet.js +72 -0
  132. package/dist/tools/sheets/createTable.js +120 -0
  133. package/dist/tools/sheets/deleteChart.js +41 -0
  134. package/dist/tools/sheets/deleteConditionalFormatting.js +46 -0
  135. package/dist/tools/sheets/deleteSheet.js +43 -0
  136. package/dist/tools/sheets/deleteTable.js +56 -0
  137. package/dist/tools/sheets/duplicateSheet.js +53 -0
  138. package/dist/tools/sheets/formatCells.js +122 -0
  139. package/dist/tools/sheets/freezeRowsAndColumns.js +58 -0
  140. package/dist/tools/sheets/getConditionalFormatting.js +98 -0
  141. package/dist/tools/sheets/getSpreadsheetInfo.js +44 -0
  142. package/dist/tools/sheets/getTable.js +48 -0
  143. package/dist/tools/sheets/groupRows.js +62 -0
  144. package/dist/tools/sheets/index.js +84 -0
  145. package/dist/tools/sheets/insertChart.js +225 -0
  146. package/dist/tools/sheets/listGoogleSheets.js +66 -0
  147. package/dist/tools/sheets/listTables.js +55 -0
  148. package/dist/tools/sheets/protectRange.js +59 -0
  149. package/dist/tools/sheets/readCellFormat.js +143 -0
  150. package/dist/tools/sheets/readSpreadsheet.js +36 -0
  151. package/dist/tools/sheets/renameSheet.js +48 -0
  152. package/dist/tools/sheets/setCellBorders.js +94 -0
  153. package/dist/tools/sheets/setColumnWidths.js +43 -0
  154. package/dist/tools/sheets/setDropdownValidation.js +51 -0
  155. package/dist/tools/sheets/setRowHeights.js +63 -0
  156. package/dist/tools/sheets/ungroupAllRows.js +66 -0
  157. package/dist/tools/sheets/updateTableRange.js +51 -0
  158. package/dist/tools/sheets/writeSpreadsheet.js +45 -0
  159. package/dist/tools/utils/appendMarkdownToGoogleDoc.js +95 -0
  160. package/dist/tools/utils/index.js +8 -0
  161. package/dist/tools/utils/replaceDocumentWithMarkdown.js +161 -0
  162. package/dist/tools/utils/replaceRangeWithMarkdown.js +72 -0
  163. package/dist/types.js +208 -0
  164. package/dist/upstreamAuth.js +62 -0
  165. package/package.json +51 -0
@@ -0,0 +1,164 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient, getDriveClient } from '../../clients.js';
4
+ import { DocumentIdParameter, NotImplementedError } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
7
+ import { buildTabsFieldMask } from './tabFieldMasks.js';
8
+ export function register(server) {
9
+ server.addTool({
10
+ name: 'readDocument',
11
+ description: "Reads the content of a Google Document. Returns plain text by default. Use format='markdown' to get formatted content suitable for editing and re-uploading with replaceDocumentWithMarkdown, or format='json' for the raw document structure.",
12
+ parameters: DocumentIdParameter.extend({
13
+ format: z
14
+ .enum(['text', 'json', 'markdown'])
15
+ .optional()
16
+ .default('text')
17
+ .describe("Output format: 'text' (plain text), 'json' (raw API structure, complex), 'markdown' (experimental conversion)."),
18
+ maxLength: z
19
+ .number()
20
+ .optional()
21
+ .describe('Maximum character limit for text output. If not specified, returns full document content. Use this to limit very large documents.'),
22
+ tabId: z
23
+ .string()
24
+ .optional()
25
+ .describe('The ID of the specific tab to read. If not specified, reads the first tab (or legacy document.body for documents without tabs).'),
26
+ }),
27
+ execute: async (args, { log }) => {
28
+ const docs = await getDocsClient();
29
+ log.info(`Reading Google Doc: ${args.documentId}, Format: ${args.format}${args.tabId ? `, Tab: ${args.tabId}` : ''}`);
30
+ try {
31
+ // Determine if we need tabs content
32
+ const needsTabsContent = !!args.tabId;
33
+ const fields = args.format === 'json' || args.format === 'markdown'
34
+ ? '*' // Get everything for structure analysis
35
+ : 'body(content(paragraph(elements(textRun(content)))))'; // Just text content
36
+ const res = await docs.documents.get({
37
+ documentId: args.documentId,
38
+ includeTabsContent: needsTabsContent,
39
+ suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS',
40
+ fields: needsTabsContent
41
+ ? `title,documentId,${buildTabsFieldMask('documentTab(body,documentStyle,namedStyles,lists)')}`
42
+ : fields,
43
+ });
44
+ log.info(`Fetched doc: ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
45
+ // If tabId is specified, find the specific tab
46
+ let contentSource;
47
+ if (args.tabId) {
48
+ const targetTab = GDocsHelpers.findTabById(res.data, args.tabId);
49
+ if (!targetTab) {
50
+ throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
51
+ }
52
+ if (!targetTab.documentTab) {
53
+ throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
54
+ }
55
+ contentSource = { body: targetTab.documentTab.body };
56
+ log.info(`Using content from tab: ${targetTab.tabProperties?.title || 'Untitled'}`);
57
+ }
58
+ else {
59
+ // Use the document body (backward compatible)
60
+ contentSource = res.data;
61
+ }
62
+ if (args.format === 'json') {
63
+ const jsonContent = JSON.stringify(contentSource, null, 2);
64
+ // Apply length limit to JSON if specified
65
+ if (args.maxLength && jsonContent.length > args.maxLength) {
66
+ return (jsonContent.substring(0, args.maxLength) +
67
+ `\n... [JSON truncated: ${jsonContent.length} total chars]`);
68
+ }
69
+ return jsonContent;
70
+ }
71
+ if (args.format === 'markdown') {
72
+ const markdownContent = docsJsonToMarkdown(contentSource);
73
+ const totalLength = markdownContent.length;
74
+ log.info(`Generated markdown: ${totalLength} characters`);
75
+ // Apply length limit to markdown if specified
76
+ if (args.maxLength && totalLength > args.maxLength) {
77
+ const truncatedContent = markdownContent.substring(0, args.maxLength);
78
+ return `${truncatedContent}\n\n... [Markdown truncated to ${args.maxLength} chars of ${totalLength} total. Use maxLength parameter to adjust limit or remove it to get full content.]`;
79
+ }
80
+ return markdownContent;
81
+ }
82
+ // Default: Text format - extract all text content
83
+ let textContent = '';
84
+ let elementCount = 0;
85
+ const extractFromElements = (elements) => {
86
+ for (const element of elements || []) {
87
+ elementCount++;
88
+ if (element.paragraph?.elements) {
89
+ for (const pe of element.paragraph.elements) {
90
+ if (pe.textRun?.content)
91
+ textContent += pe.textRun.content;
92
+ }
93
+ }
94
+ if (element.table?.tableRows) {
95
+ for (const row of element.table.tableRows) {
96
+ for (const cell of row.tableCells || []) {
97
+ extractFromElements(cell.content || []);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ };
103
+ // Process all content elements from contentSource
104
+ contentSource.body?.content?.forEach((element) => {
105
+ extractFromElements([element]);
106
+ });
107
+ if (!textContent.trim())
108
+ return 'Document found, but appears empty.';
109
+ const totalLength = textContent.length;
110
+ log.info(`Document contains ${totalLength} characters across ${elementCount} elements`);
111
+ log.info(`maxLength parameter: ${args.maxLength || 'not specified'}`);
112
+ // Apply length limit only if specified
113
+ if (args.maxLength && totalLength > args.maxLength) {
114
+ const truncatedContent = textContent.substring(0, args.maxLength);
115
+ log.info(`Truncating content from ${totalLength} to ${args.maxLength} characters`);
116
+ return `Content (truncated to ${args.maxLength} chars of ${totalLength} total):\n---\n${truncatedContent}\n\n... [Document continues for ${totalLength - args.maxLength} more characters. Use maxLength parameter to adjust limit or remove it to get full content.]`;
117
+ }
118
+ // Return full content
119
+ const fullResponse = `Content (${totalLength} characters):\n---\n${textContent}`;
120
+ const responseLength = fullResponse.length;
121
+ log.info(`Returning full content: ${responseLength} characters in response (${totalLength} content + ${responseLength - totalLength} metadata)`);
122
+ return fullResponse;
123
+ }
124
+ catch (error) {
125
+ log.error(`Error reading doc ${args.documentId}: ${error.message || 'Unknown error'} (code: ${error.code || 'N/A'})`);
126
+ // Handle errors thrown by helpers or API directly
127
+ if (error instanceof UserError)
128
+ throw error;
129
+ if (error instanceof NotImplementedError)
130
+ throw error;
131
+ // Generic fallback for API errors not caught by helpers
132
+ if (error.code === 404)
133
+ throw new UserError(`Doc not found (ID: ${args.documentId}).`);
134
+ if (error.code === 403) {
135
+ // The Docs API may be blocked by Workspace admin policy even when the Drive API is
136
+ // accessible. Fall back to drive.files.export() for plain-text format, which uses
137
+ // the Drive API and respects supportsAllDrives for Shared Drive documents.
138
+ if (!args.format || args.format === 'text') {
139
+ try {
140
+ log.info(`Docs API returned 403, falling back to Drive export for ${args.documentId}`);
141
+ const drive = await getDriveClient();
142
+ const exportRes = await drive.files.export({ fileId: args.documentId, mimeType: 'text/plain' }, { responseType: 'text' });
143
+ const textContent = exportRes.data;
144
+ if (!textContent?.trim())
145
+ return 'Document found, but appears empty.';
146
+ if (args.maxLength && textContent.length > args.maxLength) {
147
+ return `Content (truncated to ${args.maxLength} chars of ${textContent.length} total):\n---\n${textContent.substring(0, args.maxLength)}\n\n... [Document continues. Use maxLength parameter to adjust limit or remove it to get full content.]`;
148
+ }
149
+ return `Content (${textContent.length} characters):\n---\n${textContent}`;
150
+ }
151
+ catch (exportError) {
152
+ log.error(`Drive export fallback also failed: ${exportError.message}`);
153
+ }
154
+ }
155
+ throw new UserError(`Permission denied for doc (ID: ${args.documentId}). The Google Docs API may be restricted by your Workspace admin.`);
156
+ }
157
+ // Extract detailed error information from Google API response
158
+ const errorDetails = error.response?.data?.error?.message || error.message || 'Unknown error';
159
+ const errorCode = error.response?.data?.error?.code || error.code;
160
+ throw new UserError(`Failed to read doc: ${errorDetails}${errorCode ? ` (Code: ${errorCode})` : ''}`);
161
+ }
162
+ },
163
+ });
164
+ }
@@ -0,0 +1,47 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
6
+ export function register(server) {
7
+ server.addTool({
8
+ name: 'renameTab',
9
+ description: 'Renames a tab in a Google Docs document. Use listDocumentTabs to get tab IDs first.',
10
+ parameters: DocumentIdParameter.extend({
11
+ tabId: z
12
+ .string()
13
+ .describe('The ID of the tab to rename. Use listDocumentTabs to get tab IDs.'),
14
+ newTitle: z.string().min(1).describe('The new title for the tab.'),
15
+ }),
16
+ execute: async (args, { log }) => {
17
+ const docs = await getDocsClient();
18
+ log.info(`Renaming tab ${args.tabId} to "${args.newTitle}" in doc ${args.documentId}`);
19
+ try {
20
+ // Verify the tab exists
21
+ const targetTab = await GDocsHelpers.getDocumentTab(docs, args.documentId, args.tabId);
22
+ await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
23
+ {
24
+ updateDocumentTabProperties: {
25
+ tabProperties: {
26
+ tabId: args.tabId,
27
+ title: args.newTitle,
28
+ },
29
+ fields: 'title',
30
+ },
31
+ },
32
+ ]);
33
+ return `Successfully renamed tab "${args.tabId}" to "${args.newTitle}".`;
34
+ }
35
+ catch (error) {
36
+ log.error(`Error renaming tab in doc ${args.documentId}: ${error.message || error}`);
37
+ if (error instanceof UserError)
38
+ throw error;
39
+ if (error.code === 404)
40
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
41
+ if (error.code === 403)
42
+ throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
43
+ throw new UserError(`Failed to rename tab: ${error.message || 'Unknown error'}`);
44
+ }
45
+ },
46
+ });
47
+ }
@@ -0,0 +1,55 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getDocsClient } from '../../clients.js';
4
+ import { DocumentIdParameter } from '../../types.js';
5
+ import { getTableById } from './structureHelpers.js';
6
+ import { replaceTableRowData as replaceTableRowDataInternal } from './tableRowDataHelpers.js';
7
+ import { TABLE_CONTENT_INDEXED_BODY_FIELDS, buildDocumentGetFields } from './tabFieldMasks.js';
8
+ export function register(server) {
9
+ server.addTool({
10
+ name: 'replaceTableRowData',
11
+ description: 'Replaces the plain-text contents of a single row in an existing Google Docs table while preserving the table structure and formatting.',
12
+ parameters: DocumentIdParameter.extend({
13
+ tableId: z
14
+ .string()
15
+ .min(1)
16
+ .describe('The MCP table ID returned by listDocumentTables, for example "table:body:0".'),
17
+ rowIndex: z.number().int().min(0).describe('Zero-based row index to replace.'),
18
+ values: z
19
+ .array(z.string())
20
+ .max(50)
21
+ .describe('Plain-text values to place into the row cells from left to right.'),
22
+ tabId: z
23
+ .string()
24
+ .optional()
25
+ .describe('The ID of the specific tab containing the table. If not specified, uses the first tab or legacy document body.'),
26
+ }),
27
+ execute: async (args, { log }) => {
28
+ const docs = await getDocsClient();
29
+ log.info(`Replacing row ${args.rowIndex} in ${args.tableId} for doc ${args.documentId}${args.tabId ? ` (tab: ${args.tabId})` : ''}`);
30
+ try {
31
+ const res = await docs.documents.get({
32
+ documentId: args.documentId,
33
+ ...(args.tabId && { includeTabsContent: true }),
34
+ fields: buildDocumentGetFields(TABLE_CONTENT_INDEXED_BODY_FIELDS, args.tabId),
35
+ });
36
+ const table = getTableById(res.data, args.tableId, args.tabId);
37
+ if (!table) {
38
+ throw new UserError(`Table "${args.tableId}" not found in document.`);
39
+ }
40
+ await replaceTableRowDataInternal(docs, args.documentId, table, args.rowIndex, args.values, args.tabId);
41
+ return `Successfully replaced row ${args.rowIndex} in table ${args.tableId}.`;
42
+ }
43
+ catch (error) {
44
+ log.error(`Error replacing row ${args.rowIndex} in ${args.tableId} for doc ${args.documentId}: ${error.message || error}`);
45
+ if (error instanceof UserError)
46
+ throw error;
47
+ if (error.code === 404)
48
+ throw new UserError(`Document not found (ID: ${args.documentId}).`);
49
+ if (error.code === 403)
50
+ throw new UserError(`Permission denied for document (ID: ${args.documentId}).`);
51
+ throw new UserError(`Failed to replace table row data: ${error.message || 'Unknown error'}`);
52
+ }
53
+ },
54
+ });
55
+ }
@@ -0,0 +1,71 @@
1
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
2
+ function getContentSource(doc, tabId) {
3
+ if (tabId) {
4
+ const targetTab = GDocsHelpers.findTabById(doc, tabId);
5
+ if (!targetTab?.documentTab?.body?.content)
6
+ return [];
7
+ return targetTab.documentTab.body.content;
8
+ }
9
+ if (doc.body?.content)
10
+ return doc.body.content;
11
+ if (doc.tabs?.[0]?.documentTab?.body?.content)
12
+ return doc.tabs[0].documentTab.body.content;
13
+ return [];
14
+ }
15
+ function visitContent(content, out) {
16
+ for (const element of content) {
17
+ for (const paragraphElement of element.paragraph?.elements ?? []) {
18
+ if (paragraphElement.dateElement) {
19
+ out.push({
20
+ type: 'date',
21
+ startIndex: paragraphElement.startIndex ?? null,
22
+ endIndex: paragraphElement.endIndex ?? null,
23
+ text: paragraphElement.dateElement.dateElementProperties?.displayText ?? null,
24
+ properties: {
25
+ dateId: paragraphElement.dateElement.dateId ?? null,
26
+ ...paragraphElement.dateElement.dateElementProperties,
27
+ },
28
+ });
29
+ }
30
+ if (paragraphElement.richLink) {
31
+ out.push({
32
+ type: 'richLink',
33
+ startIndex: paragraphElement.startIndex ?? null,
34
+ endIndex: paragraphElement.endIndex ?? null,
35
+ text: paragraphElement.richLink.richLinkProperties?.title ?? null,
36
+ properties: {
37
+ richLinkId: paragraphElement.richLink.richLinkId ?? null,
38
+ ...paragraphElement.richLink.richLinkProperties,
39
+ },
40
+ });
41
+ }
42
+ if (paragraphElement.person) {
43
+ out.push({
44
+ type: 'person',
45
+ startIndex: paragraphElement.startIndex ?? null,
46
+ endIndex: paragraphElement.endIndex ?? null,
47
+ text: paragraphElement.person.personProperties?.name ??
48
+ paragraphElement.person.personProperties?.email ??
49
+ null,
50
+ properties: {
51
+ personId: paragraphElement.person.personId ?? null,
52
+ ...paragraphElement.person.personProperties,
53
+ },
54
+ });
55
+ }
56
+ }
57
+ if (element.table?.tableRows) {
58
+ for (const row of element.table.tableRows) {
59
+ for (const cell of row.tableCells ?? []) {
60
+ visitContent(cell.content ?? [], out);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ export function extractSmartChips(doc, tabId) {
67
+ const content = getContentSource(doc, tabId);
68
+ const chips = [];
69
+ visitContent(content, chips);
70
+ return chips;
71
+ }
@@ -0,0 +1,250 @@
1
+ import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
2
+ function getContentSource(doc, tabId) {
3
+ if (tabId) {
4
+ const targetTab = GDocsHelpers.findTabById(doc, tabId);
5
+ if (!targetTab?.documentTab?.body?.content) {
6
+ return [];
7
+ }
8
+ return targetTab.documentTab.body.content;
9
+ }
10
+ if (doc.body?.content) {
11
+ return doc.body.content;
12
+ }
13
+ if (doc.tabs?.[0]?.documentTab?.body?.content) {
14
+ return doc.tabs[0].documentTab.body.content;
15
+ }
16
+ return [];
17
+ }
18
+ function extractParagraphText(paragraph) {
19
+ return (paragraph?.elements
20
+ ?.map((element) => element.textRun?.content ?? '')
21
+ .join('')
22
+ .replace(/\n+$/g, '') ?? '');
23
+ }
24
+ function extractCellText(content = []) {
25
+ const parts = [];
26
+ for (const element of content) {
27
+ if (element.paragraph) {
28
+ const text = extractParagraphText(element.paragraph);
29
+ if (text)
30
+ parts.push(text);
31
+ }
32
+ if (element.table?.tableRows) {
33
+ for (const row of element.table.tableRows) {
34
+ for (const cell of row.tableCells ?? []) {
35
+ const text = extractCellText(cell.content ?? []);
36
+ if (text)
37
+ parts.push(text);
38
+ }
39
+ }
40
+ }
41
+ }
42
+ return parts.join('\n').trim();
43
+ }
44
+ function extractCellContentRange(content = []) {
45
+ let minStart = null;
46
+ let maxEnd = null;
47
+ const visitContent = (elements) => {
48
+ for (const element of elements) {
49
+ for (const paragraphElement of element.paragraph?.elements ?? []) {
50
+ const startIndex = paragraphElement.startIndex;
51
+ if (typeof startIndex === 'number') {
52
+ minStart = minStart === null ? startIndex : Math.min(minStart, startIndex);
53
+ }
54
+ const endIndex = paragraphElement.endIndex;
55
+ if (typeof endIndex === 'number') {
56
+ maxEnd = maxEnd === null ? endIndex : Math.max(maxEnd, endIndex);
57
+ }
58
+ }
59
+ if (element.table?.tableRows) {
60
+ for (const row of element.table.tableRows) {
61
+ for (const cell of row.tableCells ?? []) {
62
+ visitContent(cell.content ?? []);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ };
68
+ visitContent(content);
69
+ return {
70
+ contentStartIndex: minStart,
71
+ contentEndIndex: maxEnd,
72
+ };
73
+ }
74
+ function dimensionToPt(dimension) {
75
+ if (!dimension?.magnitude || dimension.unit !== 'PT')
76
+ return undefined;
77
+ return dimension.magnitude;
78
+ }
79
+ function normalizeCellStyle(rowIndex, columnIndex, cell) {
80
+ const style = cell.tableCellStyle;
81
+ const firstParagraphHasBoldText = (cell.content ?? []).some((element) => (element.paragraph?.elements ?? []).some((paragraphElement) => paragraphElement.textRun?.textStyle?.bold));
82
+ if (!style && !firstParagraphHasBoldText)
83
+ return null;
84
+ const contentAlignment = style?.contentAlignment === 'TOP' ||
85
+ style?.contentAlignment === 'MIDDLE' ||
86
+ style?.contentAlignment === 'BOTTOM' ||
87
+ style?.contentAlignment === 'CONTENT_ALIGNMENT_UNSPECIFIED'
88
+ ? style.contentAlignment
89
+ : null;
90
+ return {
91
+ rowIndex,
92
+ columnIndex,
93
+ backgroundColor: style?.backgroundColor?.color?.rgbColor ?? undefined,
94
+ contentAlignment,
95
+ paddingTopPt: dimensionToPt(style?.paddingTop),
96
+ paddingBottomPt: dimensionToPt(style?.paddingBottom),
97
+ paddingLeftPt: dimensionToPt(style?.paddingLeft),
98
+ paddingRightPt: dimensionToPt(style?.paddingRight),
99
+ borderTop: style?.borderTop ?? undefined,
100
+ borderBottom: style?.borderBottom ?? undefined,
101
+ borderLeft: style?.borderLeft ?? undefined,
102
+ borderRight: style?.borderRight ?? undefined,
103
+ hasBoldText: firstParagraphHasBoldText || undefined,
104
+ };
105
+ }
106
+ export function extractDocumentTables(doc, tabId) {
107
+ const content = getContentSource(doc, tabId);
108
+ const tables = [];
109
+ const tabKey = tabId ?? 'body';
110
+ for (const element of content) {
111
+ if (!element.table?.tableRows)
112
+ continue;
113
+ const ordinal = tables.length;
114
+ const cells = [];
115
+ let columnCount = 0;
116
+ element.table.tableRows.forEach((row, rowIndex) => {
117
+ const rowCells = row.tableCells ?? [];
118
+ columnCount = Math.max(columnCount, rowCells.length);
119
+ rowCells.forEach((cell, columnIndex) => {
120
+ const { contentStartIndex, contentEndIndex } = extractCellContentRange(cell.content ?? []);
121
+ cells.push({
122
+ rowIndex,
123
+ columnIndex,
124
+ startIndex: cell.startIndex ?? null,
125
+ endIndex: cell.endIndex ?? null,
126
+ contentStartIndex,
127
+ contentEndIndex,
128
+ text: extractCellText(cell.content ?? []),
129
+ });
130
+ });
131
+ });
132
+ tables.push({
133
+ tableId: `table:${tabKey}:${ordinal}`,
134
+ ordinal,
135
+ startIndex: element.startIndex ?? null,
136
+ endIndex: element.endIndex ?? null,
137
+ rowCount: element.table.tableRows.length,
138
+ columnCount,
139
+ cells,
140
+ });
141
+ }
142
+ return tables;
143
+ }
144
+ export function getTableById(doc, tableId, tabId) {
145
+ return extractDocumentTables(doc, tabId).find((table) => table.tableId === tableId) ?? null;
146
+ }
147
+ export function findTableNearestStartIndex(doc, insertionIndex, tabId) {
148
+ const tables = extractDocumentTables(doc, tabId).filter((table) => typeof table.startIndex === 'number' && table.startIndex >= insertionIndex);
149
+ if (tables.length === 0)
150
+ return null;
151
+ return tables.sort((a, b) => a.startIndex - b.startIndex)[0] ?? null;
152
+ }
153
+ export function extractTableSnapshot(doc, tableId, tabId) {
154
+ const content = getContentSource(doc, tabId);
155
+ const tabKey = tabId ?? 'body';
156
+ let ordinal = 0;
157
+ for (const element of content) {
158
+ if (!element.table?.tableRows)
159
+ continue;
160
+ const currentTableId = `table:${tabKey}:${ordinal}`;
161
+ ordinal++;
162
+ if (currentTableId !== tableId)
163
+ continue;
164
+ const data = [];
165
+ const rowStyles = [];
166
+ const cellStyles = [];
167
+ let pinnedHeaderRowsCount = 0;
168
+ element.table.tableRows.forEach((row, rowIndex) => {
169
+ const rowData = [];
170
+ const rowStyle = row.tableRowStyle;
171
+ if (rowStyle) {
172
+ rowStyles.push({
173
+ rowIndex,
174
+ minRowHeightPt: dimensionToPt(rowStyle.minRowHeight),
175
+ preventOverflow: rowStyle.preventOverflow ?? undefined,
176
+ tableHeader: rowStyle.tableHeader ?? undefined,
177
+ });
178
+ }
179
+ if ((rowStyle?.tableHeader ?? false) && pinnedHeaderRowsCount === rowIndex) {
180
+ pinnedHeaderRowsCount++;
181
+ }
182
+ (row.tableCells ?? []).forEach((cell, columnIndex) => {
183
+ rowData.push(extractCellText(cell.content ?? []));
184
+ const cellStyle = normalizeCellStyle(rowIndex, columnIndex, cell);
185
+ if (cellStyle)
186
+ cellStyles.push(cellStyle);
187
+ });
188
+ data.push(rowData);
189
+ });
190
+ const columnStyles = element.table.tableStyle?.tableColumnProperties?.map((column, columnIndex) => ({
191
+ columnIndex,
192
+ widthPt: dimensionToPt(column.width),
193
+ widthType: column.widthType,
194
+ })) ?? [];
195
+ return {
196
+ tableId: currentTableId,
197
+ startIndex: element.startIndex ?? null,
198
+ endIndex: element.endIndex ?? null,
199
+ rowCount: element.table.rows ?? data.length,
200
+ columnCount: element.table.columns ?? Math.max(...data.map((row) => row.length), 0),
201
+ data,
202
+ columnStyles,
203
+ rowStyles,
204
+ cellStyles,
205
+ pinnedHeaderRowsCount,
206
+ };
207
+ }
208
+ return null;
209
+ }
210
+ export function findHeadings(doc, headings, tabId) {
211
+ const content = getContentSource(doc, tabId);
212
+ const normalizedTargets = new Set(headings.map((heading) => heading.trim()));
213
+ const tables = extractDocumentTables(doc, tabId);
214
+ const results = [];
215
+ let seenTables = 0;
216
+ for (let index = 0; index < content.length; index++) {
217
+ const element = content[index];
218
+ if (element.table?.tableRows) {
219
+ seenTables++;
220
+ continue;
221
+ }
222
+ const namedStyleType = element.paragraph?.paragraphStyle?.namedStyleType;
223
+ if (!namedStyleType || !namedStyleType.startsWith('HEADING_'))
224
+ continue;
225
+ const headingText = extractParagraphText(element.paragraph).trim();
226
+ if (!normalizedTargets.has(headingText))
227
+ continue;
228
+ let tableIdFollowing;
229
+ for (let nextIndex = index + 1; nextIndex < content.length; nextIndex++) {
230
+ const nextElement = content[nextIndex];
231
+ if (nextElement.table?.tableRows) {
232
+ tableIdFollowing = tables[seenTables]?.tableId;
233
+ break;
234
+ }
235
+ if (nextElement.paragraph) {
236
+ const nextStyle = nextElement.paragraph.paragraphStyle?.namedStyleType;
237
+ if (nextStyle?.startsWith('HEADING_'))
238
+ break;
239
+ }
240
+ }
241
+ results.push({
242
+ headingText,
243
+ headingLevel: namedStyleType,
244
+ startIndex: element.startIndex ?? null,
245
+ endIndex: element.endIndex ?? null,
246
+ tableIdFollowing,
247
+ });
248
+ }
249
+ return results;
250
+ }
@@ -0,0 +1,48 @@
1
+ export const TAB_ID_PROPERTIES = 'tabProperties(tabId)';
2
+ export const TAB_LIST_PROPERTIES = 'tabProperties(tabId,title,index,parentTabId)';
3
+ /**
4
+ * Builds a tabs field mask that includes childTabs recursion (3 levels deep) so that
5
+ * findTabById() can locate tabs nested at any depth.
6
+ *
7
+ * @param documentTabFields - the documentTab(...) subfields needed, e.g. 'documentTab(body(content(endIndex)))'
8
+ */
9
+ export function buildTabsFieldMask(documentTabFields) {
10
+ const child3 = `childTabs(${TAB_ID_PROPERTIES},${documentTabFields})`;
11
+ const child2 = `childTabs(${TAB_ID_PROPERTIES},${documentTabFields},${child3})`;
12
+ const child1 = `childTabs(${TAB_ID_PROPERTIES},${documentTabFields},${child2})`;
13
+ return `tabs(${TAB_ID_PROPERTIES},${documentTabFields},${child1})`;
14
+ }
15
+ const BODY_RANGE = 'documentTab(body(content(startIndex,endIndex)))';
16
+ const BODY_END = 'documentTab(body(content(endIndex)))';
17
+ export const TAB_BODY_RANGE_FIELDS = buildTabsFieldMask(BODY_RANGE);
18
+ export const TAB_BODY_END_INDEX_FIELDS = buildTabsFieldMask(BODY_END);
19
+ const TAB_LIST_CHILD_FIELDS = `childTabs(${TAB_LIST_PROPERTIES},childTabs(${TAB_LIST_PROPERTIES},childTabs(${TAB_LIST_PROPERTIES})))`;
20
+ export const TAB_LIST_FIELDS = `title,tabs(${TAB_LIST_PROPERTIES},${TAB_LIST_CHILD_FIELDS})`;
21
+ export const TAB_LIST_WITH_CONTENT_FIELDS = `title,tabs(${TAB_LIST_PROPERTIES},${TAB_LIST_CHILD_FIELDS},documentTab(body(content(endIndex))))`;
22
+ export const TAB_FIELD_MASKS = {
23
+ TAB_BODY_RANGE_FIELDS,
24
+ TAB_BODY_END_INDEX_FIELDS,
25
+ TAB_LIST_FIELDS,
26
+ TAB_LIST_WITH_CONTENT_FIELDS,
27
+ };
28
+ // ── Shared body-level field strings for document.get calls ──────────────────
29
+ // Used to build conditional field masks: body fields for legacy (no tabId),
30
+ // or buildTabsFieldMask(`documentTab(${...})`) when a tabId is provided.
31
+ /** Minimal table index fields — rows/cells start+endIndex only. */
32
+ export const TABLE_INDEX_BODY_FIELDS = 'body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex)))))';
33
+ /** Table fields with basic cell text (no content indices). */
34
+ export const TABLE_CONTENT_BASIC_BODY_FIELDS = 'body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(paragraph(elements(textRun(content)))))))))';
35
+ /** Table fields with full content + element indices (needed for in-cell editing). */
36
+ export const TABLE_CONTENT_INDEXED_BODY_FIELDS = 'body(content(startIndex,endIndex,table(tableRows(tableCells(startIndex,endIndex,content(startIndex,endIndex,paragraph(elements(startIndex,endIndex,textRun(content)))))))))';
37
+ /** Heading + table fields for section-by-heading lookups. */
38
+ export const HEADING_BODY_FIELDS = 'body(content(startIndex,endIndex,paragraph(paragraphStyle(namedStyleType),elements(textRun(content))),table(tableRows(tableCells(startIndex,endIndex,content(paragraph(elements(textRun(content)))))))))';
39
+ /** Smart chip fields (date, person, richLink) in body + table cells. */
40
+ export const SMART_CHIP_BODY_FIELDS = 'body(content(paragraph(elements(startIndex,endIndex,dateElement(dateId,dateElementProperties),richLink(richLinkId,richLinkProperties),person(personId,personProperties))),table(tableRows(tableCells(content(paragraph(elements(startIndex,endIndex,dateElement(dateId,dateElementProperties),richLink(richLinkId,richLinkProperties),person(personId,personProperties)))))))))';
41
+ /**
42
+ * Returns the correct fields string for a documents.get call.
43
+ * When tabId is set, wraps bodyFields in a tabs field mask.
44
+ * When tabId is absent, returns bodyFields directly (legacy body mode).
45
+ */
46
+ export function buildDocumentGetFields(bodyFields, tabId) {
47
+ return tabId ? buildTabsFieldMask(`documentTab(${bodyFields})`) : bodyFields;
48
+ }