@bobfrankston/gcal 0.1.7 → 0.1.9

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 (3) hide show
  1. package/gcal.ts +53 -4
  2. package/glib/gutils.ts +43 -0
  3. package/package.json +3 -2
package/gcal.ts CHANGED
@@ -90,7 +90,8 @@ async function listEvents(
90
90
  accessToken: string,
91
91
  calendarId = 'primary',
92
92
  maxResults = 10,
93
- timeMin?: string
93
+ timeMin?: string,
94
+ timeMax?: string
94
95
  ): Promise<GoogleEvent[]> {
95
96
  const params = new URLSearchParams({
96
97
  maxResults: maxResults.toString(),
@@ -98,6 +99,9 @@ async function listEvents(
98
99
  orderBy: 'startTime',
99
100
  timeMin: timeMin || new Date().toISOString()
100
101
  });
102
+ if (timeMax) {
103
+ params.set('timeMax', timeMax);
104
+ }
101
105
 
102
106
  const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
103
107
  const res = await apiFetch(url, accessToken);
@@ -239,14 +243,19 @@ Options:
239
243
  -defaultUser <email> Set default user for future use
240
244
  -c, -calendar <id> Calendar ID (default: primary)
241
245
  -n <count> Number of events to list
246
+ -after <when> List events after this date/time
247
+ -before <when> List events before this date/time
242
248
  -v, -verbose Show event IDs and links
243
249
 
244
250
  Examples:
245
251
  gcal meeting.ics Import ICS file
246
252
  gcal list List next 10 events
253
+ gcal list -after tomorrow -before "jan 30"
247
254
  gcal add "Dentist" "Friday 3pm" "1h"
248
255
  gcal add "Lunch" "1/14/2026 12:00" "1h"
249
- gcal add "Meeting" "tomorrow 10:00"
256
+ gcal add "Meeting" "tomorrow 10am"
257
+ gcal add "Call" "3pm" "30m"
258
+ gcal add "Appointment" "jan 15 2pm"
250
259
  gcal -defaultUser bob@gmail.com Set default user
251
260
 
252
261
  File Association (Windows):
@@ -265,6 +274,8 @@ interface ParsedArgs {
265
274
  help: boolean;
266
275
  verbose: boolean;
267
276
  icsFile: string; /** Direct .ics file path */
277
+ after: string; /** Filter: events after this date/time */
278
+ before: string; /** Filter: events before this date/time */
268
279
  }
269
280
 
270
281
  function parseArgs(argv: string[]): ParsedArgs {
@@ -277,7 +288,9 @@ function parseArgs(argv: string[]): ParsedArgs {
277
288
  count: 10,
278
289
  help: false,
279
290
  verbose: false,
280
- icsFile: ''
291
+ icsFile: '',
292
+ after: '',
293
+ before: ''
281
294
  };
282
295
 
283
296
  const unknown: string[] = [];
@@ -302,6 +315,14 @@ function parseArgs(argv: string[]): ParsedArgs {
302
315
  case '-n':
303
316
  result.count = parseInt(argv[++i]) || 10;
304
317
  break;
318
+ case '-after':
319
+ case '--after':
320
+ result.after = argv[++i] || '';
321
+ break;
322
+ case '-before':
323
+ case '--before':
324
+ result.before = argv[++i] || '';
325
+ break;
305
326
  case '-v':
306
327
  case '-verbose':
307
328
  case '--verbose':
@@ -427,7 +448,9 @@ async function main(): Promise<void> {
427
448
  case 'list': {
428
449
  const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
429
450
  const token = await getAccessToken(user, false);
430
- const events = await listEvents(token, parsed.calendar, count);
451
+ const timeMin = parsed.after ? parseDateTime(parsed.after).toISOString() : undefined;
452
+ const timeMax = parsed.before ? parseDateTime(parsed.before).toISOString() : undefined;
453
+ const events = await listEvents(token, parsed.calendar, count, timeMin, timeMax);
431
454
 
432
455
  if (events.length === 0) {
433
456
  console.log('No upcoming events found.');
@@ -483,6 +506,32 @@ async function main(): Promise<void> {
483
506
  const durationMins = parseDuration(duration);
484
507
  const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
485
508
 
509
+ // Check for suspicious dates (likely parsing errors)
510
+ const now = new Date();
511
+ const twoYearsFromNow = new Date(now);
512
+ twoYearsFromNow.setFullYear(twoYearsFromNow.getFullYear() + 2);
513
+
514
+ let warning = '';
515
+ if (startTime < now) {
516
+ warning = `Date is in the past: ${formatDateTime({ dateTime: startTime.toISOString() })}`;
517
+ } else if (startTime > twoYearsFromNow) {
518
+ warning = `Date is more than 2 years away: ${formatDateTime({ dateTime: startTime.toISOString() })}`;
519
+ }
520
+
521
+ if (warning) {
522
+ console.log(`\nWarning: ${warning}`);
523
+ console.log(`Input was: "${when}"`);
524
+ const readline = await import('readline');
525
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
526
+ const response = await new Promise<string>(resolve => {
527
+ rl.question('Continue? (y/N) ', answer => { rl.close(); resolve(answer); });
528
+ });
529
+ if (response.toLowerCase() !== 'y') {
530
+ console.log('Cancelled.');
531
+ process.exit(0);
532
+ }
533
+ }
534
+
486
535
  const event: GoogleEvent = {
487
536
  summary: title,
488
537
  start: {
package/glib/gutils.ts CHANGED
@@ -259,6 +259,26 @@ export function parseDateTime(input: string): Date {
259
259
  return d;
260
260
  }
261
261
 
262
+ // Handle month names: "jan 15", "jan 15 2026", "jan 15 3pm", "january 15 2026 3pm"
263
+ const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
264
+ 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);
265
+ if (monthMatch) {
266
+ const [, month, day, year, hour, min, ampm] = monthMatch;
267
+ const monthIndex = months.findIndex(m => month.toLowerCase().startsWith(m));
268
+ const d = new Date(parseInt(year || now.getFullYear().toString()), monthIndex, parseInt(day));
269
+ if (hour) {
270
+ let h = parseInt(hour);
271
+ if (ampm?.toLowerCase() === 'pm' && h < 12) h += 12;
272
+ if (ampm?.toLowerCase() === 'am' && h === 12) h = 0;
273
+ d.setHours(h, parseInt(min || '0'), 0, 0);
274
+ } else {
275
+ d.setHours(0, 0, 0, 0);
276
+ }
277
+ if (!isNaN(d.getTime())) {
278
+ return d;
279
+ }
280
+ }
281
+
262
282
  // Handle explicit date/time formats: "MM/DD/YYYY HH:mm" or "YYYY-MM-DD HH:mm"
263
283
  const dateTimeMatch = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2})$/);
264
284
  if (dateTimeMatch) {
@@ -287,6 +307,29 @@ export function parseDateTime(input: string): Date {
287
307
  return d;
288
308
  }
289
309
 
310
+ // Handle time with am/pm: "10am", "3pm", "10:30am", "3:45pm" - assume today
311
+ const timeAmPmMatch = lower.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/);
312
+ if (timeAmPmMatch) {
313
+ const [, hour, min, ampm] = timeAmPmMatch;
314
+ const d = new Date(now);
315
+ let h = parseInt(hour);
316
+ if (ampm === 'pm' && h < 12) h += 12;
317
+ if (ampm === 'am' && h === 12) h = 0;
318
+ d.setHours(h, parseInt(min || '0'), 0, 0);
319
+ return d;
320
+ }
321
+
322
+ // Handle bare hour: "10", "14" - interpret as today at that hour (not a month)
323
+ const bareHourMatch = input.match(/^(\d{1,2})$/);
324
+ if (bareHourMatch) {
325
+ const hour = parseInt(bareHourMatch[1]);
326
+ if (hour >= 0 && hour <= 23) {
327
+ const d = new Date(now);
328
+ d.setHours(hour, 0, 0, 0);
329
+ return d;
330
+ }
331
+ }
332
+
290
333
  // Try native Date parsing
291
334
  const parsed = new Date(input);
292
335
  if (!isNaN(parsed.getTime())) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/gcal",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Google Calendar CLI tool with ICS import support",
5
5
  "type": "module",
6
6
  "main": "gcal.ts",
@@ -23,7 +23,8 @@
23
23
  "preversion": "npm run check && git add -A",
24
24
  "postversion": "git push && git push --tags",
25
25
  "release": "npm run prerelease:local && npm version patch && npm publish --access public",
26
- "release:local": "npm run prerelease:local && npm version patch && npm publish --access public --ignore-scripts"
26
+ "release:local": "npm run prerelease:local && npm version patch && npm publish --access public --ignore-scripts",
27
+ "installer": "npm run release && npm install -g ."
27
28
  },
28
29
  "keywords": [
29
30
  "google",