diff --git a/package-lock.json b/package-lock.json index 2362fb1..ae0a2a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@types/express": "^5.0.6", "@types/node": "^25.0.10", "@types/node-schedule": "^2.1.8", + "discord.js": "^14.25.1", "dotenv": "^17.2.3", "express": "^5.2.1", "googleapis": "^170.1.0", @@ -32,12 +33,13 @@ "bin": { "lettabot": "dist/cli.js", "lettabot-message": "dist/cli/message.js", + "lettabot-react": "dist/cli/react.js", "lettabot-schedule": "dist/cron/cli.js" }, "optionalDependencies": { "@slack/bolt": "^4.6.0", "@whiskeysockets/baileys": "^6.7.21", - "discord.js": "^14.18.0" + "discord.js": "^14.25.1" } }, "letta-code": { diff --git a/package.json b/package.json index c935732..410d406 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,6 @@ "optionalDependencies": { "@slack/bolt": "^4.6.0", "@whiskeysockets/baileys": "^6.7.21", - "discord.js": "^14.18.0" + "discord.js": "^14.25.1" } } diff --git a/src/config/io.ts b/src/config/io.ts index 6c1719e..59dc492 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -152,6 +152,11 @@ export function configToEnv(config: LettaBotConfig): Record { env.HEARTBEAT_INTERVAL_MIN = String(config.features.heartbeat.intervalMin || 30); } + // Integrations - Google (Gmail polling) + if (config.integrations?.google?.enabled && config.integrations.google.account) { + env.GMAIL_ACCOUNT = config.integrations.google.account; + } + return env; } diff --git a/src/config/types.ts b/src/config/types.ts index de94df4..e69167e 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -44,6 +44,11 @@ export interface LettaBotConfig { intervalMin?: number; }; }; + + // Integrations (Google Workspace, etc.) + integrations?: { + google?: GoogleConfig; + }; } export interface ProviderConfig { @@ -89,6 +94,12 @@ export interface DiscordConfig { allowedUsers?: string[]; } +export interface GoogleConfig { + enabled: boolean; + account?: string; + services?: string[]; // e.g., ['gmail', 'calendar', 'drive', 'contacts', 'docs', 'sheets'] +} + // Default config export const DEFAULT_CONFIG: LettaBotConfig = { server: { diff --git a/src/onboard.ts b/src/onboard.ts index 930eade..f8e3545 100644 --- a/src/onboard.ts +++ b/src/onboard.ts @@ -37,7 +37,9 @@ interface OnboardConfig { whatsapp: { enabled: boolean; selfChat?: boolean; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[] }; signal: { enabled: boolean; phone?: string; selfChat?: boolean; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[] }; discord: { enabled: boolean; token?: string; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[] }; - gmail: { enabled: boolean; account?: string }; + + // Google Workspace (via gog CLI) + google: { enabled: boolean; account?: string; services?: string[] }; // Features heartbeat: { enabled: boolean; interval?: string }; @@ -854,6 +856,230 @@ async function stepFeatures(config: OnboardConfig): Promise { if (!p.isCancel(setupCron)) config.cron = setupCron; } +// ============================================================================ +// Google Workspace Setup (via gog CLI) +// ============================================================================ + +const GOG_SERVICES = ['gmail', 'calendar', 'drive', 'contacts', 'docs', 'sheets']; + +async function stepGoogle(config: OnboardConfig): Promise { + // Ask if user wants to set up Google + const setupGoogle = await p.confirm({ + message: 'Set up Google Workspace? (Gmail, Calendar, Drive, etc.)', + initialValue: config.google.enabled, + }); + if (p.isCancel(setupGoogle)) { p.cancel('Setup cancelled'); process.exit(0); } + + if (!setupGoogle) { + config.google.enabled = false; + return; + } + + // Check if gog is installed + const gogInstalled = spawnSync('which', ['gog'], { stdio: 'pipe' }).status === 0; + + if (!gogInstalled) { + p.log.warning('gog CLI is not installed.'); + + // Check if brew is available (macOS) + const brewInstalled = spawnSync('which', ['brew'], { stdio: 'pipe' }).status === 0; + + if (brewInstalled) { + const installGog = await p.confirm({ + message: 'Install gog via Homebrew?', + initialValue: true, + }); + if (p.isCancel(installGog)) { p.cancel('Setup cancelled'); process.exit(0); } + + if (installGog) { + const spinner = p.spinner(); + spinner.start('Installing gog...'); + + const result = spawnSync('brew', ['install', 'steipete/tap/gogcli'], { + stdio: 'pipe', + timeout: 300000, // 5 min timeout + }); + + if (result.status === 0) { + spinner.stop('gog installed successfully'); + } else { + spinner.stop('Failed to install gog'); + p.log.error('Installation failed. Try manually: brew install steipete/tap/gogcli'); + config.google.enabled = false; + return; + } + } else { + p.log.info('Install gog manually: brew install steipete/tap/gogcli'); + config.google.enabled = false; + return; + } + } else { + p.log.info('Install gog manually from: https://gogcli.sh'); + config.google.enabled = false; + return; + } + } + + // Check for existing credentials + const credentialsResult = spawnSync('gog', ['auth', 'list'], { stdio: 'pipe' }); + const hasCredentials = credentialsResult.status === 0 && + credentialsResult.stdout.toString().trim().length > 0 && + !credentialsResult.stdout.toString().includes('No accounts'); + + if (!hasCredentials) { + // Check if credentials.json exists + const configDir = process.env.XDG_CONFIG_HOME || `${process.env.HOME}/.config`; + const credPaths = [ + `${configDir}/gogcli/credentials.json`, + `${process.env.HOME}/Library/Application Support/gogcli/credentials.json`, + ]; + + const hasCredFile = credPaths.some(p => existsSync(p)); + + if (!hasCredFile) { + p.note( + 'To use Google Workspace, you need OAuth credentials:\n\n' + + '1. Go to console.cloud.google.com\n' + + '2. Create a project (or select existing)\n' + + '3. Enable APIs: Gmail, Calendar, Drive, etc.\n' + + '4. Create OAuth 2.0 credentials (Desktop app)\n' + + '5. Download the JSON file\n' + + '6. Run: gog auth credentials /path/to/credentials.json', + 'Google OAuth Setup' + ); + + const hasCredentials = await p.confirm({ + message: 'Have you already set up OAuth credentials with gog?', + initialValue: false, + }); + if (p.isCancel(hasCredentials)) { p.cancel('Setup cancelled'); process.exit(0); } + + if (!hasCredentials) { + p.log.info('Run `gog auth credentials /path/to/client_secret.json` after downloading credentials.'); + config.google.enabled = false; + return; + } + } + } + + // List existing accounts or add new one + let accounts: string[] = []; + if (hasCredentials) { + const listResult = spawnSync('gog', ['auth', 'list', '--json'], { stdio: 'pipe' }); + if (listResult.status === 0) { + try { + const parsed = JSON.parse(listResult.stdout.toString()); + if (Array.isArray(parsed)) { + accounts = parsed.map((a: { email?: string; account?: string }) => a.email || a.account || '').filter(Boolean); + } + } catch { + // Parse as text output + accounts = listResult.stdout.toString() + .split('\n') + .map(line => line.trim()) + .filter(line => line.includes('@')); + } + } + } + + let selectedAccount: string | undefined; + + if (accounts.length > 0) { + const accountChoice = await p.select({ + message: 'Google account', + 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], + }); + if (p.isCancel(accountChoice)) { p.cancel('Setup cancelled'); process.exit(0); } + + if (accountChoice === '__new__') { + selectedAccount = await addGoogleAccount(); + } else { + selectedAccount = accountChoice as string; + } + } else { + selectedAccount = await addGoogleAccount(); + } + + if (!selectedAccount) { + config.google.enabled = false; + 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); } + + config.google.enabled = true; + config.google.account = selectedAccount; + config.google.services = selectedServices as string[]; + + p.log.success(`Google Workspace configured: ${selectedAccount}`); +} + +async function addGoogleAccount(): Promise { + const email = await p.text({ + message: 'Google account email', + placeholder: 'you@gmail.com', + }); + if (p.isCancel(email) || !email) return undefined; + + const services = await p.multiselect({ + message: 'Services to authorize', + options: GOG_SERVICES.map(s => ({ + value: s, + label: s.charAt(0).toUpperCase() + s.slice(1), + })), + initialValues: ['gmail', 'calendar', 'drive', 'contacts'], + required: true, + }); + if (p.isCancel(services)) return undefined; + + p.note( + 'A browser window will open for Google authorization.\n' + + 'Sign in with your Google account and grant permissions.', + 'Authorization' + ); + + const spinner = p.spinner(); + spinner.start('Authorizing...'); + + // Run gog auth add (this will open browser) + const result = spawnSync('gog', [ + 'auth', 'add', email, + '--services', (services as string[]).join(','), + ], { + stdio: 'inherit', // Let it interact with terminal for browser auth + timeout: 300000, // 5 min timeout + }); + + if (result.status === 0) { + spinner.stop('Account authorized'); + return email; + } else { + spinner.stop('Authorization failed'); + p.log.error('Failed to authorize account. Try manually: gog auth add ' + email); + return undefined; + } +} + // ============================================================================ // Summary & Review // ============================================================================ @@ -899,6 +1125,11 @@ function showSummary(config: OnboardConfig): void { if (config.cron) features.push('Cron'); lines.push(`Features: ${features.length > 0 ? features.join(', ') : 'None'}`); + // Google + if (config.google.enabled) { + lines.push(`Google: ${config.google.account} (${config.google.services?.join(', ') || 'all'})`); + } + p.note(lines.join('\n'), 'Configuration'); } @@ -916,6 +1147,7 @@ async function reviewLoop(config: OnboardConfig, env: Record): P { value: 'agent', label: 'Change agent', hint: '' }, { value: 'channels', label: 'Change channels', hint: '' }, { value: 'features', label: 'Change features', hint: '' }, + { value: 'google', label: 'Change Google Workspace', hint: '' }, ], }); if (p.isCancel(choice)) { p.cancel('Setup cancelled'); process.exit(0); } @@ -933,6 +1165,7 @@ async function reviewLoop(config: OnboardConfig, env: Record): P } else if (choice === 'channels') await stepChannels(config, env); else if (choice === 'features') await stepFeatures(config); + else if (choice === 'google') await stepGoogle(config); } } @@ -1017,7 +1250,11 @@ export async function onboard(): Promise { selfChat: existingConfig.channels.signal?.selfChat, dmPolicy: existingConfig.channels.signal?.dmPolicy, }, - gmail: { enabled: false }, + google: { + enabled: existingConfig.integrations?.google?.enabled || false, + account: existingConfig.integrations?.google?.account, + services: existingConfig.integrations?.google?.services, + }, heartbeat: { enabled: existingConfig.features?.heartbeat?.enabled || false, interval: existingConfig.features?.heartbeat?.intervalMin?.toString(), @@ -1049,6 +1286,7 @@ export async function onboard(): Promise { await stepModel(config, env); await stepChannels(config, env); await stepFeatures(config); + await stepGoogle(config); // Review loop await reviewLoop(config, env); @@ -1166,6 +1404,9 @@ export async function onboard(): Promise { config.whatsapp.enabled ? ` ✓ WhatsApp (${formatAccess(config.whatsapp.dmPolicy, config.whatsapp.allowedUsers)})` : ' ✗ WhatsApp', config.signal.enabled ? ` ✓ Signal (${formatAccess(config.signal.dmPolicy, config.signal.allowedUsers)})` : ' ✗ Signal', '', + 'Integrations:', + config.google.enabled ? ` ✓ Google (${config.google.account} - ${config.google.services?.join(', ') || 'all'})` : ' ✗ Google Workspace', + '', 'Features:', config.heartbeat.enabled ? ` ✓ Heartbeat (${config.heartbeat.interval}min)` : ' ✗ Heartbeat', config.cron ? ' ✓ Cron jobs' : ' ✗ Cron jobs', @@ -1235,6 +1476,15 @@ export async function onboard(): Promise { intervalMin: config.heartbeat.interval ? parseInt(config.heartbeat.interval) : undefined, }, }, + ...(config.google.enabled ? { + integrations: { + google: { + enabled: true, + account: config.google.account, + services: config.google.services, + }, + }, + } : {}), }; // Add BYOK providers if configured