zele 0.3.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 (157) hide show
  1. package/README.md +1 -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 +26 -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 +112 -126
  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.js +33 -261
  31. package/dist/commands/watch.js.map +1 -1
  32. package/dist/db.js +12 -13
  33. package/dist/db.js.map +1 -1
  34. package/dist/generated/browser.d.ts +12 -27
  35. package/dist/generated/client.d.ts +13 -28
  36. package/dist/generated/client.js +1 -1
  37. package/dist/generated/commonInputTypes.d.ts +90 -26
  38. package/dist/generated/enums.d.ts +0 -4
  39. package/dist/generated/enums.js +0 -3
  40. package/dist/generated/enums.js.map +1 -1
  41. package/dist/generated/internal/class.d.ts +22 -55
  42. package/dist/generated/internal/class.js +12 -4
  43. package/dist/generated/internal/class.js.map +1 -1
  44. package/dist/generated/internal/prismaNamespace.d.ts +272 -511
  45. package/dist/generated/internal/prismaNamespace.js +54 -66
  46. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  47. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +60 -74
  48. package/dist/generated/internal/prismaNamespaceBrowser.js +50 -62
  49. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  50. package/dist/generated/models/Account.d.ts +1637 -0
  51. package/dist/generated/models/Account.js +2 -0
  52. package/dist/generated/models/Account.js.map +1 -0
  53. package/dist/generated/models/CalendarList.d.ts +1161 -0
  54. package/dist/generated/models/CalendarList.js +2 -0
  55. package/dist/generated/models/CalendarList.js.map +1 -0
  56. package/dist/generated/models/Label.d.ts +1161 -0
  57. package/dist/generated/models/Label.js +2 -0
  58. package/dist/generated/models/Label.js.map +1 -0
  59. package/dist/generated/models/Profile.d.ts +1269 -0
  60. package/dist/generated/models/Profile.js +2 -0
  61. package/dist/generated/models/Profile.js.map +1 -0
  62. package/dist/generated/models/SyncState.d.ts +1130 -0
  63. package/dist/generated/models/SyncState.js +2 -0
  64. package/dist/generated/models/SyncState.js.map +1 -0
  65. package/dist/generated/models/Thread.d.ts +1608 -0
  66. package/dist/generated/models/Thread.js +2 -0
  67. package/dist/generated/models/Thread.js.map +1 -0
  68. package/dist/generated/models.d.ts +6 -9
  69. package/dist/gmail-client.d.ts +119 -94
  70. package/dist/gmail-client.js +862 -322
  71. package/dist/gmail-client.js.map +1 -1
  72. package/dist/mail-tui.d.ts +1 -0
  73. package/dist/mail-tui.js +517 -0
  74. package/dist/mail-tui.js.map +1 -0
  75. package/dist/output.d.ts +6 -0
  76. package/dist/output.js +124 -11
  77. package/dist/output.js.map +1 -1
  78. package/package.json +39 -11
  79. package/schema.prisma +81 -113
  80. package/src/api-utils.ts +103 -5
  81. package/src/auth.ts +224 -143
  82. package/src/calendar-client.ts +196 -89
  83. package/src/cli.ts +30 -1
  84. package/src/commands/attachment.ts +18 -19
  85. package/src/commands/auth-cmd.ts +19 -9
  86. package/src/commands/calendar.ts +42 -85
  87. package/src/commands/draft.ts +19 -22
  88. package/src/commands/label.ts +21 -57
  89. package/src/commands/mail-actions.ts +11 -19
  90. package/src/commands/mail.ts +102 -147
  91. package/src/commands/profile.ts +12 -28
  92. package/src/commands/watch.ts +37 -304
  93. package/src/db.ts +13 -16
  94. package/src/generated/browser.ts +49 -0
  95. package/src/generated/client.ts +71 -0
  96. package/src/generated/commonInputTypes.ts +332 -0
  97. package/src/generated/enums.ts +17 -0
  98. package/src/generated/internal/class.ts +250 -0
  99. package/src/generated/internal/prismaNamespace.ts +1198 -0
  100. package/src/generated/internal/prismaNamespaceBrowser.ts +169 -0
  101. package/src/generated/models/Account.ts +1848 -0
  102. package/src/generated/models/CalendarList.ts +1331 -0
  103. package/src/generated/models/Label.ts +1331 -0
  104. package/src/generated/models/Profile.ts +1439 -0
  105. package/src/generated/models/SyncState.ts +1300 -0
  106. package/src/generated/models/Thread.ts +1787 -0
  107. package/src/generated/models.ts +17 -0
  108. package/src/gmail-client.test.ts +59 -0
  109. package/src/gmail-client.ts +1034 -429
  110. package/src/mail-tui.tsx +1061 -0
  111. package/src/output.test.ts +1093 -0
  112. package/src/output.ts +128 -13
  113. package/src/schema.sql +58 -68
  114. package/src/test-fixtures/email-html/safe-claude-event.html +28 -0
  115. package/src/test-fixtures/email-html/safe-product-announcement.html +25 -0
  116. package/src/test-fixtures/email-html/safe-tracked-links.html +27 -0
  117. package/src/test-fixtures/email-html-snapshots/safe-claude-event.html.md +9 -0
  118. package/src/test-fixtures/email-html-snapshots/safe-product-announcement.html.md +13 -0
  119. package/src/test-fixtures/email-html-snapshots/safe-tracked-links.html.md +7 -0
  120. package/AGENTS.md +0 -26
  121. package/CHANGELOG.md +0 -43
  122. package/dist/generated/models/accounts.d.ts +0 -2000
  123. package/dist/generated/models/accounts.js +0 -2
  124. package/dist/generated/models/accounts.js.map +0 -1
  125. package/dist/generated/models/calendar_events.d.ts +0 -1433
  126. package/dist/generated/models/calendar_events.js +0 -2
  127. package/dist/generated/models/calendar_events.js.map +0 -1
  128. package/dist/generated/models/calendar_lists.d.ts +0 -1131
  129. package/dist/generated/models/calendar_lists.js +0 -2
  130. package/dist/generated/models/calendar_lists.js.map +0 -1
  131. package/dist/generated/models/label_counts.d.ts +0 -1131
  132. package/dist/generated/models/label_counts.js +0 -2
  133. package/dist/generated/models/label_counts.js.map +0 -1
  134. package/dist/generated/models/labels.d.ts +0 -1131
  135. package/dist/generated/models/labels.js +0 -2
  136. package/dist/generated/models/labels.js.map +0 -1
  137. package/dist/generated/models/profiles.d.ts +0 -1131
  138. package/dist/generated/models/profiles.js +0 -2
  139. package/dist/generated/models/profiles.js.map +0 -1
  140. package/dist/generated/models/sync_states.d.ts +0 -1107
  141. package/dist/generated/models/sync_states.js +0 -2
  142. package/dist/generated/models/sync_states.js.map +0 -1
  143. package/dist/generated/models/thread_lists.d.ts +0 -1404
  144. package/dist/generated/models/thread_lists.js +0 -2
  145. package/dist/generated/models/thread_lists.js.map +0 -1
  146. package/dist/generated/models/threads.d.ts +0 -1247
  147. package/dist/generated/models/threads.js +0 -2
  148. package/dist/generated/models/threads.js.map +0 -1
  149. package/dist/gmail-cache.d.ts +0 -60
  150. package/dist/gmail-cache.js +0 -264
  151. package/dist/gmail-cache.js.map +0 -1
  152. package/docs/gogcli-gmail-implementation.md +0 -599
  153. package/scripts/test-device-code-clients.ts +0 -186
  154. package/scripts/test-micropython-scopes.ts +0 -72
  155. package/scripts/test-oauth-clients.ts +0 -257
  156. package/src/gmail-cache.ts +0 -339
  157. package/tsconfig.json +0 -16
@@ -1,36 +1,31 @@
1
1
  // Mail action commands: star, unstar, archive, trash, untrash, mark read/unread, label modify.
2
- // Bulk operations on threads — all invalidate relevant caches after mutation.
2
+ // Bulk operations on threads — cache invalidation is handled by the client methods.
3
3
 
4
4
  import type { Goke } from 'goke'
5
5
  import { z } from 'zod'
6
6
  import { getClient } from '../auth.js'
7
- import { GmailClient } from '../gmail-client.js'
8
- import * as cache from '../gmail-cache.js'
7
+ import type { GmailClient } from '../gmail-client.js'
9
8
  import * as out from '../output.js'
9
+ import { handleCommandError } from '../output.js'
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
- // Helper: run a bulk action with cache invalidation
12
+ // Helper: run a bulk action
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
15
  async function bulkAction(
16
16
  threadIds: string[],
17
17
  actionName: string,
18
- account: string[] | undefined,
19
- fn: (client: GmailClient, ids: string[]) => Promise<void>,
18
+ accountFilter: string[] | undefined,
19
+ fn: (client: GmailClient, ids: string[]) => Promise<void | Error>,
20
20
  ) {
21
21
  if (threadIds.length === 0) {
22
22
  out.error('No thread IDs provided')
23
23
  process.exit(1)
24
24
  }
25
25
 
26
- const { email, client } = await getClient(account)
27
-
28
- await fn(client, threadIds)
29
-
30
- // Invalidate caches
31
- await cache.invalidateThreads(email, threadIds)
32
- await cache.invalidateThreadLists(email)
33
- await cache.invalidateLabelCounts(email)
26
+ const { client } = await getClient(accountFilter)
27
+ const result = await fn(client, threadIds)
28
+ if (result instanceof Error) handleCommandError(result)
34
29
 
35
30
  out.printYaml({ action: actionName, thread_ids: threadIds, success: true })
36
31
  }
@@ -106,12 +101,9 @@ export function registerMailActionCommands(cli: Goke) {
106
101
  cli
107
102
  .command('mail trash-spam', 'Trash all spam threads')
108
103
  .action(async (options) => {
109
- const { email, client } = await getClient(options.account)
110
-
104
+ const { client } = await getClient(options.account)
111
105
  const result = await client.trashAllSpam()
112
-
113
- await cache.invalidateThreadLists(email)
114
- await cache.invalidateLabelCounts(email)
106
+ if (result instanceof Error) handleCommandError(result)
115
107
 
116
108
  out.printYaml(result)
117
109
  out.success(`Trashed ${result.count} spam thread(s)`)
@@ -1,15 +1,17 @@
1
1
  // Mail commands: list, search, read, send, reply, forward.
2
- // Core email operations wrapping GmailClient with cache-first reads
3
- // and YAML output for list views.
2
+ // Core email operations wrapping GmailClient with YAML output for list views.
3
+ // Cache is handled by the client — commands just call methods and use data.
4
4
  // Multi-account: list/search fetch all accounts concurrently and merge by date.
5
5
 
6
6
  import type { Goke } from 'goke'
7
7
  import { z } from 'zod'
8
8
  import fs from 'node:fs'
9
+ import React from 'react'
9
10
  import { getClients, getClient } from '../auth.js'
10
- import { GmailClient, type ThreadData, type ThreadListResult } from '../gmail-client.js'
11
- import * as cache from '../gmail-cache.js'
11
+ import type { ThreadListResult } from '../gmail-client.js'
12
+ import { AuthError } from '../api-utils.js'
12
13
  import * as out from '../output.js'
14
+ import { handleCommandError } from '../output.js'
13
15
  import pc from 'picocolors'
14
16
 
15
17
  // ---------------------------------------------------------------------------
@@ -17,6 +19,18 @@ import pc from 'picocolors'
17
19
  // ---------------------------------------------------------------------------
18
20
 
19
21
  export function registerMailCommands(cli: Goke) {
22
+ // =========================================================================
23
+ // mail (TUI)
24
+ // =========================================================================
25
+
26
+ cli
27
+ .command('mail', 'Browse emails in TUI')
28
+ .action(async () => {
29
+ const { renderWithProviders } = await import('termcast')
30
+ const { default: Command } = await import('../mail-tui.js')
31
+ await renderWithProviders(React.createElement(Command))
32
+ })
33
+
20
34
  // =========================================================================
21
35
  // mail list
22
36
  // =========================================================================
@@ -27,7 +41,6 @@ export function registerMailCommands(cli: Goke) {
27
41
  .option('--max [max]', 'Max results per page (default: 20)')
28
42
  .option('--page <page>', 'Pagination token')
29
43
  .option('--label <label>', 'Filter by label name')
30
- .option('--no-cache', 'Skip cache')
31
44
  .action(async (options) => {
32
45
  const folder = options.folder ?? 'inbox'
33
46
  const max = options.max ? Number(options.max) : 20
@@ -38,47 +51,25 @@ export function registerMailCommands(cli: Goke) {
38
51
  process.exit(1)
39
52
  }
40
53
 
41
- const cacheParams = {
42
- folder,
43
- maxResults: max,
44
- labelIds: options.label ? [options.label] : undefined,
45
- pageToken: options.page,
46
- }
47
-
48
- // Fetch from all accounts concurrently, tolerating individual failures
49
- const settled = await Promise.allSettled(
54
+ // Fetch from all accounts concurrently
55
+ const results = await Promise.all(
50
56
  clients.map(async ({ email, client }) => {
51
- if (!options.noCache) {
52
- const cached = await cache.getCachedThreadList<ThreadListResult>(email, cacheParams)
53
- if (cached) {
54
- return { email, result: cached }
55
- }
56
- }
57
-
58
57
  const result = await client.listThreads({
59
58
  folder,
60
59
  maxResults: max,
61
60
  labelIds: options.label ? [options.label] : undefined,
62
61
  pageToken: options.page,
63
62
  })
64
-
65
- if (!options.noCache) {
66
- await cache.cacheThreadList(email, cacheParams, result)
67
- }
68
-
63
+ if (result instanceof Error) return result
69
64
  return { email, result }
70
65
  }),
71
66
  )
72
67
 
73
- const allResults = settled
74
- .filter((r): r is PromiseFulfilledResult<{ email: string; result: ThreadListResult }> => {
75
- if (r.status === 'rejected') {
76
- out.error(`Failed to fetch: ${r.reason}`)
77
- return false
78
- }
68
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
69
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
70
+ if (r instanceof Error) { out.error(`Failed to fetch: ${r.message}`); return false }
79
71
  return true
80
72
  })
81
- .map((r) => r.value)
82
73
 
83
74
  // Merge threads from all accounts, sorted by date descending, capped at max
84
75
  const merged = allResults
@@ -97,6 +88,7 @@ export function registerMailCommands(cli: Goke) {
97
88
  out.printList(
98
89
  merged.map((t) => ({
99
90
  ...(showAccount ? { account: t.account } : {}),
91
+ id: t.id,
100
92
  flags: out.formatFlags(t),
101
93
  from: out.formatSender(t.from),
102
94
  subject: t.subject,
@@ -125,27 +117,24 @@ export function registerMailCommands(cli: Goke) {
125
117
  process.exit(1)
126
118
  }
127
119
 
128
- // Search all accounts concurrently, tolerating individual failures
129
- const settled = await Promise.allSettled(
120
+ // Search all accounts concurrently
121
+ const results = await Promise.all(
130
122
  clients.map(async ({ email, client }) => {
131
123
  const result = await client.listThreads({
132
124
  query,
133
125
  maxResults: max,
134
126
  pageToken: options.page,
135
127
  })
128
+ if (result instanceof Error) return result
136
129
  return { email, result }
137
130
  }),
138
131
  )
139
132
 
140
- const allResults = settled
141
- .filter((r): r is PromiseFulfilledResult<{ email: string; result: ThreadListResult }> => {
142
- if (r.status === 'rejected') {
143
- out.error(`Failed to search: ${r.reason}`)
144
- return false
145
- }
133
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
134
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
135
+ if (r instanceof Error) { out.error(`Failed to search: ${r.message}`); return false }
146
136
  return true
147
137
  })
148
- .map((r) => r.value)
149
138
 
150
139
  const merged = allResults
151
140
  .flatMap(({ email, result }) =>
@@ -163,6 +152,7 @@ export function registerMailCommands(cli: Goke) {
163
152
  out.printList(
164
153
  merged.map((t) => ({
165
154
  ...(showAccount ? { account: t.account } : {}),
155
+ id: t.id,
166
156
  flags: out.formatFlags(t),
167
157
  from: out.formatSender(t.from),
168
158
  subject: t.subject,
@@ -181,65 +171,93 @@ export function registerMailCommands(cli: Goke) {
181
171
  cli
182
172
  .command('mail read <threadId>', 'Read a full email thread')
183
173
  .option('--raw', 'Show raw message (first message only)')
184
- .option('--no-cache', 'Skip cache')
174
+ .option('--raw-html', 'Show raw HTML body per message (no markdown conversion)')
185
175
  .action(async (threadId, options) => {
186
- const { email, client } = await getClient(options.account)
176
+ const { client } = await getClient(options.account)
177
+
178
+ if (options.raw && options.rawHtml) {
179
+ out.error('--raw and --raw-html cannot be used together')
180
+ process.exit(1)
181
+ }
187
182
 
188
183
  if (options.raw) {
189
- const thread = await client.getThread({ threadId })
184
+ const { parsed: thread } = await client.getThread({ threadId })
190
185
  if (thread.messages.length === 0) {
191
186
  out.hint('No messages in thread')
192
187
  return
193
188
  }
194
189
  const rawMsg = await client.getRawMessage({ messageId: thread.messages[0]!.id })
195
- process.stdout.write(rawMsg + '\n')
190
+ if (rawMsg instanceof Error) handleCommandError(rawMsg)
191
+ console.log(rawMsg)
196
192
  return
197
193
  }
198
194
 
199
- // Cache-first read
200
- let thread: ThreadData | undefined
201
- if (!options.noCache) {
202
- thread = await cache.getCachedThread<ThreadData>(email, threadId)
203
- }
204
- if (!thread) {
205
- thread = await client.getThread({ threadId })
206
- if (!options.noCache) {
207
- await cache.cacheThread(email, threadId, thread)
208
- }
209
- }
195
+ const { parsed: thread } = await client.getThread({ threadId })
210
196
 
211
197
  if (thread.messages.length === 0) {
212
198
  out.hint('No messages in thread')
213
199
  return
214
200
  }
215
201
 
202
+ if (options.rawHtml) {
203
+ thread.messages.forEach((msg, index) => {
204
+ console.log(msg.body)
205
+ if (index < thread.messages.length - 1) {
206
+ console.log('\n<!-- ZELE_MESSAGE_SEPARATOR -->\n')
207
+ }
208
+ })
209
+ return
210
+ }
211
+
212
+ const w = Math.min(process.stdout.columns || 72, 72)
213
+ const rule = pc.dim('─'.repeat(w))
214
+
216
215
  // Render thread header
217
- process.stdout.write(pc.bold(thread.subject) + '\n')
218
- process.stdout.write(pc.dim(`Thread ID: ${thread.id} | ${thread.messageCount} message(s)`) + '\n')
219
- process.stdout.write(pc.dim('─'.repeat(60)) + '\n\n')
216
+ console.log(pc.bold(thread.subject))
217
+ // Collect unique participants
218
+ const participants = new Map<string, string>()
219
+ for (const msg of thread.messages) {
220
+ participants.set(msg.from.email, msg.from.name || msg.from.email)
221
+ for (const r of msg.to) participants.set(r.email, r.name || r.email)
222
+ }
223
+ const participantStr = [...participants.values()].join(', ')
224
+ console.log(pc.dim(`${thread.messageCount} message(s) · ${participantStr}`))
225
+ console.log(pc.dim(`ID: ${thread.id}`))
226
+ console.log(rule + '\n')
220
227
 
221
228
  // Render each message
222
229
  for (const msg of thread.messages) {
223
230
  const fromStr = out.formatSender(msg.from)
224
231
  const dateStr = out.formatDate(msg.date)
225
- const flags = out.formatFlags(msg)
226
232
 
227
- process.stdout.write(pc.bold(fromStr) + (flags ? ` ${flags}` : '') + '\n')
228
- process.stdout.write(pc.dim(`To: ${msg.to.map((t) => t.email).join(', ')}`) + '\n')
233
+ // Flags as dim tags
234
+ const flagParts: string[] = []
235
+ if (msg.unread) flagParts.push(pc.yellow('[unread]'))
236
+ if (msg.starred) flagParts.push(pc.yellow('[starred]'))
237
+ const flagStr = flagParts.length > 0 ? ' ' + flagParts.join(' ') : ''
238
+
239
+ console.log(pc.bold(`From: `) + fromStr + flagStr)
240
+ console.log(pc.dim(` To: ${msg.to.map((t) => t.email).join(', ')}`))
229
241
  if (msg.cc && msg.cc.length > 0) {
230
- process.stdout.write(pc.dim(`Cc: ${msg.cc.map((c) => c.email).join(', ')}`) + '\n')
242
+ console.log(pc.dim(` Cc: ${msg.cc.map((c) => c.email).join(', ')}`))
231
243
  }
232
- process.stdout.write(pc.dim(`Date: ${dateStr} | ID: ${msg.id}`) + '\n')
244
+ console.log(pc.dim(`Date: ${dateStr}`))
233
245
 
234
246
  if (msg.attachments.length > 0) {
235
- process.stdout.write(pc.dim(`Attachments: ${msg.attachments.map((a) => a.filename).join(', ')}`) + '\n')
247
+ const attList = msg.attachments.map((a) => {
248
+ const size = a.size < 1024 ? `${a.size} B`
249
+ : a.size < 1048576 ? `${(a.size / 1024).toFixed(1)} KB`
250
+ : `${(a.size / 1048576).toFixed(1)} MB`
251
+ return `${a.filename} (${size})`
252
+ })
253
+ console.log(pc.dim(`Attachments: ${attList.join(', ')}`))
236
254
  }
237
255
 
238
- process.stdout.write('\n')
256
+ console.log()
239
257
 
240
258
  const body = out.renderEmailBody(msg.body, msg.mimeType)
241
- process.stdout.write(body + '\n')
242
- process.stdout.write('\n' + pc.dim('─'.repeat(60)) + '\n\n')
259
+ console.log(body)
260
+ console.log('\n' + rule + '\n')
243
261
  }
244
262
  })
245
263
 
@@ -287,7 +305,7 @@ export function registerMailCommands(cli: Goke) {
287
305
  const parseEmails = (str: string) =>
288
306
  str.split(',').map((e) => e.trim()).filter(Boolean).map((email) => ({ email }))
289
307
 
290
- const { email, client } = await getClient(options.account)
308
+ const { client } = await getClient(options.account)
291
309
 
292
310
  const result = await client.sendMessage({
293
311
  to: parseEmails(options.to),
@@ -298,8 +316,6 @@ export function registerMailCommands(cli: Goke) {
298
316
  fromEmail: options.from,
299
317
  })
300
318
 
301
- await cache.invalidateThreadLists(email)
302
-
303
319
  out.printYaml(result)
304
320
  out.success(`Sent to ${options.to}`)
305
321
  })
@@ -316,16 +332,6 @@ export function registerMailCommands(cli: Goke) {
316
332
  .option('--all', 'Reply all (include all original recipients)')
317
333
  .option('--from <from>', z.string().describe('Send-as alias email'))
318
334
  .action(async (threadId, options) => {
319
- const { email, client } = await getClient(options.account)
320
-
321
- const thread = await client.getThread({ threadId })
322
- if (thread.messages.length === 0) {
323
- out.error('No messages in thread')
324
- process.exit(1)
325
- }
326
-
327
- const lastMsg = thread.messages[thread.messages.length - 1]!
328
-
329
335
  let body = options.body ?? ''
330
336
  if (options.bodyFile) {
331
337
  if (options.bodyFile === '-') {
@@ -344,49 +350,20 @@ export function registerMailCommands(cli: Goke) {
344
350
  process.exit(1)
345
351
  }
346
352
 
347
- const replyTo = lastMsg.replyTo ?? lastMsg.from.email
348
- const to = [{ email: replyTo }]
349
-
350
- let cc: Array<{ email: string }> | undefined
351
- if (options.all) {
352
- const profile = await client.getProfile()
353
- const myEmail = profile.emailAddress.toLowerCase()
354
-
355
- const allRecipients = [
356
- ...lastMsg.to.map((r) => r.email),
357
- ...(lastMsg.cc?.map((r) => r.email) ?? []),
358
- ]
359
- .filter((e) => e.toLowerCase() !== myEmail)
360
- .filter((e) => e.toLowerCase() !== replyTo.toLowerCase())
361
-
362
- if (allRecipients.length > 0) {
363
- cc = allRecipients.map((e) => ({ email: e }))
364
- }
365
- }
366
-
367
- if (options.cc) {
368
- const extra = options.cc
369
- .split(',')
370
- .map((e: string) => ({ email: e.trim() }))
371
- .filter((e: { email: string }) => e.email)
372
- cc = [...(cc ?? []), ...extra]
373
- }
353
+ const { client } = await getClient(options.account)
374
354
 
375
- const refs = [lastMsg.references, lastMsg.messageId].filter(Boolean).join(' ')
355
+ const cc = options.cc
356
+ ? options.cc.split(',').map((e: string) => ({ email: e.trim() })).filter((e: { email: string }) => e.email)
357
+ : undefined
376
358
 
377
- const result = await client.sendMessage({
378
- to,
379
- subject: lastMsg.subject.startsWith('Re:') ? lastMsg.subject : `Re: ${lastMsg.subject}`,
359
+ const result = await client.replyToThread({
360
+ threadId,
380
361
  body,
362
+ replyAll: options.all,
381
363
  cc,
382
- threadId,
383
- inReplyTo: lastMsg.messageId,
384
- references: refs || undefined,
385
364
  fromEmail: options.from,
386
365
  })
387
-
388
- await cache.invalidateThread(email, threadId)
389
- await cache.invalidateThreadLists(email)
366
+ if (result instanceof Error) handleCommandError(result)
390
367
 
391
368
  out.printYaml(result)
392
369
  out.success('Reply sent')
@@ -407,42 +384,20 @@ export function registerMailCommands(cli: Goke) {
407
384
  process.exit(1)
408
385
  }
409
386
 
410
- const { email, client } = await getClient(options.account)
411
-
412
- const thread = await client.getThread({ threadId })
413
- if (thread.messages.length === 0) {
414
- out.error('No messages in thread')
415
- process.exit(1)
416
- }
417
-
418
- const lastMsg = thread.messages[thread.messages.length - 1]!
419
- const forwardedBody = out.renderEmailBody(lastMsg.body, lastMsg.mimeType)
420
-
421
- const fullBody = [
422
- options.body ?? '',
423
- '',
424
- '---------- Forwarded message ----------',
425
- `From: ${out.formatSender(lastMsg.from)}`,
426
- `Date: ${lastMsg.date}`,
427
- `Subject: ${lastMsg.subject}`,
428
- `To: ${lastMsg.to.map((t) => t.email).join(', ')}`,
429
- '',
430
- forwardedBody,
431
- ].join('\n')
432
-
433
387
  const recipients = options.to
434
388
  .split(',')
435
389
  .map((e: string) => ({ email: e.trim() }))
436
390
  .filter((e: { email: string }) => e.email)
437
391
 
438
- const result = await client.sendMessage({
392
+ const { client } = await getClient(options.account)
393
+
394
+ const result = await client.forwardThread({
395
+ threadId,
439
396
  to: recipients,
440
- subject: `Fwd: ${lastMsg.subject}`,
441
- body: fullBody,
397
+ body: options.body,
442
398
  fromEmail: options.from,
443
399
  })
444
-
445
- await cache.invalidateThreadLists(email)
400
+ if (result instanceof Error) handleCommandError(result)
446
401
 
447
402
  out.printYaml(result)
448
403
  out.success(`Forwarded to ${options.to}`)
@@ -1,52 +1,36 @@
1
1
  // Profile command: show account info.
2
2
  // Displays email address, message/thread counts, and aliases as YAML.
3
+ // Cache is handled by the client — commands just call methods and use data.
3
4
  // Multi-account: shows all accounts or filtered by --account.
4
5
 
5
6
  import type { Goke } from 'goke'
6
7
  import { getClients } from '../auth.js'
7
- import { GmailClient } from '../gmail-client.js'
8
- import * as cache from '../gmail-cache.js'
8
+ import { AuthError } from '../api-utils.js'
9
9
  import * as out from '../output.js'
10
10
 
11
11
  export function registerProfileCommands(cli: Goke) {
12
12
  cli
13
13
  .command('profile', 'Show Gmail account info')
14
- .option('--no-cache', 'Skip cache')
15
14
  .action(async (options) => {
16
15
  const clients = await getClients(options.account)
17
16
 
18
- type Profile = Awaited<ReturnType<GmailClient['getProfile']>>
19
-
20
- // Fetch all accounts concurrently, tolerating individual failures
21
- const settled = await Promise.allSettled(
22
- clients.map(async ({ email, client }) => {
23
- let profile: Profile | undefined
24
- if (!options.noCache) {
25
- profile = await cache.getCachedProfile<Profile>(email)
26
- }
27
- if (!profile) {
28
- profile = await client.getProfile()
29
- if (!options.noCache) {
30
- await cache.cacheProfile(email, profile)
31
- }
32
- }
33
-
17
+ // Fetch all accounts concurrently
18
+ const allResults = await Promise.all(
19
+ clients.map(async ({ client }) => {
20
+ const profile = await client.getProfile()
21
+ if (profile instanceof Error) return profile
34
22
  // Always fetch aliases fresh
35
23
  const aliases = await client.getEmailAliases()
36
-
37
- return { email, profile, aliases }
24
+ if (aliases instanceof Error) return aliases
25
+ return { profile, aliases }
38
26
  }),
39
27
  )
40
28
 
41
- const results = settled
42
- .filter((r): r is PromiseFulfilledResult<{ email: string; profile: Profile; aliases: Awaited<ReturnType<GmailClient['getEmailAliases']>> }> => {
43
- if (r.status === 'rejected') {
44
- out.error(`Failed to fetch profile: ${r.reason}`)
45
- return false
46
- }
29
+ const results = allResults.filter((r): r is Exclude<typeof r, Error> => {
30
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
31
+ if (r instanceof Error) { out.error(`Failed to fetch profile: ${r.message}`); return false }
47
32
  return true
48
33
  })
49
- .map((r) => r.value)
50
34
 
51
35
  for (const { profile, aliases } of results) {
52
36
  out.printYaml({