@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
@@ -816,5 +816,527 @@ eventCmd
816
816
  }
817
817
  });
818
818
 
819
+ // ============================================
820
+ // Bulk Commands
821
+ // ============================================
822
+ const bulkCmd = program
823
+ .command('bulk')
824
+ .description('Bulk operations on events');
825
+
826
+ bulkCmd
827
+ .command('preview')
828
+ .description('Preview events matching a query')
829
+ .option('-c, --calendar <id>', 'Calendar ID', 'primary')
830
+ .option('-q, --query <text>', 'Search query')
831
+ .option('--time-min <date>', 'Start date (RFC3339)')
832
+ .option('--time-max <date>', 'End date (RFC3339)')
833
+ .option('-n, --max <number>', 'Maximum events to preview', '20')
834
+ .action(async (opts) => {
835
+ try {
836
+ const client = await getClient();
837
+ const label = opts.query ? `matching: ${opts.query}` : `on calendar: ${opts.calendar}`;
838
+ info(`Previewing events ${label}`);
839
+
840
+ const result = await client.bulk.preview(opts.calendar, {
841
+ query: opts.query,
842
+ timeMin: opts.timeMin,
843
+ timeMax: opts.timeMax,
844
+ maxResults: parseInt(opts.max),
845
+ });
846
+
847
+ if (result.events.length === 0) {
848
+ info('No events found');
849
+ return;
850
+ }
851
+
852
+ success(`Found ${result.total} event(s):`);
853
+ const output = result.events.map(e => ({
854
+ id: e.id,
855
+ summary: e.summary,
856
+ start: e.start,
857
+ end: e.end,
858
+ status: e.status,
859
+ location: e.location || '-',
860
+ }));
861
+ print(output, getFormat(bulkCmd));
862
+ } catch (err) {
863
+ error(String(err));
864
+ process.exit(1);
865
+ }
866
+ });
867
+
868
+ bulkCmd
869
+ .command('delete')
870
+ .description('Delete events matching a query (DANGER!)')
871
+ .option('-c, --calendar <id>', 'Calendar ID', 'primary')
872
+ .option('-q, --query <text>', 'Search query')
873
+ .option('--time-min <date>', 'Start date (RFC3339)')
874
+ .option('--time-max <date>', 'End date (RFC3339)')
875
+ .option('-n, --max <number>', 'Maximum events to process', '100')
876
+ .option('-k, --concurrency <number>', 'Maximum concurrent API calls', '10')
877
+ .option('--dry-run', 'Preview changes without applying them')
878
+ .option('--confirm', 'Confirm permanent deletion')
879
+ .action(async (opts) => {
880
+ try {
881
+ if (!opts.dryRun && !opts.confirm) {
882
+ error('Permanent deletion requires --confirm flag');
883
+ info('Use --dry-run to preview what would be deleted');
884
+ process.exit(1);
885
+ }
886
+
887
+ const client = await getClient();
888
+ warn(`${opts.dryRun ? '[DRY RUN] ' : ''}PERMANENTLY DELETING events on ${opts.calendar}`);
889
+ if (opts.query) info(` Query: ${opts.query}`);
890
+ if (opts.timeMin) info(` From: ${opts.timeMin}`);
891
+ if (opts.timeMax) info(` To: ${opts.timeMax}`);
892
+
893
+ const result = await client.bulk.delete({
894
+ calendarId: opts.calendar,
895
+ query: opts.query,
896
+ timeMin: opts.timeMin,
897
+ timeMax: opts.timeMax,
898
+ maxResults: parseInt(opts.max),
899
+ concurrency: parseInt(opts.concurrency),
900
+ dryRun: opts.dryRun,
901
+ onProgress: (current, total) => {
902
+ process.stdout.write(`\r Progress: ${current}/${total}`);
903
+ },
904
+ });
905
+ console.log();
906
+
907
+ success(`${opts.dryRun ? '[DRY RUN] ' : ''}Bulk delete complete:`);
908
+ info(` Total: ${result.total}`);
909
+ info(` Success: ${result.success}`);
910
+ if (result.failed > 0) warn(` Failed: ${result.failed}`);
911
+ } catch (err) {
912
+ error(String(err));
913
+ process.exit(1);
914
+ }
915
+ });
916
+
917
+ bulkCmd
918
+ .command('create')
919
+ .description('Create multiple events from a JSON file')
920
+ .option('-c, --calendar <id>', 'Calendar ID', 'primary')
921
+ .option('-f, --file <path>', 'Path to JSON file with events array')
922
+ .option('-k, --concurrency <number>', 'Maximum concurrent API calls', '10')
923
+ .option('--dry-run', 'Preview without creating')
924
+ .option('--notify', 'Send notifications to attendees')
925
+ .action(async (opts) => {
926
+ try {
927
+ const { readFileSync } = await import('fs');
928
+
929
+ if (!opts.file) {
930
+ error('--file is required');
931
+ process.exit(1);
932
+ }
933
+
934
+ const events = JSON.parse(readFileSync(opts.file, 'utf-8'));
935
+ if (!Array.isArray(events)) {
936
+ error('JSON file must contain an array of events');
937
+ process.exit(1);
938
+ }
939
+
940
+ const client = await getClient();
941
+ info(`${opts.dryRun ? '[DRY RUN] ' : ''}Creating ${events.length} event(s) on ${opts.calendar}`);
942
+
943
+ const result = await client.bulk.create({
944
+ calendarId: opts.calendar,
945
+ events,
946
+ concurrency: parseInt(opts.concurrency),
947
+ dryRun: opts.dryRun,
948
+ sendNotifications: opts.notify === true,
949
+ onProgress: (current, total) => {
950
+ process.stdout.write(`\r Progress: ${current}/${total}`);
951
+ },
952
+ });
953
+ console.log();
954
+
955
+ success(`${opts.dryRun ? '[DRY RUN] ' : ''}Bulk create complete:`);
956
+ info(` Total: ${result.total}`);
957
+ info(` Success: ${result.success}`);
958
+ if (result.failed > 0) warn(` Failed: ${result.failed}`);
959
+ } catch (err) {
960
+ error(String(err));
961
+ process.exit(1);
962
+ }
963
+ });
964
+
965
+ bulkCmd
966
+ .command('update')
967
+ .description('Update events matching a query with partial data')
968
+ .option('-c, --calendar <id>', 'Calendar ID', 'primary')
969
+ .option('-q, --query <text>', 'Search query')
970
+ .option('--time-min <date>', 'Start date (RFC3339)')
971
+ .option('--time-max <date>', 'End date (RFC3339)')
972
+ .option('-n, --max <number>', 'Maximum events to process', '100')
973
+ .option('-k, --concurrency <number>', 'Maximum concurrent API calls', '10')
974
+ .option('--dry-run', 'Preview changes without applying them')
975
+ .option('--updates <json>', 'JSON object with fields to update (e.g. \'{"location":"Remote"}\')')
976
+ .action(async (opts) => {
977
+ try {
978
+ if (!opts.updates) {
979
+ error('--updates is required (JSON object)');
980
+ process.exit(1);
981
+ }
982
+
983
+ const updates = JSON.parse(opts.updates);
984
+ const client = await getClient();
985
+ info(`${opts.dryRun ? '[DRY RUN] ' : ''}Updating events on ${opts.calendar}`);
986
+ if (opts.query) info(` Query: ${opts.query}`);
987
+ info(` Updates: ${JSON.stringify(updates)}`);
988
+
989
+ const result = await client.bulk.update({
990
+ calendarId: opts.calendar,
991
+ query: opts.query,
992
+ timeMin: opts.timeMin,
993
+ timeMax: opts.timeMax,
994
+ maxResults: parseInt(opts.max),
995
+ concurrency: parseInt(opts.concurrency),
996
+ updates,
997
+ dryRun: opts.dryRun,
998
+ onProgress: (current, total) => {
999
+ process.stdout.write(`\r Progress: ${current}/${total}`);
1000
+ },
1001
+ });
1002
+ console.log();
1003
+
1004
+ success(`${opts.dryRun ? '[DRY RUN] ' : ''}Bulk update complete:`);
1005
+ info(` Total: ${result.total}`);
1006
+ info(` Success: ${result.success}`);
1007
+ if (result.failed > 0) warn(` Failed: ${result.failed}`);
1008
+ } catch (err) {
1009
+ error(String(err));
1010
+ process.exit(1);
1011
+ }
1012
+ });
1013
+
1014
+ bulkCmd
1015
+ .command('accept')
1016
+ .description('Accept event invitations matching a query')
1017
+ .option('-c, --calendar <id>', 'Calendar ID', 'primary')
1018
+ .option('-q, --query <text>', 'Search query')
1019
+ .option('--time-min <date>', 'Start date (RFC3339)')
1020
+ .option('--time-max <date>', 'End date (RFC3339)')
1021
+ .option('-n, --max <number>', 'Maximum events to process', '100')
1022
+ .option('-k, --concurrency <number>', 'Maximum concurrent API calls', '10')
1023
+ .option('--dry-run', 'Preview changes without applying them')
1024
+ .option('--comment <text>', 'Optional comment')
1025
+ .action(async (opts) => {
1026
+ try {
1027
+ const client = await getClient();
1028
+ info(`${opts.dryRun ? '[DRY RUN] ' : ''}Accepting events on ${opts.calendar}`);
1029
+ if (opts.query) info(` Query: ${opts.query}`);
1030
+
1031
+ const result = await client.bulk.accept({
1032
+ calendarId: opts.calendar,
1033
+ query: opts.query,
1034
+ timeMin: opts.timeMin,
1035
+ timeMax: opts.timeMax,
1036
+ maxResults: parseInt(opts.max),
1037
+ concurrency: parseInt(opts.concurrency),
1038
+ dryRun: opts.dryRun,
1039
+ comment: opts.comment,
1040
+ onProgress: (current, total) => {
1041
+ process.stdout.write(`\r Progress: ${current}/${total}`);
1042
+ },
1043
+ });
1044
+ console.log();
1045
+
1046
+ success(`${opts.dryRun ? '[DRY RUN] ' : ''}Bulk accept complete:`);
1047
+ info(` Total: ${result.total}`);
1048
+ info(` Success: ${result.success}`);
1049
+ if (result.failed > 0) warn(` Failed: ${result.failed}`);
1050
+ } catch (err) {
1051
+ error(String(err));
1052
+ process.exit(1);
1053
+ }
1054
+ });
1055
+
1056
+ bulkCmd
1057
+ .command('decline')
1058
+ .description('Decline event invitations matching a query')
1059
+ .option('-c, --calendar <id>', 'Calendar ID', 'primary')
1060
+ .option('-q, --query <text>', 'Search query')
1061
+ .option('--time-min <date>', 'Start date (RFC3339)')
1062
+ .option('--time-max <date>', 'End date (RFC3339)')
1063
+ .option('-n, --max <number>', 'Maximum events to process', '100')
1064
+ .option('-k, --concurrency <number>', 'Maximum concurrent API calls', '10')
1065
+ .option('--dry-run', 'Preview changes without applying them')
1066
+ .option('--comment <text>', 'Optional comment')
1067
+ .action(async (opts) => {
1068
+ try {
1069
+ const client = await getClient();
1070
+ info(`${opts.dryRun ? '[DRY RUN] ' : ''}Declining events on ${opts.calendar}`);
1071
+ if (opts.query) info(` Query: ${opts.query}`);
1072
+
1073
+ const result = await client.bulk.decline({
1074
+ calendarId: opts.calendar,
1075
+ query: opts.query,
1076
+ timeMin: opts.timeMin,
1077
+ timeMax: opts.timeMax,
1078
+ maxResults: parseInt(opts.max),
1079
+ concurrency: parseInt(opts.concurrency),
1080
+ dryRun: opts.dryRun,
1081
+ comment: opts.comment,
1082
+ onProgress: (current, total) => {
1083
+ process.stdout.write(`\r Progress: ${current}/${total}`);
1084
+ },
1085
+ });
1086
+ console.log();
1087
+
1088
+ success(`${opts.dryRun ? '[DRY RUN] ' : ''}Bulk decline complete:`);
1089
+ info(` Total: ${result.total}`);
1090
+ info(` Success: ${result.success}`);
1091
+ if (result.failed > 0) warn(` Failed: ${result.failed}`);
1092
+ } catch (err) {
1093
+ error(String(err));
1094
+ process.exit(1);
1095
+ }
1096
+ });
1097
+
1098
+ bulkCmd
1099
+ .command('tentative')
1100
+ .description('Mark events as tentative matching a query')
1101
+ .option('-c, --calendar <id>', 'Calendar ID', 'primary')
1102
+ .option('-q, --query <text>', 'Search query')
1103
+ .option('--time-min <date>', 'Start date (RFC3339)')
1104
+ .option('--time-max <date>', 'End date (RFC3339)')
1105
+ .option('-n, --max <number>', 'Maximum events to process', '100')
1106
+ .option('-k, --concurrency <number>', 'Maximum concurrent API calls', '10')
1107
+ .option('--dry-run', 'Preview changes without applying them')
1108
+ .option('--comment <text>', 'Optional comment')
1109
+ .action(async (opts) => {
1110
+ try {
1111
+ const client = await getClient();
1112
+ info(`${opts.dryRun ? '[DRY RUN] ' : ''}Marking events as tentative on ${opts.calendar}`);
1113
+ if (opts.query) info(` Query: ${opts.query}`);
1114
+
1115
+ const result = await client.bulk.tentative({
1116
+ calendarId: opts.calendar,
1117
+ query: opts.query,
1118
+ timeMin: opts.timeMin,
1119
+ timeMax: opts.timeMax,
1120
+ maxResults: parseInt(opts.max),
1121
+ concurrency: parseInt(opts.concurrency),
1122
+ dryRun: opts.dryRun,
1123
+ comment: opts.comment,
1124
+ onProgress: (current, total) => {
1125
+ process.stdout.write(`\r Progress: ${current}/${total}`);
1126
+ },
1127
+ });
1128
+ console.log();
1129
+
1130
+ success(`${opts.dryRun ? '[DRY RUN] ' : ''}Bulk tentative complete:`);
1131
+ info(` Total: ${result.total}`);
1132
+ info(` Success: ${result.success}`);
1133
+ if (result.failed > 0) warn(` Failed: ${result.failed}`);
1134
+ } catch (err) {
1135
+ error(String(err));
1136
+ process.exit(1);
1137
+ }
1138
+ });
1139
+
1140
+ // ============================================
1141
+ // Free/Busy Commands
1142
+ // ============================================
1143
+ const freebusyCmd = program
1144
+ .command('freebusy')
1145
+ .description('Query free/busy information');
1146
+
1147
+ freebusyCmd
1148
+ .command('query')
1149
+ .description('Query free/busy for one or more calendars')
1150
+ .requiredOption('-c, --calendar <ids>', 'Comma-separated calendar IDs')
1151
+ .requiredOption('--time-min <date>', 'Start time (RFC3339)')
1152
+ .requiredOption('--time-max <date>', 'End time (RFC3339)')
1153
+ .option('--timezone <tz>', 'Timezone')
1154
+ .action(async (opts) => {
1155
+ try {
1156
+ const client = await getClient();
1157
+ const calendarIds = opts.calendar.split(',').map((s: string) => s.trim());
1158
+
1159
+ const result = await client.freebusy.query({
1160
+ timeMin: opts.timeMin,
1161
+ timeMax: opts.timeMax,
1162
+ items: calendarIds.map((id: string) => ({ id })),
1163
+ timeZone: opts.timezone,
1164
+ });
1165
+
1166
+ const format = getFormat(freebusyCmd);
1167
+ if (format === 'json') {
1168
+ print(result, format);
1169
+ } else {
1170
+ for (const [calId, calInfo] of Object.entries(result.calendars)) {
1171
+ console.log(chalk.bold(` ${calId}`));
1172
+ if (calInfo.busy.length === 0) {
1173
+ info(' Free');
1174
+ } else {
1175
+ for (const slot of calInfo.busy) {
1176
+ const start = new Date(slot.start).toLocaleString();
1177
+ const end = new Date(slot.end).toLocaleString();
1178
+ console.log(` ${chalk.yellow('Busy:')} ${start} → ${end}`);
1179
+ }
1180
+ }
1181
+ if (calInfo.errors && calInfo.errors.length > 0) {
1182
+ for (const e of calInfo.errors) {
1183
+ warn(` Error: ${e.message}`);
1184
+ }
1185
+ }
1186
+ }
1187
+ }
1188
+ } catch (err) {
1189
+ error(String(err));
1190
+ process.exit(1);
1191
+ }
1192
+ });
1193
+
1194
+ freebusyCmd
1195
+ .command('check')
1196
+ .description('Quick check if a calendar is free during a time range')
1197
+ .requiredOption('-c, --calendar <id>', 'Calendar ID')
1198
+ .requiredOption('--time-min <date>', 'Start time (RFC3339)')
1199
+ .requiredOption('--time-max <date>', 'End time (RFC3339)')
1200
+ .action(async (opts) => {
1201
+ try {
1202
+ const client = await getClient();
1203
+ const free = await client.freebusy.isFree(opts.calendar, opts.timeMin, opts.timeMax);
1204
+
1205
+ const format = getFormat(freebusyCmd);
1206
+ if (format === 'json') {
1207
+ print({ calendar: opts.calendar, free, timeMin: opts.timeMin, timeMax: opts.timeMax }, format);
1208
+ } else {
1209
+ if (free) {
1210
+ success(`${opts.calendar} is free from ${new Date(opts.timeMin).toLocaleString()} to ${new Date(opts.timeMax).toLocaleString()}`);
1211
+ } else {
1212
+ warn(`${opts.calendar} is busy during ${new Date(opts.timeMin).toLocaleString()} to ${new Date(opts.timeMax).toLocaleString()}`);
1213
+ }
1214
+ }
1215
+ } catch (err) {
1216
+ error(String(err));
1217
+ process.exit(1);
1218
+ }
1219
+ });
1220
+
1221
+ // ============================================
1222
+ // ACL Commands
1223
+ // ============================================
1224
+ const aclCmd = program
1225
+ .command('acl')
1226
+ .description('Manage calendar access control lists');
1227
+
1228
+ aclCmd
1229
+ .command('list')
1230
+ .description('List ACL rules for a calendar')
1231
+ .requiredOption('-c, --calendar <id>', 'Calendar ID')
1232
+ .action(async (opts) => {
1233
+ try {
1234
+ const client = await getClient();
1235
+ const result = await client.acl.list(opts.calendar);
1236
+
1237
+ const format = getFormat(aclCmd);
1238
+ if (format === 'json') {
1239
+ print(result, format);
1240
+ } else {
1241
+ if (result.items.length === 0) {
1242
+ info(`No ACL rules found for ${opts.calendar}`);
1243
+ return;
1244
+ }
1245
+ success(`ACL rules for ${opts.calendar}:`);
1246
+ for (const rule of result.items) {
1247
+ const scopeStr = rule.scope.type === 'default'
1248
+ ? 'default'
1249
+ : `${rule.scope.type}:${rule.scope.value}`;
1250
+ console.log(` ${rule.role.padEnd(18)} ${scopeStr}`);
1251
+ }
1252
+ }
1253
+ } catch (err) {
1254
+ error(String(err));
1255
+ process.exit(1);
1256
+ }
1257
+ });
1258
+
1259
+ aclCmd
1260
+ .command('add')
1261
+ .description('Add an ACL rule to a calendar')
1262
+ .requiredOption('-c, --calendar <id>', 'Calendar ID')
1263
+ .requiredOption('-r, --role <role>', 'Role: freeBusyReader, reader, writer, owner')
1264
+ .requiredOption('--scope-type <type>', 'Scope type: user, group, domain, default')
1265
+ .option('--scope-value <value>', 'Scope value (email for user/group, domain for domain)')
1266
+ .action(async (opts) => {
1267
+ try {
1268
+ const role = opts.role as 'freeBusyReader' | 'reader' | 'writer' | 'owner';
1269
+ const validRoles = ['freeBusyReader', 'reader', 'writer', 'owner'];
1270
+ if (!validRoles.includes(role)) {
1271
+ error(`Invalid role: ${role}. Must be one of: ${validRoles.join(', ')}`);
1272
+ process.exit(1);
1273
+ }
1274
+
1275
+ const scopeType = opts.scopeType as 'user' | 'group' | 'domain' | 'default';
1276
+ const validTypes = ['user', 'group', 'domain', 'default'];
1277
+ if (!validTypes.includes(scopeType)) {
1278
+ error(`Invalid scope type: ${scopeType}. Must be one of: ${validTypes.join(', ')}`);
1279
+ process.exit(1);
1280
+ }
1281
+
1282
+ if (scopeType !== 'default' && !opts.scopeValue) {
1283
+ error('--scope-value is required for user/group/domain scope types');
1284
+ process.exit(1);
1285
+ }
1286
+
1287
+ const client = await getClient();
1288
+ const rule = await client.acl.insert(opts.calendar, {
1289
+ role,
1290
+ scope: {
1291
+ type: scopeType,
1292
+ value: opts.scopeValue,
1293
+ },
1294
+ });
1295
+
1296
+ success(`ACL rule added:`);
1297
+ info(` Role: ${rule.role}`);
1298
+ info(` Scope: ${rule.scope.type}${rule.scope.value ? `:${rule.scope.value}` : ''}`);
1299
+ } catch (err) {
1300
+ error(String(err));
1301
+ process.exit(1);
1302
+ }
1303
+ });
1304
+
1305
+ aclCmd
1306
+ .command('remove')
1307
+ .description('Remove an ACL rule from a calendar')
1308
+ .requiredOption('-c, --calendar <id>', 'Calendar ID')
1309
+ .requiredOption('-i, --rule-id <id>', 'ACL rule ID to remove')
1310
+ .action(async (opts) => {
1311
+ try {
1312
+ const client = await getClient();
1313
+ await client.acl.delete(opts.calendar, opts.ruleId);
1314
+ success(`ACL rule "${opts.ruleId}" removed from ${opts.calendar}`);
1315
+ } catch (err) {
1316
+ error(String(err));
1317
+ process.exit(1);
1318
+ }
1319
+ });
1320
+
1321
+ aclCmd
1322
+ .command('share')
1323
+ .description('Share a calendar with a user')
1324
+ .requiredOption('-c, --calendar <id>', 'Calendar ID')
1325
+ .requiredOption('--with <email>', 'Email address to share with')
1326
+ .option('-r, --role <role>', 'Access level: reader (default), writer, freeBusyReader', 'reader')
1327
+ .action(async (opts) => {
1328
+ try {
1329
+ const client = await getClient();
1330
+ const rule = await client.acl.insert(opts.calendar, {
1331
+ role: opts.role,
1332
+ scope: { type: 'user', value: opts.with },
1333
+ });
1334
+ success(`Shared ${opts.calendar} with ${opts.with} as ${rule.role}`);
1335
+ } catch (err) {
1336
+ error(String(err));
1337
+ process.exit(1);
1338
+ }
1339
+ });
1340
+
819
1341
  // Parse and execute
820
1342
  program.parse();
@@ -345,3 +345,93 @@ export class GoogleCalendarApiError extends Error {
345
345
  this.errors = errors;
346
346
  }
347
347
  }
348
+
349
+ // ============================================
350
+ // Free/Busy Types
351
+ // ============================================
352
+
353
+ export interface FreeBusyTimeRange {
354
+ start: string; // RFC3339
355
+ end: string; // RFC3339
356
+ }
357
+
358
+ export interface FreeBusyGroup {
359
+ id: string;
360
+ members?: { id: string }[];
361
+ }
362
+
363
+ export interface FreeBusyCalendarRequest {
364
+ id: string;
365
+ }
366
+
367
+ export interface FreeBusyGroupRequest {
368
+ id: string;
369
+ }
370
+
371
+ export interface FreeBusyRequest {
372
+ timeMin?: string; // RFC3339
373
+ timeMax?: string; // RFC3339
374
+ items: (FreeBusyCalendarRequest | FreeBusyGroupRequest)[];
375
+ groupExpansionMax?: number;
376
+ timeZone?: string;
377
+ }
378
+
379
+ export interface FreeBusyBusyPeriod {
380
+ start: string;
381
+ end: string;
382
+ }
383
+
384
+ export interface FreeBusyCalendar {
385
+ busy: FreeBusyBusyPeriod[];
386
+ errors?: ApiErrorDetail[];
387
+ }
388
+
389
+ export interface FreeBusyCalendarResponse {
390
+ calendars: { [calendarId: string]: FreeBusyCalendar };
391
+ groups?: { [groupId: string]: FreeBusyGroup };
392
+ kind: 'calendar#freeBusy';
393
+ timeMax: string;
394
+ timeMin: string;
395
+ }
396
+
397
+ // ============================================
398
+ // ACL Types
399
+ // ============================================
400
+
401
+ export type CalendarAclRole =
402
+ | 'none'
403
+ | 'freeBusyReader'
404
+ | 'reader'
405
+ | 'writer'
406
+ | 'owner';
407
+
408
+ export type CalendarAclScopeType =
409
+ | 'default'
410
+ | 'user'
411
+ | 'group'
412
+ | 'domain';
413
+
414
+ export interface CalendarAclScope {
415
+ type: CalendarAclScopeType;
416
+ value?: string; // email for user/group, domain for domain
417
+ }
418
+
419
+ export interface CalendarAcl {
420
+ kind: 'calendar#aclRule';
421
+ etag: string;
422
+ id: string;
423
+ role: CalendarAclRole;
424
+ scope: CalendarAclScope;
425
+ }
426
+
427
+ export interface CalendarAclListResponse {
428
+ kind: 'calendar#acl';
429
+ etag: string;
430
+ nextPageToken?: string;
431
+ items: CalendarAcl[];
432
+ }
433
+
434
+ export interface CalendarAclInsert {
435
+ role: CalendarAclRole;
436
+ scope: CalendarAclScope;
437
+ }