add-gmail

📁 creatuluw/nanoclaw 📅 9 days ago
1
总安装量
1
周安装量
#45805
全站排名
安装命令
npx skills add https://github.com/creatuluw/nanoclaw --skill add-gmail

Agent 安装分布

opencode 1

Skill 文档

Add Gmail Integration

This skill adds Gmail capabilities to NanoClaw. It can be configured in two modes:

  1. Tool Mode – Agent can read/send emails, but only when triggered from WhatsApp
  2. Channel Mode – Emails can trigger the agent, schedule tasks, and receive email replies

Initial Questions

Ask the user:

How do you want to use Gmail with NanoClaw?

Option 1: Tool Mode

  • Agent can read and send emails when you ask it to
  • Triggered only from WhatsApp (e.g., “@Andy check my email” or “@Andy send an email to…”)
  • Simpler setup, no email polling

Option 2: Channel Mode

  • Everything in Tool Mode, plus:
  • Emails to a specific address/label trigger the agent
  • Agent replies via email (not WhatsApp)
  • Can schedule tasks via email
  • Requires email polling infrastructure

Store their choice and proceed to the appropriate section.


Prerequisites (Both Modes)

1. Check Existing Gmail Setup

First, check if Gmail is already configured:

ls -la ~/.gmail-mcp/ 2>/dev/null || echo "No Gmail config found"

If credentials.json exists, skip to “Verify Gmail Access” below.

2. Create Gmail Config Directory

mkdir -p ~/.gmail-mcp

3. GCP Project Setup

USER ACTION REQUIRED

Tell the user:

I need you to set up Google Cloud OAuth credentials. I’ll walk you through it:

  1. Open https://console.cloud.google.com in your browser
  2. Create a new project (or select existing) – click the project dropdown at the top

Wait for user confirmation, then continue:

  1. Now enable the Gmail API:
    • In the left sidebar, go to APIs & Services → Library
    • Search for “Gmail API”
    • Click on it, then click Enable

Wait for user confirmation, then continue:

  1. Now create OAuth credentials:
    • Go to APIs & Services → Credentials (in the left sidebar)
    • Click + CREATE CREDENTIALS at the top
    • Select OAuth client ID
    • If prompted for consent screen, choose “External”, fill in app name (e.g., “NanoClaw”), your email, and save
    • For Application type, select Desktop app
    • Name it anything (e.g., “NanoClaw Gmail”)
    • Click Create

Wait for user confirmation, then continue:

  1. Download the credentials:
    • Click DOWNLOAD JSON on the popup (or find it in the credentials list and click the download icon)
    • Save it as gcp-oauth.keys.json

Where did you save the file? (Give me the full path, or just paste the file contents here)

If user provides a path, copy it:

cp "/path/user/provided/gcp-oauth.keys.json" ~/.gmail-mcp/gcp-oauth.keys.json

If user pastes the JSON content, write it directly:

cat > ~/.gmail-mcp/gcp-oauth.keys.json << 'EOF'
{paste the JSON here}
EOF

Verify the file is valid JSON:

cat ~/.gmail-mcp/gcp-oauth.keys.json | head -5

4. OAuth Authorization

USER ACTION REQUIRED

Tell the user:

I’m going to run the Gmail authorization. A browser window will open asking you to sign in to Google and grant access.

Important: If you see a warning that the app isn’t verified, click “Advanced” then “Go to [app name] (unsafe)” – this is normal for personal OAuth apps.

Run the authorization:

npx -y @gongrzhe/server-gmail-autoauth-mcp auth

If that doesn’t work (some versions don’t have an auth subcommand), run it and let it prompt:

timeout 60 npx -y @gongrzhe/server-gmail-autoauth-mcp || true

Tell user:

Complete the authorization in your browser. The window should close automatically when done. Let me know when you’ve authorized.

5. Verify Gmail Access

Check that credentials were saved:

if [ -f ~/.gmail-mcp/credentials.json ]; then
  echo "Gmail authorization successful!"
  ls -la ~/.gmail-mcp/
else
  echo "ERROR: credentials.json not found - authorization may have failed"
fi

Test the connection by listing labels (quick sanity check):

echo '{"method": "tools/list"}' | timeout 10 npx -y @gongrzhe/server-gmail-autoauth-mcp 2>/dev/null | head -20 || echo "MCP responded (check output above)"

If everything works, proceed to implementation.


Tool Mode Implementation

For Tool Mode, integrate Gmail MCP into the agent runner. Execute these changes directly.

Step 1: Add Gmail MCP to Agent Runner

Read container/agent-runner/src/index.ts and find the mcpServers config in the query() call.

Add gmail to the mcpServers object:

gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] }

Find the allowedTools array and add Gmail tools:

'mcp__gmail__*'

The result should look like:

mcpServers: {
  nanoclaw: ipcMcp,
  gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] }
},
allowedTools: [
  'Bash',
  'Read', 'Write', 'Edit', 'Glob', 'Grep',
  'WebSearch', 'WebFetch',
  'mcp__nanoclaw__*',
  'mcp__gmail__*'
],

Step 2: Mount Gmail Credentials in Container

Read src/container-runner.ts and find the buildVolumeMounts function.

Add this mount block (after the .claude mount is a good location):

// Gmail credentials directory
const gmailDir = path.join(homeDir, '.gmail-mcp');
if (fs.existsSync(gmailDir)) {
  mounts.push({
    hostPath: gmailDir,
    containerPath: '/home/node/.gmail-mcp',
    readonly: false  // MCP may need to refresh tokens
  });
}

Step 3: Update Group Memory

Append to groups/CLAUDE.md (the global memory file):


## Email (Gmail)

You have access to Gmail via MCP tools:
- `mcp__gmail__search_emails` - Search emails with query
- `mcp__gmail__get_email` - Get full email content by ID
- `mcp__gmail__send_email` - Send an email
- `mcp__gmail__draft_email` - Create a draft
- `mcp__gmail__list_labels` - List available labels

Example: "Check my unread emails from today" or "Send an email to john@example.com about the meeting"

Also append the same section to groups/main/CLAUDE.md.

Step 4: Rebuild and Restart

Run these commands:

cd container && ./build.sh

Wait for container build to complete, then:

cd .. && npm run build

Wait for TypeScript compilation, then restart the service:

launchctl kickstart -k gui/$(id -u)/com.nanoclaw

Check that it started:

sleep 2 && launchctl list | grep nanoclaw

Step 5: Test Gmail Integration

Tell the user:

Gmail integration is set up! Test it by sending this message in your WhatsApp main channel:

@Andy check my recent emails

Or:

@Andy list my Gmail labels

Watch the logs for any errors:

tail -f logs/nanoclaw.log

Channel Mode Implementation

Channel Mode includes everything from Tool Mode, plus email polling and routing.

Additional Questions for Channel Mode

Ask the user:

How should the agent be triggered from email?

Option A: Specific Label

  • Create a Gmail label (e.g., “NanoClaw”)
  • Emails with this label trigger the agent
  • You manually label emails or set up Gmail filters

Option B: Email Address Pattern

  • Emails to a specific address pattern (e.g., andy+task@gmail.com)
  • Uses Gmail’s plus-addressing feature

Option C: Subject Prefix

  • Emails with a subject starting with a keyword (e.g., “[Andy]”)
  • Anyone can trigger the agent by using the prefix

Also ask:

How should email conversations be grouped?

Option A: Per Email Thread

  • Each email thread gets its own conversation context
  • Agent remembers the thread history

Option B: Per Sender

  • All emails from the same sender share context
  • Agent remembers all interactions with that person

Option C: Single Context

  • All emails share the main group context
  • Like an additional input to the main channel

Store their choices for implementation.

Step 1: Complete Tool Mode First

Complete all Tool Mode steps above before continuing. Verify Gmail tools work by having the user test @Andy check my recent emails.

Step 2: Add Email Polling Configuration

Read src/types.ts and add this interface:

export interface EmailChannelConfig {
  enabled: boolean;
  triggerMode: 'label' | 'address' | 'subject';
  triggerValue: string;  // Label name, address pattern, or subject prefix
  contextMode: 'thread' | 'sender' | 'single';
  pollIntervalMs: number;
  replyPrefix?: string;  // Optional prefix for replies
}

Read src/config.ts and add this configuration (customize values based on user’s earlier answers):

export const EMAIL_CHANNEL: EmailChannelConfig = {
  enabled: true,
  triggerMode: 'label',  // or 'address' or 'subject'
  triggerValue: 'NanoClaw',  // the label name, address pattern, or prefix
  contextMode: 'thread',
  pollIntervalMs: 60000,  // Check every minute
  replyPrefix: '[Andy] '
};

Step 3: Add Email State Tracking

Read src/db.ts and add these functions for tracking processed emails:

// Track processed emails to avoid duplicates
export function initEmailTable(): void {
  db.exec(`
    CREATE TABLE IF NOT EXISTS processed_emails (
      message_id TEXT PRIMARY KEY,
      thread_id TEXT NOT NULL,
      sender TEXT NOT NULL,
      subject TEXT,
      processed_at TEXT NOT NULL,
      response_sent INTEGER DEFAULT 0
    )
  `);
}

export function isEmailProcessed(messageId: string): boolean {
  const row = db.prepare('SELECT 1 FROM processed_emails WHERE message_id = ?').get(messageId);
  return !!row;
}

export function markEmailProcessed(messageId: string, threadId: string, sender: string, subject: string): void {
  db.prepare(`
    INSERT OR REPLACE INTO processed_emails (message_id, thread_id, sender, subject, processed_at)
    VALUES (?, ?, ?, ?, ?)
  `).run(messageId, threadId, sender, subject, new Date().toISOString());
}

export function markEmailResponded(messageId: string): void {
  db.prepare('UPDATE processed_emails SET response_sent = 1 WHERE message_id = ?').run(messageId);
}

Also find the initDatabase() function in src/db.ts and add a call to initEmailTable().

Step 4: Create Email Channel Module

Create a new file src/email-channel.ts with this content:

import { EMAIL_CHANNEL } from './config.js';
import { isEmailProcessed, markEmailProcessed, markEmailResponded } from './db.js';
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: { target: 'pino-pretty', options: { colorize: true } }
});

interface EmailMessage {
  id: string;
  threadId: string;
  from: string;
  subject: string;
  body: string;
  date: string;
}

// Gmail MCP client functions (call via subprocess or import the MCP directly)
// These would invoke the Gmail MCP tools

export async function checkForNewEmails(): Promise<EmailMessage[]> {
  // Build query based on trigger mode
  let query: string;
  switch (EMAIL_CHANNEL.triggerMode) {
    case 'label':
      query = `label:${EMAIL_CHANNEL.triggerValue} is:unread`;
      break;
    case 'address':
      query = `to:${EMAIL_CHANNEL.triggerValue} is:unread`;
      break;
    case 'subject':
      query = `subject:${EMAIL_CHANNEL.triggerValue} is:unread`;
      break;
  }

  // This requires calling Gmail MCP's search_emails tool
  // Implementation depends on how you want to invoke MCP from Node
  // Option 1: Use @anthropic-ai/claude-agent-sdk with just gmail MCP
  // Option 2: Run npx gmail MCP as subprocess and parse output
  // Option 3: Import gmail-autoauth-mcp directly

  // Placeholder - implement based on preference
  return [];
}

export async function sendEmailReply(
  threadId: string,
  to: string,
  subject: string,
  body: string
): Promise<void> {
  // Call Gmail MCP's send_email tool with in_reply_to for threading
  // Prefix subject with replyPrefix if configured
  const replySubject = subject.startsWith('Re:')
    ? subject
    : `Re: ${subject}`;

  const prefixedBody = EMAIL_CHANNEL.replyPrefix
    ? `${EMAIL_CHANNEL.replyPrefix}${body}`
    : body;

  // Implementation: invoke Gmail MCP send_email
}

export function getContextKey(email: EmailMessage): string {
  switch (EMAIL_CHANNEL.contextMode) {
    case 'thread':
      return `email-thread-${email.threadId}`;
    case 'sender':
      return `email-sender-${email.from.toLowerCase()}`;
    case 'single':
      return 'email-main';
  }
}

Step 5: Add Email Polling to Main Loop

Read src/index.ts and add the email polling infrastructure. First, add these imports at the top:

import { checkForNewEmails, sendEmailReply, getContextKey } from './email-channel.js';
import { EMAIL_CHANNEL } from './config.js';
import { isEmailProcessed, markEmailProcessed, markEmailResponded } from './db.js';

async function startEmailLoop(): Promise<void> {
  if (!EMAIL_CHANNEL.enabled) {
    logger.info('Email channel disabled');
    return;
  }

  logger.info(`Email channel running (trigger: ${EMAIL_CHANNEL.triggerMode}:${EMAIL_CHANNEL.triggerValue})`);

  while (true) {
    try {
      const emails = await checkForNewEmails();

      for (const email of emails) {
        if (isEmailProcessed(email.id)) continue;

        logger.info({ from: email.from, subject: email.subject }, 'Processing email');
        markEmailProcessed(email.id, email.threadId, email.from, email.subject);

        // Determine which group/context to use
        const contextKey = getContextKey(email);

        // Build prompt with email content
        const prompt = `<email>
<from>${email.from}</from>
<subject>${email.subject}</subject>
<body>${email.body}</body>
</email>

Respond to this email. Your response will be sent as an email reply.`;

        // Run agent with email context
        // You'll need to create a registered group for email or use a special handler
        const response = await runEmailAgent(contextKey, prompt, email);

        if (response) {
          await sendEmailReply(email.threadId, email.from, email.subject, response);
          markEmailResponded(email.id);
          logger.info({ to: email.from }, 'Email reply sent');
        }
      }
    } catch (err) {
      logger.error({ err }, 'Error in email loop');
    }

    await new Promise(resolve => setTimeout(resolve, EMAIL_CHANNEL.pollIntervalMs));
  }
}

Then find the `connectWhatsApp` function and add `startEmailLoop()` call after `startMessageLoop()`:

```typescript
// In the connection === 'open' block, after startMessageLoop():
startEmailLoop();

Step 6: Implement Email Agent Runner

Add this function to src/index.ts (or create a separate src/email-agent.ts if preferred):

async function runEmailAgent(
  contextKey: string,
  prompt: string,
  email: EmailMessage
): Promise<string | null> {
  // Email uses either:
  // 1. A dedicated "email" group folder
  // 2. Or dynamic folders per thread/sender

  const groupFolder = EMAIL_CHANNEL.contextMode === 'single'
    ? 'main'  // Use main group context
    : `email/${contextKey}`;  // Isolated email context

  // Ensure folder exists
  const groupDir = path.join(GROUPS_DIR, groupFolder);
  fs.mkdirSync(groupDir, { recursive: true });

  // Create minimal registered group for email
  const emailGroup: RegisteredGroup = {
    name: contextKey,
    folder: groupFolder,
    trigger: '',  // No trigger for email
    added_at: new Date().toISOString()
  };

  // Use existing runContainerAgent
  const output = await runContainerAgent(emailGroup, {
    prompt,
    sessionId: sessions[groupFolder],
    groupFolder,
    chatJid: `email:${email.from}`,  // Use email: prefix for JID
    isMain: false,
    isScheduledTask: false
  });

  if (output.newSessionId) {
    sessions[groupFolder] = output.newSessionId;
    saveJson(path.join(DATA_DIR, 'sessions.json'), sessions);
  }

  return output.status === 'success' ? output.result : null;
}

Step 7: Update IPC for Email Responses (Optional)

If you want the agent to be able to send emails proactively from within a session, read container/agent-runner/src/ipc-mcp.ts and add this tool:

// Add to the MCP tools
{
  name: 'send_email_reply',
  description: 'Send an email reply in the current thread',
  inputSchema: {
    type: 'object',
    properties: {
      body: { type: 'string', description: 'Email body content' }
    },
    required: ['body']
  }
}

Then add handling in src/index.ts in the processTaskIpc function or create a new IPC handler for email actions.

Step 8: Create Email Group Memory

Create the email group directory and memory file:

mkdir -p groups/email

Write groups/email/CLAUDE.md:

# Email Channel

You are responding to emails. Your responses will be sent as email replies.

## Guidelines

- Be professional and clear
- Keep responses concise but complete
- Use proper email formatting (greetings, sign-off)
- If the email requires action you can't take, explain what the user should do

## Context

Each email thread or sender (depending on configuration) has its own conversation history.

Step 9: Rebuild and Test

Rebuild the container (required since agent-runner changed):

cd container && ./build.sh

Wait for build to complete, then compile TypeScript:

cd .. && npm run build

Restart the service:

launchctl kickstart -k gui/$(id -u)/com.nanoclaw

Verify it started and check for email channel startup message:

sleep 3 && tail -20 logs/nanoclaw.log | grep -i email

Tell the user:

Email channel is now active! Test it by sending an email that matches your trigger:

  • Label mode: Apply the “${triggerValue}” label to any email
  • Address mode: Send an email to ${triggerValue}
  • Subject mode: Send an email with subject starting with “${triggerValue}”

The agent should process it within a minute and send a reply.

Monitor for the test:

tail -f logs/nanoclaw.log | grep -E "(email|Email)"

Troubleshooting

Gmail MCP not responding

# Test Gmail MCP directly
npx -y @gongrzhe/server-gmail-autoauth-mcp

OAuth token expired

# Re-authorize
rm ~/.gmail-mcp/credentials.json
npx -y @gongrzhe/server-gmail-autoauth-mcp

Emails not being detected

  • Check the trigger configuration matches your test email
  • Verify the label exists (for label mode)
  • Check processed_emails table for already-processed emails

Container can’t access Gmail

  • Verify ~/.gmail-mcp is mounted in container
  • Check container logs: cat groups/main/logs/container-*.log | tail -50

Removing Gmail Integration

To remove Gmail entirely:

  1. Remove from container/agent-runner/src/index.ts:

    • Delete gmail from mcpServers
    • Remove mcp__gmail__* from allowedTools
  2. Remove from src/container-runner.ts:

    • Delete the ~/.gmail-mcp mount block
  3. Remove from src/index.ts (Channel Mode only):

    • Delete startEmailLoop() call
    • Delete email-related imports
  4. Delete src/email-channel.ts (if created)

  5. Remove Gmail sections from groups/*/CLAUDE.md

  6. Rebuild:

    cd container && ./build.sh && cd ..
    npm run build
    launchctl kickstart -k gui/$(id -u)/com.nanoclaw