@bobfrankston/gcal 0.1.46 → 0.1.47

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/README.md CHANGED
@@ -1,96 +1,150 @@
1
- # @bobfrankston/gcal
2
-
3
- Google Calendar CLI tool with ICS import support.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install -g @bobfrankston/gcal
9
- ```
10
-
11
- ## Usage
12
-
13
- ```bash
14
- gcal <file.ics> Import ICS file (file association)
15
- gcal <command> [options] Run command
16
- ```
17
-
18
- ## Commands
19
-
20
- | Command | Description |
21
- |---------|-------------|
22
- | `list [n]` | List upcoming n events (default: 10) |
23
- | `list -since <date>` | List events from `<date>` forward (past dates allowed) |
24
- | `list -till <date>` | List events up to `<date>` |
25
- | `list -since <d1> -till <d2>` | List events in a date range |
26
- | `add <title> <when> [duration]` | Add event (explicit args) |
27
- | `add` | Add event (type description interactively) |
28
- | `add "free text description"` | Add event (AI parses single text arg) |
29
- | `add -clip` | Add event from clipboard text (AI-parsed) |
30
- | `del\|delete <id> [id2...]` | Delete event(s) by ID (prefix match) |
31
- | `remind <id> <dur> [dur2...]` | Add reminder(s) to existing event |
32
- | `resched <id> <when> [duration]` | Reschedule event (preserves duration by default) |
33
- | `snooze <id> [when]` | Snooze event; defaults to `+1d` |
34
- | `import <file.ics>` | Import events from ICS file |
35
- | `calendars` | List available calendars |
36
- | `assoc` | Set up .ics file association (Windows) |
37
- | `help` | Show help |
38
-
39
- ## Options
40
-
41
- | Flag | Description |
42
- |------|-------------|
43
- | `-u`, `-user <email>` | Set default Google account |
44
- | `-c`, `-calendar <id>` | Calendar ID (default: primary) |
45
- | `-n <count>` | Number of events to list |
46
- | `-v`, `-verbose` | Show event IDs and links |
47
- | `-b`, `-birthdays` | Include birthday events (hidden by default) |
48
- | `-clip` | Read from clipboard (for add command) |
49
- | `-r`, `-reminder <dur>` | Add popup reminder (e.g., 30m, 1h); repeatable |
50
- | `-since <date>` | Start listing from `<date>` (e.g. `"10 days ago"`, `"April 1"`, `yesterday`) |
51
- | `-till <date>` | End listing at `<date>` |
52
- | `-all` | Delete all instances of recurring event |
53
-
54
- ## Examples
55
-
56
- ```bash
57
- gcal meeting.ics # Import ICS file
58
- gcal list # List next 10 events
59
- gcal list -since "10 days ago" # Include events from 10 days ago forward
60
- gcal list -since "april 1" -till "may 1" # Events in April
61
- gcal list -since "april 1" -n 50 # 50 events since April 1
62
- gcal add "Dentist" "Friday 3pm" "1h"
63
- gcal add "Lunch" "1/14/2026 12:00" "1h"
64
- gcal add "Meeting" "tomorrow 10:00"
65
- gcal add "Appointment" "jan 15 2pm"
66
- gcal add "Dentist appointment Friday 3pm for 1 hour"
67
- gcal add -clip # Add from clipboard text
68
- gcal add "Dentist" "Friday 3pm" -r 30m # Add with 30-min reminder
69
- gcal remind abc12345 30m # Add 30-min reminder to event
70
- gcal resched abc12345 "next friday 3pm" # Reschedule to new date/time
71
- gcal resched abc12345 tomorrow # Move to tomorrow (preserves time-of-day)
72
- gcal snooze abc12345 # Snooze +1 day
73
- gcal snooze abc12345 +1w # Snooze +1 week
74
- gcal add # Type event description
75
- gcal -u bob@gmail.com # Set default user
76
- ```
77
-
78
- ### Reschedule / snooze
79
-
80
- `resched` and `snooze` find events up to 30 days in the past by default, so stale
81
- reminder events remain findable. Use `-since <date>` to widen that window. For
82
- timed events, if `<when>` lacks a time-of-day (e.g. `tomorrow`, `next friday`) the
83
- original start time is preserved. All-day events stay all-day. Relative offsets
84
- `+1d` / `+1w` / `+1h` / `+1m` advance from the event's current start.
85
-
86
- ## File Association (Windows)
87
-
88
- ```bash
89
- gcal assoc # Sets up .ics → gcal automatically
90
- ```
91
-
92
- On first run, gcal will offer to set this up. Run `gcal assoc` anytime to (re)configure it.
93
-
94
- ## License
95
-
96
- MIT
1
+ # @bobfrankston/gcal
2
+
3
+ Google Calendar **and** Google Tasks CLI tools. Two binaries (`gcal`, `gtask`) shipped together — they share OAuth credentials, scopes, and token files, so you authenticate once.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @bobfrankston/gcal
9
+ ```
10
+
11
+ Provides:
12
+ - `gcal` — Google Calendar
13
+ - `gtask` — Google Tasks
14
+
15
+ Both share OAuth client credentials with `gcards` (`%APPDATA%\gcards\credentials.json`).
16
+
17
+ ## gcal — Google Calendar
18
+
19
+ ```bash
20
+ gcal <file.ics> # Import ICS file (file association)
21
+ gcal <command> [options]
22
+ gcal help <command> # Detailed help for one command
23
+ ```
24
+
25
+ ### Commands
26
+
27
+ | Command | Description |
28
+ |---------|-------------|
29
+ | `list [n]` | List upcoming n events (default: 10) |
30
+ | `add <title> <when> [duration]` | Add event (explicit) |
31
+ | `add "<free text>"` | Add event (AI-parsed) |
32
+ | `add -clip` | Add event from clipboard (AI-parsed) |
33
+ | `add` | Add event interactively |
34
+ | `del \| delete <id> [id2...]` | Delete event(s) by ID prefix |
35
+ | `remind <id> <dur> [dur2...]` | Add reminder(s) |
36
+ | `resched <id> <when> [duration]` | Reschedule (preserves duration) |
37
+ | `snooze <id> [when]` | Snooze (default `+1d`) |
38
+ | `import <file.ics>` | Import events from ICS |
39
+ | `calendars` | List available calendars |
40
+ | `assoc` | Set up `.ics` file association (Windows) |
41
+ | `help [command]` | Show help |
42
+
43
+ ### Options
44
+
45
+ | Flag | Description |
46
+ |------|-------------|
47
+ | `-u`, `-user <email>` | Set / use default Google account |
48
+ | `-c`, `-calendar <id>` | Calendar ID (default: primary) |
49
+ | `-n <count>` | Number of events to list |
50
+ | `-v`, `-verbose` | Show event IDs and links |
51
+ | `-b`, `-birthdays` | Include birthday events |
52
+ | `-clip` | Read from clipboard (for `add`) |
53
+ | `-r`, `-reminder <dur>` | Add popup reminder (e.g. `30m`, `1h`); repeatable |
54
+ | `-since <date>` | Start listing from `<date>` |
55
+ | `-till <date>` | End listing at `<date>` |
56
+ | `-all` | Delete all instances of recurring event |
57
+
58
+ ### Examples
59
+
60
+ ```bash
61
+ gcal meeting.ics
62
+ gcal list
63
+ gcal list -since "10 days ago"
64
+ gcal list -since "april 1" -till "may 1"
65
+ gcal add "Dentist" "Friday 3pm" "1h"
66
+ gcal add "Lunch" "1/14/2026 12:00" "1h"
67
+ gcal add "Dentist appointment Friday 3pm for 1 hour"
68
+ gcal add -clip
69
+ gcal add "Dentist" "Friday 3pm" -r 30m
70
+ gcal remind abc12345 30m
71
+ gcal resched abc12345 "next friday 3pm"
72
+ gcal snooze abc12345 +1w
73
+ gcal -u bob@gmail.com
74
+ ```
75
+
76
+ ### Reschedule / snooze notes
77
+
78
+ `resched` and `snooze` find events up to 30 days in the past by default (so stale reminders remain findable). Widen with `-since <date>`. For timed events, if `<when>` lacks a time-of-day (e.g. `tomorrow`), the original time is preserved. All-day events stay all-day. Relative offsets `+1d` / `+1w` / `+1h` / `+1m` advance from the event's current start.
79
+
80
+ ### Windows file association
81
+
82
+ ```bash
83
+ gcal assoc # Sets up .ics gcal
84
+ ```
85
+
86
+ On first run, gcal offers to set this up. Run `gcal assoc` anytime to (re)configure.
87
+
88
+ ## gtask — Google Tasks
89
+
90
+ ```bash
91
+ gtask <command> [options]
92
+ gtask help <command>
93
+ ```
94
+
95
+ ### Commands
96
+
97
+ | Command | Description |
98
+ |---------|-------------|
99
+ | `add <title> [when]` | Add a task (optional due date — date-only) |
100
+ | `list` | List open tasks |
101
+ | `lists` | List all tasklists |
102
+ | `done <id>` | Mark task completed |
103
+ | `undone <id>` | Reopen a completed task |
104
+ | `del <id>` | Delete a task |
105
+ | `edit <id> [-t title] [-when date] [-n notes]` | Update fields |
106
+ | `clear` | Remove all completed tasks from list |
107
+ | `move <id> -l <list>` | Move task to another tasklist |
108
+ | `help [command]` | Show help |
109
+
110
+ ### Options
111
+
112
+ | Flag | Description |
113
+ |------|-------------|
114
+ | `-u`, `-user <email>` | Google account |
115
+ | `-l`, `-list <name\|id>` | Tasklist (default: primary) |
116
+ | `-n`, `-notes <text>` | Notes for `add` / `edit` |
117
+ | `-t`, `-title <text>` | New title for `edit` |
118
+ | `-when <date>` | New due date for `edit` |
119
+ | `-a`, `-all` | Include completed tasks in `list` |
120
+
121
+ ### Examples
122
+
123
+ ```bash
124
+ gtask add "Write report"
125
+ gtask add "Write report" friday
126
+ gtask add "Pay bills" "april 30" -n "rent + utilities"
127
+ gtask add "Call plumber" tomorrow -l Errands
128
+ gtask list
129
+ gtask list -l Errands
130
+ gtask list -a
131
+ gtask done abc12345
132
+ gtask edit abc12345 -when "next monday"
133
+ gtask move abc12345 -l Personal
134
+ gtask clear -l Errands
135
+ ```
136
+
137
+ ### Notes on Google Tasks
138
+
139
+ - **Due dates are date-only.** The Tasks API stores RFC3339 timestamps, but the Google UI ignores time-of-day. `gtask` writes midnight UTC.
140
+ - **No reminders.** Tasks have no notification mechanism in the API. Reminders only appear if you create the task through the Calendar UI's task-with-time feature.
141
+ - **No recurrence.** Recurring tasks aren't exposed via the API.
142
+ - **Hierarchy is flat.** One level of subtasks via `move?parent=`. Not currently surfaced by `gtask`.
143
+
144
+ ## Shared OAuth
145
+
146
+ Both tools request the combined scope set `calendar + tasks` (read or write depending on operation). The first time you run either tool after upgrading, Google prompts once for the new combined consent; afterward both share `token.json` / `token-write.json`.
147
+
148
+ ## License
149
+
150
+ MIT
package/gcal.js CHANGED
@@ -11,65 +11,12 @@ import fs from 'fs';
11
11
  import path from 'path';
12
12
  import { execSync } from 'child_process';
13
13
  import { createInterface } from 'readline/promises';
14
- import { authenticateOAuth } from '@bobfrankston/oauthsupport';
15
- import { CREDENTIALS_FILE, loadConfig, saveConfig, getUserPaths, ensureUserDir, formatDateTime, formatDuration, parseDuration, parseDateTime, hasTimeComponent, parseAllDay, formatYMD, ts, normalizeUser } from './glib/gutils.js';
14
+ import { loadConfig, saveConfig, formatDateTime, formatDuration, parseDuration, parseDateTime, hasTimeComponent, parseAllDay, formatYMD, normalizeUser } from './glib/gutils.js';
15
+ import { setupAbortHandler, getAccessToken, apiFetch } from './glib/goauth.js';
16
16
  import { extractEventsFromText, readClipboard } from './glib/aihelper.js';
17
17
  import pkg from './package.json' with { type: 'json' };
18
18
  const VERSION = pkg.version;
19
19
  const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3';
20
- const CALENDAR_SCOPE_READ = 'https://www.googleapis.com/auth/calendar.readonly';
21
- const CALENDAR_SCOPE_WRITE = 'https://www.googleapis.com/auth/calendar';
22
- let abortController = null;
23
- function setupAbortHandler() {
24
- abortController = new AbortController();
25
- let ctrlCCount = 0;
26
- process.on('SIGINT', () => {
27
- ctrlCCount++;
28
- abortController?.abort();
29
- if (ctrlCCount >= 2) {
30
- console.log('\n\nForce exit.');
31
- process.exit(1);
32
- }
33
- console.log('\n\nCtrl+C pressed - aborting... (press again to force exit)');
34
- });
35
- }
36
- async function getAccessToken(user, writeAccess = false, forceRefresh = false) {
37
- if (!fs.existsSync(CREDENTIALS_FILE)) {
38
- console.error(`\nCredentials file not found: ${CREDENTIALS_FILE}\n`);
39
- console.error(`gcal uses the same credentials as gcards.`);
40
- console.error(`Make sure gcards is set up with OAuth credentials first.`);
41
- console.error(`See: https://github.com/BobFrankston/oauthsupport/blob/master/SETUP-GOOGLE-OAUTH.md`);
42
- process.exit(1);
43
- }
44
- const paths = getUserPaths(user);
45
- ensureUserDir(user);
46
- const scope = writeAccess ? CALENDAR_SCOPE_WRITE : CALENDAR_SCOPE_READ;
47
- const tokenFileName = writeAccess ? 'token-write.json' : 'token.json';
48
- const tokenFilePath = path.join(paths.userDir, tokenFileName);
49
- if (forceRefresh && fs.existsSync(tokenFilePath)) {
50
- fs.unlinkSync(tokenFilePath);
51
- console.log(`${ts()} Token expired, refreshing...`);
52
- }
53
- const token = await authenticateOAuth(CREDENTIALS_FILE, {
54
- scope,
55
- tokenDirectory: paths.userDir,
56
- tokenFileName,
57
- credentialsKey: 'installed',
58
- signal: abortController?.signal
59
- });
60
- if (!token) {
61
- throw new Error('OAuth authentication failed');
62
- }
63
- return token.access_token;
64
- }
65
- async function apiFetch(url, accessToken, options = {}) {
66
- const headers = {
67
- 'Authorization': `Bearer ${accessToken}`,
68
- 'Content-Type': 'application/json',
69
- ...options.headers
70
- };
71
- return fetch(url, { ...options, headers });
72
- }
73
20
  async function listCalendars(accessToken) {
74
21
  const url = `${CALENDAR_API_BASE}/users/me/calendarList`;
75
22
  const res = await apiFetch(url, accessToken);
@@ -197,68 +144,117 @@ async function importIcsFile(filePath, accessToken, calendarId = 'primary') {
197
144
  }
198
145
  return result;
199
146
  }
200
- function showUsage() {
201
- console.log(`
202
- gcal v${VERSION} - Google Calendar CLI
147
+ const USAGE_SUMMARY = `gcal v${VERSION} - Google Calendar CLI
203
148
 
204
- Usage:
205
- gcal <file.ics> Import ICS file (file association)
206
- gcal <command> [options] Run command
149
+ Usage: gcal <file.ics> Import ICS file (file association)
150
+ gcal <command> [options] Run command
151
+ gcal help <command> Detailed help for a command
207
152
 
208
153
  Commands:
209
- list [n] List upcoming n events (default: 10)
210
- list -since <date> List events from <date> forward (past ok)
211
- list -till <date> List events up to <date>
212
- list -since <d1> -till <d2> List events in a date range
213
- add <title> <when> [duration] Add event (explicit args)
214
- add Add event (type description interactively)
215
- add "free text description" Add event (AI parses single text arg)
216
- add -clip Add event from clipboard text (AI-parsed)
217
- del|delete <id> [id2...] Delete event(s) by ID (prefix match)
218
- remind <id> <dur> [dur2...] Add reminder(s) to existing event
219
- resched <id> <when> [duration] Reschedule event (preserve duration by default)
220
- snooze <id> [when] Snooze event (default: +1 day)
221
- import <file.ics> Import events from ICS file
222
- calendars List available calendars
223
- assoc Set up .ics file association (Windows)
224
- help Show this help
154
+ list List upcoming events
155
+ add Add event (explicit, AI, or interactive)
156
+ del | delete Delete event(s) by ID
157
+ remind Add reminder(s) to existing event
158
+ resched Reschedule event
159
+ snooze Snooze event (default: +1d)
160
+ import Import events from ICS file
161
+ calendars List available calendars
162
+ assoc Set up .ics file association (Windows)
163
+ help [command] Show help
164
+
165
+ Global options:
166
+ -u, -user <email> Set / use default Google account
167
+ -c, -calendar <id> Calendar ID (default: primary)
168
+
169
+ Companion tool: gtask (Google Tasks - shares OAuth with gcal)
170
+ `;
171
+ const USAGE = {
172
+ list: `gcal list [n] [-since <date>] [-till <date>] [-c <calendar>] [-b] [-v]
173
+ List upcoming events. Default n=10.
174
+ -since <date> Start from date (past dates allowed)
175
+ -till <date> End at date
176
+ -b Include birthday events (hidden by default)
177
+ -v Verbose (show full IDs and links)
178
+
179
+ Examples:
180
+ gcal list
181
+ gcal list 20
182
+ gcal list -since "10 days ago"
183
+ gcal list -since "april 1" -till "may 1"
184
+ gcal list -since "april 1" -n 50
185
+ `,
186
+ add: `gcal add <title> <when> [duration] Explicit
187
+ gcal add "<free text>" AI-parsed single arg
188
+ gcal add -clip AI-parsed from clipboard
189
+ gcal add Interactive (type description)
190
+ Add a calendar event. Default duration 1h. Use -r <dur> to add reminder(s).
191
+
192
+ Examples:
193
+ gcal add "Dentist" "Friday 3pm" "1h"
194
+ gcal add "Lunch" "1/14/2026 12:00" "1h"
195
+ gcal add "Meeting" "tomorrow 10:00"
196
+ gcal add "Appointment" "jan 15 2pm"
197
+ gcal add "Dentist appointment Friday 3pm for 1 hour"
198
+ gcal add -clip
199
+ gcal add "Dentist" "Friday 3pm" -r 30m
200
+ `,
201
+ del: `gcal del <id> [id2...] [-all] [-b]
202
+ gcal delete <id> [id2...]
203
+ Delete event(s) by ID prefix.
204
+ -all Delete entire recurring series (not just instance)
205
+ -b Allow deletion of birthday events
206
+ `,
207
+ delete: `gcal delete <id> [id2...] [-all]
208
+ Alias for "del".
209
+ `,
210
+ remind: `gcal remind <id> <duration> [duration2...]
211
+ Add popup reminder(s) to an existing event.
225
212
 
226
- Options:
227
- -u, -user <email> Set default Google account
228
- -c, -calendar <id> Calendar ID (default: primary)
229
- -n <count> Number of events to list
230
- -v, -verbose Show event IDs and links
231
- -b, -birthdays Include birthday events (hidden by default)
232
- -clip Read from clipboard (for add command)
233
- -r, -reminder <dur> Add popup reminder (e.g., 30m, 1h); repeatable
234
- -since <date> Start listing from <date> (e.g. "10 days ago", "April 1")
235
- -till <date> End listing at <date>
236
- -all Delete all instances of recurring event
213
+ Examples:
214
+ gcal remind abc12345 30m
215
+ gcal remind abc12345 30m 1h
216
+ `,
217
+ resched: `gcal resched <id> <when> [duration]
218
+ Reschedule an event. Preserves duration unless [duration] given.
219
+ If <when> lacks a time-of-day, the original time is preserved.
220
+ All-day events stay all-day. Searches up to 30 days back by default
221
+ (widen with -since).
237
222
 
238
- Examples:
239
- gcal meeting.ics Import ICS file
240
- gcal list List next 10 events
241
- gcal list -since "10 days ago" List events from 10 days ago forward
242
- gcal list -since "april 1" -till "may 1" Events in April
243
- gcal list -since "april 1" -n 50 List 50 events since April 1
244
- gcal add "Dentist" "Friday 3pm" "1h"
245
- gcal add "Lunch" "1/14/2026 12:00" "1h"
246
- gcal add "Meeting" "tomorrow 10:00"
247
- gcal add "Appointment" "jan 15 2pm"
248
- gcal add "Dentist appointment Friday 3pm for 1 hour"
249
- gcal add -clip Add from clipboard text
250
- gcal add "Dentist" "Friday 3pm" -r 30m Add with 30-min reminder
251
- gcal remind abc12345 30m Add 30-min reminder to event
252
- gcal resched abc12345 "next friday 3pm" Reschedule to new date/time
253
- gcal resched abc12345 tomorrow Move to tomorrow (preserve time-of-day)
254
- gcal snooze abc12345 Snooze event +1 day
255
- gcal snooze abc12345 +1w Snooze event +1 week
256
- gcal add Type event description (one or multiple)
257
- gcal -u bob@gmail.com Set default user
223
+ Examples:
224
+ gcal resched abc12345 "next friday 3pm"
225
+ gcal resched abc12345 tomorrow
226
+ gcal resched abc12345 +1w
227
+ `,
228
+ snooze: `gcal snooze <id> [when]
229
+ Like resched, but defaults to +1d if no <when> given.
258
230
 
259
- File Association (Windows):
260
- gcal assoc Set up automatically
261
- `);
231
+ Examples:
232
+ gcal snooze abc12345
233
+ gcal snooze abc12345 +1w
234
+ `,
235
+ import: `gcal import <file.ics>
236
+ gcal <file.ics> (via file association)
237
+ Import events from an iCalendar file.
238
+ `,
239
+ calendars: `gcal calendars
240
+ List available calendars (id, name, access role).
241
+ `,
242
+ assoc: `gcal assoc
243
+ Set up Windows .ics file association so double-clicking imports to gcal.
244
+ `,
245
+ help: `gcal help [command]
246
+ Show summary, or detailed help for a single command.
247
+ `
248
+ };
249
+ function showUsage(cmd) {
250
+ if (cmd && USAGE[cmd]) {
251
+ console.log(USAGE[cmd]);
252
+ return;
253
+ }
254
+ if (cmd) {
255
+ console.error(`Unknown command: ${cmd}\n`);
256
+ }
257
+ console.log(USAGE_SUMMARY);
262
258
  }
263
259
  function parseArgs(argv) {
264
260
  const result = {
@@ -273,7 +269,8 @@ function parseArgs(argv) {
273
269
  birthdays: false,
274
270
  clip: false,
275
271
  all: false,
276
- reminders: []
272
+ reminders: [],
273
+ helpCmd: ''
277
274
  };
278
275
  const unknown = [];
279
276
  let i = 0;
@@ -346,7 +343,6 @@ function parseArgs(argv) {
346
343
  case '-h':
347
344
  case '-help':
348
345
  case '--help':
349
- case 'help':
350
346
  result.help = true;
351
347
  break;
352
348
  case '-V':
@@ -378,6 +374,10 @@ function parseArgs(argv) {
378
374
  console.error(`Unknown options: ${unknown.join(', ')}`);
379
375
  process.exit(1);
380
376
  }
377
+ if (result.command === 'help') {
378
+ result.help = true;
379
+ result.helpCmd = result.args[0] || '';
380
+ }
381
381
  return result;
382
382
  }
383
383
  function buildReminders(minutes) {
@@ -444,8 +444,8 @@ async function main() {
444
444
  }
445
445
  }
446
446
  if (parsed.help) {
447
- showUsage();
448
- if (process.platform === 'win32' && !checkIcsAssoc()) {
447
+ showUsage(parsed.helpCmd);
448
+ if (!parsed.helpCmd && process.platform === 'win32' && !checkIcsAssoc()) {
449
449
  console.log('Note: .ics file association not set. Run "gcal assoc" to set it up.');
450
450
  }
451
451
  process.exit(0);