Compare commits
2 Commits
FEAT-conne
...
FEAT-incre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31618b1acf | ||
| 087839d2bf |
7
accountsAndCredentials/token.json.bak
Normal file
7
accountsAndCredentials/token.json.bak
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
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
|
STEP 1: Constraints
|
||||||
- Do not make up information.
|
- 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 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
|
1. Sender name and email address
|
||||||
2. Email subject line
|
2. Email subject line
|
||||||
3. Date and time received
|
3. Date and time received
|
||||||
|
|||||||
@@ -3,303 +3,303 @@ import { gmail_v1 } from "googleapis";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { server } from "../McpServer.js";
|
import { server } from "../McpServer.js";
|
||||||
import {
|
import {
|
||||||
accountSchema,
|
accountSchema,
|
||||||
getGmailClient,
|
getGmailClient,
|
||||||
getSummaryPath,
|
getSummaryPath,
|
||||||
loadClassificationPrompt,
|
loadClassificationPrompt,
|
||||||
loadActionPrompt,
|
loadActionPrompt,
|
||||||
} from "../loaders/prompt-config-loaders.js";
|
} from "../loaders/prompt-config-loaders.js";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Email parsing helpers
|
// Email parsing helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export const getHeader = (
|
export const getHeader = (
|
||||||
headers: gmail_v1.Schema$MessagePartHeader[] | undefined,
|
headers: gmail_v1.Schema$MessagePartHeader[] | undefined,
|
||||||
name: string
|
name: string
|
||||||
): string => {
|
): string => {
|
||||||
if (!headers) return "";
|
if (!headers) return "";
|
||||||
const header = headers.find(
|
const header = headers.find(
|
||||||
(h) => h.name?.toLowerCase() === name.toLowerCase()
|
(h) => h.name?.toLowerCase() === name.toLowerCase()
|
||||||
);
|
);
|
||||||
return header?.value ?? "";
|
return header?.value ?? "";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decodeBody = (message: gmail_v1.Schema$Message): string => {
|
export const decodeBody = (message: gmail_v1.Schema$Message): string => {
|
||||||
const parts = message.payload?.parts;
|
const parts = message.payload?.parts;
|
||||||
let encoded = "";
|
let encoded = "";
|
||||||
|
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const textPart = parts.find((p) => p.mimeType === "text/plain");
|
const textPart = parts.find((p) => p.mimeType === "text/plain");
|
||||||
encoded = textPart?.body?.data ?? "";
|
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) {
|
if (!encoded) {
|
||||||
const htmlPart = parts.find((p) => p.mimeType === "text/html");
|
return message.snippet ?? "";
|
||||||
encoded = htmlPart?.body?.data ?? "";
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
encoded = message.payload?.body?.data ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!encoded) {
|
return Buffer.from(encoded, "base64url").toString("utf-8");
|
||||||
return message.snippet ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return Buffer.from(encoded, "base64url").toString("utf-8");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tool: fetch_new_emails
|
// Tool: fetch_new_emails
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"fetch_new_emails",
|
"fetch_new_emails",
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
"Fetch unread emails from a Gmail inbox. Returns sender, date, " +
|
"Fetch unread emails from a Gmail inbox. Returns sender, date, " +
|
||||||
"subject, message ID, and body text for each message. The message IDs " +
|
"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.",
|
"can be passed to delete_emails later. Specify which account to fetch from.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
account: accountSchema,
|
account: accountSchema,
|
||||||
maxResults: z
|
maxResults: z
|
||||||
.number()
|
.number()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(100)
|
.max(200)
|
||||||
.describe("Maximum number of unread emails to fetch (1-100)"),
|
.describe("Maximum number of unread emails to fetch (1-200)"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
async ({ account, maxResults }) => {
|
||||||
async ({ account, maxResults }) => {
|
try {
|
||||||
try {
|
const gmail = getGmailClient(account);
|
||||||
const gmail = getGmailClient(account);
|
|
||||||
|
|
||||||
const listResponse = await gmail.users.messages.list({
|
const listResponse = await gmail.users.messages.list({
|
||||||
userId: "me",
|
userId: "me",
|
||||||
q: "is:unread",
|
q: "is:unread",
|
||||||
maxResults,
|
maxResults,
|
||||||
});
|
});
|
||||||
|
|
||||||
const messageIds = listResponse.data.messages ?? [];
|
const messageIds = listResponse.data.messages ?? [];
|
||||||
|
|
||||||
if (messageIds.length === 0) {
|
if (messageIds.length === 0) {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text" as const, text: "No unread emails found." }],
|
content: [{ type: "text" as const, text: "No unread emails found." }],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const emails: string[] = [];
|
const emails: string[] = [];
|
||||||
|
|
||||||
for (const msg of messageIds) {
|
for (const msg of messageIds) {
|
||||||
const detail = await gmail.users.messages.get({
|
const detail = await gmail.users.messages.get({
|
||||||
userId: "me",
|
userId: "me",
|
||||||
id: msg.id!,
|
id: msg.id!,
|
||||||
format: "full",
|
format: "full",
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers = detail.data.payload?.headers;
|
const headers = detail.data.payload?.headers;
|
||||||
const from = getHeader(headers, "From");
|
const from = getHeader(headers, "From");
|
||||||
const subject = getHeader(headers, "Subject");
|
const subject = getHeader(headers, "Subject");
|
||||||
const date = getHeader(headers, "Date");
|
const date = getHeader(headers, "Date");
|
||||||
const body = decodeBody(detail.data);
|
const body = decodeBody(detail.data);
|
||||||
|
|
||||||
const truncatedBody =
|
const truncatedBody =
|
||||||
body.length > 2000 ? body.substring(0, 2000) + "\n[...truncated]" : body;
|
body.length > 2000 ? body.substring(0, 2000) + "\n[...truncated]" : body;
|
||||||
|
|
||||||
emails.push(
|
emails.push(
|
||||||
[
|
[
|
||||||
`MESSAGE_ID: ${msg.id}`,
|
`MESSAGE_ID: ${msg.id}`,
|
||||||
`FROM: ${from}`,
|
`FROM: ${from}`,
|
||||||
`DATE: ${date}`,
|
`DATE: ${date}`,
|
||||||
`SUBJECT: ${subject}`,
|
`SUBJECT: ${subject}`,
|
||||||
`BODY:\n${truncatedBody}`,
|
`BODY:\n${truncatedBody}`,
|
||||||
].join("\n")
|
].join("\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const classificationInstructions = loadClassificationPrompt();
|
const classificationInstructions = loadClassificationPrompt();
|
||||||
const actionInstructions = loadActionPrompt();
|
const actionInstructions = loadActionPrompt();
|
||||||
const instructionsBlock =
|
const instructionsBlock =
|
||||||
(classificationInstructions
|
(classificationInstructions
|
||||||
? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS (PHASE 1):\n${"=".repeat(60)}\n${classificationInstructions}`
|
? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS (PHASE 1):\n${"=".repeat(60)}\n${classificationInstructions}`
|
||||||
: "") +
|
: "") +
|
||||||
(actionInstructions
|
(actionInstructions
|
||||||
? `\n\n${"=".repeat(60)}\nACTION INSTRUCTIONS (PHASE 2):\n${"=".repeat(60)}\n${actionInstructions}`
|
? `\n\n${"=".repeat(60)}\nACTION INSTRUCTIONS (PHASE 2):\n${"=".repeat(60)}\n${actionInstructions}`
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text:
|
text:
|
||||||
`Found ${emails.length} unread email(s):\n\n` +
|
`Found ${emails.length} unread email(s):\n\n` +
|
||||||
`${"=".repeat(60)}\n${emails.join(`\n${"=".repeat(60)}\n`)}` +
|
`${"=".repeat(60)}\n${emails.join(`\n${"=".repeat(60)}\n`)}` +
|
||||||
instructionsBlock,
|
instructionsBlock,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errMsg = error instanceof Error ? error.message : String(error);
|
const errMsg = error instanceof Error ? error.message : String(error);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: `Error fetching emails: ${errMsg}`,
|
text: `Error fetching emails: ${errMsg}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tool: delete_emails
|
// Tool: delete_emails
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"delete_emails",
|
"delete_emails",
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
"Move emails to trash by their Gmail message IDs. Use this for " +
|
"Move emails to trash by their Gmail message IDs. Use this for " +
|
||||||
"category A (acknowledgements) and category C (rejections) emails. " +
|
"category A (acknowledgements) and category C (rejections) emails. " +
|
||||||
"Specify which account the emails belong to.",
|
"Specify which account the emails belong to.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
account: accountSchema,
|
account: accountSchema,
|
||||||
messageIds: z
|
messageIds: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.describe("Array of Gmail message IDs to move to trash"),
|
.describe("Array of Gmail message IDs to move to trash"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
async ({ account, messageIds }) => {
|
||||||
async ({ account, messageIds }) => {
|
|
||||||
try {
|
|
||||||
const gmail = getGmailClient(account);
|
|
||||||
const results: string[] = [];
|
|
||||||
|
|
||||||
for (const id of messageIds) {
|
|
||||||
try {
|
try {
|
||||||
await gmail.users.messages.trash({
|
const gmail = getGmailClient(account);
|
||||||
userId: "me",
|
const results: string[] = [];
|
||||||
id,
|
|
||||||
});
|
|
||||||
results.push(`Trashed: ${id}`);
|
|
||||||
} catch (err) {
|
|
||||||
const errMsg = err instanceof Error ? err.message : String(err);
|
|
||||||
results.push(`Failed to trash ${id}: ${errMsg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
for (const id of messageIds) {
|
||||||
content: [
|
try {
|
||||||
{
|
await gmail.users.messages.trash({
|
||||||
type: "text" as const,
|
userId: "me",
|
||||||
text: `Delete results:\n${results.join("\n")}`,
|
id,
|
||||||
},
|
});
|
||||||
],
|
results.push(`Trashed: ${id}`);
|
||||||
};
|
} catch (err) {
|
||||||
} catch (error) {
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
const errMsg = error instanceof Error ? error.message : String(error);
|
results.push(`Failed to trash ${id}: ${errMsg}`);
|
||||||
return {
|
}
|
||||||
content: [
|
}
|
||||||
{
|
|
||||||
type: "text" as const,
|
return {
|
||||||
text: `Error deleting emails: ${errMsg}`,
|
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
|
// Tool: append_to_summary
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
interface SummaryEntry {
|
interface SummaryEntry {
|
||||||
senderName: string;
|
senderName: string;
|
||||||
senderEmail: string;
|
senderEmail: string;
|
||||||
dateReceived: string;
|
dateReceived: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
category: "B" | "D";
|
category: "B" | "D";
|
||||||
addedAt: string;
|
addedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"append_to_summary",
|
"append_to_summary",
|
||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
"Append classified email entries to the local summary file. Use this " +
|
"Append classified email entries to the local summary file. Use this " +
|
||||||
"for category B (advancement to next step) and category D (other) emails. " +
|
"for category B (advancement to next step) and category D (other) emails. " +
|
||||||
"Each entry records the sender, date, subject, and category. " +
|
"Each entry records the sender, date, subject, and category. " +
|
||||||
"Specify which account the emails belong to.",
|
"Specify which account the emails belong to.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
account: accountSchema,
|
account: accountSchema,
|
||||||
entries: z
|
entries: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
senderName: z.string().describe("Name of the sender"),
|
senderName: z.string().describe("Name of the sender"),
|
||||||
senderEmail: z.string().describe("Email address of the sender"),
|
senderEmail: z.string().describe("Email address of the sender"),
|
||||||
dateReceived: z
|
dateReceived: z
|
||||||
.string()
|
.string()
|
||||||
.describe("Date and time the email was received"),
|
.describe("Date and time the email was received"),
|
||||||
subject: z.string().describe("Email subject line"),
|
subject: z.string().describe("Email subject line"),
|
||||||
category: z
|
category: z
|
||||||
.enum(["B", "D"])
|
.enum(["B", "D"])
|
||||||
.describe(
|
.describe(
|
||||||
"Category: B = advancement to next step, D = other/uncategorized"
|
"Category: B = advancement to next step, D = other/uncategorized"
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.describe("Array of email summary entries to append"),
|
.describe("Array of email summary entries to append"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
async ({ account, entries }) => {
|
||||||
async ({ account, entries }) => {
|
try {
|
||||||
try {
|
const summaryPath = getSummaryPath(account);
|
||||||
const summaryPath = getSummaryPath(account);
|
|
||||||
|
|
||||||
let summary: SummaryEntry[] = [];
|
let summary: SummaryEntry[] = [];
|
||||||
if (fs.existsSync(summaryPath)) {
|
if (fs.existsSync(summaryPath)) {
|
||||||
summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8"));
|
summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const nowIso = now.toISOString();
|
const nowIso = now.toISOString();
|
||||||
const newEntries: SummaryEntry[] = entries.map((e) => ({
|
const newEntries: SummaryEntry[] = entries.map((e) => ({
|
||||||
...e,
|
...e,
|
||||||
addedAt: nowIso,
|
addedAt: nowIso,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
summary.push(...newEntries);
|
summary.push(...newEntries);
|
||||||
|
|
||||||
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
||||||
const cutoff = now.getTime() - RETENTION_MS;
|
const cutoff = now.getTime() - RETENTION_MS;
|
||||||
const beforePurge = summary.length;
|
const beforePurge = summary.length;
|
||||||
summary = summary.filter(
|
summary = summary.filter(
|
||||||
(entry) => new Date(entry.addedAt).getTime() >= cutoff
|
(entry) => new Date(entry.addedAt).getTime() >= cutoff
|
||||||
);
|
);
|
||||||
const purged = beforePurge - summary.length;
|
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
|
const purgeNote = purged > 0
|
||||||
? `\nPurged ${purged} entry/entries older than 30 days.`
|
? `\nPurged ${purged} entry/entries older than 30 days.`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text:
|
text:
|
||||||
`Appended ${newEntries.length} entry/entries to summary.\n` +
|
`Appended ${newEntries.length} entry/entries to summary.\n` +
|
||||||
`Total entries in summary: ${summary.length}\n` +
|
`Total entries in summary: ${summary.length}\n` +
|
||||||
`Summary file: ${summaryPath}` +
|
`Summary file: ${summaryPath}` +
|
||||||
purgeNote,
|
purgeNote,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errMsg = error instanceof Error ? error.message : String(error);
|
const errMsg = error instanceof Error ? error.message : String(error);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: `Error appending to summary: ${errMsg}`,
|
text: `Error appending to summary: ${errMsg}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user