From e7feb61e6f107bbc03b237a077824d809c70351a Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Mon, 31 Aug 2026 09:11:43 -0400 Subject: [PATCH 1/2] add start tool --- src/tools/tools-email.ts | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/tools/tools-email.ts b/src/tools/tools-email.ts index b8ba0e2..2985fc1 100644 --- a/src/tools/tools-email.ts +++ b/src/tools/tools-email.ts @@ -358,3 +358,58 @@ server.registerTool( } } ); +// --------------------------------------------------------------------------- // +// Tool: star_emails +// --------------------------------------------------------------------------- // +server.registerTool( + "star_emails", + { + description: "Mark reviewed emails with a star and mark them as read in Gmail. " + + "Use this for all reviewed messages (Categories A, B, C, and D) " + + "to signal they have been processed. Specify which account the emails belong to.", + inputSchema: { + account: accountSchema, + messageIds: z + .array(z.string()) + .describe("Array of Gmail message IDs to star and mark read"), + }, + }, + async ({ account, messageIds }) => { + try { + if (messageIds.length === 0) { + return { content: [{ type: "text" as const, text: "No message IDs provided." }] }; + } + + const gmail = getGmailClient(account); + + // Batch modify allows updating up to 1000 messages in a single API call + await gmail.users.messages.batchModify({ + userId: "me", + requestBody: { + ids: messageIds, + addLabelIds: ["STARRED"], + removeLabelIds: ["UNREAD"] + } + }); + + return { + content: [ + { + type: "text" as const, + text: `Successfully starred and marked read ${messageIds.length} email(s).`, + }, + ], + }; + } catch (error) { + const errMsg = error instanceof Error ? error.message : String(error); + return { + content: [ + { + type: "text" as const, + text: `Error starring emails: ${errMsg}`, + }, + ], + }; + } + } +); From 25ac1ec68fe9681d182cd811e662426989c32efb Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Mon, 31 Aug 2026 09:21:06 -0400 Subject: [PATCH 2/2] revise tool calls --- README.md | 16 ++++-- src/prompts/classify-emails.txt | 9 ++-- src/tools/tools-email.ts | 65 +++---------------------- test/integration/email-workflow.test.ts | 47 ++++++++++++++++++ 4 files changed, 68 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 353e91b..d9bb360 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,10 @@ The server fetches unread emails, classifies them using your configured prompts, | Category | Description | Default Action | |----------|-------------|----------------| -| **A** | Acknowledgements, auto-replies | Delete | -| **B** | Advancement (interview requests, next steps) | Log + Calendar event | -| **C** | Rejections | Delete | -| **D** | Other/uncategorized | Log for review | +| **A** | Acknowledgements, auto-replies | Star and mark read | +| **B** | Advancement (interview requests, next steps) | Log + Calendar event (never star) | +| **C** | Rejections | Star and mark read | +| **D** | Other/uncategorized | Star, mark read, and log for review | ### Customizing Prompts @@ -119,7 +119,7 @@ Changes take effect immediately without rebuilding. |------|-------------| | `fetch_new_emails` | Fetch unread emails with classification instructions appended | | `delete_emails` | Move specified message IDs to trash | -| `star_emails` | Star messages and mark as read | +| `star_emails` | Star Category A/C/D messages and mark as read | | `append_to_summary` | Log email metadata to local JSON (auto-purges after 30 days) | | `log_recruiter_contact` | Append or update a row in Google Sheets | | `create_calendar_event` | Create a Google Calendar event (skips past dates) | @@ -130,6 +130,12 @@ Changes take effect immediately without rebuilding. fetch_new_emails(account: "work", maxResults: 50) ``` +### Example: Star and Mark Read (Categories A, C, D only) + +``` +star_emails(account: "work", messageIds: ["msg-id-1", "msg-id-2"]) +``` + ### Example: Create Calendar Event ``` diff --git a/src/prompts/classify-emails.txt b/src/prompts/classify-emails.txt index 6e3c1c1..8e11624 100644 --- a/src/prompts/classify-emails.txt +++ b/src/prompts/classify-emails.txt @@ -9,23 +9,22 @@ STEP 2: CLASSIFY each email into exactly one category. Category A - Acknowledgement Only The employer confirms receipt of my application but requires no action from me. Typical language: "We received your application," "Your application is under review," "We will contact you if selected." No next steps are requested. -Action you should take: Add a "Star" using the gmail built-in actions. +Action you should take: Call star_emails with this message's ID (same account used for fetch_new_emails). That stars the email and marks it read. Category B - Advancement to Next Step The employer wants to move forward. This includes: invitations to schedule a phone screen or interview, requests to complete an assessment or assignment, requests for additional information or documents, or any communication that requires a response or action from me. If an email both acknowledges receipt AND requests action, classify it as B. Actions you should take: 1) add a calendar event with all pertinent info: a) person I will be meeting with, b) company c) role I will be discussing d) method for joining meeting - ie, telephone? If yes, will the representative all me, and is the number correct (718) 749-8292 c) or, is it Zoom, Google Meet, Webex or another video format, if so, include the link for joining -NEVER, EVER ADD A STAR TO A VATEGORY B EMAIL. +NEVER, EVER ADD A STAR TO A CATEGORY B EMAIL. Do not pass Category B message IDs to star_emails. Category C - Rejection The employer declines to move forward. Typical language: "We have decided to pursue other candidates," "Unfortunately, you were not selected," "We will not be moving forward with your application." -Action you should take: -Add a "Star" using the gmail built-in actions. +Action you should take: Call star_emails with this message's ID (same account used for fetch_new_emails). That stars the email and marks it read. Category D - Other The email does not fit into categories A, B, or C. This includes non-job-application emails, newsletters, promotional content, or ambiguous messages that do not clearly match another category. -Action you should take: Add a "Star" using the gmail built-in actions. +Action you should take: Call star_emails with this message's ID (same account used for fetch_new_emails). That stars the email and marks it read. STEP 3: SUMMARIZE emails classified as Category B and Category D in the chat in an organized list with headings. Example format for this: 1. Sender name and email address diff --git a/src/tools/tools-email.ts b/src/tools/tools-email.ts index 2985fc1..a3c364e 100644 --- a/src/tools/tools-email.ts +++ b/src/tools/tools-email.ts @@ -55,8 +55,9 @@ server.registerTool( { description: "Fetch unread emails from a Gmail inbox. Returns sender, date, " + - "subject, message ID, and body text for each message. The message IDs " + - "must be passed to star_emails after evaluation. Specify which account to fetch from.", + "subject, message ID, and body text for each message. After evaluation, " + + "pass Category A, C, and D message IDs to star_emails. Never pass " + + "Category B IDs to star_emails. Specify which account to fetch from.", inputSchema: { account: accountSchema, maxResults: z @@ -310,63 +311,9 @@ server.registerTool( "star_emails", { description: "Mark reviewed emails with a star and mark them as read in Gmail. " + - "Use this for all reviewed messages (Categories A, B, C, and D) " + - "to signal they have been processed. Specify which account the emails belong to.", - inputSchema: { - account: accountSchema, - messageIds: z - .array(z.string()) - .describe("Array of Gmail message IDs to star and mark read"), - }, - }, - async ({ account, messageIds }) => { - try { - if (messageIds.length === 0) { - return { content: [{ type: "text" as const, text: "No message IDs provided." }] }; - } - - const gmail = getGmailClient(account); - - // Batch modify allows updating up to 1000 messages in a single API call - await gmail.users.messages.batchModify({ - userId: "me", - requestBody: { - ids: messageIds, - addLabelIds: ["STARRED"], - removeLabelIds: ["UNREAD"] - } - }); - - return { - content: [ - { - type: "text" as const, - text: `Successfully starred and marked read ${messageIds.length} email(s).`, - }, - ], - }; - } catch (error) { - const errMsg = error instanceof Error ? error.message : String(error); - return { - content: [ - { - type: "text" as const, - text: `Error starring emails: ${errMsg}`, - }, - ], - }; - } - } -); -// --------------------------------------------------------------------------- // -// Tool: star_emails -// --------------------------------------------------------------------------- // -server.registerTool( - "star_emails", - { - description: "Mark reviewed emails with a star and mark them as read in Gmail. " + - "Use this for all reviewed messages (Categories A, B, C, and D) " + - "to signal they have been processed. Specify which account the emails belong to.", + "Use this for Category A (acknowledgements), C (rejections), and D (other) " + + "messages to signal they have been processed. Never use this for Category B " + + "(advancement) emails. Specify which account the emails belong to.", inputSchema: { account: accountSchema, messageIds: z diff --git a/test/integration/email-workflow.test.ts b/test/integration/email-workflow.test.ts index 0c5dea4..2ef99a9 100644 --- a/test/integration/email-workflow.test.ts +++ b/test/integration/email-workflow.test.ts @@ -14,6 +14,7 @@ import { MOCK_MESSAGE_LIST, MOCK_MESSAGES, MOCK_EMPTY_LIST } from "../fixtures/m const mockGmailList = vi.fn(); const mockGmailGet = vi.fn(); const mockGmailTrash = vi.fn(); +const mockGmailBatchModify = vi.fn(); let tmpDir: string; vi.mock("../../src/loaders/prompt-config-loaders.js", () => { @@ -45,6 +46,7 @@ vi.mock("../../src/loaders/prompt-config-loaders.js", () => { list: mockGmailList, get: mockGmailGet, trash: mockGmailTrash, + batchModify: mockGmailBatchModify, }, }, }), @@ -207,6 +209,51 @@ describe("Phase 1: Email Review Workflow", () => { }); }); + // ----- Step 2b: Star A, C, and D emails ----- + describe("star_emails", () => { + it("stars and marks read the specified message IDs", async () => { + mockGmailBatchModify.mockResolvedValue({}); + + const text = await callTool("star_emails", { + account: "work", + messageIds: ["msg-cat-a-001", "msg-cat-c-001", "msg-cat-d-001"], + }); + + expect(mockGmailBatchModify).toHaveBeenCalledTimes(1); + expect(mockGmailBatchModify).toHaveBeenCalledWith({ + userId: "me", + requestBody: { + ids: ["msg-cat-a-001", "msg-cat-c-001", "msg-cat-d-001"], + addLabelIds: ["STARRED"], + removeLabelIds: ["UNREAD"], + }, + }); + expect(text).toContain("Successfully starred and marked read 3 email(s)."); + }); + + it("returns early when no message IDs are provided", async () => { + const text = await callTool("star_emails", { + account: "work", + messageIds: [], + }); + + expect(mockGmailBatchModify).not.toHaveBeenCalled(); + expect(text).toBe("No message IDs provided."); + }); + + it("handles Gmail API errors gracefully", async () => { + mockGmailBatchModify.mockRejectedValue(new Error("Quota exceeded")); + + const text = await callTool("star_emails", { + account: "work", + messageIds: ["msg-cat-a-001"], + }); + + expect(text).toContain("Error starring emails"); + expect(text).toContain("Quota exceeded"); + }); + }); + // ----- Step 3: Summarize B and D ----- describe("append_to_summary", () => { it("creates a summary file and appends entries", async () => {