zele 0.3.16 → 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 (63) hide show
  1. package/README.md +91 -36
  2. package/dist/api-utils.d.ts +4 -0
  3. package/dist/api-utils.js +6 -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/commands/attachment.js +2 -0
  9. package/dist/commands/attachment.js.map +1 -1
  10. package/dist/commands/auth-cmd.js +104 -6
  11. package/dist/commands/auth-cmd.js.map +1 -1
  12. package/dist/commands/draft.js +7 -1
  13. package/dist/commands/draft.js.map +1 -1
  14. package/dist/commands/filter.js +7 -2
  15. package/dist/commands/filter.js.map +1 -1
  16. package/dist/commands/label.js +19 -9
  17. package/dist/commands/label.js.map +1 -1
  18. package/dist/commands/mail-actions.js.map +1 -1
  19. package/dist/commands/mail.js +49 -22
  20. package/dist/commands/mail.js.map +1 -1
  21. package/dist/commands/profile.js +25 -18
  22. package/dist/commands/profile.js.map +1 -1
  23. package/dist/db.js +24 -0
  24. package/dist/db.js.map +1 -1
  25. package/dist/generated/internal/class.js +2 -2
  26. package/dist/generated/internal/class.js.map +1 -1
  27. package/dist/generated/internal/prismaNamespace.d.ts +2 -0
  28. package/dist/generated/internal/prismaNamespace.js +2 -0
  29. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  30. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +2 -0
  31. package/dist/generated/internal/prismaNamespaceBrowser.js +2 -0
  32. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  33. package/dist/generated/models/Account.d.ts +97 -1
  34. package/dist/gmail-client.d.ts +14 -0
  35. package/dist/gmail-client.js +46 -0
  36. package/dist/gmail-client.js.map +1 -1
  37. package/dist/imap-smtp-client.d.ts +235 -0
  38. package/dist/imap-smtp-client.js +1225 -0
  39. package/dist/imap-smtp-client.js.map +1 -0
  40. package/dist/mail-tui.js.map +1 -1
  41. package/package.json +5 -2
  42. package/schema.prisma +7 -5
  43. package/skills/zele/SKILL.md +50 -21
  44. package/src/api-utils.ts +6 -0
  45. package/src/auth.ts +282 -14
  46. package/src/commands/attachment.ts +1 -0
  47. package/src/commands/auth-cmd.ts +112 -6
  48. package/src/commands/draft.ts +5 -1
  49. package/src/commands/filter.ts +9 -3
  50. package/src/commands/label.ts +22 -11
  51. package/src/commands/mail-actions.ts +2 -1
  52. package/src/commands/mail.ts +52 -22
  53. package/src/commands/profile.ts +27 -17
  54. package/src/db.ts +28 -0
  55. package/src/generated/internal/class.ts +2 -2
  56. package/src/generated/internal/prismaNamespace.ts +2 -0
  57. package/src/generated/internal/prismaNamespaceBrowser.ts +2 -0
  58. package/src/generated/models/Account.ts +97 -1
  59. package/src/gmail-client.test.ts +155 -2
  60. package/src/gmail-client.ts +65 -0
  61. package/src/imap-smtp-client.ts +1381 -0
  62. package/src/mail-tui.tsx +2 -1
  63. package/src/schema.sql +2 -0
@@ -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
  }),
@@ -5,6 +5,7 @@ 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')
@@ -11,6 +11,7 @@ import React from 'react'
11
11
  import { lookup as mimeLookup } from 'mrmime'
12
12
  import { getClients, getClient, listAccounts, login } from '../auth.js'
13
13
  import type { ThreadListResult } from '../gmail-client.js'
14
+ import type { GmailClient } from '../gmail-client.js'
14
15
  import { AuthError } from '../api-utils.js'
15
16
  import * as out from '../output.js'
16
17
  import { handleCommandError } from '../output.js'
@@ -66,19 +67,24 @@ export function registerMailCommands(cli: Goke) {
66
67
 
67
68
  // Fetch threads and labels from all accounts concurrently
68
69
  const results = await Promise.all(
69
- clients.map(async ({ email, client }) => {
70
- const [result, labelsResult] = await Promise.all([
71
- client.listThreads({
72
- folder,
73
- maxResults: max,
74
- labelIds: options.label ? [options.label] : undefined,
75
- pageToken: options.page,
76
- query: options.filter,
77
- }),
78
- client.listLabels(),
79
- ])
70
+ clients.map(async ({ email, client, accountType }) => {
71
+ const result = await client.listThreads({
72
+ folder,
73
+ maxResults: max,
74
+ labelIds: options.label ? [options.label] : undefined,
75
+ pageToken: options.page,
76
+ query: options.filter,
77
+ })
80
78
  if (result instanceof Error) return result
81
- const labelMap = labelsResult instanceof Error ? new Map<string, string>() : new Map(labelsResult.parsed.map((l) => [l.id, l.name]))
79
+
80
+ // Labels are Google-only — skip for IMAP accounts
81
+ let labelMap = new Map<string, string>()
82
+ if (accountType === 'google') {
83
+ const labelsResult = await (client as GmailClient).listLabels()
84
+ if (!(labelsResult instanceof Error)) {
85
+ labelMap = new Map(labelsResult.parsed.map((l) => [l.id, l.name]))
86
+ }
87
+ }
82
88
  return { email, result, labelMap }
83
89
  }),
84
90
  )
@@ -150,17 +156,21 @@ export function registerMailCommands(cli: Goke) {
150
156
 
151
157
  // Search all accounts concurrently (fetch labels alongside for name resolution)
152
158
  const results = await Promise.all(
153
- clients.map(async ({ email, client }) => {
154
- const [result, labelsResult] = await Promise.all([
155
- client.listThreads({
156
- query,
157
- maxResults: max,
158
- pageToken: options.page,
159
- }),
160
- client.listLabels(),
161
- ])
159
+ clients.map(async ({ email, client, accountType }) => {
160
+ const result = await client.listThreads({
161
+ query,
162
+ maxResults: max,
163
+ pageToken: options.page,
164
+ })
162
165
  if (result instanceof Error) return result
163
- const labelMap = labelsResult instanceof Error ? new Map<string, string>() : new Map(labelsResult.parsed.map((l) => [l.id, l.name]))
166
+
167
+ let labelMap = new Map<string, string>()
168
+ if (accountType === 'google') {
169
+ const labelsResult = await (client as GmailClient).listLabels()
170
+ if (!(labelsResult instanceof Error)) {
171
+ labelMap = new Map(labelsResult.parsed.map((l) => [l.id, l.name]))
172
+ }
173
+ }
164
174
  return { email, result, labelMap }
165
175
  }),
166
176
  )
@@ -220,6 +230,7 @@ export function registerMailCommands(cli: Goke) {
220
230
  .command('mail read [...threadIds]', 'Read full email threads (does not mark as read)')
221
231
  .option('--raw', 'Show raw message (first message only, single thread)')
222
232
  .option('--raw-html', 'Show raw HTML body per message (no markdown conversion)')
233
+ .option('--verify', 'Show expanded email authentication details (SPF/DKIM/DMARC)')
223
234
  .action(async (threadIds, options) => {
224
235
  if (threadIds.length === 0) {
225
236
  out.error('No thread IDs provided')
@@ -323,6 +334,24 @@ export function registerMailCommands(cli: Goke) {
323
334
  }
324
335
  console.log(pc.dim(`Date: ${dateStr}`))
325
336
 
337
+ if (msg.auth) {
338
+ const check = (verdict: string) => {
339
+ return verdict === 'pass'
340
+ ? pc.green('✓')
341
+ : pc.red('✗')
342
+ }
343
+ const parts = [
344
+ `${check(msg.auth.spf)} SPF`,
345
+ `${check(msg.auth.dkim)} DKIM`,
346
+ `${check(msg.auth.dmarc)} DMARC`,
347
+ ]
348
+ const label = msg.auth.authentic ? pc.green('authentic') : pc.red('UNVERIFIED')
349
+ console.log(`Auth: ${parts.join(' ')} (${label})`)
350
+ if (options.verify) {
351
+ console.log(pc.dim(` Raw: ${msg.auth.raw}`))
352
+ }
353
+ }
354
+
326
355
  if (msg.attachments.length > 0) {
327
356
  const attList = msg.attachments.map((a) => {
328
357
  const size = a.size < 1024 ? `${a.size} B`
@@ -414,6 +443,7 @@ export function registerMailCommands(cli: Goke) {
414
443
  fromEmail: options.from,
415
444
  attachments,
416
445
  })
446
+ if (result instanceof Error) handleCommandError(result)
417
447
 
418
448
  out.printYaml(result)
419
449
  out.success(`Sent to ${options.to}`)
@@ -5,24 +5,30 @@
5
5
 
6
6
  import type { Goke } from 'goke'
7
7
  import { getClients } from '../auth.js'
8
+ import type { GmailClient } from '../gmail-client.js'
8
9
  import { AuthError } from '../api-utils.js'
9
10
  import * as out from '../output.js'
10
11
 
11
12
  export function registerProfileCommands(cli: Goke) {
12
13
  cli
13
- .command('profile', 'Show Gmail account info')
14
+ .command('profile', 'Show account info')
14
15
  .action(async (options) => {
15
16
  const clients = await getClients(options.account)
16
17
 
17
18
  // Fetch all accounts concurrently
18
19
  const allResults = await Promise.all(
19
- clients.map(async ({ client }) => {
20
+ clients.map(async ({ client, accountType }) => {
20
21
  const profile = await client.getProfile()
21
22
  if (profile instanceof Error) return profile
22
- // Always fetch aliases fresh
23
- const aliases = await client.getEmailAliases()
24
- if (aliases instanceof Error) return aliases
25
- return { profile, aliases }
23
+
24
+ if (accountType === 'google') {
25
+ // Google accounts have aliases
26
+ const aliases = await (client as GmailClient).getEmailAliases()
27
+ if (aliases instanceof Error) return aliases
28
+ return { profile, aliases, accountType }
29
+ }
30
+
31
+ return { profile, aliases: [{ email: profile.emailAddress, primary: true }], accountType }
26
32
  }),
27
33
  )
28
34
 
@@ -32,18 +38,22 @@ export function registerProfileCommands(cli: Goke) {
32
38
  return true
33
39
  })
34
40
 
35
- for (const { profile, aliases } of results) {
36
- out.printYaml({
41
+ for (const { profile, aliases, accountType } of results) {
42
+ const data: Record<string, unknown> = {
37
43
  email: profile.emailAddress,
38
- messages_total: profile.messagesTotal,
39
- threads_total: profile.threadsTotal,
40
- history_id: profile.historyId,
41
- aliases: aliases.map((a) => ({
42
- email: a.email,
43
- name: a.name ?? null,
44
- primary: a.primary,
45
- })),
46
- })
44
+ type: accountType,
45
+ }
46
+ if (accountType === 'google') {
47
+ data.messages_total = profile.messagesTotal
48
+ data.threads_total = profile.threadsTotal
49
+ data.history_id = profile.historyId
50
+ }
51
+ data.aliases = aliases.map((a) => ({
52
+ email: a.email,
53
+ name: a.name ?? null,
54
+ primary: a.primary,
55
+ }))
56
+ out.printYaml(data)
47
57
  }
48
58
  })
49
59
  }
package/src/db.ts CHANGED
@@ -58,6 +58,10 @@ async function initializePrisma(): Promise<PrismaClient> {
58
58
  // Run schema.sql — uses CREATE TABLE IF NOT EXISTS so it's idempotent
59
59
  await applySchema(prisma)
60
60
 
61
+ // Add new columns to existing Account tables (idempotent migration).
62
+ // CREATE TABLE IF NOT EXISTS doesn't add columns to pre-existing tables.
63
+ await migrateAccountColumns(prisma)
64
+
61
65
  // Secure database files (owner read/write only)
62
66
  secureDatabase()
63
67
 
@@ -93,6 +97,30 @@ async function applySchema(prisma: PrismaClient): Promise<void> {
93
97
  }
94
98
  }
95
99
 
100
+ /**
101
+ * Idempotent migration: add accountType and capabilities columns to Account
102
+ * if they don't already exist (for DBs created before IMAP/SMTP support).
103
+ * Also backfill existing Google accounts with their default capabilities.
104
+ */
105
+ async function migrateAccountColumns(prisma: PrismaClient): Promise<void> {
106
+ const cols = await prisma.$queryRawUnsafe<Array<{ name: string }>>(`PRAGMA table_info("Account")`)
107
+ const colNames = new Set(cols.map((c) => c.name))
108
+
109
+ if (!colNames.has('accountType')) {
110
+ await prisma.$executeRawUnsafe(`ALTER TABLE "Account" ADD COLUMN "accountType" TEXT NOT NULL DEFAULT 'google'`)
111
+ }
112
+ if (!colNames.has('capabilities')) {
113
+ await prisma.$executeRawUnsafe(`ALTER TABLE "Account" ADD COLUMN "capabilities" TEXT NOT NULL DEFAULT ''`)
114
+ }
115
+
116
+ // Backfill: existing Google accounts should have capabilities set
117
+ await prisma.$executeRawUnsafe(`
118
+ UPDATE "Account"
119
+ SET "capabilities" = 'gmail,calendar,smtp'
120
+ WHERE "accountType" = 'google' AND ("capabilities" = '' OR "capabilities" IS NULL)
121
+ `)
122
+ }
123
+
96
124
  /**
97
125
  * Set restrictive permissions on database files.
98
126
  * SQLite WAL mode creates additional -wal and -shm files that also need protection.
@@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
20
20
  "clientVersion": "7.3.0",
21
21
  "engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735",
22
22
  "activeProvider": "sqlite",
23
- "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"./src/generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\n// Lifecycle status for account credentials stored in `Account`.\nenum AccountStatus {\n active\n disabled\n}\n\n// Stores one OAuth credential set per (email, appId) pair.\n// appId is the Google OAuth client ID used during login, enabling\n// multiple OAuth apps per email (different quotas, scopes, etc.).\n// Default is the Thunderbird client ID (the original/only client).\nmodel Account {\n email String\n appId String\n accountStatus AccountStatus\n tokens String // JSON-encoded OAuth2 Credentials\n createdAt DateTime\n updatedAt DateTime @updatedAt\n\n threads Thread[]\n labels Label?\n profiles Profile?\n syncStates SyncState[]\n calendarLists CalendarList?\n\n @@id([email, appId])\n}\n\n// Caches hydrated thread payloads per account + thread ID.\n// Used by mail read and post-mutation cache invalidation.\n// rawData stores the raw Google gmail_v1.Schema$Thread response (format: full)\n// so the cache is resilient to changes in our own ThreadData type.\n// Indexed columns are extracted for queryability and display.\nmodel Thread {\n id Int @id @default(autoincrement())\n email String\n appId String\n threadId String\n subject String // extracted for display/search\n snippet String // extracted for display\n fromEmail String // extracted for filtering\n fromName String // extracted for display\n date String // extracted for sorting (RFC2822 from header)\n labelIds String // comma-separated, extracted for filtering\n hasUnread Boolean // extracted for filtering\n msgCount Int // extracted for display\n historyId String? // for sync\n rawData String // raw Google API response JSON (gmail_v1.Schema$Thread)\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@unique([email, appId, threadId])\n}\n\n// Caches label metadata per account (label id/name/type payload).\n// Used by label list/get and related command outputs.\n// rawData stores the raw Google gmail_v1.Schema$Label[] response.\nmodel Label {\n email String\n appId String\n rawData String // raw Google API response JSON (gmail_v1.Schema$Label[])\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId])\n}\n\n// Caches Gmail profile payload per account (totals/history id).\n// Used by profile command and account metadata lookups.\n// Fully flattened — no JSON blob needed, only 4 fields from Google.\nmodel Profile {\n email String\n appId String\n emailAddress String // from Gmail API\n messagesTotal Int // from Gmail API\n threadsTotal Int // from Gmail API\n historyId String // from Gmail API\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId])\n}\n\n// Caches calendar list per account.\n// Used by cal list to avoid fetching calendar metadata on every invocation.\n// rawData stores parsed CalendarListItem[] JSON (not raw tsdav — parsed at write time).\nmodel CalendarList {\n email String\n appId String\n rawData String // JSON blob of CalendarListItem[]\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId])\n}\n\n// Stores persistent per-account sync metadata as generic key/value pairs.\n// Use this for lightweight sync cursors and markers that are not cached API\n// payloads, for example `history_id` (incremental Gmail history cursor),\n// `last_full_sync_at`, or other small account-scoped checkpoints.\nmodel SyncState {\n email String\n appId String\n key String\n value String\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId, key])\n}\n",
23
+ "inlineSchema": "generator client {\n provider = \"prisma-client\"\n output = \"./src/generated\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\n// Lifecycle status for account credentials stored in `Account`.\nenum AccountStatus {\n active\n disabled\n}\n\n// Stores one credential set per (email, appId) pair.\n// appId is the Google OAuth client ID for Google accounts, or 'imap_smtp' for IMAP/SMTP accounts.\n// accountType discriminates the credential format stored in tokens.\n// capabilities is a comma-separated list of features: \"gmail,calendar,smtp\" or \"imap,smtp\".\nmodel Account {\n email String\n appId String\n accountType String @default(\"google\") // \"google\" | \"imap_smtp\"\n capabilities String @default(\"\") // comma-separated: \"gmail,calendar,smtp\" or \"imap,smtp\" or \"imap\"\n accountStatus AccountStatus\n tokens String // JSON: OAuth2 Credentials (google) or ImapSmtpCredentials (imap_smtp)\n createdAt DateTime\n updatedAt DateTime @updatedAt\n\n threads Thread[]\n labels Label?\n profiles Profile?\n syncStates SyncState[]\n calendarLists CalendarList?\n\n @@id([email, appId])\n}\n\n// Caches hydrated thread payloads per account + thread ID.\n// Used by mail read and post-mutation cache invalidation.\n// rawData stores the raw Google gmail_v1.Schema$Thread response (format: full)\n// so the cache is resilient to changes in our own ThreadData type.\n// Indexed columns are extracted for queryability and display.\nmodel Thread {\n id Int @id @default(autoincrement())\n email String\n appId String\n threadId String\n subject String // extracted for display/search\n snippet String // extracted for display\n fromEmail String // extracted for filtering\n fromName String // extracted for display\n date String // extracted for sorting (RFC2822 from header)\n labelIds String // comma-separated, extracted for filtering\n hasUnread Boolean // extracted for filtering\n msgCount Int // extracted for display\n historyId String? // for sync\n rawData String // raw Google API response JSON (gmail_v1.Schema$Thread)\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@unique([email, appId, threadId])\n}\n\n// Caches label metadata per account (label id/name/type payload).\n// Used by label list/get and related command outputs.\n// rawData stores the raw Google gmail_v1.Schema$Label[] response.\nmodel Label {\n email String\n appId String\n rawData String // raw Google API response JSON (gmail_v1.Schema$Label[])\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId])\n}\n\n// Caches Gmail profile payload per account (totals/history id).\n// Used by profile command and account metadata lookups.\n// Fully flattened — no JSON blob needed, only 4 fields from Google.\nmodel Profile {\n email String\n appId String\n emailAddress String // from Gmail API\n messagesTotal Int // from Gmail API\n threadsTotal Int // from Gmail API\n historyId String // from Gmail API\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId])\n}\n\n// Caches calendar list per account.\n// Used by cal list to avoid fetching calendar metadata on every invocation.\n// rawData stores parsed CalendarListItem[] JSON (not raw tsdav — parsed at write time).\nmodel CalendarList {\n email String\n appId String\n rawData String // JSON blob of CalendarListItem[]\n ttlMs Int\n createdAt DateTime\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId])\n}\n\n// Stores persistent per-account sync metadata as generic key/value pairs.\n// Use this for lightweight sync cursors and markers that are not cached API\n// payloads, for example `history_id` (incremental Gmail history cursor),\n// `last_full_sync_at`, or other small account-scoped checkpoints.\nmodel SyncState {\n email String\n appId String\n key String\n value String\n\n account Account @relation(fields: [email, appId], references: [email, appId], onDelete: Cascade)\n\n @@id([email, appId, key])\n}\n",
24
24
  "runtimeDataModel": {
25
25
  "models": {},
26
26
  "enums": {},
@@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = {
28
28
  }
29
29
  }
30
30
 
31
- config.runtimeDataModel = JSON.parse("{\"models\":{\"Account\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountStatus\",\"kind\":\"enum\",\"type\":\"AccountStatus\"},{\"name\":\"tokens\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"threads\",\"kind\":\"object\",\"type\":\"Thread\",\"relationName\":\"AccountToThread\"},{\"name\":\"labels\",\"kind\":\"object\",\"type\":\"Label\",\"relationName\":\"AccountToLabel\"},{\"name\":\"profiles\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"AccountToProfile\"},{\"name\":\"syncStates\",\"kind\":\"object\",\"type\":\"SyncState\",\"relationName\":\"AccountToSyncState\"},{\"name\":\"calendarLists\",\"kind\":\"object\",\"type\":\"CalendarList\",\"relationName\":\"AccountToCalendarList\"}],\"dbName\":null},\"Thread\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"threadId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"snippet\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fromEmail\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fromName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"date\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"labelIds\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hasUnread\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"msgCount\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"historyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rawData\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToThread\"}],\"dbName\":null},\"Label\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rawData\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToLabel\"}],\"dbName\":null},\"Profile\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"messagesTotal\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"threadsTotal\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"historyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToProfile\"}],\"dbName\":null},\"CalendarList\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rawData\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToCalendarList\"}],\"dbName\":null},\"SyncState\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToSyncState\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
31
+ config.runtimeDataModel = JSON.parse("{\"models\":{\"Account\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"capabilities\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"accountStatus\",\"kind\":\"enum\",\"type\":\"AccountStatus\"},{\"name\":\"tokens\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"threads\",\"kind\":\"object\",\"type\":\"Thread\",\"relationName\":\"AccountToThread\"},{\"name\":\"labels\",\"kind\":\"object\",\"type\":\"Label\",\"relationName\":\"AccountToLabel\"},{\"name\":\"profiles\",\"kind\":\"object\",\"type\":\"Profile\",\"relationName\":\"AccountToProfile\"},{\"name\":\"syncStates\",\"kind\":\"object\",\"type\":\"SyncState\",\"relationName\":\"AccountToSyncState\"},{\"name\":\"calendarLists\",\"kind\":\"object\",\"type\":\"CalendarList\",\"relationName\":\"AccountToCalendarList\"}],\"dbName\":null},\"Thread\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"threadId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"snippet\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fromEmail\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"fromName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"date\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"labelIds\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hasUnread\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"msgCount\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"historyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rawData\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToThread\"}],\"dbName\":null},\"Label\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rawData\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToLabel\"}],\"dbName\":null},\"Profile\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"emailAddress\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"messagesTotal\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"threadsTotal\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"historyId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToProfile\"}],\"dbName\":null},\"CalendarList\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"rawData\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"ttlMs\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToCalendarList\"}],\"dbName\":null},\"SyncState\":{\"fields\":[{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"account\",\"kind\":\"object\",\"type\":\"Account\",\"relationName\":\"AccountToSyncState\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
32
32
 
33
33
  async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
34
34
  const { Buffer } = await import('node:buffer')
@@ -892,6 +892,8 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
892
892
  export const AccountScalarFieldEnum = {
893
893
  email: 'email',
894
894
  appId: 'appId',
895
+ accountType: 'accountType',
896
+ capabilities: 'capabilities',
895
897
  accountStatus: 'accountStatus',
896
898
  tokens: 'tokens',
897
899
  createdAt: 'createdAt',
@@ -75,6 +75,8 @@ export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof
75
75
  export const AccountScalarFieldEnum = {
76
76
  email: 'email',
77
77
  appId: 'appId',
78
+ accountType: 'accountType',
79
+ capabilities: 'capabilities',
78
80
  accountStatus: 'accountStatus',
79
81
  tokens: 'tokens',
80
82
  createdAt: 'createdAt',