- Updated add-voice-transcription to use AskUserQuestion for API key prompt - Updated add-gmail to use AskUserQuestion for mode selection and channel config - Updated add-discord to use AskUserQuestion for mode and token prompts - Updated add-parallel to use AskUserQuestion for API key and permission prompts - Updated add-telegram to use AskUserQuestion for mode and token prompts - Updated setup to use AskUserQuestion for Node.js, Docker, and container runtime prompts The AskUserQuestion tool provides a structured way to collect user input during skill execution, making the interaction pattern consistent across all skills.
19 KiB
name, description
| name | description |
|---|---|
| add-gmail | Add Gmail integration to NanoClaw. Can be configured as a tool (agent reads/sends emails when triggered from WhatsApp) or as a full channel (emails can trigger the agent, schedule tasks, and receive replies). Guides through GCP OAuth setup and implements the integration. |
Add Gmail Integration
This skill adds Gmail capabilities to NanoClaw. It can be configured in two modes:
- Tool Mode - Agent can read/send emails, but only when triggered from WhatsApp
- Channel Mode - Emails can trigger the agent, schedule tasks, and receive email replies
Initial Questions
Use AskUserQuestion to determine the configuration:
AskUserQuestion: How do you want to use Gmail with NanoClaw?
- Tool Mode - Agent can read/send emails when triggered from WhatsApp (simpler setup)
- Channel Mode - Emails can trigger the agent, schedule tasks, and receive email replies (requires polling)
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:
- Open https://console.cloud.google.com in your browser
- Create a new project (or select existing) - click the project dropdown at the top
Wait for user confirmation, then continue:
- 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:
- 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:
- 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.jsonWhere 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 # macOS
# Linux: systemctl --user restart nanoclaw
Check that it started:
sleep 2 && launchctl list | grep nanoclaw # macOS
# Linux: systemctl --user status 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 emailsOr:
@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
Use AskUserQuestion to configure email triggering:
AskUserQuestion: How should the agent be triggered from email?
- Specific Label - Create a Gmail label (e.g., "NanoClaw"), emails with this label trigger the agent
- Email Address Pattern - Emails to a specific address pattern (e.g., andy+task@gmail.com) via plus-addressing
- Subject Prefix - Emails with a subject starting with a keyword (e.g., "[Andy]")
AskUserQuestion: How should email conversations be grouped?
- Per Email Thread - Each email thread gets its own conversation context
- Per Sender - All emails from the same sender share context
- Single Context - All emails share the main group context
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';
Then add the startEmailLoop function:
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 add startEmailLoop() in the main() function, after startMessageLoop():
// In main(), 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;
setSession(groupFolder, output.newSessionId);
}
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/ipc.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 # macOS
# Linux: systemctl --user restart 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_emailstable for already-processed emails
Container can't access Gmail
- Verify
~/.gmail-mcpis mounted in container - Check container logs:
cat groups/main/logs/container-*.log | tail -50
Removing Gmail Integration
To remove Gmail entirely:
-
Remove from
container/agent-runner/src/index.ts:- Delete
gmailfrommcpServers - Remove
mcp__gmail__*fromallowedTools
- Delete
-
Remove from
src/container-runner.ts:- Delete the
~/.gmail-mcpmount block
- Delete the
-
Remove from
src/index.ts(Channel Mode only):- Delete
startEmailLoop()call - Delete email-related imports
- Delete
-
Delete
src/email-channel.ts(if created) -
Remove Gmail sections from
groups/*/CLAUDE.md -
Rebuild:
cd container && ./build.sh && cd .. npm run build launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS # Linux: systemctl --user restart nanoclaw