@bobfrankston/gcal 0.1.10 → 0.1.12

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 +61 -67
  2. package/glib/gutils.ts +12 -17
  3. package/package.json +50 -46
package/gcal.ts CHANGED
@@ -96,12 +96,11 @@ async function listEvents(
96
96
  const params = new URLSearchParams({
97
97
  maxResults: maxResults.toString(),
98
98
  singleEvents: 'true',
99
- orderBy: 'startTime',
100
- timeMin: timeMin || new Date().toISOString()
99
+ orderBy: 'startTime'
101
100
  });
102
- if (timeMax) {
103
- params.set('timeMax', timeMax);
104
- }
101
+ if (timeMin) params.set('timeMin', timeMin);
102
+ if (timeMax) params.set('timeMax', timeMax);
103
+ if (!timeMin && !timeMax) params.set('timeMin', new Date().toISOString());
105
104
 
106
105
  const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
107
106
  const res = await apiFetch(url, accessToken);
@@ -232,6 +231,7 @@ Usage:
232
231
 
233
232
  Commands:
234
233
  list [n] List upcoming n events (default: 10)
234
+ past [n] List past n events (default: 10)
235
235
  add <title> <when> [duration] Add event
236
236
  del|delete <id> [id2...] Delete event(s) by ID (prefix match)
237
237
  import <file.ics> Import events from ICS file
@@ -243,19 +243,14 @@ Options:
243
243
  -defaultUser <email> Set default user for future use
244
244
  -c, -calendar <id> Calendar ID (default: primary)
245
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
248
- -verbose Show event IDs and links
249
- -v, --version Show version
246
+ -v, -verbose Show event IDs and links
250
247
 
251
248
  Examples:
252
249
  gcal meeting.ics Import ICS file
253
250
  gcal list List next 10 events
254
- gcal list -after tomorrow -before "jan 30"
255
251
  gcal add "Dentist" "Friday 3pm" "1h"
256
252
  gcal add "Lunch" "1/14/2026 12:00" "1h"
257
- gcal add "Meeting" "tomorrow 10am"
258
- gcal add "Call" "3pm" "30m"
253
+ gcal add "Meeting" "tomorrow 10:00"
259
254
  gcal add "Appointment" "jan 15 2pm"
260
255
  gcal -defaultUser bob@gmail.com Set default user
261
256
 
@@ -274,10 +269,7 @@ interface ParsedArgs {
274
269
  count: number;
275
270
  help: boolean;
276
271
  verbose: boolean;
277
- version: boolean;
278
272
  icsFile: string; /** Direct .ics file path */
279
- after: string; /** Filter: events after this date/time */
280
- before: string; /** Filter: events before this date/time */
281
273
  }
282
274
 
283
275
  function parseArgs(argv: string[]): ParsedArgs {
@@ -290,10 +282,7 @@ function parseArgs(argv: string[]): ParsedArgs {
290
282
  count: 10,
291
283
  help: false,
292
284
  verbose: false,
293
- version: false,
294
- icsFile: '',
295
- after: '',
296
- before: ''
285
+ icsFile: ''
297
286
  };
298
287
 
299
288
  const unknown: string[] = [];
@@ -318,23 +307,11 @@ function parseArgs(argv: string[]): ParsedArgs {
318
307
  case '-n':
319
308
  result.count = parseInt(argv[++i]) || 10;
320
309
  break;
321
- case '-after':
322
- case '--after':
323
- result.after = argv[++i] || '';
324
- break;
325
- case '-before':
326
- case '--before':
327
- result.before = argv[++i] || '';
328
- break;
310
+ case '-v':
329
311
  case '-verbose':
330
312
  case '--verbose':
331
313
  result.verbose = true;
332
314
  break;
333
- case '-v':
334
- case '-V':
335
- case '--version':
336
- result.version = true;
337
- break;
338
315
  case '-h':
339
316
  case '-help':
340
317
  case '--help':
@@ -405,12 +382,6 @@ async function main(): Promise<void> {
405
382
  }
406
383
  }
407
384
 
408
- if (parsed.version) {
409
- const pkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, 'package.json'), 'utf-8'));
410
- console.log(`gcal ${pkg.version}`);
411
- process.exit(0);
412
- }
413
-
414
385
  if (parsed.help) {
415
386
  showUsage();
416
387
  process.exit(0);
@@ -461,9 +432,7 @@ async function main(): Promise<void> {
461
432
  case 'list': {
462
433
  const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
463
434
  const token = await getAccessToken(user, false);
464
- const timeMin = parsed.after ? parseDateTime(parsed.after).toISOString() : undefined;
465
- const timeMax = parsed.before ? parseDateTime(parsed.before).toISOString() : undefined;
466
- const events = await listEvents(token, parsed.calendar, count, timeMin, timeMax);
435
+ const events = await listEvents(token, parsed.calendar, count);
467
436
 
468
437
  if (events.length === 0) {
469
438
  console.log('No upcoming events found.');
@@ -507,6 +476,57 @@ async function main(): Promise<void> {
507
476
  break;
508
477
  }
509
478
 
479
+ case 'past': {
480
+ const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
481
+ const token = await getAccessToken(user, false);
482
+ // Get events before now, then reverse to show most recent first
483
+ const now = new Date();
484
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
485
+ const events = await listEvents(token, parsed.calendar, count, thirtyDaysAgo.toISOString(), now.toISOString());
486
+ events.reverse(); // Most recent first
487
+
488
+ if (events.length === 0) {
489
+ console.log('No past events found in last 30 days.');
490
+ } else {
491
+ console.log(`\nPast events (${events.length}):\n`);
492
+
493
+ // Build table data (same format as list)
494
+ const rows: string[][] = [];
495
+ for (const event of events) {
496
+ const shortId = (event.id || '').slice(0, 8);
497
+ const start = event.start ? formatDateTime(event.start) : '?';
498
+ const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
499
+ const summary = event.summary || '(no title)';
500
+ const loc = event.location || '';
501
+ if (parsed.verbose) {
502
+ rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
503
+ } else {
504
+ rows.push([shortId, start, duration, summary, loc]);
505
+ }
506
+ }
507
+
508
+ // Calculate column widths
509
+ const headers = parsed.verbose
510
+ ? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
511
+ : ['ID', 'When', 'Dur', 'Event', 'Location'];
512
+ const colWidths = headers.map((h, i) =>
513
+ Math.max(h.length, ...rows.map(r => (r[i] || '').length))
514
+ );
515
+
516
+ // Print header
517
+ const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
518
+ console.log(headerLine);
519
+ console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
520
+
521
+ // Print rows
522
+ for (const row of rows) {
523
+ const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
524
+ console.log(line);
525
+ }
526
+ }
527
+ break;
528
+ }
529
+
510
530
  case 'add': {
511
531
  if (parsed.args.length < 2) {
512
532
  console.error('Usage: gcal add <title> <when> [duration]');
@@ -519,32 +539,6 @@ async function main(): Promise<void> {
519
539
  const durationMins = parseDuration(duration);
520
540
  const endTime = new Date(startTime.getTime() + durationMins * 60 * 1000);
521
541
 
522
- // Check for suspicious dates (likely parsing errors)
523
- const now = new Date();
524
- const twoYearsFromNow = new Date(now);
525
- twoYearsFromNow.setFullYear(twoYearsFromNow.getFullYear() + 2);
526
-
527
- let warning = '';
528
- if (startTime < now) {
529
- warning = `Date is in the past: ${formatDateTime({ dateTime: startTime.toISOString() })}`;
530
- } else if (startTime > twoYearsFromNow) {
531
- warning = `Date is more than 2 years away: ${formatDateTime({ dateTime: startTime.toISOString() })}`;
532
- }
533
-
534
- if (warning) {
535
- console.log(`\nWarning: ${warning}`);
536
- console.log(`Input was: "${when}"`);
537
- const readline = await import('readline');
538
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
539
- const response = await new Promise<string>(resolve => {
540
- rl.question('Continue? (y/N) ', answer => { rl.close(); resolve(answer); });
541
- });
542
- if (response.toLowerCase() !== 'y') {
543
- console.log('Cancelled.');
544
- process.exit(0);
545
- }
546
- }
547
-
548
542
  const event: GoogleEvent = {
549
543
  summary: title,
550
544
  start: {
package/glib/gutils.ts CHANGED
@@ -298,36 +298,31 @@ export function parseDateTime(input: string): Date {
298
298
  }
299
299
  }
300
300
 
301
- // Handle time only (HH:mm) - assume today
301
+ // Handle time only (HH:mm) - assume today, or tomorrow if time already passed
302
302
  const timeMatch = input.match(/^(\d{1,2}):(\d{2})$/);
303
303
  if (timeMatch) {
304
304
  const [, hour, min] = timeMatch;
305
305
  const d = new Date(now);
306
306
  d.setHours(parseInt(hour), parseInt(min), 0, 0);
307
+ if (d <= now) {
308
+ d.setDate(d.getDate() + 1); // Time passed, use tomorrow
309
+ }
307
310
  return d;
308
311
  }
309
312
 
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);
313
+ // Handle time with am/pm (2pm, 10am, 2:30pm) - assume today, or tomorrow if time already passed
314
+ const ampmMatch = lower.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/);
315
+ if (ampmMatch) {
316
+ const [, hour, min, ampm] = ampmMatch;
315
317
  let h = parseInt(hour);
316
318
  if (ampm === 'pm' && h < 12) h += 12;
317
319
  if (ampm === 'am' && h === 12) h = 0;
320
+ const d = new Date(now);
318
321
  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;
322
+ if (d <= now) {
323
+ d.setDate(d.getDate() + 1); // Time passed, use tomorrow
330
324
  }
325
+ return d;
331
326
  }
332
327
 
333
328
  // Try native Date parsing
package/package.json CHANGED
@@ -1,46 +1,50 @@
1
- {
2
- "name": "@bobfrankston/gcal",
3
- "version": "0.1.10",
4
- "description": "Google Calendar CLI tool with ICS import support",
5
- "type": "module",
6
- "main": "gcal.ts",
7
- "bin": {
8
- "gcal": "gcal.ts"
9
- },
10
- "files": [
11
- "gcal.ts",
12
- "glib/**/*.ts",
13
- "tsconfig.json",
14
- "README.md"
15
- ],
16
- "repository": {
17
- "type": "git",
18
- "url": "git+https://github.com/BobFrankston/gcal.git"
19
- },
20
- "scripts": {
21
- "check": "tsc --noEmit",
22
- "prerelease:local": "git add -A && (git diff-index --quiet HEAD || git commit -m \"Pre-release commit\")",
23
- "preversion": "npm run check && git add -A",
24
- "postversion": "git push && git push --tags",
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",
27
- "installer": "npm run release && npm install -g ."
28
- },
29
- "keywords": [
30
- "google",
31
- "calendar",
32
- "ics",
33
- "ical",
34
- "oauth",
35
- "cli"
36
- ],
37
- "author": "Bob Frankston",
38
- "license": "MIT",
39
- "devDependencies": {
40
- "@types/node": "^25.0.3"
41
- },
42
- "dependencies": {
43
- "@bobfrankston/oauthsupport": "^1.0.1",
44
- "ical.js": "^2.1.0"
45
- }
46
- }
1
+ {
2
+ "name": "@bobfrankston/gcal",
3
+ "version": "0.1.12",
4
+ "description": "Google Calendar CLI tool with ICS import support",
5
+ "type": "module",
6
+ "main": "gcal.ts",
7
+ "bin": {
8
+ "gcal": "gcal.ts"
9
+ },
10
+ "files": [
11
+ "gcal.ts",
12
+ "glib/**/*.ts",
13
+ "tsconfig.json",
14
+ "README.md"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/BobFrankston/gcal.git"
19
+ },
20
+ "scripts": {
21
+ "check": "tsc --noEmit",
22
+ "prerelease:local": "git add -A && (git diff-index --quiet HEAD || git commit -m \"Pre-release commit\")",
23
+ "preversion": "npm run check && git add -A",
24
+ "postversion": "git push && git push --tags",
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",
27
+ "installer": "npm run release && npm install -g ."
28
+ },
29
+ "keywords": [
30
+ "google",
31
+ "calendar",
32
+ "ics",
33
+ "ical",
34
+ "oauth",
35
+ "cli"
36
+ ],
37
+ "author": "Bob Frankston",
38
+ "license": "MIT",
39
+ "devDependencies": {
40
+ "@types/node": "^25.0.9"
41
+ },
42
+ "dependencies": {
43
+ "@bobfrankston/oauthsupport": "^1.0.5",
44
+ "ical.js": "^2.1.0"
45
+ },
46
+ ".dependencies": {
47
+ "@bobfrankston/oauthsupport": "file:../../../projects/OAuth/OauthSupport",
48
+ "ical.js": "^2.1.0"
49
+ }
50
+ }