zele 0.3.15 → 0.3.16
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.
- package/README.md +25 -0
- package/dist/api-utils.d.ts +3 -0
- package/dist/api-utils.js +6 -0
- package/dist/api-utils.js.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/calendar-time.js +6 -0
- package/dist/calendar-time.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth-cmd.js +1 -1
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/calendar.js +1 -1
- package/dist/commands/calendar.js.map +1 -1
- package/dist/commands/draft.js +2 -2
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/filter.d.ts +2 -0
- package/dist/commands/filter.js +59 -0
- package/dist/commands/filter.js.map +1 -0
- package/dist/commands/mail-actions.js +12 -2
- package/dist/commands/mail-actions.js.map +1 -1
- package/dist/commands/mail.js +176 -93
- package/dist/commands/mail.js.map +1 -1
- package/dist/gmail-client.d.ts +28 -0
- package/dist/gmail-client.js +129 -9
- package/dist/gmail-client.js.map +1 -1
- package/dist/mail-tui.js +3 -2
- package/dist/mail-tui.js.map +1 -1
- package/dist/output.d.ts +2 -0
- package/dist/output.js +4 -0
- package/dist/output.js.map +1 -1
- package/package.json +5 -4
- package/skills/zele/SKILL.md +112 -0
- package/src/api-utils.ts +7 -0
- package/src/auth.ts +1 -1
- package/src/calendar-time.test.ts +35 -0
- package/src/calendar-time.ts +5 -0
- package/src/cli.ts +6 -1
- package/src/commands/auth-cmd.ts +1 -1
- package/src/commands/calendar.ts +1 -1
- package/src/commands/draft.ts +2 -2
- package/src/commands/filter.ts +68 -0
- package/src/commands/mail-actions.ts +14 -2
- package/src/commands/mail.ts +186 -98
- package/src/gmail-client.ts +156 -16
- package/src/mail-tui.tsx +1 -2
- package/src/output.ts +8 -1
package/src/gmail-client.ts
CHANGED
|
@@ -78,10 +78,16 @@ export interface ThreadListItem {
|
|
|
78
78
|
snippet: string
|
|
79
79
|
subject: string
|
|
80
80
|
from: Sender
|
|
81
|
+
to: Sender[]
|
|
82
|
+
cc: Sender[]
|
|
81
83
|
date: string
|
|
82
84
|
labelIds: string[]
|
|
83
85
|
unread: boolean
|
|
86
|
+
starred: boolean
|
|
84
87
|
messageCount: number
|
|
88
|
+
inReplyTo: string | null
|
|
89
|
+
hasAttachments: boolean
|
|
90
|
+
listUnsubscribe: string | null
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
export interface ThreadListResult {
|
|
@@ -806,7 +812,8 @@ export class GmailClient {
|
|
|
806
812
|
)
|
|
807
813
|
if (messageIds instanceof Error) return messageIds
|
|
808
814
|
if (messageIds.length === 0) return
|
|
809
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['UNREAD'] })
|
|
815
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['UNREAD'] })
|
|
816
|
+
if (mod instanceof Error) return mod
|
|
810
817
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
811
818
|
}
|
|
812
819
|
|
|
@@ -816,7 +823,8 @@ export class GmailClient {
|
|
|
816
823
|
)
|
|
817
824
|
if (messageIds instanceof Error) return messageIds
|
|
818
825
|
if (messageIds.length === 0) return
|
|
819
|
-
await this.batchModifyMessages(messageIds, { addLabelIds: ['UNREAD'] })
|
|
826
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['UNREAD'] })
|
|
827
|
+
if (mod instanceof Error) return mod
|
|
820
828
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
821
829
|
}
|
|
822
830
|
|
|
@@ -824,7 +832,8 @@ export class GmailClient {
|
|
|
824
832
|
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
825
833
|
if (messageIds instanceof Error) return messageIds
|
|
826
834
|
if (messageIds.length === 0) return
|
|
827
|
-
await this.batchModifyMessages(messageIds, { addLabelIds: ['STARRED'] })
|
|
835
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['STARRED'] })
|
|
836
|
+
if (mod instanceof Error) return mod
|
|
828
837
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
829
838
|
}
|
|
830
839
|
|
|
@@ -834,7 +843,8 @@ export class GmailClient {
|
|
|
834
843
|
)
|
|
835
844
|
if (messageIds instanceof Error) return messageIds
|
|
836
845
|
if (messageIds.length === 0) return
|
|
837
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['STARRED'] })
|
|
846
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['STARRED'] })
|
|
847
|
+
if (mod instanceof Error) return mod
|
|
838
848
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
839
849
|
}
|
|
840
850
|
|
|
@@ -861,10 +871,11 @@ export class GmailClient {
|
|
|
861
871
|
if (messageIds instanceof Error) return messageIds
|
|
862
872
|
if (messageIds.length === 0) return
|
|
863
873
|
|
|
864
|
-
await this.batchModifyMessages(messageIds, {
|
|
874
|
+
const mod = await this.batchModifyMessages(messageIds, {
|
|
865
875
|
addLabelIds: resolvedAdd.filter((r): r is string => typeof r === 'string'),
|
|
866
876
|
removeLabelIds: resolvedRemove,
|
|
867
877
|
})
|
|
878
|
+
if (mod instanceof Error) return mod
|
|
868
879
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
869
880
|
}
|
|
870
881
|
|
|
@@ -892,7 +903,28 @@ export class GmailClient {
|
|
|
892
903
|
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
893
904
|
if (messageIds instanceof Error) return messageIds
|
|
894
905
|
if (messageIds.length === 0) return
|
|
895
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['INBOX'] })
|
|
906
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['INBOX'] })
|
|
907
|
+
if (mod instanceof Error) return mod
|
|
908
|
+
await this.invalidateAfterThreadMutation(threadIds)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async markAsSpam({ threadIds }: { threadIds: string[] }): Promise<void | AuthError | ApiError> {
|
|
912
|
+
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
913
|
+
if (messageIds instanceof Error) return messageIds
|
|
914
|
+
if (messageIds.length === 0) return
|
|
915
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['SPAM'], removeLabelIds: ['INBOX'] })
|
|
916
|
+
if (mod instanceof Error) return mod
|
|
917
|
+
await this.invalidateAfterThreadMutation(threadIds)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async unmarkSpam({ threadIds }: { threadIds: string[] }): Promise<void | AuthError | ApiError> {
|
|
921
|
+
const messageIds = await this.getMessageIdsForThreads(threadIds, (labelIds) =>
|
|
922
|
+
labelIds.includes('SPAM'),
|
|
923
|
+
)
|
|
924
|
+
if (messageIds instanceof Error) return messageIds
|
|
925
|
+
if (messageIds.length === 0) return
|
|
926
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['SPAM'], addLabelIds: ['INBOX'] })
|
|
927
|
+
if (mod instanceof Error) return mod
|
|
896
928
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
897
929
|
}
|
|
898
930
|
|
|
@@ -919,10 +951,11 @@ export class GmailClient {
|
|
|
919
951
|
const threadIds = res.threads.map((t) => t.id)
|
|
920
952
|
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
921
953
|
if (messageIds instanceof Error) return messageIds
|
|
922
|
-
await this.batchModifyMessages(messageIds, {
|
|
954
|
+
const mod = await this.batchModifyMessages(messageIds, {
|
|
923
955
|
addLabelIds: ['TRASH'],
|
|
924
956
|
removeLabelIds: ['SPAM', 'INBOX'],
|
|
925
957
|
})
|
|
958
|
+
if (mod instanceof Error) return mod
|
|
926
959
|
|
|
927
960
|
totalDeleted += threadIds.length
|
|
928
961
|
pageToken = res.nextPageToken ?? undefined
|
|
@@ -1021,6 +1054,61 @@ export class GmailClient {
|
|
|
1021
1054
|
await this.invalidateLabels()
|
|
1022
1055
|
}
|
|
1023
1056
|
|
|
1057
|
+
// =========================================================================
|
|
1058
|
+
// Filters
|
|
1059
|
+
// =========================================================================
|
|
1060
|
+
|
|
1061
|
+
async listFilters(): Promise<{ parsed: gmail_v1.Schema$Filter[] } | AuthError | ApiError> {
|
|
1062
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1063
|
+
withRetry(() => this.gmail.users.settings.filters.list({ userId: 'me' })),
|
|
1064
|
+
)
|
|
1065
|
+
if (res instanceof Error) return res
|
|
1066
|
+
return { parsed: res.data.filter ?? [] }
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async createFilter(opts: {
|
|
1070
|
+
from?: string
|
|
1071
|
+
query?: string
|
|
1072
|
+
addLabelIds?: string[]
|
|
1073
|
+
removeLabelIds?: string[]
|
|
1074
|
+
}): Promise<gmail_v1.Schema$Filter | AuthError | ApiError> {
|
|
1075
|
+
const criteria: gmail_v1.Schema$FilterCriteria = {}
|
|
1076
|
+
if (opts.from) criteria.from = opts.from
|
|
1077
|
+
if (opts.query) criteria.query = opts.query
|
|
1078
|
+
|
|
1079
|
+
const action: gmail_v1.Schema$FilterAction = {}
|
|
1080
|
+
if (opts.addLabelIds?.length) action.addLabelIds = opts.addLabelIds
|
|
1081
|
+
if (opts.removeLabelIds?.length) action.removeLabelIds = opts.removeLabelIds
|
|
1082
|
+
|
|
1083
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1084
|
+
withRetry(() =>
|
|
1085
|
+
this.gmail.users.settings.filters.create({
|
|
1086
|
+
userId: 'me',
|
|
1087
|
+
requestBody: { criteria, action },
|
|
1088
|
+
}),
|
|
1089
|
+
),
|
|
1090
|
+
)
|
|
1091
|
+
if (res instanceof Error) return res
|
|
1092
|
+
return res.data
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async deleteFilter(filterId: string): Promise<void | AuthError | ApiError> {
|
|
1096
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1097
|
+
withRetry(() =>
|
|
1098
|
+
this.gmail.users.settings.filters.delete({
|
|
1099
|
+
userId: 'me',
|
|
1100
|
+
id: filterId,
|
|
1101
|
+
}),
|
|
1102
|
+
),
|
|
1103
|
+
)
|
|
1104
|
+
if (res instanceof Error) return res
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** Resolve a label name to its ID, auto-creating if missing. Public wrapper for filter commands. */
|
|
1108
|
+
async resolveLabel(nameOrId: string): Promise<string | AuthError | ApiError> {
|
|
1109
|
+
return this.resolveLabelId(nameOrId)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1024
1112
|
// =========================================================================
|
|
1025
1113
|
// Label counts (unread counts per folder/label)
|
|
1026
1114
|
// =========================================================================
|
|
@@ -1260,16 +1348,47 @@ export class GmailClient {
|
|
|
1260
1348
|
}
|
|
1261
1349
|
}
|
|
1262
1350
|
|
|
1351
|
+
// Parse recipients from latest message
|
|
1352
|
+
const toHeader = getHeader('to') ?? ''
|
|
1353
|
+
const ccHeaders = headers
|
|
1354
|
+
.filter((h) => h.name?.toLowerCase() === 'cc')
|
|
1355
|
+
.map((h) => h.value ?? '')
|
|
1356
|
+
.filter((v) => v.length > 0)
|
|
1357
|
+
|
|
1358
|
+
// Check if any non-draft message in the thread is a reply (has In-Reply-To header)
|
|
1359
|
+
const inReplyTo =
|
|
1360
|
+
nonDraftMessages
|
|
1361
|
+
.map(
|
|
1362
|
+
(m) =>
|
|
1363
|
+
m.payload?.headers?.find((h) => h.name?.toLowerCase() === 'in-reply-to')?.value ??
|
|
1364
|
+
null,
|
|
1365
|
+
)
|
|
1366
|
+
.find((v) => v !== null) ?? null
|
|
1367
|
+
|
|
1368
|
+
// Check if any message has attachments (non-inline)
|
|
1369
|
+
const hasAttachments = messages.some((m) => this.hasNonInlineAttachments(m.payload))
|
|
1370
|
+
|
|
1371
|
+
// List-Unsubscribe from latest message
|
|
1372
|
+
const listUnsubscribe = getHeader('list-unsubscribe') ?? null
|
|
1373
|
+
|
|
1263
1374
|
return {
|
|
1264
1375
|
id: raw.id ?? '',
|
|
1265
1376
|
historyId: raw.historyId ?? null,
|
|
1266
1377
|
snippet: sanitizeSnippet(latest?.snippet ?? ''),
|
|
1267
1378
|
subject: (getHeader('subject') ?? '(no subject)').replace(/"/g, '').trim(),
|
|
1268
1379
|
from: displayFrom,
|
|
1380
|
+
to: toHeader ? parseAddressList(toHeader) : [],
|
|
1381
|
+
cc: ccHeaders.length > 0
|
|
1382
|
+
? ccHeaders.filter((h) => h.trim().length > 0).flatMap((h) => parseAddressList(h))
|
|
1383
|
+
: [],
|
|
1269
1384
|
date: getHeader('date') ?? '',
|
|
1270
1385
|
labelIds: allLabels,
|
|
1271
1386
|
unread: allLabels.includes('UNREAD'),
|
|
1387
|
+
starred: allLabels.includes('STARRED'),
|
|
1272
1388
|
messageCount: nonDraftMessages.length,
|
|
1389
|
+
inReplyTo,
|
|
1390
|
+
hasAttachments,
|
|
1391
|
+
listUnsubscribe,
|
|
1273
1392
|
}
|
|
1274
1393
|
}
|
|
1275
1394
|
|
|
@@ -1393,6 +1512,24 @@ export class GmailClient {
|
|
|
1393
1512
|
return results
|
|
1394
1513
|
}
|
|
1395
1514
|
|
|
1515
|
+
/** Quick check: does the message payload tree contain any non-inline attachments?
|
|
1516
|
+
* Checks the root part itself (some messages have attachment metadata there)
|
|
1517
|
+
* then recurses into child parts. */
|
|
1518
|
+
private hasNonInlineAttachments(part: gmail_v1.Schema$MessagePart | undefined): boolean {
|
|
1519
|
+
if (!part) return false
|
|
1520
|
+
if (part.filename && part.filename.length > 0 && part.body?.attachmentId) {
|
|
1521
|
+
const disposition =
|
|
1522
|
+
part.headers?.find((h) => h.name?.toLowerCase() === 'content-disposition')?.value ?? ''
|
|
1523
|
+
const hasContentId = part.headers?.some((h) => h.name?.toLowerCase() === 'content-id')
|
|
1524
|
+
const isInline = disposition.toLowerCase().includes('inline')
|
|
1525
|
+
if (!isInline || !hasContentId) return true
|
|
1526
|
+
}
|
|
1527
|
+
for (const child of part.parts ?? []) {
|
|
1528
|
+
if (this.hasNonInlineAttachments(child)) return true
|
|
1529
|
+
}
|
|
1530
|
+
return false
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1396
1533
|
async getEmailAliases(): Promise<Array<{ email: string; name?: string; primary: boolean }> | AuthError | ApiError> {
|
|
1397
1534
|
const profile = await this.getProfile()
|
|
1398
1535
|
if (profile instanceof Error) return profile
|
|
@@ -1821,22 +1958,25 @@ export class GmailClient {
|
|
|
1821
1958
|
private async batchModifyMessages(
|
|
1822
1959
|
messageIds: string[],
|
|
1823
1960
|
body: { addLabelIds?: string[]; removeLabelIds?: string[] },
|
|
1824
|
-
) {
|
|
1961
|
+
): Promise<void | AuthError | ApiError> {
|
|
1825
1962
|
if (messageIds.length === 0) return
|
|
1826
1963
|
|
|
1827
1964
|
// Gmail batchModify accepts up to 1000 IDs
|
|
1828
1965
|
const chunkSize = 1000
|
|
1829
1966
|
for (let i = 0; i < messageIds.length; i += chunkSize) {
|
|
1830
1967
|
const chunk = messageIds.slice(i, i + chunkSize)
|
|
1831
|
-
await
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1968
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1969
|
+
withRetry(() =>
|
|
1970
|
+
this.gmail.users.messages.batchModify({
|
|
1971
|
+
userId: 'me',
|
|
1972
|
+
requestBody: {
|
|
1973
|
+
ids: chunk,
|
|
1974
|
+
...body,
|
|
1975
|
+
},
|
|
1976
|
+
}),
|
|
1977
|
+
),
|
|
1839
1978
|
)
|
|
1979
|
+
if (res instanceof Error) return res
|
|
1840
1980
|
}
|
|
1841
1981
|
}
|
|
1842
1982
|
|
package/src/mail-tui.tsx
CHANGED
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
useNavigation,
|
|
35
35
|
showFailureToast,
|
|
36
36
|
} from 'termcast'
|
|
37
|
-
// @ts-expect-error https://github.com/anomalyco/opentui/pull/614
|
|
38
37
|
import { useTerminalDimensions } from '@opentui/react'
|
|
39
38
|
import { useCachedPromise, useCachedState } from '@termcast/utils'
|
|
40
39
|
import { useState, useMemo, useCallback, useEffect } from 'react'
|
|
@@ -1195,7 +1194,7 @@ export default function Command() {
|
|
|
1195
1194
|
<Action
|
|
1196
1195
|
title={thread.unread ? 'Mark as Read' : 'Mark as Unread'}
|
|
1197
1196
|
icon={thread.unread ? Icon.Eye : Icon.EyeDisabled}
|
|
1198
|
-
shortcut={{ modifiers: ['ctrl'], key: 'u' }}
|
|
1197
|
+
// shortcut={{ modifiers: ['ctrl'], key: 'u' }}
|
|
1199
1198
|
onAction={() => withMutation(async () => {
|
|
1200
1199
|
const { client } = await getClient([thread.account])
|
|
1201
1200
|
const result = thread.unread
|
package/src/output.ts
CHANGED
|
@@ -326,10 +326,17 @@ export function formatSender(sender: { name?: string; email: string }): string {
|
|
|
326
326
|
// Status indicators
|
|
327
327
|
// ---------------------------------------------------------------------------
|
|
328
328
|
|
|
329
|
-
export function formatFlags(item: {
|
|
329
|
+
export function formatFlags(item: {
|
|
330
|
+
unread?: boolean
|
|
331
|
+
starred?: boolean
|
|
332
|
+
hasAttachments?: boolean
|
|
333
|
+
inReplyTo?: string | null
|
|
334
|
+
}): string {
|
|
330
335
|
const parts: string[] = []
|
|
331
336
|
if (item.starred) parts.push('starred')
|
|
332
337
|
if (item.unread) parts.push('unread')
|
|
338
|
+
if (item.hasAttachments) parts.push('attachment')
|
|
339
|
+
if (item.inReplyTo) parts.push('reply')
|
|
333
340
|
return parts.join(', ')
|
|
334
341
|
}
|
|
335
342
|
|