Compare commits
41 Commits
refine-mai
...
db1603b6e5
| Author | SHA1 | Date | |
|---|---|---|---|
| db1603b6e5 | |||
|
|
25ac1ec68f | ||
|
|
e7feb61e6f | ||
| dac6f37331 | |||
|
|
54c6f7e81b | ||
|
|
55d0e77788 | ||
| aaef1b31c4 | |||
| c76725b111 | |||
| e88a0226b6 | |||
| cfef93a374 | |||
| 48017d958e | |||
| 7a6112479f | |||
|
|
94a0a485d9 | ||
|
|
cebfb0d589 | ||
|
|
c22d616595 | ||
|
|
cfcac8607c | ||
|
|
8ad47f1d27 | ||
| 38c7e48a1d | |||
|
|
6817014b7f | ||
| dade7f1fb8 | |||
|
|
632b0029be | ||
| 549f2b0949 | |||
| c1f03a7cbe | |||
|
|
b767a1f09c | ||
| 76d4b490bf | |||
|
|
31618b1acf | ||
| 087839d2bf | |||
| b9337d1b0c | |||
|
|
ee12a44ec1 | ||
| dbfa913e5e | |||
|
|
72ec89a1c6 | ||
|
|
a91c8c0009 | ||
|
|
8874b32334 | ||
|
|
f3cf4a51bd | ||
|
|
065645983a | ||
|
|
98585b52ad | ||
|
|
ea62edbc21 | ||
|
|
21b4e656f6 | ||
|
|
fc3223a119 | ||
|
|
9adc9cc3e0 | ||
|
|
7a02c49bd2 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,11 +1,12 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
# OAuth secrets
|
# OAuth secrets (inside accountsAndCredentials/, example files are NOT ignored)
|
||||||
credentials.json
|
accountsAndCredentials/credentials.json
|
||||||
credentials.*
|
accountsAndCredentials/accounts.json
|
||||||
token*.json
|
accountsAndCredentials/token*.json
|
||||||
accounts.json
|
accountsAndCredentials/token.json.bak
|
||||||
|
!accountsAndCredentials/*.example.json
|
||||||
|
|
||||||
# Runtime output
|
# Runtime output
|
||||||
mailSummaries/
|
mailSummaries/
|
||||||
|
|||||||
403
README.md
403
README.md
@@ -1,322 +1,163 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="assets/logo.png" alt="Declawed" width="120" />
|
<img src="assets/logo.png" alt="deClawed" width="120" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Declawed: your kuddly assisty-kitty
|
# deClawed
|
||||||
|
|
||||||
# Inbox brimming with stank-ass algorithmically-generated turds?
|
**LLM-powered email, meeting, teleconference and event triage and workflow automation via the Model Context Protocol**
|
||||||
|
|
||||||
# (Cat) nip that sh*t in the bud!!!
|
deClawed is an MCP server that connects email inbox to an LLM-capable client (such as Claude Desktop), within an orchestration harness: Agents have access to tools to classify incoming emails, automate routine actions (delete, archive, summarize). The framwork integrates with Google Mail, Sheets and Calendar to keep your high-volume email workflow organized and actionable.
|
||||||
|
|
||||||
### Declawed is a configurable, prompt purrr-fectable LLM mail management assisty-kitty.
|
|
||||||
### Puuurfect for tedious daily sorts/classification/responses
|
|
||||||
### Cuts down on the frustartion of feeling like you'll never get your paws on that catnip-stuffed mouse dangling from the string.
|
|
||||||
|
|
||||||
# Privacy
|
|
||||||
|
|
||||||
# More than the blue-plate crustacean
|
|
||||||
|
|
||||||
A local Model Context Protocol (MCP) server integrates with an LLM API you choose -- both of which offer greater ops and privacy control and transparency.
|
|
||||||
|
|
||||||
# Avoid flakey (molty?) black box installs.
|
|
||||||
|
|
||||||
Billed as QUICK AND EASY! ...loaded with more Trojans than an Olympic Village.
|
|
||||||
... Silent processes: data harvest phone-homes, behavioral analytic reporting, telemetry backdoors - all the nasties crawling the dark detritus. You know lobsters eat poop, right?
|
|
||||||
|
|
||||||
# Declawed: **actually** easy
|
|
||||||
|
|
||||||
## Stop drilling byzantine menus in mail, scehduling and sheets pltforms to 1. configure filters that don't even work 2. change every three months
|
|
||||||
|
|
||||||
## Short, declarative prompts are teh equivelent of wrapping slop, bulk-mail's feet in tin foil and tossing it in the bathtub.
|
|
||||||
|
|
||||||
### Prompts are discerning, unlike filters. For exaple, they keep the gold and bin the junk in forums/feeds where solid-gold insights are only 10% of the signal/noise.
|
|
||||||
### With refinement, prompting runs auto: cleaning that stank litter(in)box before you get a whiff.
|
|
||||||
### Morning greets you with a fresh, spring-meadow aroma of opportunities and insights -- not an avalanche of turds -- like its f*cxing 1998 again.
|
|
||||||
|
|
||||||
# Kitty connex are simple
|
|
||||||
Assisty-kitty easily interfaces with mail and calendar and other app APIs (about any other service you want to plug in) ... it keeps things moving so you can go you chase your tail or take a 19-hour nap in a sunbeam.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Scope and Architecture
|
## Getting Started
|
||||||
|
|
||||||
## Current implementation requires building/using:
|
### Requirements
|
||||||
-- Claude Desktop (as UI - proprietary UI coming soonish)
|
|
||||||
-- Building/Connecting a Local Model Context Protocol Server
|
|
||||||
-- A DNS-config’d domain, with MX records pointing to:
|
|
||||||
-- A commercial or self-hosted SMTP Serve
|
|
||||||
|
|
||||||
## Coming soonish:
|
| Dependency | Version |
|
||||||
-- Our own custom UI
|
|------------|---------|
|
||||||
-- Suppost for wiring up local LLMs.
|
| Node.js | 16+ |
|
||||||
|
| npm | 8+ |
|
||||||
|
| Google Cloud Project | With Gmail API enabled |
|
||||||
|
| MCP Client | Claude Desktop (or any MCP-compatible client) |
|
||||||
|
|
||||||
# Setup
|
### Installation
|
||||||
|
|
||||||
### 1. Install Node.js (v16+ required).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node --version
|
git clone https://git.sjdev.online/kjannette/deClawed-Assity-Kitty
|
||||||
npm --version
|
|
||||||
```
|
|
||||||
|
|
||||||
If not installed, grab it from [nodejs.org](https://nodejs.org/).
|
|
||||||
|
|
||||||
### 2. Clone and Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/kjannette/deClawed-Assisty-Kitty.git
|
|
||||||
cd deClawed-Assity-Kitty
|
cd deClawed-Assity-Kitty
|
||||||
npm install
|
npm install
|
||||||
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Set Up Google Cloud Credentials
|
### Google Cloud Setup
|
||||||
|
|
||||||
#### 3a. Create a Google Cloud Project
|
1. Create a project at [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Enable the **Gmail API** (and optionally **Sheets API** and **Calendar API**)
|
||||||
|
3. Configure OAuth consent screen (External, add your email as a test user)
|
||||||
|
4. Create OAuth credentials (Desktop app) and download `credentials.json`
|
||||||
|
5. Place `credentials.json` in the `accountsAndCredentials/` directory
|
||||||
|
|
||||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
### Account Configuration
|
||||||
2. Sign in with the Google account that owns the target Gmail
|
|
||||||
3. Click the project dropdown (top-left) > **New Project**
|
|
||||||
4. Name it (e.g., `assistant-mcp`) and click **Create**
|
|
||||||
5. Select the new project from the dropdown
|
|
||||||
|
|
||||||
#### 3b. Enable APIs
|
Create `accountsAndCredentials/accounts.json`:
|
||||||
|
|
||||||
In **APIs & Services > Library**, enable:
|
|
||||||
- **Gmail API**
|
|
||||||
- **Google Sheets API** (if mapping mail to sheets)
|
|
||||||
- **Google Calendar API** (if using calendar event creation, etc.s)
|
|
||||||
|
|
||||||
#### 3c. Configure the OAuth Consent Screen
|
|
||||||
|
|
||||||
1. Go to **Google Auth Platform > Branding** (or **APIs & Services > OAuth consent screen**)
|
|
||||||
2. Set user type to **External**, click **Create**
|
|
||||||
3. Fill in app name, support email, and developer contact email
|
|
||||||
4. Save and continue
|
|
||||||
|
|
||||||
#### 3d. Add OAuth Scopes
|
|
||||||
|
|
||||||
In **Google Auth Platform > Data Access**, add:
|
|
||||||
- `https://www.googleapis.com/auth/gmail.modify`
|
|
||||||
- `https://www.googleapis.com/auth/spreadsheets` (for Sheets integration)
|
|
||||||
- `https://www.googleapis.com/auth/calendar.events` (for Calendar integration)
|
|
||||||
|
|
||||||
#### 3e. Add Yourself as a Test User
|
|
||||||
|
|
||||||
Go to **Google Auth Platform > Audience** and add each Gmail address you'll use.
|
|
||||||
|
|
||||||
#### 3f. Create OAuth Client Credentials
|
|
||||||
|
|
||||||
1. Go to **Google Auth Platform > Clients** (or **APIs & Services > Credentials**)
|
|
||||||
2. Click **Create Client** > Application type: **Desktop app**
|
|
||||||
3. **Download the JSON**, rename it to `credentials.json`
|
|
||||||
4. Place it in the project root
|
|
||||||
|
|
||||||
### 4. Configure Accounts
|
|
||||||
|
|
||||||
Create file `accounts.json` in the project root. Each key is an account alias with its own token file and optional Sheets/Calendar config:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"work": {
|
"work": {
|
||||||
"label": "you@yourdomain.com",
|
"label": "you@example.com",
|
||||||
"tokenFile": "token.json",
|
"tokenFile": "token.json",
|
||||||
"spreadsheetId": "YOUR_GOOGLE_SHEET_ID",
|
"spreadsheetId": "YOUR_GOOGLE_SHEET_ID",
|
||||||
"calendarId": "primary"
|
"calendarId": "primary"
|
||||||
},
|
|
||||||
"secondary": {
|
|
||||||
"label": "you@gmail.com",
|
|
||||||
"tokenFile": "token-secondary.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **`label`** -- display name (typically the email address)
|
### Authorize Gmail Access
|
||||||
- **`tokenFile`** -- per-account OAuth token (auto-generated during auth)
|
|
||||||
- **`spreadsheetId`** -- Google Sheets ID for recruiter contact logging (optional)
|
|
||||||
- **`calendarId`** -- Google Calendar ID for event creation (optional, `"primary"` uses the default calendar)
|
|
||||||
|
|
||||||
### 5. Authorize Gmail Accounts
|
|
||||||
|
|
||||||
Build and run the auth script for each account:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run auth # authorizes the "work" account
|
npm run auth # Authorize default account
|
||||||
npm run auth -- secondary # authorizes the "secondary" account
|
npm run auth -- secondary # Authorize additional accounts
|
||||||
```
|
```
|
||||||
|
|
||||||
Each run will:
|
Follow the browser prompts to complete OAuth. Tokens are saved locally and auto-refresh.
|
||||||
1. Print a URL -- open it in your browser
|
|
||||||
2. Sign in and click **Allow**
|
|
||||||
3. You'll land on a "localhost refused to connect" page (normal)
|
|
||||||
4. Copy the **entire URL** from the address bar and paste it back into the terminal
|
|
||||||
5. The script saves the token file (e.g., `token.json` or `token-secondary.json`)
|
|
||||||
|
|
||||||
You only need to do this once per account. Tokens auto-refresh.
|
### Connect to Claude Desktop
|
||||||
|
|
||||||
### 6. Write the Prompts
|
Add the server to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
||||||
|
|
||||||
Two plain-text prompt files in `src/prompts/` control the workflow:
|
|
||||||
|
|
||||||
| File | Phase | Purpose |
|
|
||||||
|------|-------|---------|
|
|
||||||
| `src/prompts/classify-emails.txt` | 1 -- Classification | Defines categories A/B/C/D and how to sort emails |
|
|
||||||
| `src/prompts/take-action-on-emails.txt` | 2 -- Action | Tells the LLM what to do with each category (delete, log, schedule, etc.) |
|
|
||||||
|
|
||||||
**Tips:**
|
|
||||||
- Use clear, explicit category definitions with example language
|
|
||||||
- Handle ambiguous cases (e.g., "If an email both acknowledges receipt AND requests action, classify as B")
|
|
||||||
- Prompt files are loaded at runtime -- edit them anytime, no rebuild required
|
|
||||||
|
|
||||||
### 7. Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Compiles `src/**/*.ts` into `build/`.
|
|
||||||
|
|
||||||
## 8. Configure Claude Desktop
|
|
||||||
|
|
||||||
Edit your Claude Desktop config:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Add the server:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"assistant": {
|
"assistant": {
|
||||||
"command": "/ABSOLUTE/PATH/TO/node",
|
"command": "/path/to/node",
|
||||||
"args": [
|
"args": ["/path/to/deClawed-Assity-Kitty/build/index.js"]
|
||||||
"/ABSOLUTE/PATH/TO/deClawed-Assity-Kitty/build/index.js"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace paths with the output of `which node` and your actual project location.
|
Restart Claude Desktop. The server appears under **Connectors**.
|
||||||
|
|
||||||
## 9. Add/Configure API Keys, Credentials, and Other Secrets
|
|
||||||
|
|
||||||
Secret files are **gitignored** -- they never leave your machine. Example templates are provided so you know what shape each file needs to be in.
|
|
||||||
|
|
||||||
#### 9a. `credentials.json`
|
|
||||||
|
|
||||||
This file holds your Google OAuth client credentials. **You do not write this by hand** -- it is downloaded from the Google Cloud Console (see Step 3f above). Copy the example and then replace it with the real download:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp credentials.example.json credentials.json
|
|
||||||
# Now replace credentials.json with the file downloaded from Google Cloud Console.
|
|
||||||
```
|
|
||||||
|
|
||||||
The structure looks like this (the example file ships with empty values):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"installed": {
|
|
||||||
"client_id": "",
|
|
||||||
"project_id": "",
|
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 9b. `accounts.json`
|
|
||||||
|
|
||||||
Defines each Gmail account the server manages. Copy the example and fill in your values:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp accounts.example.json accounts.json
|
|
||||||
```
|
|
||||||
|
|
||||||
| Field | What to put here |
|
|
||||||
|-------|-----------------|
|
|
||||||
| `label` | The email address for this account (display only) |
|
|
||||||
| `tokenFile` | Filename for this account's OAuth token (e.g., `token.json`) |
|
|
||||||
| `spreadsheetId` | The ID from your Google Sheet URL: `docs.google.com/spreadsheets/d/<THIS_PART>/edit` (optional) |
|
|
||||||
| `calendarId` | `"primary"` for your default calendar, or a specific calendar ID (optional) |
|
|
||||||
|
|
||||||
Add as many accounts as you need. Each key (e.g., `"work"`, `"secondary"`) becomes the account name used in tool calls and auth commands.
|
|
||||||
|
|
||||||
#### 9c. `token*.json` (auto-generated)
|
|
||||||
|
|
||||||
Token files are **created automatically** when you run `npm run auth` (Step 5). You do not need to create or edit them manually. A `token.example.json` is provided for reference only -- it shows the structure but the values are populated by the OAuth flow.
|
|
||||||
|
|
||||||
#### Summary of secret files
|
|
||||||
|
|
||||||
| Example template | Actual file (gitignored) | How to create |
|
|
||||||
|-----------------|--------------------------|---------------|
|
|
||||||
| `credentials.example.json` | `credentials.json` | Download from Google Cloud Console |
|
|
||||||
| `accounts.example.json` | `accounts.json` | Copy example, fill in your email/sheet/calendar IDs |
|
|
||||||
| `token.example.json` | `token.json`, `token-secondary.json`, etc. | Auto-generated by `npm run auth` |
|
|
||||||
|
|
||||||
### 10. Restart Claude Desktop
|
|
||||||
|
|
||||||
Fully quit (**Cmd+Q**, not just close the window) and reopen. The `assistant` server should appear under **Connectors**.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MCP Tools
|
## Usage
|
||||||
|
|
||||||
|
### Basic Workflow
|
||||||
|
|
||||||
|
In Claude Desktop, invoke the email review workflow:
|
||||||
|
|
||||||
|
```
|
||||||
|
Review my inbox
|
||||||
|
```
|
||||||
|
|
||||||
|
The server fetches unread emails, classifies them using your configured prompts, and executes the appropriate actions.
|
||||||
|
|
||||||
|
### Classification Categories
|
||||||
|
|
||||||
|
| Category | Description | Default Action |
|
||||||
|
|----------|-------------|----------------|
|
||||||
|
| **A** | Acknowledgements, auto-replies | Star and mark read |
|
||||||
|
| **B** | Advancement (interview requests, next steps) | Log + Calendar event (never star) |
|
||||||
|
| **C** | Rejections | Star and mark read |
|
||||||
|
| **D** | Other/uncategorized | Star, mark read, and log for review |
|
||||||
|
|
||||||
|
### Customizing Prompts
|
||||||
|
|
||||||
|
Edit the plain-text files in `src/prompts/` to adjust classification rules and actions:
|
||||||
|
|
||||||
|
- `classify-emails.txt` — Defines category criteria
|
||||||
|
- `take-action-on-emails.txt` — Specifies actions per category
|
||||||
|
|
||||||
|
Changes take effect immediately without rebuilding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP Tools Reference
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `fetch_new_emails` | Fetches unread emails for a given account. Classification and action prompts are automatically appended to the response. |
|
| `fetch_new_emails` | Fetch unread emails with classification instructions appended |
|
||||||
| `delete_emails` | Moves emails to trash by Gmail message ID. Used for categories A (acknowledgements/junk/”THANKS!”/offers) and C (rejections). |
|
| `delete_emails` | Move specified message IDs to trash |
|
||||||
| `append_to_summary` | Logs classified emails to per-account summary files in `mailSummaries/`. Entries older than 30 days are auto-purged. |
|
| `star_emails` | Star Category A/C/D messages and mark as read |
|
||||||
| `log_contact` | Logs or updates contact info in a Google Sheet. Merges rows by email + role. |
|
| `append_to_summary` | Log email metadata to local JSON (auto-purges after 30 days) |
|
||||||
| `create_calendar_event` | Creates a Google Calendar event for scheduled calls/meetings. Includes meeting links, attendees and their contact info. |
|
| `log_recruiter_contact` | Append or update a row in Google Sheets |
|
||||||
|
| `create_calendar_event` | Create a Google Calendar event (skips past dates) |
|
||||||
|
|
||||||
## MCP Prompts (built in as of now-ish - add your own)
|
### Example: Fetch Emails
|
||||||
### BETTER YET - DO A PR OR FORK
|
|
||||||
|
|
||||||
| Prompt | Account | Description |
|
```
|
||||||
|--------|---------|-------------|
|
fetch_new_emails(account: "work", maxResults: 50)
|
||||||
| `review_emails` | work | Loads the classification + action prompts for the work inbox |
|
```
|
||||||
| `review_secondary_emails` | secondary | Same workflow, but for a second email account inbox |
|
|
||||||
|
|
||||||
Invoke these from Claude Desktop's Connectors menu, or just type "Review my inbox" / "Review my secondary inbox."
|
### Example: Star and Mark Read (Categories A, C, D only)
|
||||||
|
|
||||||
|
```
|
||||||
|
star_emails(account: "work", messageIds: ["msg-id-1", "msg-id-2"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Create Calendar Event
|
||||||
|
|
||||||
|
```
|
||||||
|
create_calendar_event(
|
||||||
|
account: "work",
|
||||||
|
title: "[Interview] Acme Corp - Senior Engineer",
|
||||||
|
startDateTime: "2026-09-01T14:00:00-04:00",
|
||||||
|
durationMinutes: 60,
|
||||||
|
location: "https://zoom.us/j/123456789"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Two-Phase Workflow
|
## CLI Commands
|
||||||
|
|
||||||
**Phase 1 -- Classify**
|
|
||||||
|
|
||||||
The LLM calls `fetch_new_emails`, which returns email data with the classification instructions from `classify-emails.txt` appended. Each email is sorted into:
|
|
||||||
|
|
||||||
| Category | Meaning | Action |
|
|
||||||
|----------|---------|--------|
|
|
||||||
| **A** | Acknowledgement / auto-reply | Delete |
|
|
||||||
| **B** | Advancement to next step | Summarize + log |
|
|
||||||
| **C** | Rejection | Delete |
|
|
||||||
| **D** | Other / uncategorized | Summarize + log |
|
|
||||||
|
|
||||||
**Phase 2 -- Act**
|
|
||||||
|
|
||||||
Using `take-action-on-emails.txt`, the LLM:
|
|
||||||
- Calls `delete_emails` for A + C
|
|
||||||
- Calls `append_to_summary` for B + D
|
|
||||||
- Calls `log_recruiter_contact` to track contacts in Sheets (if configured)
|
|
||||||
- Calls `create_calendar_event` for any scheduled interviews/calls (if configured)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Commands
|
|
||||||
|
|
||||||
| Command | Purpose |
|
| Command | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| `npm run build` | Recompile after editing source files |
|
| `npm run build` | Compile TypeScript to `build/` |
|
||||||
| `npm run auth` | Authorize the default (work) account |
|
| `npm run auth` | Authorize the default account |
|
||||||
| `npm run auth -- secondary` | Authorize the secondary account |
|
| `npm run auth -- <name>` | Authorize a named account |
|
||||||
| `npm test` | Run the test suite |
|
| `npm test` | Run test suite |
|
||||||
| `npm run test:watch` | Run tests in watch mode |
|
| `npm run test:watch` | Run tests in watch mode |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -326,42 +167,28 @@ Using `take-action-on-emails.txt`, the LLM:
|
|||||||
```
|
```
|
||||||
deClawed-Assity-Kitty/
|
deClawed-Assity-Kitty/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── index.ts # Entry point -- imports modules, starts server
|
│ ├── index.ts # Entry point
|
||||||
│ ├── McpServer.ts # MCP server instance
|
│ ├── McpServer.ts # MCP server instance
|
||||||
│ ├── auth.ts # Multi-account OAuth setup script
|
│ ├── auth.ts # OAuth setup script
|
||||||
│ ├── loaders/
|
│ ├── loaders/ # Config and prompt loaders
|
||||||
│ │ └── prompt-config-loaders.ts # Account, prompt, and OAuth client loaders
|
│ ├── prompts/ # Classification and action prompts
|
||||||
│ ├── prompt-controller-service/
|
│ └── tools/ # MCP tool implementations
|
||||||
│ │ └── prompt-controller-service.ts # MCP prompt registration
|
├── accountsAndCredentials/ # OAuth credentials and tokens (gitignored)
|
||||||
│ ├── prompts/
|
├── mailSummaries/ # Local email logs (gitignored)
|
||||||
│ │ ├── classify-emails.txt # Phase 1: classification instructions
|
├── test/ # Unit and integration tests
|
||||||
│ │ └── take-action-on-emails.txt # Phase 2: action instructions
|
└── build/ # Compiled output
|
||||||
│ └── tools/
|
|
||||||
│ ├── tools-email.ts # fetch, delete, append_to_summary
|
|
||||||
│ ├── tools-calendar.ts # create_calendar_event
|
|
||||||
│ └── tools-spreadsheet.ts # log_recruiter_contact
|
|
||||||
├── test/
|
|
||||||
│ ├── fixtures/
|
|
||||||
│ │ └── mock-emails.ts # Mock Gmail API responses
|
|
||||||
│ ├── integration/
|
|
||||||
│ │ └── email-workflow.test.ts # Integration tests
|
|
||||||
│ └── unit/
|
|
||||||
│ └── email-parsing.test.ts # Unit tests for parsing helpers
|
|
||||||
├── mailSummaries/ # Per-account summary output (auto-generated)
|
|
||||||
├── build/ # Compiled JS (auto-generated)
|
|
||||||
├── accounts.json # Multi-account configuration
|
|
||||||
├── credentials.json # Google OAuth client credentials
|
|
||||||
├── token*.json # Per-account OAuth tokens (auto-generated)
|
|
||||||
├── package.json
|
|
||||||
├── tsconfig.json
|
|
||||||
├── vitest.config.ts
|
|
||||||
└── README.md
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- All credentials and tokens are stored locally and excluded from version control
|
||||||
|
- OAuth tokens auto-refresh; re-authorization is only needed if revoked
|
||||||
|
- The server runs locally via stdio—no network exposure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ISC
|
||||||
|
|
||||||
|
|||||||
11
accountsAndCredentials/credentials.example.json
Normal file
11
accountsAndCredentials/credentials.example.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"installed": {
|
||||||
|
"client_id": "",
|
||||||
|
"project_id": "",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
7
accountsAndCredentials/token.example.json
Normal file
7
accountsAndCredentials/token.example.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"access_token": "",
|
||||||
|
"refresh_token": "",
|
||||||
|
"scope": "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/calendar.events",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"expiry_date": 0
|
||||||
|
}
|
||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||||
"googleapis": "^171.4.0",
|
"googleapis": "^171.4.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
@@ -501,12 +501,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.26.0",
|
"version": "1.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
|
||||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
"integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.19.9",
|
"@hono/node-server": "^1.19.9 || ^2.0.5",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||||
"googleapis": "^171.4.0",
|
"googleapis": "^171.4.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ 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 SECRETS_DIR = path.join(PROJECT_ROOT, "accountsAndCredentials");
|
||||||
const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json");
|
const CREDENTIALS_PATH = path.join(SECRETS_DIR, "credentials.json");
|
||||||
|
const ACCOUNTS_PATH = path.join(SECRETS_DIR, "accounts.json");
|
||||||
|
|
||||||
const SCOPES = [
|
const SCOPES = [
|
||||||
"https://www.googleapis.com/auth/gmail.modify",
|
"https://www.googleapis.com/auth/gmail.modify",
|
||||||
@@ -26,7 +27,7 @@ function resolveTokenPath(accountKey: string): string {
|
|||||||
`Unknown account "${accountKey}". Available: ${available}`
|
`Unknown account "${accountKey}". Available: ${available}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return path.join(PROJECT_ROOT, acct.tokenFile);
|
return path.join(SECRETS_DIR, acct.tokenFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authorize(): Promise<void> {
|
async function authorize(): Promise<void> {
|
||||||
@@ -44,7 +45,7 @@ async function authorize(): Promise<void> {
|
|||||||
"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, Sheets, and Calendar APIs\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 accountsAndCredentials/ folder\n"
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ 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 CREDENTIALS_PATH = path.join(PROJECT_ROOT, "credentials.json");
|
const SECRETS_DIR = path.join(PROJECT_ROOT, "accountsAndCredentials");
|
||||||
const ACCOUNTS_PATH = path.join(PROJECT_ROOT, "accounts.json");
|
const CREDENTIALS_PATH = path.join(SECRETS_DIR, "credentials.json");
|
||||||
const CLASSIFY_PROMPT_PATH = path.join(PROJECT_ROOT, "classify-emails.txt");
|
const ACCOUNTS_PATH = path.join(SECRETS_DIR, "accounts.json");
|
||||||
const ACTION_PROMPT_PATH = path.join(
|
const PROMPTS_DIR = path.join(PROJECT_ROOT, "src", "prompts");
|
||||||
PROJECT_ROOT, "src", "prompts", "take-action-on-emails.txt"
|
const CLASSIFY_PROMPT_PATH = path.join(PROMPTS_DIR, "classify-emails.txt");
|
||||||
);
|
const ACTION_PROMPT_PATH = path.join(PROMPTS_DIR, "take-action-on-emails.txt");
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Account configuration
|
// Account configuration
|
||||||
@@ -46,7 +48,7 @@ export const getTokenPath = (account: string): string => {
|
|||||||
`Unknown account "${account}". Available accounts: ${available}`
|
`Unknown account "${account}". Available accounts: ${available}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return path.join(PROJECT_ROOT, acct.tokenFile);
|
return path.join(SECRETS_DIR, acct.tokenFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSummaryPath = (account: string): string => {
|
export const getSummaryPath = (account: string): string => {
|
||||||
|
|||||||
@@ -6,27 +6,23 @@ import {
|
|||||||
} 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();
|
||||||
const phase2 = loadActionPrompt();
|
|
||||||
return {
|
return {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "user" as const,
|
role: "user" as const,
|
||||||
content: {
|
content: {
|
||||||
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.")
|
||||||
(phase1 || "Review my new emails and classify them by job application category.") +
|
|
||||||
(phase2 ? `\n\n${phase2}` : ""),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -35,16 +31,14 @@ server.registerPrompt(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 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 phase1 = loadClassificationPrompt();
|
|
||||||
const phase2 = loadActionPrompt();
|
const phase2 = loadActionPrompt();
|
||||||
return {
|
return {
|
||||||
messages: [
|
messages: [
|
||||||
@@ -52,10 +46,8 @@ server.registerPrompt(
|
|||||||
role: "user" as const,
|
role: "user" as const,
|
||||||
content: {
|
content: {
|
||||||
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 = "secondary" for ALL tool calls in this session.\n\n` +
|
(phase2 || "Process remaining action items for flagged recruiters.")
|
||||||
(phase1 || "Review my new emails and classify them by job application category.") +
|
|
||||||
(phase2 ? `\n\n${phase2}` : ""),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
107
src/prompts/classify-emails-general.txt
Normal file
107
src/prompts/classify-emails-general.txt
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
You are an expert Executive Assistant AI specialized in email management. Your goal is to organize, prioritize, and triage incoming emails to maximize productivity and ensure nothing important is missed.
|
||||||
|
|
||||||
|
Review the inbox emails provided. Apply the following instructions.
|
||||||
|
|
||||||
|
STEP 1: CONSTRAINTS
|
||||||
|
- NEVER fabricate information. If data is not explicitly present in an email, it does not exist.
|
||||||
|
- When classification is ambiguous, err on the side of higher priority (e.g., classify as Action Required rather than Informational).
|
||||||
|
|
||||||
|
STEP 2: CLASSIFY each email into exactly one category.
|
||||||
|
|
||||||
|
Category A - Action Required
|
||||||
|
Emails that require a response, decision, or task completion from the user. Indicators include:
|
||||||
|
1. Direct questions addressed to the user
|
||||||
|
2. Requests for information, documents, or approvals
|
||||||
|
3. Meeting invitations or scheduling requests
|
||||||
|
4. Deadlines or time-sensitive requests
|
||||||
|
5. Follow-ups on prior conversations that need a reply
|
||||||
|
6. Emails from humans (not automation) explicitly asking for something
|
||||||
|
|
||||||
|
This category does NOT include:
|
||||||
|
- Automated notifications that require only a click (e.g., "verify your email," "reset password")
|
||||||
|
- Subscription confirmations or account alerts
|
||||||
|
|
||||||
|
Category B - Informational
|
||||||
|
Emails that provide useful information but require no immediate action. Examples:
|
||||||
|
1. Project updates or status reports
|
||||||
|
2. FYI messages or CC'd correspondence
|
||||||
|
3. Newsletters or digests the user has opted into
|
||||||
|
4. Industry news or curated content
|
||||||
|
5. Internal announcements (policy changes, team updates, etc.)
|
||||||
|
6. Shared documents or resources for reference
|
||||||
|
|
||||||
|
Category C - Transactional
|
||||||
|
Automated or system-generated emails confirming an action or status. Examples:
|
||||||
|
1. Order confirmations, shipping notifications, delivery updates
|
||||||
|
2. Payment receipts and invoices
|
||||||
|
3. Appointment reminders from services (doctors, salons, etc.)
|
||||||
|
4. Travel itineraries and booking confirmations
|
||||||
|
5. Password resets, login alerts, two-factor codes
|
||||||
|
6. Subscription renewals or billing notices
|
||||||
|
|
||||||
|
Category D - Low Priority
|
||||||
|
Emails that can be safely ignored, archived, or deleted. Examples:
|
||||||
|
1. Marketing and promotional emails
|
||||||
|
2. Sales outreach from unknown senders
|
||||||
|
3. Surveys or feedback requests
|
||||||
|
4. Social media notifications
|
||||||
|
5. Spam or unsolicited bulk mail
|
||||||
|
6. Emails from mailing lists the user rarely engages with
|
||||||
|
|
||||||
|
Category E - Personal
|
||||||
|
Emails from known personal contacts or containing personal (non-work) content. Examples:
|
||||||
|
1. Messages from friends or family
|
||||||
|
2. Personal appointment confirmations
|
||||||
|
3. Social invitations or event coordination
|
||||||
|
4. Non-work correspondence from colleagues
|
||||||
|
|
||||||
|
STEP 3: MARK PROCESSED EMAILS
|
||||||
|
|
||||||
|
Star all emails classified as Category A (Action Required).
|
||||||
|
Do NOT star emails in Categories B, C, D, or E.
|
||||||
|
|
||||||
|
STEP 4: REPORT EMAIL COUNT
|
||||||
|
|
||||||
|
Confirm the total number of emails reviewed in the chat.
|
||||||
|
|
||||||
|
STEP 5: SUMMARIZE PRIORITY EMAILS
|
||||||
|
|
||||||
|
Summarize all Category A (Action Required) and Category E (Personal) emails in the chat, under separate headers.
|
||||||
|
|
||||||
|
For each email, include:
|
||||||
|
- From: sender name and email address
|
||||||
|
- Subject: email subject line
|
||||||
|
- Date: date received
|
||||||
|
- Time: time received (convert to user's local timezone if known, otherwise use original)
|
||||||
|
- Summary: 1-2 sentence summary of the content and what action may be needed
|
||||||
|
|
||||||
|
STEP 6: EXECUTE ACTIONS
|
||||||
|
|
||||||
|
For Category A emails:
|
||||||
|
- If the email contains a meeting request with a specific date/time, create a calendar event using the create_calendar_event tool.
|
||||||
|
- Title: Use the subject line or a clear description of the meeting
|
||||||
|
- Start: Extract date/time from the email (ISO 8601 format)
|
||||||
|
- Duration: Use the duration specified, or default to 60 minutes
|
||||||
|
- Location: Include any meeting link, address, or phone number mentioned
|
||||||
|
- Description: Include relevant context (attendees, agenda, contact info)
|
||||||
|
|
||||||
|
For Category C emails:
|
||||||
|
- If the email contains an appointment or reservation with a specific date/time (e.g., doctor appointment, flight, hotel check-in), create a calendar event.
|
||||||
|
- Title: "[Reminder] " + service/company name + description
|
||||||
|
- Start: Extract date/time from the email
|
||||||
|
- Duration: Use duration if specified, or default to 60 minutes
|
||||||
|
- Location: Include address or relevant details
|
||||||
|
- Description: Include confirmation numbers, contact info, or instructions
|
||||||
|
|
||||||
|
For Category D emails:
|
||||||
|
- Use delete_emails to move these to trash (if configured for auto-cleanup).
|
||||||
|
|
||||||
|
STRICT CONSTRAINTS FOR CALENDAR EVENTS:
|
||||||
|
- Do not fabricate dates, times, links, phone numbers, or any other information.
|
||||||
|
- Before creating an event, verify the proposed date is in the future. If the date has passed, skip calendar creation.
|
||||||
|
- Do not create duplicate events for the same appointment (same date/time and same organizer/service).
|
||||||
|
- Use the same account for calendar events as was used for email fetching.
|
||||||
|
|
||||||
|
STEP 7: LOG SUMMARY
|
||||||
|
|
||||||
|
Use append_to_summary to log all Category A and Category E emails for future reference.
|
||||||
@@ -9,23 +9,27 @@ STEP 2: CLASSIFY each email into exactly one category.
|
|||||||
|
|
||||||
Category A - Acknowledgement Only
|
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.
|
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.
|
||||||
|
Action you should take: Call star_emails with this message's ID (same account used for fetch_new_emails). That stars the email and marks it read.
|
||||||
|
|
||||||
Category B - Advancement to Next Step
|
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.
|
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.
|
||||||
|
Actions you should take: 1) add a calendar event with all pertinent info: a) person I will be meeting with,
|
||||||
|
b) company c) role I will be discussing d) method for joining meeting - ie, telephone? If yes, will the representative all me, and is the number correct (718) 749-8292 c) or, is it Zoom, Google Meet, Webex or another video format, if so, include the link for joining
|
||||||
|
|
||||||
|
NEVER, EVER ADD A STAR TO A CATEGORY B EMAIL. Do not pass Category B message IDs to star_emails.
|
||||||
|
|
||||||
Category C - Rejection
|
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."
|
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."
|
||||||
|
Action you should take: Call star_emails with this message's ID (same account used for fetch_new_emails). That stars the email and marks it read.
|
||||||
|
|
||||||
Category D - Other
|
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.
|
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.
|
||||||
|
Action you should take: Call star_emails with this message's ID (same account used for fetch_new_emails). That stars the email and marks it read.
|
||||||
|
|
||||||
STEP 3: DELETE emails classified as Category A and Category C.
|
STEP 3: SUMMARIZE emails classified as Category B and Category D in the chat in an organized list with headings. Example format for this:
|
||||||
|
|
||||||
STEP 4: ("Anti step") - NEVER DELETE Category B emails.
|
|
||||||
|
|
||||||
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 might take
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,23 @@
|
|||||||
PHASE 2: TAKE ACTION ON CATEGORY B EMAILS
|
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).
|
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
|
STEP 1: CREATE CALENDAR EVENTS
|
||||||
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:
|
Immediately after logging to the spreadsheet, check whether ANY of these fields were populated or updated:
|
||||||
- Recruiter Call Scheduled
|
- Recruiter Call Scheduled
|
||||||
- Company First Interview
|
- Company First Interview
|
||||||
- Company Second 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:
|
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"
|
- 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)
|
- Start date/time: extracted from the email (convert to ISO 8601)
|
||||||
- Duration: 30 minutes unless otherwise specified in the email
|
- Duration: 30 minutes unless otherwise specified in the email
|
||||||
- Location: the video meeting link, phone number, or platform name
|
- 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
|
- 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:
|
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.
|
- 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, compare the proposed meeting date/time against today's date. 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.
|
- 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.
|
- 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.
|
- Always use the same account for calendar events as was used for email fetching.
|
||||||
- Do not create duplicate calendar events for the same meeting.
|
- 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 do ot guess.
|
- If information is not present in the email, leave that field empty (it's OK!) - do not guess or make up information.
|
||||||
|
|||||||
@@ -55,15 +55,16 @@ server.registerTool(
|
|||||||
{
|
{
|
||||||
description:
|
description:
|
||||||
"Fetch unread emails from a 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 " +
|
"subject, message ID, and body text for each message. After evaluation, " +
|
||||||
"can be passed to delete_emails later. Specify which account to fetch from.",
|
"pass Category A, C, and D message IDs to star_emails. Never pass " +
|
||||||
|
"Category B IDs to star_emails. Specify which account to fetch from.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
account: accountSchema,
|
account: accountSchema,
|
||||||
maxResults: z
|
maxResults: z
|
||||||
.number()
|
.number()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(100)
|
.max(200)
|
||||||
.describe("Maximum number of unread emails to fetch (1-100)"),
|
.describe("Maximum number of unread emails to fetch (1-200)"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ account, maxResults }) => {
|
async ({ account, maxResults }) => {
|
||||||
@@ -303,3 +304,59 @@ 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 Category A (acknowledgements), C (rejections), and D (other) " +
|
||||||
|
"messages to signal they have been processed. Never use this for Category B " +
|
||||||
|
"(advancement) emails. 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}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { MOCK_MESSAGE_LIST, MOCK_MESSAGES, MOCK_EMPTY_LIST } from "../fixtures/m
|
|||||||
const mockGmailList = vi.fn();
|
const mockGmailList = vi.fn();
|
||||||
const mockGmailGet = vi.fn();
|
const mockGmailGet = vi.fn();
|
||||||
const mockGmailTrash = vi.fn();
|
const mockGmailTrash = vi.fn();
|
||||||
|
const mockGmailBatchModify = vi.fn();
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
|
|
||||||
vi.mock("../../src/loaders/prompt-config-loaders.js", () => {
|
vi.mock("../../src/loaders/prompt-config-loaders.js", () => {
|
||||||
@@ -45,6 +46,7 @@ vi.mock("../../src/loaders/prompt-config-loaders.js", () => {
|
|||||||
list: mockGmailList,
|
list: mockGmailList,
|
||||||
get: mockGmailGet,
|
get: mockGmailGet,
|
||||||
trash: mockGmailTrash,
|
trash: mockGmailTrash,
|
||||||
|
batchModify: mockGmailBatchModify,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -207,6 +209,51 @@ describe("Phase 1: Email Review Workflow", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ----- Step 2b: Star A, C, and D emails -----
|
||||||
|
describe("star_emails", () => {
|
||||||
|
it("stars and marks read the specified message IDs", async () => {
|
||||||
|
mockGmailBatchModify.mockResolvedValue({});
|
||||||
|
|
||||||
|
const text = await callTool("star_emails", {
|
||||||
|
account: "work",
|
||||||
|
messageIds: ["msg-cat-a-001", "msg-cat-c-001", "msg-cat-d-001"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGmailBatchModify).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockGmailBatchModify).toHaveBeenCalledWith({
|
||||||
|
userId: "me",
|
||||||
|
requestBody: {
|
||||||
|
ids: ["msg-cat-a-001", "msg-cat-c-001", "msg-cat-d-001"],
|
||||||
|
addLabelIds: ["STARRED"],
|
||||||
|
removeLabelIds: ["UNREAD"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(text).toContain("Successfully starred and marked read 3 email(s).");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns early when no message IDs are provided", async () => {
|
||||||
|
const text = await callTool("star_emails", {
|
||||||
|
account: "work",
|
||||||
|
messageIds: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGmailBatchModify).not.toHaveBeenCalled();
|
||||||
|
expect(text).toBe("No message IDs provided.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles Gmail API errors gracefully", async () => {
|
||||||
|
mockGmailBatchModify.mockRejectedValue(new Error("Quota exceeded"));
|
||||||
|
|
||||||
|
const text = await callTool("star_emails", {
|
||||||
|
account: "work",
|
||||||
|
messageIds: ["msg-cat-a-001"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text).toContain("Error starring emails");
|
||||||
|
expect(text).toContain("Quota exceeded");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ----- Step 3: Summarize B and D -----
|
// ----- Step 3: Summarize B and D -----
|
||||||
describe("append_to_summary", () => {
|
describe("append_to_summary", () => {
|
||||||
it("creates a summary file and appends entries", async () => {
|
it("creates a summary file and appends entries", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user