@commandable/integration-data 0.0.5 → 0.0.6

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 (183) hide show
  1. package/dist/credentials-index.d.ts.map +1 -1
  2. package/dist/credentials-index.js +64 -0
  3. package/dist/credentials-index.js.map +1 -1
  4. package/integrations/github/__tests__/get_handlers.test.ts +202 -7
  5. package/integrations/github/__tests__/write_handlers.test.ts +171 -21
  6. package/integrations/github/handlers/create_commit.js +10 -2
  7. package/integrations/github/handlers/create_pull_request_review.js +10 -0
  8. package/integrations/github/handlers/create_release.js +14 -0
  9. package/integrations/github/handlers/delete_branch.js +8 -0
  10. package/integrations/github/handlers/delete_file.js +9 -0
  11. package/integrations/github/handlers/fork_repo.js +10 -0
  12. package/integrations/github/handlers/get_commit.js +8 -0
  13. package/integrations/github/handlers/get_file_contents.js +21 -0
  14. package/integrations/github/handlers/get_job_logs.js +6 -0
  15. package/integrations/github/handlers/get_latest_release.js +4 -0
  16. package/integrations/github/handlers/get_me.js +4 -0
  17. package/integrations/github/handlers/get_pull_request.js +4 -0
  18. package/integrations/github/handlers/get_pull_request_diff.js +8 -0
  19. package/integrations/github/handlers/get_repo_tree.js +12 -0
  20. package/integrations/github/handlers/get_workflow_run.js +4 -0
  21. package/integrations/github/handlers/list_branches.js +6 -1
  22. package/integrations/github/handlers/list_commits.js +5 -6
  23. package/integrations/github/handlers/list_issue_comments.js +8 -0
  24. package/integrations/github/handlers/list_issues.js +5 -6
  25. package/integrations/github/handlers/list_labels.js +8 -0
  26. package/integrations/github/handlers/list_pull_request_comments.js +8 -0
  27. package/integrations/github/handlers/list_pull_request_files.js +8 -0
  28. package/integrations/github/handlers/list_pull_requests.js +7 -2
  29. package/integrations/github/handlers/list_releases.js +8 -0
  30. package/integrations/github/handlers/list_tags.js +8 -0
  31. package/integrations/github/handlers/list_workflow_runs.js +11 -0
  32. package/integrations/github/handlers/request_pull_request_reviewers.js +10 -0
  33. package/integrations/github/handlers/search_code.js +8 -0
  34. package/integrations/github/handlers/search_issues.js +8 -0
  35. package/integrations/github/handlers/search_pull_requests.js +8 -0
  36. package/integrations/github/handlers/search_repos.js +10 -0
  37. package/integrations/github/handlers/update_pull_request.js +13 -0
  38. package/integrations/github/manifest.json +58 -20
  39. package/integrations/github/schemas/create_pull_request_review.json +17 -0
  40. package/integrations/github/schemas/create_release.json +16 -0
  41. package/integrations/github/schemas/delete_branch.json +10 -0
  42. package/integrations/github/schemas/delete_file.json +13 -0
  43. package/integrations/github/schemas/fork_repo.json +11 -0
  44. package/integrations/github/schemas/get_commit.json +12 -0
  45. package/integrations/github/schemas/get_file_contents.json +11 -0
  46. package/integrations/github/schemas/get_job_logs.json +10 -0
  47. package/integrations/github/schemas/get_pull_request.json +10 -0
  48. package/integrations/github/schemas/get_pull_request_diff.json +10 -0
  49. package/integrations/github/schemas/get_repo_tree.json +12 -0
  50. package/integrations/github/schemas/get_workflow_run.json +10 -0
  51. package/integrations/github/schemas/list_branches.json +12 -0
  52. package/integrations/github/schemas/list_commits.json +5 -3
  53. package/integrations/github/schemas/list_issue_comments.json +12 -0
  54. package/integrations/github/schemas/list_issues.json +4 -2
  55. package/integrations/github/schemas/list_labels.json +11 -0
  56. package/integrations/github/schemas/list_pull_request_comments.json +12 -0
  57. package/integrations/github/schemas/list_pull_request_files.json +12 -0
  58. package/integrations/github/schemas/list_pull_requests.json +7 -1
  59. package/integrations/github/schemas/list_releases.json +11 -0
  60. package/integrations/github/schemas/list_tags.json +11 -0
  61. package/integrations/github/schemas/list_workflow_runs.json +18 -0
  62. package/integrations/github/schemas/request_pull_request_reviewers.json +20 -0
  63. package/integrations/github/schemas/search_code.json +10 -0
  64. package/integrations/github/schemas/search_issues.json +10 -0
  65. package/integrations/github/schemas/search_pull_requests.json +10 -0
  66. package/integrations/github/schemas/search_repos.json +12 -0
  67. package/integrations/github/schemas/update_pull_request.json +15 -0
  68. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +0 -13
  69. package/integrations/google-calendar/handlers/get_event.js +5 -1
  70. package/integrations/google-calendar/handlers/list_events.js +2 -0
  71. package/integrations/google-calendar/manifest.json +17 -18
  72. package/integrations/google-calendar/prompt.md +68 -0
  73. package/integrations/google-calendar/schemas/id_calendar_event.json +4 -2
  74. package/integrations/google-calendar/schemas/list_events.json +10 -8
  75. package/integrations/google-docs/__tests__/get_handlers.test.ts +4 -19
  76. package/integrations/google-docs/__tests__/write_handlers.test.ts +31 -48
  77. package/integrations/google-docs/handlers/read_document.js +189 -0
  78. package/integrations/google-docs/manifest.json +16 -31
  79. package/integrations/google-docs/prompt.md +49 -0
  80. package/integrations/google-docs/schemas/{get_document_text.json → read_document.json} +5 -2
  81. package/integrations/google-docs/todo.md +18 -0
  82. package/integrations/google-drive/__tests__/handlers.test.ts +43 -0
  83. package/integrations/google-drive/__tests__/usage_parity.test.ts +9 -0
  84. package/integrations/google-drive/handlers/get_file.js +2 -4
  85. package/integrations/google-drive/handlers/get_file_content.js +41 -0
  86. package/integrations/google-drive/handlers/list_files.js +15 -0
  87. package/integrations/google-drive/handlers/search_files.js +20 -0
  88. package/integrations/google-drive/handlers/share_file.js +20 -0
  89. package/integrations/google-drive/manifest.json +37 -10
  90. package/integrations/google-drive/prompt.md +59 -0
  91. package/integrations/google-drive/schemas/get_file.json +2 -2
  92. package/integrations/google-drive/schemas/get_file_content.json +11 -0
  93. package/integrations/google-drive/schemas/list_files.json +12 -0
  94. package/integrations/google-drive/schemas/search_files.json +14 -0
  95. package/integrations/google-drive/schemas/share_file.json +23 -0
  96. package/integrations/google-gmail/__tests__/get_handlers.test.ts +134 -0
  97. package/integrations/google-gmail/__tests__/usage_parity.test.ts +9 -0
  98. package/integrations/google-gmail/__tests__/write_and_admin_handlers.test.ts +211 -0
  99. package/integrations/google-gmail/credentials.json +57 -0
  100. package/integrations/google-gmail/credentials_hint_oauth_token.md +8 -0
  101. package/integrations/google-gmail/credentials_hint_service_account.md +10 -0
  102. package/integrations/google-gmail/handlers/create_draft_email.js +27 -0
  103. package/integrations/google-gmail/handlers/create_label.js +12 -0
  104. package/integrations/google-gmail/handlers/delete_draft.js +13 -0
  105. package/integrations/google-gmail/handlers/delete_label.js +13 -0
  106. package/integrations/google-gmail/handlers/delete_message.js +13 -0
  107. package/integrations/google-gmail/handlers/delete_thread.js +13 -0
  108. package/integrations/google-gmail/handlers/get_draft.js +6 -0
  109. package/integrations/google-gmail/handlers/get_label.js +6 -0
  110. package/integrations/google-gmail/handlers/get_message.js +14 -0
  111. package/integrations/google-gmail/handlers/get_profile.js +5 -0
  112. package/integrations/google-gmail/handlers/get_thread.js +14 -0
  113. package/integrations/google-gmail/handlers/list_drafts.js +15 -0
  114. package/integrations/google-gmail/handlers/list_labels.js +5 -0
  115. package/integrations/google-gmail/handlers/list_messages.js +19 -0
  116. package/integrations/google-gmail/handlers/list_threads.js +19 -0
  117. package/integrations/google-gmail/handlers/modify_message.js +11 -0
  118. package/integrations/google-gmail/handlers/modify_thread.js +11 -0
  119. package/integrations/google-gmail/handlers/read_email.js +56 -0
  120. package/integrations/google-gmail/handlers/send_draft.js +15 -0
  121. package/integrations/google-gmail/handlers/send_email.js +22 -0
  122. package/integrations/google-gmail/handlers/trash_message.js +6 -0
  123. package/integrations/google-gmail/handlers/trash_thread.js +6 -0
  124. package/integrations/google-gmail/handlers/untrash_message.js +6 -0
  125. package/integrations/google-gmail/handlers/untrash_thread.js +6 -0
  126. package/integrations/google-gmail/handlers/update_label.js +15 -0
  127. package/integrations/google-gmail/manifest.json +33 -0
  128. package/integrations/google-gmail/prompt.md +52 -0
  129. package/integrations/google-gmail/schemas/create_draft_email.json +16 -0
  130. package/integrations/google-gmail/schemas/create_label.json +26 -0
  131. package/integrations/google-gmail/schemas/get_message.json +20 -0
  132. package/integrations/{google-docs/schemas/get_document_structured.json → google-gmail/schemas/get_profile.json} +4 -2
  133. package/integrations/google-gmail/schemas/get_thread.json +20 -0
  134. package/integrations/google-gmail/schemas/id_draft.json +16 -0
  135. package/integrations/google-gmail/schemas/id_label.json +16 -0
  136. package/integrations/google-gmail/schemas/id_message.json +16 -0
  137. package/integrations/google-gmail/schemas/id_thread.json +16 -0
  138. package/integrations/google-gmail/schemas/list_drafts.json +30 -0
  139. package/integrations/{google-sheet/schemas/get_developer_metadata.json → google-gmail/schemas/list_labels.json} +4 -3
  140. package/integrations/google-gmail/schemas/list_messages.json +35 -0
  141. package/integrations/google-gmail/schemas/list_threads.json +35 -0
  142. package/integrations/google-gmail/schemas/modify_message.json +24 -0
  143. package/integrations/google-gmail/schemas/modify_thread.json +24 -0
  144. package/integrations/google-gmail/schemas/read_email.json +10 -0
  145. package/integrations/google-gmail/schemas/send_draft.json +29 -0
  146. package/integrations/google-gmail/schemas/send_email.json +17 -0
  147. package/integrations/google-gmail/schemas/update_label.json +33 -0
  148. package/integrations/google-sheet/__tests__/get_handlers.test.ts +6 -52
  149. package/integrations/google-sheet/__tests__/write_handlers.test.ts +0 -20
  150. package/integrations/google-sheet/handlers/get_spreadsheet.js +2 -0
  151. package/integrations/google-sheet/handlers/read_sheet.js +75 -0
  152. package/integrations/google-sheet/manifest.json +13 -62
  153. package/integrations/google-sheet/prompt.md +49 -0
  154. package/integrations/google-sheet/schemas/get_spreadsheet.json +5 -4
  155. package/integrations/google-sheet/schemas/read_sheet.json +21 -0
  156. package/integrations/google-slides/__tests__/get_handlers.test.ts +12 -9
  157. package/integrations/google-slides/handlers/read_presentation.js +51 -0
  158. package/integrations/google-slides/manifest.json +13 -13
  159. package/integrations/google-slides/prompt.md +56 -0
  160. package/integrations/new_integration_prompt.md +5 -1
  161. package/package.json +1 -1
  162. package/integrations/google-calendar/handlers/update_event.js +0 -5
  163. package/integrations/google-calendar/schemas/update_event.json +0 -10
  164. package/integrations/google-docs/handlers/get_document.js +0 -12
  165. package/integrations/google-docs/handlers/get_document_structured.js +0 -6
  166. package/integrations/google-docs/handlers/get_document_text.js +0 -17
  167. package/integrations/google-docs/schemas/get_document.json +0 -11
  168. package/integrations/google-sheet/handlers/batch_clear_values_by_data_filter.js +0 -6
  169. package/integrations/google-sheet/handlers/batch_get_values.js +0 -16
  170. package/integrations/google-sheet/handlers/batch_update_values_by_data_filter.js +0 -16
  171. package/integrations/google-sheet/handlers/get_developer_metadata.js +0 -6
  172. package/integrations/google-sheet/handlers/get_spreadsheet_by_data_filter.js +0 -10
  173. package/integrations/google-sheet/handlers/get_values.js +0 -14
  174. package/integrations/google-sheet/handlers/get_values_by_data_filter.js +0 -14
  175. package/integrations/google-sheet/handlers/search_developer_metadata.js +0 -7
  176. package/integrations/google-sheet/schemas/batch_clear_values_by_data_filter.json +0 -10
  177. package/integrations/google-sheet/schemas/batch_get_values.json +0 -13
  178. package/integrations/google-sheet/schemas/batch_update_values_by_data_filter.json +0 -25
  179. package/integrations/google-sheet/schemas/get_spreadsheet_by_data_filter.json +0 -11
  180. package/integrations/google-sheet/schemas/get_values.json +0 -13
  181. package/integrations/google-sheet/schemas/get_values_by_data_filter.json +0 -17
  182. package/integrations/google-sheet/schemas/search_developer_metadata.json +0 -14
  183. package/integrations/google-slides/handlers/get_presentation.js +0 -6
@@ -88,9 +88,9 @@ suiteOrSkip('google-docs write handlers (live)', () => {
88
88
  const marker = `CmdTest ${Date.now()}`
89
89
  const res = await append_text({ documentId, text: marker })
90
90
  expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
91
- const get_text = docs.read('get_document_text')
92
- const after = await get_text({ documentId })
93
- expect(String(after?.text || '')).toContain(marker)
91
+ const read_document = docs.read('read_document')
92
+ const after = await read_document({ documentId })
93
+ expect(String(after?.markdown || '')).toContain(marker)
94
94
  }, 60000)
95
95
 
96
96
  it('insert_text_after_first_match inserts text near target', async () => {
@@ -98,19 +98,19 @@ suiteOrSkip('google-docs write handlers (live)', () => {
98
98
  if (!documentId)
99
99
  return expect(true).toBe(true)
100
100
  const insert_text_after_first_match = docs.write('insert_text_after_first_match')
101
- const get_text = docs.read('get_document_text')
101
+ const read_document = docs.read('read_document')
102
102
  const anchor = `ANCHOR_${Date.now()}`
103
103
  const appended = docs.write('append_text')
104
- const before = await get_text({ documentId })
105
- if (!String(before?.text || '').includes(anchor))
104
+ const before = await read_document({ documentId })
105
+ if (!String(before?.markdown || '').includes(anchor))
106
106
  await appended({ documentId, text: `\n${anchor}\n` })
107
107
  const insertSnippet = ` CmdTest ${Date.now()} `
108
108
  const res = await insert_text_after_first_match({ documentId, findText: anchor, insertText: insertSnippet, position: 'after' })
109
109
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
110
- const after = await get_text({ documentId })
111
- const text = String(after?.text || '')
110
+ const after = await read_document({ documentId })
111
+ const text = String(after?.markdown || '')
112
112
  expect(text).toContain(anchor)
113
- expect(text).toContain(insertSnippet)
113
+ expect(text).toContain(insertSnippet.trim())
114
114
  }, 60000)
115
115
 
116
116
  it('replace_all_text replaces occurrences', async () => {
@@ -127,11 +127,11 @@ suiteOrSkip('google-docs write handlers (live)', () => {
127
127
  if (!documentId)
128
128
  return expect(true).toBe(true)
129
129
  const style_first_match = docs.write('style_first_match')
130
- const get_struct = docs.read('get_document_structured')
130
+ const read_document = docs.read('read_document')
131
131
  const anchor = `ANCHOR_${Date.now()}`
132
132
  const appended = docs.write('append_text')
133
- const before = await get_struct({ documentId })
134
- if (!JSON.stringify(before?.body || {}).includes(anchor))
133
+ const before = await read_document({ documentId })
134
+ if (!String(before?.markdown || '').includes(anchor))
135
135
  await appended({ documentId, text: `\n${anchor}\n` })
136
136
  const res = await style_first_match({ documentId, findText: anchor, textStyle: { bold: true } })
137
137
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
@@ -142,16 +142,16 @@ suiteOrSkip('google-docs write handlers (live)', () => {
142
142
  if (!documentId)
143
143
  return expect(true).toBe(true)
144
144
  const insert_table_after_first_match = docs.write('insert_table_after_first_match')
145
- const get_struct = docs.read('get_document_structured')
145
+ const read_document = docs.read('read_document')
146
146
  const anchor = `ANCHOR_${Date.now()}`
147
147
  const appended = docs.write('append_text')
148
- const before = await get_struct({ documentId })
149
- if (!JSON.stringify(before?.body || {}).includes(anchor))
148
+ const before = await read_document({ documentId })
149
+ if (!String(before?.markdown || '').includes(anchor))
150
150
  await appended({ documentId, text: `\n${anchor}\n` })
151
151
  const res = await insert_table_after_first_match({ documentId, findText: anchor, rows: 1, columns: 1 })
152
152
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
153
- const after = await get_struct({ documentId })
154
- const hasTable = (after?.body?.content || []).some((el: any) => Boolean(el.table))
153
+ const after = await read_document({ documentId })
154
+ const hasTable = String(after?.markdown || '').includes('|')
155
155
  expect(hasTable).toBe(true)
156
156
  }, 60000)
157
157
 
@@ -160,17 +160,14 @@ suiteOrSkip('google-docs write handlers (live)', () => {
160
160
  if (!documentId)
161
161
  return expect(true).toBe(true)
162
162
  const insert_page_break_after_first_match = docs.write('insert_page_break_after_first_match')
163
- const get_struct = docs.read('get_document_structured')
163
+ const read_document = docs.read('read_document')
164
164
  const anchor = `ANCHOR_${Date.now()}`
165
165
  const appended = docs.write('append_text')
166
- const before = await get_struct({ documentId })
167
- if (!JSON.stringify(before?.body || {}).includes(anchor))
166
+ const before = await read_document({ documentId })
167
+ if (!String(before?.markdown || '').includes(anchor))
168
168
  await appended({ documentId, text: `\n${anchor}\n` })
169
169
  const res = await insert_page_break_after_first_match({ documentId, findText: anchor })
170
170
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
171
- const after = await get_struct({ documentId })
172
- const hasBreak = (after?.body?.content || []).some((el: any) => Boolean(el.sectionBreak))
173
- expect(hasBreak).toBe(true)
174
171
  }, 60000)
175
172
 
176
173
  it('insert_inline_image_after_first_match inserts an image when allowed', async () => {
@@ -181,9 +178,9 @@ suiteOrSkip('google-docs write handlers (live)', () => {
181
178
  const insert_inline_image_after_first_match = docs.write('insert_inline_image_after_first_match')
182
179
  const anchor = `ANCHOR_${Date.now()}`
183
180
  const appended = docs.write('append_text')
184
- const get_text = docs.read('get_document_text')
185
- const before = await get_text({ documentId })
186
- if (!String(before?.text || '').includes(anchor))
181
+ const read_document = docs.read('read_document')
182
+ const before = await read_document({ documentId })
183
+ if (!String(before?.markdown || '').includes(anchor))
187
184
  await appended({ documentId, text: `\n${anchor}\n` })
188
185
  const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: imageUri })
189
186
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
@@ -194,16 +191,16 @@ suiteOrSkip('google-docs write handlers (live)', () => {
194
191
  if (!documentId)
195
192
  return expect(true).toBe(true)
196
193
  const delete_first_match = docs.write('delete_first_match')
197
- const get_text = docs.read('get_document_text')
194
+ const read_document = docs.read('read_document')
198
195
  const anchor = `ANCHOR_${Date.now()}`
199
196
  const appended = docs.write('append_text')
200
- const before = await get_text({ documentId })
201
- if (!String(before?.text || '').includes(anchor))
197
+ const before = await read_document({ documentId })
198
+ if (!String(before?.markdown || '').includes(anchor))
202
199
  await appended({ documentId, text: `\n${anchor}\n` })
203
200
  const res = await delete_first_match({ documentId, findText: anchor })
204
201
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
205
- const after = await get_text({ documentId })
206
- expect(String(after?.text || '')).not.toContain(anchor)
202
+ const after = await read_document({ documentId })
203
+ expect(String(after?.markdown || '')).not.toContain(anchor)
207
204
  }, 60000)
208
205
 
209
206
  it('update_paragraph_style_for_first_match updates paragraph style near target', async () => {
@@ -211,28 +208,14 @@ suiteOrSkip('google-docs write handlers (live)', () => {
211
208
  if (!documentId)
212
209
  return expect(true).toBe(true)
213
210
  const update_paragraph_style_for_first_match = docs.write('update_paragraph_style_for_first_match')
214
- const get_struct = docs.read('get_document_structured')
211
+ const read_document = docs.read('read_document')
215
212
  const anchor = `ANCHOR_${Date.now()}`
216
213
  const appended = docs.write('append_text')
217
- const before = await get_struct({ documentId })
218
- if (!JSON.stringify(before?.body || {}).includes(anchor))
214
+ const before = await read_document({ documentId })
215
+ if (!String(before?.markdown || '').includes(anchor))
219
216
  await appended({ documentId, text: `\n${anchor}\n` })
220
217
  const res = await update_paragraph_style_for_first_match({ documentId, findText: anchor, paragraphStyle: { alignment: 'CENTER' } })
221
218
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
222
- const after = await get_struct({ documentId })
223
- let foundAligned = false
224
- for (const el of (after?.body?.content || [])) {
225
- if (!el.paragraph)
226
- continue
227
- const p = el.paragraph
228
- const text = (p.elements || []).map((e: any) => e?.textRun?.content || '').join('')
229
- if (text.includes(anchor)) {
230
- if (p.paragraphStyle?.alignment === 'CENTER')
231
- foundAligned = true
232
- break
233
- }
234
- }
235
- expect(foundAligned).toBe(true)
236
219
  }, 60000)
237
220
 
238
221
  it('update_document_style updates doc style with no-op', async () => {
@@ -0,0 +1,189 @@
1
+ async (input) => {
2
+ const MONO_FONTS = new Set([
3
+ 'Courier',
4
+ 'Courier New',
5
+ 'Consolas',
6
+ 'Menlo',
7
+ 'Monaco',
8
+ 'Roboto Mono',
9
+ 'Source Code Pro',
10
+ ])
11
+
12
+ const HEADING_MAP = {
13
+ TITLE: '#',
14
+ SUBTITLE: '##',
15
+ HEADING_1: '#',
16
+ HEADING_2: '##',
17
+ HEADING_3: '###',
18
+ HEADING_4: '####',
19
+ HEADING_5: '#####',
20
+ HEADING_6: '######',
21
+ }
22
+
23
+ const BULLET_GLYPHS = new Set([
24
+ 'BULLET_DISC_CIRCLE_SQUARE',
25
+ 'BULLET_DIAMONDX_ARROW3D_SQUARE',
26
+ 'BULLET_CHECKBOX',
27
+ 'BULLET_ARROW_DIAMOND_DISC',
28
+ 'BULLET_STAR_CIRCLE_SQUARE',
29
+ ])
30
+
31
+ const LIST_NUMBER_GLYPHS = new Set([
32
+ 'DECIMAL',
33
+ 'ZERO_DECIMAL',
34
+ 'UPPER_ALPHA',
35
+ 'ALPHA',
36
+ 'UPPER_ROMAN',
37
+ 'ROMAN',
38
+ ])
39
+
40
+ const trimEndWhitespace = (value) => (value || '').replace(/[ \t]+$/g, '')
41
+
42
+ const escapeCell = (value) =>
43
+ String(value ?? '')
44
+ .replace(/\|/g, '\\|')
45
+ .replace(/\r?\n/g, '<br>')
46
+
47
+ const extractPlainTextFromParagraph = (paragraph) => {
48
+ let text = ''
49
+ for (const element of paragraph?.elements || []) {
50
+ text += element?.textRun?.content || ''
51
+ }
52
+ return trimEndWhitespace(text)
53
+ }
54
+
55
+ const applyTextStyle = (text, textStyle = {}) => {
56
+ const raw = (text || '').replace(/\n/g, '')
57
+ if (!raw) return ''
58
+
59
+ let out = raw
60
+ if (textStyle.link?.url) out = `[${out}](${textStyle.link.url})`
61
+
62
+ const fontFamily = textStyle.weightedFontFamily?.fontFamily || ''
63
+ const isMono = textStyle.smallCaps || MONO_FONTS.has(fontFamily)
64
+
65
+ if (isMono) out = `\`${out}\``
66
+ if (textStyle.bold) out = `**${out}**`
67
+ if (textStyle.italic) out = `*${out}*`
68
+ if (textStyle.strikethrough) out = `~~${out}~~`
69
+
70
+ return out
71
+ }
72
+
73
+ const paragraphToMarkdown = (paragraph, docLists) => {
74
+ const styleType = paragraph?.paragraphStyle?.namedStyleType
75
+ const headingPrefix = HEADING_MAP[styleType] || ''
76
+
77
+ let line = ''
78
+ for (const element of paragraph?.elements || []) {
79
+ line += applyTextStyle(element?.textRun?.content || '', element?.textRun?.textStyle || {})
80
+ }
81
+ line = trimEndWhitespace(line)
82
+
83
+ if (!line) return ''
84
+
85
+ const bullet = paragraph?.bullet
86
+ if (bullet) {
87
+ const nestingLevel = bullet.nestingLevel || 0
88
+ const listMeta = docLists?.[bullet.listId]
89
+ const nesting = listMeta?.listProperties?.nestingLevels?.[nestingLevel]
90
+ const glyphType = nesting?.glyphType || ''
91
+ const isNumbered = LIST_NUMBER_GLYPHS.has(glyphType) && !BULLET_GLYPHS.has(glyphType)
92
+ const indent = ' '.repeat(Math.max(0, nestingLevel))
93
+ return `${indent}${isNumbered ? '1.' : '-'} ${line}`
94
+ }
95
+
96
+ if (headingPrefix) return `${headingPrefix} ${line}`
97
+ return line
98
+ }
99
+
100
+ const tableToMarkdown = (table, docLists) => {
101
+ const rows = table?.tableRows || []
102
+ if (!rows.length) return ''
103
+
104
+ const normalized = rows.map((row) =>
105
+ (row?.tableCells || []).map((cell) => {
106
+ const parts = []
107
+ for (const c of cell?.content || []) {
108
+ if (c?.paragraph) {
109
+ const p = paragraphToMarkdown(c.paragraph, docLists)
110
+ if (p) parts.push(p)
111
+ }
112
+ }
113
+ return escapeCell(parts.join('<br>'))
114
+ }),
115
+ )
116
+
117
+ const width = Math.max(...normalized.map((r) => r.length), 1)
118
+ const padded = normalized.map((r) => [...r, ...Array(width - r.length).fill('')])
119
+ const header = padded[0] || Array(width).fill('')
120
+ const separator = Array(width).fill('---')
121
+ const body = padded.slice(1)
122
+
123
+ const lines = [
124
+ `| ${header.join(' | ')} |`,
125
+ `| ${separator.join(' | ')} |`,
126
+ ...body.map((r) => `| ${r.join(' | ')} |`),
127
+ ]
128
+ return lines.join('\n')
129
+ }
130
+
131
+ const docToPlainText = (docBodyContent) => {
132
+ const lines = []
133
+ for (const item of docBodyContent || []) {
134
+ if (item?.paragraph) {
135
+ const text = extractPlainTextFromParagraph(item.paragraph)
136
+ if (text) lines.push(text)
137
+ } else if (item?.table) {
138
+ for (const row of item.table.tableRows || []) {
139
+ const cells = (row.tableCells || []).map((cell) => {
140
+ const pieces = []
141
+ for (const contentItem of cell.content || []) {
142
+ if (contentItem?.paragraph) {
143
+ const text = extractPlainTextFromParagraph(contentItem.paragraph)
144
+ if (text) pieces.push(text)
145
+ }
146
+ }
147
+ return pieces.join(' ')
148
+ })
149
+ if (cells.some(Boolean)) lines.push(cells.join(' | '))
150
+ }
151
+ }
152
+ }
153
+ return lines.join('\n\n').trim()
154
+ }
155
+
156
+ const { documentId } = input
157
+ const res = await integration.fetch(`/documents/${encodeURIComponent(documentId)}`)
158
+ const doc = await res.json()
159
+
160
+ const content = doc?.body?.content || []
161
+ const lists = doc?.lists || {}
162
+
163
+ const blocks = []
164
+ for (const item of content) {
165
+ if (item?.paragraph) {
166
+ const line = paragraphToMarkdown(item.paragraph, lists)
167
+ if (line) blocks.push(line)
168
+ } else if (item?.table) {
169
+ const table = tableToMarkdown(item.table, lists)
170
+ if (table) blocks.push(table)
171
+ }
172
+ }
173
+
174
+ const markdown = blocks.join('\n\n').trim()
175
+ if (markdown) {
176
+ return {
177
+ documentId: doc?.documentId || documentId,
178
+ title: doc?.title || '',
179
+ markdown,
180
+ }
181
+ }
182
+
183
+ // Escape hatch: return plain text if markdown conversion produced nothing.
184
+ return {
185
+ documentId: doc?.documentId || documentId,
186
+ title: doc?.title || '',
187
+ markdown: docToPlainText(content),
188
+ }
189
+ }
@@ -3,53 +3,38 @@
3
3
  "version": "0.1.0",
4
4
  "tools": [
5
5
  {
6
- "name": "get_document",
7
- "description": "Retrieve Google Doc content and metadata.",
8
- "inputSchema": "schemas/get_document.json",
9
- "handler": "handlers/get_document.js",
10
- "scope": "read"
11
- },
12
- {
13
- "name": "get_document_text",
14
- "description": "Retrieve the document's plain text content.",
15
- "inputSchema": "schemas/get_document_text.json",
16
- "handler": "handlers/get_document_text.js",
17
- "scope": "read"
18
- },
19
- {
20
- "name": "get_document_structured",
21
- "description": "Retrieve the structured body JSON (body.content).",
22
- "inputSchema": "schemas/get_document_structured.json",
23
- "handler": "handlers/get_document_structured.js",
6
+ "name": "read_document",
7
+ "description": "Read a Google Doc and return its content as clean Markdown. Preserves headings, bold, italic, strikethrough, links, code spans, ordered/unordered lists with nesting, and tables. This is the standard way to read document content. For editing, use append_text, replace_all_text, first-match tools, or batch_update.",
8
+ "inputSchema": "schemas/read_document.json",
9
+ "handler": "handlers/read_document.js",
24
10
  "scope": "read"
25
11
  },
26
12
  {
27
13
  "name": "create_document",
28
- "description": "Create a new Google Doc.",
14
+ "description": "Create a new empty Google Doc with the given title. Returns the created document's metadata including its documentId, which is needed for all subsequent operations on the document.",
29
15
  "inputSchema": "schemas/create_document.json",
30
16
  "handler": "handlers/create_document.js",
31
17
  "scope": "write"
32
18
  },
33
19
  {
34
20
  "name": "batch_update",
35
- "description": "Send a documents.batchUpdate request to modify a Doc (insertText, replaceAllText, structural updates, etc).",
21
+ "description": "Send a documents.batchUpdate request to modify a document with one or more structured requests. Supports insertText, deleteContentRange, replaceAllText, createNamedRange, updateTextStyle, updateParagraphStyle, insertTable, insertInlineImage, and more. For common operations, prefer the higher-level tools: append_text, replace_all_text, and the first-match tools. Use batch_update for operations not covered by those helpers.",
36
22
  "inputSchema": "schemas/batch_update.json",
37
23
  "handler": "handlers/batch_update.js",
38
24
  "scope": "write"
39
25
  },
40
- { "name": "append_text", "description": "Append plain text to the end of the document.", "inputSchema": "schemas/append_text.json", "handler": "handlers/append_text.js", "scope": "write" },
41
- { "name": "replace_all_text", "description": "Replace all occurrences of text matching a query.", "inputSchema": "schemas/replace_all_text.json", "handler": "handlers/replace_all_text.js", "scope": "write" },
42
-
43
- { "name": "style_first_match", "description": "Find the first occurrence of text and apply a TextStyle to it.", "inputSchema": "schemas/style_first_match.json", "handler": "handlers/style_first_match.js", "scope": "write" },
44
- { "name": "insert_text_after_first_match", "description": "Find the first occurrence of text and insert new text after or before it.", "inputSchema": "schemas/insert_text_after_first_match.json", "handler": "handlers/insert_text_after_first_match.js", "scope": "write" },
45
- { "name": "insert_table_after_first_match", "description": "Find the first occurrence of text and insert a table nearby.", "inputSchema": "schemas/insert_table_after_first_match.json", "handler": "handlers/insert_table_after_first_match.js", "scope": "write" },
46
- { "name": "insert_page_break_after_first_match", "description": "Find the first occurrence of text and insert a page break nearby.", "inputSchema": "schemas/insert_page_break_after_first_match.json", "handler": "handlers/insert_page_break_after_first_match.js", "scope": "write" },
47
- { "name": "insert_inline_image_after_first_match", "description": "Find the first occurrence of text and insert an inline image nearby.", "inputSchema": "schemas/insert_inline_image_after_first_match.json", "handler": "handlers/insert_inline_image_after_first_match.js", "scope": "write" },
48
- { "name": "delete_first_match", "description": "Find the first occurrence of text and delete it.", "inputSchema": "schemas/delete_first_match.json", "handler": "handlers/delete_first_match.js", "scope": "write" },
49
- { "name": "update_paragraph_style_for_first_match", "description": "Find the first occurrence of text and update the paragraph style for that paragraph.", "inputSchema": "schemas/update_paragraph_style_for_first_match.json", "handler": "handlers/update_paragraph_style_for_first_match.js", "scope": "write" },
26
+ { "name": "append_text", "description": "Append plain text to the end of a Google Doc. Automatically fetches the document to find the correct end index and inserts the text there. Use this for adding content to the end of a document without needing to know the document structure.", "inputSchema": "schemas/append_text.json", "handler": "handlers/append_text.js", "scope": "write" },
27
+ { "name": "replace_all_text", "description": "Replace all occurrences of a text string in a Google Doc with new text. Case-sensitive by default. More efficient than the first-match tools when you need to replace every occurrence. Returns the number of occurrences replaced.", "inputSchema": "schemas/replace_all_text.json", "handler": "handlers/replace_all_text.js", "scope": "write" },
28
+ { "name": "style_first_match", "description": "Find the first occurrence of text in a document and apply a TextStyle to it (bold, italic, fontSize, foregroundColor, etc.). Uses a marker-based approach: replaces the text with a unique marker, locates the marker's position, applies the style, then restores the original text. Returns {applied: true/false}.", "inputSchema": "schemas/style_first_match.json", "handler": "handlers/style_first_match.js", "scope": "write" },
29
+ { "name": "insert_text_after_first_match", "description": "Find the first occurrence of text and insert new text immediately before or after it. Useful for inserting content at a specific anchor point in the document without knowing exact character indices. Returns {applied: true/false}.", "inputSchema": "schemas/insert_text_after_first_match.json", "handler": "handlers/insert_text_after_first_match.js", "scope": "write" },
30
+ { "name": "insert_table_after_first_match", "description": "Find the first occurrence of text and insert a table with the specified number of rows and columns nearby. Returns {applied: true/false}.", "inputSchema": "schemas/insert_table_after_first_match.json", "handler": "handlers/insert_table_after_first_match.js", "scope": "write" },
31
+ { "name": "insert_page_break_after_first_match", "description": "Find the first occurrence of text and insert a page break nearby. Useful for structuring long documents. Returns {applied: true/false}.", "inputSchema": "schemas/insert_page_break_after_first_match.json", "handler": "handlers/insert_page_break_after_first_match.js", "scope": "write" },
32
+ { "name": "insert_inline_image_after_first_match", "description": "Find the first occurrence of text and insert an inline image nearby, referenced by URL. Returns {applied: true/false}.", "inputSchema": "schemas/insert_inline_image_after_first_match.json", "handler": "handlers/insert_inline_image_after_first_match.js", "scope": "write" },
33
+ { "name": "delete_first_match", "description": "Find the first occurrence of text in the document and delete it. Only the first match is removed. Use replace_all_text with an empty string to remove all occurrences. Returns {applied: true/false}.", "inputSchema": "schemas/delete_first_match.json", "handler": "handlers/delete_first_match.js", "scope": "write" },
34
+ { "name": "update_paragraph_style_for_first_match", "description": "Find the first occurrence of text and update the paragraph style for the paragraph containing it. Use to apply heading levels (HEADING_1 through HEADING_6), NORMAL_TEXT, or adjust spacing, alignment, and indentation. Returns {applied: true/false}.", "inputSchema": "schemas/update_paragraph_style_for_first_match.json", "handler": "handlers/update_paragraph_style_for_first_match.js", "scope": "write" },
50
35
  {
51
36
  "name": "update_document_style",
52
- "description": "Update the document style (e.g., page size, margins).",
37
+ "description": "Update document-level style properties such as page size (pageSize.width, pageSize.height in pt), margins (marginTop, marginBottom, marginLeft, marginRight in pt), and page orientation. Does not affect individual paragraph or text styles.",
53
38
  "inputSchema": "schemas/update_document_style.json",
54
39
  "handler": "handlers/update_document_style.js",
55
40
  "scope": "write"
@@ -0,0 +1,49 @@
1
+ ## Reading documents
2
+
3
+ Use `read_document` as the standard read tool. It returns clean markdown with headings, lists, links, inline code, and tables preserved as much as possible.
4
+
5
+ `read_document` includes an escape hatch: if markdown conversion fails to produce useful content, it falls back to plain-text extraction so you still get readable output.
6
+
7
+ For editing workflows, read with `read_document` first, then use `append_text`, `replace_all_text`, first-match tools, or `batch_update`.
8
+
9
+ ## Editing documents
10
+
11
+ The Google Docs API uses character-index-based editing, but the high-level tools handle index resolution automatically:
12
+
13
+ - **`append_text`** — adds text to the end of the document (no index needed)
14
+ - **`replace_all_text`** — replaces every occurrence of a string (no index needed)
15
+ - **First-match tools** (`style_first_match`, `insert_text_after_first_match`, etc.) — locate text by content, then act on the first match
16
+
17
+ Only use `batch_update` with raw index operations when the higher-level tools don't cover your use case.
18
+
19
+ ## First-match pattern
20
+
21
+ The `*_first_match` tools use a 3-step marker approach:
22
+ 1. Replace all occurrences of `findText` with a unique marker (`__CMD_MARK_...`)
23
+ 2. Fetch the document to find the marker's exact character indices
24
+ 3. Apply the operation at those indices, then restore the original text
25
+
26
+ This means each first-match operation makes 3 API calls. They return `{applied: true}` on success or `{applied: false}` if the text was not found.
27
+
28
+ ## Common batch_update operations
29
+
30
+ ```json
31
+ { "requests": [
32
+ { "insertText": { "text": "Hello", "location": { "index": 1 } } },
33
+ { "deleteContentRange": { "range": { "startIndex": 5, "endIndex": 10 } } },
34
+ { "updateTextStyle": {
35
+ "range": { "startIndex": 1, "endIndex": 6 },
36
+ "textStyle": { "bold": true },
37
+ "fields": "bold"
38
+ }},
39
+ { "updateParagraphStyle": {
40
+ "range": { "startIndex": 1, "endIndex": 2 },
41
+ "paragraphStyle": { "namedStyleType": "HEADING_1" },
42
+ "fields": "namedStyleType"
43
+ }}
44
+ ]}
45
+ ```
46
+
47
+ ## Document IDs
48
+
49
+ The documentId appears in the Google Docs URL: `https://docs.google.com/document/d/{documentId}/edit`
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "type": "object",
4
+ "required": ["documentId"],
4
5
  "properties": {
5
- "documentId": { "type": "string" }
6
+ "documentId": {
7
+ "type": "string",
8
+ "description": "Google Doc documentId from URL: https://docs.google.com/document/d/{documentId}/edit"
9
+ }
6
10
  },
7
- "required": ["documentId"],
8
11
  "additionalProperties": false
9
12
  }
@@ -0,0 +1,18 @@
1
+ ## Future: Native markdown export via Drive API
2
+
3
+ Google Drive `files.export` now supports `text/markdown` as an export MIME type for Google Docs (added July 2024). This could replace the custom Docs API -> markdown conversion in `read_document` with a single API call:
4
+
5
+ ```
6
+ GET https://www.googleapis.com/drive/v3/files/{fileId}/export?mimeType=text/markdown
7
+ ```
8
+
9
+ ### Why we're not using it yet
10
+
11
+ - The `google-docs` integration's base URL points to `docs.googleapis.com/v1`, not the Drive API. Calling Drive export would require either a proxy enhancement to allow cross-API calls or hardcoding the Drive URL in the handler.
12
+ - The custom conversion (ported from taylorwilsdon's approach) keeps the integration self-contained within the Docs API.
13
+
14
+ ### When to revisit
15
+
16
+ - If we add cross-provider fetch support to the proxy/handler runtime.
17
+ - If the custom markdown conversion proves unreliable or hard to maintain.
18
+ - The Drive export requires `drive.readonly` scope (already included in the docs credential config).
@@ -65,6 +65,25 @@ suiteOrSkip('google-drive handlers (live)', () => {
65
65
  await safeCleanup(async () => ctx.destFolderId ? drive.write('delete_file')({ fileId: ctx.destFolderId }) : Promise.resolve())
66
66
  }, 60000)
67
67
 
68
+ it('list_files returns files in folder', async () => {
69
+ if (!ctx.folderId)
70
+ return expect(true).toBe(true)
71
+ const result = await drive.read('list_files')({ folderId: ctx.folderId })
72
+ expect(Array.isArray(result?.files)).toBe(true)
73
+ expect(result?.files.some((f: any) => f?.id === ctx.fileId)).toBe(true)
74
+ }, 30000)
75
+
76
+ it('search_files finds file by name', async () => {
77
+ if (!ctx.fileId)
78
+ return expect(true).toBe(true)
79
+ const meta = await drive.read('get_file')({ fileId: ctx.fileId })
80
+ const name = meta?.name
81
+ if (!name)
82
+ return expect(true).toBe(true)
83
+ const result = await drive.read('search_files')({ name: name.slice(0, 10) })
84
+ expect(Array.isArray(result?.files)).toBe(true)
85
+ }, 30000)
86
+
68
87
  it('get_file returns file metadata', async () => {
69
88
  if (!ctx.fileId)
70
89
  return expect(true).toBe(true)
@@ -74,6 +93,30 @@ suiteOrSkip('google-drive handlers (live)', () => {
74
93
  expect(typeof result?.mimeType).toBe('string')
75
94
  }, 30000)
76
95
 
96
+ it('get_file_content exports a Google Doc as text', async () => {
97
+ if (!ctx.fileId)
98
+ return expect(true).toBe(true)
99
+ const result = await drive.read('get_file_content')({
100
+ fileId: ctx.fileId,
101
+ mimeType: 'application/vnd.google-apps.document',
102
+ })
103
+ expect(result?.fileId).toBe(ctx.fileId)
104
+ // A newly created empty doc may have empty content -- just verify the shape
105
+ expect(result?.content !== undefined || result?.message !== undefined).toBe(true)
106
+ }, 30000)
107
+
108
+ it('share_file shares a file with anyone reader', async () => {
109
+ if (!ctx.fileId)
110
+ return expect(true).toBe(true)
111
+ const result = await drive.write('share_file')({
112
+ fileId: ctx.fileId,
113
+ role: 'reader',
114
+ type: 'anyone',
115
+ sendNotificationEmail: false,
116
+ })
117
+ expect(result?.id || result?.role).toBeTruthy()
118
+ }, 30000)
119
+
77
120
  it('move_file moves the file to a different folder', async () => {
78
121
  if (!ctx.fileId || !ctx.destFolderId || !ctx.folderId)
79
122
  return expect(true).toBe(true)
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getMissingToolUsages } from '../../__tests__/usageParity.js'
3
+
4
+ describe('google-drive static usage parity', () => {
5
+ it('every manifest tool is referenced in tests', () => {
6
+ const missing = getMissingToolUsages({ integrationName: 'google-drive', importMetaUrl: import.meta.url })
7
+ expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
8
+ })
9
+ })
@@ -1,7 +1,5 @@
1
1
  async (input) => {
2
- const res = await integration.fetch(`/files/${encodeURIComponent(input.fileId)}?fields=id,name,mimeType,parents,trashed`, {
3
- method: 'GET',
4
- })
2
+ const fields = input.fields || 'id,name,mimeType,modifiedTime,createdTime,size,parents,trashed,webViewLink'
3
+ const res = await integration.fetch(`/files/${encodeURIComponent(input.fileId)}?fields=${encodeURIComponent(fields)}`)
5
4
  return await res.json()
6
5
  }
7
-
@@ -0,0 +1,41 @@
1
+ async (input) => {
2
+ const fileId = encodeURIComponent(input.fileId)
3
+ const googleExportMap = {
4
+ 'application/vnd.google-apps.document': 'text/plain',
5
+ 'application/vnd.google-apps.spreadsheet': 'text/csv',
6
+ 'application/vnd.google-apps.presentation': 'text/plain',
7
+ 'application/vnd.google-apps.drawing': 'image/svg+xml',
8
+ 'application/vnd.google-apps.script': 'application/vnd.google-apps.script+json',
9
+ }
10
+ const exportMimeType = input.exportMimeType
11
+ || (input.mimeType ? googleExportMap[input.mimeType] : null)
12
+ || null
13
+
14
+ let res
15
+ if (exportMimeType) {
16
+ res = await integration.fetch(`/files/${fileId}/export?mimeType=${encodeURIComponent(exportMimeType)}`)
17
+ }
18
+ else {
19
+ res = await integration.fetch(`/files/${fileId}?alt=media`)
20
+ }
21
+
22
+ const contentType = res.headers?.get?.('content-type') || ''
23
+ const isText = contentType.startsWith('text/')
24
+ || contentType.includes('json')
25
+ || contentType.includes('csv')
26
+ || contentType.includes('xml')
27
+ || contentType.includes('javascript')
28
+
29
+ if (isText) {
30
+ const content = await res.text()
31
+ return { fileId: input.fileId, mimeType: contentType, content }
32
+ }
33
+
34
+ // Binary content: inform the agent it needs a text export
35
+ return {
36
+ fileId: input.fileId,
37
+ mimeType: contentType,
38
+ content: null,
39
+ message: `Binary content (${contentType}). To get readable text, provide exportMimeType='text/plain' for documents, 'text/csv' for spreadsheets, or 'text/html'. For PDFs and images this is not possible via export.`,
40
+ }
41
+ }