From cde346f42c5f71551bd0cb155755e7919eb70edb Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Tue, 17 Feb 2026 13:22:40 -0500 Subject: [PATCH] more --- README.md | 16 ++--- src/index.ts | 137 +++++++++++++++++++++++++++++++++-------- summary-secondary.json | 130 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+), 34 deletions(-) create mode 100644 summary-secondary.json diff --git a/README.md b/README.md index 58e80a6..c49cfb8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# Declawed (A Mail Bot) +# Declawed: A Configurable, Promptable AI Mail Assisty Kitty -A local Model Context Protocol (MCP) server connecing LLM platform APIs (ie Claude Desktop), or locally hosted models to your mail account. -Ssimple. -No Surprises, unlike the blue-plate-special-crustacean of the day. -Automates review, classification, and action -- response, deletion, archiving -- for inbox items. +A local Model Context Protocol (MCP) server and LLM integration platform. Built to connect to LLM APIs - be itClaude Desktop, *others*... or locally hosted models. Executes your prompts to manage your mail, while you... watch Fellini films, solve climate change, sip Mai Tais, or.... whatever. + +Ssssimple. Siamsese, if you please.... Eats the blue-plate-crustacean for breakfast. + +Infinitely mod-able. Dead simple. Privacy centric. --- # Scope -## Current implementaiton contemplates: Claude Desktop + Local Custom Model Context Protocol Server + Goog-MX'ed SMTP for your Registered, DNS'd XXX.YYY domain -(highly config'able- more to follow) +## Current implementation contemplates: Claude Desktop + Custom, Local Model Context Protocol Server + Commercial SMTP Server (MX'config’d properly) + your DNS-config’d XXX.YYY --- @@ -268,3 +268,5 @@ assistant/ ├── tsconfig.json # TypeScript compiler config └── README.md # This file ``` + + diff --git a/src/index.ts b/src/index.ts index 17994dd..24a43ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,10 +12,54 @@ import path from "path"; const __dirname = path.dirname(new URL(import.meta.url).pathname); const PROJECT_ROOT = path.resolve(__dirname, ".."); const CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json"); -const TOKEN_PATH = path.join(PROJECT_ROOT, "token.json"); -const SUMMARY_PATH = path.join(PROJECT_ROOT, "summary.json"); +const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json"); const PROMPT_PATH = path.join(PROJECT_ROOT, "classify-emails.txt"); +// --------------------------------------------------------------------------- +// Account configuration +// --------------------------------------------------------------------------- +interface AccountConfig { + label: string; + tokenFile: string; +} + +interface AccountsMap { + [key: string]: AccountConfig; +} + +function loadAccounts(): AccountsMap { + if (!fs.existsSync(ACCOUNTS_PATH)) { + throw new Error(`Missing accounts.json at ${ACCOUNTS_PATH}.`); + } + return JSON.parse(fs.readFileSync(ACCOUNTS_PATH, "utf-8")); +} + +function getTokenPath(account: string): string { + const accounts = loadAccounts(); + const acct = accounts[account]; + if (!acct) { + const available = Object.keys(accounts).join(", "); + throw new Error( + `Unknown account "${account}". Available accounts: ${available}` + ); + } + return path.join(PROJECT_ROOT, acct.tokenFile); +} + +function getSummaryPath(account: string): string { + if (account === "work") { + return path.join(PROJECT_ROOT, "summary.json"); + } + return path.join(PROJECT_ROOT, `summary-${account}.json`); +} + +const VALID_ACCOUNTS = ["work", "secondary"] as const; +const accountSchema = z + .enum(VALID_ACCOUNTS) + .describe( + "Which email account to use: \"work\" (sj@sjdev.co) or \"secondary\" (ken.jannette@gmail.com)" + ); + // --------------------------------------------------------------------------- // Load classification prompt // --------------------------------------------------------------------------- @@ -28,17 +72,19 @@ function loadClassificationPrompt(): string { } // --------------------------------------------------------------------------- -// Gmail auth helper +// Gmail auth helper — parameterized by account key // --------------------------------------------------------------------------- -function getGmailClient(): gmail_v1.Gmail { +function getGmailClient(account: string): gmail_v1.Gmail { + const tokenPath = getTokenPath(account); + if (!fs.existsSync(CREDENTIALS_PATH)) { throw new Error( `Missing credentials.json at ${CREDENTIALS_PATH}. Run "npm run auth" first.` ); } - if (!fs.existsSync(TOKEN_PATH)) { + if (!fs.existsSync(tokenPath)) { throw new Error( - `Missing token.json at ${TOKEN_PATH}. Run "npm run auth" first.` + `Missing token file at ${tokenPath} for account "${account}". Run "npm run auth" first.` ); } @@ -52,17 +98,17 @@ function getGmailClient(): gmail_v1.Gmail { redirect_uris[0] ); - const token = JSON.parse(fs.readFileSync(TOKEN_PATH, "utf-8")); + const token = JSON.parse(fs.readFileSync(tokenPath, "utf-8")); oAuth2Client.setCredentials(token); // Persist refreshed tokens automatically oAuth2Client.on("tokens", (newTokens) => { - const current = JSON.parse(fs.readFileSync(TOKEN_PATH, "utf-8")); + const current = JSON.parse(fs.readFileSync(tokenPath, "utf-8")); fs.writeFileSync( - TOKEN_PATH, + tokenPath, JSON.stringify({ ...current, ...newTokens }, null, 2) ); - console.error("Token refreshed and saved."); + console.error(`Token refreshed and saved for account "${account}".`); }); return google.gmail({ version: "v1", auth: oAuth2Client }); @@ -117,13 +163,13 @@ const server = new McpServer({ }); // --------------------------------------------------------------------------- -// Prompt: review_emails +// Prompt: review_emails (work account) // --------------------------------------------------------------------------- server.registerPrompt( "review_emails", { description: - "Review inbox, classify job application emails (A/B/C/D), delete A+C, summarize B+D.", + "Review WORK inbox (sj@sjdev.co): classify job application emails (A/B/C/D), delete A+C, summarize B+D.", }, () => { const instructions = loadClassificationPrompt(); @@ -133,8 +179,38 @@ server.registerPrompt( role: "user" as const, content: { type: "text" as const, - text: instructions || - "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` + + (instructions || + "Review my new emails and classify them by job application category."), + }, + }, + ], + }; + } +); + +// --------------------------------------------------------------------------- +// Prompt: review_secondary_emails (secondary account) +// --------------------------------------------------------------------------- +server.registerPrompt( + "review_secondary_emails", + { + description: + "Review SECONDARY inbox (ken.jannette@gmail.com): classify job application emails (A/B/C/D), delete A+C, summarize B+D.", + }, + () => { + const instructions = loadClassificationPrompt(); + return { + messages: [ + { + role: "user" as const, + content: { + type: "text" as const, + text: + `ACCOUNT: Use account = "secondary" for ALL tool calls in this session.\n\n` + + (instructions || + "Review my new emails and classify them by job application category."), }, }, ], @@ -149,10 +225,11 @@ server.registerTool( "fetch_new_emails", { description: - "Fetch unread emails from the 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 " + - "can be passed to delete_emails later.", + "can be passed to delete_emails later. Specify which account to fetch from.", inputSchema: { + account: accountSchema, maxResults: z .number() .min(1) @@ -160,9 +237,9 @@ server.registerTool( .describe("Maximum number of unread emails to fetch (1-100)"), }, }, - async ({ maxResults }) => { + async ({ account, maxResults }) => { try { - const gmail = getGmailClient(); + const gmail = getGmailClient(account); const listResponse = await gmail.users.messages.list({ userId: "me", @@ -246,16 +323,18 @@ server.registerTool( { description: "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.", inputSchema: { + account: accountSchema, messageIds: z .array(z.string()) .describe("Array of Gmail message IDs to move to trash"), }, }, - async ({ messageIds }) => { + async ({ account, messageIds }) => { try { - const gmail = getGmailClient(); + const gmail = getGmailClient(account); const results: string[] = []; for (const id of messageIds) { @@ -311,8 +390,10 @@ server.registerTool( 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.", + "Each entry records the sender, date, subject, and category. " + + "Specify which account the emails belong to.", inputSchema: { + account: accountSchema, entries: z .array( z.object({ @@ -332,12 +413,14 @@ server.registerTool( .describe("Array of email summary entries to append"), }, }, - async ({ entries }) => { + async ({ account, entries }) => { try { + const summaryPath = getSummaryPath(account); + // Load existing summary or start fresh let summary: SummaryEntry[] = []; - if (fs.existsSync(SUMMARY_PATH)) { - summary = JSON.parse(fs.readFileSync(SUMMARY_PATH, "utf-8")); + if (fs.existsSync(summaryPath)) { + summary = JSON.parse(fs.readFileSync(summaryPath, "utf-8")); } const now = new Date().toISOString(); @@ -347,7 +430,7 @@ server.registerTool( })); summary.push(...newEntries); - fs.writeFileSync(SUMMARY_PATH, JSON.stringify(summary, null, 2)); + fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2)); return { content: [ @@ -356,7 +439,7 @@ server.registerTool( text: `Appended ${newEntries.length} entry/entries to summary.\n` + `Total entries in summary: ${summary.length}\n` + - `Summary file: ${SUMMARY_PATH}`, + `Summary file: ${summaryPath}`, }, ], }; diff --git a/summary-secondary.json b/summary-secondary.json new file mode 100644 index 0000000..f384e02 --- /dev/null +++ b/summary-secondary.json @@ -0,0 +1,130 @@ +[ + { + "senderName": "Yelena Nikolov", + "senderEmail": "Yelena.Nikolov@meridianlink.com", + "dateReceived": "Tue, 17 Feb 2026 13:57:42 +0000", + "subject": "Interview next steps with MeridianLink", + "category": "B", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "LDEAskHR", + "senderEmail": "LDEAskHR@epiqglobal.com", + "dateReceived": "Fri, 13 Feb 2026 17:12:26 +0000", + "subject": "Immediate Action Requested - Background check", + "category": "B", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Talent Experience Team", + "senderEmail": "talentsupport@mbopartners.com", + "dateReceived": "Fri, 13 Feb 2026 17:03:27 +0000", + "subject": "[URGENT ACTION NEEDED!] FINAL NOTICE - Security Training - OVERDUE", + "category": "B", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Talent Experience Team", + "senderEmail": "talentsupport@mbopartners.com", + "dateReceived": "Fri, 13 Feb 2026 17:01:12 +0000", + "subject": "[ACTION NEEDED!] KPMG Ethics & Integrity Training for KPMG's Contingent Worker Training - OVERDUE", + "category": "B", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Penny from Rocket Money", + "senderEmail": "support@rocketmoney.com", + "dateReceived": "Tue, 17 Feb 2026 14:00:20 +0000", + "subject": "Re: [External] New Cell Number -m Cannot 2FA to My Account", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Mondaq Newsletters", + "senderEmail": "newsletters@webiis01.mondaq.com", + "dateReceived": "Tue, 17 Feb 2026 13:37:53 +0000", + "subject": "Mondaq Personalized News Alert", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "MoneyLion Partner", + "senderEmail": "partners@iemail.moneylion.com", + "dateReceived": "Tue, 17 Feb 2026 04:00:40 +0000", + "subject": "3 days left to view your personalized offers.", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "JobCopilot", + "senderEmail": "noreply@jobcopilot.com", + "dateReceived": "Mon, 16 Feb 2026 23:33:56 +0000", + "subject": "JobCopilot has applied to 50 jobs", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "no-reply@equifax.com", + "senderEmail": "no-reply@equifax.com", + "dateReceived": "Mon, 16 Feb 2026 21:01:48 +0000", + "subject": "New Equifax Credit Report Available", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Call-On-Doc", + "senderEmail": "no-reply@callondoc.com", + "dateReceived": "Mon, 16 Feb 2026 20:46:24 +0000", + "subject": "CallonDoc - Your Login Email has Been Changed.", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Bank of America", + "senderEmail": "customerservice@ealerts.bankofamerica.com", + "dateReceived": "Mon, 16 Feb 2026 09:55:20 -0600", + "subject": "Kenneth S Jannette sent you $150.00", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Concora Credit", + "senderEmail": "donotreply_concoracredit@mail.concoracredit.com", + "dateReceived": "Sat, 14 Feb 2026 16:01:10 -0600", + "subject": "Your Statement Is Ready To View", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "MBO Talent Experience & Support Team", + "senderEmail": "talentsupport@mbopartners.com", + "dateReceived": "Fri, 13 Feb 2026 10:44:08 -0600", + "subject": "KPMG Standard Work Hours and Overtime Policy", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Noun Project", + "senderEmail": "yourfriends@thenounproject.com", + "dateReceived": "Fri, 13 Feb 2026 01:00:53 +0000", + "subject": "Your Noun Project Invoice", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "The Termly Team", + "senderEmail": "termlyservices@email.termly.io", + "dateReceived": "Thu, 12 Feb 2026 20:16:59 +0000", + "subject": "Upcoming IAB TCF v2.3 update: disclosedVendors support", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + }, + { + "senderName": "Brave Search API", + "senderEmail": "search-api@brave.com", + "dateReceived": "Thu, 12 Feb 2026 19:57:43 +0000", + "subject": "New ToS & plans for the Brave Search API", + "category": "D", + "addedAt": "2026-02-17T14:10:30.911Z" + } +] \ No newline at end of file