restruct and refact

This commit is contained in:
KS Jannette
2026-02-18 07:04:41 -05:00
parent 1094f9c972
commit 94b07531d0
7 changed files with 823 additions and 808 deletions

View File

@@ -1 +1,111 @@
//lines 701 - 804 from index.tz
import { z } from "zod";
import { server } from "../McpServer.js";
import {
accountSchema,
getCalendarClient,
getCalendarId,
} from "../loaders/prompt-config-loaders.js";
// ---------------------------------------------------------------------------
// 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 start = new Date(startDateTime);
if (start.getTime() < Date.now()) {
return {
content: [
{
type: "text" as const,
text:
`Skipped calendar event "${title}" — the proposed date ` +
`(${start.toISOString()}) is in the past.`,
},
],
};
}
const calendar = getCalendarClient(account);
const calendarId = getCalendarId(account);
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}`,
},
],
};
}
}
);

View File

@@ -1 +1,305 @@
//lines 265 - 523 index.ts
import { z } from "zod";
import { gmail_v1 } from "googleapis";
import fs from "fs";
import { server } from "../McpServer.js";
import {
accountSchema,
getGmailClient,
getSummaryPath,
loadClassificationPrompt,
loadActionPrompt,
} from "../loaders/prompt-config-loaders.js";
// ---------------------------------------------------------------------------
// Email parsing helpers
// ---------------------------------------------------------------------------
const getHeader = (
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 ?? "";
};
const decodeBody = (message: gmail_v1.Schema$Message): string => {
const parts = message.payload?.parts;
let encoded = "";
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) {
return message.snippet ?? "";
}
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)"),
},
},
async ({ account, maxResults }) => {
try {
const gmail = getGmailClient(account);
const listResponse = await gmail.users.messages.list({
userId: "me",
q: "is:unread",
maxResults,
});
const messageIds = listResponse.data.messages ?? [];
if (messageIds.length === 0) {
return {
content: [{ type: "text" as const, text: "No unread emails found." }],
};
}
const emails: string[] = [];
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 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")
);
}
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}`,
},
],
};
}
}
);
// ---------------------------------------------------------------------------
// 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"),
},
},
async ({ account, messageIds }) => {
try {
const gmail = getGmailClient(account);
const results: string[] = [];
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;
}
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"),
},
},
async ({ account, entries }) => {
try {
const summaryPath = getSummaryPath(account);
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,
}));
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;
fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
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}`,
},
],
};
}
}
);

View File

@@ -0,0 +1,179 @@
import { z } from "zod";
import { server } from "../McpServer.js";
import {
accountSchema,
getSheetsClient,
getSpreadsheetId,
} from "../loaders/prompt-config-loaders.js";
// ---------------------------------------------------------------------------
// 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 ?? "",
];
const existing = await sheets.spreadsheets.values.get({
spreadsheetId,
range: SHEET_RANGE,
});
const rows = existing.data.values ?? [];
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) {
const existingRow = rows[matchIdx];
const merged = incomingRow.map((val, col) => {
if (col === 5 && val && existingRow[col]) {
return `${existingRow[col]}; ${val}`;
}
return val || existingRow[col] || "";
});
const rowNum = matchIdx + 1;
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}.`,
},
],
};
}
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}`,
},
],
};
}
}
);