@slates-integrations/google-ads 0.2.0-rc.2

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.
@@ -0,0 +1,245 @@
1
+ import { createAxios } from 'slates';
2
+ import type { AxiosInstance } from 'axios';
3
+
4
+ let API_VERSION = 'v19';
5
+ let BASE_URL = `https://googleads.googleapis.com/${API_VERSION}`;
6
+
7
+ export interface GoogleAdsClientConfig {
8
+ token: string;
9
+ developerToken: string;
10
+ loginCustomerId?: string;
11
+ }
12
+
13
+ export interface MutateOperation {
14
+ create?: Record<string, any>;
15
+ update?: Record<string, any>;
16
+ remove?: string;
17
+ updateMask?: string;
18
+ }
19
+
20
+ export interface SearchResponse {
21
+ results: Record<string, any>[];
22
+ totalResultsCount?: string;
23
+ nextPageToken?: string;
24
+ fieldMask?: string;
25
+ }
26
+
27
+ export class GoogleAdsClient {
28
+ private http: AxiosInstance;
29
+
30
+ constructor(config: GoogleAdsClientConfig) {
31
+ let headers: Record<string, string> = {
32
+ Authorization: `Bearer ${config.token}`,
33
+ 'developer-token': config.developerToken,
34
+ 'Content-Type': 'application/json'
35
+ };
36
+
37
+ if (config.loginCustomerId) {
38
+ headers['login-customer-id'] = config.loginCustomerId.replace(/-/g, '');
39
+ }
40
+
41
+ this.http = createAxios({
42
+ baseURL: BASE_URL,
43
+ headers
44
+ });
45
+ }
46
+
47
+ // Account Management
48
+
49
+ async listAccessibleCustomers(): Promise<string[]> {
50
+ let response = await this.http.get('/customers:listAccessibleCustomers');
51
+ return response.data.resourceNames || [];
52
+ }
53
+
54
+ async getCustomer(customerId: string): Promise<Record<string, any>> {
55
+ let cid = customerId.replace(/-/g, '');
56
+ let response = await this.http.post(`/customers/${cid}/googleAds:search`, {
57
+ query: `SELECT customer.id, customer.descriptive_name, customer.currency_code, customer.time_zone, customer.manager, customer.status FROM customer LIMIT 1`
58
+ });
59
+ let results = response.data.results || [];
60
+ return results[0]?.customer || {};
61
+ }
62
+
63
+ // GAQL Search
64
+
65
+ async search(
66
+ customerId: string,
67
+ query: string,
68
+ pageSize?: number,
69
+ pageToken?: string
70
+ ): Promise<SearchResponse> {
71
+ let cid = customerId.replace(/-/g, '');
72
+ let body: Record<string, any> = { query };
73
+ if (pageSize) body.pageSize = pageSize;
74
+ if (pageToken) body.pageToken = pageToken;
75
+
76
+ let response = await this.http.post(`/customers/${cid}/googleAds:search`, body);
77
+ return response.data;
78
+ }
79
+
80
+ async searchStream(customerId: string, query: string): Promise<Record<string, any>[]> {
81
+ let cid = customerId.replace(/-/g, '');
82
+ let response = await this.http.post(`/customers/${cid}/googleAds:searchStream`, { query });
83
+ let results: Record<string, any>[] = [];
84
+ if (Array.isArray(response.data)) {
85
+ for (let batch of response.data) {
86
+ if (batch.results) {
87
+ results.push(...batch.results);
88
+ }
89
+ }
90
+ }
91
+ return results;
92
+ }
93
+
94
+ // Campaign Management
95
+
96
+ async mutateCampaigns(
97
+ customerId: string,
98
+ operations: MutateOperation[]
99
+ ): Promise<Record<string, any>> {
100
+ let cid = customerId.replace(/-/g, '');
101
+ let response = await this.http.post(`/customers/${cid}/campaigns:mutate`, {
102
+ operations
103
+ });
104
+ return response.data;
105
+ }
106
+
107
+ // Campaign Budget Management
108
+
109
+ async mutateCampaignBudgets(
110
+ customerId: string,
111
+ operations: MutateOperation[]
112
+ ): Promise<Record<string, any>> {
113
+ let cid = customerId.replace(/-/g, '');
114
+ let response = await this.http.post(`/customers/${cid}/campaignBudgets:mutate`, {
115
+ operations
116
+ });
117
+ return response.data;
118
+ }
119
+
120
+ // Ad Group Management
121
+
122
+ async mutateAdGroups(
123
+ customerId: string,
124
+ operations: MutateOperation[]
125
+ ): Promise<Record<string, any>> {
126
+ let cid = customerId.replace(/-/g, '');
127
+ let response = await this.http.post(`/customers/${cid}/adGroups:mutate`, {
128
+ operations
129
+ });
130
+ return response.data;
131
+ }
132
+
133
+ // Ad Group Ad Management
134
+
135
+ async mutateAdGroupAds(
136
+ customerId: string,
137
+ operations: MutateOperation[]
138
+ ): Promise<Record<string, any>> {
139
+ let cid = customerId.replace(/-/g, '');
140
+ let response = await this.http.post(`/customers/${cid}/adGroupAds:mutate`, {
141
+ operations
142
+ });
143
+ return response.data;
144
+ }
145
+
146
+ // Ad Group Criterion (Keywords) Management
147
+
148
+ async mutateAdGroupCriteria(
149
+ customerId: string,
150
+ operations: MutateOperation[]
151
+ ): Promise<Record<string, any>> {
152
+ let cid = customerId.replace(/-/g, '');
153
+ let response = await this.http.post(`/customers/${cid}/adGroupCriteria:mutate`, {
154
+ operations
155
+ });
156
+ return response.data;
157
+ }
158
+
159
+ // Campaign Criterion (Negative Keywords, Targeting) Management
160
+
161
+ async mutateCampaignCriteria(
162
+ customerId: string,
163
+ operations: MutateOperation[]
164
+ ): Promise<Record<string, any>> {
165
+ let cid = customerId.replace(/-/g, '');
166
+ let response = await this.http.post(`/customers/${cid}/campaignCriteria:mutate`, {
167
+ operations
168
+ });
169
+ return response.data;
170
+ }
171
+
172
+ // Bidding Strategy Management
173
+
174
+ async mutateBiddingStrategies(
175
+ customerId: string,
176
+ operations: MutateOperation[]
177
+ ): Promise<Record<string, any>> {
178
+ let cid = customerId.replace(/-/g, '');
179
+ let response = await this.http.post(`/customers/${cid}/biddingStrategies:mutate`, {
180
+ operations
181
+ });
182
+ return response.data;
183
+ }
184
+
185
+ // Conversion Action Management
186
+
187
+ async mutateConversionActions(
188
+ customerId: string,
189
+ operations: MutateOperation[]
190
+ ): Promise<Record<string, any>> {
191
+ let cid = customerId.replace(/-/g, '');
192
+ let response = await this.http.post(`/customers/${cid}/conversionActions:mutate`, {
193
+ operations
194
+ });
195
+ return response.data;
196
+ }
197
+
198
+ // Keyword Planning
199
+
200
+ async generateKeywordIdeas(
201
+ customerId: string,
202
+ params: {
203
+ language?: string;
204
+ geoTargetConstants?: string[];
205
+ keywordSeed?: { keywords: string[] };
206
+ urlSeed?: { url: string };
207
+ keywordAndUrlSeed?: { keywords: string[]; url: string };
208
+ includeAdultKeywords?: boolean;
209
+ pageSize?: number;
210
+ pageToken?: string;
211
+ }
212
+ ): Promise<Record<string, any>> {
213
+ let cid = customerId.replace(/-/g, '');
214
+ let response = await this.http.post(`/customers/${cid}:generateKeywordIdeas`, params);
215
+ return response.data;
216
+ }
217
+
218
+ // Offline Conversion Upload
219
+
220
+ async uploadClickConversions(
221
+ customerId: string,
222
+ conversions: Record<string, any>[],
223
+ partialFailure?: boolean
224
+ ): Promise<Record<string, any>> {
225
+ let cid = customerId.replace(/-/g, '');
226
+ let response = await this.http.post(`/customers/${cid}:uploadClickConversions`, {
227
+ conversions,
228
+ partialFailure: partialFailure ?? true
229
+ });
230
+ return response.data;
231
+ }
232
+
233
+ // Audience / User List Management
234
+
235
+ async mutateUserLists(
236
+ customerId: string,
237
+ operations: MutateOperation[]
238
+ ): Promise<Record<string, any>> {
239
+ let cid = customerId.replace(/-/g, '');
240
+ let response = await this.http.post(`/customers/${cid}/userLists:mutate`, {
241
+ operations
242
+ });
243
+ return response.data;
244
+ }
245
+ }
@@ -0,0 +1,20 @@
1
+ import { GoogleAdsClient } from './client';
2
+
3
+ export interface AuthOutput {
4
+ token: string;
5
+ refreshToken?: string;
6
+ expiresAt?: string;
7
+ developerToken: string;
8
+ }
9
+
10
+ export interface ConfigOutput {
11
+ loginCustomerId?: string;
12
+ }
13
+
14
+ export let createClient = (auth: AuthOutput, config: ConfigOutput): GoogleAdsClient => {
15
+ return new GoogleAdsClient({
16
+ token: auth.token,
17
+ developerToken: auth.developerToken,
18
+ loginCustomerId: config.loginCustomerId
19
+ });
20
+ };
@@ -0,0 +1,75 @@
1
+ import { createLocalSlateTestClient, expectSlateContract } from '@slates/test';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { provider } from './index';
4
+ import { googleAdsActionScopes } from './scopes';
5
+
6
+ describe('google-ads provider contract', () => {
7
+ it('exposes the expected provider, tool, trigger, and auth surface', async () => {
8
+ let client = createLocalSlateTestClient({ slate: provider as any });
9
+ let contract = await expectSlateContract({
10
+ client,
11
+ provider: {
12
+ id: 'google-ads',
13
+ name: 'Google Ads',
14
+ description:
15
+ 'Google Ads integration for managing campaigns, ad groups, ads, keywords, bidding strategies, conversion tracking, audience targeting, and reporting across Google Search, Display, Video, and Shopping networks.'
16
+ },
17
+ toolIds: [
18
+ 'list_accounts',
19
+ 'search_reports',
20
+ 'manage_campaigns',
21
+ 'manage_ad_groups',
22
+ 'manage_ads',
23
+ 'manage_keywords',
24
+ 'manage_bidding_strategies',
25
+ 'manage_conversion_actions',
26
+ 'generate_keyword_ideas',
27
+ 'upload_offline_conversions',
28
+ 'manage_audience_lists'
29
+ ],
30
+ triggerIds: ['lead_form_submit'],
31
+ authMethodIds: ['google_oauth'],
32
+ tools: [
33
+ { id: 'list_accounts', readOnly: true, destructive: false },
34
+ { id: 'search_reports', readOnly: true, destructive: false },
35
+ { id: 'manage_campaigns', readOnly: false, destructive: false },
36
+ { id: 'manage_ad_groups', readOnly: false, destructive: false },
37
+ { id: 'manage_ads', readOnly: false, destructive: false },
38
+ { id: 'manage_keywords', readOnly: false, destructive: false },
39
+ { id: 'manage_bidding_strategies', readOnly: false, destructive: false },
40
+ { id: 'manage_conversion_actions', readOnly: false, destructive: false },
41
+ { id: 'generate_keyword_ideas', readOnly: true, destructive: false },
42
+ { id: 'upload_offline_conversions', readOnly: false, destructive: false },
43
+ { id: 'manage_audience_lists', readOnly: false, destructive: false }
44
+ ],
45
+ triggers: [{ id: 'lead_form_submit', invocationType: 'webhook' }]
46
+ });
47
+
48
+ expect(contract.actions).toHaveLength(12);
49
+ expect(Object.keys(contract.configSchema.properties ?? {})).toEqual(['loginCustomerId']);
50
+
51
+ let expectedScopes = {
52
+ list_accounts: googleAdsActionScopes.listAccounts,
53
+ search_reports: googleAdsActionScopes.searchReports,
54
+ manage_campaigns: googleAdsActionScopes.manageCampaigns,
55
+ manage_ad_groups: googleAdsActionScopes.manageAdGroups,
56
+ manage_ads: googleAdsActionScopes.manageAds,
57
+ manage_keywords: googleAdsActionScopes.manageKeywords,
58
+ manage_bidding_strategies: googleAdsActionScopes.manageBiddingStrategies,
59
+ manage_conversion_actions: googleAdsActionScopes.manageConversionActions,
60
+ generate_keyword_ideas: googleAdsActionScopes.generateKeywordIdeas,
61
+ upload_offline_conversions: googleAdsActionScopes.uploadOfflineConversions,
62
+ manage_audience_lists: googleAdsActionScopes.manageAudienceLists,
63
+ lead_form_submit: googleAdsActionScopes.leadFormSubmit
64
+ };
65
+
66
+ for (let [actionId, scopes] of Object.entries(expectedScopes)) {
67
+ expect(contract.actions.find(action => action.id === actionId)?.scopes).toEqual(scopes);
68
+ }
69
+
70
+ let oauth = await client.getAuthMethod('google_oauth');
71
+ expect(oauth.authenticationMethod.type).toBe('auth.oauth');
72
+ expect(oauth.authenticationMethod.capabilities.handleTokenRefresh?.enabled).toBe(true);
73
+ expect(oauth.authenticationMethod.capabilities.getProfile?.enabled).toBe(true);
74
+ });
75
+ });
package/src/scopes.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { anyOf } from 'slates';
2
+
3
+ export let googleAdsScopes = {
4
+ adwords: 'https://www.googleapis.com/auth/adwords'
5
+ } as const;
6
+
7
+ let adwordsAccess = anyOf(googleAdsScopes.adwords);
8
+
9
+ export let googleAdsActionScopes = {
10
+ listAccounts: adwordsAccess,
11
+ searchReports: adwordsAccess,
12
+ manageCampaigns: adwordsAccess,
13
+ manageAdGroups: adwordsAccess,
14
+ manageAds: adwordsAccess,
15
+ manageKeywords: adwordsAccess,
16
+ manageBiddingStrategies: adwordsAccess,
17
+ manageConversionActions: adwordsAccess,
18
+ generateKeywordIdeas: adwordsAccess,
19
+ uploadOfflineConversions: adwordsAccess,
20
+ manageAudienceLists: adwordsAccess,
21
+ leadFormSubmit: adwordsAccess
22
+ } as const;
package/src/spec.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { SlateSpecification } from 'slates';
2
+ import { auth } from './auth';
3
+ import { config } from './config';
4
+
5
+ export let spec = SlateSpecification.create({
6
+ key: 'google-ads',
7
+ name: 'Google Ads',
8
+ description:
9
+ 'Google Ads integration for managing campaigns, ad groups, ads, keywords, bidding strategies, conversion tracking, audience targeting, and reporting across Google Search, Display, Video, and Shopping networks.',
10
+ metadata: {},
11
+ config,
12
+ auth
13
+ });
@@ -0,0 +1,114 @@
1
+ import { SlateTool } from 'slates';
2
+ import { googleAdsActionScopes } from '../scopes';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { z } from 'zod';
6
+
7
+ export let generateKeywordIdeas = SlateTool.create(spec, {
8
+ name: 'Generate Keyword Ideas',
9
+ key: 'generate_keyword_ideas',
10
+ description: `Generates keyword suggestions based on seed keywords, a URL, or both. Returns keyword ideas with historical metrics including average monthly searches, competition level, and suggested bid ranges.
11
+
12
+ Similar to the Keyword Planner tool in the Google Ads UI. Useful for keyword research, discovering new targeting opportunities, and estimating traffic potential.`,
13
+ instructions: [
14
+ 'Provide at least one of: seed keywords or a URL to generate ideas from.',
15
+ 'Language and geo target constants use resource name format, e.g., "languageConstants/1000" for English, "geoTargetConstants/2840" for United States.'
16
+ ],
17
+ tags: {
18
+ readOnly: true
19
+ }
20
+ })
21
+ .scopes(googleAdsActionScopes.generateKeywordIdeas)
22
+ .input(
23
+ z.object({
24
+ customerId: z.string().describe('The Google Ads customer account ID'),
25
+ seedKeywords: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe('Seed keywords to generate ideas from'),
29
+ url: z.string().optional().describe('URL to extract keyword ideas from'),
30
+ language: z
31
+ .string()
32
+ .optional()
33
+ .describe('Language resource name (e.g., "languageConstants/1000" for English)'),
34
+ geoTargetConstants: z
35
+ .array(z.string())
36
+ .optional()
37
+ .describe('Geo target resource names (e.g., ["geoTargetConstants/2840"] for US)'),
38
+ includeAdultKeywords: z
39
+ .boolean()
40
+ .optional()
41
+ .describe('Whether to include adult keywords'),
42
+ pageSize: z.number().optional().describe('Maximum number of keyword ideas to return'),
43
+ pageToken: z.string().optional().describe('Page token for pagination')
44
+ })
45
+ )
46
+ .output(
47
+ z.object({
48
+ keywordIdeas: z
49
+ .array(
50
+ z.object({
51
+ text: z.string().optional().describe('The keyword text'),
52
+ avgMonthlySearches: z.string().optional().describe('Average monthly searches'),
53
+ competition: z
54
+ .string()
55
+ .optional()
56
+ .describe('Competition level (LOW, MEDIUM, HIGH)'),
57
+ competitionIndex: z.number().optional().describe('Competition index 0-100'),
58
+ lowTopOfPageBidMicros: z
59
+ .string()
60
+ .optional()
61
+ .describe('Low range of top-of-page bid in micros'),
62
+ highTopOfPageBidMicros: z
63
+ .string()
64
+ .optional()
65
+ .describe('High range of top-of-page bid in micros')
66
+ })
67
+ )
68
+ .describe('List of keyword ideas with metrics'),
69
+ nextPageToken: z.string().optional().describe('Token for the next page of results'),
70
+ totalSize: z.string().optional().describe('Total number of keyword ideas available')
71
+ })
72
+ )
73
+ .handleInvocation(async ctx => {
74
+ let client = createClient(ctx.auth, ctx.config);
75
+ let { customerId } = ctx.input;
76
+
77
+ let params: Record<string, any> = {};
78
+
79
+ if (ctx.input.language) params.language = ctx.input.language;
80
+ if (ctx.input.geoTargetConstants) params.geoTargetConstants = ctx.input.geoTargetConstants;
81
+ if (ctx.input.includeAdultKeywords)
82
+ params.includeAdultKeywords = ctx.input.includeAdultKeywords;
83
+ if (ctx.input.pageSize) params.pageSize = ctx.input.pageSize;
84
+ if (ctx.input.pageToken) params.pageToken = ctx.input.pageToken;
85
+
86
+ if (ctx.input.seedKeywords && ctx.input.url) {
87
+ params.keywordAndUrlSeed = { keywords: ctx.input.seedKeywords, url: ctx.input.url };
88
+ } else if (ctx.input.seedKeywords) {
89
+ params.keywordSeed = { keywords: ctx.input.seedKeywords };
90
+ } else if (ctx.input.url) {
91
+ params.urlSeed = { url: ctx.input.url };
92
+ }
93
+
94
+ let response = await client.generateKeywordIdeas(customerId, params);
95
+
96
+ let keywordIdeas = (response.results || []).map((idea: any) => ({
97
+ text: idea.text,
98
+ avgMonthlySearches: idea.keywordIdeaMetrics?.avgMonthlySearches?.toString(),
99
+ competition: idea.keywordIdeaMetrics?.competition,
100
+ competitionIndex: idea.keywordIdeaMetrics?.competitionIndex,
101
+ lowTopOfPageBidMicros: idea.keywordIdeaMetrics?.lowTopOfPageBidMicros?.toString(),
102
+ highTopOfPageBidMicros: idea.keywordIdeaMetrics?.highTopOfPageBidMicros?.toString()
103
+ }));
104
+
105
+ return {
106
+ output: {
107
+ keywordIdeas,
108
+ nextPageToken: response.nextPageToken,
109
+ totalSize: response.totalSize?.toString()
110
+ },
111
+ message: `Generated **${keywordIdeas.length}** keyword idea(s).`
112
+ };
113
+ })
114
+ .build();
@@ -0,0 +1,11 @@
1
+ export * from './list-accounts';
2
+ export * from './search-reports';
3
+ export * from './manage-campaigns';
4
+ export * from './manage-ad-groups';
5
+ export * from './manage-ads';
6
+ export * from './manage-keywords';
7
+ export * from './manage-bidding-strategies';
8
+ export * from './manage-conversion-actions';
9
+ export * from './generate-keyword-ideas';
10
+ export * from './upload-offline-conversions';
11
+ export * from './manage-audience-lists';
@@ -0,0 +1,90 @@
1
+ import { SlateTool } from 'slates';
2
+ import { googleAdsActionScopes } from '../scopes';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { z } from 'zod';
6
+
7
+ export let listAccounts = SlateTool.create(spec, {
8
+ name: 'List Accounts',
9
+ key: 'list_accounts',
10
+ description: `Lists all Google Ads customer accounts accessible to the authenticated user. Returns account IDs, names, currency, timezone, and status for each account. Useful for discovering which accounts can be managed and obtaining customer IDs needed for other operations.`,
11
+ tags: {
12
+ readOnly: true
13
+ }
14
+ })
15
+ .scopes(googleAdsActionScopes.listAccounts)
16
+ .input(
17
+ z.object({
18
+ includeDetails: z
19
+ .boolean()
20
+ .optional()
21
+ .describe(
22
+ 'If true, fetches detailed information (name, currency, timezone) for each account. If false, returns only resource names.'
23
+ )
24
+ })
25
+ )
26
+ .output(
27
+ z.object({
28
+ accounts: z
29
+ .array(
30
+ z.object({
31
+ resourceName: z
32
+ .string()
33
+ .describe('Resource name in format customers/{customer_id}'),
34
+ customerId: z.string().optional().describe('The customer account ID'),
35
+ name: z.string().optional().describe('Descriptive name of the account'),
36
+ currencyCode: z.string().optional().describe('Currency code (e.g., USD, EUR)'),
37
+ timeZone: z.string().optional().describe('Account timezone'),
38
+ isManager: z.boolean().optional().describe('Whether this is a manager account'),
39
+ status: z.string().optional().describe('Account status')
40
+ })
41
+ )
42
+ .describe('List of accessible accounts')
43
+ })
44
+ )
45
+ .handleInvocation(async ctx => {
46
+ let client = createClient(ctx.auth, ctx.config);
47
+
48
+ let resourceNames = await client.listAccessibleCustomers();
49
+
50
+ let accounts: {
51
+ resourceName: string;
52
+ customerId?: string;
53
+ name?: string;
54
+ currencyCode?: string;
55
+ timeZone?: string;
56
+ isManager?: boolean;
57
+ status?: string;
58
+ }[] = [];
59
+
60
+ if (ctx.input.includeDetails) {
61
+ for (let resourceName of resourceNames) {
62
+ let customerId = resourceName.replace('customers/', '');
63
+ try {
64
+ let customer = await client.getCustomer(customerId);
65
+ accounts.push({
66
+ resourceName,
67
+ customerId: customer.id?.toString(),
68
+ name: customer.descriptiveName,
69
+ currencyCode: customer.currencyCode,
70
+ timeZone: customer.timeZone,
71
+ isManager: customer.manager,
72
+ status: customer.status
73
+ });
74
+ } catch {
75
+ accounts.push({ resourceName, customerId });
76
+ }
77
+ }
78
+ } else {
79
+ accounts = resourceNames.map(rn => ({
80
+ resourceName: rn,
81
+ customerId: rn.replace('customers/', '')
82
+ }));
83
+ }
84
+
85
+ return {
86
+ output: { accounts },
87
+ message: `Found **${accounts.length}** accessible Google Ads account(s).`
88
+ };
89
+ })
90
+ .build();