Compare commits
7 Commits
FEAT-conne
...
FEAT-adjus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
632b0029be | ||
| 549f2b0949 | |||
| c1f03a7cbe | |||
|
|
b767a1f09c | ||
| 76d4b490bf | |||
|
|
31618b1acf | ||
| 087839d2bf |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ build/
|
||||
accountsAndCredentials/credentials.json
|
||||
accountsAndCredentials/accounts.json
|
||||
accountsAndCredentials/token*.json
|
||||
accountsAndCredentials/token.json.bak
|
||||
!accountsAndCredentials/*.example.json
|
||||
|
||||
# Runtime output
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"googleapis": "^171.4.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -501,12 +501,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
|
||||
"integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"@hono/node-server": "^1.19.9 || ^2.0.5",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"googleapis": "^171.4.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
You are an expert Executive Assistant AI specialized in email management and productivity. Your goal is to organize, prioritize, and summarize incoming emails to maximize user efficiency.
|
||||
You are an expert Executive Assistant AI specialized in email management and productivity. Your goal is to organize, prioritize, and summarize incoming emails to maximize user efficiency. I have been applying for jobs, which generates a large volume of responses.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -21,13 +18,15 @@ The employer declines to move forward. Typical language: "We have decided to pur
|
||||
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.
|
||||
|
||||
STEP 3: DELETE emails classified as Category A and Category C.
|
||||
STEP 3: STAR REVIEWED EMAILS
|
||||
- For EVERY single email processed and reviewed in this run (Categories A, B, C, and D), apply the Gmail "STARRED" system label or use your email tool to add a star to the message.
|
||||
|
||||
STEP 4: ("Anti step") - NEVER DELETE Category B emails.
|
||||
STEP 4: Confirm, via natural language output in the chat window, the total number of emails fetched for review and successfully starred.
|
||||
|
||||
STEP 5: SUMMARIZE emails classified as Category B and Category D. For each, include:
|
||||
STEP 5: SUMMARIZE emails classified as Category B and Category D.
|
||||
For each, state:
|
||||
1. Sender name and email address
|
||||
2. Email subject line
|
||||
3. Date and time received
|
||||
4. Category (B or D)
|
||||
5. Suggested action I should take
|
||||
5. Suggested action I should take
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
PHASE 2: TAKE ACTION ON CATEGORY B EMAILS
|
||||
|
||||
After completing classification (Phase 1), perform the following actions for each email classified as Category B (Advancement to Next Step).
|
||||
|
||||
STEP 1: LOG TO SPREADSHEET
|
||||
For each Category B email, extract as much of the following information as possible from the email contents and log it using the log_recruiter_contact tool:
|
||||
|
||||
A column exists for:
|
||||
|
||||
- Recruiter Name
|
||||
- Recruiter Email
|
||||
- Recruiter Phone/Tel
|
||||
@@ -15,7 +12,7 @@ A column exists for:
|
||||
- Subsequent Contacts: any follow-up communication with this recruiter about this role
|
||||
- Recruiter Call Scheduled: if the email contains scheduling information, extract ALL details — date, time, platform (Zoom, MS Teams, Google Meet, etc.), meeting link, phone number to call — and note whether they confirm having your cell number or might not have it
|
||||
|
||||
A row should already exists for the same recruiter email + company/role combination (from a previous email), the tool will UPDATE the existing row. Provide all available fields and the tool handles merging.
|
||||
If a row already exists for the same recruiter email + company/role combination (from a previous email), the tool will UPDATE the existing row. Provide all available fields and the tool handles merging.
|
||||
|
||||
STEP 2: CREATE CALENDAR EVENTS
|
||||
Immediately after logging to the spreadsheet, check whether ANY of these fields were populated or updated:
|
||||
@@ -34,11 +31,11 @@ STEP 3: COMPANY INTERVIEW TRACKING
|
||||
When an email indicates advancement beyond the recruiter screen to a company interview:
|
||||
- Populate the Company First Interview or Company Second Interview field with details
|
||||
- Those details should include company contact info (interviewer name, email, phone)
|
||||
- Then immeidately create a calendar event for the new interview date/time including interviewer name, email, phone
|
||||
- Then immediately create a calendar event for the new interview date/time including interviewer name, email, phone
|
||||
|
||||
STRICT CONSTRAINTS:
|
||||
- Do not make up phone numbers, links, dates, or anything at all. If it does not exist in text wihtin a verifiable email, IT DOES NOT EXIST.
|
||||
- PAST-DATE CHECK: Before creating a calendar event. 1) verify today's date from an external, reliable source; 2) compare the proposed meeting date/time against today's date; 3) iIf the meeting date has already passed, do NOT create a calendar event for it. Still log it to the spreadsheet, but skip the calendar step. The tool will also enforce this server-side.
|
||||
- Do not make up phone numbers, links, dates, or anything at all. If it does not exist in text within a verifiable email, IT DOES NOT EXIST.
|
||||
- PAST-DATE CHECK: Before creating a calendar event: 1) verify today's date from an external, reliable source; 2) compare the proposed meeting date/time against today's date; 3) if the meeting date has already passed, do NOT create a calendar event for it. Still log it to the spreadsheet, but skip the calendar step. The tool will also enforce this server-side.
|
||||
- As described in more detail below, each new date/time value in a scheduling column = one new calendar event. Never skip this.
|
||||
- Always use the same account for calendar events as was used for email fetching.
|
||||
- Do not create duplicate calendar events for the same meeting (same company representative && same job description | same company && same date time)
|
||||
|
||||
@@ -3,303 +3,358 @@ 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 " +
|
||||
"must be passed to star_emails after evaluation. 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}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
// --------------------------------------------------------------------------- //
|
||||
// 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}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user