1 Commits

Author SHA1 Message Date
KS Jannette
d2be350c88 more 2026-02-17 13:27:50 -05:00
7 changed files with 76 additions and 448 deletions

2
.gitignore vendored
View File

@@ -9,4 +9,4 @@ accounts.json
# Runtime output # Runtime output
summary.json summary.json
summary-secondary.json token-secondary.json

View File

@@ -10,7 +10,7 @@ Infinitely mod-able. Dead simple. Privacy centric.
# Scope # Scope
## Current implementation contemplates: Claude Desktop + Custom, Local Model Context Protocol Server + Commercial SMTP Server (MX'configd properly) + your DNS-configd XXX.YYY ## Current implementation contemplates: Claude Desktop + Local Model Context Protocol Server + Commercial SMTP Server (MX'configd properly) + your DNS-configd XXX.YYY
--- ---

30
classify-emails.txt Normal file
View File

@@ -0,0 +1,30 @@
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. Review my inbox and apply the following instructions to all new 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.
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."
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 4: 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
4. Category (B or D)
5. Suggested action I should take

View File

@@ -6,43 +6,18 @@ import readline from "readline";
const __dirname = path.dirname(new URL(import.meta.url).pathname); const __dirname = path.dirname(new URL(import.meta.url).pathname);
const PROJECT_ROOT = path.resolve(__dirname, ".."); const PROJECT_ROOT = path.resolve(__dirname, "..");
const CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json"); const CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json");
const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json"); const TOKEN_PATH = path.join(PROJECT_ROOT, "token.json");
const SCOPES = [ // Gmail modify scope: read, send, delete, and manage labels
"https://www.googleapis.com/auth/gmail.modify", const SCOPES = ["https://www.googleapis.com/auth/gmail.modify"];
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/calendar.events",
];
function resolveTokenPath(accountKey: string): string {
if (!fs.existsSync(ACCOUNTS_PATH)) {
throw new Error(`Missing ${ACCOUNTS_PATH}. Create accounts.json first.`);
}
const accounts = JSON.parse(fs.readFileSync(ACCOUNTS_PATH, "utf-8"));
const acct = accounts[accountKey];
if (!acct) {
const available = Object.keys(accounts).join(", ");
throw new Error(
`Unknown account "${accountKey}". Available: ${available}`
);
}
return path.join(PROJECT_ROOT, acct.tokenFile);
}
async function authorize(): Promise<void> { async function authorize(): Promise<void> {
const accountKey = process.argv[2] || "work";
const tokenPath = resolveTokenPath(accountKey);
console.log(`Authorizing account: "${accountKey}"`);
console.log(`Token file: ${tokenPath}`);
console.log(`Scopes: ${SCOPES.join(", ")}\n`);
if (!fs.existsSync(CREDENTIALS_PATH)) { if (!fs.existsSync(CREDENTIALS_PATH)) {
console.error( console.error(
`Missing ${CREDENTIALS_PATH}\n\n` + `Missing ${CREDENTIALS_PATH}\n\n` +
"To create this file:\n" + "To create this file:\n" +
"1. Go to https://console.cloud.google.com/\n" + "1. Go to https://console.cloud.google.com/\n" +
"2. Create a project and enable the Gmail, Sheets, and Calendar APIs\n" + "2. Create a project and enable the Gmail API\n" +
"3. Create OAuth 2.0 credentials (Desktop app type)\n" + "3. Create OAuth 2.0 credentials (Desktop app type)\n" +
"4. Download the JSON and save it as credentials.json in the project root\n" "4. Download the JSON and save it as credentials.json in the project root\n"
); );
@@ -59,18 +34,16 @@ async function authorize(): Promise<void> {
redirect_uris[0] redirect_uris[0]
); );
if (fs.existsSync(tokenPath)) { // Check if we already have a valid token
const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8")); if (fs.existsSync(TOKEN_PATH)) {
const token = JSON.parse(fs.readFileSync(TOKEN_PATH, "utf-8"));
oAuth2Client.setCredentials(token); oAuth2Client.setCredentials(token);
console.log("Token already exists at:", tokenPath); console.log("Token already exists at:", TOKEN_PATH);
console.log( console.log("To re-authorize, delete token.json and run this script again.");
"To re-authorize with new scopes, delete the token file and run again:\n" +
` rm ${tokenPath}\n` +
` npm run auth -- ${accountKey}`
);
return; return;
} }
// Generate auth URL and prompt user
const authUrl = oAuth2Client.generateAuthUrl({ const authUrl = oAuth2Client.generateAuthUrl({
access_type: "offline", access_type: "offline",
scope: SCOPES, scope: SCOPES,
@@ -96,6 +69,7 @@ async function authorize(): Promise<void> {
); );
}); });
// Extract the code whether they pasted the full URL or just the code
let code = rawInput; let code = rawInput;
if (rawInput.includes("code=")) { if (rawInput.includes("code=")) {
const url = new URL(rawInput); const url = new URL(rawInput);
@@ -107,8 +81,8 @@ async function authorize(): Promise<void> {
const { tokens } = await oAuth2Client.getToken(code); const { tokens } = await oAuth2Client.getToken(code);
oAuth2Client.setCredentials(tokens); oAuth2Client.setCredentials(tokens);
fs.writeFileSync(tokenPath, JSON.stringify(tokens, null, 2)); fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2));
console.log("\nToken saved to:", tokenPath); console.log("\nToken saved to:", TOKEN_PATH);
console.log("You can now start the MCP server."); console.log("You can now start the MCP server.");
} }

View File

@@ -1,8 +1,7 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; import { z } from "zod";
import { google, gmail_v1, sheets_v4, calendar_v3 } from "googleapis"; import { google, gmail_v1 } from "googleapis";
import { OAuth2Client } from "google-auth-library";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
@@ -14,10 +13,7 @@ const __dirname = path.dirname(new URL(import.meta.url).pathname);
const PROJECT_ROOT = path.resolve(__dirname, ".."); const PROJECT_ROOT = path.resolve(__dirname, "..");
const CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json"); const CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json");
const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json"); const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json");
const CLASSIFY_PROMPT_PATH = path.join(PROJECT_ROOT, "classify-emails.txt"); const PROMPT_PATH = path.join(PROJECT_ROOT, "classify-emails.txt");
const ACTION_PROMPT_PATH = path.join(
PROJECT_ROOT, "src", "prompts", "take-action-on-emails.txt"
);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Account configuration // Account configuration
@@ -25,8 +21,6 @@ const ACTION_PROMPT_PATH = path.join(
interface AccountConfig { interface AccountConfig {
label: string; label: string;
tokenFile: string; tokenFile: string;
spreadsheetId?: string;
calendarId?: string;
} }
interface AccountsMap { interface AccountsMap {
@@ -60,37 +54,27 @@ function getSummaryPath(account: string): string {
} }
const VALID_ACCOUNTS = ["work", "secondary"] as const; const VALID_ACCOUNTS = ["work", "secondary"] as const;
const accounts = loadAccounts();
const accountDescription = VALID_ACCOUNTS
.map((key) => `"${key}" (${accounts[key]?.label ?? key})`)
.join(" or ");
const accountSchema = z const accountSchema = z
.enum(VALID_ACCOUNTS) .enum(VALID_ACCOUNTS)
.describe(`Which email account to use: ${accountDescription}`); .describe(
"Which email account to use: \"work\" (sj@sjdev.co) or \"secondary\" (ken.jannette@gmail.com)"
);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Prompt loaders // Load classification prompt
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function loadPromptFile(filePath: string, label: string): string { function loadClassificationPrompt(): string {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(PROMPT_PATH)) {
console.error(`Warning: ${filePath} not found. ${label} will be missing.`); console.error(`Warning: ${PROMPT_PATH} not found. Classification instructions will be missing.`);
return ""; return "";
} }
return fs.readFileSync(filePath, "utf-8"); return fs.readFileSync(PROMPT_PATH, "utf-8");
}
function loadClassificationPrompt(): string {
return loadPromptFile(CLASSIFY_PROMPT_PATH, "Classification instructions");
}
function loadActionPrompt(): string {
return loadPromptFile(ACTION_PROMPT_PATH, "Action instructions");
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Auth helpersgeneric OAuth2 client, then service-specific factories // Gmail auth helper — parameterized by account key
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function getOAuth2Client(account: string): OAuth2Client { function getGmailClient(account: string): gmail_v1.Gmail {
const tokenPath = getTokenPath(account); const tokenPath = getTokenPath(account);
if (!fs.existsSync(CREDENTIALS_PATH)) { if (!fs.existsSync(CREDENTIALS_PATH)) {
@@ -117,6 +101,7 @@ function getOAuth2Client(account: string): OAuth2Client {
const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8")); const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
oAuth2Client.setCredentials(token); oAuth2Client.setCredentials(token);
// Persist refreshed tokens automatically
oAuth2Client.on("tokens", (newTokens) => { oAuth2Client.on("tokens", (newTokens) => {
const current = JSON.parse(fs.readFileSync(tokenPath, "utf-8")); const current = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
fs.writeFileSync( fs.writeFileSync(
@@ -126,33 +111,7 @@ function getOAuth2Client(account: string): OAuth2Client {
console.error(`Token refreshed and saved for account "${account}".`); console.error(`Token refreshed and saved for account "${account}".`);
}); });
return oAuth2Client; return google.gmail({ version: "v1", auth: oAuth2Client });
}
function getGmailClient(account: string): gmail_v1.Gmail {
return google.gmail({ version: "v1", auth: getOAuth2Client(account) });
}
function getSheetsClient(account: string): sheets_v4.Sheets {
return google.sheets({ version: "v4", auth: getOAuth2Client(account) });
}
function getCalendarClient(account: string): calendar_v3.Calendar {
return google.calendar({ version: "v3", auth: getOAuth2Client(account) });
}
function getSpreadsheetId(account: string): string {
const acct = accounts[account];
if (!acct?.spreadsheetId || acct.spreadsheetId === "PASTE_YOUR_SHEET_ID_HERE") {
throw new Error(
`No spreadsheetId configured for account "${account}" in accounts.json.`
);
}
return acct.spreadsheetId;
}
function getCalendarId(account: string): string {
return accounts[account]?.calendarId ?? "primary";
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -210,11 +169,10 @@ server.registerPrompt(
"review_emails", "review_emails",
{ {
description: description:
`Review WORK inbox (${accounts.work?.label ?? "work"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`, "Review WORK inbox (sj@sjdev.co): classify job application emails (A/B/C/D), delete A+C, summarize B+D.",
}, },
() => { () => {
const phase1 = loadClassificationPrompt(); const instructions = loadClassificationPrompt();
const phase2 = loadActionPrompt();
return { return {
messages: [ messages: [
{ {
@@ -223,8 +181,8 @@ server.registerPrompt(
type: "text" as const, type: "text" as const,
text: text:
`ACCOUNT: Use account = "work" for ALL tool calls in this session.\n\n` + `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.") + (instructions ||
(phase2 ? `\n\n${phase2}` : ""), "Review my new emails and classify them by job application category."),
}, },
}, },
], ],
@@ -239,11 +197,10 @@ server.registerPrompt(
"review_secondary_emails", "review_secondary_emails",
{ {
description: description:
`Review SECONDARY inbox (${accounts.secondary?.label ?? "secondary"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`, "Review SECONDARY inbox (ken.jannette@gmail.com): classify job application emails (A/B/C/D), delete A+C, summarize B+D.",
}, },
() => { () => {
const phase1 = loadClassificationPrompt(); const instructions = loadClassificationPrompt();
const phase2 = loadActionPrompt();
return { return {
messages: [ messages: [
{ {
@@ -252,8 +209,8 @@ server.registerPrompt(
type: "text" as const, type: "text" as const,
text: text:
`ACCOUNT: Use account = "secondary" for ALL tool calls in this session.\n\n` + `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.") + (instructions ||
(phase2 ? `\n\n${phase2}` : ""), "Review my new emails and classify them by job application category."),
}, },
}, },
], ],
@@ -329,14 +286,9 @@ server.registerTool(
} }
const classificationInstructions = loadClassificationPrompt(); const classificationInstructions = loadClassificationPrompt();
const actionInstructions = loadActionPrompt(); const instructionsBlock = classificationInstructions
const instructionsBlock = ? `\n\n${"=".repeat(60)}\nCLASSIFICATION INSTRUCTIONS:\n${"=".repeat(60)}\n${classificationInstructions}`
(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 { return {
content: [ content: [
@@ -471,30 +423,15 @@ server.registerTool(
summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8")); summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8"));
} }
const now = new Date(); const now = new Date().toISOString();
const nowIso = now.toISOString();
const newEntries: SummaryEntry[] = entries.map((e) => ({ const newEntries: SummaryEntry[] = entries.map((e) => ({
...e, ...e,
addedAt: nowIso, addedAt: now,
})); }));
summary.push(...newEntries); summary.push(...newEntries);
// Purge entries older than 30 days
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.`
: "";
return { return {
content: [ content: [
{ {
@@ -502,8 +439,7 @@ server.registerTool(
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,
}, },
], ],
}; };
@@ -521,273 +457,6 @@ server.registerTool(
} }
); );
// ---------------------------------------------------------------------------
// Sheet column layout for "Recruiter Communication Log and Call Schedule"
// ---------------------------------------------------------------------------
const SHEET_COLUMNS = [
"Recruiter Name", // A
"Recruiter Email", // B
"Recruiter Tel", // C
"Company/Role", // D
"First Contact", // E
"Subsequent Contact(s)", // F
"Recruiter Call Scheduled", // G
"Company Contact Info", // H
"Company First Interview", // I
"Company Second Interview", // J
];
const SHEET_RANGE = "Sheet1";
// ---------------------------------------------------------------------------
// Tool: log_recruiter_contact
// ---------------------------------------------------------------------------
server.registerTool(
"log_recruiter_contact",
{
description:
"Log or update recruiter contact info in the tracking spreadsheet. " +
"If a row already exists for the same recruiterEmail + companyRole, it " +
"updates the existing row (merging non-empty fields). Otherwise appends " +
"a new row. Use for Category B emails after classification.",
inputSchema: {
account: accountSchema,
recruiterName: z.string().describe("Recruiter's full name"),
recruiterEmail: z.string().describe("Recruiter's email address"),
recruiterTel: z
.string()
.optional()
.describe("Recruiter's phone number, if mentioned in the email"),
companyRole: z
.string()
.describe("Company name and role the recruiter seeks to fill"),
firstContact: z
.string()
.describe("Date/time of first contact (from the email's Date header)"),
subsequentContacts: z
.string()
.optional()
.describe("Any follow-up contact context mentioned in the email"),
recruiterCallScheduled: z
.string()
.optional()
.describe(
"Scheduled call details: date, time, platform (Zoom/Teams), " +
"meeting link or phone, and whether they have your cell number"
),
companyContactInfo: z
.string()
.optional()
.describe("Company interviewer name/email/tel if advancing to company interview"),
companyFirstInterview: z
.string()
.optional()
.describe("Company first interview: date, time, platform, link/phone, cell number note"),
companySecondInterview: z
.string()
.optional()
.describe("Company second interview: date, time, platform, link/phone, cell number note"),
},
},
async ({
account,
recruiterName,
recruiterEmail,
recruiterTel,
companyRole,
firstContact,
subsequentContacts,
recruiterCallScheduled,
companyContactInfo,
companyFirstInterview,
companySecondInterview,
}) => {
try {
const sheets = getSheetsClient(account);
const spreadsheetId = getSpreadsheetId(account);
const incomingRow = [
recruiterName,
recruiterEmail,
recruiterTel ?? "",
companyRole,
firstContact,
subsequentContacts ?? "",
recruiterCallScheduled ?? "",
companyContactInfo ?? "",
companyFirstInterview ?? "",
companySecondInterview ?? "",
];
// Read existing data to check for a matching row
const existing = await sheets.spreadsheets.values.get({
spreadsheetId,
range: SHEET_RANGE,
});
const rows = existing.data.values ?? [];
// Find row matching recruiterEmail (col B) + companyRole (col D)
const emailNorm = recruiterEmail.toLowerCase().trim();
const roleNorm = companyRole.toLowerCase().trim();
let matchIdx = -1;
for (let i = 0; i < rows.length; i++) {
const rowEmail = (rows[i][1] ?? "").toString().toLowerCase().trim();
const rowRole = (rows[i][3] ?? "").toString().toLowerCase().trim();
if (rowEmail === emailNorm && rowRole === roleNorm) {
matchIdx = i;
break;
}
}
if (matchIdx >= 0) {
// Merge: keep existing values where incoming is empty
const existingRow = rows[matchIdx];
const merged = incomingRow.map((val, col) => {
if (col === 5 && val && existingRow[col]) {
// Subsequent Contacts: append rather than overwrite
return `${existingRow[col]}; ${val}`;
}
return val || existingRow[col] || "";
});
const rowNum = matchIdx + 1; // 1-indexed
await sheets.spreadsheets.values.update({
spreadsheetId,
range: `${SHEET_RANGE}!A${rowNum}:J${rowNum}`,
valueInputOption: "USER_ENTERED",
requestBody: { values: [merged] },
});
return {
content: [
{
type: "text" as const,
text: `Updated existing row ${rowNum} for ${recruiterName} / ${companyRole}.`,
},
],
};
}
// No match — append new row
await sheets.spreadsheets.values.append({
spreadsheetId,
range: SHEET_RANGE,
valueInputOption: "USER_ENTERED",
requestBody: { values: [incomingRow] },
});
return {
content: [
{
type: "text" as const,
text: `Appended new row for ${recruiterName} / ${companyRole}.`,
},
],
};
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error logging recruiter contact: ${errMsg}`,
},
],
};
}
}
);
// ---------------------------------------------------------------------------
// Tool: create_calendar_event
// ---------------------------------------------------------------------------
server.registerTool(
"create_calendar_event",
{
description:
"Create a Google Calendar event. Use immediately after logging a " +
"recruiter call, company first interview, or company second interview " +
"to the spreadsheet. One calendar event per scheduling column populated.",
inputSchema: {
account: accountSchema,
title: z
.string()
.describe(
"Event title, e.g. '[Recruiter Call] Acme Corp - Sr Engineer' " +
"or '[Interview] Acme Corp - Sr Engineer'"
),
startDateTime: z
.string()
.describe("Event start in ISO 8601 format, e.g. 2026-02-20T14:00:00-06:00"),
durationMinutes: z
.number()
.min(5)
.max(480)
.optional()
.describe("Duration in minutes (default 60)"),
description: z
.string()
.optional()
.describe(
"Event description: recruiter/company contact info, notes, " +
"whether they have your cell number, etc."
),
location: z
.string()
.optional()
.describe("Video meeting link (Zoom/Teams URL) or phone number"),
},
},
async ({ account, title, startDateTime, durationMinutes, description, location }) => {
try {
const calendar = getCalendarClient(account);
const calendarId = getCalendarId(account);
const start = new Date(startDateTime);
const end = new Date(start.getTime() + (durationMinutes ?? 60) * 60_000);
const event = await calendar.events.insert({
calendarId,
requestBody: {
summary: title,
start: {
dateTime: start.toISOString(),
},
end: {
dateTime: end.toISOString(),
},
description: description ?? "",
location: location ?? "",
},
});
return {
content: [
{
type: "text" as const,
text:
`Created calendar event: "${title}"\n` +
`Start: ${start.toISOString()}\n` +
`End: ${end.toISOString()}\n` +
`Event ID: ${event.data.id}\n` +
`Link: ${event.data.htmlLink}`,
},
],
};
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text" as const,
text: `Error creating calendar event: ${errMsg}`,
},
],
};
}
}
);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Start server // Start server
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -21,11 +21,10 @@ The email does not fit into categories A, B, or C. This includes non-job-applica
STEP 3: DELETE emails classified as Category A and Category C. STEP 3: DELETE emails classified as Category A and Category C.
STEP 4: ("Anti step") - NEVER DELETE Category B emails. STEP 4: 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, 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
4. Category (B or D) 4. Category (B or D)
5. Suggested action I should take 5. Suggested action I should take

View File

@@ -1,44 +0,0 @@
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
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
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.
- 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.
- If information is not present in the email, leave that field empty do ot guess.