@bobfrankston/gcal 0.1.62 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gcal.ts CHANGED
@@ -12,7 +12,7 @@ import fs from 'fs';
12
12
  import path from 'path';
13
13
  import { execSync } from 'child_process';
14
14
  import { createInterface } from 'readline/promises';
15
- import type { GoogleEvent, EventsListResponse, CalendarListEntry, CalendarListResponse } from './glib/types.ts';
15
+ import type { GoogleEvent, EventDateTime, EventsListResponse, CalendarListEntry, CalendarListResponse } from './glib/types.ts';
16
16
  import {
17
17
  loadConfig, saveConfig,
18
18
  formatDateTime, formatDuration, parseDuration, parseDateTime,
@@ -290,6 +290,7 @@ Commands:
290
290
  show Show full details for an event (-json for JSON)
291
291
  open Open event in browser
292
292
  add Add event (explicit, AI, or interactive)
293
+ update | edit | set Change an event's time/title/location/busy/...
293
294
  del | delete Delete event(s) by ID
294
295
  remind Add reminder(s) to existing event
295
296
  resched Reschedule event
@@ -338,20 +339,55 @@ const USAGE: Record<string, string> = {
338
339
  Examples:
339
340
  gcal open abc12345
340
341
  `,
341
- add: `gcal add <title> <when> [duration] Explicit
342
+ add: `gcal add <title> <when> [duration] Explicit (timed)
343
+ gcal add <title> <date> [days] -allday Explicit (all-day)
342
344
  gcal add "<free text>" AI-parsed single arg
343
345
  gcal add -clip AI-parsed from clipboard
344
346
  gcal add Interactive (type description)
345
347
  Add a calendar event. Default duration 1h. Use -r <dur> to add reminder(s).
346
348
 
349
+ -allday Create an all-day event. The third arg is a day count
350
+ (default 1); the event spans that many days.
351
+ -free Mark the event as Free (does not block time / not busy).
352
+ -busy Mark the event as Busy (the default).
353
+ -open Open the event in the browser after creating it.
354
+
347
355
  Examples:
348
356
  gcal add "Dentist" "Friday 3pm" "1h"
349
357
  gcal add "Lunch" "1/14/2026 12:00" "1h"
350
358
  gcal add "Meeting" "tomorrow 10:00"
351
359
  gcal add "Appointment" "jan 15 2pm"
360
+ gcal add "Vacation" "jul 1" -allday (1 day, all-day)
361
+ gcal add "Conference" "jul 1" 3 -allday (3-day all-day event)
362
+ gcal add "Out of office" "jul 1" -allday -free (all-day, not busy)
352
363
  gcal add "Dentist appointment Friday 3pm for 1 hour"
353
364
  gcal add -clip
354
365
  gcal add "Dentist" "Friday 3pm" -r 30m
366
+ `,
367
+ update: `gcal update <id> [options]
368
+ gcal edit <id> ... (aliases: edit, set)
369
+ Change settings on an existing event. Only the options you give are
370
+ changed. Searches up to 30 days back; widen with -since.
371
+
372
+ -when <when> New start time/date (accepts +Nd / +Nh advances too).
373
+ -dur <dur> New duration ("1h30m"); for all-day events, a day count.
374
+ -title <text> New event title.
375
+ -loc <text> New location ("" to clear).
376
+ -note <text> New description ("" to clear).
377
+ -free Mark as Free (not busy).
378
+ -busy Mark as Busy.
379
+ -r <dur> Add a popup reminder (repeatable).
380
+ -rx <dur> Remove a reminder matching that duration (repeatable).
381
+ -nr Remove all reminders.
382
+ -open Open the event in the browser afterward.
383
+
384
+ Examples:
385
+ gcal update abc12345 -free
386
+ gcal update abc12345 -busy -title "Team sync"
387
+ gcal update abc12345 -when "wed 2pm" -dur 90m
388
+ gcal update abc12345 -r 30m -r 1h (add two reminders)
389
+ gcal update abc12345 -rx 30m (remove the 30m reminder)
390
+ gcal update abc12345 -nr (clear all reminders)
355
391
  `,
356
392
  del: `gcal del <id> [id2...] [-all] [-b]
357
393
  gcal delete <id> [id2...]
@@ -411,7 +447,9 @@ const USAGE: Record<string, string> = {
411
447
  const HELP_ALIASES: Record<string, string> = {
412
448
  'listc': 'calendars',
413
449
  'list-calendars': 'calendars',
414
- 'list-recurring': 'listr'
450
+ 'list-recurring': 'listr',
451
+ 'set': 'update',
452
+ 'edit': 'update'
415
453
  };
416
454
 
417
455
  function showUsage(cmd?: string): void {
@@ -439,6 +477,16 @@ interface ParsedArgs {
439
477
  clip: boolean;
440
478
  all: boolean;
441
479
  json: boolean;
480
+ allDay: boolean;
481
+ transparency: string; /** 'opaque' (busy) | 'transparent' (free); '' = unset */
482
+ setTitle?: string; /** update: new summary (undefined = leave alone) */
483
+ setLoc?: string; /** update: new location */
484
+ setNote?: string; /** update: new description */
485
+ setWhen?: string; /** update: new start (date/time or +Nd advance) */
486
+ setDur?: string; /** update: new duration (timed) or day count (all-day) */
487
+ removeReminders: number[]; /** update: reminder minutes to drop */
488
+ clearReminders: boolean; /** update: drop all reminders */
489
+ open: boolean; /** open the event in the browser after add/update */
442
490
  reminders: number[];
443
491
  rrule: string; /** RRULE body, e.g. "FREQ=DAILY". RRULE: prefix added automatically. */
444
492
  since?: Date;
@@ -460,6 +508,11 @@ function parseArgs(argv: string[]): ParsedArgs {
460
508
  clip: false,
461
509
  all: false,
462
510
  json: false,
511
+ allDay: false,
512
+ transparency: '',
513
+ removeReminders: [],
514
+ clearReminders: false,
515
+ open: false,
463
516
  reminders: [],
464
517
  rrule: '',
465
518
  helpCmd: ''
@@ -506,6 +559,57 @@ function parseArgs(argv: string[]): ParsedArgs {
506
559
  case '--json':
507
560
  result.json = true;
508
561
  break;
562
+ case '-allday':
563
+ case '-all-day':
564
+ case '--allday':
565
+ result.allDay = true;
566
+ break;
567
+ case '-free':
568
+ case '--free':
569
+ result.transparency = 'transparent';
570
+ break;
571
+ case '-busy':
572
+ case '--busy':
573
+ result.transparency = 'opaque';
574
+ break;
575
+ case '-title':
576
+ case '--title':
577
+ result.setTitle = argv[++i] || '';
578
+ break;
579
+ case '-loc':
580
+ case '-location':
581
+ case '--location':
582
+ result.setLoc = argv[++i] || '';
583
+ break;
584
+ case '-note':
585
+ case '-desc':
586
+ case '-description':
587
+ case '--note':
588
+ result.setNote = argv[++i] || '';
589
+ break;
590
+ case '-when':
591
+ case '--when':
592
+ result.setWhen = argv[++i] || '';
593
+ break;
594
+ case '-dur':
595
+ case '-duration':
596
+ case '--duration':
597
+ result.setDur = argv[++i] || '';
598
+ break;
599
+ case '-rx':
600
+ case '-rmr':
601
+ case '--remove-reminder':
602
+ result.removeReminders.push(parseDuration(argv[++i] || ''));
603
+ break;
604
+ case '-nr':
605
+ case '-noreminders':
606
+ case '--no-reminders':
607
+ result.clearReminders = true;
608
+ break;
609
+ case '-open':
610
+ case '--open':
611
+ result.open = true;
612
+ break;
509
613
  case '-r':
510
614
  case '-reminder':
511
615
  case '--reminder': {
@@ -592,6 +696,102 @@ function buildReminders(minutes: number[]): GoogleEvent['reminders'] | undefined
592
696
  };
593
697
  }
594
698
 
699
+ /** Compute a start/end patch for moving and/or resizing an event.
700
+ * whenArg undefined => keep original start; durationArg undefined => keep original length.
701
+ * For all-day events durationArg is a day count; for timed events a duration string. */
702
+ function reschedulePatch(
703
+ event: GoogleEvent,
704
+ whenArg: string | undefined,
705
+ durationArg: string | undefined
706
+ ): { patch: Partial<GoogleEvent>; startDisplay: EventDateTime; endDisplay: EventDateTime } {
707
+ const origIsAllDay = !!event.start?.date;
708
+
709
+ if (origIsAllDay) {
710
+ const origStart = parseAllDay(event.start!.date!);
711
+ const origEnd = parseAllDay(event.end!.date!);
712
+ const origDurDays = Math.max(1, Math.round((origEnd.getTime() - origStart.getTime()) / 86400_000));
713
+
714
+ let newStart: Date;
715
+ if (whenArg) {
716
+ const adv = whenArg.match(/^\+(\d+)([dw])$/i);
717
+ if (adv) {
718
+ const [, n, unit] = adv;
719
+ const amt = parseInt(n);
720
+ newStart = new Date(origStart);
721
+ newStart.setDate(newStart.getDate() + (unit.toLowerCase() === 'w' ? amt * 7 : amt));
722
+ } else {
723
+ newStart = parseDateTime(whenArg);
724
+ newStart.setHours(0, 0, 0, 0);
725
+ }
726
+ } else {
727
+ newStart = new Date(origStart);
728
+ }
729
+ const durDays = durationArg ? (parseInt(durationArg, 10) || origDurDays) : origDurDays;
730
+ const newEnd = new Date(newStart);
731
+ newEnd.setDate(newEnd.getDate() + durDays);
732
+
733
+ const patch = { start: { date: formatYMD(newStart) }, end: { date: formatYMD(newEnd) } };
734
+ return { patch, startDisplay: patch.start, endDisplay: patch.end };
735
+ }
736
+
737
+ const origStart = new Date(event.start!.dateTime!);
738
+ const origEnd = new Date(event.end!.dateTime!);
739
+ const origDurMs = origEnd.getTime() - origStart.getTime();
740
+
741
+ let newStart: Date;
742
+ if (whenArg) {
743
+ const adv = whenArg.match(/^\+(\d+)([dwhm])$/i);
744
+ if (adv) {
745
+ const [, n, unit] = adv;
746
+ const amt = parseInt(n);
747
+ newStart = new Date(origStart);
748
+ switch (unit.toLowerCase()) {
749
+ case 'd': newStart.setDate(newStart.getDate() + amt); break;
750
+ case 'w': newStart.setDate(newStart.getDate() + amt * 7); break;
751
+ case 'h': newStart.setHours(newStart.getHours() + amt); break;
752
+ case 'm': newStart.setMinutes(newStart.getMinutes() + amt); break;
753
+ }
754
+ } else {
755
+ newStart = parseDateTime(whenArg);
756
+ if (!hasTimeComponent(whenArg)) {
757
+ newStart.setHours(origStart.getHours(), origStart.getMinutes(), 0, 0);
758
+ }
759
+ }
760
+ } else {
761
+ newStart = new Date(origStart);
762
+ }
763
+ const durMs = durationArg ? parseDuration(durationArg) * 60_000 : origDurMs;
764
+ const newEnd = new Date(newStart.getTime() + durMs);
765
+
766
+ const tz = event.start!.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
767
+ const patch = {
768
+ start: { dateTime: newStart.toISOString(), timeZone: tz },
769
+ end: { dateTime: newEnd.toISOString(), timeZone: tz }
770
+ };
771
+ return { patch, startDisplay: patch.start, endDisplay: patch.end };
772
+ }
773
+
774
+ /** Format a reminder override list for display, e.g. "10m, 1h" or "(none)". */
775
+ function formatReminders(overrides?: { minutes?: number }[]): string {
776
+ if (!overrides || overrides.length === 0) return '(none)';
777
+ return overrides
778
+ .map(r => r.minutes ?? 0)
779
+ .sort((a, b) => a - b)
780
+ .map(m => (m >= 60 && m % 60 === 0 ? `${m / 60}h` : `${m}m`))
781
+ .join(', ');
782
+ }
783
+
784
+ /** Open a URL in the platform's default browser. */
785
+ function openUrl(url: string): void {
786
+ if (process.platform === 'win32') {
787
+ execSync(`start "" "${url}"`, { stdio: 'ignore', shell: 'cmd.exe' });
788
+ } else if (process.platform === 'darwin') {
789
+ execSync(`open "${url}"`, { stdio: 'ignore' });
790
+ } else {
791
+ execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
792
+ }
793
+ }
794
+
595
795
  /** Match events by ID prefix and dedup recurring instances to the earliest.
596
796
  * `events` must be ordered by startTime (as returned by listEvents). */
597
797
  function findByPrefix(events: GoogleEvent[], prefix: string, includeBirthdays: boolean): GoogleEvent[] {
@@ -863,33 +1063,49 @@ async function main(): Promise<void> {
863
1063
  case 'add': {
864
1064
  // Explicit mode: gcal add "title" "when" [duration]
865
1065
  if (parsed.args.length >= 2 && !parsed.clip) {
866
- const [title, when, duration = '1h'] = parsed.args;
1066
+ const [title, when, third] = parsed.args;
867
1067
  const startTime = parseDateTime(when);
868
- const durationMins = parseDuration(duration);
869
- const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
870
1068
 
871
1069
  const event: GoogleEvent = {
872
1070
  summary: title,
873
- start: {
874
- dateTime: startTime.toISOString(),
875
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
876
- },
877
- end: {
878
- dateTime: endTime.toISOString(),
879
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
880
- },
1071
+ start: {},
1072
+ end: {},
881
1073
  reminders: buildReminders(parsed.reminders)
882
1074
  };
1075
+
1076
+ if (parsed.allDay) {
1077
+ // All-day: [duration] is a day count (default 1). Google's
1078
+ // end.date is exclusive, so a 1-day event ends the next day.
1079
+ const days = third ? (parseInt(third, 10) || 1) : 1;
1080
+ const startD = new Date(startTime);
1081
+ startD.setHours(0, 0, 0, 0);
1082
+ const endD = new Date(startD);
1083
+ endD.setDate(endD.getDate() + days);
1084
+ event.start = { date: formatYMD(startD) };
1085
+ event.end = { date: formatYMD(endD) };
1086
+ } else {
1087
+ const durationMins = parseDuration(third || '1h');
1088
+ const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
1089
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
1090
+ event.start = { dateTime: startTime.toISOString(), timeZone: tz };
1091
+ event.end = { dateTime: endTime.toISOString(), timeZone: tz };
1092
+ }
1093
+ if (parsed.transparency) event.transparency = parsed.transparency;
883
1094
  if (parsed.rrule) event.recurrence = [`RRULE:${parsed.rrule}`];
884
1095
 
885
1096
  const token = await getAccessToken(user, true);
886
- await checkProximity(token, parsed.calendar, startTime, endTime);
1097
+ if (!parsed.allDay) {
1098
+ await checkProximity(token, parsed.calendar,
1099
+ new Date(event.start.dateTime!), new Date(event.end.dateTime!));
1100
+ }
887
1101
  const created = await createEvent(token, event, parsed.calendar);
888
1102
  console.log(`\nEvent created: ${created.summary}`);
889
1103
  console.log(` When: ${formatDateTime(created.start)} - ${formatDateTime(created.end)}`);
1104
+ if (created.transparency === 'transparent') console.log(` Free (not busy)`);
890
1105
  if (created.htmlLink) {
891
1106
  console.log(` Link: ${created.htmlLink}`);
892
1107
  }
1108
+ if (parsed.open && created.htmlLink) openUrl(created.htmlLink);
893
1109
  break;
894
1110
  }
895
1111
 
@@ -949,6 +1165,7 @@ async function main(): Promise<void> {
949
1165
  description: extracted.description,
950
1166
  reminders: buildReminders(parsed.reminders)
951
1167
  };
1168
+ if (parsed.transparency) event.transparency = parsed.transparency;
952
1169
  events.push(event);
953
1170
 
954
1171
  console.log(`\n Event: ${extracted.summary}`);
@@ -992,6 +1209,7 @@ async function main(): Promise<void> {
992
1209
  if (created.htmlLink) {
993
1210
  console.log(` Link: ${created.htmlLink}`);
994
1211
  }
1212
+ if (parsed.open && created.htmlLink) openUrl(created.htmlLink);
995
1213
  }
996
1214
  break;
997
1215
  }
@@ -1128,6 +1346,111 @@ async function main(): Promise<void> {
1128
1346
  break;
1129
1347
  }
1130
1348
 
1349
+ case 'update':
1350
+ case 'edit':
1351
+ case 'set': {
1352
+ if (parsed.args.length < 1) {
1353
+ console.error('Usage: gcal update <id> [-when <when>] [-dur <dur>] [-title <text>]');
1354
+ console.error(' [-loc <text>] [-note <text>] [-free|-busy]');
1355
+ console.error(' [-r <dur>] [-rx <dur>] [-nr] [-open]');
1356
+ console.error('Use "gcal list" to see event IDs');
1357
+ process.exit(1);
1358
+ }
1359
+
1360
+ const patch: Partial<GoogleEvent> = {};
1361
+ if (parsed.setTitle !== undefined) patch.summary = parsed.setTitle;
1362
+ if (parsed.setLoc !== undefined) patch.location = parsed.setLoc;
1363
+ if (parsed.setNote !== undefined) patch.description = parsed.setNote;
1364
+ if (parsed.transparency) patch.transparency = parsed.transparency;
1365
+
1366
+ const changingTime = parsed.setWhen !== undefined || parsed.setDur !== undefined;
1367
+ const changingReminders = parsed.clearReminders
1368
+ || parsed.reminders.length > 0 || parsed.removeReminders.length > 0;
1369
+
1370
+ if (Object.keys(patch).length === 0 && !changingTime && !changingReminders) {
1371
+ console.error('Nothing to change. Specify -when, -dur, -title, -loc, -note,');
1372
+ console.error(' -free, -busy, -r (add reminder), -rx (remove reminder), or -nr.');
1373
+ process.exit(1);
1374
+ }
1375
+
1376
+ const idPrefix = parsed.args[0];
1377
+ const lookback = parsed.since
1378
+ ? parsed.since.toISOString()
1379
+ : new Date(Date.now() - 30 * 86400_000).toISOString();
1380
+ const timeMax = parsed.till ? parsed.till.toISOString() : undefined;
1381
+
1382
+ const token = await getAccessToken(user, true);
1383
+ const events = await listEvents(token, parsed.calendar, 250, lookback, timeMax);
1384
+ const unique = findByPrefix(events, idPrefix, parsed.birthdays);
1385
+
1386
+ if (unique.length === 0) {
1387
+ console.error(`${idPrefix}: not found (searched from ${lookback.slice(0, 10)})`);
1388
+ process.exit(1);
1389
+ }
1390
+ if (unique.length > 1) {
1391
+ console.error(`${idPrefix}: ambiguous (${unique.length} matches)`);
1392
+ for (const e of unique) {
1393
+ console.error(` ${e.id?.slice(0, 8)} - ${e.summary}`);
1394
+ }
1395
+ process.exit(1);
1396
+ }
1397
+
1398
+ const event = unique[0];
1399
+
1400
+ // Time / duration change (reuses the resched logic)
1401
+ let timeFrom = '';
1402
+ let timeTo = '';
1403
+ if (changingTime) {
1404
+ const r = reschedulePatch(event, parsed.setWhen, parsed.setDur);
1405
+ patch.start = r.patch.start;
1406
+ patch.end = r.patch.end;
1407
+ timeFrom = `${formatDateTime(event.start!)} - ${formatDateTime(event.end!)}`;
1408
+ timeTo = `${formatDateTime(r.startDisplay)} - ${formatDateTime(r.endDisplay)}`;
1409
+ }
1410
+
1411
+ // Reminders: -nr clears all; otherwise merge existing with -r adds and -rx removes
1412
+ if (parsed.clearReminders) {
1413
+ patch.reminders = { useDefault: false, overrides: [] };
1414
+ } else if (changingReminders) {
1415
+ const mins = new Set<number>((event.reminders?.overrides ?? []).map(r => r.minutes ?? 0));
1416
+ for (const m of parsed.removeReminders) mins.delete(m);
1417
+ for (const m of parsed.reminders) mins.add(m);
1418
+ patch.reminders = {
1419
+ useDefault: false,
1420
+ overrides: [...mins].sort((a, b) => a - b).map(m => ({ method: 'popup' as const, minutes: m }))
1421
+ };
1422
+ }
1423
+
1424
+ // Proximity check when the timed slot moved
1425
+ if (patch.start?.dateTime && patch.end?.dateTime) {
1426
+ await checkProximity(
1427
+ token,
1428
+ parsed.calendar,
1429
+ new Date(patch.start.dateTime),
1430
+ new Date(patch.end.dateTime),
1431
+ (event.id || '').split('_')[0]
1432
+ );
1433
+ }
1434
+
1435
+ const updated = await patchEvent(token, event.id!, patch, parsed.calendar);
1436
+ console.log(`Updated: ${updated.summary}`);
1437
+ if (patch.summary !== undefined) console.log(` Title: ${updated.summary}`);
1438
+ if (changingTime) {
1439
+ console.log(` When: ${timeFrom}`);
1440
+ console.log(` -> ${timeTo}`);
1441
+ }
1442
+ if (patch.location !== undefined) console.log(` Where: ${updated.location || '(cleared)'}`);
1443
+ if (patch.description !== undefined) console.log(` Note: ${updated.description || '(cleared)'}`);
1444
+ if (patch.transparency !== undefined) {
1445
+ console.log(` Shows as: ${updated.transparency === 'transparent' ? 'free' : 'busy'}`);
1446
+ }
1447
+ if (patch.reminders !== undefined) {
1448
+ console.log(` Reminders: ${formatReminders(updated.reminders?.overrides)}`);
1449
+ }
1450
+ if (parsed.open && updated.htmlLink) openUrl(updated.htmlLink);
1451
+ break;
1452
+ }
1453
+
1131
1454
  case 'resched':
1132
1455
  case 'reschedule':
1133
1456
  case 'snooze': {
@@ -1177,73 +1500,10 @@ async function main(): Promise<void> {
1177
1500
  }
1178
1501
  }
1179
1502
 
1180
- const origIsAllDay = !!event.start?.date;
1181
- let patch: Partial<GoogleEvent>;
1182
- let newStartDisplay: { date?: string; dateTime?: string; timeZone?: string };
1183
- let newEndDisplay: { date?: string; dateTime?: string; timeZone?: string };
1184
-
1185
- if (origIsAllDay) {
1186
- const origStart = parseAllDay(event.start!.date!);
1187
- const origEnd = parseAllDay(event.end!.date!);
1188
- const origDurDays = Math.max(1, Math.round((origEnd.getTime() - origStart.getTime()) / 86400_000));
1189
-
1190
- let newStart: Date;
1191
- const adv = whenArg.match(/^\+(\d+)([dw])$/i);
1192
- if (adv) {
1193
- const [, n, unit] = adv;
1194
- const amt = parseInt(n);
1195
- newStart = new Date(origStart);
1196
- newStart.setDate(newStart.getDate() + (unit.toLowerCase() === 'w' ? amt * 7 : amt));
1197
- } else {
1198
- newStart = parseDateTime(whenArg);
1199
- newStart.setHours(0, 0, 0, 0);
1200
- }
1201
- const newEnd = new Date(newStart);
1202
- newEnd.setDate(newEnd.getDate() + origDurDays);
1203
-
1204
- patch = {
1205
- start: { date: formatYMD(newStart) },
1206
- end: { date: formatYMD(newEnd) }
1207
- };
1208
- newStartDisplay = { date: formatYMD(newStart) };
1209
- newEndDisplay = { date: formatYMD(newEnd) };
1210
- } else {
1211
- const origStart = new Date(event.start!.dateTime!);
1212
- const origEnd = new Date(event.end!.dateTime!);
1213
- const origDurMs = origEnd.getTime() - origStart.getTime();
1214
-
1215
- let newStart: Date;
1216
- const adv = whenArg.match(/^\+(\d+)([dwhm])$/i);
1217
- if (adv) {
1218
- const [, n, unit] = adv;
1219
- const amt = parseInt(n);
1220
- newStart = new Date(origStart);
1221
- switch (unit.toLowerCase()) {
1222
- case 'd': newStart.setDate(newStart.getDate() + amt); break;
1223
- case 'w': newStart.setDate(newStart.getDate() + amt * 7); break;
1224
- case 'h': newStart.setHours(newStart.getHours() + amt); break;
1225
- case 'm': newStart.setMinutes(newStart.getMinutes() + amt); break;
1226
- }
1227
- } else {
1228
- newStart = parseDateTime(whenArg);
1229
- if (!hasTimeComponent(whenArg)) {
1230
- newStart.setHours(origStart.getHours(), origStart.getMinutes(), 0, 0);
1231
- }
1232
- }
1233
- const durMs = durationArg ? parseDuration(durationArg) * 60_000 : origDurMs;
1234
- const newEnd = new Date(newStart.getTime() + durMs);
1235
-
1236
- const tz = event.start!.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
1237
- patch = {
1238
- start: { dateTime: newStart.toISOString(), timeZone: tz },
1239
- end: { dateTime: newEnd.toISOString(), timeZone: tz }
1240
- };
1241
- newStartDisplay = patch.start!;
1242
- newEndDisplay = patch.end!;
1243
- }
1503
+ const { patch, startDisplay, endDisplay } = reschedulePatch(event, whenArg, durationArg);
1244
1504
 
1245
1505
  // Proximity check for timed events (skip all-day)
1246
- if (!origIsAllDay && patch.start?.dateTime && patch.end?.dateTime) {
1506
+ if (patch.start?.dateTime && patch.end?.dateTime) {
1247
1507
  await checkProximity(
1248
1508
  token,
1249
1509
  parsed.calendar,
@@ -1256,7 +1516,8 @@ async function main(): Promise<void> {
1256
1516
  const updated = await patchEvent(token, event.id!, patch, parsed.calendar);
1257
1517
  console.log(`Rescheduled: ${updated.summary}`);
1258
1518
  console.log(` From: ${formatDateTime(event.start!)} - ${formatDateTime(event.end!)}`);
1259
- console.log(` To: ${formatDateTime(newStartDisplay)} - ${formatDateTime(newEndDisplay)}`);
1519
+ console.log(` To: ${formatDateTime(startDisplay)} - ${formatDateTime(endDisplay)}`);
1520
+ if (parsed.open && updated.htmlLink) openUrl(updated.htmlLink);
1260
1521
  break;
1261
1522
  }
1262
1523
 
@@ -1344,6 +1605,7 @@ async function main(): Promise<void> {
1344
1605
  if (event.hangoutLink) console.log(` Meet: ${event.hangoutLink}`);
1345
1606
  if (event.htmlLink) console.log(` Link: ${event.htmlLink}`);
1346
1607
  console.log(` Status: ${event.status || 'confirmed'}`);
1608
+ console.log(` Shows as: ${event.transparency === 'transparent' ? 'free' : 'busy'}`);
1347
1609
  if (event.created) console.log(` Created: ${formatDateTime({ dateTime: event.created })}`);
1348
1610
  if (event.updated) console.log(` Updated: ${formatDateTime({ dateTime: event.updated })}`);
1349
1611
  console.log(` ID: ${event.id}`);
@@ -1389,13 +1651,7 @@ async function main(): Promise<void> {
1389
1651
  console.log(`Opening: ${event.summary || '(no title)'}`);
1390
1652
  console.log(` ${event.htmlLink}`);
1391
1653
 
1392
- if (process.platform === 'win32') {
1393
- execSync(`start "" "${event.htmlLink}"`, { stdio: 'ignore', shell: 'cmd.exe' });
1394
- } else if (process.platform === 'darwin') {
1395
- execSync(`open "${event.htmlLink}"`, { stdio: 'ignore' });
1396
- } else {
1397
- execSync(`xdg-open "${event.htmlLink}"`, { stdio: 'ignore' });
1398
- }
1654
+ openUrl(event.htmlLink);
1399
1655
  break;
1400
1656
  }
1401
1657
 
@@ -1 +1 @@
1
- {"version":3,"file":"gutils.d.ts","sourceRoot":"","sources":["gutils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,UAAU;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,OAAO,QAAoC,CAAC;AAEzD,+DAA+D;AAC/D,wBAAgB,SAAS,IAAI,MAAM,CAKlC;AAED,kFAAkF;AAClF,wBAAgB,UAAU,IAAI,MAAM,CAMnC;AAED,eAAO,MAAM,QAAQ,QAAe,CAAC;AACrC,eAAO,MAAM,WAAW,QAAwC,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAA8C,CAAC;AAE5E,wBAAgB,UAAU,IAAI,UAAU,CASvC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAMnD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAUpD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQhD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAStC;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAIpD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,MAAM,CAsDzE;AAID,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAgBlG;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmB7H;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAkKjD;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGjD;AAED,kDAAkD;AAClD,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAKzC;AAED,4BAA4B;AAC5B,wBAAgB,EAAE,IAAI,MAAM,CAG3B"}
1
+ {"version":3,"file":"gutils.d.ts","sourceRoot":"","sources":["gutils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,WAAW,UAAU;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,OAAO,QAAoC,CAAC;AAEzD,+DAA+D;AAC/D,wBAAgB,SAAS,IAAI,MAAM,CAKlC;AAED,kFAAkF;AAClF,wBAAgB,UAAU,IAAI,MAAM,CAMnC;AAED,eAAO,MAAM,QAAQ,QAAe,CAAC;AACrC,eAAO,MAAM,WAAW,QAAwC,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,QAA8C,CAAC;AAE5E,wBAAgB,UAAU,IAAI,UAAU,CASvC;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAMnD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAUpD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQhD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAStC;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAIpD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,MAAM,CAsDzE;AAID,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAgBlG;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAmB7H;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUtD;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CA+KjD;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGjD;AAED,kDAAkD;AAClD,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,GAAG,MAAM,CAKzC;AAED,4BAA4B;AAC5B,wBAAgB,EAAE,IAAI,MAAM,CAG3B"}
package/glib/gutils.js CHANGED
@@ -306,6 +306,19 @@ export function parseDateTime(input) {
306
306
  d.setHours(h, parseInt(min || '0'), 0, 0);
307
307
  return d;
308
308
  }
309
+ // Handle weekday name alone: "wed", "next wed", "friday" -> that day at midnight
310
+ const dayOnlyMatch = lower.match(/^(next\s+)?(sun|mon|tue|wed|thu|fri|sat)[a-z]*$/i);
311
+ if (dayOnlyMatch) {
312
+ const [, next, day] = dayOnlyMatch;
313
+ const targetDay = weekdayAbbr.indexOf(day.toLowerCase().slice(0, 3));
314
+ const d = new Date(now);
315
+ let daysUntil = targetDay - d.getDay();
316
+ if (daysUntil <= 0 || next)
317
+ daysUntil += 7;
318
+ d.setDate(d.getDate() + daysUntil);
319
+ d.setHours(0, 0, 0, 0);
320
+ return d;
321
+ }
309
322
  // Handle month names: "jan 15", "jan 15 2026", "jan 15 3pm", "january 15 2026 3pm"
310
323
  const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
311
324
  const monthMatch = lower.match(/^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\s+(\d{1,2})(?:\s+(\d{4}))?(?:\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/i);