zele 0.3.15 → 0.3.17

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 (80) hide show
  1. package/README.md +113 -33
  2. package/dist/api-utils.d.ts +7 -0
  3. package/dist/api-utils.js +12 -0
  4. package/dist/api-utils.js.map +1 -1
  5. package/dist/auth.d.ts +71 -9
  6. package/dist/auth.js +187 -11
  7. package/dist/auth.js.map +1 -1
  8. package/dist/calendar-time.js +6 -0
  9. package/dist/calendar-time.js.map +1 -1
  10. package/dist/cli.js +6 -1
  11. package/dist/cli.js.map +1 -1
  12. package/dist/commands/attachment.js +2 -0
  13. package/dist/commands/attachment.js.map +1 -1
  14. package/dist/commands/auth-cmd.js +104 -6
  15. package/dist/commands/auth-cmd.js.map +1 -1
  16. package/dist/commands/calendar.js +1 -1
  17. package/dist/commands/calendar.js.map +1 -1
  18. package/dist/commands/draft.js +9 -3
  19. package/dist/commands/draft.js.map +1 -1
  20. package/dist/commands/filter.d.ts +2 -0
  21. package/dist/commands/filter.js +64 -0
  22. package/dist/commands/filter.js.map +1 -0
  23. package/dist/commands/label.js +19 -9
  24. package/dist/commands/label.js.map +1 -1
  25. package/dist/commands/mail-actions.js +12 -2
  26. package/dist/commands/mail-actions.js.map +1 -1
  27. package/dist/commands/mail.js +194 -84
  28. package/dist/commands/mail.js.map +1 -1
  29. package/dist/commands/profile.js +25 -18
  30. package/dist/commands/profile.js.map +1 -1
  31. package/dist/db.js +24 -0
  32. package/dist/db.js.map +1 -1
  33. package/dist/generated/internal/class.js +2 -2
  34. package/dist/generated/internal/class.js.map +1 -1
  35. package/dist/generated/internal/prismaNamespace.d.ts +2 -0
  36. package/dist/generated/internal/prismaNamespace.js +2 -0
  37. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  38. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +2 -0
  39. package/dist/generated/internal/prismaNamespaceBrowser.js +2 -0
  40. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  41. package/dist/generated/models/Account.d.ts +97 -1
  42. package/dist/gmail-client.d.ts +42 -0
  43. package/dist/gmail-client.js +175 -9
  44. package/dist/gmail-client.js.map +1 -1
  45. package/dist/imap-smtp-client.d.ts +235 -0
  46. package/dist/imap-smtp-client.js +1225 -0
  47. package/dist/imap-smtp-client.js.map +1 -0
  48. package/dist/mail-tui.js +3 -2
  49. package/dist/mail-tui.js.map +1 -1
  50. package/dist/output.d.ts +2 -0
  51. package/dist/output.js +4 -0
  52. package/dist/output.js.map +1 -1
  53. package/package.json +9 -5
  54. package/schema.prisma +7 -5
  55. package/skills/zele/SKILL.md +141 -0
  56. package/src/api-utils.ts +13 -0
  57. package/src/auth.ts +283 -15
  58. package/src/calendar-time.test.ts +35 -0
  59. package/src/calendar-time.ts +5 -0
  60. package/src/cli.ts +6 -1
  61. package/src/commands/attachment.ts +1 -0
  62. package/src/commands/auth-cmd.ts +112 -6
  63. package/src/commands/calendar.ts +1 -1
  64. package/src/commands/draft.ts +7 -3
  65. package/src/commands/filter.ts +74 -0
  66. package/src/commands/label.ts +22 -11
  67. package/src/commands/mail-actions.ts +16 -3
  68. package/src/commands/mail.ts +207 -89
  69. package/src/commands/profile.ts +27 -17
  70. package/src/db.ts +28 -0
  71. package/src/generated/internal/class.ts +2 -2
  72. package/src/generated/internal/prismaNamespace.ts +2 -0
  73. package/src/generated/internal/prismaNamespaceBrowser.ts +2 -0
  74. package/src/generated/models/Account.ts +97 -1
  75. package/src/gmail-client.test.ts +155 -2
  76. package/src/gmail-client.ts +221 -16
  77. package/src/imap-smtp-client.ts +1381 -0
  78. package/src/mail-tui.tsx +3 -3
  79. package/src/output.ts +8 -1
  80. package/src/schema.sql +2 -0
@@ -78,7 +78,7 @@ export function registerCalendarCommands(cli: Goke) {
78
78
  .option('--all', 'Fetch from all calendars')
79
79
  .option('--query <query>', 'Free text search')
80
80
  .option('--max [max]', 'Max results (default: 20)')
81
- .option('--page <page>', 'Pagination token')
81
+ .option('--page <page>', 'Pagination token (requires --account, only works for a single account)')
82
82
  .action(async (options) => {
83
83
  const max = options.max ? Number(options.max) : 20
84
84
  const calendarId = options.calendar ?? 'primary'
@@ -8,6 +8,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'
@@ -21,7 +22,7 @@ export function registerDraftCommands(cli: Goke) {
21
22
  cli
22
23
  .command('draft list', 'List drafts')
23
24
  .option('--max <max>', z.number().default(20).describe('Max results'))
24
- .option('--page <page>', z.string().describe('Pagination token'))
25
+ .option('--page <page>', z.string().describe('Pagination token (requires --account, only works for a single account)'))
25
26
  .option('--query <query>', z.string().describe('Search query'))
26
27
  .action(async (options) => {
27
28
  const clients = await getClients(options.account)
@@ -72,7 +73,7 @@ export function registerDraftCommands(cli: Goke) {
72
73
  subject: d.subject,
73
74
  date: out.formatDate(d.date),
74
75
  })),
75
- { summary: `${merged.length} draft(s)` },
76
+ { summary: `${merged.length} draft(s)`, nextPage: allResults[0]?.result.nextPageToken },
76
77
  )
77
78
  })
78
79
 
@@ -154,6 +155,7 @@ export function registerDraftCommands(cli: Goke) {
154
155
  threadId: options.thread,
155
156
  fromEmail: options.from,
156
157
  })
158
+ if (result instanceof Error) handleCommandError(result)
157
159
 
158
160
  out.printYaml(result)
159
161
  out.success('Draft created')
@@ -168,6 +170,7 @@ export function registerDraftCommands(cli: Goke) {
168
170
  .action(async (draftId, options) => {
169
171
  const { client } = await getClient(options.account)
170
172
  const result = await client.sendDraft({ draftId })
173
+ if (result instanceof Error) handleCommandError(result)
171
174
 
172
175
  out.printYaml(result)
173
176
  out.success('Draft sent')
@@ -197,7 +200,8 @@ export function registerDraftCommands(cli: Goke) {
197
200
 
198
201
  const { client } = await getClient(options.account)
199
202
 
200
- await client.deleteDraft({ draftId })
203
+ const deleteResult = await client.deleteDraft({ draftId })
204
+ if (deleteResult instanceof Error) handleCommandError(deleteResult)
201
205
 
202
206
  out.printYaml({ draft_id: draftId, deleted: true })
203
207
  })
@@ -0,0 +1,74 @@
1
+ // Filter commands: list, create, delete Gmail filters.
2
+ // Multi-account support via getClients/getClient like label.ts.
3
+
4
+ import type { Goke } from 'goke'
5
+ import { getClients } from '../auth.js'
6
+ import { AuthError, UnsupportedError, isScopeError } from '../api-utils.js'
7
+ import type { GmailClient } from '../gmail-client.js'
8
+ import * as out from '../output.js'
9
+ import { handleCommandError } from '../output.js'
10
+
11
+ export function registerFilterCommands(cli: Goke) {
12
+ // =========================================================================
13
+ // filter list
14
+ // =========================================================================
15
+
16
+ cli
17
+ .command('mail filter list', 'List all Gmail filters')
18
+ .action(async (options) => {
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
+ }
24
+
25
+ const results = await Promise.all(
26
+ googleClients.map(async ({ email, client }) => {
27
+ const res = await (client as GmailClient).listFilters()
28
+ if (res instanceof Error) return res
29
+ return { email, filters: res.parsed }
30
+ }),
31
+ )
32
+
33
+ const allResults = results.filter((r): r is Exclude<typeof r, Error> => {
34
+ if (r instanceof AuthError) {
35
+ if (isScopeError(r)) {
36
+ out.error(`Missing required OAuth scopes. Run: zele login to grant updated permissions`)
37
+ } else {
38
+ out.error(`${r.message}. Try: zele login`)
39
+ }
40
+ return false
41
+ }
42
+ if (r instanceof Error) { out.error(`Failed to fetch filters: ${r.message}`); return false }
43
+ return true
44
+ })
45
+
46
+ const merged = allResults.flatMap(({ email, filters }) =>
47
+ filters.map((f) => ({ ...f, account: email })),
48
+ )
49
+
50
+ if (merged.length === 0) {
51
+ out.hint('No filters found')
52
+ return
53
+ }
54
+
55
+ const showAccount = clients.length > 1
56
+ for (const f of merged) {
57
+ out.printYaml({
58
+ ...(showAccount ? { account: f.account } : {}),
59
+ id: f.id,
60
+ criteria: f.criteria,
61
+ action: f.action,
62
+ })
63
+ }
64
+
65
+ out.hint(`${merged.length} filter(s)`)
66
+ })
67
+
68
+ // TODO: add `mail filter create` and `mail filter delete` commands once
69
+ // gmail.settings.basic scope is added to the GCP OAuth consent screen.
70
+ // The https://mail.google.com/ scope covers reading filters but Google
71
+ // enforces the narrower scope for write operations (create/delete).
72
+ // The gmail-client.ts methods (createFilter, deleteFilter, resolveLabel)
73
+ // are already implemented and ready to use.
74
+ }
@@ -5,9 +5,11 @@
5
5
 
6
6
  import type { Goke } from 'goke'
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
14
  export function registerLabelCommands(cli: Goke) {
13
15
  // =========================================================================
@@ -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
  }),
@@ -1,10 +1,11 @@
1
- // Mail action commands: star, unstar, archive, trash, untrash, mark read/unread, label modify.
1
+ // Mail action commands: star, unstar, archive, trash, untrash, mark read/unread, spam, unspam, label modify.
2
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
7
  import type { GmailClient } from '../gmail-client.js'
8
+ import type { ImapSmtpClient } from '../imap-smtp-client.js'
8
9
  import * as out from '../output.js'
9
10
  import { handleCommandError } from '../output.js'
10
11
 
@@ -16,7 +17,7 @@ async function bulkAction(
16
17
  threadIds: string[],
17
18
  actionName: string,
18
19
  accountFilter: string[] | undefined,
19
- fn: (client: GmailClient, ids: string[]) => Promise<void | Error>,
20
+ fn: (client: GmailClient | ImapSmtpClient, ids: string[]) => Promise<void | Error>,
20
21
  ) {
21
22
  if (threadIds.length === 0) {
22
23
  out.error('No thread IDs provided')
@@ -66,7 +67,7 @@ export function registerMailActionCommands(cli: Goke) {
66
67
  })
67
68
 
68
69
  cli
69
- .command('mail read-mark [...threadIds]', 'Mark threads as read')
70
+ .command('mail read-mark [...threadIds]', 'Mark threads as read (removes UNREAD label)')
70
71
  .action(async (threadIds, options) => {
71
72
  await bulkAction(threadIds, 'Marked as read', options.account, (c, ids) => c.markAsRead({ threadIds: ids }))
72
73
  })
@@ -77,6 +78,18 @@ export function registerMailActionCommands(cli: Goke) {
77
78
  await bulkAction(threadIds, 'Marked as unread', options.account, (c, ids) => c.markAsUnread({ threadIds: ids }))
78
79
  })
79
80
 
81
+ cli
82
+ .command('mail spam [...threadIds]', 'Mark threads as spam')
83
+ .action(async (threadIds, options) => {
84
+ await bulkAction(threadIds, 'Marked as spam', options.account, (c, ids) => c.markAsSpam({ threadIds: ids }))
85
+ })
86
+
87
+ cli
88
+ .command('mail unspam [...threadIds]', 'Remove threads from spam')
89
+ .action(async (threadIds, options) => {
90
+ await bulkAction(threadIds, 'Removed from spam', options.account, (c, ids) => c.unmarkSpam({ threadIds: ids }))
91
+ })
92
+
80
93
  cli
81
94
  .command('mail label [...threadIds]', 'Add or remove labels from threads')
82
95
  .option('--add <add>', z.string().describe('Labels to add (comma-separated)'))