zele 0.3.16 → 0.3.20

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 (90) hide show
  1. package/README.md +155 -36
  2. package/dist/api-utils.d.ts +14 -0
  3. package/dist/api-utils.js +20 -0
  4. package/dist/api-utils.js.map +1 -1
  5. package/dist/auth.d.ts +71 -9
  6. package/dist/auth.js +186 -10
  7. package/dist/auth.js.map +1 -1
  8. package/dist/cli-types.d.ts +4 -0
  9. package/dist/cli-types.js +6 -0
  10. package/dist/cli-types.js.map +1 -0
  11. package/dist/cli.js +1 -5
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/attachment.d.ts +2 -2
  14. package/dist/commands/attachment.js +2 -0
  15. package/dist/commands/attachment.js.map +1 -1
  16. package/dist/commands/auth-cmd.d.ts +2 -2
  17. package/dist/commands/auth-cmd.js +104 -6
  18. package/dist/commands/auth-cmd.js.map +1 -1
  19. package/dist/commands/calendar.d.ts +2 -2
  20. package/dist/commands/calendar.js.map +1 -1
  21. package/dist/commands/draft.d.ts +2 -2
  22. package/dist/commands/draft.js +58 -4
  23. package/dist/commands/draft.js.map +1 -1
  24. package/dist/commands/filter.d.ts +2 -2
  25. package/dist/commands/filter.js +7 -2
  26. package/dist/commands/filter.js.map +1 -1
  27. package/dist/commands/label.d.ts +2 -2
  28. package/dist/commands/label.js +19 -9
  29. package/dist/commands/label.js.map +1 -1
  30. package/dist/commands/mail-actions.d.ts +2 -2
  31. package/dist/commands/mail-actions.js +290 -1
  32. package/dist/commands/mail-actions.js.map +1 -1
  33. package/dist/commands/mail.d.ts +2 -2
  34. package/dist/commands/mail.js +90 -23
  35. package/dist/commands/mail.js.map +1 -1
  36. package/dist/commands/profile.d.ts +2 -2
  37. package/dist/commands/profile.js +25 -18
  38. package/dist/commands/profile.js.map +1 -1
  39. package/dist/commands/watch.d.ts +2 -2
  40. package/dist/commands/watch.js.map +1 -1
  41. package/dist/db.js +24 -0
  42. package/dist/db.js.map +1 -1
  43. package/dist/generated/internal/class.js +2 -2
  44. package/dist/generated/internal/class.js.map +1 -1
  45. package/dist/generated/internal/prismaNamespace.d.ts +2 -0
  46. package/dist/generated/internal/prismaNamespace.js +2 -0
  47. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  48. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +2 -0
  49. package/dist/generated/internal/prismaNamespaceBrowser.js +2 -0
  50. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  51. package/dist/generated/models/Account.d.ts +97 -1
  52. package/dist/gmail-client.d.ts +73 -3
  53. package/dist/gmail-client.js +165 -5
  54. package/dist/gmail-client.js.map +1 -1
  55. package/dist/imap-smtp-client.d.ts +306 -0
  56. package/dist/imap-smtp-client.js +1349 -0
  57. package/dist/imap-smtp-client.js.map +1 -0
  58. package/dist/mail-tui.js.map +1 -1
  59. package/dist/unsubscribe.d.ts +76 -0
  60. package/dist/unsubscribe.js +224 -0
  61. package/dist/unsubscribe.js.map +1 -0
  62. package/package.json +6 -3
  63. package/schema.prisma +7 -5
  64. package/skills/zele/SKILL.md +26 -96
  65. package/src/api-utils.ts +20 -0
  66. package/src/auth.ts +282 -14
  67. package/src/cli-types.ts +8 -0
  68. package/src/cli.ts +2 -7
  69. package/src/commands/attachment.ts +3 -2
  70. package/src/commands/auth-cmd.ts +114 -8
  71. package/src/commands/calendar.ts +2 -2
  72. package/src/commands/draft.ts +65 -6
  73. package/src/commands/filter.ts +11 -5
  74. package/src/commands/label.ts +24 -13
  75. package/src/commands/mail-actions.ts +317 -5
  76. package/src/commands/mail.ts +97 -25
  77. package/src/commands/profile.ts +29 -19
  78. package/src/commands/watch.ts +2 -2
  79. package/src/db.ts +28 -0
  80. package/src/generated/internal/class.ts +2 -2
  81. package/src/generated/internal/prismaNamespace.ts +2 -0
  82. package/src/generated/internal/prismaNamespaceBrowser.ts +2 -0
  83. package/src/generated/models/Account.ts +97 -1
  84. package/src/gmail-client.test.ts +155 -2
  85. package/src/gmail-client.ts +258 -6
  86. package/src/imap-smtp-client.ts +1560 -0
  87. package/src/mail-tui.tsx +2 -1
  88. package/src/schema.sql +2 -0
  89. package/src/unsubscribe.test.ts +487 -0
  90. package/src/unsubscribe.ts +255 -0
@@ -2,7 +2,7 @@
2
2
  // Lists attachments for a thread and downloads them to disk.
3
3
  // Skips re-download if file already exists with same size (like gogcli).
4
4
 
5
- import type { Goke } from 'goke'
5
+ import type { ZeleCli } from '../cli-types.js'
6
6
  import { z } from 'zod'
7
7
  import fs from 'node:fs'
8
8
  import path from 'node:path'
@@ -10,7 +10,7 @@ import { getClient } from '../auth.js'
10
10
  import * as out from '../output.js'
11
11
  import { handleCommandError } from '../output.js'
12
12
 
13
- export function registerAttachmentCommands(cli: Goke) {
13
+ export function registerAttachmentCommands(cli: ZeleCli) {
14
14
  // =========================================================================
15
15
  // attachment list
16
16
  // =========================================================================
@@ -86,6 +86,7 @@ export function registerAttachmentCommands(cli: Goke) {
86
86
 
87
87
  // Download
88
88
  const base64Data = await client.getAttachment({ messageId, attachmentId })
89
+ if (base64Data instanceof Error) return handleCommandError(base64Data)
89
90
  const buffer = Buffer.from(base64Data, 'base64')
90
91
 
91
92
  // Ensure output directory exists
@@ -1,17 +1,72 @@
1
- // Auth commands: login, logout, whoami.
2
- // Manages OAuth2 authentication for zele.
1
+ // Auth commands: login, login imap, logout, whoami.
2
+ // Manages authentication for zele (Google OAuth and IMAP/SMTP credentials).
3
3
  // Supports multiple accounts: login adds accounts, logout removes one.
4
4
 
5
- import type { Goke } from 'goke'
6
- import { login, logout, listAccounts, getAuthStatuses } from '../auth.js'
5
+ import type { ZeleCli } from '../cli-types.js'
6
+ import { z } from 'zod'
7
+ import pc from 'picocolors'
8
+ import { login, loginImap, logout, listAccounts, getAuthStatuses } from '../auth.js'
7
9
  import { closePrisma } from '../db.js'
8
10
  import * as out from '../output.js'
9
11
  import { handleCommandError } from '../output.js'
10
12
 
11
- export function registerAuthCommands(cli: Goke) {
13
+ export function registerAuthCommands(cli: ZeleCli) {
12
14
  cli
13
- .command('login', 'Authenticate with Google (opens browser). For headless/agent environments, run inside tmux: the command prints an authorization URL to open in a browser, then waits for the localhost redirect URL to be pasted back.')
15
+ .command('login', 'Authenticate with Google (opens browser) or show IMAP/SMTP login instructions')
14
16
  .action(async () => {
17
+ // In a TTY, ask if they want Google or Other
18
+ if (process.stdin.isTTY) {
19
+ const readline = await import('node:readline')
20
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
21
+
22
+ console.error(pc.bold('\nChoose authentication method:\n'))
23
+ console.error(' ' + pc.cyan('1') + ' Google (opens browser for OAuth)')
24
+ console.error(' ' + pc.cyan('2') + ' Other (IMAP/SMTP with password)\n')
25
+
26
+ const answer = await new Promise<string>((resolve) => {
27
+ rl.question('Enter choice [1]: ', resolve)
28
+ })
29
+ rl.close()
30
+
31
+ const choice = answer.trim() || '1'
32
+
33
+ if (choice === '2') {
34
+ console.error(pc.bold('\nTo add an IMAP/SMTP account, run:\n'))
35
+ console.error(pc.dim(' # Fastmail'))
36
+ console.error(` zele login imap \\`)
37
+ console.error(` --email you@fastmail.com \\`)
38
+ console.error(` --imap-host imap.fastmail.com --imap-port 993 \\`)
39
+ console.error(` --smtp-host smtp.fastmail.com --smtp-port 465 \\`)
40
+ console.error(` --password "your-app-password"`)
41
+ console.error()
42
+ console.error(pc.dim(' # Gmail (app password)'))
43
+ console.error(` zele login imap \\`)
44
+ console.error(` --email you@gmail.com \\`)
45
+ console.error(` --imap-host imap.gmail.com --imap-port 993 \\`)
46
+ console.error(` --smtp-host smtp.gmail.com --smtp-port 465 \\`)
47
+ console.error(` --password "your-app-password"`)
48
+ console.error()
49
+ console.error(pc.dim(' # Outlook/Hotmail'))
50
+ console.error(` zele login imap \\`)
51
+ console.error(` --email you@outlook.com \\`)
52
+ console.error(` --imap-host outlook.office365.com --imap-port 993 \\`)
53
+ console.error(` --smtp-host smtp-mail.outlook.com --smtp-port 587 \\`)
54
+ console.error(` --password "your-password"`)
55
+ console.error()
56
+ console.error(pc.dim(' # Generic (any IMAP/SMTP provider)'))
57
+ console.error(` zele login imap \\`)
58
+ console.error(` --email you@example.com \\`)
59
+ console.error(` --imap-host imap.example.com --imap-port 993 \\`)
60
+ console.error(` --smtp-host smtp.example.com --smtp-port 465 \\`)
61
+ console.error(` --password "your-password"`)
62
+ console.error()
63
+ console.error(pc.dim('Omit --smtp-host for read-only (IMAP only, no sending).'))
64
+ console.error(pc.dim('Use --imap-user/--smtp-user if the login username differs from your email.'))
65
+ return
66
+ }
67
+ }
68
+
69
+ // Default: Google OAuth flow
15
70
  const result = await login()
16
71
  if (result instanceof Error) handleCommandError(result)
17
72
  const { email } = result
@@ -20,6 +75,56 @@ export function registerAuthCommands(cli: Goke) {
20
75
  process.exit(0)
21
76
  })
22
77
 
78
+ cli
79
+ .command('login imap', 'Add an IMAP/SMTP email account (non-interactive, designed for agents)')
80
+ .option('--email <email>', z.string().describe('Email address'))
81
+ .option('--imap-host <imapHost>', z.string().describe('IMAP server hostname'))
82
+ .option('--imap-port <imapPort>', z.string().describe('IMAP server port (default: 993)'))
83
+ .option('--smtp-host <smtpHost>', z.string().describe('SMTP server hostname (optional, enables sending)'))
84
+ .option('--smtp-port <smtpPort>', z.string().describe('SMTP server port (default: 465)'))
85
+ .option('--password <password>', z.string().describe('Password (shared for IMAP and SMTP unless overridden)'))
86
+ .option('--imap-user <imapUser>', z.string().describe('IMAP username (defaults to --email)'))
87
+ .option('--imap-password <imapPassword>', z.string().describe('IMAP password (overrides --password)'))
88
+ .option('--smtp-user <smtpUser>', z.string().describe('SMTP username (defaults to --email)'))
89
+ .option('--smtp-password <smtpPassword>', z.string().describe('SMTP password (overrides --password)'))
90
+ .option('--no-tls', 'Disable TLS (not recommended)')
91
+ .action(async (options) => {
92
+ if (!options.email) {
93
+ out.error('--email is required')
94
+ process.exit(1)
95
+ }
96
+ if (!options.imapHost) {
97
+ out.error('--imap-host is required')
98
+ process.exit(1)
99
+ }
100
+ if (!options.password && !options.imapPassword) {
101
+ out.error('--password or --imap-password is required')
102
+ process.exit(1)
103
+ }
104
+
105
+ out.hint('Testing IMAP connection...')
106
+
107
+ const result = await loginImap({
108
+ email: options.email,
109
+ imapHost: options.imapHost,
110
+ imapPort: options.imapPort ? Number(options.imapPort) : undefined,
111
+ smtpHost: options.smtpHost,
112
+ smtpPort: options.smtpPort ? Number(options.smtpPort) : undefined,
113
+ password: options.password,
114
+ imapUser: options.imapUser,
115
+ imapPassword: options.imapPassword,
116
+ smtpUser: options.smtpUser,
117
+ smtpPassword: options.smtpPassword,
118
+ tls: options.noTls !== true,
119
+ })
120
+ if (result instanceof Error) handleCommandError(result)
121
+
122
+ const caps = options.smtpHost ? 'IMAP + SMTP' : 'IMAP only'
123
+ out.success(`Authenticated ${result.email} (${caps})`)
124
+ await closePrisma()
125
+ process.exit(0)
126
+ })
127
+
23
128
  cli
24
129
  .command('logout [email]', 'Remove stored credentials for an account')
25
130
  .option('--force', 'Skip confirmation')
@@ -88,9 +193,10 @@ export function registerAuthCommands(cli: Goke) {
88
193
  out.printList(
89
194
  statuses.map((s) => ({
90
195
  email: s.email,
91
- app_id: s.appId,
196
+ type: s.accountType,
197
+ capabilities: s.capabilities.join(', '),
92
198
  status: 'Authenticated',
93
- expires: s.expiresAt?.toISOString() ?? 'unknown',
199
+ ...(s.expiresAt ? { expires: s.expiresAt.toISOString() } : {}),
94
200
  })),
95
201
  { summary: `${statuses.length} account(s)` },
96
202
  )
@@ -3,7 +3,7 @@
3
3
  // Cache is handled by the client — commands just call methods and use data.
4
4
  // Multi-account: list/events fetch all accounts concurrently and merge by start time.
5
5
 
6
- import type { Goke } from 'goke'
6
+ import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
8
  import readline from 'node:readline'
9
9
  import { getCalendarClients, getCalendarClient } from '../auth.js'
@@ -17,7 +17,7 @@ import { resolveTimeRange, parseTimeExpression, parseDuration, isDateOnly } from
17
17
  // Register commands
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
- export function registerCalendarCommands(cli: Goke) {
20
+ export function registerCalendarCommands(cli: ZeleCli) {
21
21
  // =========================================================================
22
22
  // cal list
23
23
  // =========================================================================
@@ -3,17 +3,18 @@
3
3
  // Cache invalidation is handled by the client (sendDraft invalidates threadLists).
4
4
  // Multi-account: list fetches all accounts concurrently and merges by date.
5
5
 
6
- import type { Goke } from 'goke'
6
+ import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
8
  import fs from 'node:fs'
9
9
  import { getClients, getClient } from '../auth.js'
10
10
  import type { GmailClient } from '../gmail-client.js'
11
+ import type { ImapSmtpClient } from '../imap-smtp-client.js'
11
12
  import { AuthError } from '../api-utils.js'
12
13
  import * as out from '../output.js'
13
14
  import { handleCommandError } from '../output.js'
14
15
  import pc from 'picocolors'
15
16
 
16
- export function registerDraftCommands(cli: Goke) {
17
+ export function registerDraftCommands(cli: ZeleCli) {
17
18
  // =========================================================================
18
19
  // draft list
19
20
  // =========================================================================
@@ -88,14 +89,17 @@ export function registerDraftCommands(cli: Goke) {
88
89
  const draft = await client.getDraft({ draftId })
89
90
  if (draft instanceof Error) handleCommandError(draft)
90
91
 
92
+ const fmtRecipients = (list: Array<{ name?: string; email: string }>) =>
93
+ list.map((r) => r.name && r.name !== r.email ? `${r.name} <${r.email}>` : r.email).join(', ')
94
+
91
95
  console.log(pc.bold(`Draft: ${draft.message.subject}`))
92
96
  console.log(pc.dim(`Draft ID: ${draft.id}`))
93
- console.log(`To: ${draft.to.join(', ') || '(none)'}`)
97
+ console.log(`To: ${fmtRecipients(draft.to) || '(none)'}`)
94
98
  if (draft.cc.length > 0) {
95
- console.log(`Cc: ${draft.cc.join(', ')}`)
99
+ console.log(`Cc: ${fmtRecipients(draft.cc)}`)
96
100
  }
97
101
  if (draft.bcc.length > 0) {
98
- console.log(`Bcc: ${draft.bcc.join(', ')}`)
102
+ console.log(`Bcc: ${fmtRecipients(draft.bcc)}`)
99
103
  }
100
104
  console.log()
101
105
 
@@ -154,11 +158,64 @@ export function registerDraftCommands(cli: Goke) {
154
158
  threadId: options.thread,
155
159
  fromEmail: options.from,
156
160
  })
161
+ if (result instanceof Error) handleCommandError(result)
157
162
 
158
163
  out.printYaml(result)
159
164
  out.success('Draft created')
160
165
  })
161
166
 
167
+ // =========================================================================
168
+ // draft update
169
+ // =========================================================================
170
+
171
+ cli
172
+ .command('draft update <draftId>', 'Update an existing draft')
173
+ .option('--to <to>', z.string().describe('New recipient email(s), comma-separated'))
174
+ .option('--subject <subject>', z.string().describe('New subject'))
175
+ .option('--body <body>', z.string().describe('New body text'))
176
+ .option('--body-file <bodyFile>', z.string().describe('Read new body from file (use - for stdin)'))
177
+ .option('--cc <cc>', z.string().describe('New CC recipients (comma-separated)'))
178
+ .option('--bcc <bcc>', z.string().describe('New BCC recipients (comma-separated)'))
179
+ .option('--from <from>', z.string().describe('Send-as alias email'))
180
+ .action(async (draftId, options) => {
181
+ const { client } = await getClient(options.account)
182
+
183
+ // Fetch existing draft to merge unchanged fields
184
+ const existing = await client.getDraft({ draftId })
185
+ if (existing instanceof Error) handleCommandError(existing)
186
+
187
+ let body = options.body
188
+ if (options.bodyFile) {
189
+ if (options.bodyFile === '-') {
190
+ const chunks: Buffer[] = []
191
+ for await (const chunk of process.stdin) {
192
+ chunks.push(chunk)
193
+ }
194
+ body = Buffer.concat(chunks).toString('utf-8')
195
+ } else {
196
+ body = fs.readFileSync(options.bodyFile, 'utf-8')
197
+ }
198
+ }
199
+
200
+ const parseEmails = (str: string) =>
201
+ str.split(',').map((e) => e.trim()).filter(Boolean).map((email) => ({ email }))
202
+
203
+ const result = await client.updateDraft({
204
+ draftId,
205
+ to: options.to ? parseEmails(options.to) : existing.to,
206
+ subject: options.subject ?? existing.message.subject,
207
+ body: body ?? existing.message.body,
208
+ cc: options.cc ? parseEmails(options.cc) : existing.cc,
209
+ bcc: options.bcc ? parseEmails(options.bcc) : existing.bcc,
210
+ threadId: existing.message.threadId || undefined,
211
+ fromEmail: options.from ?? existing.message.from.email,
212
+ })
213
+ if (result instanceof Error) handleCommandError(result)
214
+
215
+ out.printYaml(result)
216
+ out.success('Draft updated')
217
+ })
218
+
162
219
  // =========================================================================
163
220
  // draft send
164
221
  // =========================================================================
@@ -168,6 +225,7 @@ export function registerDraftCommands(cli: Goke) {
168
225
  .action(async (draftId, options) => {
169
226
  const { client } = await getClient(options.account)
170
227
  const result = await client.sendDraft({ draftId })
228
+ if (result instanceof Error) handleCommandError(result)
171
229
 
172
230
  out.printYaml(result)
173
231
  out.success('Draft sent')
@@ -197,7 +255,8 @@ export function registerDraftCommands(cli: Goke) {
197
255
 
198
256
  const { client } = await getClient(options.account)
199
257
 
200
- await client.deleteDraft({ draftId })
258
+ const deleteResult = await client.deleteDraft({ draftId })
259
+ if (deleteResult instanceof Error) handleCommandError(deleteResult)
201
260
 
202
261
  out.printYaml({ draft_id: draftId, deleted: true })
203
262
  })
@@ -1,12 +1,14 @@
1
1
  // Filter commands: list, create, delete Gmail filters.
2
2
  // Multi-account support via getClients/getClient like label.ts.
3
3
 
4
- import type { Goke } from 'goke'
4
+ import type { ZeleCli } from '../cli-types.js'
5
5
  import { getClients } from '../auth.js'
6
- import { AuthError, isScopeError } from '../api-utils.js'
6
+ import { AuthError, UnsupportedError, isScopeError } from '../api-utils.js'
7
+ import type { GmailClient } from '../gmail-client.js'
7
8
  import * as out from '../output.js'
9
+ import { handleCommandError } from '../output.js'
8
10
 
9
- export function registerFilterCommands(cli: Goke) {
11
+ export function registerFilterCommands(cli: ZeleCli) {
10
12
  // =========================================================================
11
13
  // filter list
12
14
  // =========================================================================
@@ -15,10 +17,14 @@ export function registerFilterCommands(cli: Goke) {
15
17
  .command('mail filter list', 'List all Gmail filters')
16
18
  .action(async (options) => {
17
19
  const clients = await getClients(options.account)
20
+ const googleClients = clients.filter((c) => c.accountType === 'google')
21
+ if (googleClients.length === 0) {
22
+ handleCommandError(new UnsupportedError({ feature: 'Filters', accountType: 'IMAP/SMTP', hint: 'Filters are a Gmail-specific feature.' }))
23
+ }
18
24
 
19
25
  const results = await Promise.all(
20
- clients.map(async ({ email, client }) => {
21
- const res = await client.listFilters()
26
+ googleClients.map(async ({ email, client }) => {
27
+ const res = await (client as GmailClient).listFilters()
22
28
  if (res instanceof Error) return res
23
29
  return { email, filters: res.parsed }
24
30
  }),
@@ -3,13 +3,15 @@
3
3
  // Cache is handled by the client — commands just call methods and use data.
4
4
  // Multi-account: list and counts fetch all accounts concurrently and merge.
5
5
 
6
- import type { Goke } from 'goke'
6
+ import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
- import { getClients, getClient } from '../auth.js'
9
- import { AuthError } from '../api-utils.js'
8
+ import { getClients, getGmailClient } from '../auth.js'
9
+ import { AuthError, UnsupportedError } from '../api-utils.js'
10
+ import type { GmailClient } from '../gmail-client.js'
10
11
  import * as out from '../output.js'
12
+ import { handleCommandError } from '../output.js'
11
13
 
12
- export function registerLabelCommands(cli: Goke) {
14
+ export function registerLabelCommands(cli: ZeleCli) {
13
15
  // =========================================================================
14
16
  // label list
15
17
  // =========================================================================
@@ -18,11 +20,16 @@ export function registerLabelCommands(cli: Goke) {
18
20
  .command('label list', 'List all labels')
19
21
  .action(async (options) => {
20
22
  const clients = await getClients(options.account)
23
+ // Labels are Google-only — filter to Google accounts
24
+ const googleClients = clients.filter((c) => c.accountType === 'google')
25
+ if (googleClients.length === 0) {
26
+ handleCommandError(new UnsupportedError({ feature: 'Labels', accountType: 'IMAP/SMTP', hint: 'IMAP accounts use folders. Use --folder to browse different mailboxes.' }))
27
+ }
21
28
 
22
- // Fetch from all accounts concurrently
29
+ // Fetch from all Google accounts concurrently
23
30
  const results = await Promise.all(
24
- clients.map(async ({ email, client }) => {
25
- const labelsResult = await client.listLabels()
31
+ googleClients.map(async ({ email, client }) => {
32
+ const labelsResult = await (client as GmailClient).listLabels()
26
33
  if (labelsResult instanceof Error) return labelsResult
27
34
  return { email, labels: labelsResult.parsed }
28
35
  }),
@@ -69,7 +76,7 @@ export function registerLabelCommands(cli: Goke) {
69
76
  cli
70
77
  .command('label get <labelId>', 'Get label details with counts')
71
78
  .action(async (labelId, options) => {
72
- const { client } = await getClient(options.account)
79
+ const { client } = await getGmailClient(options.account)
73
80
 
74
81
  const label = await client.getLabel({ labelId })
75
82
 
@@ -93,7 +100,7 @@ export function registerLabelCommands(cli: Goke) {
93
100
  .option('--bg-color <bgColor>', z.string().describe('Background color (hex, e.g. #4986e7)'))
94
101
  .option('--text-color <textColor>', z.string().describe('Text color (hex, e.g. #ffffff)'))
95
102
  .action(async (name, options) => {
96
- const { client } = await getClient(options.account)
103
+ const { client } = await getGmailClient(options.account)
97
104
 
98
105
  const result = await client.createLabel({
99
106
  name,
@@ -128,7 +135,7 @@ export function registerLabelCommands(cli: Goke) {
128
135
  }
129
136
  }
130
137
 
131
- const { client } = await getClient(options.account)
138
+ const { client } = await getGmailClient(options.account)
132
139
  await client.deleteLabel({ labelId })
133
140
 
134
141
  out.printYaml({ label_id: labelId, deleted: true })
@@ -142,11 +149,15 @@ export function registerLabelCommands(cli: Goke) {
142
149
  .command('label counts', 'Show unread counts per label')
143
150
  .action(async (options) => {
144
151
  const clients = await getClients(options.account)
152
+ const googleClients = clients.filter((c) => c.accountType === 'google')
153
+ if (googleClients.length === 0) {
154
+ handleCommandError(new UnsupportedError({ feature: 'Label counts', accountType: 'IMAP/SMTP', hint: 'IMAP accounts use folders, not labels.' }))
155
+ }
145
156
 
146
- // Fetch from all accounts concurrently
157
+ // Fetch from all Google accounts concurrently
147
158
  const results = await Promise.all(
148
- clients.map(async ({ email, client }) => {
149
- const countsResult = await client.getLabelCounts()
159
+ googleClients.map(async ({ email, client }) => {
160
+ const countsResult = await (client as GmailClient).getLabelCounts()
150
161
  if (countsResult instanceof Error) return countsResult
151
162
  return { email, counts: countsResult.parsed }
152
163
  }),