@commandable/integration-data 0.0.1 → 0.0.4

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 (79) 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 +18 -15
  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 +219 -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/manifest.json +2 -2
  26. package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
  27. package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
  28. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
  29. package/integrations/google-calendar/credentials.json +50 -29
  30. package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
  31. package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
  32. package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
  33. package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
  34. package/integrations/google-docs/__tests__/write_handlers.test.ts +248 -245
  35. package/integrations/google-docs/credentials.json +50 -29
  36. package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
  37. package/integrations/google-docs/credentials_hint_service_account.md +10 -0
  38. package/integrations/google-drive/credentials.json +57 -0
  39. package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
  40. package/integrations/google-drive/credentials_hint_service_account.md +10 -0
  41. package/integrations/google-drive/handlers/create_file.js +15 -0
  42. package/integrations/google-drive/handlers/create_folder.js +15 -0
  43. package/integrations/google-drive/handlers/delete_file.js +14 -0
  44. package/integrations/google-drive/handlers/get_file.js +7 -0
  45. package/integrations/google-drive/handlers/move_file.js +12 -0
  46. package/integrations/google-drive/manifest.json +42 -0
  47. package/integrations/google-drive/schemas/create_file.json +12 -0
  48. package/integrations/google-drive/schemas/create_folder.json +11 -0
  49. package/integrations/google-drive/schemas/delete_file.json +10 -0
  50. package/integrations/google-drive/schemas/get_file.json +10 -0
  51. package/integrations/google-drive/schemas/move_file.json +12 -0
  52. package/integrations/google-sheet/__tests__/get_handlers.test.ts +47 -55
  53. package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
  54. package/integrations/google-sheet/__tests__/write_handlers.test.ts +64 -63
  55. package/integrations/google-sheet/credentials.json +50 -29
  56. package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
  57. package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
  58. package/integrations/google-slides/__tests__/get_handlers.test.ts +37 -36
  59. package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
  60. package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -58
  61. package/integrations/google-slides/credentials.json +50 -29
  62. package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
  63. package/integrations/google-slides/credentials_hint_service_account.md +10 -0
  64. package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
  65. package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
  66. package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
  67. package/integrations/notion/credentials.json +22 -17
  68. package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
  69. package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
  70. package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
  71. package/integrations/trello/credentials.json +26 -21
  72. package/integrations/trello/handlers/close_board.js +6 -0
  73. package/integrations/trello/handlers/create_board.js +11 -0
  74. package/integrations/trello/handlers/delete_board.js +13 -0
  75. package/integrations/trello/manifest.json +21 -0
  76. package/integrations/trello/schemas/close_board.json +10 -0
  77. package/integrations/trello/schemas/create_board.json +12 -0
  78. package/integrations/trello/schemas/delete_board.json +10 -0
  79. package/package.json +1 -1
@@ -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 spreadsheets.",
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 spreadsheets scope."
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 scope: `https://www.googleapis.com/auth/spreadsheets`
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 Sheets 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 spreadsheets with the service account's `client_email`
9
+
10
+ For Google Workspace users: optionally configure domain-wide delegation and set `subject` to the user's email.
@@ -1,68 +1,69 @@
1
- import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
2
+ import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, hasEnv, safeCleanup } from '../../__tests__/liveHarness.js'
4
3
 
5
- // LIVE Google Slides read tests using managed OAuth
4
+ // LIVE Google Slides read tests using credentials
6
5
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GSLIDES_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-slides')
10
- // - GSLIDES_TEST_PRESENTATION_ID (an accessible presentation ID)
6
+ // - Either GOOGLE_TOKEN, OR GOOGLE_SERVICE_ACCOUNT_JSON
11
7
 
12
- const env = process.env as Record<string, string>
13
- const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
14
- const suite = hasEnv(
15
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
16
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
17
- 'GSLIDES_TEST_CONNECTION_ID',
18
- )
8
+ const suite = (hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON'))
19
9
  ? describe
20
10
  : describe.skip
21
11
 
22
12
  suite('google-slides read handlers (live)', () => {
23
- let buildReadHandler: (name: string) => ((input: any) => Promise<any>)
13
+ let slides: ReturnType<typeof createToolbox>
14
+ let drive: ReturnType<typeof createToolbox>
15
+ let folderId: string | undefined
16
+ let presentationId: string | undefined
24
17
 
25
18
  beforeAll(async () => {
26
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GSLIDES_TEST_CONNECTION_ID } = env
19
+ const env = process.env as Record<string, string | undefined>
20
+ const credentialStore = createCredentialStore(async () => ({
21
+ token: env.GOOGLE_TOKEN || '',
22
+ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
23
+ }))
24
+ const proxy = createProxy(credentialStore)
25
+ slides = createToolbox('google-slides', proxy, createIntegrationNode('google-slides', { label: 'Google Slides', credentialId: 'google-slides-creds' }))
26
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
27
27
 
28
- const proxy = new IntegrationProxy({
29
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
30
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
31
- })
32
- const integrationNode = { id: 'node-gslides', type: 'google-slides', label: 'Google Slides', connectionId: GSLIDES_TEST_CONNECTION_ID } as any
28
+ // Create dedicated folder + presentation for this run
29
+ const folder = await drive.write('create_folder')({ name: `CmdTest Slides ${Date.now()}` })
30
+ folderId = folder?.id
31
+ expect(folderId).toBeTruthy()
33
32
 
34
- const tools = loadIntegrationTools('google-slides')
35
- expect(tools).toBeTruthy()
33
+ const created = await drive.write('create_file')({
34
+ name: `CmdTest Slides ${Date.now()}`,
35
+ mimeType: 'application/vnd.google-apps.presentation',
36
+ parentId: folderId,
37
+ })
38
+ presentationId = created?.id
39
+ expect(presentationId).toBeTruthy()
40
+ }, 60000)
36
41
 
37
- buildReadHandler = (name: string) => {
38
- const tool = tools!.read.find(t => t.name === name)
39
- expect(tool, `read tool ${name} exists`).toBeTruthy()
40
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
41
- const build = new Function('integration', `return (${tool!.handlerCode});`)
42
- return build(integration) as (input: any) => Promise<any>
43
- }
42
+ afterAll(async () => {
43
+ if (!folderId)
44
+ return
45
+ await safeCleanup(async () => presentationId ? drive.write('delete_file')({ fileId: presentationId }) : Promise.resolve())
46
+ await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
44
47
  }, 60000)
45
48
 
46
49
  it('get_presentation returns metadata', async () => {
47
- const presentationId = env.GSLIDES_TEST_PRESENTATION_ID
48
50
  if (!presentationId)
49
51
  return expect(true).toBe(true)
50
- const handler = buildReadHandler('get_presentation')
52
+ const handler = slides.read('get_presentation')
51
53
  const result = await handler({ presentationId })
52
54
  expect(result?.presentationId || Array.isArray(result?.slides)).toBeTruthy()
53
55
  }, 30000)
54
56
 
55
57
  it('get_page_thumbnail returns URL data', async () => {
56
- const presentationId = env.GSLIDES_TEST_PRESENTATION_ID
57
58
  if (!presentationId)
58
59
  return expect(true).toBe(true)
59
60
  // First query the presentation to discover a page id
60
- const getPresentation = buildReadHandler('get_presentation')
61
+ const getPresentation = slides.read('get_presentation')
61
62
  const meta = await getPresentation({ presentationId })
62
63
  const firstSlide = meta?.slides?.[0]
63
64
  if (!firstSlide?.objectId)
64
65
  return expect(true).toBe(true)
65
- const handler = buildReadHandler('get_page_thumbnail')
66
+ const handler = slides.read('get_page_thumbnail')
66
67
  const result = await handler({ presentationId, 'pageObjectId': firstSlide.objectId, 'thumbnailProperties.thumbnailSize': 'MEDIUM', 'thumbnailProperties.mimeType': 'PNG' })
67
68
  expect(typeof result?.contentUrl === 'string' || typeof result?.thumbnailUrl === 'string').toBe(true)
68
69
  }, 30000)
@@ -1,34 +1,9 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
1
  import { describe, expect, it } from 'vitest'
5
- import { loadIntegrationManifest } from '../../../src/integrations/dataLoader.js'
6
-
7
- function escapeRegExp(str: string): string {
8
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
9
- }
2
+ import { getMissingToolUsages } from '../../__tests__/usageParity.js'
10
3
 
11
4
  describe('google-slides static usage parity', () => {
12
- it('every manifest tool is referenced in tests via build*(name)', () => {
13
- const manifest = loadIntegrationManifest('google-slides')!
14
- const toolNames = (manifest.tools as any[]).map(t => t.name)
15
-
16
- const testsDir = fileURLToPath(new URL('.', import.meta.url))
17
- expect(existsSync(testsDir)).toBe(true)
18
- const testFiles = readdirSync(testsDir)
19
- .filter(f => /\.test\.(t|j)s$/.test(f) && !f.includes('usage_parity.test'))
20
- .map(f => resolve(testsDir, f))
21
-
22
- const fileContents = testFiles.map(f => readFileSync(f, 'utf8'))
23
-
24
- const missing: string[] = []
25
- for (const name of toolNames) {
26
- const nameRe = new RegExp(`build(?:Read|Write|Admin)?(?:Handler)?\\(\\s*['\"\`]${escapeRegExp(name)}['\"\`]\\s*\\)`, 'm')
27
- const found = fileContents.some(src => nameRe.test(src))
28
- if (!found)
29
- missing.push(name)
30
- }
31
-
5
+ it('every manifest tool is referenced in tests', () => {
6
+ const missing = getMissingToolUsages({ integrationName: 'google-slides', importMetaUrl: import.meta.url })
32
7
  expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
33
8
  })
34
9
  })
@@ -1,76 +1,67 @@
1
- import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
2
+ import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, hasEnv, safeCleanup } from '../../__tests__/liveHarness.js'
4
3
 
5
- // LIVE Google Slides write tests using managed OAuth
4
+ // LIVE Google Slides write tests using credentials
6
5
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - GSLIDES_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-slides')
10
- // - GSLIDES_TEST_PRESENTATION_ID (a presentation ID with write access)
11
-
12
- interface Ctx { presentationId?: string }
13
-
14
- const env = process.env as Record<string, string>
15
- const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
16
- const suite = hasEnv(
17
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
18
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
19
- 'GSLIDES_TEST_CONNECTION_ID',
20
- 'GSLIDES_TEST_PRESENTATION_ID',
21
- )
6
+ // - Either GOOGLE_TOKEN, OR GOOGLE_SERVICE_ACCOUNT_JSON
7
+
8
+ interface Ctx { presentationId?: string, folderId?: string, anchorText?: string }
9
+
10
+ const suite = (hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON'))
22
11
  ? describe
23
12
  : describe.skip
24
13
 
25
14
  suite('google-slides write handlers (live)', () => {
26
15
  const ctx: Ctx = {}
27
- let buildWriteHandler: (name: string) => ((input: any) => Promise<any>)
16
+ let slides: ReturnType<typeof createToolbox>
17
+ let drive: ReturnType<typeof createToolbox>
28
18
 
29
19
  beforeAll(async () => {
30
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, GSLIDES_TEST_CONNECTION_ID, GSLIDES_TEST_PRESENTATION_ID } = env
31
-
32
- const proxy = new IntegrationProxy({
33
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
34
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
20
+ const env = process.env as Record<string, string | undefined>
21
+ const credentialStore = createCredentialStore(async () => ({
22
+ token: env.GOOGLE_TOKEN || '',
23
+ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
24
+ }))
25
+ const proxy = createProxy(credentialStore)
26
+ slides = createToolbox('google-slides', proxy, createIntegrationNode('google-slides', { label: 'Google Slides', credentialId: 'google-slides-creds' }))
27
+ drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
28
+
29
+ const folder = await drive.write('create_folder')({ name: `CmdTest Slides Write ${Date.now()}` })
30
+ ctx.folderId = folder?.id
31
+ expect(ctx.folderId).toBeTruthy()
32
+
33
+ const created = await drive.write('create_file')({
34
+ name: `CmdTest Slides ${Date.now()}`,
35
+ mimeType: 'application/vnd.google-apps.presentation',
36
+ parentId: ctx.folderId,
35
37
  })
36
- const integrationNode = { id: 'node-gslides', type: 'google-slides', label: 'Google Slides', connectionId: GSLIDES_TEST_CONNECTION_ID } as any
37
-
38
- const tools = loadIntegrationTools('google-slides')
39
- expect(tools).toBeTruthy()
38
+ ctx.presentationId = created?.id
39
+ expect(ctx.presentationId).toBeTruthy()
40
40
 
41
- buildWriteHandler = (name: string) => {
42
- const tool = tools!.write.find(t => t.name === name)
43
- expect(tool, `write tool ${name} exists`).toBeTruthy()
44
- const integration = { fetch: (path: string, init?: RequestInit) => proxy.call(integrationNode, path, init) }
45
- const build = new Function('integration', `return (${tool!.handlerCode});`)
46
- return build(integration) as (input: any) => Promise<any>
47
- }
48
-
49
- ctx.presentationId = GSLIDES_TEST_PRESENTATION_ID
41
+ ctx.anchorText = `CMD_ANCHOR_${Date.now()}`
42
+ const appendTitle = slides.write('append_text_to_title_of_slide_index')
43
+ await appendTitle({ presentationId: ctx.presentationId, slideIndex: 0, text: ` ${ctx.anchorText} ` })
50
44
  }, 60000)
51
45
 
46
+ afterAll(async () => {
47
+ await safeCleanup(async () => ctx.presentationId ? drive.write('delete_file')({ fileId: ctx.presentationId }) : Promise.resolve())
48
+ await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
49
+ }, 60_000)
50
+
52
51
  it('batch_update performs a trivial update (no-op replace)', async () => {
53
52
  if (!ctx.presentationId)
54
53
  return expect(true).toBe(true)
55
- const handler = buildWriteHandler('batch_update')
54
+ const handler = slides.write('batch_update')
56
55
  const res = await handler({ presentationId: ctx.presentationId, requests: [
57
56
  { replaceAllText: { containsText: { text: '___unlikely___', matchCase: true }, replaceText: '___unlikely___' } },
58
57
  ] })
59
58
  expect(res?.presentationId || Array.isArray(res?.replies) || res?.writeControl).toBeTruthy()
60
59
  }, 60000)
61
60
 
62
- it('create_presentation creates a presentation when allowed', async () => {
63
- if (!env.GSLIDES_ALLOW_CREATE)
64
- return expect(true).toBe(true)
65
- const handler = buildWriteHandler('create_presentation')
66
- const res = await handler({ title: `Cmd Slides ${Date.now()}` })
67
- expect(typeof res?.presentationId === 'string').toBe(true)
68
- }, 60000)
69
-
70
61
  it('append_text_to_title_of_slide_index appends to title', async () => {
71
62
  if (!ctx.presentationId)
72
63
  return expect(true).toBe(true)
73
- const handler = buildWriteHandler('append_text_to_title_of_slide_index')
64
+ const handler = slides.write('append_text_to_title_of_slide_index')
74
65
  const res = await handler({ presentationId: ctx.presentationId, slideIndex: 0, text: ` CmdTest ${Date.now()}` })
75
66
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
76
67
  }, 60000)
@@ -78,7 +69,7 @@ suite('google-slides write handlers (live)', () => {
78
69
  it('replace_text_first_match replaces text (no-op ok)', async () => {
79
70
  if (!ctx.presentationId)
80
71
  return expect(true).toBe(true)
81
- const handler = buildWriteHandler('replace_text_first_match')
72
+ const handler = slides.write('replace_text_first_match')
82
73
  const res = await handler({ presentationId: ctx.presentationId, findText: '___unlikely___', replaceText: '___unlikely___' })
83
74
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
84
75
  }, 60000)
@@ -86,40 +77,56 @@ suite('google-slides write handlers (live)', () => {
86
77
  it('style_text_first_match applies style', async () => {
87
78
  if (!ctx.presentationId)
88
79
  return expect(true).toBe(true)
89
- const handler = buildWriteHandler('style_text_first_match')
90
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', textStyle: { bold: true } })
80
+ if (!ctx.anchorText)
81
+ return expect(true).toBe(true)
82
+ const handler = slides.write('style_text_first_match')
83
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, textStyle: { bold: true } })
91
84
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
92
85
  }, 60000)
93
86
 
94
87
  it('insert_shape_after_first_match inserts a shape', async () => {
95
88
  if (!ctx.presentationId)
96
89
  return expect(true).toBe(true)
97
- const handler = buildWriteHandler('insert_shape_after_first_match')
98
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', shapeType: 'RECTANGLE' })
90
+ if (!ctx.anchorText)
91
+ return expect(true).toBe(true)
92
+ const handler = slides.write('insert_shape_after_first_match')
93
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, shapeType: 'RECTANGLE' })
99
94
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
100
95
  }, 60000)
101
96
 
102
97
  it('insert_image_after_first_match inserts an image when allowed', async () => {
98
+ const env = process.env as Record<string, string | undefined>
103
99
  if (!ctx.presentationId || !env.GSLIDES_TEST_IMAGE_URI)
104
100
  return expect(true).toBe(true)
105
- const handler = buildWriteHandler('insert_image_after_first_match')
106
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', uri: env.GSLIDES_TEST_IMAGE_URI })
101
+ if (!ctx.anchorText)
102
+ return expect(true).toBe(true)
103
+ const handler = slides.write('insert_image_after_first_match')
104
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, uri: env.GSLIDES_TEST_IMAGE_URI })
107
105
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
108
106
  }, 60000)
109
107
 
110
108
  it('create_slide_after_first_match creates a slide near anchor', async () => {
111
109
  if (!ctx.presentationId)
112
110
  return expect(true).toBe(true)
113
- const handler = buildWriteHandler('create_slide_after_first_match')
114
- const res = await handler({ presentationId: ctx.presentationId, findText: 'the', layout: 'BLANK' })
111
+ if (!ctx.anchorText)
112
+ return expect(true).toBe(true)
113
+ const handler = slides.write('create_slide_after_first_match')
114
+ const res = await handler({ presentationId: ctx.presentationId, findText: ctx.anchorText, layout: 'BLANK' })
115
115
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
116
116
  }, 60000)
117
117
 
118
118
  it('set_background_color_for_slide_index sets color', async () => {
119
119
  if (!ctx.presentationId)
120
120
  return expect(true).toBe(true)
121
- const handler = buildWriteHandler('set_background_color_for_slide_index')
121
+ const handler = slides.write('set_background_color_for_slide_index')
122
122
  const res = await handler({ presentationId: ctx.presentationId, slideIndex: 0, rgbColor: { red: 0.9, green: 0.9, blue: 0.9 } })
123
123
  expect(res?.presentationId || Array.isArray(res?.replies)).toBeTruthy()
124
124
  }, 60000)
125
+
126
+ it('create_presentation creates a presentation (self-cleaning)', async () => {
127
+ const created = await slides.write('create_presentation')({ title: `CmdTest Slides Tool ${Date.now()}` })
128
+ const id = created?.presentationId
129
+ expect(typeof id).toBe('string')
130
+ await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
131
+ }, 60000)
125
132
  })
@@ -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 presentations + 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 presentations 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/presentations` 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 Slides 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 presentations with the service account's `client_email`
9
+
10
+ For Google Workspace users: optionally configure domain-wide delegation and set `subject` to the user's email.
@@ -1,12 +1,10 @@
1
1
  import { beforeAll, describe, expect, it } from 'vitest'
2
- import { IntegrationProxy } from '../../../src/integrations/proxy.js'
3
- import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
2
+ import { IntegrationProxy } from '../../../../server/src/integrations/proxy.js'
3
+ import { loadIntegrationTools } from '../../../../server/src/integrations/dataLoader.js'
4
4
 
5
- // LIVE Notion integration tests using managed OAuth
5
+ // LIVE Notion integration tests using credentials
6
6
  // Required env vars:
7
- // - COMMANDABLE_MANAGED_OAUTH_BASE_URL
8
- // - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
9
- // - NOTION_TEST_CONNECTION_ID (managed OAuth connection for provider 'notion')
7
+ // - NOTION_TOKEN
10
8
 
11
9
  interface Ctx {
12
10
  database_id?: string
@@ -17,9 +15,7 @@ interface Ctx {
17
15
  const env = process.env as Record<string, string>
18
16
  const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
19
17
  const suite = hasEnv(
20
- 'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
21
- 'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
22
- 'NOTION_TEST_CONNECTION_ID',
18
+ 'NOTION_TOKEN',
23
19
  )
24
20
  ? describe
25
21
  : describe.skip
@@ -29,13 +25,20 @@ suite('notion read handlers (live)', () => {
29
25
  let buildHandler: (name: string) => ((input: any) => Promise<any>)
30
26
 
31
27
  beforeAll(async () => {
32
- const { COMMANDABLE_MANAGED_OAUTH_BASE_URL, COMMANDABLE_MANAGED_OAUTH_SECRET_KEY, NOTION_TEST_CONNECTION_ID } = env
28
+ const credentialStore = {
29
+ getCredentials: async () => ({ token: env.NOTION_TOKEN || '' }),
30
+ }
33
31
 
34
- const proxy = new IntegrationProxy({
35
- managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
36
- managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
37
- })
38
- const integrationNode = { id: 'node-notion', type: 'notion', label: 'Notion', connectionId: NOTION_TEST_CONNECTION_ID } as any
32
+ const proxy = new IntegrationProxy({ credentialStore })
33
+ const integrationNode = {
34
+ spaceId: 'ci',
35
+ id: 'node-notion',
36
+ referenceId: 'node-notion',
37
+ type: 'notion',
38
+ label: 'Notion',
39
+ connectionMethod: 'credentials',
40
+ credentialId: 'notion-creds',
41
+ } as any
39
42
 
40
43
  const tools = loadIntegrationTools('notion')
41
44
  expect(tools).toBeTruthy()