diff --git a/docs/configuration.md b/docs/configuration.md index 42365a6..c071b23 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -387,15 +387,18 @@ polling: intervalMs: 60000 # Check every 60 seconds (default: 60000) gmail: enabled: true - account: user@example.com # Gmail account to poll + accounts: # Gmail accounts to poll + - user@example.com + - other@example.com ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `polling.enabled` | boolean | auto | Master switch. Defaults to `true` if any sub-config is enabled | | `polling.intervalMs` | number | `60000` | Polling interval in milliseconds | -| `polling.gmail.enabled` | boolean | auto | Enable Gmail polling. Auto-detected from `account` | +| `polling.gmail.enabled` | boolean | auto | Enable Gmail polling. Auto-detected from `account` or `accounts` | | `polling.gmail.account` | string | - | Gmail account to poll for unread messages | +| `polling.gmail.accounts` | string[] | - | Gmail accounts to poll for unread messages | ### Legacy config path @@ -405,7 +408,9 @@ For backward compatibility, Gmail polling can also be configured under `integrat integrations: google: enabled: true - account: user@example.com + accounts: + - account: user@example.com + services: [gmail, calendar] pollIntervalSec: 60 ``` @@ -415,7 +420,7 @@ The top-level `polling` section takes priority if both are present. | Env Variable | Polling Config Equivalent | |--------------|--------------------------| -| `GMAIL_ACCOUNT` | `polling.gmail.account` | +| `GMAIL_ACCOUNT` | `polling.gmail.account` (comma-separated list allowed) | | `POLLING_INTERVAL_MS` | `polling.intervalMs` | | `PORT` | `api.port` | | `API_HOST` | `api.host` | @@ -547,7 +552,7 @@ Environment variables override config file values: | `WHATSAPP_SELF_CHAT_MODE` | `channels.whatsapp.selfChat` | | `SIGNAL_PHONE_NUMBER` | `channels.signal.phone` | | `OPENAI_API_KEY` | `transcription.apiKey` | -| `GMAIL_ACCOUNT` | `polling.gmail.account` | +| `GMAIL_ACCOUNT` | `polling.gmail.account` (comma-separated list allowed) | | `POLLING_INTERVAL_MS` | `polling.intervalMs` | See [SKILL.md](../SKILL.md) for complete environment variable reference. diff --git a/src/config/io.ts b/src/config/io.ts index bdb3855..a33d4ac 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -210,16 +210,26 @@ export function configToEnv(config: LettaBotConfig): Record { } // Polling - top-level polling config (preferred) - if (config.polling?.gmail?.enabled && config.polling.gmail.account) { - env.GMAIL_ACCOUNT = config.polling.gmail.account; + if (config.polling?.gmail?.enabled) { + const accounts = config.polling.gmail.accounts !== undefined + ? config.polling.gmail.accounts + : (config.polling.gmail.account ? [config.polling.gmail.account] : []); + if (accounts.length > 0) { + env.GMAIL_ACCOUNT = accounts.join(','); + } } if (config.polling?.intervalMs) { env.POLLING_INTERVAL_MS = String(config.polling.intervalMs); } // Integrations - Google (legacy path for Gmail polling, lower priority) - if (!env.GMAIL_ACCOUNT && config.integrations?.google?.enabled && config.integrations.google.account) { - env.GMAIL_ACCOUNT = config.integrations.google.account; + if (!env.GMAIL_ACCOUNT && config.integrations?.google?.enabled) { + const legacyAccounts = config.integrations.google.accounts + ? config.integrations.google.accounts.map(a => a.account) + : (config.integrations.google.account ? [config.integrations.google.account] : []); + if (legacyAccounts.length > 0) { + env.GMAIL_ACCOUNT = legacyAccounts.join(','); + } } if (!env.POLLING_INTERVAL_MS && config.integrations?.google?.pollIntervalSec) { env.POLLING_INTERVAL_MS = String(config.integrations.google.pollIntervalSec * 1000); diff --git a/src/config/types.ts b/src/config/types.ts index 0437a09..279c062 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -131,6 +131,7 @@ export interface PollingYamlConfig { gmail?: { enabled?: boolean; // Enable Gmail polling account?: string; // Gmail account to poll (e.g., user@example.com) + accounts?: string[]; // Multiple Gmail accounts to poll }; } @@ -200,9 +201,15 @@ export interface DiscordConfig { instantGroups?: string[]; // Guild/server IDs or channel IDs that bypass batching } +export interface GoogleAccountConfig { + account: string; + services?: string[]; // e.g., ['gmail', 'calendar', 'drive', 'contacts', 'docs', 'sheets'] +} + export interface GoogleConfig { enabled: boolean; account?: string; + accounts?: GoogleAccountConfig[]; services?: string[]; // e.g., ['gmail', 'calendar', 'drive', 'contacts', 'docs', 'sheets'] pollIntervalSec?: number; // Polling interval in seconds (default: 60) } diff --git a/src/main.ts b/src/main.ts index 9af7e15..58e2058 100644 --- a/src/main.ts +++ b/src/main.ts @@ -150,7 +150,7 @@ import { DiscordAdapter } from './channels/discord.js'; import { GroupBatcher } from './core/group-batcher.js'; import { CronService } from './cron/service.js'; import { HeartbeatService } from './cron/heartbeat.js'; -import { PollingService } from './polling/service.js'; +import { PollingService, parseGmailAccounts } from './polling/service.js'; import { agentExists, findAgentByName, ensureNoToolApprovals } from './tools/letta-api.js'; // Skills are now installed to agent-scoped location after agent creation (see bot.ts) @@ -569,24 +569,42 @@ async function main() { } bot.onTriggerHeartbeat = () => heartbeatService.trigger(); - // Per-agent polling - const pollConfig = agentConfig.polling || (agentConfig.integrations?.google ? { - enabled: agentConfig.integrations.google.enabled, - intervalMs: (agentConfig.integrations.google.pollIntervalSec || 60) * 1000, - gmail: { - enabled: agentConfig.integrations.google.enabled, - account: agentConfig.integrations.google.account || '', - }, - } : undefined); + // Per-agent polling -- resolve accounts from polling > integrations.google (legacy) > env + const pollConfig = (() => { + const pollingAccounts = parseGmailAccounts( + agentConfig.polling?.gmail?.accounts || agentConfig.polling?.gmail?.account + ); + const legacyAccounts = (() => { + const legacy = agentConfig.integrations?.google; + if (legacy?.accounts?.length) { + return parseGmailAccounts(legacy.accounts.map(a => a.account)); + } + return parseGmailAccounts(legacy?.account); + })(); + const envAccounts = parseGmailAccounts(process.env.GMAIL_ACCOUNT); + const gmailAccounts = pollingAccounts.length > 0 + ? pollingAccounts + : legacyAccounts.length > 0 + ? legacyAccounts + : envAccounts; + const gmailEnabled = agentConfig.polling?.gmail?.enabled + ?? agentConfig.integrations?.google?.enabled + ?? gmailAccounts.length > 0; + return { + enabled: agentConfig.polling?.enabled ?? gmailEnabled, + intervalMs: agentConfig.polling?.intervalMs + ?? (agentConfig.integrations?.google?.pollIntervalSec + ? agentConfig.integrations.google.pollIntervalSec * 1000 + : 60000), + gmail: { enabled: gmailEnabled, accounts: gmailAccounts }, + }; + })(); - if (pollConfig?.enabled && pollConfig.gmail?.enabled) { + if (pollConfig.enabled && pollConfig.gmail.enabled && pollConfig.gmail.accounts.length > 0) { const pollingService = new PollingService(bot, { - intervalMs: pollConfig.intervalMs || 60000, + intervalMs: pollConfig.intervalMs, workingDir: globalConfig.workingDir, - gmail: { - enabled: pollConfig.gmail.enabled, - account: pollConfig.gmail.account || '', - }, + gmail: pollConfig.gmail, }); pollingService.start(); services.pollingServices.push(pollingService); diff --git a/src/onboard.ts b/src/onboard.ts index 3757ac9..ca83ae0 100644 --- a/src/onboard.ts +++ b/src/onboard.ts @@ -154,7 +154,7 @@ interface OnboardConfig { discord: { enabled: boolean; token?: string; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[] }; // Google Workspace (via gog CLI) - google: { enabled: boolean; account?: string; services?: string[] }; + google: { enabled: boolean; accounts: Array<{ account: string; services: string[] }> }; // Features heartbeat: { enabled: boolean; interval?: string }; @@ -751,6 +751,7 @@ async function stepGoogle(config: OnboardConfig): Promise { if (!setupGoogle) { config.google.enabled = false; + config.google.accounts = []; return; } @@ -785,16 +786,19 @@ async function stepGoogle(config: OnboardConfig): Promise { spinner.stop('Failed to install gog'); p.log.error('Installation failed. Try manually: brew install steipete/tap/gogcli'); config.google.enabled = false; + config.google.accounts = []; return; } } else { p.log.info('Install gog manually: brew install steipete/tap/gogcli'); config.google.enabled = false; + config.google.accounts = []; return; } } else { p.log.info('Install gog manually from: https://gogcli.sh'); config.google.enabled = false; + config.google.accounts = []; return; } } @@ -836,6 +840,7 @@ async function stepGoogle(config: OnboardConfig): Promise { if (!hasCredentials) { p.log.info('Run `gog auth credentials /path/to/client_secret.json` after downloading credentials.'); config.google.enabled = false; + config.google.accounts = []; return; } } @@ -860,60 +865,116 @@ async function stepGoogle(config: OnboardConfig): Promise { } } } + + const configuredAccounts = new Map(); + for (const entry of config.google.accounts) { + configuredAccounts.set(entry.account, entry.services || []); + } - let selectedAccount: string | undefined; - - if (accounts.length > 0) { - const accountChoice = await p.select({ - message: 'Google account', + const newAccounts: Array<{ account: string; services: string[] }> = []; + let selectedAccounts: string[] = []; + + if (accounts.length === 0) { + const firstAccount = await addGoogleAccount(); + if (!firstAccount) { + config.google.enabled = false; + config.google.accounts = []; + return; + } + newAccounts.push(firstAccount); + while (true) { + const more = await p.confirm({ + message: 'Add another Google account?', + initialValue: false, + }); + if (p.isCancel(more)) { p.cancel('Setup cancelled'); process.exit(0); } + if (!more) break; + const added = await addGoogleAccount(); + if (added) { + newAccounts.push(added); + } else { + break; + } + } + } else { + const accountChoices = await p.multiselect({ + message: 'Google accounts to enable', options: [ ...accounts.map(a => ({ value: a, label: a, hint: 'Existing account' })), { value: '__new__', label: 'Add new account', hint: 'Authorize another account' }, ], - initialValue: config.google.account || accounts[0], + initialValues: config.google.accounts.map(a => a.account).filter(a => accounts.includes(a)), + required: true, }); - if (p.isCancel(accountChoice)) { p.cancel('Setup cancelled'); process.exit(0); } - - if (accountChoice === '__new__') { - selectedAccount = await addGoogleAccount(); - } else { - selectedAccount = accountChoice as string; + if (p.isCancel(accountChoices)) { p.cancel('Setup cancelled'); process.exit(0); } + + selectedAccounts = (accountChoices as string[]).filter(a => a !== '__new__'); + if ((accountChoices as string[]).includes('__new__')) { + while (true) { + const added = await addGoogleAccount(); + if (added) { + newAccounts.push(added); + } else { + break; + } + const more = await p.confirm({ + message: 'Add another Google account?', + initialValue: false, + }); + if (p.isCancel(more)) { p.cancel('Setup cancelled'); process.exit(0); } + if (!more) break; + } } - } else { - selectedAccount = await addGoogleAccount(); } - - if (!selectedAccount) { + + const allAccounts = Array.from(new Set([ + ...selectedAccounts, + ...newAccounts.map(a => a.account), + ])); + + if (allAccounts.length === 0) { config.google.enabled = false; + config.google.accounts = []; return; } - - // Select services - const selectedServices = await p.multiselect({ - message: 'Which Google services do you want to enable?', - options: GOG_SERVICES.map(s => ({ - value: s, - label: s.charAt(0).toUpperCase() + s.slice(1), - hint: s === 'gmail' ? 'Read/send emails' : - s === 'calendar' ? 'View/create events' : - s === 'drive' ? 'Access files' : - s === 'contacts' ? 'Look up contacts' : - s === 'docs' ? 'Read documents' : - 'Read/edit spreadsheets', - })), - initialValues: config.google.services || ['gmail', 'calendar'], - required: true, - }); - if (p.isCancel(selectedServices)) { p.cancel('Setup cancelled'); process.exit(0); } - + + const newAccountsByEmail = new Map(); + for (const entry of newAccounts) { + newAccountsByEmail.set(entry.account, entry.services); + } + + const finalizedAccounts: Array<{ account: string; services: string[] }> = []; + for (const account of allAccounts) { + const presetServices = newAccountsByEmail.get(account) || configuredAccounts.get(account) || ['gmail', 'calendar']; + const selectedServices = newAccountsByEmail.has(account) ? presetServices : await p.multiselect({ + message: `Services to enable for ${account}`, + options: GOG_SERVICES.map(s => ({ + value: s, + label: s.charAt(0).toUpperCase() + s.slice(1), + hint: s === 'gmail' ? 'Read/send emails' : + s === 'calendar' ? 'View/create events' : + s === 'drive' ? 'Access files' : + s === 'contacts' ? 'Look up contacts' : + s === 'docs' ? 'Read documents' : + 'Read/edit spreadsheets', + })), + initialValues: presetServices, + required: true, + }); + if (p.isCancel(selectedServices)) { p.cancel('Setup cancelled'); process.exit(0); } + finalizedAccounts.push({ + account, + services: selectedServices as string[], + }); + } + config.google.enabled = true; - config.google.account = selectedAccount; - config.google.services = selectedServices as string[]; + config.google.accounts = finalizedAccounts; - p.log.success(`Google Workspace configured: ${selectedAccount}`); + p.log.success(`Google Workspace configured: ${finalizedAccounts.length} account(s)`); } -async function addGoogleAccount(): Promise { +async function addGoogleAccount(): Promise<{ account: string; services: string[] } | undefined> { const email = await p.text({ message: 'Google account email', placeholder: 'you@gmail.com', @@ -951,7 +1012,7 @@ async function addGoogleAccount(): Promise { if (result.status === 0) { spinner.stop('Account authorized'); - return email; + return { account: email, services: services as string[] }; } else { spinner.stop('Authorization failed'); p.log.error('Failed to authorize account. Try manually: gog auth add ' + email); @@ -1009,7 +1070,10 @@ function showSummary(config: OnboardConfig): void { // Google if (config.google.enabled) { - lines.push(`Google: ${config.google.account} (${config.google.services?.join(', ') || 'all'})`); + const googleAccounts = config.google.accounts.map(a => ( + `${a.account} (${a.services?.join(', ') || 'all'})` + )); + lines.push(`Google: ${googleAccounts.join(', ')}`); } p.note(lines.join('\n'), 'Configuration'); @@ -1257,11 +1321,21 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise { + const existingAccounts = existingConfig.integrations?.google?.accounts + ? existingConfig.integrations.google.accounts.map(a => ({ + account: a.account, + services: a.services || [], + })) + : (existingConfig.integrations?.google?.account ? [{ + account: existingConfig.integrations.google.account, + services: existingConfig.integrations.google.services || [], + }] : []); + return { + enabled: (existingConfig.integrations?.google?.enabled || false) || existingAccounts.length > 0, + accounts: existingAccounts, + }; + })(), heartbeat: { enabled: existingConfig.features?.heartbeat?.enabled || false, interval: existingConfig.features?.heartbeat?.intervalMin?.toString(), @@ -1420,7 +1494,9 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise 0 + ? ` ✓ Google (${config.google.accounts.map(a => `${a.account} - ${a.services?.join(', ') || 'all'}`).join(', ')})` + : ' ✗ Google Workspace', '', 'Features:', config.heartbeat.enabled ? ` ✓ Heartbeat (${config.heartbeat.interval}min)` : ' ✗ Heartbeat', @@ -1503,10 +1579,17 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise { + const gmailAccounts = config.google.accounts + .filter(a => a.services?.includes('gmail')) + .map(a => a.account); + return gmailAccounts.length > 0 ? { + polling: { gmail: { accounts: gmailAccounts } }, + } : {}; + })()), } : {}), }; diff --git a/src/polling/service.test.ts b/src/polling/service.test.ts new file mode 100644 index 0000000..b6d3cda --- /dev/null +++ b/src/polling/service.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { parseGmailAccounts } from './service.js'; + +describe('parseGmailAccounts', () => { + it('returns empty array for undefined', () => { + expect(parseGmailAccounts(undefined)).toEqual([]); + }); + + it('returns empty array for empty string', () => { + expect(parseGmailAccounts('')).toEqual([]); + }); + + it('parses single account string', () => { + expect(parseGmailAccounts('user@gmail.com')).toEqual(['user@gmail.com']); + }); + + it('parses comma-separated string', () => { + expect(parseGmailAccounts('a@gmail.com,b@gmail.com')).toEqual(['a@gmail.com', 'b@gmail.com']); + }); + + it('trims whitespace', () => { + expect(parseGmailAccounts(' a@gmail.com , b@gmail.com ')).toEqual(['a@gmail.com', 'b@gmail.com']); + }); + + it('deduplicates accounts', () => { + expect(parseGmailAccounts('a@gmail.com,a@gmail.com,b@gmail.com')).toEqual(['a@gmail.com', 'b@gmail.com']); + }); + + it('skips empty segments', () => { + expect(parseGmailAccounts('a@gmail.com,,b@gmail.com,')).toEqual(['a@gmail.com', 'b@gmail.com']); + }); + + it('accepts string array', () => { + expect(parseGmailAccounts(['a@gmail.com', 'b@gmail.com'])).toEqual(['a@gmail.com', 'b@gmail.com']); + }); + + it('deduplicates string array', () => { + expect(parseGmailAccounts(['a@gmail.com', 'a@gmail.com'])).toEqual(['a@gmail.com']); + }); + + it('trims string array values', () => { + expect(parseGmailAccounts([' a@gmail.com ', ' b@gmail.com '])).toEqual(['a@gmail.com', 'b@gmail.com']); + }); +}); diff --git a/src/polling/service.ts b/src/polling/service.ts index 7dd8c37..626a49c 100644 --- a/src/polling/service.ts +++ b/src/polling/service.ts @@ -10,12 +10,28 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import type { AgentSession } from '../core/interfaces.js'; +/** + * Parse Gmail accounts from a string (comma-separated) or string array. + * Deduplicates and trims whitespace. + */ +export function parseGmailAccounts(raw?: string | string[]): string[] { + if (!raw) return []; + const values = Array.isArray(raw) ? raw : raw.split(','); + const seen = new Set(); + for (const value of values) { + const trimmed = value.trim(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + } + return Array.from(seen); +} + export interface PollingConfig { intervalMs: number; // Polling interval in milliseconds workingDir: string; // For persisting state gmail?: { enabled: boolean; - account: string; + accounts: string[]; }; } @@ -24,8 +40,8 @@ export class PollingService { private bot: AgentSession; private config: PollingConfig; - // Track seen email IDs to detect new emails (persisted to disk) - private seenEmailIds: Set = new Set(); + // Track seen email IDs per account to detect new emails (persisted to disk) + private seenEmailIdsByAccount: Map> = new Map(); private seenEmailsPath: string; constructor(bot: AgentSession, config: PollingConfig) { @@ -42,8 +58,28 @@ export class PollingService { try { if (existsSync(this.seenEmailsPath)) { const data = JSON.parse(readFileSync(this.seenEmailsPath, 'utf-8')); - this.seenEmailIds = new Set(data.ids || []); - console.log(`[Polling] Loaded ${this.seenEmailIds.size} seen email IDs`); + + // New per-account format: { accounts: { "email": { ids: [...] } } } + if (data && typeof data === 'object' && data.accounts && typeof data.accounts === 'object') { + for (const [account, accountData] of Object.entries(data.accounts)) { + const ids = Array.isArray((accountData as { ids?: string[] }).ids) + ? (accountData as { ids?: string[] }).ids! + : []; + this.seenEmailIdsByAccount.set(account, new Set(ids)); + } + console.log(`[Polling] Loaded seen email IDs for ${this.seenEmailIdsByAccount.size} account(s)`); + return; + } + + // Legacy single-account format: { ids: [...] } + if (data && Array.isArray(data.ids)) { + const accounts = this.config.gmail?.accounts || []; + const targetAccount = accounts[0]; + if (targetAccount) { + this.seenEmailIdsByAccount.set(targetAccount, new Set(data.ids)); + console.log(`[Polling] Migrated legacy seen emails to ${targetAccount}`); + } + } } } catch (e) { console.error('[Polling] Failed to load seen emails:', e); @@ -55,9 +91,17 @@ export class PollingService { */ private saveSeenEmails(): void { try { + const accounts: Record = {}; + const now = new Date().toISOString(); + for (const [account, ids] of this.seenEmailIdsByAccount.entries()) { + accounts[account] = { + ids: Array.from(ids), + updatedAt: now, + }; + } writeFileSync(this.seenEmailsPath, JSON.stringify({ - ids: Array.from(this.seenEmailIds), - updatedAt: new Date().toISOString(), + accounts, + updatedAt: now, }, null, 2)); } catch (e) { console.error('[Polling] Failed to save seen emails:', e); @@ -74,7 +118,13 @@ export class PollingService { } const enabledPollers: string[] = []; - if (this.config.gmail?.enabled) enabledPollers.push('Gmail'); + if (this.config.gmail?.enabled) { + if (this.config.gmail.accounts.length > 0) { + enabledPollers.push(`Gmail (${this.config.gmail.accounts.length} account${this.config.gmail.accounts.length === 1 ? '' : 's'})`); + } else { + console.log('[Polling] Gmail enabled but no accounts configured'); + } + } if (enabledPollers.length === 0) { console.log('[Polling] No pollers enabled'); @@ -106,16 +156,21 @@ export class PollingService { */ private async poll(): Promise { if (this.config.gmail?.enabled) { - await this.pollGmail(); + for (const account of this.config.gmail.accounts) { + await this.pollGmail(account); + } } } /** * Poll Gmail for new unread messages */ - private async pollGmail(): Promise { - const account = this.config.gmail?.account; + private async pollGmail(account: string): Promise { if (!account) return; + if (!this.seenEmailIdsByAccount.has(account)) { + this.seenEmailIdsByAccount.set(account, new Set()); + } + const seenEmailIds = this.seenEmailIdsByAccount.get(account)!; try { // Check for unread emails (use longer window to catch any we might have missed) @@ -130,7 +185,7 @@ export class PollingService { }); if (result.status !== 0) { - console.log(`[Polling] 📧 Gmail check failed: ${result.stderr || 'unknown error'}`); + console.log(`[Polling] Gmail check failed for ${account}: ${result.stderr || 'unknown error'}`); return; } @@ -147,7 +202,7 @@ export class PollingService { const id = line.split(/\s+/)[0]; // First column is ID if (id) { currentEmailIds.add(id); - if (!this.seenEmailIds.has(id)) { + if (!seenEmailIds.has(id)) { newEmails.push(line); } } @@ -155,17 +210,17 @@ export class PollingService { // Add new IDs to seen set (don't replace - we want to remember all seen emails) for (const id of currentEmailIds) { - this.seenEmailIds.add(id); + seenEmailIds.add(id); } this.saveSeenEmails(); // Only notify if there are NEW emails we haven't seen before if (newEmails.length === 0) { - console.log(`[Polling] 📧 No new emails (${currentEmailIds.size} unread total)`); + console.log(`[Polling] No new emails for ${account} (${currentEmailIds.size} unread total)`); return; } - console.log(`[Polling] 📧 Found ${newEmails.length} NEW email(s)!`); + console.log(`[Polling] Found ${newEmails.length} NEW email(s) for ${account}!`); // Build output with header + new emails only const header = lines[0]; @@ -179,7 +234,7 @@ export class PollingService { '║ To send a message, use: lettabot-message send --text "..." ║', '╚════════════════════════════════════════════════════════════════╝', '', - `[email] ${newEmails.length} new unread email(s):`, + `[email] ${newEmails.length} new unread email(s) for ${account}:`, '', newEmailsOutput, '', @@ -189,11 +244,11 @@ export class PollingService { const response = await this.bot.sendToAgent(message); // Log response but do NOT auto-deliver (silent mode) - console.log(`[Polling] 📧 Agent finished (SILENT MODE)`); + console.log(`[Polling] Agent finished (SILENT MODE)`); console.log(` - Response: ${response?.slice(0, 100)}${(response?.length || 0) > 100 ? '...' : ''}`); console.log(` - (Response NOT auto-delivered - agent uses lettabot-message CLI)`) } catch (e) { - console.error('[Polling] 📧 Gmail error:', e); + console.error(`[Polling] Gmail error for ${account}:`, e); } } }