@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.
- package/README.md +57 -0
- package/docs/SPEC.md +98 -0
- package/logo.png +0 -0
- package/package.json +21 -0
- package/slate.json +21 -0
- package/src/auth.contract.test.ts +174 -0
- package/src/auth.ts +150 -0
- package/src/config.ts +13 -0
- package/src/index.ts +34 -0
- package/src/lib/client.ts +245 -0
- package/src/lib/helpers.ts +20 -0
- package/src/provider.contract.test.ts +75 -0
- package/src/scopes.ts +22 -0
- package/src/spec.ts +13 -0
- package/src/tools/generate-keyword-ideas.ts +114 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/list-accounts.ts +90 -0
- package/src/tools/manage-ad-groups.ts +125 -0
- package/src/tools/manage-ads.ts +162 -0
- package/src/tools/manage-audience-lists.ts +134 -0
- package/src/tools/manage-bidding-strategies.ts +153 -0
- package/src/tools/manage-campaigns.ts +275 -0
- package/src/tools/manage-conversion-actions.ts +224 -0
- package/src/tools/manage-keywords.ts +167 -0
- package/src/tools/search-reports.ts +71 -0
- package/src/tools/upload-offline-conversions.ts +93 -0
- package/src/triggers/index.ts +1 -0
- package/src/triggers/lead-form-submit.ts +135 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +7 -0
|
@@ -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();
|