zele 0.3.20 → 0.4.0

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.
@@ -5,6 +5,7 @@
5
5
  import type { ZeleCli } from '../cli-types.js'
6
6
  import { z } from 'zod'
7
7
  import pc from 'picocolors'
8
+ import * as clack from '@clack/prompts'
8
9
  import { login, loginImap, logout, listAccounts, getAuthStatuses } from '../auth.js'
9
10
  import { closePrisma } from '../db.js'
10
11
  import * as out from '../output.js'
@@ -13,60 +14,71 @@ import { handleCommandError } from '../output.js'
13
14
  export function registerAuthCommands(cli: ZeleCli) {
14
15
  cli
15
16
  .command('login', 'Authenticate with Google (opens browser) or show IMAP/SMTP login instructions')
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 })
17
+ .option(
18
+ '--method <method>',
19
+ z.enum(['google', 'imap']).optional().describe('Authentication method (google or imap)'),
20
+ )
21
+ .action(async (options) => {
22
+ let method = options.method
21
23
 
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')
24
+ if (!method) {
25
+ if (!process.stdin.isTTY) {
26
+ out.error('Run non-interactively with: zele login --method google|imap')
27
+ process.exit(1)
28
+ }
25
29
 
26
- const answer = await new Promise<string>((resolve) => {
27
- rl.question('Enter choice [1]: ', resolve)
30
+ const choice = await clack.select({
31
+ message: 'Choose authentication method',
32
+ options: [
33
+ { value: 'google', label: 'Google', hint: 'opens browser for OAuth' },
34
+ { value: 'imap', label: 'Other', hint: 'IMAP/SMTP with password' },
35
+ ],
28
36
  })
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
37
+
38
+ if (clack.isCancel(choice)) {
39
+ out.hint('Cancelled')
40
+ process.exit(0)
66
41
  }
42
+
43
+ method = choice
44
+ }
45
+
46
+ if (method === 'imap') {
47
+ console.error(pc.bold('\nTo add an IMAP/SMTP account, run:\n'))
48
+ console.error(pc.dim(' # Fastmail'))
49
+ console.error(` zele login imap \\`)
50
+ console.error(` --email you@fastmail.com \\`)
51
+ console.error(` --imap-host imap.fastmail.com --imap-port 993 \\`)
52
+ console.error(` --smtp-host smtp.fastmail.com --smtp-port 465 \\`)
53
+ console.error(` --password "your-app-password"`)
54
+ console.error()
55
+ console.error(pc.dim(' # Gmail (app password)'))
56
+ console.error(` zele login imap \\`)
57
+ console.error(` --email you@gmail.com \\`)
58
+ console.error(` --imap-host imap.gmail.com --imap-port 993 \\`)
59
+ console.error(` --smtp-host smtp.gmail.com --smtp-port 465 \\`)
60
+ console.error(` --password "your-app-password"`)
61
+ console.error()
62
+ console.error(pc.dim(' # Outlook/Hotmail'))
63
+ console.error(` zele login imap \\`)
64
+ console.error(` --email you@outlook.com \\`)
65
+ console.error(` --imap-host outlook.office365.com --imap-port 993 \\`)
66
+ console.error(` --smtp-host smtp-mail.outlook.com --smtp-port 587 \\`)
67
+ console.error(` --password "your-password"`)
68
+ console.error()
69
+ console.error(pc.dim(' # Generic (any IMAP/SMTP provider)'))
70
+ console.error(` zele login imap \\`)
71
+ console.error(` --email you@example.com \\`)
72
+ console.error(` --imap-host imap.example.com --imap-port 993 \\`)
73
+ console.error(` --smtp-host smtp.example.com --smtp-port 465 \\`)
74
+ console.error(` --password "your-password"`)
75
+ console.error()
76
+ console.error(pc.dim('Omit --smtp-host for read-only (IMAP only, no sending).'))
77
+ console.error(pc.dim('Use --imap-user/--smtp-user if the login username differs from your email.'))
78
+ return
67
79
  }
68
80
 
69
- // Default: Google OAuth flow
81
+ // Google OAuth flow
70
82
  const result = await login()
71
83
  if (result instanceof Error) handleCommandError(result)
72
84
  const { email } = result
@@ -162,14 +174,12 @@ export function registerAuthCommands(cli: ZeleCli) {
162
174
  process.exit(1)
163
175
  }
164
176
 
165
- const readline = await import('node:readline')
166
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
167
- const answer = await new Promise<string>((resolve) => {
168
- rl.question(`Remove credentials for ${targetEmail}? [y/N] `, resolve)
177
+ const confirmed = await clack.confirm({
178
+ message: `Remove credentials for ${targetEmail}?`,
179
+ initialValue: false,
169
180
  })
170
- rl.close()
171
181
 
172
- if (answer.toLowerCase() !== 'y') {
182
+ if (clack.isCancel(confirmed) || !confirmed) {
173
183
  out.hint('Cancelled')
174
184
  return
175
185
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
- import readline from 'node:readline'
8
+ import * as clack from '@clack/prompts'
9
9
  import { getCalendarClients, getCalendarClient } from '../auth.js'
10
10
  import type { CalendarClient, CalendarEvent, CalendarListItem, EventListResult } from '../calendar-client.js'
11
11
  import { AuthError } from '../api-utils.js'
@@ -76,11 +76,11 @@ export function registerCalendarCommands(cli: ZeleCli) {
76
76
  .option('--week', 'Show this week')
77
77
  .option('--days <days>', z.number().describe('Show next N days'))
78
78
  .option('--all', 'Fetch from all calendars')
79
- .option('--query <query>', 'Free text search')
80
- .option('--max [max]', 'Max results (default: 20)')
79
+ .option('--filter <filter>', 'Free text search')
80
+ .option('--limit [limit]', 'Max results (default: 20)')
81
81
  .option('--page <page>', 'Pagination token (requires --account, only works for a single account)')
82
82
  .action(async (options) => {
83
- const max = options.max ? Number(options.max) : 20
83
+ const limit = options.limit ? Number(options.limit) : 20
84
84
  const calendarId = options.calendar ?? 'primary'
85
85
 
86
86
  if (options.all && options.calendar) {
@@ -122,8 +122,8 @@ export function registerCalendarCommands(cli: ZeleCli) {
122
122
  calendarId: cal.id,
123
123
  timeMin,
124
124
  timeMax,
125
- query: options.query,
126
- maxResults: max,
125
+ query: options.filter,
126
+ maxResults: limit,
127
127
  })
128
128
  if (r instanceof Error) return r
129
129
  return r.events.map((e) => ({ ...e, calendarId: cal.id }))
@@ -145,8 +145,8 @@ export function registerCalendarCommands(cli: ZeleCli) {
145
145
  calendarId,
146
146
  timeMin,
147
147
  timeMax,
148
- query: options.query,
149
- maxResults: max,
148
+ query: options.filter,
149
+ maxResults: limit,
150
150
  pageToken: options.page,
151
151
  })
152
152
  if (r instanceof Error) return r
@@ -171,7 +171,7 @@ export function registerCalendarCommands(cli: ZeleCli) {
171
171
  result.events.map((e) => ({ ...e, account: email })),
172
172
  )
173
173
  .sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
174
- .slice(0, max)
174
+ .slice(0, limit)
175
175
 
176
176
  if (merged.length === 0) {
177
177
  out.printList([], { summary: 'No events found' })
@@ -418,13 +418,12 @@ export function registerCalendarCommands(cli: ZeleCli) {
418
418
  const { client } = await getCalendarClient(options.account)
419
419
 
420
420
  if (!options.force && process.stdin.isTTY) {
421
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
422
- const answer = await new Promise<string>((resolve) => {
423
- rl.question(`Delete event ${eventId}? [y/N] `, resolve)
421
+ const confirmed = await clack.confirm({
422
+ message: `Delete event ${eventId}?`,
423
+ initialValue: false,
424
424
  })
425
- rl.close()
426
425
 
427
- if (answer.toLowerCase() !== 'y') {
426
+ if (clack.isCancel(confirmed) || !confirmed) {
428
427
  out.hint('Cancelled')
429
428
  return
430
429
  }
@@ -6,6 +6,7 @@
6
6
  import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
8
  import fs from 'node:fs'
9
+ import * as clack from '@clack/prompts'
9
10
  import { getClients, getClient } from '../auth.js'
10
11
  import type { GmailClient } from '../gmail-client.js'
11
12
  import type { ImapSmtpClient } from '../imap-smtp-client.js'
@@ -21,9 +22,9 @@ export function registerDraftCommands(cli: ZeleCli) {
21
22
 
22
23
  cli
23
24
  .command('draft list', 'List drafts')
24
- .option('--max <max>', z.number().default(20).describe('Max results'))
25
+ .option('--limit <limit>', z.number().default(20).describe('Max results'))
25
26
  .option('--page <page>', z.string().describe('Pagination token (requires --account, only works for a single account)'))
26
- .option('--query <query>', z.string().describe('Search query'))
27
+ .option('--filter <filter>', z.string().describe('Search query'))
27
28
  .action(async (options) => {
28
29
  const clients = await getClients(options.account)
29
30
 
@@ -36,8 +37,8 @@ export function registerDraftCommands(cli: ZeleCli) {
36
37
  const results = await Promise.all(
37
38
  clients.map(async ({ email, client }) => {
38
39
  const result = await client.listDrafts({
39
- query: options.query,
40
- maxResults: options.max,
40
+ query: options.filter,
41
+ maxResults: options.limit,
41
42
  pageToken: options.page,
42
43
  })
43
44
  if (result instanceof Error) return result
@@ -51,13 +52,13 @@ export function registerDraftCommands(cli: ZeleCli) {
51
52
  return true
52
53
  })
53
54
 
54
- // Merge drafts from all accounts, sorted by date descending, capped at max
55
+ // Merge drafts from all accounts, sorted by date descending, capped at limit
55
56
  const merged = allResults
56
57
  .flatMap(({ email, result }) =>
57
58
  result.drafts.map((d) => ({ ...d, account: email })),
58
59
  )
59
60
  .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
60
- .slice(0, options.max)
61
+ .slice(0, options.limit)
61
62
 
62
63
  if (merged.length === 0) {
63
64
  out.printList([], { summary: 'No drafts found' })
@@ -240,14 +241,12 @@ export function registerDraftCommands(cli: ZeleCli) {
240
241
  .option('--force', 'Skip confirmation')
241
242
  .action(async (draftId, options) => {
242
243
  if (!options.force && process.stdin.isTTY) {
243
- const readline = await import('node:readline')
244
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
245
- const answer = await new Promise<string>((resolve) => {
246
- rl.question(`Delete draft ${draftId}? [y/N] `, resolve)
244
+ const confirmed = await clack.confirm({
245
+ message: `Delete draft ${draftId}?`,
246
+ initialValue: false,
247
247
  })
248
- rl.close()
249
248
 
250
- if (answer.toLowerCase() !== 'y') {
249
+ if (clack.isCancel(confirmed) || !confirmed) {
251
250
  out.hint('Cancelled')
252
251
  return
253
252
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { ZeleCli } from '../cli-types.js'
7
7
  import { z } from 'zod'
8
+ import * as clack from '@clack/prompts'
8
9
  import { getClients, getGmailClient } from '../auth.js'
9
10
  import { AuthError, UnsupportedError } from '../api-utils.js'
10
11
  import type { GmailClient } from '../gmail-client.js'
@@ -122,14 +123,12 @@ export function registerLabelCommands(cli: ZeleCli) {
122
123
  .option('--force', 'Skip confirmation')
123
124
  .action(async (labelId, options) => {
124
125
  if (!options.force && process.stdin.isTTY) {
125
- const readline = await import('node:readline')
126
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
127
- const answer = await new Promise<string>((resolve) => {
128
- rl.question(`Delete label ${labelId}? [y/N] `, resolve)
126
+ const confirmed = await clack.confirm({
127
+ message: `Delete label ${labelId}?`,
128
+ initialValue: false,
129
129
  })
130
- rl.close()
131
130
 
132
- if (answer.toLowerCase() !== 'y') {
131
+ if (clack.isCancel(confirmed) || !confirmed) {
133
132
  out.hint('Cancelled')
134
133
  return
135
134
  }
@@ -52,15 +52,15 @@ export function registerMailCommands(cli: ZeleCli) {
52
52
  cli
53
53
  .command('mail list', 'List email threads')
54
54
  .option('--folder [folder]', 'Folder to list (inbox, sent, trash, spam, starred, drafts, archive, all) (default: inbox)')
55
- .option('--max [max]', 'Max results per page (default: 20)')
55
+ .option('--limit [limit]', 'Max threads to show (default: 20)')
56
56
  .option('--page <page>', 'Pagination token (requires --account, only works for a single account)')
57
57
  .option('--label <label>', 'Filter by label name')
58
58
  .option('--filter <filter>', 'Gmail search filter (e.g. "is:unread", "from:github", "has:attachment")')
59
59
  .action(async (options) => {
60
- // `options.folder` / `options.max` are `string | undefined` now.
60
+ // `options.folder` / `options.limit` are `string | undefined` now.
61
61
  // `''` (bare flag) falls back to the default via `||`.
62
62
  const folder = options.folder || 'inbox'
63
- const max = options.max ? Number(options.max) : 20
63
+ const limit = options.limit ? Number(options.limit) : 20
64
64
  const clients = await getClients(options.account)
65
65
 
66
66
  if (options.page && clients.length > 1) {
@@ -73,7 +73,7 @@ export function registerMailCommands(cli: ZeleCli) {
73
73
  clients.map(async ({ email, client, accountType }) => {
74
74
  const result = await client.listThreads({
75
75
  folder,
76
- maxResults: max,
76
+ maxResults: limit,
77
77
  labelIds: options.label ? [options.label] : undefined,
78
78
  pageToken: options.page,
79
79
  query: options.filter,
@@ -102,13 +102,13 @@ export function registerMailCommands(cli: ZeleCli) {
102
102
  const labelMap = new Map<string, string>()
103
103
  for (const r of allResults) for (const [id, name] of r.labelMap) labelMap.set(id, name)
104
104
 
105
- // Merge threads from all accounts, sorted by date descending, capped at max
105
+ // Merge threads from all accounts, sorted by date descending, capped at limit
106
106
  const merged = allResults
107
107
  .flatMap(({ email, result }) =>
108
108
  result.threads.map((t) => ({ ...t, account: email })),
109
109
  )
110
110
  .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
111
- .slice(0, max)
111
+ .slice(0, limit)
112
112
 
113
113
  if (merged.length === 0) {
114
114
  out.printList([], { summary: 'No threads found' })
@@ -150,10 +150,10 @@ export function registerMailCommands(cli: ZeleCli) {
150
150
 
151
151
  cli
152
152
  .command('mail search <query>', 'Search email threads using Gmail query syntax (from:, to:, subject:, has:attachment, etc). See https://support.google.com/mail/answer/7190')
153
- .option('--max [max]', 'Max results (default: 20)')
153
+ .option('--limit [limit]', 'Max results to show (default: 20)')
154
154
  .option('--page <page>', 'Pagination token (requires --account, only works for a single account)')
155
155
  .action(async (query, options) => {
156
- const max = options.max ? Number(options.max) : 20
156
+ const limit = options.limit ? Number(options.limit) : 20
157
157
  const clients = await getClients(options.account)
158
158
 
159
159
  if (options.page && clients.length > 1) {
@@ -166,7 +166,7 @@ export function registerMailCommands(cli: ZeleCli) {
166
166
  clients.map(async ({ email, client, accountType }) => {
167
167
  const result = await client.listThreads({
168
168
  query,
169
- maxResults: max,
169
+ maxResults: limit,
170
170
  pageToken: options.page,
171
171
  })
172
172
  if (result instanceof Error) return result
@@ -197,7 +197,7 @@ export function registerMailCommands(cli: ZeleCli) {
197
197
  result.threads.map((t) => ({ ...t, account: email })),
198
198
  )
199
199
  .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
200
- .slice(0, max)
200
+ .slice(0, limit)
201
201
 
202
202
  if (merged.length === 0) {
203
203
  out.printList([], { summary: `No results for "${query}"` })
@@ -18,7 +18,7 @@ export function registerWatchCommands(cli: ZeleCli) {
18
18
  .command('mail watch', 'Watch for new emails (poll via History API)')
19
19
  .option('--interval [interval]', z.string().describe('Poll interval in seconds (default: 15)'))
20
20
  .option('--folder [folder]', z.string().describe('Folder to watch (default: inbox)'))
21
- .option('--query [query]', z.string().describe('Filter messages client-side (from:, to:, cc:, subject:, is:unread, is:starred, has:attachment, -negate). See https://support.google.com/mail/answer/7190'))
21
+ .option('--filter [filter]', z.string().describe('Filter messages client-side (from:, to:, cc:, subject:, is:unread, is:starred, has:attachment, -negate). See https://support.google.com/mail/answer/7190'))
22
22
  .option('--once', z.boolean().describe('Print changes once and exit (no loop)'))
23
23
  .action(async (options) => {
24
24
  const interval = options.interval ? Number(options.interval) : 15
@@ -43,7 +43,7 @@ export function registerWatchCommands(cli: ZeleCli) {
43
43
  client.watchInbox({
44
44
  folder,
45
45
  intervalMs: interval * 1000,
46
- query: options.query,
46
+ query: options.filter,
47
47
  once: options.once,
48
48
  }),
49
49
  )
package/src/output.ts CHANGED
@@ -140,7 +140,6 @@ turndown.addRule('hidden-elements', {
140
140
  export function htmlToMarkdown(html: string): string {
141
141
  // Pre-clean: remove common email noise
142
142
  const cleaned = html
143
- .replace(/<!\-\-[\s\S]*?\-\->/g, '') // HTML comments
144
143
  .replace(/<o:p>[\s\S]*?<\/o:p>/gi, '') // Outlook tags
145
144
  .replace(/<!\[if[\s\S]*?<!\[endif\]>/gi, '') // Outlook conditional comments
146
145