2 Commits

2 changed files with 122 additions and 128 deletions

View File

@@ -3,11 +3,13 @@ import { google, gmail_v1, sheets_v4, calendar_v3 } from "googleapis";
import { OAuth2Client } from "google-auth-library"; import { OAuth2Client } from "google-auth-library";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { fileURLToPath } from "url";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Paths // 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, "..", ".."); export const PROJECT_ROOT = path.resolve(__dirname, "..", "..");
const SECRETS_DIR = path.join(PROJECT_ROOT, "accountsAndCredentials"); const SECRETS_DIR = path.join(PROJECT_ROOT, "accountsAndCredentials");
const CREDENTIALS_PATH = path.join(SECRETS_DIR, "credentials.json"); const CREDENTIALS_PATH = path.join(SECRETS_DIR, "credentials.json");
@@ -20,130 +22,130 @@ const ACTION_PROMPT_PATH = path.join(PROMPTS_DIR, "take-action-on-emails.txt");
// Account configuration // Account configuration
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface AccountConfig { export interface AccountConfig {
label: string; label: string;
tokenFile: string; tokenFile: string;
spreadsheetId?: string; spreadsheetId?: string;
calendarId?: string; calendarId?: string;
} }
export interface AccountsMap { export interface AccountsMap {
[key: string]: AccountConfig; [key: string]: AccountConfig;
} }
export const loadAccounts = (): AccountsMap => { export const loadAccounts = (): AccountsMap => {
if (!fs.existsSync(ACCOUNTS_PATH)) { if (!fs.existsSync(ACCOUNTS_PATH)) {
throw new Error(`Missing accounts.json at ${ACCOUNTS_PATH}.`); throw new Error(`Missing accounts.json at ${ACCOUNTS_PATH}.`);
} }
return JSON.parse(fs.readFileSync(ACCOUNTS_PATH, "utf-8")); return JSON.parse(fs.readFileSync(ACCOUNTS_PATH, "utf-8"));
}; };
export const getTokenPath = (account: string): string => { export const getTokenPath = (account: string): string => {
const accts = loadAccounts(); const accts = loadAccounts();
const acct = accts[account]; const acct = accts[account];
if (!acct) { if (!acct) {
const available = Object.keys(accts).join(", "); const available = Object.keys(accts).join(", ");
throw new Error( throw new Error(
`Unknown account "${account}". Available accounts: ${available}` `Unknown account "${account}". Available accounts: ${available}`
); );
} }
return path.join(SECRETS_DIR, acct.tokenFile); return path.join(SECRETS_DIR, acct.tokenFile);
}; };
export const getSummaryPath = (account: string): string => { export const getSummaryPath = (account: string): string => {
if (account === "work") { if (account === "work") {
return path.join(PROJECT_ROOT, "mailSummaries", "summary.json"); return path.join(PROJECT_ROOT, "mailSummaries", "summary.json");
} }
return path.join(PROJECT_ROOT, "mailSummaries", `summary-${account}.json`); return path.join(PROJECT_ROOT, "mailSummaries", `summary-${account}.json`);
}; };
export const VALID_ACCOUNTS = ["work", "secondary"] as const; export const VALID_ACCOUNTS = ["work", "secondary"] as const;
export const accounts = loadAccounts(); export const accounts = loadAccounts();
const accountDescription = VALID_ACCOUNTS const accountDescription = VALID_ACCOUNTS
.map((key) => `"${key}" (${accounts[key]?.label ?? key})`) .map((key) => `"${key}" (${accounts[key]?.label ?? key})`)
.join(" or "); .join(" or ");
export const accountSchema = z export const accountSchema = z
.enum(VALID_ACCOUNTS) .enum(VALID_ACCOUNTS)
.describe(`Which email account to use: ${accountDescription}`); .describe(`Which email account to use: ${accountDescription}`);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Prompt loaders // Prompt loaders
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const loadPromptFile = (filePath: string, label: string): string => { const loadPromptFile = (filePath: string, label: string): string => {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
console.error(`Warning: ${filePath} not found. ${label} will be missing.`); console.error(`Warning: ${filePath} not found. ${label} will be missing.`);
return ""; return "";
} }
return fs.readFileSync(filePath, "utf-8"); return fs.readFileSync(filePath, "utf-8");
}; };
export const loadClassificationPrompt = (): string => export const loadClassificationPrompt = (): string =>
loadPromptFile(CLASSIFY_PROMPT_PATH, "Classification instructions"); loadPromptFile(CLASSIFY_PROMPT_PATH, "Classification instructions");
export const loadActionPrompt = (): string => export const loadActionPrompt = (): string =>
loadPromptFile(ACTION_PROMPT_PATH, "Action instructions"); loadPromptFile(ACTION_PROMPT_PATH, "Action instructions");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Auth helpers — generic OAuth2 client, then service-specific factories // Auth helpers — generic OAuth2 client, then service-specific factories
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const getOAuth2Client = (account: string): OAuth2Client => { export const getOAuth2Client = (account: string): OAuth2Client => {
const tokenPath = getTokenPath(account); const tokenPath = getTokenPath(account);
if (!fs.existsSync(CREDENTIALS_PATH)) { if (!fs.existsSync(CREDENTIALS_PATH)) {
throw new Error( throw new Error(
`Missing credentials.json at ${CREDENTIALS_PATH}. Run "npm run auth" first.` `Missing credentials.json at ${CREDENTIALS_PATH}. Run "npm run auth" first.`
);
}
if (!fs.existsSync(tokenPath)) {
throw new Error(
`Missing token file at ${tokenPath} for account "${account}". Run "npm run auth" first.`
);
}
const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
const { client_id, client_secret, redirect_uris } =
credentials.installed || credentials.web;
const oAuth2Client = new google.auth.OAuth2(
client_id,
client_secret,
redirect_uris[0]
); );
}
if (!fs.existsSync(tokenPath)) {
throw new Error(
`Missing token file at ${tokenPath} for account "${account}". Run "npm run auth" first.`
);
}
const credentials = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8")); const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
const { client_id, client_secret, redirect_uris } = oAuth2Client.setCredentials(token);
credentials.installed || credentials.web;
const oAuth2Client = new google.auth.OAuth2( oAuth2Client.on("tokens", (newTokens) => {
client_id, const current = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
client_secret, fs.writeFileSync(
redirect_uris[0] tokenPath,
); JSON.stringify({ ...current, ...newTokens }, null, 2)
);
console.error(`Token refreshed and saved for account "${account}".`);
});
const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8")); return oAuth2Client;
oAuth2Client.setCredentials(token);
oAuth2Client.on("tokens", (newTokens) => {
const current = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
fs.writeFileSync(
tokenPath,
JSON.stringify({ ...current, ...newTokens }, null, 2)
);
console.error(`Token refreshed and saved for account "${account}".`);
});
return oAuth2Client;
}; };
export const getGmailClient = (account: string): gmail_v1.Gmail => export const getGmailClient = (account: string): gmail_v1.Gmail =>
google.gmail({ version: "v1", auth: getOAuth2Client(account) }); google.gmail({ version: "v1", auth: getOAuth2Client(account) });
export const getSheetsClient = (account: string): sheets_v4.Sheets => export const getSheetsClient = (account: string): sheets_v4.Sheets =>
google.sheets({ version: "v4", auth: getOAuth2Client(account) }); google.sheets({ version: "v4", auth: getOAuth2Client(account) });
export const getCalendarClient = (account: string): calendar_v3.Calendar => export const getCalendarClient = (account: string): calendar_v3.Calendar =>
google.calendar({ version: "v3", auth: getOAuth2Client(account) }); google.calendar({ version: "v3", auth: getOAuth2Client(account) });
export const getSpreadsheetId = (account: string): string => { export const getSpreadsheetId = (account: string): string => {
const acct = accounts[account]; const acct = accounts[account];
if (!acct?.spreadsheetId || acct.spreadsheetId === "PASTE_YOUR_SHEET_ID_HERE") { if (!acct?.spreadsheetId || acct.spreadsheetId === "PASTE_YOUR_SHEET_ID_HERE") {
throw new Error( throw new Error(
`No spreadsheetId configured for account "${account}" in accounts.json.` `No spreadsheetId configured for account "${account}" in accounts.json.`
); );
} }
return acct.spreadsheetId; return acct.spreadsheetId;
}; };
export const getCalendarId = (account: string): string => export const getCalendarId = (account: string): string =>
accounts[account]?.calendarId ?? "primary"; accounts[account]?.calendarId ?? "primary";

View File

@@ -1,64 +1,56 @@
import { server } from "../McpServer.js"; import { server } from "../McpServer.js";
import { import {
accounts, accounts,
loadClassificationPrompt, loadClassificationPrompt,
loadActionPrompt, loadActionPrompt,
} from "../loaders/prompt-config-loaders.js"; } from "../loaders/prompt-config-loaders.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Prompt: review_emails (work account) // Prompt: Classify Emails (Phase 1)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
server.registerPrompt( server.registerPrompt(
"review_emails", "classify-emails",
{ {
description: description: "Phase 1: Organize, prioritize, and summarize incoming recruiter emails.",
`Review WORK inbox (${accounts.work?.label ?? "work"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`, },
}, () => {
() => { const phase1 = loadClassificationPrompt();
const phase1 = loadClassificationPrompt(); return {
const phase2 = loadActionPrompt(); messages: [
return { {
messages: [ role: "user" as const,
{ content: {
role: "user" as const, type: "text" as const,
content: { text: `ACCOUNT: Use account = "work" for ALL tool calls in this session.\n\n` +
type: "text" as const, (phase1 || "Review my new emails and classify them by job application category.")
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}` : ""), };
}, }
},
],
};
}
); );
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Prompt: review_secondary_emails (secondary account) // Prompt: Take Action on Emails (Phase 2)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
server.registerPrompt( server.registerPrompt(
"review_secondary_emails", "take-action-on-emails",
{ {
description: description: "Phase 2: Log Category B recruiters to spreadsheet and create calendar events.",
`Review SECONDARY inbox (${accounts.secondary?.label ?? "secondary"}): classify emails, delete A+C, summarize B+D, log B to spreadsheet, create calendar events.`, },
}, () => {
() => { const phase2 = loadActionPrompt();
const phase1 = loadClassificationPrompt(); return {
const phase2 = loadActionPrompt(); messages: [
return { {
messages: [ role: "user" as const,
{ content: {
role: "user" as const, type: "text" as const,
content: { text: `ACCOUNT: Use account = "work" for ALL tool calls in this session.\n\n` +
type: "text" as const, (phase2 || "Process remaining action items for flagged recruiters.")
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}` : ""), };
}, }
},
],
};
}
); );