From 31618b1acf8346b8bf300fd188e8de326f57e8ed Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Tue, 11 Aug 2026 02:12:43 -0400 Subject: [PATCH] upped maximum number of emails that can be fetched for review --- accountsAndCredentials/token.json.bak | 7 + src/prompts/classify-emails.txt | 6 +- src/tools/tools-email.ts | 476 +++++++++++++------------- 3 files changed, 249 insertions(+), 240 deletions(-) create mode 100644 accountsAndCredentials/token.json.bak diff --git a/accountsAndCredentials/token.json.bak b/accountsAndCredentials/token.json.bak new file mode 100644 index 0000000..ca357ee --- /dev/null +++ b/accountsAndCredentials/token.json.bak @@ -0,0 +1,7 @@ +{ + "access_token": "ya29.a0AT3oNZ9Db5cbvOq43MccPAw3bB6dmyKxfHkvLlHvvAnMGd-fQqyHYa0raFy5Tx0G0iJysQz_Hb9A-POPTp3_KJ7-6KpovEJPsgoTxy_jKlt81aa2CKKFWZlAClrvldH4I5XTJt6hyrDwu8fFvfBrpoqbVe0FK9yJWIMua3GAfChFjEebPMIqGF7VCwGjhXNMdOikNp_5SQaCgYKAagSARMSFQHGX2Miikn-iDccI4e7x_vlkScaaw0209", + "refresh_token": "1//0fD27YPvUVJODCgYIARAAGA8SNwF-L9IrSeT9ypacVvfAmW0K7J6M7PS4o4i4LBBLKwgm54lvNs1-VyIWnYav_88jWcP3Ugb_7Xw", + "scope": "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/gmail.modify", + "token_type": "Bearer", + "expiry_date": 1783036437865 +} \ No newline at end of file diff --git a/src/prompts/classify-emails.txt b/src/prompts/classify-emails.txt index 79f832d..4ad924e 100644 --- a/src/prompts/classify-emails.txt +++ b/src/prompts/classify-emails.txt @@ -2,7 +2,7 @@ You are an expert Executive Assistant AI specialized in email management and pro I have been applying for jobs, which generates a large volume of responses. -Review, in reverse chronoloical order, the first 150 emails in my inbox. Apply the following instructions to those emails. +Review the first 200 emails in my inbox. Apply the following instructions to those emails. STEP 1: Constraints - Do not make up information. @@ -25,7 +25,9 @@ STEP 3: DELETE emails classified as Category A and Category C. STEP 4: ("Anti step") - NEVER DELETE Category B emails. -STEP 5: SUMMARIZE emails classified as Category B and Category D. For each, include: +STEP 5: Confirm via natrual lanugage output in the cat, the total number of emails fetched for review. + +STEP 6: SUMMARIZE emails classified as Category B and Category D. For each, include: 1. Sender name and email address 2. Email subject line 3. Date and time received diff --git a/src/tools/tools-email.ts b/src/tools/tools-email.ts index df941e6..90cef7f 100644 --- a/src/tools/tools-email.ts +++ b/src/tools/tools-email.ts @@ -3,303 +3,303 @@ import { gmail_v1 } from "googleapis"; import fs from "fs"; import { server } from "../McpServer.js"; import { - accountSchema, - getGmailClient, - getSummaryPath, - loadClassificationPrompt, - loadActionPrompt, + accountSchema, + getGmailClient, + getSummaryPath, + loadClassificationPrompt, + loadActionPrompt, } from "../loaders/prompt-config-loaders.js"; // --------------------------------------------------------------------------- // Email parsing helpers // --------------------------------------------------------------------------- export const getHeader = ( - headers: gmail_v1.Schema$MessagePartHeader[] | undefined, - name: string + headers: gmail_v1.Schema$MessagePartHeader[] | undefined, + name: string ): string => { - if (!headers) return ""; - const header = headers.find( - (h) => h.name?.toLowerCase() === name.toLowerCase() - ); - return header?.value ?? ""; + if (!headers) return ""; + const header = headers.find( + (h) => h.name?.toLowerCase() === name.toLowerCase() + ); + return header?.value ?? ""; }; export const decodeBody = (message: gmail_v1.Schema$Message): string => { - const parts = message.payload?.parts; - let encoded = ""; + const parts = message.payload?.parts; + let encoded = ""; - if (parts) { - const textPart = parts.find((p) => p.mimeType === "text/plain"); - encoded = textPart?.body?.data ?? ""; + if (parts) { + const textPart = parts.find((p) => p.mimeType === "text/plain"); + encoded = textPart?.body?.data ?? ""; + + if (!encoded) { + const htmlPart = parts.find((p) => p.mimeType === "text/html"); + encoded = htmlPart?.body?.data ?? ""; + } + } else { + encoded = message.payload?.body?.data ?? ""; + } if (!encoded) { - const htmlPart = parts.find((p) => p.mimeType === "text/html"); - encoded = htmlPart?.body?.data ?? ""; + return message.snippet ?? ""; } - } else { - encoded = message.payload?.body?.data ?? ""; - } - if (!encoded) { - return message.snippet ?? ""; - } - - return Buffer.from(encoded, "base64url").toString("utf-8"); + return Buffer.from(encoded, "base64url").toString("utf-8"); }; // --------------------------------------------------------------------------- // Tool: fetch_new_emails // --------------------------------------------------------------------------- server.registerTool( - "fetch_new_emails", - { - description: - "Fetch unread emails from a Gmail inbox. Returns sender, date, " + - "subject, message ID, and body text for each message. The message IDs " + - "can be passed to delete_emails later. Specify which account to fetch from.", - inputSchema: { - account: accountSchema, - maxResults: z - .number() - .min(1) - .max(100) - .describe("Maximum number of unread emails to fetch (1-100)"), + "fetch_new_emails", + { + description: + "Fetch unread emails from a Gmail inbox. Returns sender, date, " + + "subject, message ID, and body text for each message. The message IDs " + + "can be passed to delete_emails later. Specify which account to fetch from.", + inputSchema: { + account: accountSchema, + maxResults: z + .number() + .min(1) + .max(200) + .describe("Maximum number of unread emails to fetch (1-200)"), + }, }, - }, - async ({ account, maxResults }) => { - try { - const gmail = getGmailClient(account); + async ({ account, maxResults }) => { + try { + const gmail = getGmailClient(account); - const listResponse = await gmail.users.messages.list({ - userId: "me", - q: "is:unread", - maxResults, - }); + const listResponse = await gmail.users.messages.list({ + userId: "me", + q: "is:unread", + maxResults, + }); - const messageIds = listResponse.data.messages ?? []; + const messageIds = listResponse.data.messages ?? []; - if (messageIds.length === 0) { - return { - content: [{ type: "text" as const, text: "No unread emails found." }], - }; - } + if (messageIds.length === 0) { + return { + content: [{ type: "text" as const, text: "No unread emails found." }], + }; + } - const emails: string[] = []; + const emails: string[] = []; - for (const msg of messageIds) { - const detail = await gmail.users.messages.get({ - userId: "me", - id: msg.id!, - format: "full", - }); + for (const msg of messageIds) { + const detail = await gmail.users.messages.get({ + userId: "me", + id: msg.id!, + format: "full", + }); - const headers = detail.data.payload?.headers; - const from = getHeader(headers, "From"); - const subject = getHeader(headers, "Subject"); - const date = getHeader(headers, "Date"); - const body = decodeBody(detail.data); + const headers = detail.data.payload?.headers; + const from = getHeader(headers, "From"); + const subject = getHeader(headers, "Subject"); + const date = getHeader(headers, "Date"); + const body = decodeBody(detail.data); - const truncatedBody = - body.length > 2000 ? body.substring(0, 2000) + "\n[...truncated]" : body; + const truncatedBody = + body.length > 2000 ? body.substring(0, 2000) + "\n[...truncated]" : body; - emails.push( - [ - `MESSAGE_ID: ${msg.id}`, - `FROM: ${from}`, - `DATE: ${date}`, - `SUBJECT: ${subject}`, - `BODY:\n${truncatedBody}`, - ].join("\n") - ); - } + emails.push( + [ + `MESSAGE_ID: ${msg.id}`, + `FROM: ${from}`, + `DATE: ${date}`, + `SUBJECT: ${subject}`, + `BODY:\n${truncatedBody}`, + ].join("\n") + ); + } - const classificationInstructions = loadClassificationPrompt(); - const actionInstructions = loadActionPrompt(); - const instructionsBlock = - (classificationInstructions - ? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS (PHASE 1):\n${"=".repeat(60)}\n${classificationInstructions}` - : "") + - (actionInstructions - ? `\n\n${"=".repeat(60)}\nACTION INSTRUCTIONS (PHASE 2):\n${"=".repeat(60)}\n${actionInstructions}` - : ""); + const classificationInstructions = loadClassificationPrompt(); + const actionInstructions = loadActionPrompt(); + const instructionsBlock = + (classificationInstructions + ? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS (PHASE 1):\n${"=".repeat(60)}\n${classificationInstructions}` + : "") + + (actionInstructions + ? `\n\n${"=".repeat(60)}\nACTION INSTRUCTIONS (PHASE 2):\n${"=".repeat(60)}\n${actionInstructions}` + : ""); - return { - content: [ - { - type: "text" as const, - text: - `Found ${emails.length} unread email(s):\n\n` + - `${"=".repeat(60)}\n${emails.join(`\n${"=".repeat(60)}\n`)}` + - instructionsBlock, - }, - ], - }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - return { - content: [ - { - type: "text" as const, - text: `Error fetching emails: ${errMsg}`, - }, - ], - }; + return { + content: [ + { + type: "text" as const, + text: + `Found ${emails.length} unread email(s):\n\n` + + `${"=".repeat(60)}\n${emails.join(`\n${"=".repeat(60)}\n`)}` + + instructionsBlock, + }, + ], + }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text" as const, + text: `Error fetching emails: ${errMsg}`, + }, + ], + }; + } } - } ); // --------------------------------------------------------------------------- // Tool: delete_emails // --------------------------------------------------------------------------- server.registerTool( - "delete_emails", - { - description: - "Move emails to trash by their Gmail message IDs. Use this for " + - "category A (acknowledgements) and category C (rejections) emails. " + - "Specify which account the emails belong to.", - inputSchema: { - account: accountSchema, - messageIds: z - .array(z.string()) - .describe("Array of Gmail message IDs to move to trash"), + "delete_emails", + { + description: + "Move emails to trash by their Gmail message IDs. Use this for " + + "category A (acknowledgements) and category C (rejections) emails. " + + "Specify which account the emails belong to.", + inputSchema: { + account: accountSchema, + messageIds: z + .array(z.string()) + .describe("Array of Gmail message IDs to move to trash"), + }, }, - }, - async ({ account, messageIds }) => { - try { - const gmail = getGmailClient(account); - const results: string[] = []; - - for (const id of messageIds) { + async ({ account, messageIds }) => { try { - await gmail.users.messages.trash({ - userId: "me", - id, - }); - results.push(`Trashed: ${id}`); - } catch (err) { - const errMsg = err instanceof Error ? err.message : String(err); - results.push(`Failed to trash ${id}: ${errMsg}`); - } - } + const gmail = getGmailClient(account); + const results: string[] = []; - return { - content: [ - { - type: "text" as const, - text: `Delete results:\n${results.join("\n")}`, - }, - ], - }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - return { - content: [ - { - type: "text" as const, - text: `Error deleting emails: ${errMsg}`, - }, - ], - }; + for (const id of messageIds) { + try { + await gmail.users.messages.trash({ + userId: "me", + id, + }); + results.push(`Trashed: ${id}`); + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + results.push(`Failed to trash ${id}: ${errMsg}`); + } + } + + return { + content: [ + { + type: "text" as const, + text: `Delete results:\n${results.join("\n")}`, + }, + ], + }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text" as const, + text: `Error deleting emails: ${errMsg}`, + }, + ], + }; + } } - } ); // --------------------------------------------------------------------------- // Tool: append_to_summary // --------------------------------------------------------------------------- interface SummaryEntry { - senderName: string; - senderEmail: string; - dateReceived: string; - subject: string; - category: "B" | "D"; - addedAt: string; + senderName: string; + senderEmail: string; + dateReceived: string; + subject: string; + category: "B" | "D"; + addedAt: string; } server.registerTool( - "append_to_summary", - { - description: - "Append classified email entries to the local summary file. Use this " + - "for category B (advancement to next step) and category D (other) emails. " + - "Each entry records the sender, date, subject, and category. " + - "Specify which account the emails belong to.", - inputSchema: { - account: accountSchema, - entries: z - .array( - z.object({ - senderName: z.string().describe("Name of the sender"), - senderEmail: z.string().describe("Email address of the sender"), - dateReceived: z - .string() - .describe("Date and time the email was received"), - subject: z.string().describe("Email subject line"), - category: z - .enum(["B", "D"]) - .describe( - "Category: B = advancement to next step, D = other/uncategorized" - ), - }) - ) - .describe("Array of email summary entries to append"), + "append_to_summary", + { + description: + "Append classified email entries to the local summary file. Use this " + + "for category B (advancement to next step) and category D (other) emails. " + + "Each entry records the sender, date, subject, and category. " + + "Specify which account the emails belong to.", + inputSchema: { + account: accountSchema, + entries: z + .array( + z.object({ + senderName: z.string().describe("Name of the sender"), + senderEmail: z.string().describe("Email address of the sender"), + dateReceived: z + .string() + .describe("Date and time the email was received"), + subject: z.string().describe("Email subject line"), + category: z + .enum(["B", "D"]) + .describe( + "Category: B = advancement to next step, D = other/uncategorized" + ), + }) + ) + .describe("Array of email summary entries to append"), + }, }, - }, - async ({ account, entries }) => { - try { - const summaryPath = getSummaryPath(account); + async ({ account, entries }) => { + try { + const summaryPath = getSummaryPath(account); - let summary: SummaryEntry[] = []; - if (fs.existsSync(summaryPath)) { - summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8")); - } + let summary: SummaryEntry[] = []; + if (fs.existsSync(summaryPath)) { + summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8")); + } - const now = new Date(); - const nowIso = now.toISOString(); - const newEntries: SummaryEntry[] = entries.map((e) => ({ - ...e, - addedAt: nowIso, - })); + const now = new Date(); + const nowIso = now.toISOString(); + const newEntries: SummaryEntry[] = entries.map((e) => ({ + ...e, + addedAt: nowIso, + })); - summary.push(...newEntries); + summary.push(...newEntries); - const RETENTION_MS = 30 * 24 * 60 * 60 * 1000; - const cutoff = now.getTime() - RETENTION_MS; - const beforePurge = summary.length; - summary = summary.filter( - (entry) => new Date(entry.addedAt).getTime() >= cutoff - ); - const purged = beforePurge - summary.length; + const RETENTION_MS = 30 * 24 * 60 * 60 * 1000; + const cutoff = now.getTime() - RETENTION_MS; + const beforePurge = summary.length; + summary = summary.filter( + (entry) => new Date(entry.addedAt).getTime() >= cutoff + ); + const purged = beforePurge - summary.length; - fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2)); + fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2)); - const purgeNote = purged > 0 - ? `\nPurged ${purged} entry/entries older than 30 days.` - : ""; + const purgeNote = purged > 0 + ? `\nPurged ${purged} entry/entries older than 30 days.` + : ""; - return { - content: [ - { - type: "text" as const, - text: - `Appended ${newEntries.length} entry/entries to summary.\n` + - `Total entries in summary: ${summary.length}\n` + - `Summary file: ${summaryPath}` + - purgeNote, - }, - ], - }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - return { - content: [ - { - type: "text" as const, - text: `Error appending to summary: ${errMsg}`, - }, - ], - }; + return { + content: [ + { + type: "text" as const, + text: + `Appended ${newEntries.length} entry/entries to summary.\n` + + `Total entries in summary: ${summary.length}\n` + + `Summary file: ${summaryPath}` + + purgeNote, + }, + ], + }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text" as const, + text: `Error appending to summary: ${errMsg}`, + }, + ], + }; + } } - } ); -- 2.43.0