@bobfrankston/gcal 0.1.11 → 0.1.13
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 +93 -5
- package/glib/gutils.ts +8 -2
- package/package.json +4 -1
package/gcal.ts
CHANGED
|
@@ -90,14 +90,17 @@ 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(),
|
|
97
98
|
singleEvents: 'true',
|
|
98
|
-
orderBy: 'startTime'
|
|
99
|
-
timeMin: timeMin || new Date().toISOString()
|
|
99
|
+
orderBy: 'startTime'
|
|
100
100
|
});
|
|
101
|
+
if (timeMin) params.set('timeMin', timeMin);
|
|
102
|
+
if (timeMax) params.set('timeMax', timeMax);
|
|
103
|
+
if (!timeMin && !timeMax) params.set('timeMin', new Date().toISOString());
|
|
101
104
|
|
|
102
105
|
const url = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events?${params}`;
|
|
103
106
|
const res = await apiFetch(url, accessToken);
|
|
@@ -228,6 +231,7 @@ Usage:
|
|
|
228
231
|
|
|
229
232
|
Commands:
|
|
230
233
|
list [n] List upcoming n events (default: 10)
|
|
234
|
+
past [n] List past n events (default: 10)
|
|
231
235
|
add <title> <when> [duration] Add event
|
|
232
236
|
del|delete <id> [id2...] Delete event(s) by ID (prefix match)
|
|
233
237
|
import <file.ics> Import events from ICS file
|
|
@@ -239,6 +243,7 @@ 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
|
+
-r, -reminder <time> Reminder before event (10, 30m, 1h, 1d)
|
|
242
247
|
-v, -verbose Show event IDs and links
|
|
243
248
|
|
|
244
249
|
Examples:
|
|
@@ -247,7 +252,7 @@ Examples:
|
|
|
247
252
|
gcal add "Dentist" "Friday 3pm" "1h"
|
|
248
253
|
gcal add "Lunch" "1/14/2026 12:00" "1h"
|
|
249
254
|
gcal add "Meeting" "tomorrow 10:00"
|
|
250
|
-
gcal add "Appointment" "jan 15 2pm"
|
|
255
|
+
gcal add "Appointment" "jan 15 2pm" -r 1h
|
|
251
256
|
gcal -defaultUser bob@gmail.com Set default user
|
|
252
257
|
|
|
253
258
|
File Association (Windows):
|
|
@@ -266,6 +271,20 @@ interface ParsedArgs {
|
|
|
266
271
|
help: boolean;
|
|
267
272
|
verbose: boolean;
|
|
268
273
|
icsFile: string; /** Direct .ics file path */
|
|
274
|
+
reminder: number; /** Reminder in minutes, -1 = not set */
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Parse reminder string: number (minutes), or suffix m/h/d */
|
|
278
|
+
function parseReminder(val: string): number {
|
|
279
|
+
const match = val.match(/^(\d+)(m|h|d)?$/i);
|
|
280
|
+
if (!match) return -1;
|
|
281
|
+
const num = parseInt(match[1]);
|
|
282
|
+
const unit = (match[2] || 'm').toLowerCase();
|
|
283
|
+
switch (unit) {
|
|
284
|
+
case 'h': return num * 60;
|
|
285
|
+
case 'd': return num * 60 * 24;
|
|
286
|
+
default: return num;
|
|
287
|
+
}
|
|
269
288
|
}
|
|
270
289
|
|
|
271
290
|
function parseArgs(argv: string[]): ParsedArgs {
|
|
@@ -278,7 +297,8 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
278
297
|
count: 10,
|
|
279
298
|
help: false,
|
|
280
299
|
verbose: false,
|
|
281
|
-
icsFile: ''
|
|
300
|
+
icsFile: '',
|
|
301
|
+
reminder: -1
|
|
282
302
|
};
|
|
283
303
|
|
|
284
304
|
const unknown: string[] = [];
|
|
@@ -308,6 +328,11 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
308
328
|
case '--verbose':
|
|
309
329
|
result.verbose = true;
|
|
310
330
|
break;
|
|
331
|
+
case '-r':
|
|
332
|
+
case '-reminder':
|
|
333
|
+
case '--reminder':
|
|
334
|
+
result.reminder = parseReminder(argv[++i] || '');
|
|
335
|
+
break;
|
|
311
336
|
case '-h':
|
|
312
337
|
case '-help':
|
|
313
338
|
case '--help':
|
|
@@ -472,6 +497,57 @@ async function main(): Promise<void> {
|
|
|
472
497
|
break;
|
|
473
498
|
}
|
|
474
499
|
|
|
500
|
+
case 'past': {
|
|
501
|
+
const count = parsed.args[0] ? parseInt(parsed.args[0]) : parsed.count;
|
|
502
|
+
const token = await getAccessToken(user, false);
|
|
503
|
+
// Get events before now, then reverse to show most recent first
|
|
504
|
+
const now = new Date();
|
|
505
|
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
506
|
+
const events = await listEvents(token, parsed.calendar, count, thirtyDaysAgo.toISOString(), now.toISOString());
|
|
507
|
+
events.reverse(); // Most recent first
|
|
508
|
+
|
|
509
|
+
if (events.length === 0) {
|
|
510
|
+
console.log('No past events found in last 30 days.');
|
|
511
|
+
} else {
|
|
512
|
+
console.log(`\nPast events (${events.length}):\n`);
|
|
513
|
+
|
|
514
|
+
// Build table data (same format as list)
|
|
515
|
+
const rows: string[][] = [];
|
|
516
|
+
for (const event of events) {
|
|
517
|
+
const shortId = (event.id || '').slice(0, 8);
|
|
518
|
+
const start = event.start ? formatDateTime(event.start) : '?';
|
|
519
|
+
const duration = (event.start && event.end) ? formatDuration(event.start, event.end) : '';
|
|
520
|
+
const summary = event.summary || '(no title)';
|
|
521
|
+
const loc = event.location || '';
|
|
522
|
+
if (parsed.verbose) {
|
|
523
|
+
rows.push([shortId, start, duration, summary, loc, event.htmlLink || '']);
|
|
524
|
+
} else {
|
|
525
|
+
rows.push([shortId, start, duration, summary, loc]);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Calculate column widths
|
|
530
|
+
const headers = parsed.verbose
|
|
531
|
+
? ['ID', 'When', 'Dur', 'Event', 'Location', 'Link']
|
|
532
|
+
: ['ID', 'When', 'Dur', 'Event', 'Location'];
|
|
533
|
+
const colWidths = headers.map((h, i) =>
|
|
534
|
+
Math.max(h.length, ...rows.map(r => (r[i] || '').length))
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
// Print header
|
|
538
|
+
const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(' ');
|
|
539
|
+
console.log(headerLine);
|
|
540
|
+
console.log(colWidths.map(w => '-'.repeat(w)).join(' '));
|
|
541
|
+
|
|
542
|
+
// Print rows
|
|
543
|
+
for (const row of rows) {
|
|
544
|
+
const line = row.map((cell, i) => (cell || '').padEnd(colWidths[i])).join(' ');
|
|
545
|
+
console.log(line);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
|
|
475
551
|
case 'add': {
|
|
476
552
|
if (parsed.args.length < 2) {
|
|
477
553
|
console.error('Usage: gcal add <title> <when> [duration]');
|
|
@@ -496,10 +572,22 @@ async function main(): Promise<void> {
|
|
|
496
572
|
}
|
|
497
573
|
};
|
|
498
574
|
|
|
575
|
+
if (parsed.reminder >= 0) {
|
|
576
|
+
event.reminders = {
|
|
577
|
+
useDefault: false,
|
|
578
|
+
overrides: [{ method: 'popup', minutes: parsed.reminder }]
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
499
582
|
const token = await getAccessToken(user, true);
|
|
500
583
|
const created = await createEvent(token, event, parsed.calendar);
|
|
501
584
|
console.log(`\nEvent created: ${created.summary}`);
|
|
502
585
|
console.log(` When: ${formatDateTime(created.start)} - ${formatDateTime(created.end)}`);
|
|
586
|
+
if (parsed.reminder >= 0) {
|
|
587
|
+
const mins = parsed.reminder;
|
|
588
|
+
const reminderStr = mins >= 1440 ? `${mins / 1440}d` : mins >= 60 ? `${mins / 60}h` : `${mins}m`;
|
|
589
|
+
console.log(` Reminder: ${reminderStr} before`);
|
|
590
|
+
}
|
|
503
591
|
if (created.htmlLink) {
|
|
504
592
|
console.log(` Link: ${created.htmlLink}`);
|
|
505
593
|
}
|
package/glib/gutils.ts
CHANGED
|
@@ -298,16 +298,19 @@ 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 (2pm, 10am, 2:30pm) - assume today
|
|
313
|
+
// Handle time with am/pm (2pm, 10am, 2:30pm) - assume today, or tomorrow if time already passed
|
|
311
314
|
const ampmMatch = lower.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)$/);
|
|
312
315
|
if (ampmMatch) {
|
|
313
316
|
const [, hour, min, ampm] = ampmMatch;
|
|
@@ -316,6 +319,9 @@ export function parseDateTime(input: string): Date {
|
|
|
316
319
|
if (ampm === 'am' && h === 12) h = 0;
|
|
317
320
|
const d = new Date(now);
|
|
318
321
|
d.setHours(h, parseInt(min || '0'), 0, 0);
|
|
322
|
+
if (d <= now) {
|
|
323
|
+
d.setDate(d.getDate() + 1); // Time passed, use tomorrow
|
|
324
|
+
}
|
|
319
325
|
return d;
|
|
320
326
|
}
|
|
321
327
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/gcal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Google Calendar CLI tool with ICS import support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "gcal.ts",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"@bobfrankston/oauthsupport": "^1.0.5",
|
|
44
44
|
"ical.js": "^2.1.0"
|
|
45
45
|
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
46
49
|
".dependencies": {
|
|
47
50
|
"@bobfrankston/oauthsupport": "file:../../../projects/OAuth/OauthSupport",
|
|
48
51
|
"ical.js": "^2.1.0"
|