diff --git a/.claude/skills/add-gmail/SKILL.md b/.claude/skills/add-gmail/SKILL.md index 29b21c6..03a8cee 100644 --- a/.claude/skills/add-gmail/SKILL.md +++ b/.claude/skills/add-gmail/SKILL.md @@ -29,7 +29,7 @@ npx tsx scripts/apply-skill.ts --init ### Path A: Tool-only (user chose "No") -Do NOT run the full apply script. Only two source files need changes. This avoids adding dead code (`gmail.ts`, `gmail.test.ts`, config flags, index.ts channel logic, routing tests, `googleapis` dependency). +Do NOT run the full apply script. Only two source files need changes. This avoids adding dead code (`gmail.ts`, `gmail.test.ts`, index.ts channel logic, routing tests, `googleapis` dependency). #### 1. Mount Gmail credentials in container @@ -63,19 +63,16 @@ This deterministically: - Adds `src/channels/gmail.ts` (GmailChannel class implementing Channel interface) - Adds `src/channels/gmail.test.ts` (unit tests) -- Three-way merges Gmail support into `src/index.ts` (conditional GmailChannel creation) -- Three-way merges Gmail config into `src/config.ts` (`GMAIL_CHANNEL_ENABLED`) +- Three-way merges Gmail channel wiring into `src/index.ts` (GmailChannel creation) - Three-way merges Gmail credentials mount into `src/container-runner.ts` (~/.gmail-mcp -> /home/node/.gmail-mcp) - Three-way merges Gmail MCP server into `container/agent-runner/src/index.ts` (@gongrzhe/server-gmail-autoauth-mcp) - Three-way merges Gmail JID tests into `src/routing.test.ts` - Installs the `googleapis` npm dependency -- Updates `.env.example` with `GMAIL_CHANNEL_ENABLED` - Records the application in `.nanoclaw/state.yaml` If the apply reports merge conflicts, read the intent files: - `modify/src/index.ts.intent.md` — what changed and invariants for index.ts -- `modify/src/config.ts.intent.md` — what changed for config.ts - `modify/src/container-runner.ts.intent.md` — what changed for container-runner.ts - `modify/container/agent-runner/src/index.ts.intent.md` — what changed for agent-runner @@ -106,7 +103,7 @@ All tests must pass (including the new gmail tests) and build must be clean befo ls -la ~/.gmail-mcp/ 2>/dev/null || echo "No Gmail config found" ``` -If `credentials.json` already exists, skip to "Configure environment" below. +If `credentials.json` already exists, skip to "Build and restart" below. ### GCP Project Setup @@ -146,20 +143,6 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp auth If that fails (some versions don't have an auth subcommand), try `timeout 60 npx -y @gongrzhe/server-gmail-autoauth-mcp || true`. Verify with `ls ~/.gmail-mcp/credentials.json`. -### Configure environment - -**Channel mode only** — add to `.env`: - -``` -GMAIL_CHANNEL_ENABLED=true -``` - -Sync to container environment: - -```bash -mkdir -p data/env && cp .env data/env/env -``` - ### Build and restart Clear stale per-group agent-runner copies (they only get re-created if missing, so existing copies won't pick up the new Gmail server): @@ -188,7 +171,7 @@ launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS Tell the user: -> Gmail is connected! Send this in your WhatsApp main channel: +> Gmail is connected! Send this in your main channel: > > `@Andy check my recent emails` or `@Andy list my Gmail labels` @@ -228,9 +211,8 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp - Verify `~/.gmail-mcp` is mounted: check `src/container-runner.ts` for the `.gmail-mcp` mount - Check container logs: `cat groups/main/logs/container-*.log | tail -50` -### Emails not being detected (Channel Mode only) +### Emails not being detected (Channel mode only) -- Check `GMAIL_CHANNEL_ENABLED=true` in `.env` AND `data/env/env` - By default, the channel polls unread Primary inbox emails (`is:unread category:primary`) - Check logs for Gmail polling errors @@ -248,12 +230,10 @@ npx -y @gongrzhe/server-gmail-autoauth-mcp 1. Delete `src/channels/gmail.ts` and `src/channels/gmail.test.ts` 2. Remove `GmailChannel` import and creation from `src/index.ts` -3. Remove Gmail config (`GMAIL_CHANNEL_ENABLED`) from `src/config.ts` -4. Remove `~/.gmail-mcp` mount from `src/container-runner.ts` -5. Remove `gmail` MCP server and `mcp__gmail__*` from `container/agent-runner/src/index.ts` -6. Remove Gmail JID tests from `src/routing.test.ts` -7. Uninstall: `npm uninstall googleapis` -8. Remove env var from `.env` and `data/env/env` -9. Remove `gmail` from `.nanoclaw/state.yaml` -10. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true` -11. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) +3. Remove `~/.gmail-mcp` mount from `src/container-runner.ts` +4. Remove `gmail` MCP server and `mcp__gmail__*` from `container/agent-runner/src/index.ts` +5. Remove Gmail JID tests from `src/routing.test.ts` +6. Uninstall: `npm uninstall googleapis` +7. Remove `gmail` from `.nanoclaw/state.yaml` +8. Clear stale agent-runner copies: `rm -r data/sessions/*/agent-runner-src 2>/dev/null || true` +9. Rebuild: `cd container && ./build.sh && cd .. && npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw` (macOS) or `systemctl --user restart nanoclaw` (Linux) diff --git a/.claude/skills/add-gmail/manifest.yaml b/.claude/skills/add-gmail/manifest.yaml index f407b75..ea7c66a 100644 --- a/.claude/skills/add-gmail/manifest.yaml +++ b/.claude/skills/add-gmail/manifest.yaml @@ -7,15 +7,12 @@ adds: - src/channels/gmail.test.ts modifies: - src/index.ts - - src/config.ts - src/container-runner.ts - container/agent-runner/src/index.ts - src/routing.test.ts structured: npm_dependencies: googleapis: "^144.0.0" - env_additions: - - GMAIL_CHANNEL_ENABLED conflicts: [] depends: [] test: "npx vitest run src/channels/gmail.test.ts" diff --git a/.claude/skills/add-gmail/modify/src/config.ts b/.claude/skills/add-gmail/modify/src/config.ts deleted file mode 100644 index c52fb7f..0000000 --- a/.claude/skills/add-gmail/modify/src/config.ts +++ /dev/null @@ -1,74 +0,0 @@ -import os from 'os'; -import path from 'path'; - -import { readEnvFile } from './env.js'; - -// Read config values from .env (falls back to process.env). -// Secrets are NOT read here — they stay on disk and are loaded only -// where needed (container-runner.ts) to avoid leaking to child processes. -const envConfig = readEnvFile([ - 'ASSISTANT_NAME', - 'ASSISTANT_HAS_OWN_NUMBER', - 'GMAIL_CHANNEL_ENABLED', -]); - -export const ASSISTANT_NAME = - process.env.ASSISTANT_NAME || envConfig.ASSISTANT_NAME || 'Andy'; -export const ASSISTANT_HAS_OWN_NUMBER = - (process.env.ASSISTANT_HAS_OWN_NUMBER || envConfig.ASSISTANT_HAS_OWN_NUMBER) === 'true'; -export const POLL_INTERVAL = 2000; -export const SCHEDULER_POLL_INTERVAL = 60000; - -// Absolute paths needed for container mounts -const PROJECT_ROOT = process.cwd(); -const HOME_DIR = process.env.HOME || os.homedir(); - -// Mount security: allowlist stored OUTSIDE project root, never mounted into containers -export const MOUNT_ALLOWLIST_PATH = path.join( - HOME_DIR, - '.config', - 'nanoclaw', - 'mount-allowlist.json', -); -export const STORE_DIR = path.resolve(PROJECT_ROOT, 'store'); -export const GROUPS_DIR = path.resolve(PROJECT_ROOT, 'groups'); -export const DATA_DIR = path.resolve(PROJECT_ROOT, 'data'); -export const MAIN_GROUP_FOLDER = 'main'; - -export const CONTAINER_IMAGE = - process.env.CONTAINER_IMAGE || 'nanoclaw-agent:latest'; -export const CONTAINER_TIMEOUT = parseInt( - process.env.CONTAINER_TIMEOUT || '1800000', - 10, -); -export const CONTAINER_MAX_OUTPUT_SIZE = parseInt( - process.env.CONTAINER_MAX_OUTPUT_SIZE || '10485760', - 10, -); // 10MB default -export const IPC_POLL_INTERVAL = 1000; -export const IDLE_TIMEOUT = parseInt( - process.env.IDLE_TIMEOUT || '1800000', - 10, -); // 30min default — how long to keep container alive after last result -export const MAX_CONCURRENT_CONTAINERS = Math.max( - 1, - parseInt(process.env.MAX_CONCURRENT_CONTAINERS || '5', 10) || 5, -); - -function escapeRegex(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -export const TRIGGER_PATTERN = new RegExp( - `^@${escapeRegex(ASSISTANT_NAME)}\\b`, - 'i', -); - -// Timezone for scheduled tasks (cron expressions, etc.) -// Uses system timezone by default -export const TIMEZONE = - process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone; - -// Gmail configuration -export const GMAIL_CHANNEL_ENABLED = - (process.env.GMAIL_CHANNEL_ENABLED || envConfig.GMAIL_CHANNEL_ENABLED) === 'true'; diff --git a/.claude/skills/add-gmail/modify/src/config.ts.intent.md b/.claude/skills/add-gmail/modify/src/config.ts.intent.md deleted file mode 100644 index defdffd..0000000 --- a/.claude/skills/add-gmail/modify/src/config.ts.intent.md +++ /dev/null @@ -1,20 +0,0 @@ -# Intent: src/config.ts modifications - -## What changed -Added configuration exports for Gmail channel support. - -## Key sections -- **readEnvFile call**: Must include `GMAIL_CHANNEL_ENABLED` in the keys array. NanoClaw does NOT load `.env` into `process.env` — all `.env` values must be explicitly requested via `readEnvFile()`. -- **GMAIL_CHANNEL_ENABLED**: Boolean feature flag — when `true`, the Gmail channel is connected and polls for inbound emails. When `false` (default), Gmail is available as a tool only (agent can read/send emails when asked from other channels). - -## Invariants -- All existing config exports remain unchanged -- New Gmail keys are added to the `readEnvFile` call alongside existing keys -- New exports are appended at the end of the file -- No existing behavior is modified — Gmail config is additive and minimal -- Both `process.env` and `envConfig` are checked (same pattern as `ASSISTANT_NAME`) - -## Must-keep -- All existing exports (`ASSISTANT_NAME`, `POLL_INTERVAL`, `TRIGGER_PATTERN`, etc.) -- The `readEnvFile` pattern — ALL config read from `.env` must go through this function -- The `escapeRegex` helper and `TRIGGER_PATTERN` construction diff --git a/.claude/skills/add-gmail/modify/src/index.ts b/.claude/skills/add-gmail/modify/src/index.ts index 455d7ef..1b04695 100644 --- a/.claude/skills/add-gmail/modify/src/index.ts +++ b/.claude/skills/add-gmail/modify/src/index.ts @@ -3,7 +3,6 @@ import path from 'path'; import { ASSISTANT_NAME, - GMAIL_CHANNEL_ENABLED, IDLE_TIMEOUT, MAIN_GROUP_FOLDER, POLL_INTERVAL, @@ -451,11 +450,9 @@ async function main(): Promise { channels.push(whatsapp); await whatsapp.connect(); - if (GMAIL_CHANNEL_ENABLED) { - const gmail = new GmailChannel(channelOpts); - channels.push(gmail); - await gmail.connect(); - } + const gmail = new GmailChannel(channelOpts); + channels.push(gmail); + await gmail.connect(); // Start subsystems (independently of connection handler) startSchedulerLoop({ diff --git a/.claude/skills/add-gmail/modify/src/index.ts.intent.md b/.claude/skills/add-gmail/modify/src/index.ts.intent.md index 7df8bf7..cd700f5 100644 --- a/.claude/skills/add-gmail/modify/src/index.ts.intent.md +++ b/.claude/skills/add-gmail/modify/src/index.ts.intent.md @@ -2,24 +2,21 @@ ## What changed -Added Gmail as a channel option alongside WhatsApp (and any other channels). +Added Gmail as a channel. ## Key sections ### Imports (top of file) - Added: `GmailChannel` from `./channels/gmail.js` -- Added: `GMAIL_CHANNEL_ENABLED` from `./config.js` ### main() -- Added: conditional Gmail channel creation after WhatsApp: +- Added Gmail channel creation: ``` - if (GMAIL_CHANNEL_ENABLED) { - const gmail = new GmailChannel(channelOpts); - channels.push(gmail); - await gmail.connect(); - } + const gmail = new GmailChannel(channelOpts); + channels.push(gmail); + await gmail.connect(); ``` - Gmail uses the same `channelOpts` callbacks as other channels - Incoming emails are delivered to the main group (agent decides how to respond, user can configure) @@ -31,7 +28,7 @@ Added Gmail as a channel option alongside WhatsApp (and any other channels). - State management (loadState/saveState) is unchanged - Recovery logic is unchanged - Container runtime check is unchanged -- WhatsApp and any other channel creation is untouched +- Any other channel creation is untouched - Shutdown iterates `channels` array (Gmail is included automatically) ## Must-keep diff --git a/.claude/skills/add-gmail/tests/gmail.test.ts b/.claude/skills/add-gmail/tests/gmail.test.ts index 2436bca..02d9721 100644 --- a/.claude/skills/add-gmail/tests/gmail.test.ts +++ b/.claude/skills/add-gmail/tests/gmail.test.ts @@ -29,10 +29,6 @@ describe('add-gmail skill', () => { expect(fs.existsSync(path.join(root, 'src/channels/gmail.ts'))).toBe(true); }); - it.skipIf(channelOnly)('config exports GMAIL_CHANNEL_ENABLED', () => { - expect(read('src/config.ts')).toContain('GMAIL_CHANNEL_ENABLED'); - }); - it.skipIf(channelOnly)('index.ts wires up GmailChannel', () => { expect(read('src/index.ts')).toContain('GmailChannel'); });