@hasna/connectors 1.3.22 → 1.3.23

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 (63) hide show
  1. package/bin/index.js +109 -59
  2. package/bin/mcp.js +236 -182
  3. package/bin/serve.js +77 -33
  4. package/connectors/connect-dropbox/src/api/bulk.ts +285 -0
  5. package/connectors/connect-dropbox/src/api/index.ts +4 -0
  6. package/connectors/connect-dropbox/src/cli/index.ts +151 -0
  7. package/connectors/connect-github/src/api/bulk.ts +328 -0
  8. package/connectors/connect-github/src/api/index.ts +4 -0
  9. package/connectors/connect-github/src/cli/index.ts +145 -0
  10. package/connectors/connect-gmail/src/api/history.ts +82 -0
  11. package/connectors/connect-gmail/src/api/index.ts +12 -0
  12. package/connectors/connect-gmail/src/api/settings.ts +280 -0
  13. package/connectors/connect-gmail/src/api/watch.ts +40 -0
  14. package/connectors/connect-gmail/src/cli/index.ts +237 -0
  15. package/connectors/connect-googleads/src/api/bulk.ts +354 -0
  16. package/connectors/connect-googleads/src/api/index.ts +4 -0
  17. package/connectors/connect-googleads/src/cli/index.ts +192 -0
  18. package/connectors/connect-googlecalendar/src/api/acl.ts +66 -0
  19. package/connectors/connect-googlecalendar/src/api/bulk.ts +452 -0
  20. package/connectors/connect-googlecalendar/src/api/freebusy.ts +35 -0
  21. package/connectors/connect-googlecalendar/src/api/index.ts +9 -0
  22. package/connectors/connect-googlecalendar/src/cli/index.ts +522 -0
  23. package/connectors/connect-googlecalendar/src/types/index.ts +90 -0
  24. package/connectors/connect-googlecontacts/src/api/bulk.ts +365 -0
  25. package/connectors/connect-googlecontacts/src/api/groups.ts +164 -0
  26. package/connectors/connect-googlecontacts/src/api/index.ts +8 -0
  27. package/connectors/connect-googlecontacts/src/cli/index.ts +340 -0
  28. package/connectors/connect-googledocs/src/api/bulk.ts +301 -0
  29. package/connectors/connect-googledocs/src/api/index.ts +4 -0
  30. package/connectors/connect-googledocs/src/cli/index.ts +187 -0
  31. package/connectors/connect-googledrive/src/api/bulk.ts +505 -0
  32. package/connectors/connect-googledrive/src/api/changes.ts +139 -0
  33. package/connectors/connect-googledrive/src/api/client.ts +34 -0
  34. package/connectors/connect-googledrive/src/api/index.ts +12 -0
  35. package/connectors/connect-googledrive/src/api/revisions.ts +132 -0
  36. package/connectors/connect-googledrive/src/cli/index.ts +624 -0
  37. package/connectors/connect-googledrive/src/index.ts +2 -0
  38. package/connectors/connect-googlephotos/src/api/bulk.ts +455 -0
  39. package/connectors/connect-googlephotos/src/api/index.ts +8 -0
  40. package/connectors/connect-googlephotos/src/cli/index.ts +185 -0
  41. package/connectors/connect-googletasks/src/api/bulk.ts +359 -0
  42. package/connectors/connect-googletasks/src/api/index.ts +1 -0
  43. package/connectors/connect-googletasks/src/cli/index.ts +178 -0
  44. package/connectors/connect-linear/src/api/bulk.ts +219 -0
  45. package/connectors/connect-linear/src/api/index.ts +4 -0
  46. package/connectors/connect-linear/src/cli/index.ts +116 -0
  47. package/connectors/connect-shopify/src/api/bulk.ts +288 -0
  48. package/connectors/connect-shopify/src/api/index.ts +4 -0
  49. package/connectors/connect-shopify/src/cli/index.ts +108 -0
  50. package/connectors/connect-slack/src/api/bulk.ts +237 -0
  51. package/connectors/connect-slack/src/api/index.ts +4 -0
  52. package/connectors/connect-slack/src/cli/index.ts +157 -0
  53. package/connectors/connect-stripe/src/api/bulk.ts +488 -0
  54. package/connectors/connect-stripe/src/api/index.ts +4 -0
  55. package/connectors/connect-stripe/src/cli/index.ts +206 -0
  56. package/dist/index.js +75 -33
  57. package/dist/lib/runner.d.ts +15 -5
  58. package/package.json +2 -2
  59. package/connectors/connect-cloudflare/bun.lock +0 -32
  60. package/connectors/connect-gmail/bin/index.js +0 -7027
  61. package/connectors/connect-gmail/dist/cli/index.js +0 -7035
  62. package/connectors/connect-gmail/dist/index.js +0 -2174
  63. package/dist/server/server.test.d.ts +0 -1
@@ -0,0 +1,354 @@
1
+ import type { GoogleAdsClient } from './client';
2
+ import type { MutateResponse } from '../types';
3
+
4
+ // ============================================
5
+ // Bulk Operation Types
6
+ // ============================================
7
+
8
+ export interface BulkOperationOptions {
9
+ /** Maximum concurrent API calls (default: 10) */
10
+ concurrency?: number;
11
+ /** Dry run - don't actually modify */
12
+ dryRun?: boolean;
13
+ /** Progress callback */
14
+ onProgress?: (current: number, total: number, item: unknown) => void;
15
+ /** Error callback */
16
+ onError?: (error: Error, item: unknown) => void;
17
+ }
18
+
19
+ // --- Status Change Bulk Operations ---
20
+
21
+ export interface StatusChangeEntry {
22
+ resourceId: string;
23
+ /** The resource path segment, e.g. 'campaigns', 'adGroups', 'adGroupAds', 'adGroupCriteria' */
24
+ resourceType: 'campaign' | 'adGroup' | 'ad' | 'keyword';
25
+ /** Additional identifiers for composite resources */
26
+ parentId?: string;
27
+ }
28
+
29
+ export interface BulkStatusChangeOptions extends BulkOperationOptions {
30
+ /** Entries to update */
31
+ entries: StatusChangeEntry[];
32
+ /** Target status */
33
+ status: 'ENABLED' | 'PAUSED' | 'REMOVED';
34
+ }
35
+
36
+ export interface BulkStatusChangeResult {
37
+ total: number;
38
+ success: number;
39
+ failed: number;
40
+ errors: Array<{ resourceId: string; error: string }>;
41
+ results: Array<{ resourceId: string; response: MutateResponse }>;
42
+ }
43
+
44
+ // --- Campaign Bulk Operations ---
45
+
46
+ export interface BulkCampaignOptions extends BulkOperationOptions {
47
+ campaignIds: string[];
48
+ status: 'ENABLED' | 'PAUSED' | 'REMOVED';
49
+ }
50
+
51
+ export interface BulkCampaignResult {
52
+ total: number;
53
+ success: number;
54
+ failed: number;
55
+ errors: Array<{ campaignId: string; error: string }>;
56
+ results: Array<{ campaignId: string; response: MutateResponse }>;
57
+ }
58
+
59
+ // --- Ad Group Bulk Operations ---
60
+
61
+ export interface BulkAdGroupOptions extends BulkOperationOptions {
62
+ adGroupIds: string[];
63
+ status: 'ENABLED' | 'PAUSED';
64
+ }
65
+
66
+ export interface BulkAdGroupResult {
67
+ total: number;
68
+ success: number;
69
+ failed: number;
70
+ errors: Array<{ adGroupId: string; error: string }>;
71
+ results: Array<{ adGroupId: string; response: MutateResponse }>;
72
+ }
73
+
74
+ // --- Ad Bulk Operations ---
75
+
76
+ export interface BulkAdOptions extends BulkOperationOptions {
77
+ /** Entries with adGroupId and adId pairs */
78
+ entries: Array<{ adGroupId: string; adId: string }>;
79
+ status: 'ENABLED' | 'PAUSED' | 'REMOVED';
80
+ }
81
+
82
+ export interface BulkAdResult {
83
+ total: number;
84
+ success: number;
85
+ failed: number;
86
+ errors: Array<{ adGroupId: string; adId: string; error: string }>;
87
+ results: Array<{ adGroupId: string; adId: string; response: MutateResponse }>;
88
+ }
89
+
90
+ // --- Keyword Bulk Operations ---
91
+
92
+ export interface BulkKeywordOptions extends BulkOperationOptions {
93
+ /** Entries with adGroupId and criterionId pairs */
94
+ entries: Array<{ adGroupId: string; criterionId: string }>;
95
+ status: 'ENABLED' | 'PAUSED' | 'REMOVED';
96
+ }
97
+
98
+ export interface BulkKeywordResult {
99
+ total: number;
100
+ success: number;
101
+ failed: number;
102
+ errors: Array<{ adGroupId: string; criterionId: string; error: string }>;
103
+ results: Array<{ adGroupId: string; criterionId: string; response: MutateResponse }>;
104
+ }
105
+
106
+ // ============================================
107
+ // Bulk Operations API
108
+ // ============================================
109
+
110
+ export class BulkApi {
111
+ private readonly client: GoogleAdsClient;
112
+
113
+ constructor(client: GoogleAdsClient) {
114
+ this.client = client;
115
+ }
116
+
117
+ // ============================================
118
+ // Bulk Campaign Status Changes
119
+ // ============================================
120
+
121
+ async campaigns(options: BulkCampaignOptions): Promise<BulkCampaignResult> {
122
+ const { campaignIds, status, concurrency = 10, dryRun = false, onProgress, onError } = options;
123
+
124
+ const result: BulkCampaignResult = {
125
+ total: campaignIds.length,
126
+ success: 0,
127
+ failed: 0,
128
+ errors: [],
129
+ results: [],
130
+ };
131
+
132
+ if (campaignIds.length === 0) return result;
133
+
134
+ const chunks = this.chunkArray(campaignIds, concurrency);
135
+
136
+ for (const chunk of chunks) {
137
+ await Promise.all(
138
+ chunk.map(async (campaignId) => {
139
+ try {
140
+ if (dryRun) {
141
+ result.success++;
142
+ } else {
143
+ const operation = status === 'REMOVED'
144
+ ? { remove: `customers/${this.client.getCustomerId()}/campaigns/${campaignId}` }
145
+ : {
146
+ update: {
147
+ resourceName: `customers/${this.client.getCustomerId()}/campaigns/${campaignId}`,
148
+ status,
149
+ },
150
+ updateMask: 'status',
151
+ };
152
+
153
+ const response = await this.client.mutate('campaigns', [operation]);
154
+ result.success++;
155
+ result.results.push({ campaignId, response });
156
+ }
157
+
158
+ if (onProgress) onProgress(result.success + result.failed, result.total, campaignId);
159
+ } catch (err) {
160
+ result.failed++;
161
+ const errorMessage = err instanceof Error ? err.message : String(err);
162
+ result.errors.push({ campaignId, error: errorMessage });
163
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), campaignId);
164
+ }
165
+ })
166
+ );
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ // ============================================
173
+ // Bulk Ad Group Status Changes
174
+ // ============================================
175
+
176
+ async adGroups(options: BulkAdGroupOptions): Promise<BulkAdGroupResult> {
177
+ const { adGroupIds, status, concurrency = 10, dryRun = false, onProgress, onError } = options;
178
+
179
+ const result: BulkAdGroupResult = {
180
+ total: adGroupIds.length,
181
+ success: 0,
182
+ failed: 0,
183
+ errors: [],
184
+ results: [],
185
+ };
186
+
187
+ if (adGroupIds.length === 0) return result;
188
+
189
+ const chunks = this.chunkArray(adGroupIds, concurrency);
190
+
191
+ for (const chunk of chunks) {
192
+ await Promise.all(
193
+ chunk.map(async (adGroupId) => {
194
+ try {
195
+ if (dryRun) {
196
+ result.success++;
197
+ } else {
198
+ const operation = {
199
+ update: {
200
+ resourceName: `customers/${this.client.getCustomerId()}/adGroups/${adGroupId}`,
201
+ status,
202
+ },
203
+ updateMask: 'status',
204
+ };
205
+
206
+ const response = await this.client.mutate('adGroups', [operation]);
207
+ result.success++;
208
+ result.results.push({ adGroupId, response });
209
+ }
210
+
211
+ if (onProgress) onProgress(result.success + result.failed, result.total, adGroupId);
212
+ } catch (err) {
213
+ result.failed++;
214
+ const errorMessage = err instanceof Error ? err.message : String(err);
215
+ result.errors.push({ adGroupId, error: errorMessage });
216
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), adGroupId);
217
+ }
218
+ })
219
+ );
220
+ }
221
+
222
+ return result;
223
+ }
224
+
225
+ // ============================================
226
+ // Bulk Ad Status Changes
227
+ // ============================================
228
+
229
+ async ads(options: BulkAdOptions): Promise<BulkAdResult> {
230
+ const { entries, status, concurrency = 10, dryRun = false, onProgress, onError } = options;
231
+
232
+ const result: BulkAdResult = {
233
+ total: entries.length,
234
+ success: 0,
235
+ failed: 0,
236
+ errors: [],
237
+ results: [],
238
+ };
239
+
240
+ if (entries.length === 0) return result;
241
+
242
+ const chunks = this.chunkArray(entries, concurrency);
243
+
244
+ for (const chunk of chunks) {
245
+ await Promise.all(
246
+ chunk.map(async (entry) => {
247
+ try {
248
+ if (dryRun) {
249
+ result.success++;
250
+ } else {
251
+ const operation = status === 'REMOVED'
252
+ ? { remove: `customers/${this.client.getCustomerId()}/adGroupAds/${entry.adGroupId}~${entry.adId}` }
253
+ : {
254
+ update: {
255
+ resourceName: `customers/${this.client.getCustomerId()}/adGroupAds/${entry.adGroupId}~${entry.adId}`,
256
+ status,
257
+ },
258
+ updateMask: 'status',
259
+ };
260
+
261
+ const response = await this.client.mutate('adGroupAds', [operation]);
262
+ result.success++;
263
+ result.results.push({ adGroupId: entry.adGroupId, adId: entry.adId, response });
264
+ }
265
+
266
+ if (onProgress) onProgress(result.success + result.failed, result.total, entry);
267
+ } catch (err) {
268
+ result.failed++;
269
+ const errorMessage = err instanceof Error ? err.message : String(err);
270
+ result.errors.push({
271
+ adGroupId: entry.adGroupId,
272
+ adId: entry.adId,
273
+ error: errorMessage,
274
+ });
275
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), entry);
276
+ }
277
+ })
278
+ );
279
+ }
280
+
281
+ return result;
282
+ }
283
+
284
+ // ============================================
285
+ // Bulk Keyword Status Changes
286
+ // ============================================
287
+
288
+ async keywords(options: BulkKeywordOptions): Promise<BulkKeywordResult> {
289
+ const { entries, status, concurrency = 10, dryRun = false, onProgress, onError } = options;
290
+
291
+ const result: BulkKeywordResult = {
292
+ total: entries.length,
293
+ success: 0,
294
+ failed: 0,
295
+ errors: [],
296
+ results: [],
297
+ };
298
+
299
+ if (entries.length === 0) return result;
300
+
301
+ const chunks = this.chunkArray(entries, concurrency);
302
+
303
+ for (const chunk of chunks) {
304
+ await Promise.all(
305
+ chunk.map(async (entry) => {
306
+ try {
307
+ if (dryRun) {
308
+ result.success++;
309
+ } else {
310
+ const operation = status === 'REMOVED'
311
+ ? { remove: `customers/${this.client.getCustomerId()}/adGroupCriteria/${entry.adGroupId}~${entry.criterionId}` }
312
+ : {
313
+ update: {
314
+ resourceName: `customers/${this.client.getCustomerId()}/adGroupCriteria/${entry.adGroupId}~${entry.criterionId}`,
315
+ status,
316
+ },
317
+ updateMask: 'status',
318
+ };
319
+
320
+ const response = await this.client.mutate('adGroupCriteria', [operation]);
321
+ result.success++;
322
+ result.results.push({ adGroupId: entry.adGroupId, criterionId: entry.criterionId, response });
323
+ }
324
+
325
+ if (onProgress) onProgress(result.success + result.failed, result.total, entry);
326
+ } catch (err) {
327
+ result.failed++;
328
+ const errorMessage = err instanceof Error ? err.message : String(err);
329
+ result.errors.push({
330
+ adGroupId: entry.adGroupId,
331
+ criterionId: entry.criterionId,
332
+ error: errorMessage,
333
+ });
334
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), entry);
335
+ }
336
+ })
337
+ );
338
+ }
339
+
340
+ return result;
341
+ }
342
+
343
+ // ============================================
344
+ // Helper Methods
345
+ // ============================================
346
+
347
+ private chunkArray<T>(array: T[], size: number): T[][] {
348
+ const chunks: T[][] = [];
349
+ for (let i = 0; i < array.length; i += size) {
350
+ chunks.push(array.slice(i, i + size));
351
+ }
352
+ return chunks;
353
+ }
354
+ }
@@ -4,6 +4,7 @@ import { AdGroupsApi } from './adGroups';
4
4
  import { AdsApi } from './ads';
5
5
  import { KeywordsApi } from './keywords';
6
6
  import { ReportsApi } from './reports';
7
+ import { BulkApi } from './bulk';
7
8
  import type { GoogleAdsConfig, SearchResponse } from '../types';
8
9
 
9
10
  export class GoogleAds {
@@ -14,6 +15,7 @@ export class GoogleAds {
14
15
  public ads: AdsApi;
15
16
  public keywords: KeywordsApi;
16
17
  public reports: ReportsApi;
18
+ public bulk: BulkApi;
17
19
 
18
20
  constructor(config: GoogleAdsConfig) {
19
21
  this.client = new GoogleAdsClient(config);
@@ -23,6 +25,7 @@ export class GoogleAds {
23
25
  this.ads = new AdsApi(this.client);
24
26
  this.keywords = new KeywordsApi(this.client);
25
27
  this.reports = new ReportsApi(this.client);
28
+ this.bulk = new BulkApi(this.client);
26
29
  }
27
30
 
28
31
  /**
@@ -74,3 +77,4 @@ export { AdGroupsApi } from './adGroups';
74
77
  export { AdsApi } from './ads';
75
78
  export { KeywordsApi } from './keywords';
76
79
  export { ReportsApi } from './reports';
80
+ export { BulkApi } from './bulk';
@@ -1252,6 +1252,198 @@ reports
1252
1252
  }
1253
1253
  });
1254
1254
 
1255
+ // ============================================
1256
+ // Bulk Commands
1257
+ // ============================================
1258
+
1259
+ const bulk = program.command('bulk').description('Bulk operations');
1260
+
1261
+ bulk
1262
+ .command('campaigns')
1263
+ .description('Bulk update campaign status')
1264
+ .requiredOption('--ids <ids>', 'Comma-separated campaign IDs')
1265
+ .requiredOption('--status <status>', 'Target status (ENABLED, PAUSED, REMOVED)')
1266
+ .option('--concurrency <number>', 'Max concurrent requests', '10')
1267
+ .option('--dry-run', 'Preview without making changes')
1268
+ .action(async (opts) => {
1269
+ const campaignIds = opts.ids.split(',').map((id: string) => id.trim()).filter(Boolean);
1270
+ const concurrency = parseInt(opts.concurrency, 10);
1271
+
1272
+ if (campaignIds.length === 0) {
1273
+ error('No campaign IDs provided');
1274
+ process.exit(1);
1275
+ }
1276
+
1277
+ const client = await getClient(program.opts().customer);
1278
+ info(`Updating ${campaignIds.length} campaign(s) to ${opts.status}...`);
1279
+ if (opts.dryRun) info('(dry run)');
1280
+
1281
+ const result = await client.bulk.campaigns({
1282
+ campaignIds,
1283
+ status: opts.status,
1284
+ concurrency,
1285
+ dryRun: opts.dryRun,
1286
+ onProgress: (current, total) => {
1287
+ process.stdout.write(`\rProgress: ${current}/${total}`);
1288
+ },
1289
+ onError: (err, id) => {
1290
+ console.error(`\n Failed: ${id} - ${err.message}`);
1291
+ },
1292
+ });
1293
+
1294
+ console.log();
1295
+ success(`Done: ${result.success} updated, ${result.failed} failed`);
1296
+ if (result.errors.length > 0) {
1297
+ warn('Errors:');
1298
+ for (const e of result.errors) {
1299
+ console.log(` - ${e.campaignId}: ${e.error}`);
1300
+ }
1301
+ }
1302
+ });
1303
+
1304
+ bulk
1305
+ .command('adgroups')
1306
+ .description('Bulk update ad group status')
1307
+ .requiredOption('--ids <ids>', 'Comma-separated ad group IDs')
1308
+ .requiredOption('--status <status>', 'Target status (ENABLED, PAUSED)')
1309
+ .option('--concurrency <number>', 'Max concurrent requests', '10')
1310
+ .option('--dry-run', 'Preview without making changes')
1311
+ .action(async (opts) => {
1312
+ const adGroupIds = opts.ids.split(',').map((id: string) => id.trim()).filter(Boolean);
1313
+ const concurrency = parseInt(opts.concurrency, 10);
1314
+
1315
+ if (adGroupIds.length === 0) {
1316
+ error('No ad group IDs provided');
1317
+ process.exit(1);
1318
+ }
1319
+
1320
+ const client = await getClient(program.opts().customer);
1321
+ info(`Updating ${adGroupIds.length} ad group(s) to ${opts.status}...`);
1322
+ if (opts.dryRun) info('(dry run)');
1323
+
1324
+ const result = await client.bulk.adGroups({
1325
+ adGroupIds,
1326
+ status: opts.status,
1327
+ concurrency,
1328
+ dryRun: opts.dryRun,
1329
+ onProgress: (current, total) => {
1330
+ process.stdout.write(`\rProgress: ${current}/${total}`);
1331
+ },
1332
+ onError: (err, id) => {
1333
+ console.error(`\n Failed: ${id} - ${err.message}`);
1334
+ },
1335
+ });
1336
+
1337
+ console.log();
1338
+ success(`Done: ${result.success} updated, ${result.failed} failed`);
1339
+ if (result.errors.length > 0) {
1340
+ warn('Errors:');
1341
+ for (const e of result.errors) {
1342
+ console.log(` - ${e.adGroupId}: ${e.error}`);
1343
+ }
1344
+ }
1345
+ });
1346
+
1347
+ bulk
1348
+ .command('ads')
1349
+ .description('Bulk update ad status')
1350
+ .requiredOption('--entries <entries>', 'JSON array of {adGroupId, adId}')
1351
+ .requiredOption('--status <status>', 'Target status (ENABLED, PAUSED, REMOVED)')
1352
+ .option('--concurrency <number>', 'Max concurrent requests', '10')
1353
+ .option('--dry-run', 'Preview without making changes')
1354
+ .action(async (opts) => {
1355
+ let entries: Array<{ adGroupId: string; adId: string }>;
1356
+ try {
1357
+ entries = JSON.parse(opts.entries) as Array<{ adGroupId: string; adId: string }>;
1358
+ } catch {
1359
+ error('Invalid JSON for --entries');
1360
+ process.exit(1);
1361
+ }
1362
+
1363
+ if (entries.length === 0) {
1364
+ error('No entries provided');
1365
+ process.exit(1);
1366
+ }
1367
+
1368
+ const client = await getClient(program.opts().customer);
1369
+ const concurrency = parseInt(opts.concurrency, 10);
1370
+ info(`Updating ${entries.length} ad(s)...`);
1371
+ if (opts.dryRun) info('(dry run)');
1372
+
1373
+ const result = await client.bulk.ads({
1374
+ entries,
1375
+ status: opts.status,
1376
+ concurrency,
1377
+ dryRun: opts.dryRun,
1378
+ onProgress: (current, total) => {
1379
+ process.stdout.write(`\rProgress: ${current}/${total}`);
1380
+ },
1381
+ onError: (err, entry) => {
1382
+ const e = entry as { adGroupId: string; adId: string };
1383
+ console.error(`\n Failed: ${e.adGroupId}/${e.adId} - ${err.message}`);
1384
+ },
1385
+ });
1386
+
1387
+ console.log();
1388
+ success(`Done: ${result.success} updated, ${result.failed} failed`);
1389
+ if (result.errors.length > 0) {
1390
+ warn('Errors:');
1391
+ for (const e of result.errors) {
1392
+ console.log(` - ${e.adGroupId}/${e.adId}: ${e.error}`);
1393
+ }
1394
+ }
1395
+ });
1396
+
1397
+ bulk
1398
+ .command('keywords')
1399
+ .description('Bulk update keyword status')
1400
+ .requiredOption('--entries <entries>', 'JSON array of {adGroupId, criterionId}')
1401
+ .requiredOption('--status <status>', 'Target status (ENABLED, PAUSED, REMOVED)')
1402
+ .option('--concurrency <number>', 'Max concurrent requests', '10')
1403
+ .option('--dry-run', 'Preview without making changes')
1404
+ .action(async (opts) => {
1405
+ let entries: Array<{ adGroupId: string; criterionId: string }>;
1406
+ try {
1407
+ entries = JSON.parse(opts.entries) as Array<{ adGroupId: string; criterionId: string }>;
1408
+ } catch {
1409
+ error('Invalid JSON for --entries');
1410
+ process.exit(1);
1411
+ }
1412
+
1413
+ if (entries.length === 0) {
1414
+ error('No entries provided');
1415
+ process.exit(1);
1416
+ }
1417
+
1418
+ const client = await getClient(program.opts().customer);
1419
+ const concurrency = parseInt(opts.concurrency, 10);
1420
+ info(`Updating ${entries.length} keyword(s)...`);
1421
+ if (opts.dryRun) info('(dry run)');
1422
+
1423
+ const result = await client.bulk.keywords({
1424
+ entries,
1425
+ status: opts.status,
1426
+ concurrency,
1427
+ dryRun: opts.dryRun,
1428
+ onProgress: (current, total) => {
1429
+ process.stdout.write(`\rProgress: ${current}/${total}`);
1430
+ },
1431
+ onError: (err, entry) => {
1432
+ const e = entry as { adGroupId: string; criterionId: string };
1433
+ console.error(`\n Failed: ${e.adGroupId}/${e.criterionId} - ${err.message}`);
1434
+ },
1435
+ });
1436
+
1437
+ console.log();
1438
+ success(`Done: ${result.success} updated, ${result.failed} failed`);
1439
+ if (result.errors.length > 0) {
1440
+ warn('Errors:');
1441
+ for (const e of result.errors) {
1442
+ console.log(` - ${e.adGroupId}/${e.criterionId}: ${e.error}`);
1443
+ }
1444
+ }
1445
+ });
1446
+
1255
1447
  // ============================================
1256
1448
  // Parse and Execute
1257
1449
  // ============================================
@@ -0,0 +1,66 @@
1
+ import type { GoogleCalendarClient } from './client';
2
+ import type { CalendarAcl, CalendarAclListResponse, CalendarAclInsert } from '../types';
3
+
4
+ /**
5
+ * Calendar ACL API module - manage calendar access control lists
6
+ */
7
+ export class AclApi {
8
+ constructor(private readonly client: GoogleCalendarClient) {}
9
+
10
+ /**
11
+ * List ACL rules for a calendar
12
+ */
13
+ async list(calendarId: string, options: { maxResults?: number; pageToken?: string } = {}): Promise<CalendarAclListResponse> {
14
+ const params: Record<string, number | string | undefined> = {};
15
+ if (options.maxResults) params.maxResults = options.maxResults;
16
+ if (options.pageToken) params.pageToken = options.pageToken;
17
+ return this.client.get<CalendarAclListResponse>(`/calendars/${encodeURIComponent(calendarId)}/acl`, params);
18
+ }
19
+
20
+ /**
21
+ * Iterate over all ACL rules (handles pagination)
22
+ */
23
+ async *listAll(calendarId: string): AsyncGenerator<CalendarAcl> {
24
+ let nextPageToken: string | undefined;
25
+ do {
26
+ const result = await this.list(calendarId, { pageToken: nextPageToken });
27
+ for (const rule of result.items) {
28
+ yield rule;
29
+ }
30
+ nextPageToken = result.nextPageToken;
31
+ } while (nextPageToken);
32
+ }
33
+
34
+ /**
35
+ * Get a specific ACL rule
36
+ */
37
+ async get(calendarId: string, ruleId: string): Promise<CalendarAcl> {
38
+ return this.client.get<CalendarAcl>(`/calendars/${encodeURIComponent(calendarId)}/acl/${encodeURIComponent(ruleId)}`);
39
+ }
40
+
41
+ /**
42
+ * Insert a new ACL rule
43
+ */
44
+ async insert(calendarId: string, rule: CalendarAclInsert): Promise<CalendarAcl> {
45
+ return this.client.post<CalendarAcl>(`/calendars/${encodeURIComponent(calendarId)}/acl`, {
46
+ role: rule.role,
47
+ scope: rule.scope,
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Delete an ACL rule
53
+ */
54
+ async delete(calendarId: string, ruleId: string): Promise<void> {
55
+ await this.client.delete<void>(`/calendars/${encodeURIComponent(calendarId)}/acl/${encodeURIComponent(ruleId)}`);
56
+ }
57
+
58
+ /**
59
+ * Update an ACL rule
60
+ */
61
+ async update(calendarId: string, ruleId: string, role: CalendarAclInsert['role']): Promise<CalendarAcl> {
62
+ return this.client.patch<CalendarAcl>(`/calendars/${encodeURIComponent(calendarId)}/acl/${encodeURIComponent(ruleId)}`, {
63
+ role,
64
+ });
65
+ }
66
+ }