zele 0.3.0 → 0.3.6

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 +34 -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 +119 -127
  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 +39 -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 +109 -148
  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 { getClients, getClient } from '../auth.js'
10
- import { GmailClient, type ThreadData, type ThreadListResult } from '../gmail-client.js'
11
- import * as cache from '../gmail-cache.js'
9
+ import React from 'react'
10
+ import { getClients, getClient, listAccounts, login } from '../auth.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,24 @@ 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 accounts = await listAccounts()
30
+ if (accounts.length === 0) {
31
+ const result = await login()
32
+ if (result instanceof Error) handleCommandError(result)
33
+ }
34
+
35
+ const { renderWithProviders } = await import('termcast')
36
+ const { default: Command } = await import('../mail-tui.js')
37
+ await renderWithProviders(React.createElement(Command))
38
+ })
39
+
20
40
  // =========================================================================
21
41
  // mail list
22
42
  // =========================================================================
@@ -27,7 +47,6 @@ export function registerMailCommands(cli: Goke) {
27
47
  .option('--max [max]', 'Max results per page (default: 20)')
28
48
  .option('--page <page>', 'Pagination token')
29
49
  .option('--label <label>', 'Filter by label name')
30
- .option('--no-cache', 'Skip cache')
31
50
  .action(async (options) => {
32
51
  const folder = options.folder ?? 'inbox'
33
52
  const max = options.max ? Number(options.max) : 20
@@ -38,47 +57,25 @@ export function registerMailCommands(cli: Goke) {
38
57
  process.exit(1)
39
58
  }
40
59
 
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(
60
+ // Fetch from all accounts concurrently
61
+ const results = await Promise.all(
50
62
  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
63
  const result = await client.listThreads({
59
64
  folder,
60
65
  maxResults: max,
61
66
  labelIds: options.label ? [options.label] : undefined,
62
67
  pageToken: options.page,
63
68
  })
64
-
65
- if (!options.noCache) {
66
- await cache.cacheThreadList(email, cacheParams, result)
67
- }
68
-
69
+ if (result instanceof Error) return result
69
70
  return { email, result }
70
71
  }),
71
72
  )
72
73
 
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
- }
74
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
75
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
76
+ if (r instanceof Error) { out.error(`Failed to fetch: ${r.message}`); return false }
79
77
  return true
80
78
  })
81
- .map((r) => r.value)
82
79
 
83
80
  // Merge threads from all accounts, sorted by date descending, capped at max
84
81
  const merged = allResults
@@ -97,6 +94,7 @@ export function registerMailCommands(cli: Goke) {
97
94
  out.printList(
98
95
  merged.map((t) => ({
99
96
  ...(showAccount ? { account: t.account } : {}),
97
+ id: t.id,
100
98
  flags: out.formatFlags(t),
101
99
  from: out.formatSender(t.from),
102
100
  subject: t.subject,
@@ -125,27 +123,24 @@ export function registerMailCommands(cli: Goke) {
125
123
  process.exit(1)
126
124
  }
127
125
 
128
- // Search all accounts concurrently, tolerating individual failures
129
- const settled = await Promise.allSettled(
126
+ // Search all accounts concurrently
127
+ const results = await Promise.all(
130
128
  clients.map(async ({ email, client }) => {
131
129
  const result = await client.listThreads({
132
130
  query,
133
131
  maxResults: max,
134
132
  pageToken: options.page,
135
133
  })
134
+ if (result instanceof Error) return result
136
135
  return { email, result }
137
136
  }),
138
137
  )
139
138
 
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
- }
139
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
140
+ if (r instanceof AuthError) { out.error(`${r.message}. Try: zele login`); return false }
141
+ if (r instanceof Error) { out.error(`Failed to search: ${r.message}`); return false }
146
142
  return true
147
143
  })
148
- .map((r) => r.value)
149
144
 
150
145
  const merged = allResults
151
146
  .flatMap(({ email, result }) =>
@@ -163,6 +158,7 @@ export function registerMailCommands(cli: Goke) {
163
158
  out.printList(
164
159
  merged.map((t) => ({
165
160
  ...(showAccount ? { account: t.account } : {}),
161
+ id: t.id,
166
162
  flags: out.formatFlags(t),
167
163
  from: out.formatSender(t.from),
168
164
  subject: t.subject,
@@ -181,65 +177,93 @@ export function registerMailCommands(cli: Goke) {
181
177
  cli
182
178
  .command('mail read <threadId>', 'Read a full email thread')
183
179
  .option('--raw', 'Show raw message (first message only)')
184
- .option('--no-cache', 'Skip cache')
180
+ .option('--raw-html', 'Show raw HTML body per message (no markdown conversion)')
185
181
  .action(async (threadId, options) => {
186
- const { email, client } = await getClient(options.account)
182
+ const { client } = await getClient(options.account)
183
+
184
+ if (options.raw && options.rawHtml) {
185
+ out.error('--raw and --raw-html cannot be used together')
186
+ process.exit(1)
187
+ }
187
188
 
188
189
  if (options.raw) {
189
- const thread = await client.getThread({ threadId })
190
+ const { parsed: thread } = await client.getThread({ threadId })
190
191
  if (thread.messages.length === 0) {
191
192
  out.hint('No messages in thread')
192
193
  return
193
194
  }
194
195
  const rawMsg = await client.getRawMessage({ messageId: thread.messages[0]!.id })
195
- process.stdout.write(rawMsg + '\n')
196
+ if (rawMsg instanceof Error) handleCommandError(rawMsg)
197
+ console.log(rawMsg)
196
198
  return
197
199
  }
198
200
 
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
- }
201
+ const { parsed: thread } = await client.getThread({ threadId })
210
202
 
211
203
  if (thread.messages.length === 0) {
212
204
  out.hint('No messages in thread')
213
205
  return
214
206
  }
215
207
 
208
+ if (options.rawHtml) {
209
+ thread.messages.forEach((msg, index) => {
210
+ console.log(msg.body)
211
+ if (index < thread.messages.length - 1) {
212
+ console.log('\n<!-- ZELE_MESSAGE_SEPARATOR -->\n')
213
+ }
214
+ })
215
+ return
216
+ }
217
+
218
+ const w = Math.min(process.stdout.columns || 72, 72)
219
+ const rule = pc.dim('─'.repeat(w))
220
+
216
221
  // 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')
222
+ console.log(pc.bold(thread.subject))
223
+ // Collect unique participants
224
+ const participants = new Map<string, string>()
225
+ for (const msg of thread.messages) {
226
+ participants.set(msg.from.email, msg.from.name || msg.from.email)
227
+ for (const r of msg.to) participants.set(r.email, r.name || r.email)
228
+ }
229
+ const participantStr = [...participants.values()].join(', ')
230
+ console.log(pc.dim(`${thread.messageCount} message(s) · ${participantStr}`))
231
+ console.log(pc.dim(`ID: ${thread.id}`))
232
+ console.log(rule + '\n')
220
233
 
221
234
  // Render each message
222
235
  for (const msg of thread.messages) {
223
236
  const fromStr = out.formatSender(msg.from)
224
237
  const dateStr = out.formatDate(msg.date)
225
- const flags = out.formatFlags(msg)
226
238
 
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')
239
+ // Flags as dim tags
240
+ const flagParts: string[] = []
241
+ if (msg.unread) flagParts.push(pc.yellow('[unread]'))
242
+ if (msg.starred) flagParts.push(pc.yellow('[starred]'))
243
+ const flagStr = flagParts.length > 0 ? ' ' + flagParts.join(' ') : ''
244
+
245
+ console.log(pc.bold(`From: `) + fromStr + flagStr)
246
+ console.log(pc.dim(` To: ${msg.to.map((t) => t.email).join(', ')}`))
229
247
  if (msg.cc && msg.cc.length > 0) {
230
- process.stdout.write(pc.dim(`Cc: ${msg.cc.map((c) => c.email).join(', ')}`) + '\n')
248
+ console.log(pc.dim(` Cc: ${msg.cc.map((c) => c.email).join(', ')}`))
231
249
  }
232
- process.stdout.write(pc.dim(`Date: ${dateStr} | ID: ${msg.id}`) + '\n')
250
+ console.log(pc.dim(`Date: ${dateStr}`))
233
251
 
234
252
  if (msg.attachments.length > 0) {
235
- process.stdout.write(pc.dim(`Attachments: ${msg.attachments.map((a) => a.filename).join(', ')}`) + '\n')
253
+ const attList = msg.attachments.map((a) => {
254
+ const size = a.size < 1024 ? `${a.size} B`
255
+ : a.size < 1048576 ? `${(a.size / 1024).toFixed(1)} KB`
256
+ : `${(a.size / 1048576).toFixed(1)} MB`
257
+ return `${a.filename} (${size})`
258
+ })
259
+ console.log(pc.dim(`Attachments: ${attList.join(', ')}`))
236
260
  }
237
261
 
238
- process.stdout.write('\n')
262
+ console.log()
239
263
 
240
264
  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')
265
+ console.log(body)
266
+ console.log('\n' + rule + '\n')
243
267
  }
244
268
  })
245
269
 
@@ -287,7 +311,7 @@ export function registerMailCommands(cli: Goke) {
287
311
  const parseEmails = (str: string) =>
288
312
  str.split(',').map((e) => e.trim()).filter(Boolean).map((email) => ({ email }))
289
313
 
290
- const { email, client } = await getClient(options.account)
314
+ const { client } = await getClient(options.account)
291
315
 
292
316
  const result = await client.sendMessage({
293
317
  to: parseEmails(options.to),
@@ -298,8 +322,6 @@ export function registerMailCommands(cli: Goke) {
298
322
  fromEmail: options.from,
299
323
  })
300
324
 
301
- await cache.invalidateThreadLists(email)
302
-
303
325
  out.printYaml(result)
304
326
  out.success(`Sent to ${options.to}`)
305
327
  })
@@ -316,16 +338,6 @@ export function registerMailCommands(cli: Goke) {
316
338
  .option('--all', 'Reply all (include all original recipients)')
317
339
  .option('--from <from>', z.string().describe('Send-as alias email'))
318
340
  .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
341
  let body = options.body ?? ''
330
342
  if (options.bodyFile) {
331
343
  if (options.bodyFile === '-') {
@@ -344,49 +356,20 @@ export function registerMailCommands(cli: Goke) {
344
356
  process.exit(1)
345
357
  }
346
358
 
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
- }
359
+ const { client } = await getClient(options.account)
374
360
 
375
- const refs = [lastMsg.references, lastMsg.messageId].filter(Boolean).join(' ')
361
+ const cc = options.cc
362
+ ? options.cc.split(',').map((e: string) => ({ email: e.trim() })).filter((e: { email: string }) => e.email)
363
+ : undefined
376
364
 
377
- const result = await client.sendMessage({
378
- to,
379
- subject: lastMsg.subject.startsWith('Re:') ? lastMsg.subject : `Re: ${lastMsg.subject}`,
365
+ const result = await client.replyToThread({
366
+ threadId,
380
367
  body,
368
+ replyAll: options.all,
381
369
  cc,
382
- threadId,
383
- inReplyTo: lastMsg.messageId,
384
- references: refs || undefined,
385
370
  fromEmail: options.from,
386
371
  })
387
-
388
- await cache.invalidateThread(email, threadId)
389
- await cache.invalidateThreadLists(email)
372
+ if (result instanceof Error) handleCommandError(result)
390
373
 
391
374
  out.printYaml(result)
392
375
  out.success('Reply sent')
@@ -407,42 +390,20 @@ export function registerMailCommands(cli: Goke) {
407
390
  process.exit(1)
408
391
  }
409
392
 
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
393
  const recipients = options.to
434
394
  .split(',')
435
395
  .map((e: string) => ({ email: e.trim() }))
436
396
  .filter((e: { email: string }) => e.email)
437
397
 
438
- const result = await client.sendMessage({
398
+ const { client } = await getClient(options.account)
399
+
400
+ const result = await client.forwardThread({
401
+ threadId,
439
402
  to: recipients,
440
- subject: `Fwd: ${lastMsg.subject}`,
441
- body: fullBody,
403
+ body: options.body,
442
404
  fromEmail: options.from,
443
405
  })
444
-
445
- await cache.invalidateThreadLists(email)
406
+ if (result instanceof Error) handleCommandError(result)
446
407
 
447
408
  out.printYaml(result)
448
409
  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({