@hmawla/co-assistant 1.0.7 → 1.0.8

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.
@@ -2,30 +2,29 @@ You are checking my recent emails for any that require a reply from me.
2
2
 
3
3
  ## Instructions
4
4
 
5
- 1. Use the `gmail_search_emails` tool to fetch my last 5 emails (query: `in:inbox`, maxResults: 5).
6
- 2. For each email, use `gmail_read_email` to read its full content.
7
- 3. Skip any email whose message ID appears in the deduplication list below.
8
- 4. For each **new** email, determine whether it requires a reply from me. Consider:
5
+ 1. Use `gmail__search_threads` to fetch my recent inbox threads (query: `in:inbox`, maxThreads: 18, **includeLatestBody: true**). This single call returns all threads with every message (including your sent replies) and the full body of the latest message. **Do NOT call any other Gmail tools** — this gives you everything you need.
6
+ 2. Skip any thread whose latest message ID appears in the deduplication list below. Also skip newsletters, automated notifications, marketing, no-reply senders, and receipts.
7
+ 3. For each remaining thread, check the `lastMessageIsSent` field. **If `lastMessageIsSent` is true, I already replied — SKIP this thread.**
8
+ 4. Only for threads where `lastMessageIsSent` is false, determine whether it requires a reply from me. Consider:
9
9
  - Direct questions asked to me
10
10
  - Action items or requests directed at me
11
11
  - Invitations or RSVPs awaiting my response
12
12
  - Important threads where I'm expected to respond
13
- - Do NOT flag: newsletters, automated notifications, marketing, no-reply senders, receipts
14
- 5. For each email that needs a reply, suggest a concise, professional reply draft.
13
+ 5. For each thread that needs a reply, suggest a concise, professional reply draft based on the latest incoming message body.
15
14
 
16
15
  ## Output Format
17
16
 
18
- For each email that needs a reply, format your output like this:
17
+ For each thread that needs a reply, output exactly ONE entry (not one per message):
19
18
 
20
- **📧 From:** [sender]
21
- **Subject:** [subject]
22
- **Why reply:** [brief reason]
19
+ **📧 From:** [sender of the latest message]
20
+ **Subject:** [thread subject]
21
+ **Why reply:** [brief reason based on the latest message in the thread]
23
22
  **Suggested reply:**
24
23
  > [your suggested reply text]
25
24
 
26
25
  ---
27
26
 
28
- If no emails require a reply, do not say anything unless invoked with /heartbeat
27
+ If no threads require a reply, do not say anything unless invoked with /heartbeat
29
28
 
30
29
  ## Deduplication
31
30
 
@@ -33,7 +32,7 @@ If no emails require a reply, do not say anything unless invoked with /heartbeat
33
32
 
34
33
  ## IMPORTANT — Deduplication Marker
35
34
 
36
- At the very end of your response, you MUST output exactly one line in this format with ALL email message IDs you checked (whether they needed a reply or not). This prevents re-checking the same emails next time:
35
+ At the very end of your response, you MUST output exactly one line in this format with the message ID of the **most recent message per thread** you checked (whether it needed a reply or not). Only one ID per thread. This prevents re-checking the same threads next time:
37
36
 
38
- <!-- PROCESSED: msg_id_1, msg_id_2, msg_id_3, msg_id_4, msg_id_5 -->
39
- Also, make sure to not output the same message id multiple times, so if it exist don't push it.
37
+ <!-- PROCESSED: latest_msg_id_thread1, latest_msg_id_thread2, latest_msg_id_thread3 -->
38
+ Do not output the same message ID multiple times.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmawla/co-assistant",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "AI-powered Telegram personal assistant using GitHub Copilot SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13877,18 +13877,22 @@ function extractBody(payload) {
13877
13877
  function createGmailTools(auth, logger) {
13878
13878
  const searchEmails = {
13879
13879
  name: "search_emails",
13880
- description: "Search for emails in Gmail using a query string (same syntax as the Gmail search bar)",
13880
+ description: "Search for emails in Gmail. Returns metadata by default; set includeBody=true to also return full message bodies (avoids needing separate read_email calls).",
13881
13881
  parameters: external_exports.object({
13882
13882
  /** Gmail search query (e.g. "from:alice subject:meeting"). */
13883
13883
  query: external_exports.string().describe("Gmail search query"),
13884
13884
  /** Maximum number of results to return (default 10, max 50). */
13885
- maxResults: external_exports.number().int().min(1).max(50).optional().default(10).describe("Maximum number of results to return")
13885
+ maxResults: external_exports.number().int().min(1).max(50).optional().default(10).describe("Maximum number of results to return"),
13886
+ /** When true, fetches full message bodies inline. Slower per message but
13887
+ * eliminates the need for separate read_email calls. */
13888
+ includeBody: external_exports.boolean().optional().default(false).describe("Include full message body in results")
13886
13889
  }),
13887
13890
  handler: async (args) => {
13888
13891
  try {
13889
13892
  const query = args.query;
13890
13893
  const maxResults = args.maxResults ?? 10;
13891
- logger.debug({ query, maxResults }, "search_emails called");
13894
+ const includeBody = args.includeBody ?? false;
13895
+ logger.debug({ query, maxResults, includeBody }, "search_emails called");
13892
13896
  const params = new URLSearchParams({
13893
13897
  q: query,
13894
13898
  maxResults: String(maxResults)
@@ -13905,24 +13909,28 @@ function createGmailTools(auth, logger) {
13905
13909
  if (!listData.messages?.length) {
13906
13910
  return "No emails found matching that query.";
13907
13911
  }
13912
+ const format = includeBody ? "full" : "metadata";
13908
13913
  const headers = await authHeaders(auth);
13909
13914
  const results = await Promise.all(
13910
13915
  listData.messages.map(async (msg) => {
13911
- const msgRes = await fetch(
13912
- `${GMAIL_API}/messages/${msg.id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`,
13913
- { headers }
13914
- );
13916
+ const url2 = includeBody ? `${GMAIL_API}/messages/${msg.id}?format=${format}` : `${GMAIL_API}/messages/${msg.id}?format=${format}&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`;
13917
+ const msgRes = await fetch(url2, { headers });
13915
13918
  if (!msgRes.ok) {
13916
- return { id: msg.id, error: `Failed to fetch (${msgRes.status})` };
13919
+ return { id: msg.id, threadId: msg.threadId, error: `Failed to fetch (${msgRes.status})` };
13917
13920
  }
13918
13921
  const msgData = await msgRes.json();
13919
- return {
13922
+ const result = {
13920
13923
  id: msgData.id,
13924
+ threadId: msgData.threadId,
13921
13925
  subject: getHeader(msgData.payload.headers, "Subject"),
13922
13926
  from: getHeader(msgData.payload.headers, "From"),
13923
13927
  date: getHeader(msgData.payload.headers, "Date"),
13924
13928
  snippet: msgData.snippet
13925
13929
  };
13930
+ if (includeBody) {
13931
+ result.body = extractBody(msgData.payload);
13932
+ }
13933
+ return result;
13926
13934
  })
13927
13935
  );
13928
13936
  return {
@@ -14030,7 +14038,135 @@ function createGmailTools(auth, logger) {
14030
14038
  }
14031
14039
  }
14032
14040
  };
14033
- return [searchEmails, readEmail, sendEmail];
14041
+ const getThread = {
14042
+ name: "get_thread",
14043
+ description: "Get all messages in a Gmail thread (including your sent replies). Use this to check if you already replied to a conversation before suggesting a new reply.",
14044
+ parameters: external_exports.object({
14045
+ /** The Gmail thread ID (returned by search_emails or read_email). */
14046
+ threadId: external_exports.string().describe("Gmail thread ID")
14047
+ }),
14048
+ handler: async (args) => {
14049
+ try {
14050
+ const threadId = args.threadId;
14051
+ logger.debug({ threadId }, "get_thread called");
14052
+ const res = await fetch(
14053
+ `${GMAIL_API}/threads/${threadId}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date`,
14054
+ { headers: await authHeaders(auth) }
14055
+ );
14056
+ if (!res.ok) {
14057
+ const errText = await res.text();
14058
+ logger.error({ status: res.status, errText }, "Gmail get_thread failed");
14059
+ return `Error getting thread (${res.status}): ${errText}`;
14060
+ }
14061
+ const data = await res.json();
14062
+ const messages = data.messages.map((msg) => ({
14063
+ id: msg.id,
14064
+ from: getHeader(msg.payload.headers, "From"),
14065
+ to: getHeader(msg.payload.headers, "To"),
14066
+ date: getHeader(msg.payload.headers, "Date"),
14067
+ snippet: msg.snippet,
14068
+ isSent: msg.labelIds?.includes("SENT") ?? false
14069
+ }));
14070
+ return {
14071
+ threadId: data.id,
14072
+ messageCount: messages.length,
14073
+ messages
14074
+ };
14075
+ } catch (error48) {
14076
+ const message = error48 instanceof Error ? error48.message : String(error48);
14077
+ logger.error({ error: message }, "get_thread error");
14078
+ return `Error getting thread: ${message}`;
14079
+ }
14080
+ }
14081
+ };
14082
+ const searchThreads = {
14083
+ name: "search_threads",
14084
+ description: "Search Gmail and return results grouped by thread. Each thread includes ALL messages (including your sent replies) with isSent flags, so you can tell at a glance whether you already replied. Ideal for inbox review \u2014 returns everything in a single call.",
14085
+ parameters: external_exports.object({
14086
+ /** Gmail search query (e.g. "in:inbox", "from:alice"). */
14087
+ query: external_exports.string().describe("Gmail search query"),
14088
+ /** Maximum threads to return (default 5, max 20). */
14089
+ maxThreads: external_exports.number().int().min(1).max(20).optional().default(5).describe("Maximum number of threads to return"),
14090
+ /** When true, includes the full decoded body of the latest message in
14091
+ * each thread. When false, only snippets are returned. */
14092
+ includeLatestBody: external_exports.boolean().optional().default(false).describe("Include full body of the latest message per thread")
14093
+ }),
14094
+ handler: async (args) => {
14095
+ try {
14096
+ const query = args.query;
14097
+ const maxThreads = args.maxThreads ?? 5;
14098
+ const includeLatestBody = args.includeLatestBody ?? false;
14099
+ logger.debug({ query, maxThreads, includeLatestBody }, "search_threads called");
14100
+ const params = new URLSearchParams({
14101
+ q: query,
14102
+ maxResults: String(maxThreads)
14103
+ });
14104
+ const listRes = await fetch(`${GMAIL_API}/threads?${params}`, {
14105
+ headers: await authHeaders(auth)
14106
+ });
14107
+ if (!listRes.ok) {
14108
+ const errText = await listRes.text();
14109
+ logger.error({ status: listRes.status, errText }, "Gmail threads search failed");
14110
+ return `Error searching threads (${listRes.status}): ${errText}`;
14111
+ }
14112
+ const listData = await listRes.json();
14113
+ if (!listData.threads?.length) {
14114
+ return { threadCount: 0, threads: [] };
14115
+ }
14116
+ const format = includeLatestBody ? "full" : "metadata";
14117
+ const headers = await authHeaders(auth);
14118
+ const threads = await Promise.all(
14119
+ listData.threads.map(async (t) => {
14120
+ const url2 = `${GMAIL_API}/threads/${t.id}?format=${format}&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date`;
14121
+ const res = await fetch(url2, { headers });
14122
+ if (!res.ok) {
14123
+ return { threadId: t.id, error: `Failed to fetch (${res.status})` };
14124
+ }
14125
+ const data = await res.json();
14126
+ const messages = data.messages.map((msg) => {
14127
+ const result = {
14128
+ id: msg.id,
14129
+ from: getHeader(msg.payload.headers, "From"),
14130
+ to: getHeader(msg.payload.headers, "To"),
14131
+ date: getHeader(msg.payload.headers, "Date"),
14132
+ snippet: msg.snippet,
14133
+ isSent: msg.labelIds?.includes("SENT") ?? false
14134
+ };
14135
+ return result;
14136
+ });
14137
+ const latest = data.messages[data.messages.length - 1];
14138
+ const lastIsSent = latest?.labelIds?.includes("SENT") ?? false;
14139
+ const subject = getHeader(
14140
+ data.messages[0].payload.headers,
14141
+ "Subject"
14142
+ );
14143
+ const thread = {
14144
+ threadId: data.id,
14145
+ subject,
14146
+ messageCount: messages.length,
14147
+ lastMessageIsSent: lastIsSent,
14148
+ messages
14149
+ };
14150
+ if (includeLatestBody && latest) {
14151
+ thread.latestBody = extractBody(
14152
+ latest.payload
14153
+ );
14154
+ }
14155
+ return thread;
14156
+ })
14157
+ );
14158
+ return {
14159
+ threadCount: threads.length,
14160
+ threads
14161
+ };
14162
+ } catch (error48) {
14163
+ const message = error48 instanceof Error ? error48.message : String(error48);
14164
+ logger.error({ error: message }, "search_threads error");
14165
+ return `Error searching threads: ${message}`;
14166
+ }
14167
+ }
14168
+ };
14169
+ return [searchEmails, readEmail, sendEmail, getThread, searchThreads];
14034
14170
  }
14035
14171
 
14036
14172
  // plugins/gmail/index.ts
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "gmail",
3
3
  "name": "Gmail Plugin",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "description": "Send, read, and search Gmail messages via the Gmail API",
6
6
  "author": "co-assistant",
7
7
  "requiredCredentials": [
@@ -117,7 +117,7 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
117
117
  const searchEmails: ToolDefinition = {
118
118
  name: "search_emails",
119
119
  description:
120
- "Search for emails in Gmail using a query string (same syntax as the Gmail search bar)",
120
+ "Search for emails in Gmail. Returns metadata by default; set includeBody=true to also return full message bodies (avoids needing separate read_email calls).",
121
121
  parameters: z.object({
122
122
  /** Gmail search query (e.g. "from:alice subject:meeting"). */
123
123
  query: z.string().describe("Gmail search query"),
@@ -130,13 +130,21 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
130
130
  .optional()
131
131
  .default(10)
132
132
  .describe("Maximum number of results to return"),
133
+ /** When true, fetches full message bodies inline. Slower per message but
134
+ * eliminates the need for separate read_email calls. */
135
+ includeBody: z
136
+ .boolean()
137
+ .optional()
138
+ .default(false)
139
+ .describe("Include full message body in results"),
133
140
  }),
134
141
 
135
142
  handler: async (args) => {
136
143
  try {
137
144
  const query = args.query as string;
138
145
  const maxResults = (args.maxResults as number | undefined) ?? 10;
139
- logger.debug({ query, maxResults }, "search_emails called");
146
+ const includeBody = (args.includeBody as boolean | undefined) ?? false;
147
+ logger.debug({ query, maxResults, includeBody }, "search_emails called");
140
148
 
141
149
  // Step 1 — List message IDs matching the query.
142
150
  const params = new URLSearchParams({
@@ -162,32 +170,47 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
162
170
  return "No emails found matching that query.";
163
171
  }
164
172
 
165
- // Step 2 — Fetch each message's metadata (Subject, From, Date, snippet).
173
+ // Step 2 — Fetch each message. Use "full" format when body is
174
+ // requested, otherwise "metadata" for a lighter response.
175
+ const format = includeBody ? "full" : "metadata";
166
176
  const headers = await authHeaders(auth);
167
177
  const results = await Promise.all(
168
178
  listData.messages.map(async (msg) => {
169
- const msgRes = await fetch(
170
- `${GMAIL_API}/messages/${msg.id}?format=metadata&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`,
171
- { headers },
172
- );
179
+ const url = includeBody
180
+ ? `${GMAIL_API}/messages/${msg.id}?format=${format}`
181
+ : `${GMAIL_API}/messages/${msg.id}?format=${format}&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=Date`;
182
+ const msgRes = await fetch(url, { headers });
173
183
 
174
184
  if (!msgRes.ok) {
175
- return { id: msg.id, error: `Failed to fetch (${msgRes.status})` };
185
+ return { id: msg.id, threadId: msg.threadId, error: `Failed to fetch (${msgRes.status})` };
176
186
  }
177
187
 
178
188
  const msgData = (await msgRes.json()) as {
179
189
  id: string;
190
+ threadId: string;
180
191
  snippet: string;
181
- payload: { headers: Array<{ name: string; value: string }> };
192
+ payload: {
193
+ headers: Array<{ name: string; value: string }>;
194
+ body?: { data?: string };
195
+ parts?: Array<Record<string, unknown>>;
196
+ mimeType?: string;
197
+ };
182
198
  };
183
199
 
184
- return {
200
+ const result: Record<string, unknown> = {
185
201
  id: msgData.id,
202
+ threadId: msgData.threadId,
186
203
  subject: getHeader(msgData.payload.headers, "Subject"),
187
204
  from: getHeader(msgData.payload.headers, "From"),
188
205
  date: getHeader(msgData.payload.headers, "Date"),
189
206
  snippet: msgData.snippet,
190
207
  };
208
+
209
+ if (includeBody) {
210
+ result.body = extractBody(msgData.payload as Record<string, unknown>);
211
+ }
212
+
213
+ return result;
191
214
  }),
192
215
  );
193
216
 
@@ -332,5 +355,211 @@ export function createGmailTools(auth: GmailAuth, logger: Logger): ToolDefinitio
332
355
  },
333
356
  };
334
357
 
335
- return [searchEmails, readEmail, sendEmail];
358
+ // -----------------------------------------------------------------------
359
+ // get_thread
360
+ // -----------------------------------------------------------------------
361
+ const getThread: ToolDefinition = {
362
+ name: "get_thread",
363
+ description:
364
+ "Get all messages in a Gmail thread (including your sent replies). " +
365
+ "Use this to check if you already replied to a conversation before suggesting a new reply.",
366
+ parameters: z.object({
367
+ /** The Gmail thread ID (returned by search_emails or read_email). */
368
+ threadId: z.string().describe("Gmail thread ID"),
369
+ }),
370
+
371
+ handler: async (args) => {
372
+ try {
373
+ const threadId = args.threadId as string;
374
+ logger.debug({ threadId }, "get_thread called");
375
+
376
+ const res = await fetch(
377
+ `${GMAIL_API}/threads/${threadId}?format=metadata` +
378
+ "&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date",
379
+ { headers: await authHeaders(auth) },
380
+ );
381
+
382
+ if (!res.ok) {
383
+ const errText = await res.text();
384
+ logger.error({ status: res.status, errText }, "Gmail get_thread failed");
385
+ return `Error getting thread (${res.status}): ${errText}`;
386
+ }
387
+
388
+ const data = (await res.json()) as {
389
+ id: string;
390
+ messages: Array<{
391
+ id: string;
392
+ labelIds: string[];
393
+ snippet: string;
394
+ payload: { headers: Array<{ name: string; value: string }> };
395
+ }>;
396
+ };
397
+
398
+ const messages = data.messages.map((msg) => ({
399
+ id: msg.id,
400
+ from: getHeader(msg.payload.headers, "From"),
401
+ to: getHeader(msg.payload.headers, "To"),
402
+ date: getHeader(msg.payload.headers, "Date"),
403
+ snippet: msg.snippet,
404
+ isSent: msg.labelIds?.includes("SENT") ?? false,
405
+ }));
406
+
407
+ return {
408
+ threadId: data.id,
409
+ messageCount: messages.length,
410
+ messages,
411
+ };
412
+ } catch (error) {
413
+ const message = error instanceof Error ? error.message : String(error);
414
+ logger.error({ error: message }, "get_thread error");
415
+ return `Error getting thread: ${message}`;
416
+ }
417
+ },
418
+ };
419
+
420
+ // -----------------------------------------------------------------------
421
+ // search_threads (single-call inbox analysis)
422
+ // -----------------------------------------------------------------------
423
+ const searchThreads: ToolDefinition = {
424
+ name: "search_threads",
425
+ description:
426
+ "Search Gmail and return results grouped by thread. Each thread includes " +
427
+ "ALL messages (including your sent replies) with isSent flags, so you can " +
428
+ "tell at a glance whether you already replied. Ideal for inbox review — " +
429
+ "returns everything in a single call.",
430
+ parameters: z.object({
431
+ /** Gmail search query (e.g. "in:inbox", "from:alice"). */
432
+ query: z.string().describe("Gmail search query"),
433
+ /** Maximum threads to return (default 5, max 20). */
434
+ maxThreads: z
435
+ .number()
436
+ .int()
437
+ .min(1)
438
+ .max(20)
439
+ .optional()
440
+ .default(5)
441
+ .describe("Maximum number of threads to return"),
442
+ /** When true, includes the full decoded body of the latest message in
443
+ * each thread. When false, only snippets are returned. */
444
+ includeLatestBody: z
445
+ .boolean()
446
+ .optional()
447
+ .default(false)
448
+ .describe("Include full body of the latest message per thread"),
449
+ }),
450
+
451
+ handler: async (args) => {
452
+ try {
453
+ const query = args.query as string;
454
+ const maxThreads = (args.maxThreads as number | undefined) ?? 5;
455
+ const includeLatestBody = (args.includeLatestBody as boolean | undefined) ?? false;
456
+ logger.debug({ query, maxThreads, includeLatestBody }, "search_threads called");
457
+
458
+ // Step 1 — List thread IDs matching the query.
459
+ const params = new URLSearchParams({
460
+ q: query,
461
+ maxResults: String(maxThreads),
462
+ });
463
+ const listRes = await fetch(`${GMAIL_API}/threads?${params}`, {
464
+ headers: await authHeaders(auth),
465
+ });
466
+
467
+ if (!listRes.ok) {
468
+ const errText = await listRes.text();
469
+ logger.error({ status: listRes.status, errText }, "Gmail threads search failed");
470
+ return `Error searching threads (${listRes.status}): ${errText}`;
471
+ }
472
+
473
+ const listData = (await listRes.json()) as {
474
+ threads?: Array<{ id: string; snippet: string }>;
475
+ resultSizeEstimate?: number;
476
+ };
477
+
478
+ if (!listData.threads?.length) {
479
+ return { threadCount: 0, threads: [] };
480
+ }
481
+
482
+ // Step 2 — Fetch each thread. Use "full" format for the latest
483
+ // message body when requested, "metadata" otherwise.
484
+ const format = includeLatestBody ? "full" : "metadata";
485
+ const headers = await authHeaders(auth);
486
+ const threads = await Promise.all(
487
+ listData.threads.map(async (t) => {
488
+ const url =
489
+ `${GMAIL_API}/threads/${t.id}?format=${format}` +
490
+ "&metadataHeaders=Subject&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Date";
491
+ const res = await fetch(url, { headers });
492
+
493
+ if (!res.ok) {
494
+ return { threadId: t.id, error: `Failed to fetch (${res.status})` };
495
+ }
496
+
497
+ const data = (await res.json()) as {
498
+ id: string;
499
+ messages: Array<{
500
+ id: string;
501
+ labelIds: string[];
502
+ snippet: string;
503
+ payload: {
504
+ headers: Array<{ name: string; value: string }>;
505
+ body?: { data?: string };
506
+ parts?: Array<Record<string, unknown>>;
507
+ mimeType?: string;
508
+ };
509
+ }>;
510
+ };
511
+
512
+ // Build a compact summary for each message in the thread.
513
+ const messages = data.messages.map((msg) => {
514
+ const result: Record<string, unknown> = {
515
+ id: msg.id,
516
+ from: getHeader(msg.payload.headers, "From"),
517
+ to: getHeader(msg.payload.headers, "To"),
518
+ date: getHeader(msg.payload.headers, "Date"),
519
+ snippet: msg.snippet,
520
+ isSent: msg.labelIds?.includes("SENT") ?? false,
521
+ };
522
+ return result;
523
+ });
524
+
525
+ // The last message in the array is the most recent.
526
+ const latest = data.messages[data.messages.length - 1];
527
+ const lastIsSent = latest?.labelIds?.includes("SENT") ?? false;
528
+ const subject = getHeader(
529
+ data.messages[0].payload.headers,
530
+ "Subject",
531
+ );
532
+
533
+ const thread: Record<string, unknown> = {
534
+ threadId: data.id,
535
+ subject,
536
+ messageCount: messages.length,
537
+ lastMessageIsSent: lastIsSent,
538
+ messages,
539
+ };
540
+
541
+ // Only decode the body for the latest message when requested.
542
+ if (includeLatestBody && latest) {
543
+ thread.latestBody = extractBody(
544
+ latest.payload as Record<string, unknown>,
545
+ );
546
+ }
547
+
548
+ return thread;
549
+ }),
550
+ );
551
+
552
+ return {
553
+ threadCount: threads.length,
554
+ threads,
555
+ };
556
+ } catch (error) {
557
+ const message = error instanceof Error ? error.message : String(error);
558
+ logger.error({ error: message }, "search_threads error");
559
+ return `Error searching threads: ${message}`;
560
+ }
561
+ },
562
+ };
563
+
564
+ return [searchEmails, readEmail, sendEmail, getThread, searchThreads];
336
565
  }