@commandable/integration-data 0.0.1 → 0.0.5

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 (83) hide show
  1. package/dist/credentials-index.d.ts +4 -21
  2. package/dist/credentials-index.d.ts.map +1 -1
  3. package/dist/credentials-index.js +407 -215
  4. package/dist/credentials-index.js.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/loader.d.ts +38 -2
  10. package/dist/loader.d.ts.map +1 -1
  11. package/dist/loader.js +70 -16
  12. package/dist/loader.js.map +1 -1
  13. package/integrations/__tests__/liveHarness.ts +84 -0
  14. package/integrations/__tests__/usageParity.ts +54 -0
  15. package/integrations/airtable/__tests__/get_handlers.test.ts +43 -31
  16. package/integrations/airtable/__tests__/usage_parity.test.ts +3 -29
  17. package/integrations/airtable/__tests__/write_and_admin_handlers.test.ts +20 -17
  18. package/integrations/airtable/credentials.json +21 -16
  19. package/integrations/github/__tests__/get_handlers.test.ts +101 -108
  20. package/integrations/github/__tests__/usage_parity.test.ts +15 -27
  21. package/integrations/github/__tests__/write_handlers.test.ts +223 -306
  22. package/integrations/github/credentials.json +40 -15
  23. package/integrations/github/credentials_hint_classic_pat.md +8 -0
  24. package/integrations/github/credentials_hint_fine_grained_pat.md +9 -0
  25. package/integrations/github/handlers/create_commit.js +2 -17
  26. package/integrations/github/manifest.json +2 -2
  27. package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
  28. package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
  29. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
  30. package/integrations/google-calendar/credentials.json +50 -29
  31. package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
  32. package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
  33. package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
  34. package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
  35. package/integrations/google-docs/__tests__/write_handlers.test.ts +251 -245
  36. package/integrations/google-docs/credentials.json +50 -29
  37. package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
  38. package/integrations/google-docs/credentials_hint_service_account.md +10 -0
  39. package/integrations/google-docs/handlers/insert_inline_image_after_first_match.js +1 -1
  40. package/integrations/google-docs/schemas/insert_inline_image_after_first_match.json +0 -1
  41. package/integrations/google-drive/__tests__/handlers.test.ts +102 -0
  42. package/integrations/google-drive/credentials.json +57 -0
  43. package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
  44. package/integrations/google-drive/credentials_hint_service_account.md +10 -0
  45. package/integrations/google-drive/handlers/create_file.js +15 -0
  46. package/integrations/google-drive/handlers/create_folder.js +15 -0
  47. package/integrations/google-drive/handlers/delete_file.js +14 -0
  48. package/integrations/google-drive/handlers/get_file.js +7 -0
  49. package/integrations/google-drive/handlers/move_file.js +12 -0
  50. package/integrations/google-drive/manifest.json +42 -0
  51. package/integrations/google-drive/schemas/create_file.json +12 -0
  52. package/integrations/google-drive/schemas/create_folder.json +11 -0
  53. package/integrations/google-drive/schemas/delete_file.json +10 -0
  54. package/integrations/google-drive/schemas/get_file.json +10 -0
  55. package/integrations/google-drive/schemas/move_file.json +12 -0
  56. package/integrations/google-sheet/__tests__/get_handlers.test.ts +48 -55
  57. package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
  58. package/integrations/google-sheet/__tests__/write_handlers.test.ts +65 -63
  59. package/integrations/google-sheet/credentials.json +50 -29
  60. package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
  61. package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
  62. package/integrations/google-slides/__tests__/get_handlers.test.ts +38 -36
  63. package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
  64. package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -59
  65. package/integrations/google-slides/credentials.json +50 -29
  66. package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
  67. package/integrations/google-slides/credentials_hint_service_account.md +10 -0
  68. package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
  69. package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
  70. package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
  71. package/integrations/notion/credentials.json +22 -17
  72. package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
  73. package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
  74. package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
  75. package/integrations/trello/credentials.json +26 -21
  76. package/integrations/trello/handlers/close_board.js +6 -0
  77. package/integrations/trello/handlers/create_board.js +11 -0
  78. package/integrations/trello/handlers/delete_board.js +13 -0
  79. package/integrations/trello/manifest.json +21 -0
  80. package/integrations/trello/schemas/close_board.json +10 -0
  81. package/integrations/trello/schemas/create_board.json +12 -0
  82. package/integrations/trello/schemas/delete_board.json +10 -0
  83. package/package.json +1 -1
@@ -1,249 +1,255 @@
1
- import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
4
-
5
- // LIVE Google Docs write tests using managed OAuth
6
- // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GDOCS_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-docs')
10
- // - GDOCS_TEST_DOCUMENT_ID (target document ID with write access) OR GDOCS_ALLOW_CREATE to create
11
-
12
- interface Ctx {
13
- documentId?: string
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
2
+ import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, safeCleanup } from '../../__tests__/liveHarness.js'
3
+
4
+ // LIVE Google Docs write tests -- runs once per available credential variant.
5
+ // Required env vars (at least one):
6
+ // - GOOGLE_SERVICE_ACCOUNT_JSON (service_account variant)
7
+ // - GOOGLE_TOKEN (oauth_token variant)
8
+
9
+ const env = process.env as Record<string, string | undefined>
10
+
11
+ interface VariantConfig {
12
+ key: string
13
+ credentials: () => Record<string, string>
14
14
  }
15
15
 
16
- const env = process.env as Record<string, string>
17
- const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
18
- const suite = hasEnv(
19
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
20
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
21
- 'GDOCS_TEST_CONNECTION_ID',
22
- )
23
- ? describe
24
- : describe.skip
25
-
26
- suite('google-docs write handlers (live)', () => {
27
- const ctx: Ctx = {}
28
- let buildWriteHandler: (name: string) => ((input: any) => Promise<any>)
29
- let buildReadHandler: (name: string) => ((input: any) => Promise<any>)
30
-
31
- beforeAll(async () => {
32
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GDOCS_TEST_CONNECTION_ID, GDOCS_TEST_DOCUMENT_ID } = env
33
-
34
- const proxy = new IntegrationProxy({
35
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
36
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
16
+ const variants: VariantConfig[] = [
17
+ {
18
+ key: 'service_account',
19
+ credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '', subject: env.GOOGLE_IMPERSONATE_SUBJECT || '' }),
20
+ },
21
+ {
22
+ key: 'oauth_token',
23
+ credentials: () => ({ token: env.GOOGLE_TOKEN || '' }),
24
+ },
25
+ ].filter(v => Object.values(v.credentials()).some(val => val.trim().length > 0))
26
+
27
+ const suiteOrSkip = variants.length > 0 ? describe : describe.skip
28
+
29
+ suiteOrSkip('google-docs write handlers (live)', () => {
30
+ for (const variant of variants) {
31
+ describe(`variant: ${variant.key}`, () => {
32
+ const ctx: { documentId?: string, folderId?: string } = {}
33
+ let docs: ReturnType<typeof createToolbox>
34
+ let drive: ReturnType<typeof createToolbox>
35
+
36
+ beforeAll(async () => {
37
+ const credentialStore = createCredentialStore(async () => variant.credentials())
38
+ const proxy = createProxy(credentialStore)
39
+ docs = createToolbox(
40
+ 'google-docs',
41
+ proxy,
42
+ createIntegrationNode('google-docs', { label: 'Google Docs', credentialId: 'google-docs-creds', credentialVariant: variant.key }),
43
+ variant.key,
44
+ )
45
+ drive = createToolbox(
46
+ 'google-drive',
47
+ proxy,
48
+ createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds', credentialVariant: variant.key }),
49
+ variant.key,
50
+ )
51
+
52
+ const folder = await drive.write('create_folder')({
53
+ name: `CmdTest Docs Write ${Date.now()}`,
54
+ })
55
+ ctx.folderId = folder?.id
56
+ expect(ctx.folderId).toBeTruthy()
57
+
58
+ const doc = await drive.write('create_file')({
59
+ name: `CmdTest Doc ${Date.now()}`,
60
+ mimeType: 'application/vnd.google-apps.document',
61
+ parentId: ctx.folderId,
62
+ })
63
+ ctx.documentId = doc?.id
64
+ expect(ctx.documentId).toBeTruthy()
65
+ }, 60000)
66
+
67
+ afterAll(async () => {
68
+ await safeCleanup(async () => ctx.documentId ? drive.write('delete_file')({ fileId: ctx.documentId }) : Promise.resolve())
69
+ await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
70
+ }, 60000)
71
+
72
+ it('batch_update can perform a trivial replaceAllText no-op', async () => {
73
+ const documentId = ctx.documentId
74
+ if (!documentId)
75
+ return expect(true).toBe(true)
76
+ const batch_update = docs.write('batch_update')
77
+ const res = await batch_update({ documentId, requests: [
78
+ { replaceAllText: { containsText: { text: '___unlikely___', matchCase: true }, replaceText: '___unlikely___' } },
79
+ ] })
80
+ expect(Array.isArray(res?.replies) || res?.documentId).toBeTruthy()
81
+ }, 60000)
82
+
83
+ it('append_text appends content', async () => {
84
+ const documentId = ctx.documentId
85
+ if (!documentId)
86
+ return expect(true).toBe(true)
87
+ const append_text = docs.write('append_text')
88
+ const marker = `CmdTest ${Date.now()}`
89
+ const res = await append_text({ documentId, text: marker })
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)
94
+ }, 60000)
95
+
96
+ it('insert_text_after_first_match inserts text near target', async () => {
97
+ const documentId = ctx.documentId
98
+ if (!documentId)
99
+ return expect(true).toBe(true)
100
+ const insert_text_after_first_match = docs.write('insert_text_after_first_match')
101
+ const get_text = docs.read('get_document_text')
102
+ const anchor = `ANCHOR_${Date.now()}`
103
+ const appended = docs.write('append_text')
104
+ const before = await get_text({ documentId })
105
+ if (!String(before?.text || '').includes(anchor))
106
+ await appended({ documentId, text: `\n${anchor}\n` })
107
+ const insertSnippet = ` CmdTest ${Date.now()} `
108
+ const res = await insert_text_after_first_match({ documentId, findText: anchor, insertText: insertSnippet, position: 'after' })
109
+ expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
110
+ const after = await get_text({ documentId })
111
+ const text = String(after?.text || '')
112
+ expect(text).toContain(anchor)
113
+ expect(text).toContain(insertSnippet)
114
+ }, 60000)
115
+
116
+ it('replace_all_text replaces occurrences', async () => {
117
+ const documentId = ctx.documentId
118
+ if (!documentId)
119
+ return expect(true).toBe(true)
120
+ const replace_all_text = docs.write('replace_all_text')
121
+ const res = await replace_all_text({ documentId, findText: '___unlikely___', replaceText: '___unlikely___', matchCase: true })
122
+ expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
123
+ }, 60000)
124
+
125
+ it('style_first_match applies style to first match', async () => {
126
+ const documentId = ctx.documentId
127
+ if (!documentId)
128
+ return expect(true).toBe(true)
129
+ const style_first_match = docs.write('style_first_match')
130
+ const get_struct = docs.read('get_document_structured')
131
+ const anchor = `ANCHOR_${Date.now()}`
132
+ const appended = docs.write('append_text')
133
+ const before = await get_struct({ documentId })
134
+ if (!JSON.stringify(before?.body || {}).includes(anchor))
135
+ await appended({ documentId, text: `\n${anchor}\n` })
136
+ const res = await style_first_match({ documentId, findText: anchor, textStyle: { bold: true } })
137
+ expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
138
+ }, 60000)
139
+
140
+ it('insert_table_after_first_match inserts a table near target', async () => {
141
+ const documentId = ctx.documentId
142
+ if (!documentId)
143
+ return expect(true).toBe(true)
144
+ const insert_table_after_first_match = docs.write('insert_table_after_first_match')
145
+ const get_struct = docs.read('get_document_structured')
146
+ const anchor = `ANCHOR_${Date.now()}`
147
+ const appended = docs.write('append_text')
148
+ const before = await get_struct({ documentId })
149
+ if (!JSON.stringify(before?.body || {}).includes(anchor))
150
+ await appended({ documentId, text: `\n${anchor}\n` })
151
+ const res = await insert_table_after_first_match({ documentId, findText: anchor, rows: 1, columns: 1 })
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))
155
+ expect(hasTable).toBe(true)
156
+ }, 60000)
157
+
158
+ it('insert_page_break_after_first_match inserts a break near target', async () => {
159
+ const documentId = ctx.documentId
160
+ if (!documentId)
161
+ return expect(true).toBe(true)
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')
164
+ const anchor = `ANCHOR_${Date.now()}`
165
+ const appended = docs.write('append_text')
166
+ const before = await get_struct({ documentId })
167
+ if (!JSON.stringify(before?.body || {}).includes(anchor))
168
+ await appended({ documentId, text: `\n${anchor}\n` })
169
+ const res = await insert_page_break_after_first_match({ documentId, findText: anchor })
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
+ }, 60000)
175
+
176
+ it('insert_inline_image_after_first_match inserts an image when allowed', async () => {
177
+ if (!ctx.documentId)
178
+ return expect(true).toBe(true)
179
+ const documentId = ctx.documentId
180
+ const imageUri = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
181
+ const insert_inline_image_after_first_match = docs.write('insert_inline_image_after_first_match')
182
+ const anchor = `ANCHOR_${Date.now()}`
183
+ 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))
187
+ await appended({ documentId, text: `\n${anchor}\n` })
188
+ const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: imageUri })
189
+ expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
190
+ }, 60000)
191
+
192
+ it('delete_first_match deletes a small span (no-op ok)', async () => {
193
+ const documentId = ctx.documentId
194
+ if (!documentId)
195
+ return expect(true).toBe(true)
196
+ const delete_first_match = docs.write('delete_first_match')
197
+ const get_text = docs.read('get_document_text')
198
+ const anchor = `ANCHOR_${Date.now()}`
199
+ const appended = docs.write('append_text')
200
+ const before = await get_text({ documentId })
201
+ if (!String(before?.text || '').includes(anchor))
202
+ await appended({ documentId, text: `\n${anchor}\n` })
203
+ const res = await delete_first_match({ documentId, findText: anchor })
204
+ expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
205
+ const after = await get_text({ documentId })
206
+ expect(String(after?.text || '')).not.toContain(anchor)
207
+ }, 60000)
208
+
209
+ it('update_paragraph_style_for_first_match updates paragraph style near target', async () => {
210
+ const documentId = ctx.documentId
211
+ if (!documentId)
212
+ return expect(true).toBe(true)
213
+ const update_paragraph_style_for_first_match = docs.write('update_paragraph_style_for_first_match')
214
+ const get_struct = docs.read('get_document_structured')
215
+ const anchor = `ANCHOR_${Date.now()}`
216
+ const appended = docs.write('append_text')
217
+ const before = await get_struct({ documentId })
218
+ if (!JSON.stringify(before?.body || {}).includes(anchor))
219
+ await appended({ documentId, text: `\n${anchor}\n` })
220
+ const res = await update_paragraph_style_for_first_match({ documentId, findText: anchor, paragraphStyle: { alignment: 'CENTER' } })
221
+ 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
+ }, 60000)
237
+
238
+ it('update_document_style updates doc style with no-op', async () => {
239
+ const documentId = ctx.documentId
240
+ if (!documentId)
241
+ return expect(true).toBe(true)
242
+ const update_document_style = docs.write('update_document_style')
243
+ const res = await update_document_style({ documentId, documentStyle: { useFirstPageHeaderFooter: false } })
244
+ expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
245
+ }, 60000)
246
+
247
+ it('create_document creates a document (self-cleaning)', async () => {
248
+ const created = await docs.write('create_document')({ title: `CmdTest Doc Tool ${Date.now()}` })
249
+ const id = created?.documentId
250
+ expect(typeof id).toBe('string')
251
+ await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
252
+ }, 60000)
37
253
  })
38
- const integrationNode = { id: 'node-gdocs', type: 'google-docs', label: 'Google Docs', connectionId: GDOCS_TEST_CONNECTION_ID } as any
39
-
40
- const tools = loadIntegrationTools('google-docs')
41
- expect(tools).toBeTruthy()
42
-
43
- buildWriteHandler = (name: string) => {
44
- const tool = tools!.write.find(t => t.name === name)
45
- expect(tool, `write tool ${name} exists`).toBeTruthy()
46
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
47
- const build = new Function('integration', `return (${tool!.handlerCode});`)
48
- return build(integration) as (input: any) => Promise<any>
49
- }
50
-
51
- buildReadHandler = (name: string) => {
52
- const tool = tools!.read.find(t => t.name === name)
53
- expect(tool, `read tool ${name} exists`).toBeTruthy()
54
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
55
- const build = new Function('integration', `return (${tool!.handlerCode});`)
56
- return build(integration) as (input: any) => Promise<any>
57
- }
58
-
59
- ctx.documentId = GDOCS_TEST_DOCUMENT_ID
60
- }, 60000)
61
-
62
- it('create_document creates a document when allowed', async () => {
63
- if (!process.env.GDOCS_ALLOW_CREATE)
64
- return expect(true).toBe(true)
65
- const create_document = buildWriteHandler('create_document')
66
- const res = await create_document({ title: `CmdTest Doc ${Date.now()}` })
67
- expect(res?.documentId).toBeTruthy()
68
- }, 60000)
69
-
70
- it('batch_update can perform a trivial replaceAllText no-op', async () => {
71
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
72
- if (!documentId)
73
- return expect(true).toBe(true)
74
- const batch_update = buildWriteHandler('batch_update')
75
- const res = await batch_update({ documentId, requests: [
76
- { replaceAllText: { containsText: { text: '___unlikely___', matchCase: true }, replaceText: '___unlikely___' } },
77
- ] })
78
- expect(Array.isArray(res?.replies) || res?.documentId).toBeTruthy()
79
- }, 60000)
80
-
81
- it('append_text appends content', async () => {
82
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
83
- if (!documentId)
84
- return expect(true).toBe(true)
85
- const append_text = buildWriteHandler('append_text')
86
- const marker = `CmdTest ${Date.now()}`
87
- const res = await append_text({ documentId, text: marker })
88
- expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
89
- const get_text = buildReadHandler('get_document_text')
90
- const after = await get_text({ documentId })
91
- expect(String(after?.text || '')).toContain(marker)
92
- }, 60000)
93
-
94
- it('insert_text_after_first_match inserts text near target', async () => {
95
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
96
- if (!documentId)
97
- return expect(true).toBe(true)
98
- const insert_text_after_first_match = buildWriteHandler('insert_text_after_first_match')
99
- const get_text = buildReadHandler('get_document_text')
100
- const anchor = `ANCHOR_${Date.now()}`
101
- const appended = buildWriteHandler('append_text')
102
- const before = await get_text({ documentId })
103
- if (!String(before?.text || '').includes(anchor)) {
104
- await appended({ documentId, text: `\n${anchor}\n` })
105
- }
106
- const insertSnippet = ` CmdTest ${Date.now()} `
107
- const res = await insert_text_after_first_match({ documentId, findText: anchor, insertText: insertSnippet, position: 'after' })
108
- expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
109
- const after = await get_text({ documentId })
110
- const text = String(after?.text || '')
111
- expect(text).toContain(anchor)
112
- expect(text).toContain(insertSnippet)
113
- }, 60000)
114
-
115
- it('replace_all_text replaces occurrences', async () => {
116
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
117
- if (!documentId)
118
- return expect(true).toBe(true)
119
- const replace_all_text = buildWriteHandler('replace_all_text')
120
- const res = await replace_all_text({ documentId, findText: '___unlikely___', replaceText: '___unlikely___', matchCase: true })
121
- expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
122
- }, 60000)
123
-
124
- it('style_first_match applies style to first match', async () => {
125
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
126
- if (!documentId)
127
- return expect(true).toBe(true)
128
- const style_first_match = buildWriteHandler('style_first_match')
129
- const get_struct = buildReadHandler('get_document_structured')
130
- const anchor = `ANCHOR_${Date.now()}`
131
- const appended = buildWriteHandler('append_text')
132
- const before = await get_struct({ documentId })
133
- const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
134
- if (!hasAnchorBefore)
135
- await appended({ documentId, text: `\n${anchor}\n` })
136
- const res = await style_first_match({ documentId, findText: anchor, textStyle: { bold: true } })
137
- expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
138
- }, 60000)
139
-
140
- it('insert_table_after_first_match inserts a table near target', async () => {
141
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
142
- if (!documentId)
143
- return expect(true).toBe(true)
144
- const insert_table_after_first_match = buildWriteHandler('insert_table_after_first_match')
145
- const get_struct = buildReadHandler('get_document_structured')
146
- const anchor = `ANCHOR_${Date.now()}`
147
- const appended = buildWriteHandler('append_text')
148
- const before = await get_struct({ documentId })
149
- const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
150
- if (!hasAnchorBefore)
151
- await appended({ documentId, text: `\n${anchor}\n` })
152
- const res = await insert_table_after_first_match({ documentId, findText: anchor, rows: 1, columns: 1 })
153
- expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
154
- const after = await get_struct({ documentId })
155
- const hasTable = (after?.body?.content || []).some((el: any) => Boolean(el.table))
156
- expect(hasTable).toBe(true)
157
- }, 60000)
158
-
159
- it('insert_page_break_after_first_match inserts a break near target', async () => {
160
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
161
- if (!documentId)
162
- return expect(true).toBe(true)
163
- const insert_page_break_after_first_match = buildWriteHandler('insert_page_break_after_first_match')
164
- const get_struct = buildReadHandler('get_document_structured')
165
- const anchor = `ANCHOR_${Date.now()}`
166
- const appended = buildWriteHandler('append_text')
167
- const before = await get_struct({ documentId })
168
- const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
169
- if (!hasAnchorBefore)
170
- await appended({ documentId, text: `\n${anchor}\n` })
171
- const res = await insert_page_break_after_first_match({ documentId, findText: anchor })
172
- expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
173
- const after = await get_struct({ documentId })
174
- const hasBreak = (after?.body?.content || []).some((el: any) => Boolean(el.sectionBreak))
175
- expect(hasBreak).toBe(true)
176
- }, 60000)
177
-
178
- it('insert_inline_image_after_first_match inserts an image when allowed', async () => {
179
- if (!process.env.GDOCS_TEST_DOCUMENT_ID || !process.env.GDOCS_TEST_IMAGE_URI)
180
- return expect(true).toBe(true)
181
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
182
- const insert_inline_image_after_first_match = buildWriteHandler('insert_inline_image_after_first_match')
183
- const anchor = `ANCHOR_${Date.now()}`
184
- const appended = buildWriteHandler('append_text')
185
- const get_text = buildReadHandler('get_document_text')
186
- const before = await get_text({ documentId })
187
- if (!String(before?.text || '').includes(anchor))
188
- await appended({ documentId, text: `\n${anchor}\n` })
189
- const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: process.env.GDOCS_TEST_IMAGE_URI!, altText: 'CmdTest' })
190
- expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
191
- }, 60000)
192
-
193
- it('delete_first_match deletes a small span (no-op ok)', async () => {
194
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
195
- if (!documentId)
196
- return expect(true).toBe(true)
197
- const delete_first_match = buildWriteHandler('delete_first_match')
198
- const get_text = buildReadHandler('get_document_text')
199
- const anchor = `ANCHOR_${Date.now()}`
200
- const appended = buildWriteHandler('append_text')
201
- const before = await get_text({ documentId })
202
- if (!String(before?.text || '').includes(anchor))
203
- await appended({ documentId, text: `\n${anchor}\n` })
204
- const res = await delete_first_match({ documentId, findText: anchor })
205
- expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
206
- const after = await get_text({ documentId })
207
- expect(String(after?.text || '')).not.toContain(anchor)
208
- }, 60000)
209
-
210
- it('update_paragraph_style_for_first_match updates paragraph style near target', async () => {
211
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
212
- if (!documentId)
213
- return expect(true).toBe(true)
214
- const update_paragraph_style_for_first_match = buildWriteHandler('update_paragraph_style_for_first_match')
215
- const get_struct = buildReadHandler('get_document_structured')
216
- const anchor = `ANCHOR_${Date.now()}`
217
- const appended = buildWriteHandler('append_text')
218
- const before = await get_struct({ documentId })
219
- const hasAnchorBefore = JSON.stringify(before?.body || {}).includes(anchor)
220
- if (!hasAnchorBefore)
221
- await appended({ documentId, text: `\n${anchor}\n` })
222
- const res = await update_paragraph_style_for_first_match({ documentId, findText: anchor, paragraphStyle: { alignment: 'CENTER' } })
223
- expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
224
- const after = await get_struct({ documentId })
225
- // find the paragraph containing anchor and verify alignment
226
- let foundAligned = false
227
- for (const el of (after?.body?.content || [])) {
228
- if (!el.paragraph)
229
- continue
230
- const p = el.paragraph
231
- const text = (p.elements || []).map((e: any) => e?.textRun?.content || '').join('')
232
- if (text.includes(anchor)) {
233
- if (p.paragraphStyle?.alignment === 'CENTER')
234
- foundAligned = true
235
- break
236
- }
237
- }
238
- expect(foundAligned).toBe(true)
239
- }, 60000)
240
-
241
- it('update_document_style updates doc style with no-op', async () => {
242
- const documentId = process.env.GDOCS_TEST_DOCUMENT_ID
243
- if (!documentId)
244
- return expect(true).toBe(true)
245
- const update_document_style = buildWriteHandler('update_document_style')
246
- const res = await update_document_style({ documentId, documentStyle: { useFirstPageHeaderFooter: false } })
247
- expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
248
- }, 60000)
254
+ }
249
255
  })
@@ -1,36 +1,57 @@
1
1
  {
2
- "schema": {
3
- "type": "object",
4
- "properties": {
5
- "token": {
6
- "type": "string",
7
- "title": "OAuth Access Token (optional)",
8
- "description": "Google OAuth access token to use as a Bearer token (typically short-lived)."
2
+ "variants": {
3
+ "service_account": {
4
+ "label": "Service Account (recommended)",
5
+ "schema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "serviceAccountJson": {
9
+ "type": "string",
10
+ "title": "Service Account JSON",
11
+ "description": "Full service account key JSON (contents of the downloaded JSON file from Google Cloud)."
12
+ },
13
+ "subject": {
14
+ "type": "string",
15
+ "title": "Subject / impersonated user (optional)",
16
+ "description": "Optional user email to impersonate when using Google Workspace domain-wide delegation."
17
+ },
18
+ "scopes": {
19
+ "type": "array",
20
+ "title": "OAuth scopes (optional)",
21
+ "description": "Optional override for OAuth scopes. Defaults to documents + drive.",
22
+ "items": { "type": "string" }
23
+ }
24
+ },
25
+ "required": ["serviceAccountJson"],
26
+ "additionalProperties": false
9
27
  },
10
- "serviceAccountJson": {
11
- "type": "string",
12
- "title": "Service Account JSON (recommended)",
13
- "description": "Full service account key JSON (contents of the downloaded JSON file)."
28
+ "injection": {
29
+ "headers": {
30
+ "Authorization": "Bearer {{token}}"
31
+ }
14
32
  },
15
- "subject": {
16
- "type": "string",
17
- "title": "Subject / impersonated user (optional)",
18
- "description": "Optional user email to impersonate when using Google Workspace domain-wide delegation."
33
+ "preprocess": "google_service_account"
34
+ },
35
+ "oauth_token": {
36
+ "label": "OAuth Access Token (short-lived)",
37
+ "schema": {
38
+ "type": "object",
39
+ "properties": {
40
+ "token": {
41
+ "type": "string",
42
+ "title": "OAuth Access Token",
43
+ "description": "Short-lived Google OAuth access token with documents and drive scopes."
44
+ }
45
+ },
46
+ "required": ["token"],
47
+ "additionalProperties": false
19
48
  },
20
- "scopes": {
21
- "type": "array",
22
- "title": "OAuth scopes (optional)",
23
- "description": "Optional override for OAuth scopes.",
24
- "items": { "type": "string" }
49
+ "injection": {
50
+ "headers": {
51
+ "Authorization": "Bearer {{token}}"
52
+ }
25
53
  }
26
- },
27
- "required": [],
28
- "additionalProperties": false
29
- },
30
- "injection": {
31
- "headers": {
32
- "Authorization": "Bearer {{token}}"
33
54
  }
34
- }
55
+ },
56
+ "default": "service_account"
35
57
  }
36
-
@@ -0,0 +1,8 @@
1
+ Obtain a short-lived Google OAuth access token:
2
+
3
+ 1. Use the Google OAuth 2.0 Playground (`https://developers.google.com/oauthplayground/`) or your own OAuth flow
4
+ 2. Select the scopes: `https://www.googleapis.com/auth/documents` and `https://www.googleapis.com/auth/drive`
5
+ 3. Exchange the authorization code for an access token
6
+ 4. Paste the access token here
7
+
8
+ Note: OAuth access tokens are short-lived (typically 1 hour). For long-running use, prefer the Service Account variant.
@@ -0,0 +1,10 @@
1
+ Set up a Google Cloud Service Account:
2
+
3
+ 1. Open the [Google Cloud Console](https://console.cloud.google.com/)
4
+ 2. Enable the **Google Docs API** and **Google Drive API** for your project
5
+ 3. Go to **IAM & Admin → Service Accounts** and create a new service account
6
+ 4. Under **Keys**, click **Add Key → Create new key → JSON** and download the file
7
+ 5. Paste the full contents of the JSON file here
8
+ 6. Share your target Google Docs documents with the service account's `client_email`
9
+
10
+ For Google Workspace users: if you need to access user documents, configure domain-wide delegation and set the `subject` field to the user's email.
@@ -34,7 +34,7 @@ async (input) => {
34
34
  return { ok: true }
35
35
 
36
36
  const requests = []
37
- requests.push({ insertInlineImage: { location: { index: baseIndex }, uri, altTextTitle: altText } })
37
+ requests.push({ insertInlineImage: { location: { index: baseIndex }, uri } })
38
38
  requests.push({ replaceAllText: { containsText: { text: marker, matchCase: true }, replaceText: findText } })
39
39
  const res = await integration.fetch(`/documents/${encodeURIComponent(documentId)}:batchUpdate`, { method: 'POST', body: { requests } })
40
40
  return await res.json()
@@ -5,7 +5,6 @@
5
5
  "documentId": { "type": "string" },
6
6
  "findText": { "type": "string" },
7
7
  "uri": { "type": "string" },
8
- "altText": { "type": "string" },
9
8
  "position": { "type": "string", "enum": ["after", "before"], "default": "after" }
10
9
  },
11
10
  "required": ["documentId", "findText", "uri"],