zele 0.2.0 → 0.3.5

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 (158) hide show
  1. package/README.md +38 -1
  2. package/bin/zele +27 -0
  3. package/dist/api-utils.d.ts +51 -2
  4. package/dist/api-utils.js +89 -3
  5. package/dist/api-utils.js.map +1 -1
  6. package/dist/auth.d.ts +27 -6
  7. package/dist/auth.js +185 -129
  8. package/dist/auth.js.map +1 -1
  9. package/dist/calendar-client.d.ts +16 -9
  10. package/dist/calendar-client.js +163 -59
  11. package/dist/calendar-client.js.map +1 -1
  12. package/dist/cli.js +28 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/attachment.js +17 -15
  15. package/dist/commands/attachment.js.map +1 -1
  16. package/dist/commands/auth-cmd.js +20 -9
  17. package/dist/commands/auth-cmd.js.map +1 -1
  18. package/dist/commands/calendar.js +67 -78
  19. package/dist/commands/calendar.js.map +1 -1
  20. package/dist/commands/draft.js +25 -18
  21. package/dist/commands/draft.js.map +1 -1
  22. package/dist/commands/label.js +33 -45
  23. package/dist/commands/label.js.map +1 -1
  24. package/dist/commands/mail-actions.js +11 -13
  25. package/dist/commands/mail-actions.js.map +1 -1
  26. package/dist/commands/mail.js +114 -128
  27. package/dist/commands/mail.js.map +1 -1
  28. package/dist/commands/profile.js +18 -21
  29. package/dist/commands/profile.js.map +1 -1
  30. package/dist/commands/watch.d.ts +2 -0
  31. package/dist/commands/watch.js +73 -0
  32. package/dist/commands/watch.js.map +1 -0
  33. package/dist/db.js +12 -13
  34. package/dist/db.js.map +1 -1
  35. package/dist/generated/browser.d.ts +12 -27
  36. package/dist/generated/client.d.ts +13 -28
  37. package/dist/generated/client.js +1 -1
  38. package/dist/generated/commonInputTypes.d.ts +90 -26
  39. package/dist/generated/enums.d.ts +0 -4
  40. package/dist/generated/enums.js +0 -3
  41. package/dist/generated/enums.js.map +1 -1
  42. package/dist/generated/internal/class.d.ts +22 -55
  43. package/dist/generated/internal/class.js +12 -4
  44. package/dist/generated/internal/class.js.map +1 -1
  45. package/dist/generated/internal/prismaNamespace.d.ts +272 -511
  46. package/dist/generated/internal/prismaNamespace.js +54 -66
  47. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  48. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +60 -74
  49. package/dist/generated/internal/prismaNamespaceBrowser.js +50 -62
  50. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  51. package/dist/generated/models/Account.d.ts +1637 -0
  52. package/dist/generated/models/Account.js +2 -0
  53. package/dist/generated/models/Account.js.map +1 -0
  54. package/dist/generated/models/CalendarList.d.ts +1161 -0
  55. package/dist/generated/models/CalendarList.js +2 -0
  56. package/dist/generated/models/CalendarList.js.map +1 -0
  57. package/dist/generated/models/Label.d.ts +1161 -0
  58. package/dist/generated/models/Label.js +2 -0
  59. package/dist/generated/models/Label.js.map +1 -0
  60. package/dist/generated/models/Profile.d.ts +1269 -0
  61. package/dist/generated/models/Profile.js +2 -0
  62. package/dist/generated/models/Profile.js.map +1 -0
  63. package/dist/generated/models/SyncState.d.ts +1130 -0
  64. package/dist/generated/models/SyncState.js +2 -0
  65. package/dist/generated/models/SyncState.js.map +1 -0
  66. package/dist/generated/models/Thread.d.ts +1608 -0
  67. package/dist/generated/models/Thread.js +2 -0
  68. package/dist/generated/models/Thread.js.map +1 -0
  69. package/dist/generated/models.d.ts +6 -9
  70. package/dist/gmail-client.d.ts +119 -94
  71. package/dist/gmail-client.js +862 -315
  72. package/dist/gmail-client.js.map +1 -1
  73. package/dist/mail-tui.d.ts +1 -0
  74. package/dist/mail-tui.js +517 -0
  75. package/dist/mail-tui.js.map +1 -0
  76. package/dist/output.d.ts +6 -4
  77. package/dist/output.js +124 -17
  78. package/dist/output.js.map +1 -1
  79. package/package.json +39 -11
  80. package/schema.prisma +81 -113
  81. package/src/api-utils.ts +103 -5
  82. package/src/auth.ts +224 -143
  83. package/src/calendar-client.ts +196 -89
  84. package/src/cli.ts +32 -1
  85. package/src/commands/attachment.ts +18 -19
  86. package/src/commands/auth-cmd.ts +19 -9
  87. package/src/commands/calendar.ts +42 -85
  88. package/src/commands/draft.ts +19 -22
  89. package/src/commands/label.ts +21 -57
  90. package/src/commands/mail-actions.ts +11 -19
  91. package/src/commands/mail.ts +104 -149
  92. package/src/commands/profile.ts +12 -28
  93. package/src/commands/watch.ts +88 -0
  94. package/src/db.ts +13 -16
  95. package/src/generated/browser.ts +49 -0
  96. package/src/generated/client.ts +71 -0
  97. package/src/generated/commonInputTypes.ts +332 -0
  98. package/src/generated/enums.ts +17 -0
  99. package/src/generated/internal/class.ts +250 -0
  100. package/src/generated/internal/prismaNamespace.ts +1198 -0
  101. package/src/generated/internal/prismaNamespaceBrowser.ts +169 -0
  102. package/src/generated/models/Account.ts +1848 -0
  103. package/src/generated/models/CalendarList.ts +1331 -0
  104. package/src/generated/models/Label.ts +1331 -0
  105. package/src/generated/models/Profile.ts +1439 -0
  106. package/src/generated/models/SyncState.ts +1300 -0
  107. package/src/generated/models/Thread.ts +1787 -0
  108. package/src/generated/models.ts +17 -0
  109. package/src/gmail-client.test.ts +59 -0
  110. package/src/gmail-client.ts +1034 -422
  111. package/src/mail-tui.tsx +1061 -0
  112. package/src/output.test.ts +1093 -0
  113. package/src/output.ts +128 -20
  114. package/src/schema.sql +58 -68
  115. package/src/test-fixtures/email-html/safe-claude-event.html +28 -0
  116. package/src/test-fixtures/email-html/safe-product-announcement.html +25 -0
  117. package/src/test-fixtures/email-html/safe-tracked-links.html +27 -0
  118. package/src/test-fixtures/email-html-snapshots/safe-claude-event.html.md +9 -0
  119. package/src/test-fixtures/email-html-snapshots/safe-product-announcement.html.md +13 -0
  120. package/src/test-fixtures/email-html-snapshots/safe-tracked-links.html.md +7 -0
  121. package/AGENTS.md +0 -26
  122. package/CHANGELOG.md +0 -36
  123. package/dist/generated/models/accounts.d.ts +0 -2000
  124. package/dist/generated/models/accounts.js +0 -2
  125. package/dist/generated/models/accounts.js.map +0 -1
  126. package/dist/generated/models/calendar_events.d.ts +0 -1433
  127. package/dist/generated/models/calendar_events.js +0 -2
  128. package/dist/generated/models/calendar_events.js.map +0 -1
  129. package/dist/generated/models/calendar_lists.d.ts +0 -1131
  130. package/dist/generated/models/calendar_lists.js +0 -2
  131. package/dist/generated/models/calendar_lists.js.map +0 -1
  132. package/dist/generated/models/label_counts.d.ts +0 -1131
  133. package/dist/generated/models/label_counts.js +0 -2
  134. package/dist/generated/models/label_counts.js.map +0 -1
  135. package/dist/generated/models/labels.d.ts +0 -1131
  136. package/dist/generated/models/labels.js +0 -2
  137. package/dist/generated/models/labels.js.map +0 -1
  138. package/dist/generated/models/profiles.d.ts +0 -1131
  139. package/dist/generated/models/profiles.js +0 -2
  140. package/dist/generated/models/profiles.js.map +0 -1
  141. package/dist/generated/models/sync_states.d.ts +0 -1107
  142. package/dist/generated/models/sync_states.js +0 -2
  143. package/dist/generated/models/sync_states.js.map +0 -1
  144. package/dist/generated/models/thread_lists.d.ts +0 -1404
  145. package/dist/generated/models/thread_lists.js +0 -2
  146. package/dist/generated/models/thread_lists.js.map +0 -1
  147. package/dist/generated/models/threads.d.ts +0 -1247
  148. package/dist/generated/models/threads.js +0 -2
  149. package/dist/generated/models/threads.js.map +0 -1
  150. package/dist/gmail-cache.d.ts +0 -60
  151. package/dist/gmail-cache.js +0 -264
  152. package/dist/gmail-cache.js.map +0 -1
  153. package/docs/gogcli-gmail-implementation.md +0 -599
  154. package/scripts/test-device-code-clients.ts +0 -186
  155. package/scripts/test-micropython-scopes.ts +0 -72
  156. package/scripts/test-oauth-clients.ts +0 -257
  157. package/src/gmail-cache.ts +0 -339
  158. package/tsconfig.json +0 -16
@@ -4,14 +4,20 @@
4
4
 
5
5
  import type { Goke } from 'goke'
6
6
  import { login, logout, listAccounts, getAuthStatuses } from '../auth.js'
7
+ import { closePrisma } from '../db.js'
7
8
  import * as out from '../output.js'
9
+ import { handleCommandError } from '../output.js'
8
10
 
9
11
  export function registerAuthCommands(cli: Goke) {
10
12
  cli
11
- .command('login', 'Authenticate with Google (opens browser)')
13
+ .command('login', 'Authenticate with Google (opens browser). Run in background via tmux for remote/headless environments. The command prints an authorization URL — show it to the user, ask them to complete consent in their browser, then paste back the localhost redirect URL containing the auth code.')
12
14
  .action(async () => {
13
- const { email } = await login()
15
+ const result = await login()
16
+ if (result instanceof Error) handleCommandError(result)
17
+ const { email } = result
14
18
  out.success(`Authenticated as ${email}`)
19
+ await closePrisma()
20
+ process.exit(0)
15
21
  })
16
22
 
17
23
  cli
@@ -25,21 +31,23 @@ export function registerAuthCommands(cli: Goke) {
25
31
  return
26
32
  }
27
33
 
34
+ const emails = [...new Set(accounts.map((a) => a.email))]
35
+
28
36
  // If no email specified and multiple accounts: error with list
29
- if (!email && accounts.length > 1) {
37
+ if (!email && emails.length > 1) {
30
38
  out.error('Multiple accounts logged in. Specify which to remove:')
31
- for (const a of accounts) {
32
- process.stderr.write(` ${a}\n`)
39
+ for (const e of emails) {
40
+ console.error(` ${e}`)
33
41
  }
34
42
  process.exit(1)
35
43
  }
36
44
 
37
45
  // If no email and only one account, use that one
38
- const targetEmail = email ?? accounts[0]!
46
+ const targetEmail = email ?? emails[0]!
39
47
 
40
- if (!accounts.includes(targetEmail)) {
48
+ if (!emails.includes(targetEmail)) {
41
49
  out.error(`Account not found: ${targetEmail}`)
42
- out.hint(`Logged in accounts: ${accounts.join(', ')}`)
50
+ out.hint(`Logged in accounts: ${emails.join(', ')}`)
43
51
  process.exit(1)
44
52
  }
45
53
 
@@ -62,7 +70,8 @@ export function registerAuthCommands(cli: Goke) {
62
70
  }
63
71
  }
64
72
 
65
- await logout(targetEmail)
73
+ const logoutResult = await logout(targetEmail)
74
+ if (logoutResult instanceof Error) handleCommandError(logoutResult)
66
75
  out.success(`Credentials removed for ${targetEmail}`)
67
76
  })
68
77
 
@@ -79,6 +88,7 @@ export function registerAuthCommands(cli: Goke) {
79
88
  out.printList(
80
89
  statuses.map((s) => ({
81
90
  email: s.email,
91
+ app_id: s.appId,
82
92
  status: 'Authenticated',
83
93
  expires: s.expiresAt?.toISOString() ?? 'unknown',
84
94
  })),
@@ -1,15 +1,16 @@
1
1
  // Calendar commands: list, events, get, create, update, delete, respond, freebusy.
2
- // Manages Google Calendar with YAML output and cache integration.
2
+ // Manages Google Calendar with YAML output.
3
+ // Cache is handled by the client — commands just call methods and use data.
3
4
  // Multi-account: list/events fetch all accounts concurrently and merge by start time.
4
- // Improved UX over gogcli: multi-account by default, +duration syntax, cleaner output.
5
5
 
6
6
  import type { Goke } from 'goke'
7
7
  import { z } from 'zod'
8
8
  import readline from 'node:readline'
9
9
  import { getCalendarClients, getCalendarClient } from '../auth.js'
10
10
  import type { CalendarClient, CalendarEvent, CalendarListItem, EventListResult } from '../calendar-client.js'
11
- import * as cache from '../gmail-cache.js'
11
+ import { AuthError } from '../api-utils.js'
12
12
  import * as out from '../output.js'
13
+ import { handleCommandError } from '../output.js'
13
14
  import { resolveTimeRange, parseTimeExpression, parseDuration, isDateOnly } from '../calendar-time.js'
14
15
 
15
16
  // ---------------------------------------------------------------------------
@@ -23,41 +24,22 @@ export function registerCalendarCommands(cli: Goke) {
23
24
 
24
25
  cli
25
26
  .command('cal list', 'List calendars')
26
- .option('--no-cache', 'Skip cache')
27
27
  .action(async (options) => {
28
28
  const clients = await getCalendarClients(options.account)
29
29
 
30
- const settled = await Promise.allSettled(
30
+ const results = await Promise.all(
31
31
  clients.map(async ({ email, client }) => {
32
- if (!options.noCache) {
33
- const cached = await cache.getCachedCalendarList<CalendarListItem[]>(email)
34
- if (cached) return { email, calendars: cached }
35
- }
36
-
37
32
  const calendars = await client.listCalendars()
38
-
39
- if (!options.noCache) {
40
- await cache.cacheCalendarList(email, calendars)
41
- }
42
-
33
+ if (calendars instanceof Error) return calendars
43
34
  return { email, calendars }
44
35
  }),
45
36
  )
46
37
 
47
- const allResults = settled
48
- .filter((r): r is PromiseFulfilledResult<{ email: string; calendars: CalendarListItem[] }> => {
49
- if (r.status === 'rejected') {
50
- const msg = String(r.reason)
51
- if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
52
- out.error('CalDAV authentication failed. Try: zele login')
53
- } else {
54
- out.error(`Failed to fetch calendars: ${msg}`)
55
- }
56
- return false
57
- }
38
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
39
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
40
+ if (r instanceof Error) { out.error(`Failed to fetch calendars: ${r.message}`); return false }
58
41
  return true
59
42
  })
60
- .map((r) => r.value)
61
43
 
62
44
  const showAccount = clients.length > 1
63
45
  const merged = allResults.flatMap(({ email, calendars }) =>
@@ -98,7 +80,6 @@ export function registerCalendarCommands(cli: Goke) {
98
80
  .option('--query <query>', 'Free text search')
99
81
  .option('--max [max]', 'Max results (default: 20)')
100
82
  .option('--page <page>', 'Pagination token')
101
- .option('--no-cache', 'Skip cache')
102
83
  .action(async (options) => {
103
84
  const max = options.max ? Number(options.max) : 20
104
85
  const calendarId = options.calendar ?? 'primary'
@@ -115,9 +96,10 @@ export function registerCalendarCommands(cli: Goke) {
115
96
  process.exit(1)
116
97
  }
117
98
 
118
- const settled = await Promise.allSettled(
99
+ const results = await Promise.all(
119
100
  clients.map(async ({ email, client }) => {
120
101
  const tz = await client.getTimezone(calendarId)
102
+ if (tz instanceof Error) return tz
121
103
  const { timeMin, timeMax } = resolveTimeRange({
122
104
  from: options.from,
123
105
  to: options.to,
@@ -127,28 +109,15 @@ export function registerCalendarCommands(cli: Goke) {
127
109
  days: options.days,
128
110
  }, tz)
129
111
 
130
- const cacheParams = {
131
- calendarId: options.all ? '__all__' : calendarId,
132
- timeMin,
133
- timeMax,
134
- query: options.query,
135
- maxResults: max,
136
- pageToken: options.page,
137
- }
138
-
139
- if (!options.noCache) {
140
- const cached = await cache.getCachedCalendarEvents<EventListResult>(email, cacheParams)
141
- if (cached) return { email, result: cached, tz }
142
- }
143
-
144
112
  let result: EventListResult
145
113
 
146
114
  if (options.all) {
147
115
  // Fetch from all calendars
148
116
  const calendars = await client.listCalendars()
117
+ if (calendars instanceof Error) return calendars
149
118
  const allEvents: CalendarEvent[] = []
150
119
 
151
- const perCalResults = await Promise.allSettled(
120
+ const perCalResults = await Promise.all(
152
121
  calendars.map(async (cal) => {
153
122
  const r = await client.listEvents({
154
123
  calendarId: cal.id,
@@ -157,16 +126,14 @@ export function registerCalendarCommands(cli: Goke) {
157
126
  query: options.query,
158
127
  maxResults: max,
159
128
  })
129
+ if (r instanceof Error) return r
160
130
  return r.events.map((e) => ({ ...e, calendarId: cal.id }))
161
131
  }),
162
132
  )
163
133
 
164
134
  for (const r of perCalResults) {
165
- if (r.status === 'fulfilled') {
166
- allEvents.push(...r.value)
167
- } else {
168
- out.error(`Calendar fetch failed: ${r.reason}`)
169
- }
135
+ if (r instanceof Error) { out.error(r instanceof AuthError ? `${r.message}. Try: zele login` : r.message); continue }
136
+ allEvents.push(...r)
170
137
  }
171
138
 
172
139
  result = {
@@ -175,7 +142,7 @@ export function registerCalendarCommands(cli: Goke) {
175
142
  timezone: tz,
176
143
  }
177
144
  } else {
178
- result = await client.listEvents({
145
+ const r = await client.listEvents({
179
146
  calendarId,
180
147
  timeMin,
181
148
  timeMax,
@@ -183,30 +150,19 @@ export function registerCalendarCommands(cli: Goke) {
183
150
  maxResults: max,
184
151
  pageToken: options.page,
185
152
  })
186
- }
187
-
188
- if (!options.noCache) {
189
- await cache.cacheCalendarEvents(email, cacheParams, result)
153
+ if (r instanceof Error) return r
154
+ result = r
190
155
  }
191
156
 
192
157
  return { email, result, tz }
193
158
  }),
194
159
  )
195
160
 
196
- const allResults = settled
197
- .filter((r): r is PromiseFulfilledResult<{ email: string; result: EventListResult; tz: string }> => {
198
- if (r.status === 'rejected') {
199
- const msg = String(r.reason)
200
- if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized')) {
201
- out.error('CalDAV authentication failed. Try: zele login')
202
- } else {
203
- out.error(`Failed to fetch events: ${msg}`)
204
- }
205
- return false
206
- }
161
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
162
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
163
+ if (r instanceof Error) { out.error(`Failed to fetch events: ${r.message}`); return false }
207
164
  return true
208
165
  })
209
- .map((r) => r.value)
210
166
 
211
167
  const showAccount = clients.length > 1
212
168
 
@@ -254,6 +210,7 @@ export function registerCalendarCommands(cli: Goke) {
254
210
  const { client } = await getCalendarClient(options.account)
255
211
 
256
212
  const event = await client.getEvent({ calendarId, eventId })
213
+ if (event instanceof Error) handleCommandError(event)
257
214
  const time = out.formatEventTime(event.start, event.end, event.allDay)
258
215
 
259
216
  const doc: Record<string, unknown> = {
@@ -318,8 +275,9 @@ export function registerCalendarCommands(cli: Goke) {
318
275
  }
319
276
 
320
277
  const calendarId = options.calendar ?? 'primary'
321
- const { email, client } = await getCalendarClient(options.account)
278
+ const { client } = await getCalendarClient(options.account)
322
279
  const tz = await client.getTimezone(calendarId)
280
+ if (tz instanceof Error) handleCommandError(tz)
323
281
 
324
282
  const allDay = options.allDay || (isDateOnly(options.from) && isDateOnly(options.to))
325
283
  const start = allDay ? options.from : parseTimeExpression(options.from, tz)
@@ -346,7 +304,7 @@ export function registerCalendarCommands(cli: Goke) {
346
304
 
347
305
  const reminders = options.reminder ? [parseReminder(options.reminder)] : undefined
348
306
 
349
- const event = await client.createEvent({
307
+ const eventResult = await client.createEvent({
350
308
  calendarId,
351
309
  summary: options.summary,
352
310
  start,
@@ -362,9 +320,8 @@ export function registerCalendarCommands(cli: Goke) {
362
320
  visibility: options.visibility,
363
321
  })
364
322
 
365
- await cache.invalidateCalendarEvents(email)
366
-
367
- printEventDetail(event)
323
+ if (eventResult instanceof Error) handleCommandError(eventResult)
324
+ printEventDetail(eventResult)
368
325
  out.success('Event created')
369
326
  })
370
327
 
@@ -387,7 +344,7 @@ export function registerCalendarCommands(cli: Goke) {
387
344
  .option('--visibility <visibility>', 'Event visibility')
388
345
  .action(async (eventId, options) => {
389
346
  const calendarId = options.calendar ?? 'primary'
390
- const { email, client } = await getCalendarClient(options.account)
347
+ const { client } = await getCalendarClient(options.account)
391
348
 
392
349
  const addAttendees = options.addAttendees
393
350
  ? options.addAttendees.split(',').map((e: string) => e.trim()).filter(Boolean)
@@ -417,6 +374,7 @@ export function registerCalendarCommands(cli: Goke) {
417
374
  }
418
375
  } else {
419
376
  const tz = await client.getTimezone(calendarId)
377
+ if (tz instanceof Error) handleCommandError(tz)
420
378
  if (options.from) start = parseTimeExpression(options.from, tz)
421
379
  if (options.to) {
422
380
  const durationMs = parseDuration(options.to)
@@ -429,7 +387,7 @@ export function registerCalendarCommands(cli: Goke) {
429
387
  }
430
388
  }
431
389
 
432
- const event = await client.updateEvent({
390
+ const updateResult = await client.updateEvent({
433
391
  calendarId,
434
392
  eventId,
435
393
  summary: options.summary,
@@ -445,9 +403,8 @@ export function registerCalendarCommands(cli: Goke) {
445
403
  visibility: options.visibility,
446
404
  })
447
405
 
448
- await cache.invalidateCalendarEvents(email)
449
-
450
- printEventDetail(event)
406
+ if (updateResult instanceof Error) handleCommandError(updateResult)
407
+ printEventDetail(updateResult)
451
408
  out.success('Event updated')
452
409
  })
453
410
 
@@ -461,7 +418,7 @@ export function registerCalendarCommands(cli: Goke) {
461
418
  .option('--force', 'Skip confirmation')
462
419
  .action(async (eventId, options) => {
463
420
  const calendarId = options.calendar ?? 'primary'
464
- const { email, client } = await getCalendarClient(options.account)
421
+ const { client } = await getCalendarClient(options.account)
465
422
 
466
423
  if (!options.force && process.stdin.isTTY) {
467
424
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
@@ -476,8 +433,8 @@ export function registerCalendarCommands(cli: Goke) {
476
433
  }
477
434
  }
478
435
 
479
- await client.deleteEvent({ calendarId, eventId })
480
- await cache.invalidateCalendarEvents(email)
436
+ const deleteResult = await client.deleteEvent({ calendarId, eventId })
437
+ if (deleteResult instanceof Error) handleCommandError(deleteResult)
481
438
 
482
439
  out.printYaml({ deleted: true, id: eventId })
483
440
  out.success('Event deleted')
@@ -505,20 +462,19 @@ export function registerCalendarCommands(cli: Goke) {
505
462
  }
506
463
 
507
464
  const calendarId = options.calendar ?? 'primary'
508
- const { email, client } = await getCalendarClient(options.account)
465
+ const { client } = await getCalendarClient(options.account)
509
466
 
510
- const event = await client.respondToEvent({
467
+ const respondResult = await client.respondToEvent({
511
468
  calendarId,
512
469
  eventId,
513
470
  status: options.status as 'accepted' | 'declined' | 'tentative',
514
471
  comment: options.comment,
515
472
  })
516
-
517
- await cache.invalidateCalendarEvents(email)
473
+ if (respondResult instanceof Error) handleCommandError(respondResult)
518
474
 
519
475
  out.printYaml({
520
- id: event.id,
521
- summary: event.summary,
476
+ id: respondResult.id,
477
+ summary: respondResult.summary,
522
478
  status: options.status,
523
479
  ...(options.comment ? { comment: options.comment } : {}),
524
480
  })
@@ -545,6 +501,7 @@ export function registerCalendarCommands(cli: Goke) {
545
501
 
546
502
  const { client } = await getCalendarClient(options.account)
547
503
  const tz = await client.getTimezone()
504
+ if (tz instanceof Error) handleCommandError(tz)
548
505
 
549
506
  const timeMin = parseTimeExpression(options.from, tz)
550
507
  const timeMax = parseTimeExpression(options.to, tz)
@@ -1,14 +1,16 @@
1
1
  // Draft commands: list, create, get, send, delete.
2
2
  // Manages Gmail drafts with YAML output for list views.
3
+ // Cache invalidation is handled by the client (sendDraft invalidates threadLists).
3
4
  // Multi-account: list fetches all accounts concurrently and merges by date.
4
5
 
5
6
  import type { Goke } from 'goke'
6
7
  import { z } from 'zod'
7
8
  import fs from 'node:fs'
8
9
  import { getClients, getClient } from '../auth.js'
9
- import { GmailClient } from '../gmail-client.js'
10
- import * as cache from '../gmail-cache.js'
10
+ import type { GmailClient } from '../gmail-client.js'
11
+ import { AuthError } from '../api-utils.js'
11
12
  import * as out from '../output.js'
13
+ import { handleCommandError } from '../output.js'
12
14
  import pc from 'picocolors'
13
15
 
14
16
  export function registerDraftCommands(cli: Goke) {
@@ -29,27 +31,24 @@ export function registerDraftCommands(cli: Goke) {
29
31
  process.exit(1)
30
32
  }
31
33
 
32
- // Fetch from all accounts concurrently, tolerating individual failures
33
- const settled = await Promise.allSettled(
34
+ // Fetch from all accounts concurrently
35
+ const results = await Promise.all(
34
36
  clients.map(async ({ email, client }) => {
35
37
  const result = await client.listDrafts({
36
38
  query: options.query,
37
39
  maxResults: options.max,
38
40
  pageToken: options.page,
39
41
  })
42
+ if (result instanceof Error) return result
40
43
  return { email, result }
41
44
  }),
42
45
  )
43
46
 
44
- const allResults = settled
45
- .filter((r): r is PromiseFulfilledResult<{ email: string; result: Awaited<ReturnType<GmailClient['listDrafts']>> & { email: string } }> => {
46
- if (r.status === 'rejected') {
47
- out.error(`Failed to fetch drafts: ${r.reason}`)
48
- return false
49
- }
47
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
48
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
49
+ if (r instanceof Error) { out.error(`Failed to fetch drafts: ${r.message}`); return false }
50
50
  return true
51
51
  })
52
- .map((r) => r.value)
53
52
 
54
53
  // Merge drafts from all accounts, sorted by date descending, capped at max
55
54
  const merged = allResults
@@ -88,20 +87,21 @@ export function registerDraftCommands(cli: Goke) {
88
87
  const { client } = await getClient(options.account)
89
88
 
90
89
  const draft = await client.getDraft({ draftId })
90
+ if (draft instanceof Error) handleCommandError(draft)
91
91
 
92
- process.stdout.write(pc.bold(`Draft: ${draft.message.subject}`) + '\n')
93
- process.stdout.write(pc.dim(`Draft ID: ${draft.id}`) + '\n')
94
- process.stdout.write(`To: ${draft.to.join(', ') || '(none)'}` + '\n')
92
+ console.log(pc.bold(`Draft: ${draft.message.subject}`))
93
+ console.log(pc.dim(`Draft ID: ${draft.id}`))
94
+ console.log(`To: ${draft.to.join(', ') || '(none)'}`)
95
95
  if (draft.cc.length > 0) {
96
- process.stdout.write(`Cc: ${draft.cc.join(', ')}` + '\n')
96
+ console.log(`Cc: ${draft.cc.join(', ')}`)
97
97
  }
98
98
  if (draft.bcc.length > 0) {
99
- process.stdout.write(`Bcc: ${draft.bcc.join(', ')}` + '\n')
99
+ console.log(`Bcc: ${draft.bcc.join(', ')}`)
100
100
  }
101
- process.stdout.write('\n')
101
+ console.log()
102
102
 
103
103
  const body = out.renderEmailBody(draft.message.body, draft.message.mimeType)
104
- process.stdout.write(body + '\n')
104
+ console.log(body)
105
105
  })
106
106
 
107
107
  // =========================================================================
@@ -167,12 +167,9 @@ export function registerDraftCommands(cli: Goke) {
167
167
  cli
168
168
  .command('draft send <draftId>', 'Send a draft')
169
169
  .action(async (draftId, options) => {
170
- const { email, client } = await getClient(options.account)
171
-
170
+ const { client } = await getClient(options.account)
172
171
  const result = await client.sendDraft({ draftId })
173
172
 
174
- await cache.invalidateThreadLists(email)
175
-
176
173
  out.printYaml(result)
177
174
  out.success('Draft sent')
178
175
  })
@@ -1,12 +1,12 @@
1
1
  // Label commands: list, get, create, delete, counts.
2
- // Manages Gmail labels with YAML output and cache integration.
2
+ // Manages Gmail labels with YAML output.
3
+ // Cache is handled by the client — commands just call methods and use data.
3
4
  // Multi-account: list and counts fetch all accounts concurrently and merge.
4
5
 
5
6
  import type { Goke } from 'goke'
6
7
  import { z } from 'zod'
7
8
  import { getClients, getClient } from '../auth.js'
8
- import { GmailClient } from '../gmail-client.js'
9
- import * as cache from '../gmail-cache.js'
9
+ import { AuthError } from '../api-utils.js'
10
10
  import * as out from '../output.js'
11
11
 
12
12
  export function registerLabelCommands(cli: Goke) {
@@ -16,38 +16,23 @@ export function registerLabelCommands(cli: Goke) {
16
16
 
17
17
  cli
18
18
  .command('label list', 'List all labels')
19
- .option('--no-cache', 'Skip cache')
20
19
  .action(async (options) => {
21
20
  const clients = await getClients(options.account)
22
21
 
23
- type LabelList = Awaited<ReturnType<GmailClient['listLabels']>>
24
-
25
- // Fetch from all accounts concurrently, tolerating individual failures
26
- const settled = await Promise.allSettled(
22
+ // Fetch from all accounts concurrently
23
+ const results = await Promise.all(
27
24
  clients.map(async ({ email, client }) => {
28
- if (!options.noCache) {
29
- const cached = await cache.getCachedLabels<LabelList>(email)
30
- if (cached) return { email, labels: cached }
31
- }
32
-
33
- const labels = await client.listLabels()
34
- if (!options.noCache) {
35
- await cache.cacheLabels(email, labels)
36
- }
37
-
38
- return { email, labels }
25
+ const labelsResult = await client.listLabels()
26
+ if (labelsResult instanceof Error) return labelsResult
27
+ return { email, labels: labelsResult.parsed }
39
28
  }),
40
29
  )
41
30
 
42
- const allResults = settled
43
- .filter((r): r is PromiseFulfilledResult<{ email: string; labels: LabelList }> => {
44
- if (r.status === 'rejected') {
45
- out.error(`Failed to fetch labels: ${r.reason}`)
46
- return false
47
- }
31
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
32
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
33
+ if (r instanceof Error) { out.error(`Failed to fetch labels: ${r.message}`); return false }
48
34
  return true
49
35
  })
50
- .map((r) => r.value)
51
36
 
52
37
  // Merge labels from all accounts
53
38
  const merged = allResults.flatMap(({ email, labels }) =>
@@ -109,7 +94,7 @@ export function registerLabelCommands(cli: Goke) {
109
94
  .option('--bg-color <bgColor>', z.string().describe('Background color (hex, e.g. #4986e7)'))
110
95
  .option('--text-color <textColor>', z.string().describe('Text color (hex, e.g. #ffffff)'))
111
96
  .action(async (name, options) => {
112
- const { email, client } = await getClient(options.account)
97
+ const { client } = await getClient(options.account)
113
98
 
114
99
  const result = await client.createLabel({
115
100
  name,
@@ -118,8 +103,6 @@ export function registerLabelCommands(cli: Goke) {
118
103
  : undefined,
119
104
  })
120
105
 
121
- await cache.invalidateLabels(email)
122
-
123
106
  out.printYaml(result)
124
107
  out.success(`Label created: "${result.name}"`)
125
108
  })
@@ -146,13 +129,9 @@ export function registerLabelCommands(cli: Goke) {
146
129
  }
147
130
  }
148
131
 
149
- const { email, client } = await getClient(options.account)
150
-
132
+ const { client } = await getClient(options.account)
151
133
  await client.deleteLabel({ labelId })
152
134
 
153
- await cache.invalidateLabels(email)
154
- await cache.invalidateLabelCounts(email)
155
-
156
135
  out.printYaml({ label_id: labelId, deleted: true })
157
136
  })
158
137
 
@@ -162,38 +141,23 @@ export function registerLabelCommands(cli: Goke) {
162
141
 
163
142
  cli
164
143
  .command('label counts', 'Show unread counts per label')
165
- .option('--no-cache', 'Skip cache')
166
144
  .action(async (options) => {
167
145
  const clients = await getClients(options.account)
168
146
 
169
- type CountList = Awaited<ReturnType<GmailClient['getLabelCounts']>>
170
-
171
- // Fetch from all accounts concurrently, tolerating individual failures
172
- const settled = await Promise.allSettled(
147
+ // Fetch from all accounts concurrently
148
+ const results = await Promise.all(
173
149
  clients.map(async ({ email, client }) => {
174
- if (!options.noCache) {
175
- const cached = await cache.getCachedLabelCounts<CountList>(email)
176
- if (cached) return { email, counts: cached }
177
- }
178
-
179
- const counts = await client.getLabelCounts()
180
- if (!options.noCache) {
181
- await cache.cacheLabelCounts(email, counts)
182
- }
183
-
184
- return { email, counts }
150
+ const countsResult = await client.getLabelCounts()
151
+ if (countsResult instanceof Error) return countsResult
152
+ return { email, counts: countsResult.parsed }
185
153
  }),
186
154
  )
187
155
 
188
- const allResults = settled
189
- .filter((r): r is PromiseFulfilledResult<{ email: string; counts: CountList }> => {
190
- if (r.status === 'rejected') {
191
- out.error(`Failed to fetch counts: ${r.reason}`)
192
- return false
193
- }
156
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
157
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
158
+ if (r instanceof Error) { out.error(`Failed to fetch counts: ${r.message}`); return false }
194
159
  return true
195
160
  })
196
- .map((r) => r.value)
197
161
 
198
162
  // Merge counts from all accounts
199
163
  const merged = allResults.flatMap(({ email, counts }) =>