@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
@@ -4,6 +4,8 @@ import chalk from 'chalk';
4
4
  import { createServer } from 'http';
5
5
  import { GoogleContacts } from '../api';
6
6
  import { ContactsApi } from '../api/contacts';
7
+ import { BulkApi } from '../api/bulk';
8
+ import type { ContactSummary } from '../api/bulk';
7
9
  import {
8
10
  getClientId,
9
11
  setClientId,
@@ -675,5 +677,343 @@ contactsCmd
675
677
  }
676
678
  });
677
679
 
680
+ // ============================================
681
+ // Groups Commands — CRUD + membership
682
+ // ============================================
683
+ const groupsCmd = program
684
+ .command('groups')
685
+ .description('Manage contact groups (CRUD + membership)');
686
+
687
+ groupsCmd
688
+ .command('list')
689
+ .description('List contact groups')
690
+ .option('-n, --max <number>', 'Maximum results', '100')
691
+ .option('--page-token <token>', 'Page token for pagination')
692
+ .action(async (opts) => {
693
+ try {
694
+ const client = await getClient();
695
+ const result = await client.groups.list({
696
+ pageSize: parseInt(opts.max),
697
+ pageToken: opts.pageToken,
698
+ });
699
+ print(result, getFormat(groupsCmd));
700
+ if ((result as any).nextPageToken) {
701
+ info(`More results available. Use --page-token ${(result as any).nextPageToken}`);
702
+ }
703
+ } catch (err) {
704
+ error(String(err));
705
+ process.exit(1);
706
+ }
707
+ });
708
+
709
+ groupsCmd
710
+ .command('get <groupResourceName>')
711
+ .description('Get a contact group')
712
+ .action(async (groupResourceName: string) => {
713
+ try {
714
+ const client = await getClient();
715
+ const group = await client.groups.get(groupResourceName);
716
+ print(group, getFormat(groupsCmd));
717
+ } catch (err) {
718
+ error(String(err));
719
+ process.exit(1);
720
+ }
721
+ });
722
+
723
+ groupsCmd
724
+ .command('create')
725
+ .description('Create a new contact group')
726
+ .requiredOption('--name <name>', 'Group name')
727
+ .action(async (opts) => {
728
+ try {
729
+ const client = await getClient();
730
+ const group = await client.groups.create({ name: opts.name });
731
+ success(`Group created: ${group.resourceName}`);
732
+ print(group, getFormat(groupsCmd));
733
+ } catch (err) {
734
+ error(String(err));
735
+ process.exit(1);
736
+ }
737
+ });
738
+
739
+ groupsCmd
740
+ .command('update <groupResourceName>')
741
+ .description('Update a contact group')
742
+ .requiredOption('--name <name>', 'New group name')
743
+ .action(async (groupResourceName: string, opts) => {
744
+ try {
745
+ const client = await getClient();
746
+ const group = await client.groups.update(groupResourceName, { name: opts.name });
747
+ success(`Group updated: ${group.resourceName}`);
748
+ print(group, getFormat(groupsCmd));
749
+ } catch (err) {
750
+ error(String(err));
751
+ process.exit(1);
752
+ }
753
+ });
754
+
755
+ groupsCmd
756
+ .command('delete <groupResourceName>')
757
+ .description('Delete a contact group')
758
+ .action(async (groupResourceName: string) => {
759
+ try {
760
+ const client = await getClient();
761
+ await client.groups.delete(groupResourceName);
762
+ success(`Group ${groupResourceName} deleted`);
763
+ } catch (err) {
764
+ error(String(err));
765
+ process.exit(1);
766
+ }
767
+ });
768
+
769
+ groupsCmd
770
+ .command('members <groupResourceName>')
771
+ .description('List members of a contact group')
772
+ .option('-n, --max <number>', 'Maximum results', '100')
773
+ .option('--page-token <token>', 'Page token for pagination')
774
+ .action(async (groupResourceName: string, opts) => {
775
+ try {
776
+ const client = await getClient();
777
+ const result = await client.groups.getMembers(groupResourceName, {
778
+ pageSize: parseInt(opts.max),
779
+ pageToken: opts.pageToken,
780
+ });
781
+ print(result, getFormat(groupsCmd));
782
+ if (result.nextPageToken) {
783
+ info(`More results available. Use --page-token ${result.nextPageToken}`);
784
+ }
785
+ } catch (err) {
786
+ error(String(err));
787
+ process.exit(1);
788
+ }
789
+ });
790
+
791
+ groupsCmd
792
+ .command('add-member <groupResourceName> <contactResourceName>')
793
+ .description('Add a contact to a group')
794
+ .action(async (groupResourceName: string, contactResourceName: string) => {
795
+ try {
796
+ const client = await getClient();
797
+ await client.groups.addMember(groupResourceName, contactResourceName);
798
+ success(`Contact ${contactResourceName} added to group ${groupResourceName}`);
799
+ } catch (err) {
800
+ error(String(err));
801
+ process.exit(1);
802
+ }
803
+ });
804
+
805
+ groupsCmd
806
+ .command('remove-member <groupResourceName> <contactResourceName>')
807
+ .description('Remove a contact from a group')
808
+ .action(async (groupResourceName: string, contactResourceName: string) => {
809
+ try {
810
+ const client = await getClient();
811
+ await client.groups.removeMember(groupResourceName, contactResourceName);
812
+ success(`Contact ${contactResourceName} removed from group ${groupResourceName}`);
813
+ } catch (err) {
814
+ error(String(err));
815
+ process.exit(1);
816
+ }
817
+ });
818
+
819
+ // ============================================
820
+ // Bulk Commands
821
+ // ============================================
822
+ const bulkCmd = program
823
+ .command('bulk')
824
+ .description('Bulk operations on contacts');
825
+
826
+ function makeProgress(total: number) {
827
+ let last = 0;
828
+ return (current: number) => {
829
+ if (current !== last) {
830
+ process.stdout.write(`\r Progress: ${current}/${total}`);
831
+ last = current;
832
+ }
833
+ };
834
+ }
835
+
836
+ bulkCmd
837
+ .command('preview')
838
+ .description('Preview contacts matching a query')
839
+ .option('-q, --query <query>', 'Search query')
840
+ .option('-n, --max <number>', 'Maximum results', '50')
841
+ .action(async (opts) => {
842
+ try {
843
+ const client = await getClient();
844
+ const bulk = new BulkApi(client.getClient());
845
+ const result = await bulk.preview({ query: opts.query, maxResults: parseInt(opts.max) });
846
+ success(`Found ${result.total} contact(s):`);
847
+ print(result.contacts, getFormat(bulkCmd));
848
+ } catch (err) {
849
+ error(String(err));
850
+ process.exit(1);
851
+ }
852
+ });
853
+
854
+ bulkCmd
855
+ .command('delete')
856
+ .description('Bulk delete contacts matching a query')
857
+ .option('-q, --query <query>', 'Search query')
858
+ .option('-n, --max <number>', 'Maximum results', '100')
859
+ .option('--concurrency <number>', 'Max concurrent operations', '10')
860
+ .option('--dry-run', 'Preview without deleting')
861
+ .option('--confirm', 'Confirm deletion')
862
+ .action(async (opts) => {
863
+ if (!opts.confirm && !opts.dryRun) {
864
+ warn('This will delete contacts. Use --dry-run to preview or --confirm to proceed.');
865
+ process.exit(1);
866
+ }
867
+ try {
868
+ const client = await getClient();
869
+ const bulk = new BulkApi(client.getClient());
870
+ const progress = makeProgress(0);
871
+ const result = await bulk.delete({
872
+ query: opts.query,
873
+ maxResults: parseInt(opts.max),
874
+ concurrency: parseInt(opts.concurrency),
875
+ dryRun: opts.dryRun || false,
876
+ onProgress: (cur, total) => { progress(total); progress(cur); },
877
+ onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
878
+ });
879
+ process.stdout.write('\n');
880
+ if (opts.dryRun) {
881
+ info(`Dry run: would delete ${result.success} contact(s)`);
882
+ } else {
883
+ success(`Deleted ${result.success} contact(s), ${result.failed} failed`);
884
+ }
885
+ if (result.errors.length > 0) {
886
+ print(result.errors, 'pretty');
887
+ }
888
+ } catch (err) {
889
+ error(String(err));
890
+ process.exit(1);
891
+ }
892
+ });
893
+
894
+ bulkCmd
895
+ .command('update')
896
+ .description('Bulk update contacts matching a query')
897
+ .option('-q, --query <query>', 'Search query')
898
+ .option('-n, --max <number>', 'Maximum results', '100')
899
+ .option('--concurrency <number>', 'Max concurrent operations', '10')
900
+ .option('--dry-run', 'Preview without updating')
901
+ .option('--given-name <name>', 'New given name')
902
+ .option('--family-name <name>', 'New family name')
903
+ .option('--organization <org>', 'New organization')
904
+ .option('--title <title>', 'New job title')
905
+ .action(async (opts) => {
906
+ if (!opts.givenName && !opts.familyName && !opts.organization && !opts.title) {
907
+ error('At least one update field is required (--given-name, --family-name, --organization, --title)');
908
+ process.exit(1);
909
+ }
910
+ try {
911
+ const client = await getClient();
912
+ const bulk = new BulkApi(client.getClient());
913
+ const result = await bulk.update({
914
+ query: opts.query,
915
+ maxResults: parseInt(opts.max),
916
+ concurrency: parseInt(opts.concurrency),
917
+ dryRun: opts.dryRun || false,
918
+ updates: {
919
+ givenName: opts.givenName,
920
+ familyName: opts.familyName,
921
+ organization: opts.organization || opts.title ? {
922
+ name: opts.organization,
923
+ title: opts.title,
924
+ } : undefined,
925
+ },
926
+ onProgress: (cur, total) => { process.stdout.write(`\r Progress: ${cur}/${total}`); },
927
+ onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
928
+ });
929
+ process.stdout.write('\n');
930
+ if (opts.dryRun) {
931
+ info(`Dry run: would update ${result.success} contact(s)`);
932
+ } else {
933
+ success(`Updated ${result.success} contact(s), ${result.failed} failed`);
934
+ }
935
+ if (result.errors.length > 0) {
936
+ print(result.errors, 'pretty');
937
+ }
938
+ } catch (err) {
939
+ error(String(err));
940
+ process.exit(1);
941
+ }
942
+ });
943
+
944
+ bulkCmd
945
+ .command('add-to-group <groupResourceName>')
946
+ .description('Bulk add contacts to a group')
947
+ .option('-q, --query <query>', 'Search query')
948
+ .option('--contact-names <names>', 'Comma-separated contact resource names')
949
+ .option('-n, --max <number>', 'Maximum results', '100')
950
+ .option('--concurrency <number>', 'Max concurrent operations', '10')
951
+ .option('--dry-run', 'Preview without adding')
952
+ .action(async (groupResourceName: string, opts) => {
953
+ try {
954
+ const client = await getClient();
955
+ const bulk = new BulkApi(client.getClient());
956
+ const contactNames = opts.contactNames ? opts.contactNames.split(',') : undefined;
957
+ const result = await bulk.addToGroup(groupResourceName, {
958
+ contactNames,
959
+ query: opts.query,
960
+ maxResults: parseInt(opts.max),
961
+ concurrency: parseInt(opts.concurrency),
962
+ dryRun: opts.dryRun || false,
963
+ onProgress: (cur, total) => { process.stdout.write(`\r Progress: ${cur}/${total}`); },
964
+ onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
965
+ });
966
+ process.stdout.write('\n');
967
+ if (opts.dryRun) {
968
+ info(`Dry run: would add ${result.success} contact(s) to group`);
969
+ } else {
970
+ success(`Added ${result.success} contact(s) to group, ${result.failed} failed`);
971
+ }
972
+ if (result.errors.length > 0) {
973
+ print(result.errors, 'pretty');
974
+ }
975
+ } catch (err) {
976
+ error(String(err));
977
+ process.exit(1);
978
+ }
979
+ });
980
+
981
+ bulkCmd
982
+ .command('remove-from-group <groupResourceName>')
983
+ .description('Bulk remove contacts from a group')
984
+ .option('-q, --query <query>', 'Search query')
985
+ .option('--contact-names <names>', 'Comma-separated contact resource names')
986
+ .option('-n, --max <number>', 'Maximum results', '100')
987
+ .option('--concurrency <number>', 'Max concurrent operations', '10')
988
+ .option('--dry-run', 'Preview without removing')
989
+ .action(async (groupResourceName: string, opts) => {
990
+ try {
991
+ const client = await getClient();
992
+ const bulk = new BulkApi(client.getClient());
993
+ const contactNames = opts.contactNames ? opts.contactNames.split(',') : undefined;
994
+ const result = await bulk.removeFromGroup(groupResourceName, {
995
+ contactNames,
996
+ query: opts.query,
997
+ maxResults: parseInt(opts.max),
998
+ concurrency: parseInt(opts.concurrency),
999
+ dryRun: opts.dryRun || false,
1000
+ onProgress: (cur, total) => { process.stdout.write(`\r Progress: ${cur}/${total}`); },
1001
+ onError: (err, contact) => { warn(`Failed: ${contact.displayName} - ${err.message}`); },
1002
+ });
1003
+ process.stdout.write('\n');
1004
+ if (opts.dryRun) {
1005
+ info(`Dry run: would remove ${result.success} contact(s) from group`);
1006
+ } else {
1007
+ success(`Removed ${result.success} contact(s) from group, ${result.failed} failed`);
1008
+ }
1009
+ if (result.errors.length > 0) {
1010
+ print(result.errors, 'pretty');
1011
+ }
1012
+ } catch (err) {
1013
+ error(String(err));
1014
+ process.exit(1);
1015
+ }
1016
+ });
1017
+
678
1018
  // Parse and execute
679
1019
  program.parse();
@@ -0,0 +1,301 @@
1
+ import type { GoogleDocsClient } from './client';
2
+ import type { Document, Request, BatchUpdateResponse } from '../types';
3
+
4
+ // ============================================
5
+ // Bulk Operation Types
6
+ // ============================================
7
+
8
+ export interface DocumentSummary {
9
+ documentId: string;
10
+ title: string;
11
+ }
12
+
13
+ export interface BulkOperationOptions {
14
+ /** Maximum concurrent API calls (default: 10) */
15
+ concurrency?: number;
16
+ /** Dry run - don't actually modify */
17
+ dryRun?: boolean;
18
+ /** Progress callback */
19
+ onProgress?: (current: number, total: number, item: unknown) => void;
20
+ /** Error callback */
21
+ onError?: (error: Error, item: unknown) => void;
22
+ }
23
+
24
+ export interface BulkCreateDocumentsOptions extends BulkOperationOptions {
25
+ /** Document titles to create */
26
+ titles: string[];
27
+ }
28
+
29
+ export interface BulkCreateDocumentsResult {
30
+ total: number;
31
+ success: number;
32
+ failed: number;
33
+ errors: Array<{ title: string; error: string }>;
34
+ createdDocuments: DocumentSummary[];
35
+ }
36
+
37
+ export interface FindReplaceEntry {
38
+ documentId: string;
39
+ find: string;
40
+ replace: string;
41
+ matchCase?: boolean;
42
+ }
43
+
44
+ export interface BulkFindReplaceOptions extends BulkOperationOptions {
45
+ /** Find/replace entries per document */
46
+ entries: FindReplaceEntry[];
47
+ }
48
+
49
+ export interface FindReplaceResult {
50
+ documentId: string;
51
+ occurrencesChanged: number;
52
+ find: string;
53
+ replace: string;
54
+ }
55
+
56
+ export interface BulkFindReplaceResult {
57
+ total: number;
58
+ success: number;
59
+ failed: number;
60
+ errors: Array<{ documentId: string; find: string; error: string }>;
61
+ results: FindReplaceResult[];
62
+ }
63
+
64
+ export interface BulkOperationEntry {
65
+ documentId: string;
66
+ requests: Request[];
67
+ }
68
+
69
+ export interface BulkBatchUpdateOptions extends BulkOperationOptions {
70
+ /** Batch update entries */
71
+ entries: BulkOperationEntry[];
72
+ }
73
+
74
+ export interface BulkBatchUpdateResult {
75
+ total: number;
76
+ success: number;
77
+ failed: number;
78
+ errors: Array<{ documentId: string; error: string }>;
79
+ results: Array<{ documentId: string; response: BatchUpdateResponse }>;
80
+ }
81
+
82
+ // ============================================
83
+ // Bulk Operations API
84
+ // ============================================
85
+
86
+ export class BulkApi {
87
+ private readonly client: GoogleDocsClient;
88
+
89
+ constructor(client: GoogleDocsClient) {
90
+ this.client = client;
91
+ }
92
+
93
+ // ============================================
94
+ // Create Documents
95
+ // ============================================
96
+
97
+ /**
98
+ * Bulk create documents from a list of titles
99
+ */
100
+ async createDocuments(
101
+ options: BulkCreateDocumentsOptions
102
+ ): Promise<BulkCreateDocumentsResult> {
103
+ const { titles, concurrency = 10, dryRun = false, onProgress, onError } = options;
104
+
105
+ const result: BulkCreateDocumentsResult = {
106
+ total: titles.length,
107
+ success: 0,
108
+ failed: 0,
109
+ errors: [],
110
+ createdDocuments: [],
111
+ };
112
+
113
+ if (titles.length === 0) return result;
114
+
115
+ const chunks = this.chunkArray(titles, concurrency);
116
+
117
+ for (const chunk of chunks) {
118
+ await Promise.all(
119
+ chunk.map(async (title) => {
120
+ try {
121
+ if (dryRun) {
122
+ result.success++;
123
+ } else {
124
+ const doc = await this.client.post<Document>('/documents', { title });
125
+ result.success++;
126
+ result.createdDocuments.push({
127
+ documentId: doc.documentId,
128
+ title: doc.title,
129
+ });
130
+ }
131
+
132
+ if (onProgress) onProgress(result.success + result.failed, result.total, title);
133
+ } catch (err) {
134
+ result.failed++;
135
+ const errorMessage = err instanceof Error ? err.message : String(err);
136
+ result.errors.push({ title, error: errorMessage });
137
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), title);
138
+ }
139
+ })
140
+ );
141
+ }
142
+
143
+ return result;
144
+ }
145
+
146
+ // ============================================
147
+ // Find/Replace
148
+ // ============================================
149
+
150
+ /**
151
+ * Bulk find and replace across multiple documents
152
+ */
153
+ async findReplace(
154
+ options: BulkFindReplaceOptions
155
+ ): Promise<BulkFindReplaceResult> {
156
+ const { entries, concurrency = 10, dryRun = false, onProgress, onError } = options;
157
+
158
+ const result: BulkFindReplaceResult = {
159
+ total: entries.length,
160
+ success: 0,
161
+ failed: 0,
162
+ errors: [],
163
+ results: [],
164
+ };
165
+
166
+ if (entries.length === 0) return result;
167
+
168
+ const chunks = this.chunkArray(entries, concurrency);
169
+
170
+ for (const chunk of chunks) {
171
+ await Promise.all(
172
+ chunk.map(async (entry) => {
173
+ try {
174
+ if (dryRun) {
175
+ result.success++;
176
+ result.results.push({
177
+ documentId: entry.documentId,
178
+ occurrencesChanged: 0,
179
+ find: entry.find,
180
+ replace: entry.replace,
181
+ });
182
+ } else {
183
+ const requests: Request[] = [
184
+ {
185
+ replaceAllText: {
186
+ containsText: {
187
+ text: entry.find,
188
+ matchCase: entry.matchCase || false,
189
+ },
190
+ replaceText: entry.replace,
191
+ },
192
+ },
193
+ ];
194
+
195
+ const response = await this.client.post<BatchUpdateResponse>(
196
+ `/documents/${entry.documentId}:batchUpdate`,
197
+ { requests }
198
+ );
199
+
200
+ const occurrencesChanged =
201
+ response.replies?.find((r) => r.replaceAllText)?.replaceAllText?.occurrencesChanged || 0;
202
+
203
+ result.success++;
204
+ result.results.push({
205
+ documentId: entry.documentId,
206
+ occurrencesChanged,
207
+ find: entry.find,
208
+ replace: entry.replace,
209
+ });
210
+ }
211
+
212
+ if (onProgress) onProgress(result.success + result.failed, result.total, entry);
213
+ } catch (err) {
214
+ result.failed++;
215
+ const errorMessage = err instanceof Error ? err.message : String(err);
216
+ result.errors.push({
217
+ documentId: entry.documentId,
218
+ find: entry.find,
219
+ error: errorMessage,
220
+ });
221
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), entry);
222
+ }
223
+ })
224
+ );
225
+ }
226
+
227
+ return result;
228
+ }
229
+
230
+ // ============================================
231
+ // Batch Update (generic)
232
+ // ============================================
233
+
234
+ /**
235
+ * Bulk batch updates across multiple documents
236
+ * Each entry specifies a document ID and an array of requests
237
+ */
238
+ async batchUpdate(
239
+ options: BulkBatchUpdateOptions
240
+ ): Promise<BulkBatchUpdateResult> {
241
+ const { entries, concurrency = 10, dryRun = false, onProgress, onError } = options;
242
+
243
+ const result: BulkBatchUpdateResult = {
244
+ total: entries.length,
245
+ success: 0,
246
+ failed: 0,
247
+ errors: [],
248
+ results: [],
249
+ };
250
+
251
+ if (entries.length === 0) return result;
252
+
253
+ const chunks = this.chunkArray(entries, concurrency);
254
+
255
+ for (const chunk of chunks) {
256
+ await Promise.all(
257
+ chunk.map(async (entry) => {
258
+ try {
259
+ if (dryRun) {
260
+ result.success++;
261
+ } else {
262
+ const response = await this.client.post<BatchUpdateResponse>(
263
+ `/documents/${entry.documentId}:batchUpdate`,
264
+ { requests: entry.requests }
265
+ );
266
+ result.success++;
267
+ result.results.push({
268
+ documentId: entry.documentId,
269
+ response,
270
+ });
271
+ }
272
+
273
+ if (onProgress) onProgress(result.success + result.failed, result.total, entry);
274
+ } catch (err) {
275
+ result.failed++;
276
+ const errorMessage = err instanceof Error ? err.message : String(err);
277
+ result.errors.push({
278
+ documentId: entry.documentId,
279
+ error: errorMessage,
280
+ });
281
+ if (onError) onError(err instanceof Error ? err : new Error(errorMessage), entry);
282
+ }
283
+ })
284
+ );
285
+ }
286
+
287
+ return result;
288
+ }
289
+
290
+ // ============================================
291
+ // Helper Methods
292
+ // ============================================
293
+
294
+ private chunkArray<T>(array: T[], size: number): T[][] {
295
+ const chunks: T[][] = [];
296
+ for (let i = 0; i < array.length; i += size) {
297
+ chunks.push(array.slice(i, i + size));
298
+ }
299
+ return chunks;
300
+ }
301
+ }
@@ -3,6 +3,7 @@ import { GoogleDocsClient } from './client';
3
3
  import { DocumentsApi } from './documents';
4
4
  import { ContentApi } from './content';
5
5
  import { StylesApi } from './styles';
6
+ import { BulkApi } from './bulk';
6
7
 
7
8
  /**
8
9
  * Google Docs API client
@@ -15,12 +16,14 @@ export class GoogleDocs {
15
16
  public readonly documents: DocumentsApi;
16
17
  public readonly content: ContentApi;
17
18
  public readonly styles: StylesApi;
19
+ public readonly bulk: BulkApi;
18
20
 
19
21
  constructor(config: GoogleDocsConfig) {
20
22
  this.client = new GoogleDocsClient(config);
21
23
  this.documents = new DocumentsApi(this.client);
22
24
  this.content = new ContentApi(this.client);
23
25
  this.styles = new StylesApi(this.client);
26
+ this.bulk = new BulkApi(this.client);
24
27
  }
25
28
 
26
29
  /**
@@ -63,3 +66,4 @@ export { GoogleDocsClient } from './client';
63
66
  export { DocumentsApi } from './documents';
64
67
  export { ContentApi } from './content';
65
68
  export { StylesApi } from './styles';
69
+ export { BulkApi } from './bulk';