@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
@@ -2,6 +2,7 @@
2
2
  import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import { GoogleDocs } from '../api';
5
+ import type { Request } from '../types';
5
6
  import {
6
7
  getApiKey,
7
8
  setApiKey,
@@ -278,6 +279,192 @@ program
278
279
  }
279
280
  });
280
281
 
282
+ // ============================================
283
+ // Bulk Commands
284
+ // ============================================
285
+
286
+ const bulkCmd = program
287
+ .command('bulk')
288
+ .description('Bulk operations on documents');
289
+
290
+ bulkCmd
291
+ .command('create')
292
+ .description('Bulk create documents from titles')
293
+ .requiredOption('--titles <titles>', 'Comma-separated list of document titles')
294
+ .option('--concurrency <number>', 'Max concurrent requests', '10')
295
+ .option('--dry-run', 'Preview without making changes')
296
+ .action(async (opts) => {
297
+ try {
298
+ const client = await getClient();
299
+ if (!client.hasWriteAccess()) {
300
+ error('Creating documents requires an OAuth access token.');
301
+ process.exit(1);
302
+ }
303
+
304
+ const titles = opts.titles.split(',').map((t: string) => t.trim()).filter(Boolean);
305
+ const concurrency = parseInt(opts.concurrency, 10);
306
+
307
+ if (titles.length === 0) {
308
+ error('No titles provided');
309
+ process.exit(1);
310
+ }
311
+
312
+ info(`Creating ${titles.length} document(s) (concurrency: ${concurrency})...`);
313
+ if (opts.dryRun) info('(dry run)');
314
+
315
+ const result = await client.bulk.createDocuments({
316
+ titles,
317
+ concurrency,
318
+ dryRun: opts.dryRun,
319
+ onProgress: (current, total) => {
320
+ process.stdout.write(`\rProgress: ${current}/${total}`);
321
+ },
322
+ onError: (err, title) => {
323
+ warn(`\n Failed: ${title} - ${err.message}`);
324
+ },
325
+ });
326
+
327
+ console.log(); // newline after progress
328
+ success(`Done: ${result.success} created, ${result.failed} failed`);
329
+ if (result.errors.length > 0) {
330
+ warn('Errors:');
331
+ for (const e of result.errors) {
332
+ console.log(` - ${e.title}: ${e.error}`);
333
+ }
334
+ }
335
+ if (result.createdDocuments.length > 0) {
336
+ print(result.createdDocuments, getFormat(program));
337
+ }
338
+ } catch (err) {
339
+ error(String(err));
340
+ process.exit(1);
341
+ }
342
+ });
343
+
344
+ bulkCmd
345
+ .command('find-replace')
346
+ .description('Bulk find and replace across documents')
347
+ .requiredOption('--entries <entries>', 'JSON array of {documentId, find, replace, matchCase?}')
348
+ .option('--concurrency <number>', 'Max concurrent requests', '10')
349
+ .option('--dry-run', 'Preview without making changes')
350
+ .action(async (opts) => {
351
+ try {
352
+ const client = await getClient();
353
+ if (!client.hasWriteAccess()) {
354
+ error('Modifying documents requires an OAuth access token.');
355
+ process.exit(1);
356
+ }
357
+
358
+ let entries: Array<{documentId: string; find: string; replace: string; matchCase?: boolean}>;
359
+ try {
360
+ entries = JSON.parse(opts.entries) as Array<{documentId: string; find: string; replace: string; matchCase?: boolean}>;
361
+ } catch {
362
+ error('Invalid JSON for --entries');
363
+ process.exit(1);
364
+ }
365
+
366
+ const concurrency = parseInt(opts.concurrency, 10);
367
+
368
+ if (entries.length === 0) {
369
+ error('No entries provided');
370
+ process.exit(1);
371
+ }
372
+
373
+ info(`Processing ${entries.length} find/replace operation(s)...`);
374
+ if (opts.dryRun) info('(dry run)');
375
+
376
+ const result = await client.bulk.findReplace({
377
+ entries,
378
+ concurrency,
379
+ dryRun: opts.dryRun,
380
+ onProgress: (current, total) => {
381
+ process.stdout.write(`\rProgress: ${current}/${total}`);
382
+ },
383
+ onError: (err, entry) => {
384
+ const e = entry as { documentId: string };
385
+ warn(`\n Failed: ${e.documentId} - ${err.message}`);
386
+ },
387
+ });
388
+
389
+ console.log(); // newline after progress
390
+ success(`Done: ${result.success} succeeded, ${result.failed} failed`);
391
+ if (result.errors.length > 0) {
392
+ warn('Errors:');
393
+ for (const e of result.errors) {
394
+ console.log(` - ${e.documentId} ("${e.find}"): ${e.error}`);
395
+ }
396
+ }
397
+ if (result.results.length > 0 && !opts.dryRun) {
398
+ info('Results:');
399
+ for (const r of result.results) {
400
+ console.log(` ${r.documentId}: ${r.occurrencesChanged} occurrence(s) of "${r.find}" replaced with "${r.replace}"`);
401
+ }
402
+ }
403
+ } catch (err) {
404
+ error(String(err));
405
+ process.exit(1);
406
+ }
407
+ });
408
+
409
+ bulkCmd
410
+ .command('batch-update')
411
+ .description('Bulk batch updates across documents')
412
+ .requiredOption('--entries <entries>', 'JSON array of {documentId, requests: [...]}')
413
+ .option('--concurrency <number>', 'Max concurrent requests', '10')
414
+ .option('--dry-run', 'Preview without making changes')
415
+ .action(async (opts) => {
416
+ try {
417
+ const client = await getClient();
418
+ if (!client.hasWriteAccess()) {
419
+ error('Modifying documents requires an OAuth access token.');
420
+ process.exit(1);
421
+ }
422
+
423
+ let entries: Array<{documentId: string; requests: Request[]}>;
424
+ try {
425
+ entries = JSON.parse(opts.entries) as Array<{documentId: string; requests: Request[]}>;
426
+ } catch {
427
+ error('Invalid JSON for --entries');
428
+ process.exit(1);
429
+ }
430
+
431
+ const concurrency = parseInt(opts.concurrency, 10);
432
+
433
+ if (entries.length === 0) {
434
+ error('No entries provided');
435
+ process.exit(1);
436
+ }
437
+
438
+ info(`Processing ${entries.length} batch update(s)...`);
439
+ if (opts.dryRun) info('(dry run)');
440
+
441
+ const result = await client.bulk.batchUpdate({
442
+ entries,
443
+ concurrency,
444
+ dryRun: opts.dryRun,
445
+ onProgress: (current, total) => {
446
+ process.stdout.write(`\rProgress: ${current}/${total}`);
447
+ },
448
+ onError: (err, entry) => {
449
+ const e = entry as { documentId: string };
450
+ warn(`\n Failed: ${e.documentId} - ${err.message}`);
451
+ },
452
+ });
453
+
454
+ console.log(); // newline after progress
455
+ success(`Done: ${result.success} succeeded, ${result.failed} failed`);
456
+ if (result.errors.length > 0) {
457
+ warn('Errors:');
458
+ for (const e of result.errors) {
459
+ console.log(` - ${e.documentId}: ${e.error}`);
460
+ }
461
+ }
462
+ } catch (err) {
463
+ error(String(err));
464
+ process.exit(1);
465
+ }
466
+ });
467
+
281
468
  program
282
469
  .command('replace <documentId> <find> <replace>')
283
470
  .description('Find and replace text in a document (requires OAuth token)')
@@ -0,0 +1,505 @@
1
+ import type { DriveClient } from './client.ts';
2
+ import type { DriveFile, FileListResponse, ListFilesOptions } from '../types/index.ts';
3
+
4
+ // ============================================
5
+ // Bulk Operation Types
6
+ // ============================================
7
+
8
+ export interface BulkOperationOptions {
9
+ /** Drive search query (e.g., "name contains 'report'", "mimeType='application/pdf'") */
10
+ query: string;
11
+ /** Maximum files to process (default: 100) */
12
+ maxResults?: number;
13
+ /** Maximum concurrent API calls (default: 10) */
14
+ concurrency?: number;
15
+ /** Dry run - don't actually modify, just preview */
16
+ dryRun?: boolean;
17
+ /** Progress callback */
18
+ onProgress?: (current: number, total: number, file: FileSummary) => void;
19
+ /** Error callback */
20
+ onError?: (error: Error, file: FileSummary) => void;
21
+ }
22
+
23
+ export interface BulkDeleteOptions extends BulkOperationOptions {
24
+ /** Skip permanently deleting, move to trash instead */
25
+ trash?: boolean;
26
+ }
27
+
28
+ export interface BulkMoveOptions extends BulkOperationOptions {
29
+ /** Destination folder ID */
30
+ destinationFolderId: string;
31
+ }
32
+
33
+ export interface BulkRenameOptions extends BulkOperationOptions {
34
+ /** Rename pattern: use 'prefix', 'suffix', or 'replace' */
35
+ mode: 'prefix' | 'suffix' | 'replace' | 'lowercase' | 'uppercase';
36
+ /** New prefix (for mode=prefix) */
37
+ prefix?: string;
38
+ /** New suffix (for mode=suffix, without the dot) */
39
+ suffix?: string;
40
+ /** Text to find (for mode=replace) */
41
+ find?: string;
42
+ /** Replacement text (for mode=replace) */
43
+ replace?: string;
44
+ }
45
+
46
+ export interface BulkShareOptions extends BulkOperationOptions {
47
+ /** Email address to share with */
48
+ email: string;
49
+ /** Permission role */
50
+ role: 'reader' | 'writer' | 'commenter';
51
+ /** Whether to send notification email */
52
+ sendNotification?: boolean;
53
+ }
54
+
55
+ export interface FileSummary {
56
+ id: string;
57
+ name: string;
58
+ mimeType: string;
59
+ size?: string;
60
+ parents?: string[];
61
+ webViewLink?: string;
62
+ }
63
+
64
+ export interface BulkOperationResult {
65
+ total: number;
66
+ success: number;
67
+ failed: number;
68
+ skipped: number;
69
+ errors: Array<{ fileId: string; error: string }>;
70
+ processedFiles: FileSummary[];
71
+ }
72
+
73
+ export interface PreviewResult {
74
+ files: FileSummary[];
75
+ total: number;
76
+ query: string;
77
+ }
78
+
79
+ // ============================================
80
+ // Bulk Operations API
81
+ // ============================================
82
+
83
+ export class BulkApi {
84
+ private readonly client: DriveClient;
85
+
86
+ constructor(client: DriveClient) {
87
+ this.client = client;
88
+ }
89
+
90
+ // ============================================
91
+ // Preview Operations
92
+ // ============================================
93
+
94
+ /**
95
+ * Preview files matching a query without making changes
96
+ */
97
+ async preview(query: string, maxResults: number = 50): Promise<PreviewResult> {
98
+ const files = await this.fetchFiles(query, maxResults);
99
+ return { files, total: files.length, query };
100
+ }
101
+
102
+ // ============================================
103
+ // Trash/Delete Operations
104
+ // ============================================
105
+
106
+ /**
107
+ * Bulk move files to trash
108
+ */
109
+ async trash(options: BulkOperationOptions): Promise<BulkOperationResult> {
110
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
111
+
112
+ return this.executeBatch(files, {
113
+ dryRun: options.dryRun || false,
114
+ concurrency: options.concurrency || 10,
115
+ onProgress: options.onProgress,
116
+ onError: options.onError,
117
+ operation: async (file) => {
118
+ await this.client.patch<DriveFile>('/files/' + file.id, { trashed: true }, {
119
+ fields: 'id,name',
120
+ supportsAllDrives: true,
121
+ });
122
+ },
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Bulk permanently delete files (DANGER!)
128
+ */
129
+ async delete(options: BulkDeleteOptions): Promise<BulkOperationResult> {
130
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
131
+
132
+ return this.executeBatch(files, {
133
+ dryRun: options.dryRun || false,
134
+ concurrency: options.concurrency || 10,
135
+ onProgress: options.onProgress,
136
+ onError: options.onError,
137
+ operation: async (file) => {
138
+ if (options.trash) {
139
+ await this.client.patch<DriveFile>('/files/' + file.id, { trashed: true }, {
140
+ fields: 'id,name',
141
+ supportsAllDrives: true,
142
+ });
143
+ } else {
144
+ await this.client.delete('/files/' + file.id, { supportsAllDrives: true });
145
+ }
146
+ },
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Bulk restore files from trash
152
+ */
153
+ async untrash(options: BulkOperationOptions): Promise<BulkOperationResult> {
154
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
155
+
156
+ return this.executeBatch(files, {
157
+ dryRun: options.dryRun || false,
158
+ concurrency: options.concurrency || 10,
159
+ onProgress: options.onProgress,
160
+ onError: options.onError,
161
+ operation: async (file) => {
162
+ await this.client.patch<DriveFile>('/files/' + file.id, { trashed: false }, {
163
+ fields: 'id,name',
164
+ supportsAllDrives: true,
165
+ });
166
+ },
167
+ });
168
+ }
169
+
170
+ // ============================================
171
+ // Move Operations
172
+ // ============================================
173
+
174
+ /**
175
+ * Bulk move files to a different folder
176
+ */
177
+ async move(options: BulkMoveOptions): Promise<BulkOperationResult> {
178
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
179
+
180
+ return this.executeBatch(files, {
181
+ dryRun: options.dryRun || false,
182
+ concurrency: options.concurrency || 10,
183
+ onProgress: options.onProgress,
184
+ onError: options.onError,
185
+ operation: async (file) => {
186
+ // Get current parents to remove them
187
+ const fullFile = await this.client.get<DriveFile>('/files/' + file.id, {
188
+ fields: 'parents',
189
+ supportsAllDrives: true,
190
+ });
191
+ const currentParents = fullFile.parents?.join(',') || '';
192
+
193
+ await this.client.patch<DriveFile>('/files/' + file.id, {}, {
194
+ addParents: options.destinationFolderId,
195
+ removeParents: currentParents,
196
+ fields: 'id,name',
197
+ supportsAllDrives: true,
198
+ });
199
+ },
200
+ });
201
+ }
202
+
203
+ // ============================================
204
+ // Rename Operations
205
+ // ============================================
206
+
207
+ /**
208
+ * Bulk rename files based on a pattern
209
+ */
210
+ async rename(options: BulkRenameOptions): Promise<BulkOperationResult> {
211
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
212
+
213
+ return this.executeBatch(files, {
214
+ dryRun: options.dryRun || false,
215
+ concurrency: options.concurrency || 10,
216
+ onProgress: options.onProgress,
217
+ onError: options.onError,
218
+ operation: async (file) => {
219
+ const newName = this.applyRenamePattern(file.name, options);
220
+ await this.client.patch<DriveFile>('/files/' + file.id, { name: newName }, {
221
+ fields: 'id,name',
222
+ supportsAllDrives: true,
223
+ });
224
+ },
225
+ });
226
+ }
227
+
228
+ // ============================================
229
+ // Share Operations
230
+ // ============================================
231
+
232
+ /**
233
+ * Bulk share files with a user
234
+ */
235
+ async share(options: BulkShareOptions): Promise<BulkOperationResult> {
236
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
237
+
238
+ return this.executeBatch(files, {
239
+ dryRun: options.dryRun || false,
240
+ concurrency: options.concurrency || 10,
241
+ onProgress: options.onProgress,
242
+ onError: options.onError,
243
+ operation: async (file) => {
244
+ const permission = {
245
+ type: 'user' as const,
246
+ role: options.role,
247
+ emailAddress: options.email,
248
+ };
249
+ const params: Record<string, string | number | boolean | undefined> = {
250
+ supportsAllDrives: true,
251
+ sendNotificationEmail: options.sendNotification ?? true,
252
+ };
253
+ await this.client.post('/files/' + file.id + '/permissions', permission as Record<string, unknown>, params);
254
+ },
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Bulk make files publicly accessible
260
+ */
261
+ async makePublic(options: BulkOperationOptions): Promise<BulkOperationResult> {
262
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
263
+
264
+ return this.executeBatch(files, {
265
+ dryRun: options.dryRun || false,
266
+ concurrency: options.concurrency || 10,
267
+ onProgress: options.onProgress,
268
+ onError: options.onError,
269
+ operation: async (file) => {
270
+ await this.client.post('/files/' + file.id + '/permissions', {
271
+ type: 'anyone',
272
+ role: 'reader',
273
+ }, {
274
+ supportsAllDrives: true,
275
+ sendNotificationEmail: false,
276
+ });
277
+ },
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Bulk remove public access from files
283
+ */
284
+ async removePublicAccess(options: BulkOperationOptions): Promise<BulkOperationResult> {
285
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
286
+
287
+ return this.executeBatch(files, {
288
+ dryRun: options.dryRun || false,
289
+ concurrency: options.concurrency || 10,
290
+ onProgress: options.onProgress,
291
+ onError: options.onError,
292
+ operation: async (file) => {
293
+ // List permissions to find the "anyone" permission
294
+ const perms = await this.client.get<{ permissions: Array<{ id: string; type: string }> }>(
295
+ '/files/' + file.id + '/permissions',
296
+ { supportsAllDrives: true, fields: 'permissions(id,type)' }
297
+ );
298
+ const anyonePerm = perms.permissions.find(p => p.type === 'anyone');
299
+ if (anyonePerm) {
300
+ await this.client.delete('/files/' + file.id + '/permissions/' + anyonePerm.id, {
301
+ supportsAllDrives: true,
302
+ });
303
+ }
304
+ },
305
+ });
306
+ }
307
+
308
+ // ============================================
309
+ // Star Operations
310
+ // ============================================
311
+
312
+ /**
313
+ * Bulk star files
314
+ */
315
+ async star(options: BulkOperationOptions): Promise<BulkOperationResult> {
316
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
317
+
318
+ return this.executeBatch(files, {
319
+ dryRun: options.dryRun || false,
320
+ concurrency: options.concurrency || 10,
321
+ onProgress: options.onProgress,
322
+ onError: options.onError,
323
+ operation: async (file) => {
324
+ await this.client.patch<DriveFile>('/files/' + file.id, { starred: true }, {
325
+ fields: 'id,name',
326
+ supportsAllDrives: true,
327
+ });
328
+ },
329
+ });
330
+ }
331
+
332
+ /**
333
+ * Bulk unstar files
334
+ */
335
+ async unstar(options: BulkOperationOptions): Promise<BulkOperationResult> {
336
+ const files = await this.fetchFiles(options.query, options.maxResults || 100);
337
+
338
+ return this.executeBatch(files, {
339
+ dryRun: options.dryRun || false,
340
+ concurrency: options.concurrency || 10,
341
+ onProgress: options.onProgress,
342
+ onError: options.onError,
343
+ operation: async (file) => {
344
+ await this.client.patch<DriveFile>('/files/' + file.id, { starred: false }, {
345
+ fields: 'id,name',
346
+ supportsAllDrives: true,
347
+ });
348
+ },
349
+ });
350
+ }
351
+
352
+ // ============================================
353
+ // Helper Methods
354
+ // ============================================
355
+
356
+ /**
357
+ * Fetch files matching a query with metadata
358
+ */
359
+ private async fetchFiles(query: string, maxResults: number): Promise<FileSummary[]> {
360
+ const files: FileSummary[] = [];
361
+ let pageToken: string | undefined;
362
+ const fields = 'id,name,mimeType,size,parents,webViewLink';
363
+
364
+ while (files.length < maxResults) {
365
+ const params: Record<string, string | number | boolean | undefined> = {
366
+ pageSize: Math.min(100, maxResults - files.length),
367
+ pageToken,
368
+ q: query,
369
+ fields: 'nextPageToken,files(' + fields + ')',
370
+ supportsAllDrives: true,
371
+ includeItemsFromAllDrives: true,
372
+ };
373
+
374
+ const response = await this.client.get<FileListResponse>('/files', params);
375
+
376
+ if (!response.files || response.files.length === 0) {
377
+ break;
378
+ }
379
+
380
+ for (const f of response.files) {
381
+ files.push({
382
+ id: f.id,
383
+ name: f.name,
384
+ mimeType: f.mimeType,
385
+ size: f.size,
386
+ parents: f.parents,
387
+ webViewLink: f.webViewLink,
388
+ });
389
+ }
390
+
391
+ pageToken = response.nextPageToken;
392
+ if (!pageToken) break;
393
+ }
394
+
395
+ return files;
396
+ }
397
+
398
+ /**
399
+ * Apply rename pattern to a file name
400
+ */
401
+ private applyRenamePattern(name: string, options: BulkRenameOptions): string {
402
+ switch (options.mode) {
403
+ case 'prefix':
404
+ return (options.prefix || '') + name;
405
+ case 'suffix': {
406
+ const dotIndex = name.lastIndexOf('.');
407
+ if (dotIndex > 0 && !name.startsWith('.')) {
408
+ return name.slice(0, dotIndex) + (options.suffix || '') + name.slice(dotIndex);
409
+ }
410
+ return name + (options.suffix || '');
411
+ }
412
+ case 'replace':
413
+ return options.find ? name.replace(new RegExp(options.find, 'g'), options.replace || '') : name;
414
+ case 'lowercase': {
415
+ const dotIndex = name.lastIndexOf('.');
416
+ if (dotIndex > 0 && !name.startsWith('.')) {
417
+ return name.slice(0, dotIndex).toLowerCase() + name.slice(dotIndex);
418
+ }
419
+ return name.toLowerCase();
420
+ }
421
+ case 'uppercase': {
422
+ const dotIndex = name.lastIndexOf('.');
423
+ if (dotIndex > 0 && !name.startsWith('.')) {
424
+ return name.slice(0, dotIndex).toUpperCase() + name.slice(dotIndex);
425
+ }
426
+ return name.toUpperCase();
427
+ }
428
+ default:
429
+ return name;
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Execute operations in batches with concurrency control
435
+ */
436
+ private async executeBatch(
437
+ files: FileSummary[],
438
+ options: {
439
+ dryRun: boolean;
440
+ concurrency: number;
441
+ onProgress?: (current: number, total: number, file: FileSummary) => void;
442
+ onError?: (error: Error, file: FileSummary) => void;
443
+ operation: (file: FileSummary) => Promise<void>;
444
+ }
445
+ ): Promise<BulkOperationResult> {
446
+ const { dryRun, concurrency, onProgress, onError, operation } = options;
447
+
448
+ const result: BulkOperationResult = {
449
+ total: files.length,
450
+ success: 0,
451
+ failed: 0,
452
+ skipped: 0,
453
+ errors: [],
454
+ processedFiles: [],
455
+ };
456
+
457
+ if (files.length === 0) {
458
+ return result;
459
+ }
460
+
461
+ const chunks = this.chunkArray(files, concurrency);
462
+
463
+ for (const chunk of chunks) {
464
+ await Promise.all(
465
+ chunk.map(async (file) => {
466
+ try {
467
+ if (dryRun) {
468
+ result.success++;
469
+ result.processedFiles.push(file);
470
+ } else {
471
+ await operation(file);
472
+ result.success++;
473
+ result.processedFiles.push(file);
474
+ }
475
+
476
+ if (onProgress) {
477
+ onProgress(result.success + result.failed, result.total, file);
478
+ }
479
+ } catch (err) {
480
+ result.failed++;
481
+ const errorMessage = err instanceof Error ? err.message : String(err);
482
+ result.errors.push({ fileId: file.id, error: errorMessage });
483
+
484
+ if (onError) {
485
+ onError(err instanceof Error ? err : new Error(errorMessage), file);
486
+ }
487
+ }
488
+ })
489
+ );
490
+ }
491
+
492
+ return result;
493
+ }
494
+
495
+ /**
496
+ * Split array into chunks
497
+ */
498
+ private chunkArray<T>(array: T[], size: number): T[][] {
499
+ const chunks: T[][] = [];
500
+ for (let i = 0; i < array.length; i += size) {
501
+ chunks.push(array.slice(i, i + size));
502
+ }
503
+ return chunks;
504
+ }
505
+ }