zele 0.3.0 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +1 -1
  2. package/bin/zele +27 -0
  3. package/dist/api-utils.d.ts +51 -2
  4. package/dist/api-utils.js +89 -3
  5. package/dist/api-utils.js.map +1 -1
  6. package/dist/auth.d.ts +27 -6
  7. package/dist/auth.js +185 -129
  8. package/dist/auth.js.map +1 -1
  9. package/dist/calendar-client.d.ts +16 -9
  10. package/dist/calendar-client.js +163 -59
  11. package/dist/calendar-client.js.map +1 -1
  12. package/dist/cli.js +26 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/commands/attachment.js +17 -15
  15. package/dist/commands/attachment.js.map +1 -1
  16. package/dist/commands/auth-cmd.js +20 -9
  17. package/dist/commands/auth-cmd.js.map +1 -1
  18. package/dist/commands/calendar.js +67 -78
  19. package/dist/commands/calendar.js.map +1 -1
  20. package/dist/commands/draft.js +25 -18
  21. package/dist/commands/draft.js.map +1 -1
  22. package/dist/commands/label.js +33 -45
  23. package/dist/commands/label.js.map +1 -1
  24. package/dist/commands/mail-actions.js +11 -13
  25. package/dist/commands/mail-actions.js.map +1 -1
  26. package/dist/commands/mail.js +112 -126
  27. package/dist/commands/mail.js.map +1 -1
  28. package/dist/commands/profile.js +18 -21
  29. package/dist/commands/profile.js.map +1 -1
  30. package/dist/commands/watch.js +33 -261
  31. package/dist/commands/watch.js.map +1 -1
  32. package/dist/db.js +12 -13
  33. package/dist/db.js.map +1 -1
  34. package/dist/generated/browser.d.ts +12 -27
  35. package/dist/generated/client.d.ts +13 -28
  36. package/dist/generated/client.js +1 -1
  37. package/dist/generated/commonInputTypes.d.ts +90 -26
  38. package/dist/generated/enums.d.ts +0 -4
  39. package/dist/generated/enums.js +0 -3
  40. package/dist/generated/enums.js.map +1 -1
  41. package/dist/generated/internal/class.d.ts +22 -55
  42. package/dist/generated/internal/class.js +12 -4
  43. package/dist/generated/internal/class.js.map +1 -1
  44. package/dist/generated/internal/prismaNamespace.d.ts +272 -511
  45. package/dist/generated/internal/prismaNamespace.js +54 -66
  46. package/dist/generated/internal/prismaNamespace.js.map +1 -1
  47. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +60 -74
  48. package/dist/generated/internal/prismaNamespaceBrowser.js +50 -62
  49. package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
  50. package/dist/generated/models/Account.d.ts +1637 -0
  51. package/dist/generated/models/Account.js +2 -0
  52. package/dist/generated/models/Account.js.map +1 -0
  53. package/dist/generated/models/CalendarList.d.ts +1161 -0
  54. package/dist/generated/models/CalendarList.js +2 -0
  55. package/dist/generated/models/CalendarList.js.map +1 -0
  56. package/dist/generated/models/Label.d.ts +1161 -0
  57. package/dist/generated/models/Label.js +2 -0
  58. package/dist/generated/models/Label.js.map +1 -0
  59. package/dist/generated/models/Profile.d.ts +1269 -0
  60. package/dist/generated/models/Profile.js +2 -0
  61. package/dist/generated/models/Profile.js.map +1 -0
  62. package/dist/generated/models/SyncState.d.ts +1130 -0
  63. package/dist/generated/models/SyncState.js +2 -0
  64. package/dist/generated/models/SyncState.js.map +1 -0
  65. package/dist/generated/models/Thread.d.ts +1608 -0
  66. package/dist/generated/models/Thread.js +2 -0
  67. package/dist/generated/models/Thread.js.map +1 -0
  68. package/dist/generated/models.d.ts +6 -9
  69. package/dist/gmail-client.d.ts +119 -94
  70. package/dist/gmail-client.js +862 -322
  71. package/dist/gmail-client.js.map +1 -1
  72. package/dist/mail-tui.d.ts +1 -0
  73. package/dist/mail-tui.js +517 -0
  74. package/dist/mail-tui.js.map +1 -0
  75. package/dist/output.d.ts +6 -0
  76. package/dist/output.js +124 -11
  77. package/dist/output.js.map +1 -1
  78. package/package.json +39 -11
  79. package/schema.prisma +81 -113
  80. package/src/api-utils.ts +103 -5
  81. package/src/auth.ts +224 -143
  82. package/src/calendar-client.ts +196 -89
  83. package/src/cli.ts +30 -1
  84. package/src/commands/attachment.ts +18 -19
  85. package/src/commands/auth-cmd.ts +19 -9
  86. package/src/commands/calendar.ts +42 -85
  87. package/src/commands/draft.ts +19 -22
  88. package/src/commands/label.ts +21 -57
  89. package/src/commands/mail-actions.ts +11 -19
  90. package/src/commands/mail.ts +102 -147
  91. package/src/commands/profile.ts +12 -28
  92. package/src/commands/watch.ts +37 -304
  93. package/src/db.ts +13 -16
  94. package/src/generated/browser.ts +49 -0
  95. package/src/generated/client.ts +71 -0
  96. package/src/generated/commonInputTypes.ts +332 -0
  97. package/src/generated/enums.ts +17 -0
  98. package/src/generated/internal/class.ts +250 -0
  99. package/src/generated/internal/prismaNamespace.ts +1198 -0
  100. package/src/generated/internal/prismaNamespaceBrowser.ts +169 -0
  101. package/src/generated/models/Account.ts +1848 -0
  102. package/src/generated/models/CalendarList.ts +1331 -0
  103. package/src/generated/models/Label.ts +1331 -0
  104. package/src/generated/models/Profile.ts +1439 -0
  105. package/src/generated/models/SyncState.ts +1300 -0
  106. package/src/generated/models/Thread.ts +1787 -0
  107. package/src/generated/models.ts +17 -0
  108. package/src/gmail-client.test.ts +59 -0
  109. package/src/gmail-client.ts +1034 -429
  110. package/src/mail-tui.tsx +1061 -0
  111. package/src/output.test.ts +1093 -0
  112. package/src/output.ts +128 -13
  113. package/src/schema.sql +58 -68
  114. package/src/test-fixtures/email-html/safe-claude-event.html +28 -0
  115. package/src/test-fixtures/email-html/safe-product-announcement.html +25 -0
  116. package/src/test-fixtures/email-html/safe-tracked-links.html +27 -0
  117. package/src/test-fixtures/email-html-snapshots/safe-claude-event.html.md +9 -0
  118. package/src/test-fixtures/email-html-snapshots/safe-product-announcement.html.md +13 -0
  119. package/src/test-fixtures/email-html-snapshots/safe-tracked-links.html.md +7 -0
  120. package/AGENTS.md +0 -26
  121. package/CHANGELOG.md +0 -43
  122. package/dist/generated/models/accounts.d.ts +0 -2000
  123. package/dist/generated/models/accounts.js +0 -2
  124. package/dist/generated/models/accounts.js.map +0 -1
  125. package/dist/generated/models/calendar_events.d.ts +0 -1433
  126. package/dist/generated/models/calendar_events.js +0 -2
  127. package/dist/generated/models/calendar_events.js.map +0 -1
  128. package/dist/generated/models/calendar_lists.d.ts +0 -1131
  129. package/dist/generated/models/calendar_lists.js +0 -2
  130. package/dist/generated/models/calendar_lists.js.map +0 -1
  131. package/dist/generated/models/label_counts.d.ts +0 -1131
  132. package/dist/generated/models/label_counts.js +0 -2
  133. package/dist/generated/models/label_counts.js.map +0 -1
  134. package/dist/generated/models/labels.d.ts +0 -1131
  135. package/dist/generated/models/labels.js +0 -2
  136. package/dist/generated/models/labels.js.map +0 -1
  137. package/dist/generated/models/profiles.d.ts +0 -1131
  138. package/dist/generated/models/profiles.js +0 -2
  139. package/dist/generated/models/profiles.js.map +0 -1
  140. package/dist/generated/models/sync_states.d.ts +0 -1107
  141. package/dist/generated/models/sync_states.js +0 -2
  142. package/dist/generated/models/sync_states.js.map +0 -1
  143. package/dist/generated/models/thread_lists.d.ts +0 -1404
  144. package/dist/generated/models/thread_lists.js +0 -2
  145. package/dist/generated/models/thread_lists.js.map +0 -1
  146. package/dist/generated/models/threads.d.ts +0 -1247
  147. package/dist/generated/models/threads.js +0 -2
  148. package/dist/generated/models/threads.js.map +0 -1
  149. package/dist/gmail-cache.d.ts +0 -60
  150. package/dist/gmail-cache.js +0 -264
  151. package/dist/gmail-cache.js.map +0 -1
  152. package/docs/gogcli-gmail-implementation.md +0 -599
  153. package/scripts/test-device-code-clients.ts +0 -186
  154. package/scripts/test-micropython-scopes.ts +0 -72
  155. package/scripts/test-oauth-clients.ts +0 -257
  156. package/src/gmail-cache.ts +0 -339
  157. package/tsconfig.json +0 -16
@@ -1,30 +1,14 @@
1
1
  // Mail watch command: poll for new emails using Gmail History API.
2
- // Uses incremental historyId-based sync each tick fetches only changes
3
- // since the last known historyId, not the full inbox. On a quiet inbox
4
- // this is a single API call returning nothing.
2
+ // Thin CLI wrapper around GmailClient.watchInbox() async generator.
5
3
  // Multi-account: watches all accounts concurrently and merges output.
6
4
 
7
5
  import type { Goke } from 'goke'
8
6
  import { z } from 'zod'
9
7
  import { getClients } from '../auth.js'
10
- import type { GmailClient } from '../gmail-client.js'
11
- import { mapConcurrent } from '../api-utils.js'
12
- import * as cache from '../gmail-cache.js'
8
+ import type { WatchEvent } from '../gmail-client.js'
9
+ import { AuthError } from '../api-utils.js'
13
10
  import * as out from '../output.js'
14
11
 
15
- // ---------------------------------------------------------------------------
16
- // Folder label mapping (reuses mail list conventions)
17
- // ---------------------------------------------------------------------------
18
-
19
- const FOLDER_LABELS: Record<string, string> = {
20
- inbox: 'INBOX',
21
- sent: 'SENT',
22
- trash: 'TRASH',
23
- spam: 'SPAM',
24
- starred: 'STARRED',
25
- drafts: 'DRAFT',
26
- }
27
-
28
12
  // ---------------------------------------------------------------------------
29
13
  // Register commands
30
14
  // ---------------------------------------------------------------------------
@@ -44,312 +28,61 @@ export function registerWatchCommands(cli: Goke) {
44
28
  }
45
29
 
46
30
  const folder = options.folder ?? 'inbox'
47
- const filterLabelId = FOLDER_LABELS[folder]
48
-
49
- if (!filterLabelId) {
50
- out.error(`Unsupported folder for watch: "${folder}". Supported: ${Object.keys(FOLDER_LABELS).join(', ')}`)
51
- process.exit(1)
52
- }
53
-
54
31
  const clients = await getClients(options.account)
55
32
 
56
- // Seed historyId for each account
57
- const states = await Promise.all(
58
- clients.map(async ({ email, client }) => {
59
- let historyId = await cache.getLastHistoryId(email)
60
-
61
- if (!historyId) {
62
- const profile = await client.getProfile()
63
- historyId = profile.historyId
64
- await cache.setLastHistoryId(email, historyId)
65
- out.hint(`${email}: watching from now (historyId ${historyId})`)
66
- } else {
67
- out.hint(`${email}: resuming from historyId ${historyId}`)
68
- }
69
-
70
- return { email, client, historyId }
71
- }),
72
- )
73
-
74
33
  // Clean exit on SIGINT
75
- let running = true
76
34
  process.on('SIGINT', () => {
77
- running = false
78
35
  out.hint('Stopped watching')
79
36
  process.exit(0)
80
37
  })
81
38
 
82
39
  out.hint(`Polling every ${interval}s for ${folder} changes (Ctrl+C to stop)`)
83
40
 
84
- // Poll loop
85
- while (running) {
86
- const settled = await Promise.allSettled(
87
- states.map(async (state) => {
88
- try {
89
- return await pollAccount(state, filterLabelId, options.query)
90
- } catch (err: any) {
91
- // historyId expired — Google only keeps ~7 days
92
- if (isHistoryExpired(err)) {
93
- out.hint(`${state.email}: history expired, re-seeding...`)
94
- const profile = await state.client.getProfile()
95
- state.historyId = profile.historyId
96
- await cache.setLastHistoryId(state.email, state.historyId)
97
- // Retry once after reseed (important for --once mode)
98
- return await pollAccount(state, filterLabelId, options.query)
99
- }
100
- throw err
101
- }
102
- }),
103
- )
41
+ // Watch all accounts concurrently, print events as they arrive
42
+ const generators = clients.map(({ client }) =>
43
+ client.watchInbox({
44
+ folder,
45
+ intervalMs: interval * 1000,
46
+ query: options.query,
47
+ once: options.once,
48
+ }),
49
+ )
104
50
 
105
- const allItems: Array<Record<string, unknown>> = []
106
- for (const result of settled) {
107
- if (result.status === 'fulfilled' && result.value) {
108
- allItems.push(...result.value)
109
- } else if (result.status === 'rejected') {
110
- out.error(`${result.reason?.message ?? result.reason}`)
51
+ // Consume all generators concurrently, surface errors to the user
52
+ const settled = await Promise.allSettled(
53
+ generators.map(async (gen) => {
54
+ for await (const event of gen) {
55
+ out.printList([formatWatchEvent(event)])
111
56
  }
112
- }
57
+ }),
58
+ )
113
59
 
114
- if (allItems.length > 0) {
115
- out.printList(allItems)
60
+ for (const result of settled) {
61
+ if (result.status === 'rejected') {
62
+ const err = result.reason
63
+ if (err instanceof AuthError) {
64
+ out.error(`${err.message}. Try: zele login`)
65
+ } else {
66
+ out.error(`Watch failed: ${err instanceof Error ? err.message : String(err)}`)
67
+ }
116
68
  }
117
-
118
- if (options.once) break
119
-
120
- await sleep(interval * 1000)
121
69
  }
122
70
  })
123
71
  }
124
72
 
125
- // ---------------------------------------------------------------------------
126
- // Poll a single account for changes
127
- // ---------------------------------------------------------------------------
128
-
129
- async function pollAccount(
130
- state: { email: string; client: GmailClient; historyId: string },
131
- filterLabelId: string,
132
- query: string | undefined,
133
- ): Promise<Array<Record<string, unknown>>> {
134
- const { history, historyId: newHistoryId } = await state.client.listHistory({
135
- startHistoryId: state.historyId,
136
- labelId: filterLabelId,
137
- historyTypes: ['messageAdded'],
138
- })
139
-
140
- // Update stored historyId even if no changes
141
- if (newHistoryId !== state.historyId) {
142
- state.historyId = newHistoryId
143
- await cache.setLastHistoryId(state.email, newHistoryId)
144
- }
145
-
146
- if (history.length === 0) return []
147
-
148
- // Collect unique message IDs from messageAdded events.
149
- // No client-side label filtering here — listHistory already filters by
150
- // labelId server-side, and the partial message objects in history responses
151
- // often have incomplete/missing labelIds.
152
- const seenIds = new Set<string>()
153
- const messageIds: string[] = []
154
-
155
- for (const entry of history) {
156
- for (const added of entry.messagesAdded ?? []) {
157
- const id = added.message?.id
158
- if (id && !seenIds.has(id)) {
159
- seenIds.add(id)
160
- messageIds.push(id)
161
- }
162
- }
163
- }
164
-
165
- if (messageIds.length === 0) return []
166
-
167
- // Hydrate messages with metadata (bounded concurrency)
168
- const hydrated = await mapConcurrent(messageIds, async (msgId) => {
169
- try {
170
- const msg = await state.client.getMessage({ messageId: msgId, format: 'metadata' })
171
- if ('raw' in msg) return null
172
-
173
- // If user specified a query, do a client-side check on subject/from
174
- // (the history API doesn't support query filtering natively)
175
- if (query && !matchesQuery(msg, query)) return null
176
-
177
- return {
178
- account: state.email,
179
- type: 'new_message',
180
- from: out.formatSender(msg.from),
181
- subject: msg.subject,
182
- date: out.formatDate(msg.date),
183
- thread_id: msg.threadId,
184
- message_id: msg.id,
185
- flags: out.formatFlags(msg),
186
- }
187
- } catch (err: any) {
188
- const status = err?.code ?? err?.status ?? err?.response?.status
189
- if (status === 404) return null // message deleted between history fetch and hydration
190
- out.hint(`Failed to fetch message ${msgId}: ${err.message ?? err}`)
191
- return null
192
- }
193
- })
194
-
195
- return hydrated.filter((item) => item !== null)
196
- }
197
-
198
73
  // ---------------------------------------------------------------------------
199
74
  // Helpers
200
75
  // ---------------------------------------------------------------------------
201
76
 
202
- function isHistoryExpired(err: any): boolean {
203
- const status = err?.code ?? err?.status ?? err?.response?.status
204
- if (status === 404) return true
205
- // Google sometimes returns 400 with "Invalid historyId"
206
- if (status === 400) {
207
- const message = err?.message ?? err?.response?.data?.error?.message ?? ''
208
- if (message.includes('historyId')) return true
209
- }
210
- return false
211
- }
212
-
213
- // ---------------------------------------------------------------------------
214
- // Client-side Gmail query matching
215
- // ---------------------------------------------------------------------------
216
- // The History API doesn't support server-side query filtering, so we parse
217
- // common Gmail search operators and match against message metadata.
218
- //
219
- // Supported operators: from:, to:, cc:, subject:, is:unread/read/starred,
220
- // has:attachment, and plain text (matches subject + from).
221
- // Multiple terms are AND-ed together. Quoted phrases and negation supported.
222
- //
223
- // Limitations vs full Gmail search:
224
- // - label: not supported (labelIds are API IDs like Label_123, not names)
225
- // - has:attachment uses Content-Type heuristic (metadata format lacks parts)
226
- // - OR, {}, newer_than:, older_than:, etc. are server-only — warned & skipped
227
- //
228
- // See https://support.google.com/mail/answer/7190 for the full Gmail spec.
229
- // ---------------------------------------------------------------------------
230
-
231
- // Operators that only work server-side in Gmail search — we skip these
232
- // with a warning rather than silently treating them as plain text.
233
- const SERVER_ONLY_OPERATORS = new Set([
234
- 'in', 'label', 'after', 'before', 'newer_than', 'older_than',
235
- 'filename', 'size', 'larger', 'smaller', 'deliveredto', 'rfc822msgid',
236
- 'list', 'category',
237
- ])
238
-
239
- // Set of operators we support client-side
240
- const SUPPORTED_OPERATORS = new Set([
241
- 'from', 'to', 'cc', 'subject', 'is', 'has',
242
- ])
243
-
244
- interface MatchableMessage {
245
- subject: string
246
- from: { name?: string; email: string }
247
- to: Array<{ name?: string; email: string }>
248
- cc: Array<{ name?: string; email: string }> | null
249
- labelIds: string[]
250
- unread: boolean
251
- starred: boolean
252
- mimeType: string // Content-Type of the message — used for attachment heuristic
253
- }
254
-
255
- let warnedOperators = new Set<string>()
256
-
257
- function matchesQuery(msg: MatchableMessage, query: string): boolean {
258
- const terms = parseQueryTerms(query)
259
- return terms.every((term) => matchesTerm(msg, term))
260
- }
261
-
262
- interface QueryTerm {
263
- operator: string | null // null = plain text, otherwise from/to/cc/subject/is/has
264
- value: string
265
- negated: boolean
266
- }
267
-
268
- function parseQueryTerms(query: string): QueryTerm[] {
269
- const terms: QueryTerm[] = []
270
- // Match: optional -, optional operator:, then either "quoted phrase" or non-space word
271
- const regex = /(-?)(?:(\w+):)?(?:"([^"]*)"|([\S]+))/gi
272
- let match: RegExpExecArray | null
273
-
274
- while ((match = regex.exec(query)) !== null) {
275
- const negated = match[1] === '-'
276
- const rawOperator = match[2]?.toLowerCase() ?? null
277
- const value = (match[3] ?? match[4] ?? '').toLowerCase()
278
- if (!value) continue
279
-
280
- // Skip OR keyword — it's a Gmail boolean operator, not a search term
281
- if (!rawOperator && value === 'or') continue
282
-
283
- // Warn once and skip server-only operators
284
- if (rawOperator && SERVER_ONLY_OPERATORS.has(rawOperator)) {
285
- if (!warnedOperators.has(rawOperator)) {
286
- warnedOperators.add(rawOperator)
287
- out.hint(`--query: "${rawOperator}:" is a server-only operator (use "mail search" instead), skipping`)
288
- }
289
- continue
290
- }
291
-
292
- // Warn once and skip completely unknown operators
293
- if (rawOperator && !SUPPORTED_OPERATORS.has(rawOperator)) {
294
- if (!warnedOperators.has(rawOperator)) {
295
- warnedOperators.add(rawOperator)
296
- out.hint(`--query: unknown operator "${rawOperator}:", skipping`)
297
- }
298
- continue
299
- }
300
-
301
- terms.push({ operator: rawOperator, value, negated })
77
+ function formatWatchEvent(event: WatchEvent): Record<string, unknown> {
78
+ return {
79
+ account: event.account.email,
80
+ type: event.type,
81
+ from: out.formatSender(event.message.from),
82
+ subject: event.message.subject,
83
+ date: out.formatDate(event.message.date),
84
+ thread_id: event.threadId,
85
+ message_id: event.message.id,
86
+ flags: out.formatFlags(event.message),
302
87
  }
303
-
304
- return terms
305
- }
306
-
307
- function senderMatches(sender: { name?: string; email: string }, value: string): boolean {
308
- const full = `${sender.name ?? ''} ${sender.email}`.toLowerCase()
309
- return full.includes(value)
310
- }
311
-
312
- function matchesTerm(msg: MatchableMessage, term: QueryTerm): boolean {
313
- let result: boolean
314
-
315
- switch (term.operator) {
316
- case 'from':
317
- result = senderMatches(msg.from, term.value)
318
- break
319
- case 'to':
320
- result = msg.to.some((r) => senderMatches(r, term.value))
321
- break
322
- case 'cc':
323
- result = (msg.cc ?? []).some((r) => senderMatches(r, term.value))
324
- break
325
- case 'subject':
326
- result = msg.subject.toLowerCase().includes(term.value)
327
- break
328
- case 'is':
329
- if (term.value === 'unread') result = msg.unread
330
- else if (term.value === 'read') result = !msg.unread
331
- else if (term.value === 'starred') result = msg.starred
332
- else result = false
333
- break
334
- case 'has':
335
- // In metadata format, payload.parts is absent so we can't inspect
336
- // attachments directly. Use Content-Type heuristic: multipart/mixed
337
- // almost always indicates attachments.
338
- if (term.value === 'attachment') result = msg.mimeType.includes('multipart/mixed')
339
- else result = false
340
- break
341
- default: {
342
- // Plain text: match against subject + from
343
- const subject = msg.subject.toLowerCase()
344
- const from = `${msg.from.name ?? ''} ${msg.from.email}`.toLowerCase()
345
- result = subject.includes(term.value) || from.includes(term.value)
346
- break
347
- }
348
- }
349
-
350
- return term.negated ? !result : result
351
- }
352
-
353
- function sleep(ms: number): Promise<void> {
354
- return new Promise((resolve) => setTimeout(resolve, ms))
355
88
  }
package/src/db.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Prisma singleton for zele.
2
- // Manages a single SQLite database at ~/.zele/zele.db for all state:
2
+ // Manages a single SQLite database at ~/.zele/sqlite.db for all state:
3
3
  // accounts (OAuth tokens), cache (threads, labels, profiles), and sync state.
4
- // Runs idempotent schema migration on every startup using src/schema.sql.
4
+ // Runs idempotent schema setup on every startup using src/schema.sql.
5
5
 
6
6
  import fs from 'node:fs'
7
7
  import path from 'node:path'
@@ -16,7 +16,7 @@ const __filename = fileURLToPath(import.meta.url)
16
16
  const __dirname = path.dirname(__filename)
17
17
 
18
18
  const ZELE_DIR = path.join(os.homedir(), '.zele')
19
- const DB_PATH = path.join(ZELE_DIR, 'zele.db')
19
+ const DB_PATH = path.join(ZELE_DIR, 'sqlite.db')
20
20
 
21
21
  let prismaInstance: PrismaClient | null = null
22
22
  let initPromise: Promise<PrismaClient> | null = null
@@ -44,14 +44,21 @@ async function initializePrisma(): Promise<PrismaClient> {
44
44
  const adapter = new PrismaLibSql({ url: `file:${DB_PATH}` })
45
45
  const prisma = new PrismaClient({ adapter })
46
46
 
47
- // Always run migrations schema.sql uses IF NOT EXISTS so it's idempotent
48
- await migrateSchema(prisma)
47
+ // WAL mode: allows concurrent readers + single writer, persists on the DB file.
48
+ // busy_timeout: wait up to 5s for locks to clear instead of failing instantly.
49
+ // Prevents "database is locked" errors when multiple processes (TUI, watch, CLI)
50
+ // access the DB, or after macOS sleep/wake leaves stale locks.
51
+ await prisma.$executeRawUnsafe('PRAGMA journal_mode = WAL')
52
+ await prisma.$executeRawUnsafe('PRAGMA busy_timeout = 5000')
53
+
54
+ // Run schema.sql — uses CREATE TABLE IF NOT EXISTS so it's idempotent
55
+ await applySchema(prisma)
49
56
 
50
57
  prismaInstance = prisma
51
58
  return prisma
52
59
  }
53
60
 
54
- async function migrateSchema(prisma: PrismaClient): Promise<void> {
61
+ async function applySchema(prisma: PrismaClient): Promise<void> {
55
62
  // When running from source (tsx), __dirname is src/
56
63
  // When running from dist, __dirname is dist/ and schema.sql is at ../src/schema.sql
57
64
  let schemaPath = path.join(__dirname, 'schema.sql')
@@ -74,16 +81,6 @@ async function migrateSchema(prisma: PrismaClient): Promise<void> {
74
81
  .map((s) => s.replace(/^CREATE\s+UNIQUE\s+INDEX\b(?!\s+IF)/i, 'CREATE UNIQUE INDEX IF NOT EXISTS')
75
82
  .replace(/^CREATE\s+INDEX\b(?!\s+IF)/i, 'CREATE INDEX IF NOT EXISTS'))
76
83
 
77
- // Compatibility migration: older DBs may not have account_status yet.
78
- try {
79
- await prisma.$executeRawUnsafe(
80
- "ALTER TABLE \"accounts\" ADD COLUMN \"account_status\" TEXT NOT NULL DEFAULT 'active'",
81
- )
82
- } catch {
83
- // Column already exists.
84
- }
85
-
86
-
87
84
  for (const statement of statements) {
88
85
  await prisma.$executeRawUnsafe(statement)
89
86
  }
@@ -0,0 +1,49 @@
1
+
2
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
3
+ /* eslint-disable */
4
+ // biome-ignore-all lint: generated file
5
+ // @ts-nocheck
6
+ /*
7
+ * This file should be your main import to use Prisma-related types and utilities in a browser.
8
+ * Use it to get access to models, enums, and input types.
9
+ *
10
+ * This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
11
+ * See `client.ts` for the standard, server-side entry point.
12
+ *
13
+ * 🟢 You can import this file directly.
14
+ */
15
+
16
+ import * as Prisma from './internal/prismaNamespaceBrowser.js'
17
+ export { Prisma }
18
+ export * as $Enums from './enums.js'
19
+ export * from './enums.js';
20
+ /**
21
+ * Model Account
22
+ *
23
+ */
24
+ export type Account = Prisma.AccountModel
25
+ /**
26
+ * Model Thread
27
+ *
28
+ */
29
+ export type Thread = Prisma.ThreadModel
30
+ /**
31
+ * Model Label
32
+ *
33
+ */
34
+ export type Label = Prisma.LabelModel
35
+ /**
36
+ * Model Profile
37
+ *
38
+ */
39
+ export type Profile = Prisma.ProfileModel
40
+ /**
41
+ * Model CalendarList
42
+ *
43
+ */
44
+ export type CalendarList = Prisma.CalendarListModel
45
+ /**
46
+ * Model SyncState
47
+ *
48
+ */
49
+ export type SyncState = Prisma.SyncStateModel
@@ -0,0 +1,71 @@
1
+
2
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
3
+ /* eslint-disable */
4
+ // biome-ignore-all lint: generated file
5
+ // @ts-nocheck
6
+ /*
7
+ * This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
8
+ * If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
9
+ *
10
+ * 🟢 You can import this file directly.
11
+ */
12
+
13
+ import * as process from 'node:process'
14
+ import * as path from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
17
+
18
+ import * as runtime from "@prisma/client/runtime/client"
19
+ import * as $Enums from "./enums.js"
20
+ import * as $Class from "./internal/class.js"
21
+ import * as Prisma from "./internal/prismaNamespace.js"
22
+
23
+ export * as $Enums from './enums.js'
24
+ export * from "./enums.js"
25
+ /**
26
+ * ## Prisma Client
27
+ *
28
+ * Type-safe database client for TypeScript
29
+ * @example
30
+ * ```
31
+ * const prisma = new PrismaClient()
32
+ * // Fetch zero or more Accounts
33
+ * const accounts = await prisma.account.findMany()
34
+ * ```
35
+ *
36
+ * Read more in our [docs](https://pris.ly/d/client).
37
+ */
38
+ export const PrismaClient = $Class.getPrismaClientClass()
39
+ export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
40
+ export { Prisma }
41
+
42
+ /**
43
+ * Model Account
44
+ *
45
+ */
46
+ export type Account = Prisma.AccountModel
47
+ /**
48
+ * Model Thread
49
+ *
50
+ */
51
+ export type Thread = Prisma.ThreadModel
52
+ /**
53
+ * Model Label
54
+ *
55
+ */
56
+ export type Label = Prisma.LabelModel
57
+ /**
58
+ * Model Profile
59
+ *
60
+ */
61
+ export type Profile = Prisma.ProfileModel
62
+ /**
63
+ * Model CalendarList
64
+ *
65
+ */
66
+ export type CalendarList = Prisma.CalendarListModel
67
+ /**
68
+ * Model SyncState
69
+ *
70
+ */
71
+ export type SyncState = Prisma.SyncStateModel