Compare commits
13 Commits
b9337d1b0c
...
FEAT-refin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d17c409be | ||
|
|
f96a8541c6 | ||
|
|
8ad47f1d27 | ||
| 38c7e48a1d | |||
|
|
6817014b7f | ||
| dade7f1fb8 | |||
|
|
632b0029be | ||
| 549f2b0949 | |||
| c1f03a7cbe | |||
|
|
b767a1f09c | ||
| 76d4b490bf | |||
|
|
31618b1acf | ||
| 087839d2bf |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,7 +5,10 @@ build/
|
||||
accountsAndCredentials/credentials.json
|
||||
accountsAndCredentials/accounts.json
|
||||
accountsAndCredentials/token*.json
|
||||
accountsAndCredentials/token.json.bak
|
||||
!accountsAndCredentials/*.example.json
|
||||
|
||||
# Runtime output
|
||||
mailSummaries/
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -488,3 +488,4 @@ deClawed-Assity-Kitty/
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
// revised Aug 31
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_secret": "",
|
||||
"redirect_uris": ["http://localhost"]
|
||||
"redirect_uris": [
|
||||
"http://localhost"
|
||||
]
|
||||
}
|
||||
}
|
||||
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"
|
||||
},
|
||||
|
||||
@@ -4,3 +4,5 @@ export const server = new McpServer({
|
||||
name: "assistant",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -117,3 +117,5 @@ authorize().catch((err) => {
|
||||
console.error("Authorization failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -18,3 +18,5 @@ main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -3,11 +3,13 @@ import { google, gmail_v1, sheets_v4, calendar_v3 } from "googleapis";
|
||||
import { OAuth2Client } from "google-auth-library";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
// ---------------------------------------------------------------------------
|
||||
// Paths
|
||||
// ---------------------------------------------------------------------------
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
|
||||
const SECRETS_DIR = path.join(PROJECT_ROOT, "accountsAndCredentials");
|
||||
const CREDENTIALS_PATH = path.join(SECRETS_DIR, "credentials.json");
|
||||
@@ -147,3 +149,5 @@ export const getSpreadsheetId = (account: string): string => {
|
||||
|
||||
export const getCalendarId = (account: string): string =>
|
||||
accounts[account]?.calendarId ?? "primary";
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -6,27 +6,23 @@ import {
|
||||
} from "../loaders/prompt-config-loaders.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prompt: review_emails (work account)
|
||||
// Prompt: Classify Emails (Phase 1)
|
||||
// ---------------------------------------------------------------------------
|
||||
server.registerPrompt(
|
||||
"review_emails",
|
||||
"classify-emails",
|
||||
{
|
||||
description:
|
||||
`Review WORK inbox (${accounts.work?.label ?? "work"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`,
|
||||
description: "Phase 1: Organize, prioritize, and summarize incoming recruiter emails.",
|
||||
},
|
||||
() => {
|
||||
const phase1 = loadClassificationPrompt();
|
||||
const phase2 = loadActionPrompt();
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "user" as const,
|
||||
content: {
|
||||
type: "text" as const,
|
||||
text:
|
||||
`ACCOUNT: Use account = "work" for ALL tool calls in this session.\n\n` +
|
||||
(phase1 || "Review my new emails and classify them by job application category.") +
|
||||
(phase2 ? `\n\n${phase2}` : ""),
|
||||
text: `ACCOUNT: Use account = "work" for ALL tool calls in this session.\n\n` +
|
||||
(phase1 || "Review my new emails and classify them by job application category.")
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -35,16 +31,14 @@ server.registerPrompt(
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prompt: review_secondary_emails (secondary account)
|
||||
// Prompt: Take Action on Emails (Phase 2)
|
||||
// ---------------------------------------------------------------------------
|
||||
server.registerPrompt(
|
||||
"review_secondary_emails",
|
||||
"take-action-on-emails",
|
||||
{
|
||||
description:
|
||||
`Review SECONDARY inbox (${accounts.secondary?.label ?? "secondary"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`,
|
||||
description: "Phase 2: Log Category B recruiters to spreadsheet and create calendar events.",
|
||||
},
|
||||
() => {
|
||||
const phase1 = loadClassificationPrompt();
|
||||
const phase2 = loadActionPrompt();
|
||||
return {
|
||||
messages: [
|
||||
@@ -52,13 +46,13 @@ server.registerPrompt(
|
||||
role: "user" as const,
|
||||
content: {
|
||||
type: "text" as const,
|
||||
text:
|
||||
`ACCOUNT: Use account = "secondary" for ALL tool calls in this session.\n\n` +
|
||||
(phase1 || "Review my new emails and classify them by job application category.") +
|
||||
(phase2 ? `\n\n${phase2}` : ""),
|
||||
text: `ACCOUNT: Use account = "work" for ALL tool calls in this session.\n\n` +
|
||||
(phase2 || "Process remaining action items for flagged recruiters.")
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
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.
|
||||
|
||||
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.
|
||||
This does *not* include Greenhouse "security code" emails, other "confirm identity codes" or "identity-confirmation" type emails, "verify candidate account" type 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."
|
||||
@@ -21,13 +19,44 @@ 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 email processed and reviewed in this run that is in Category A, C, and D (NOT B), 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 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, under separate headers, in the chat.
|
||||
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
|
||||
|
||||
STEP 6: **STRICT COMPLIANCE REQUIRED** **DO NOT FORGET***:
|
||||
Summarize all "Category B" emails in the chat.
|
||||
Star all reviewed email
|
||||
|
||||
Next, perform the following actions for each "Category B" email:
|
||||
|
||||
STEP 7: CREATE CALENDAR EVENTS
|
||||
Immediately after logging to the spreadsheet, check whether ANY of these fields were populated or updated:
|
||||
- Recruiter Call Scheduled
|
||||
- Company First Interview
|
||||
- Company Second Interview
|
||||
|
||||
For EACH of the above fields that contains a date/time, create a Google Calendar event using the create_calendar_event tool:
|
||||
- Title format: "[<Recruiter Name> Call] Company - Role" or "[First Interview] Company - Role" or "[Second Interview] Company - Role"
|
||||
- Start date/time: extracted from the email (convert to ISO 8601)
|
||||
- Duration: 30 minutes unless otherwise specified in the email
|
||||
- Location: the video meeting link, phone number, or platform name
|
||||
- Description: include recruiter or company contact name/email/phone, and note whether they have your cell number
|
||||
|
||||
STRICT CONSTRAINTS:
|
||||
- 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)
|
||||
- If information is not present in the email, leave that field empty (it's OK!) - do not guess or make up information.
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
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
|
||||
- Company/Role the recruiter seeks to fill
|
||||
- First Contact: the date and time the email was received
|
||||
- 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.
|
||||
|
||||
STEP 2: CREATE CALENDAR EVENTS
|
||||
STEP 1: CREATE CALENDAR EVENTS
|
||||
Immediately after logging to the spreadsheet, check whether ANY of these fields were populated or updated:
|
||||
- Recruiter Call Scheduled
|
||||
- Company First Interview
|
||||
@@ -30,16 +14,12 @@ For EACH of the above fields that contains a date/time, create a Google Calendar
|
||||
- Location: the video meeting link, phone number, or platform name
|
||||
- Description: include recruiter or company contact name/email/phone, and note whether they have your cell number
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
- If information is not present in the email, leave that field empty (it's OK!) - do not guess or make up information.
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -109,3 +109,5 @@ server.registerTool(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -56,14 +56,14 @@ 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 " +
|
||||
"can be passed to delete_emails later. Specify which account to fetch from.",
|
||||
"must be passed to star_emails after evaluation. 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)"),
|
||||
.max(200)
|
||||
.describe("Maximum number of unread emails to fetch (1-200)"),
|
||||
},
|
||||
},
|
||||
async ({ account, maxResults }) => {
|
||||
@@ -303,3 +303,60 @@ 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}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -177,3 +177,5 @@ server.registerTool(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
2
test/fixtures/mock-emails.ts
vendored
2
test/fixtures/mock-emails.ts
vendored
@@ -165,3 +165,5 @@ export const RAW_BODIES = {
|
||||
"msg-cat-c-001": catCBody,
|
||||
"msg-cat-d-001": catDBody,
|
||||
};
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -285,3 +285,5 @@ describe("Phase 1: Email Review Workflow", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -84,3 +84,5 @@ describe("decodeBody", () => {
|
||||
expect(decodeBody(msg)).toBe(htmlContent);
|
||||
});
|
||||
});
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "test", "vitest.config.ts"]
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"test",
|
||||
"vitest.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -6,3 +6,5 @@ export default defineConfig({
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
|
||||
// revised Aug 31
|
||||
|
||||
Reference in New Issue
Block a user