@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.
- package/dist/credentials-index.d.ts +4 -21
- package/dist/credentials-index.d.ts.map +1 -1
- package/dist/credentials-index.js +407 -215
- package/dist/credentials-index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +38 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +70 -16
- package/dist/loader.js.map +1 -1
- package/integrations/__tests__/liveHarness.ts +84 -0
- package/integrations/__tests__/usageParity.ts +54 -0
- package/integrations/airtable/__tests__/get_handlers.test.ts +18 -15
- package/integrations/airtable/__tests__/usage_parity.test.ts +3 -29
- package/integrations/airtable/__tests__/write_and_admin_handlers.test.ts +20 -17
- package/integrations/airtable/credentials.json +21 -16
- package/integrations/github/__tests__/get_handlers.test.ts +101 -108
- package/integrations/github/__tests__/usage_parity.test.ts +15 -27
- package/integrations/github/__tests__/write_handlers.test.ts +219 -306
- package/integrations/github/credentials.json +40 -15
- package/integrations/github/credentials_hint_classic_pat.md +8 -0
- package/integrations/github/credentials_hint_fine_grained_pat.md +9 -0
- package/integrations/github/manifest.json +2 -2
- package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
- package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
- package/integrations/google-calendar/credentials.json +50 -29
- package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
- package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
- package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-docs/__tests__/write_handlers.test.ts +248 -245
- package/integrations/google-docs/credentials.json +50 -29
- package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-docs/credentials_hint_service_account.md +10 -0
- package/integrations/google-drive/credentials.json +57 -0
- package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-drive/credentials_hint_service_account.md +10 -0
- package/integrations/google-drive/handlers/create_file.js +15 -0
- package/integrations/google-drive/handlers/create_folder.js +15 -0
- package/integrations/google-drive/handlers/delete_file.js +14 -0
- package/integrations/google-drive/handlers/get_file.js +7 -0
- package/integrations/google-drive/handlers/move_file.js +12 -0
- package/integrations/google-drive/manifest.json +42 -0
- package/integrations/google-drive/schemas/create_file.json +12 -0
- package/integrations/google-drive/schemas/create_folder.json +11 -0
- package/integrations/google-drive/schemas/delete_file.json +10 -0
- package/integrations/google-drive/schemas/get_file.json +10 -0
- package/integrations/google-drive/schemas/move_file.json +12 -0
- package/integrations/google-sheet/__tests__/get_handlers.test.ts +47 -55
- package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
- package/integrations/google-sheet/__tests__/write_handlers.test.ts +64 -63
- package/integrations/google-sheet/credentials.json +50 -29
- package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
- package/integrations/google-slides/__tests__/get_handlers.test.ts +37 -36
- package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -58
- package/integrations/google-slides/credentials.json +50 -29
- package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-slides/credentials_hint_service_account.md +10 -0
- package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
- package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
- package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
- package/integrations/notion/credentials.json +22 -17
- package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
- package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
- package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
- package/integrations/trello/credentials.json +26 -21
- package/integrations/trello/handlers/close_board.js +6 -0
- package/integrations/trello/handlers/create_board.js +11 -0
- package/integrations/trello/handlers/delete_board.js +13 -0
- package/integrations/trello/manifest.json +21 -0
- package/integrations/trello/schemas/close_board.json +10 -0
- package/integrations/trello/schemas/create_board.json +12 -0
- package/integrations/trello/schemas/delete_board.json +10 -0
- package/package.json +1 -1
|
@@ -1,36 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
|
|
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
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
"injection": {
|
|
29
|
+
"headers": {
|
|
30
|
+
"Authorization": "Bearer {{token}}"
|
|
31
|
+
}
|
|
14
32
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
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
|
|
4
|
+
// LIVE Google Slides read tests using credentials
|
|
6
5
|
// Required env vars:
|
|
7
|
-
// -
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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
|
|
13
|
-
const
|
|
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 {
|
|
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
|
|
4
|
+
// LIVE Google Slides write tests using credentials
|
|
6
5
|
// Required env vars:
|
|
7
|
-
// -
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
16
|
+
let slides: ReturnType<typeof createToolbox>
|
|
17
|
+
let drive: ReturnType<typeof createToolbox>
|
|
28
18
|
|
|
29
19
|
beforeAll(async () => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
const tools = loadIntegrationTools('google-slides')
|
|
39
|
-
expect(tools).toBeTruthy()
|
|
38
|
+
ctx.presentationId = created?.id
|
|
39
|
+
expect(ctx.presentationId).toBeTruthy()
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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 =
|
|
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
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
|
|
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
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
"injection": {
|
|
29
|
+
"headers": {
|
|
30
|
+
"Authorization": "Bearer {{token}}"
|
|
31
|
+
}
|
|
14
32
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
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 '
|
|
3
|
-
import { loadIntegrationTools } from '
|
|
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
|
|
5
|
+
// LIVE Notion integration tests using credentials
|
|
6
6
|
// Required env vars:
|
|
7
|
-
// -
|
|
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
|
-
'
|
|
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
|
|
28
|
+
const credentialStore = {
|
|
29
|
+
getCredentials: async () => ({ token: env.NOTION_TOKEN || '' }),
|
|
30
|
+
}
|
|
33
31
|
|
|
34
|
-
const proxy = new IntegrationProxy({
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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()
|