@soederpop/luca 0.0.20 → 0.0.22
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/docs/bootstrap/SKILL.md +16 -0
- package/luca.cli.ts +14 -1
- package/package.json +1 -1
- package/src/bootstrap/generated.ts +17 -1
- package/src/cli/cli.ts +45 -6
- package/src/commands/chat.ts +14 -0
- package/src/commands/prompt.ts +218 -65
- package/src/introspection/generated.agi.ts +1534 -1199
- package/src/introspection/generated.node.ts +894 -559
- package/src/introspection/generated.web.ts +1 -1
- package/src/node/container.ts +35 -1
- package/src/node/features/google-auth.ts +1 -0
- package/src/node/features/google-calendar.ts +5 -0
- package/src/node/features/google-docs.ts +8 -1
- package/src/node/features/google-drive.ts +6 -0
- package/src/node/features/google-mail.ts +540 -0
- package/src/node/features/google-sheets.ts +6 -0
- package/src/scaffolds/generated.ts +1 -1
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
+
import { Feature } from '../feature.js'
|
|
4
|
+
import { google, type gmail_v1 } from 'googleapis'
|
|
5
|
+
import type { GoogleAuth } from './google-auth.js'
|
|
6
|
+
|
|
7
|
+
export type MailMessage = {
|
|
8
|
+
id: string
|
|
9
|
+
threadId: string
|
|
10
|
+
labelIds: string[]
|
|
11
|
+
snippet: string
|
|
12
|
+
subject: string
|
|
13
|
+
from: string
|
|
14
|
+
to: string
|
|
15
|
+
cc?: string
|
|
16
|
+
date: string
|
|
17
|
+
body: string
|
|
18
|
+
bodyHtml?: string
|
|
19
|
+
isUnread: boolean
|
|
20
|
+
hasAttachments: boolean
|
|
21
|
+
attachments: MailAttachment[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type MailAttachment = {
|
|
25
|
+
filename: string
|
|
26
|
+
mimeType: string
|
|
27
|
+
size: number
|
|
28
|
+
attachmentId: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type MailThread = {
|
|
32
|
+
id: string
|
|
33
|
+
snippet: string
|
|
34
|
+
historyId: string
|
|
35
|
+
messages: MailMessage[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type MailLabel = {
|
|
39
|
+
id: string
|
|
40
|
+
name: string
|
|
41
|
+
type: string
|
|
42
|
+
messagesTotal?: number
|
|
43
|
+
messagesUnread?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SearchMailOptions = {
|
|
47
|
+
query?: string
|
|
48
|
+
from?: string
|
|
49
|
+
to?: string
|
|
50
|
+
subject?: string
|
|
51
|
+
after?: string
|
|
52
|
+
before?: string
|
|
53
|
+
hasAttachment?: boolean
|
|
54
|
+
label?: string
|
|
55
|
+
isUnread?: boolean
|
|
56
|
+
maxResults?: number
|
|
57
|
+
pageToken?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type MailMessageList = {
|
|
61
|
+
messages: MailMessage[]
|
|
62
|
+
nextPageToken?: string
|
|
63
|
+
resultSizeEstimate?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const GoogleMailStateSchema = FeatureStateSchema.extend({
|
|
67
|
+
lastQuery: z.string().optional()
|
|
68
|
+
.describe('Last search query used'),
|
|
69
|
+
lastResultCount: z.number().optional()
|
|
70
|
+
.describe('Number of messages returned in last search'),
|
|
71
|
+
lastError: z.string().optional()
|
|
72
|
+
.describe('Last Gmail API error message'),
|
|
73
|
+
watchExpiration: z.string().optional()
|
|
74
|
+
.describe('ISO timestamp when the current watch expires'),
|
|
75
|
+
})
|
|
76
|
+
export type GoogleMailState = z.infer<typeof GoogleMailStateSchema>
|
|
77
|
+
|
|
78
|
+
export const GoogleMailOptionsSchema = FeatureOptionsSchema.extend({
|
|
79
|
+
auth: z.any().describe('An authorized instance of the googleAuth feature').optional(),
|
|
80
|
+
userId: z.string().optional()
|
|
81
|
+
.describe('Gmail user ID (default: "me")'),
|
|
82
|
+
pollInterval: z.number().optional()
|
|
83
|
+
.describe('Polling interval in ms for watching new mail (default: 30000)'),
|
|
84
|
+
format: z.enum(['full', 'metadata', 'minimal', 'raw']).optional()
|
|
85
|
+
.describe('Default message format when fetching (default: "full")'),
|
|
86
|
+
})
|
|
87
|
+
export type GoogleMailOptions = z.infer<typeof GoogleMailOptionsSchema>
|
|
88
|
+
|
|
89
|
+
export const GoogleMailEventsSchema = FeatureEventsSchema.extend({
|
|
90
|
+
messagesFetched: z.tuple([z.number().describe('Number of messages returned')])
|
|
91
|
+
.describe('Messages were fetched from Gmail'),
|
|
92
|
+
newMail: z.tuple([z.array(z.any()).describe('Array of new MailMessage objects')])
|
|
93
|
+
.describe('New mail arrived (emitted by watch)'),
|
|
94
|
+
watchStarted: z.tuple([]).describe('Mail watching has started'),
|
|
95
|
+
watchStopped: z.tuple([]).describe('Mail watching has stopped'),
|
|
96
|
+
error: z.tuple([z.any().describe('The error')]).describe('Gmail API error occurred'),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Google Mail feature for searching, reading, and watching Gmail messages.
|
|
101
|
+
*
|
|
102
|
+
* Depends on the googleAuth feature for authentication. Creates a Gmail v1 API
|
|
103
|
+
* client lazily. Supports Gmail search query syntax, individual message reading,
|
|
104
|
+
* and polling-based new mail detection with event emission.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const mail = container.feature('googleMail')
|
|
109
|
+
*
|
|
110
|
+
* // Search by sender
|
|
111
|
+
* const fromBoss = await mail.search({ from: 'boss@company.com' })
|
|
112
|
+
*
|
|
113
|
+
* // Use Gmail query string
|
|
114
|
+
* const unread = await mail.search({ query: 'is:unread category:primary' })
|
|
115
|
+
*
|
|
116
|
+
* // Read a specific message
|
|
117
|
+
* const msg = await mail.getMessage('message-id-here')
|
|
118
|
+
*
|
|
119
|
+
* // Get a full thread
|
|
120
|
+
* const thread = await mail.getThread('thread-id-here')
|
|
121
|
+
*
|
|
122
|
+
* // List labels
|
|
123
|
+
* const labels = await mail.listLabels()
|
|
124
|
+
*
|
|
125
|
+
* // Watch for new mail (polls and emits 'newMail' events)
|
|
126
|
+
* mail.on('newMail', (messages) => {
|
|
127
|
+
* console.log(`Got ${messages.length} new messages!`)
|
|
128
|
+
* })
|
|
129
|
+
* await mail.startWatching()
|
|
130
|
+
*
|
|
131
|
+
* // Stop watching
|
|
132
|
+
* mail.stopWatching()
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export class GoogleMail extends Feature<GoogleMailState, GoogleMailOptions> {
|
|
136
|
+
static override shortcut = 'features.googleMail' as const
|
|
137
|
+
static override stateSchema = GoogleMailStateSchema
|
|
138
|
+
static override optionsSchema = GoogleMailOptionsSchema
|
|
139
|
+
static override eventsSchema = GoogleMailEventsSchema
|
|
140
|
+
static { Feature.register(this, 'googleMail') }
|
|
141
|
+
|
|
142
|
+
private _gmail?: gmail_v1.Gmail
|
|
143
|
+
private _watchTimer?: ReturnType<typeof setInterval>
|
|
144
|
+
private _lastHistoryId?: string
|
|
145
|
+
|
|
146
|
+
override get initialState(): GoogleMailState {
|
|
147
|
+
return { ...super.initialState }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Access the google-auth feature lazily. */
|
|
151
|
+
get auth(): GoogleAuth {
|
|
152
|
+
if (this.options.auth) {
|
|
153
|
+
return this.options.auth as GoogleAuth
|
|
154
|
+
}
|
|
155
|
+
return this.container.feature('googleAuth') as unknown as GoogleAuth
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Default user ID from options or 'me'. */
|
|
159
|
+
get userId(): string {
|
|
160
|
+
return this.options.userId || 'me'
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Default message format from options or 'full'. */
|
|
164
|
+
get defaultFormat(): 'full' | 'metadata' | 'minimal' | 'raw' {
|
|
165
|
+
return this.options.format || 'full'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Polling interval from options or 30 seconds. */
|
|
169
|
+
get pollInterval(): number {
|
|
170
|
+
return this.options.pollInterval || 30_000
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Get or create the Gmail v1 API client. */
|
|
174
|
+
private async getGmail(): Promise<gmail_v1.Gmail> {
|
|
175
|
+
if (this._gmail) return this._gmail
|
|
176
|
+
const auth = await this.auth.getAuthClient()
|
|
177
|
+
this._gmail = google.gmail({ version: 'v1', auth: auth as any })
|
|
178
|
+
return this._gmail
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Search for messages using Gmail query syntax and/or structured filters.
|
|
183
|
+
*
|
|
184
|
+
* @param options - Search filters including query, from, to, subject, date ranges
|
|
185
|
+
* @returns Messages array with optional nextPageToken
|
|
186
|
+
*/
|
|
187
|
+
async search(options: SearchMailOptions = {}): Promise<MailMessageList> {
|
|
188
|
+
const query = buildQuery(options)
|
|
189
|
+
try {
|
|
190
|
+
const gmail = await this.getGmail()
|
|
191
|
+
const res = await gmail.users.messages.list({
|
|
192
|
+
userId: this.userId,
|
|
193
|
+
q: query || undefined,
|
|
194
|
+
maxResults: options.maxResults || 20,
|
|
195
|
+
pageToken: options.pageToken || undefined,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const messageRefs = res.data.messages || []
|
|
199
|
+
const messages: MailMessage[] = []
|
|
200
|
+
|
|
201
|
+
for (const ref of messageRefs) {
|
|
202
|
+
if (ref.id) {
|
|
203
|
+
const msg = await this.getMessage(ref.id)
|
|
204
|
+
messages.push(msg)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.setState({ lastQuery: query, lastResultCount: messages.length })
|
|
209
|
+
this.emit('messagesFetched', messages.length)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
messages,
|
|
213
|
+
nextPageToken: res.data.nextPageToken || undefined,
|
|
214
|
+
resultSizeEstimate: res.data.resultSizeEstimate || undefined,
|
|
215
|
+
}
|
|
216
|
+
} catch (err: any) {
|
|
217
|
+
this.setState({ lastError: err.message })
|
|
218
|
+
this.emit('error', err)
|
|
219
|
+
throw err
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get a single message by ID.
|
|
225
|
+
*
|
|
226
|
+
* @param messageId - The message ID
|
|
227
|
+
* @param format - Message format (defaults to options.format or 'full')
|
|
228
|
+
* @returns The full mail message
|
|
229
|
+
*/
|
|
230
|
+
async getMessage(messageId: string, format?: 'full' | 'metadata' | 'minimal' | 'raw'): Promise<MailMessage> {
|
|
231
|
+
try {
|
|
232
|
+
const gmail = await this.getGmail()
|
|
233
|
+
const res = await gmail.users.messages.get({
|
|
234
|
+
userId: this.userId,
|
|
235
|
+
id: messageId,
|
|
236
|
+
format: format || this.defaultFormat,
|
|
237
|
+
})
|
|
238
|
+
return normalizeMessage(res.data)
|
|
239
|
+
} catch (err: any) {
|
|
240
|
+
this.setState({ lastError: err.message })
|
|
241
|
+
this.emit('error', err)
|
|
242
|
+
throw err
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get a full thread with all its messages.
|
|
248
|
+
*
|
|
249
|
+
* @param threadId - The thread ID
|
|
250
|
+
* @returns The thread with all messages
|
|
251
|
+
*/
|
|
252
|
+
async getThread(threadId: string): Promise<MailThread> {
|
|
253
|
+
try {
|
|
254
|
+
const gmail = await this.getGmail()
|
|
255
|
+
const res = await gmail.users.threads.get({
|
|
256
|
+
userId: this.userId,
|
|
257
|
+
id: threadId,
|
|
258
|
+
format: this.defaultFormat,
|
|
259
|
+
})
|
|
260
|
+
return {
|
|
261
|
+
id: res.data.id || '',
|
|
262
|
+
snippet: res.data.snippet || '',
|
|
263
|
+
historyId: res.data.historyId || '',
|
|
264
|
+
messages: (res.data.messages || []).map(normalizeMessage),
|
|
265
|
+
}
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
this.setState({ lastError: err.message })
|
|
268
|
+
this.emit('error', err)
|
|
269
|
+
throw err
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* List all labels for the authenticated user.
|
|
275
|
+
*
|
|
276
|
+
* @returns Array of label objects
|
|
277
|
+
*/
|
|
278
|
+
async listLabels(): Promise<MailLabel[]> {
|
|
279
|
+
try {
|
|
280
|
+
const gmail = await this.getGmail()
|
|
281
|
+
const res = await gmail.users.labels.list({ userId: this.userId })
|
|
282
|
+
const labels = res.data.labels || []
|
|
283
|
+
|
|
284
|
+
// Fetch full label info for counts
|
|
285
|
+
const detailed: MailLabel[] = []
|
|
286
|
+
for (const label of labels) {
|
|
287
|
+
if (label.id) {
|
|
288
|
+
try {
|
|
289
|
+
const full = await gmail.users.labels.get({ userId: this.userId, id: label.id })
|
|
290
|
+
detailed.push({
|
|
291
|
+
id: full.data.id || '',
|
|
292
|
+
name: full.data.name || '',
|
|
293
|
+
type: full.data.type || '',
|
|
294
|
+
messagesTotal: full.data.messagesTotal || undefined,
|
|
295
|
+
messagesUnread: full.data.messagesUnread || undefined,
|
|
296
|
+
})
|
|
297
|
+
} catch {
|
|
298
|
+
detailed.push({
|
|
299
|
+
id: label.id || '',
|
|
300
|
+
name: label.name || '',
|
|
301
|
+
type: label.type || '',
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return detailed
|
|
307
|
+
} catch (err: any) {
|
|
308
|
+
this.setState({ lastError: err.message })
|
|
309
|
+
this.emit('error', err)
|
|
310
|
+
throw err
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Start watching for new mail by polling at a regular interval.
|
|
316
|
+
* Emits 'newMail' events with an array of new messages when they arrive.
|
|
317
|
+
*
|
|
318
|
+
* Uses Gmail history API to efficiently detect only new messages since the last check.
|
|
319
|
+
*/
|
|
320
|
+
async startWatching(): Promise<void> {
|
|
321
|
+
if (this._watchTimer) return
|
|
322
|
+
|
|
323
|
+
// Get initial history ID
|
|
324
|
+
const gmail = await this.getGmail()
|
|
325
|
+
const profile = await gmail.users.getProfile({ userId: this.userId })
|
|
326
|
+
this._lastHistoryId = profile.data.historyId || undefined
|
|
327
|
+
|
|
328
|
+
this._watchTimer = setInterval(async () => {
|
|
329
|
+
try {
|
|
330
|
+
await this._checkForNewMail()
|
|
331
|
+
} catch (err: any) {
|
|
332
|
+
this.setState({ lastError: err.message })
|
|
333
|
+
this.emit('error', err)
|
|
334
|
+
}
|
|
335
|
+
}, this.pollInterval)
|
|
336
|
+
|
|
337
|
+
this.emit('watchStarted')
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Stop watching for new mail.
|
|
342
|
+
*/
|
|
343
|
+
stopWatching(): void {
|
|
344
|
+
if (this._watchTimer) {
|
|
345
|
+
clearInterval(this._watchTimer)
|
|
346
|
+
this._watchTimer = undefined
|
|
347
|
+
this._lastHistoryId = undefined
|
|
348
|
+
this.setState({ watchExpiration: undefined })
|
|
349
|
+
this.emit('watchStopped')
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Check for new messages since last history ID using Gmail history API. */
|
|
354
|
+
private async _checkForNewMail(): Promise<void> {
|
|
355
|
+
if (!this._lastHistoryId) return
|
|
356
|
+
|
|
357
|
+
const gmail = await this.getGmail()
|
|
358
|
+
try {
|
|
359
|
+
const res = await gmail.users.history.list({
|
|
360
|
+
userId: this.userId,
|
|
361
|
+
startHistoryId: this._lastHistoryId,
|
|
362
|
+
historyTypes: ['messageAdded'],
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const history = res.data.history || []
|
|
366
|
+
const newMessageIds = new Set<string>()
|
|
367
|
+
|
|
368
|
+
for (const record of history) {
|
|
369
|
+
for (const added of record.messagesAdded || []) {
|
|
370
|
+
if (added.message?.id) {
|
|
371
|
+
newMessageIds.add(added.message.id)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (res.data.historyId) {
|
|
377
|
+
this._lastHistoryId = res.data.historyId
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (newMessageIds.size > 0) {
|
|
381
|
+
const messages: MailMessage[] = []
|
|
382
|
+
for (const id of newMessageIds) {
|
|
383
|
+
try {
|
|
384
|
+
const msg = await this.getMessage(id)
|
|
385
|
+
messages.push(msg)
|
|
386
|
+
} catch {
|
|
387
|
+
// Message may have been deleted between detection and fetch
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (messages.length > 0) {
|
|
391
|
+
this.emit('newMail', messages)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} catch (err: any) {
|
|
395
|
+
// History ID may be expired — reset by fetching fresh profile
|
|
396
|
+
if (err.code === 404 || err.message?.includes('historyId')) {
|
|
397
|
+
const profile = await gmail.users.getProfile({ userId: this.userId })
|
|
398
|
+
this._lastHistoryId = profile.data.historyId || undefined
|
|
399
|
+
} else {
|
|
400
|
+
throw err
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Build a Gmail query string from structured search options. */
|
|
407
|
+
function buildQuery(options: SearchMailOptions): string {
|
|
408
|
+
const parts: string[] = []
|
|
409
|
+
|
|
410
|
+
if (options.query) parts.push(options.query)
|
|
411
|
+
if (options.from) parts.push(`from:${options.from}`)
|
|
412
|
+
if (options.to) parts.push(`to:${options.to}`)
|
|
413
|
+
if (options.subject) parts.push(`subject:${options.subject}`)
|
|
414
|
+
if (options.after) parts.push(`after:${options.after}`)
|
|
415
|
+
if (options.before) parts.push(`before:${options.before}`)
|
|
416
|
+
if (options.hasAttachment) parts.push('has:attachment')
|
|
417
|
+
if (options.label) parts.push(`label:${options.label}`)
|
|
418
|
+
if (options.isUnread) parts.push('is:unread')
|
|
419
|
+
|
|
420
|
+
return parts.join(' ')
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Extract a header value from a Gmail message. */
|
|
424
|
+
function getHeader(headers: gmail_v1.Schema$MessagePartHeader[] | undefined, name: string): string {
|
|
425
|
+
if (!headers) return ''
|
|
426
|
+
const h = headers.find(h => h.name?.toLowerCase() === name.toLowerCase())
|
|
427
|
+
return h?.value || ''
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Decode a base64url-encoded string. */
|
|
431
|
+
function decodeBody(encoded: string): string {
|
|
432
|
+
try {
|
|
433
|
+
return Buffer.from(encoded, 'base64url').toString('utf-8')
|
|
434
|
+
} catch {
|
|
435
|
+
return ''
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Extract the plain text body from a message payload. */
|
|
440
|
+
function extractBody(payload: gmail_v1.Schema$MessagePart | undefined): string {
|
|
441
|
+
if (!payload) return ''
|
|
442
|
+
|
|
443
|
+
// Simple body
|
|
444
|
+
if (payload.body?.data) {
|
|
445
|
+
return decodeBody(payload.body.data)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Multipart — look for text/plain first, then text/html
|
|
449
|
+
if (payload.parts) {
|
|
450
|
+
const textPart = payload.parts.find(p => p.mimeType === 'text/plain')
|
|
451
|
+
if (textPart?.body?.data) return decodeBody(textPart.body.data)
|
|
452
|
+
|
|
453
|
+
const htmlPart = payload.parts.find(p => p.mimeType === 'text/html')
|
|
454
|
+
if (htmlPart?.body?.data) return decodeBody(htmlPart.body.data)
|
|
455
|
+
|
|
456
|
+
// Nested multipart
|
|
457
|
+
for (const part of payload.parts) {
|
|
458
|
+
const nested = extractBody(part)
|
|
459
|
+
if (nested) return nested
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return ''
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Extract HTML body from a message payload. */
|
|
467
|
+
function extractHtmlBody(payload: gmail_v1.Schema$MessagePart | undefined): string | undefined {
|
|
468
|
+
if (!payload) return undefined
|
|
469
|
+
|
|
470
|
+
if (payload.mimeType === 'text/html' && payload.body?.data) {
|
|
471
|
+
return decodeBody(payload.body.data)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (payload.parts) {
|
|
475
|
+
const htmlPart = payload.parts.find(p => p.mimeType === 'text/html')
|
|
476
|
+
if (htmlPart?.body?.data) return decodeBody(htmlPart.body.data)
|
|
477
|
+
|
|
478
|
+
for (const part of payload.parts) {
|
|
479
|
+
const nested = extractHtmlBody(part)
|
|
480
|
+
if (nested) return nested
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return undefined
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Extract attachment metadata from a message payload. */
|
|
488
|
+
function extractAttachments(payload: gmail_v1.Schema$MessagePart | undefined): MailAttachment[] {
|
|
489
|
+
if (!payload) return []
|
|
490
|
+
|
|
491
|
+
const attachments: MailAttachment[] = []
|
|
492
|
+
|
|
493
|
+
if (payload.filename && payload.body?.attachmentId) {
|
|
494
|
+
attachments.push({
|
|
495
|
+
filename: payload.filename,
|
|
496
|
+
mimeType: payload.mimeType || '',
|
|
497
|
+
size: payload.body.size || 0,
|
|
498
|
+
attachmentId: payload.body.attachmentId,
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (payload.parts) {
|
|
503
|
+
for (const part of payload.parts) {
|
|
504
|
+
attachments.push(...extractAttachments(part))
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return attachments
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Normalize a raw Gmail message into our clean MailMessage type. */
|
|
512
|
+
function normalizeMessage(msg: gmail_v1.Schema$Message): MailMessage {
|
|
513
|
+
const headers = msg.payload?.headers
|
|
514
|
+
const attachments = extractAttachments(msg.payload)
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
id: msg.id || '',
|
|
518
|
+
threadId: msg.threadId || '',
|
|
519
|
+
labelIds: msg.labelIds || [],
|
|
520
|
+
snippet: msg.snippet || '',
|
|
521
|
+
subject: getHeader(headers, 'Subject'),
|
|
522
|
+
from: getHeader(headers, 'From'),
|
|
523
|
+
to: getHeader(headers, 'To'),
|
|
524
|
+
cc: getHeader(headers, 'Cc') || undefined,
|
|
525
|
+
date: getHeader(headers, 'Date'),
|
|
526
|
+
body: extractBody(msg.payload),
|
|
527
|
+
bodyHtml: extractHtmlBody(msg.payload),
|
|
528
|
+
isUnread: (msg.labelIds || []).includes('UNREAD'),
|
|
529
|
+
hasAttachments: attachments.length > 0,
|
|
530
|
+
attachments,
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
declare module '../../feature' {
|
|
535
|
+
interface AvailableFeatures {
|
|
536
|
+
googleMail: typeof GoogleMail
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export default GoogleMail
|
|
@@ -32,6 +32,7 @@ export const GoogleSheetsStateSchema = FeatureStateSchema.extend({
|
|
|
32
32
|
export type GoogleSheetsState = z.infer<typeof GoogleSheetsStateSchema>
|
|
33
33
|
|
|
34
34
|
export const GoogleSheetsOptionsSchema = FeatureOptionsSchema.extend({
|
|
35
|
+
auth: z.any().describe('An authorized instance of the googleAuth feature').optional(),
|
|
35
36
|
defaultSpreadsheetId: z.string().optional()
|
|
36
37
|
.describe('Default spreadsheet ID for operations'),
|
|
37
38
|
})
|
|
@@ -84,9 +85,14 @@ export class GoogleSheets extends Feature<GoogleSheetsState, GoogleSheetsOptions
|
|
|
84
85
|
|
|
85
86
|
/** Access the google-auth feature lazily. */
|
|
86
87
|
get auth(): GoogleAuth {
|
|
88
|
+
if (this.options.auth) {
|
|
89
|
+
return this.options.auth as GoogleAuth
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
return this.container.feature('googleAuth') as unknown as GoogleAuth
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
|
|
90
96
|
/** Get or create the Sheets v4 API client. */
|
|
91
97
|
private async getSheets(): Promise<sheets_v4.Sheets> {
|
|
92
98
|
if (this._sheets) return this._sheets
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-generated scaffold and MCP readme content
|
|
2
|
-
// Generated at: 2026-03-
|
|
2
|
+
// Generated at: 2026-03-21T05:25:03.287Z
|
|
3
3
|
// Source: docs/scaffolds/*.md, docs/examples/assistant/, and docs/mcp/readme.md
|
|
4
4
|
//
|
|
5
5
|
// Do not edit manually. Run: luca build-scaffolds
|