From 11609ea83ebeb5c260899040c08820df2bae062f Mon Sep 17 00:00:00 2001 From: Caren Thomas Date: Thu, 29 Jan 2026 17:44:14 -0800 Subject: [PATCH 1/2] feat: add Slack setup wizard with manifest-based creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3-step wizard using Slack manifest URL: - Step 1: Create app from manifest (scopes/events/Socket Mode pre-configured) - Step 2: Install to workspace + copy Bot Token - Step 3: Get App-Level Token from Socket Mode - Token validation with success summary - Manual entry path also gets validation - Shared validators between flows - Total time: ~2 minutes 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta --- src/onboard.ts | 103 +++++++--- src/setup/slack-wizard.ts | 411 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 487 insertions(+), 27 deletions(-) create mode 100644 src/setup/slack-wizard.ts diff --git a/src/onboard.ts b/src/onboard.ts index 0b1f115..a08b405 100644 --- a/src/onboard.ts +++ b/src/onboard.ts @@ -564,40 +564,89 @@ async function stepChannels(config: OnboardConfig, env: Record): } if (config.slack.enabled) { + const hasExistingTokens = config.slack.appToken || config.slack.botToken; + + // Show what's needed p.note( - 'See docs/slack-setup.md for full instructions.\n\n' + - 'Quick reference at api.slack.com/apps:\n' + - '• Enable Socket Mode first\n' + - '• App Token: Basic Information → App-Level Tokens\n' + - '• Bot Token: OAuth & Permissions → Bot User OAuth Token', - 'Slack Setup' + 'Requires two tokens from api.slack.com/apps:\n' + + ' • App Token (xapp-...) - Socket Mode\n' + + ' • Bot Token (xoxb-...) - Bot permissions', + 'Slack Requirements' ); - const appToken = await p.text({ - message: 'Slack App Token (xapp-...)', - initialValue: config.slack.appToken || '', + const wizardChoice = await p.select({ + message: 'Slack setup', + options: [ + { value: 'wizard', label: 'Guided setup', hint: 'Step-by-step instructions with validation' }, + { value: 'manual', label: 'Manual entry', hint: 'I already have tokens' }, + ], + initialValue: hasExistingTokens ? 'manual' : 'wizard', }); - if (!p.isCancel(appToken) && appToken) config.slack.appToken = appToken; - const botToken = await p.text({ - message: 'Slack Bot Token (xoxb-...)', - initialValue: config.slack.botToken || '', - }); - if (!p.isCancel(botToken) && botToken) config.slack.botToken = botToken; + if (p.isCancel(wizardChoice)) { + p.cancel('Setup cancelled'); + process.exit(0); + } - // Slack access control (workspace already provides some isolation) - const restrictSlack = await p.confirm({ - message: 'Slack: Restrict to specific users? (workspace already limits access)', - initialValue: (config.slack.allowedUsers?.length || 0) > 0, - }); - if (!p.isCancel(restrictSlack) && restrictSlack) { - const users = await p.text({ - message: 'Allowed Slack user IDs (comma-separated)', - placeholder: 'U01234567,U98765432', - initialValue: config.slack.allowedUsers?.join(',') || '', + if (wizardChoice === 'wizard') { + const { runSlackWizard } = await import('./setup/slack-wizard.js'); + const result = await runSlackWizard({ + appToken: config.slack.appToken, + botToken: config.slack.botToken, + allowedUsers: config.slack.allowedUsers, }); - if (!p.isCancel(users) && users) { - config.slack.allowedUsers = users.split(',').map(s => s.trim()).filter(Boolean); + + if (result) { + config.slack.appToken = result.appToken; + config.slack.botToken = result.botToken; + config.slack.allowedUsers = result.allowedUsers; + } else { + // Wizard was cancelled, disable Slack + config.slack.enabled = false; + } + } else { + // Manual token entry with validation + const { validateSlackTokens, stepAccessControl, validateAppToken, validateBotToken } = await import('./setup/slack-wizard.js'); + + p.note( + 'Get tokens from api.slack.com/apps:\n' + + '• Enable Socket Mode → App-Level Token (xapp-...)\n' + + '• Install App → Bot User OAuth Token (xoxb-...)\n\n' + + 'See docs/slack-setup.md for detailed instructions', + 'Slack Setup' + ); + + const appToken = await p.text({ + message: 'Slack App Token (xapp-...)', + initialValue: config.slack.appToken || '', + validate: validateAppToken, + }); + if (p.isCancel(appToken)) { + config.slack.enabled = false; + } else { + config.slack.appToken = appToken; + } + + const botToken = await p.text({ + message: 'Slack Bot Token (xoxb-...)', + initialValue: config.slack.botToken || '', + validate: validateBotToken, + }); + if (p.isCancel(botToken)) { + config.slack.enabled = false; + } else { + config.slack.botToken = botToken; + } + + // Validate tokens if both provided + if (config.slack.appToken && config.slack.botToken) { + await validateSlackTokens(config.slack.appToken, config.slack.botToken); + } + + // Slack access control (reuse wizard step) + const allowedUsers = await stepAccessControl(config.slack.allowedUsers); + if (allowedUsers !== undefined) { + config.slack.allowedUsers = allowedUsers; } } } diff --git a/src/setup/slack-wizard.ts b/src/setup/slack-wizard.ts new file mode 100644 index 0000000..0001ef4 --- /dev/null +++ b/src/setup/slack-wizard.ts @@ -0,0 +1,411 @@ +/** + * Interactive Slack Setup Wizard + * + * Guides users through the full Slack app configuration process + * with browser automation and validation at each step. + */ + +import * as p from '@clack/prompts'; + +interface SlackWizardResult { + appToken: string; + botToken: string; + allowedUsers?: string[]; +} + +// Shared validators (exported for use in onboard.ts manual flow) +export function validateAppToken(val: string): string | undefined { + if (!val) return 'App Token is required'; + if (!val.startsWith('xapp-')) return 'App Token should start with "xapp-"'; + if (val.length < 20) return 'Token appears too short'; +} + +export function validateBotToken(val: string): string | undefined { + if (!val) return 'Bot Token is required'; + if (!val.startsWith('xoxb-')) return 'Bot Token should start with "xoxb-"'; + if (val.length < 20) return 'Token appears too short'; +} + +function validateSlackUserId(val: string): string | undefined { + if (!val) return undefined; // Optional + const ids = val.split(',').map(s => s.trim()); + for (const id of ids) { + if (!/^U[A-Z0-9]{8,}$/i.test(id)) { + return `Invalid Slack user ID: ${id} (should start with U)`; + } + } +} + +/** + * Run the interactive Slack setup wizard + */ +export async function runSlackWizard(existingConfig?: { + appToken?: string; + botToken?: string; + allowedUsers?: string[]; +}): Promise { + p.intro('🔧 Slack Setup Wizard'); + + p.note( + 'This wizard creates a Slack app using a pre-configured manifest.\n' + + 'All permissions and settings are configured automatically!\n\n' + + 'Total time: ~2 minutes\n' + + 'Press Ctrl+C to cancel anytime', + 'Overview' + ); + + // Step 1: Create Slack App from Manifest (scopes, events, Socket Mode all pre-configured) + const createdApp = await stepCreateApp(); + if (!createdApp) return null; + + // Step 2: Install to Workspace + Get Bot Token + const botToken = await stepInstallApp(existingConfig?.botToken); + if (!botToken) return null; + + // Step 3: Enable Socket Mode + Get App Token + const appToken = await stepEnableSocketMode(existingConfig?.appToken); + if (!appToken) return null; + + // Validate tokens + await validateSlackTokens(appToken, botToken); + + // Access control + const allowedUsers = await stepAccessControl(existingConfig?.allowedUsers); + + p.outro('✅ Slack setup complete!'); + + return { + appToken, + botToken, + allowedUsers, + }; +} + +async function stepCreateApp(): Promise { + p.log.step('Step 1/3: Create Slack App from Manifest'); + + // Inline manifest for Socket Mode configuration + const manifest = `display_information: + name: LettaBot + description: AI assistant with Socket Mode for real-time conversations + background_color: "#2c2d30" +features: + bot_user: + display_name: LettaBot + always_online: false +oauth_config: + scopes: + bot: + - app_mentions:read + - chat:write + - im:history + - im:read + - im:write +settings: + org_deploy_enabled: false + socket_mode_enabled: true + token_rotation_enabled: false + event_subscriptions: + bot_events: + - app_mention + - message.im`; + + const manifestEncoded = encodeURIComponent(manifest); + const url = `https://api.slack.com/apps?new_app=1&manifest_yaml=${manifestEncoded}`; + + p.note( + 'Creates app with everything pre-configured:\n' + + ' • Socket Mode enabled\n' + + ' • 5 bot scopes (app_mentions:read, chat:write, im:*)\n' + + ' • 2 event subscriptions (app_mention, message.im)\n\n' + + 'Just review and click "Create"!', + 'One-Click Setup' + ); + + const openBrowser = await p.confirm({ + message: 'Open pre-configured app creation page?', + initialValue: true, + }); + + if (p.isCancel(openBrowser)) { + p.cancel('Setup cancelled'); + return false; + } + + if (openBrowser) { + try { + const open = (await import('open')).default; + await open(url, { wait: false }); + p.log.success('Browser opened'); + } catch { + p.log.warn('Could not open browser'); + p.log.info('Copy this URL:'); + console.log(url); + } + } else { + p.log.info('Copy this URL:'); + console.log(url); + } + + const completed = await p.confirm({ + message: 'Clicked "Create"?', + initialValue: true, + }); + + if (p.isCancel(completed) || !completed) { + p.cancel('Slack setup skipped'); + return false; + } + + return true; +} + +async function stepEnableSocketMode(existingToken?: string): Promise { + p.log.step('Step 3/3: Get App-Level Token'); + + p.note( + '1. In the left sidebar, click "Socket Mode"\n' + + '2. Click "Generate Token"\n' + + ' • Token Name: "socket-token"\n' + + ' • Scopes: connections:write (pre-selected)\n' + + '3. Copy the token (xapp-...)', + 'Instructions' + ); + + const appToken = await p.text({ + message: 'Slack App Token (xapp-...)', + placeholder: 'xapp-1-A0ABKA5451U-...', + initialValue: existingToken || '', + validate: validateAppToken, + }); + + if (p.isCancel(appToken)) { + p.cancel('Setup cancelled'); + return null; + } + + return appToken; +} + +async function stepConfigureScopes(): Promise { + p.log.step('Step 3/6: Configure Bot Permissions'); + + p.note( + '1. In the left sidebar, go to "OAuth & Permissions"\n' + + '2. Scroll to "Scopes" → "Bot Token Scopes"\n' + + '3. Click "Add an OAuth Scope" for each:\n' + + ' • app_mentions:read\n' + + ' • chat:write\n' + + ' • im:history\n' + + ' • im:read\n' + + ' • im:write', + 'Instructions' + ); + + const completed = await p.confirm({ + message: 'Enabled permissions?', + initialValue: true, + }); + + if (p.isCancel(completed) || !completed) { + p.cancel('Slack setup skipped'); + return false; + } + + return true; +} + +async function stepConfigureEvents(): Promise { + p.log.step('Step 4/6: Enable Event Subscriptions'); + + p.note( + '1. In the left sidebar, go to "Event Subscriptions"\n' + + '2. Toggle "Enable Events" → ON\n' + + '3. Scroll to "Subscribe to bot events"\n' + + '4. Click "Add Bot User Event" for each:\n' + + ' • app_mention\n' + + ' • message.im\n' + + '5. Click "Save Changes" at the bottom', + 'Instructions' + ); + + const completed = await p.confirm({ + message: 'Enabled subscriptions?', + initialValue: true, + }); + + if (p.isCancel(completed) || !completed) { + p.cancel('Slack setup skipped'); + return false; + } + + return true; +} + +async function stepConfigureAppHome(): Promise { + p.log.step('Step 5/6: Configure App Home'); + + p.note( + '1. Go to "App Home" in left sidebar\n' + + '2. Under "Show Tabs", toggle "Messages Tab" → ON\n' + + '3. Check "Allow users to send messages from the messages tab"', + 'Instructions' + ); + + const completed = await p.confirm({ + message: 'Enabled messaging?', + initialValue: true, + }); + + if (p.isCancel(completed) || !completed) { + p.log.warn('Skipping - DMs may not work without Messages Tab enabled'); + // Continue anyway, just warn + } + + return true; +} + +async function stepInstallApp(existingToken?: string): Promise { + p.log.step('Step 6/6: Install to Workspace'); + + p.note( + '1. Go to "Install App" in left sidebar\n' + + '2. Click "Install to Workspace"\n' + + '3. Click "Allow"\n' + + '4. Copy "Bot User OAuth Token" (xoxb-...)', + 'Instructions' + ); + + const botToken = await p.text({ + message: 'Slack Bot Token (xoxb-...)', + placeholder: 'xoxb-7365707142320-...', + initialValue: existingToken || '', + validate: validateBotToken, + }); + + if (p.isCancel(botToken)) { + p.cancel('Setup cancelled'); + return null; + } + + return botToken; +} + +/** + * Validate Slack tokens via API + * Exported for use in both wizard and manual flows + */ +export async function validateSlackTokens(appToken: string, botToken: string): Promise { + p.log.step('Validating Configuration'); + + const spinner = p.spinner(); + spinner.start('Testing App Token...'); + + let appTokenValid = false; + let botTokenValid = false; + let botUsername = ''; + let workspaceName = ''; + + // Test App Token with auth.test + try { + const response = await fetch('https://slack.com/api/auth.test', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${appToken}`, + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json() as { ok: boolean; error?: string }; + + if (data.ok) { + spinner.stop('✓ App Token valid'); + appTokenValid = true; + } else { + spinner.stop('✗ App Token validation failed'); + p.log.error(`Error: ${data.error || 'Unknown error'}`); + p.log.warn('The token might not work. Double-check it in your Slack app settings.'); + } + } catch (e) { + spinner.stop('Could not validate App Token (network error)'); + p.log.warn('Skipping validation - will test when server starts'); + } + + spinner.start('Testing Bot Token...'); + + // Test Bot Token with auth.test + try { + const response = await fetch('https://slack.com/api/auth.test', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${botToken}`, + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json() as { ok: boolean; error?: string; user?: string; team?: string }; + + if (data.ok) { + spinner.stop('✓ Bot Token valid'); + botTokenValid = true; + botUsername = data.user || ''; + workspaceName = data.team || ''; + } else { + spinner.stop('✗ Bot Token validation failed'); + p.log.error(`Error: ${data.error || 'Unknown error'}`); + p.log.warn('The token might not work. Double-check it in your Slack app settings.'); + } + } catch (e) { + spinner.stop('Could not validate Bot Token (network error)'); + p.log.warn('Skipping validation - will test when server starts'); + } + + // Show success summary if both tokens valid + if (appTokenValid && botTokenValid) { + p.note( + `Bot: @${botUsername}\n` + + `Workspace: ${workspaceName}\n` + + `Socket Mode: Enabled\n\n` + + `Your Slack bot is ready to receive messages!`, + '✓ Validation Successful' + ); + } +} + +/** + * Access control step - shared by wizard and manual flows + * Exported for reuse in onboard.ts + */ +export async function stepAccessControl(existingUsers?: string[]): Promise { + const restrictSlack = await p.confirm({ + message: 'Restrict to specific Slack users?', + initialValue: (existingUsers?.length || 0) > 0, + }); + + if (p.isCancel(restrictSlack)) return undefined; + + if (restrictSlack) { + p.note( + 'To find user IDs:\n' + + '1. Click on a user\'s profile in Slack\n' + + '2. Click ⋮ menu → "Copy member ID"\n' + + '3. IDs look like U01ABCD2EFG', + 'Finding User IDs' + ); + + const users = await p.text({ + message: 'Allowed Slack user IDs (comma-separated)', + placeholder: 'U01234567,U98765432', + initialValue: existingUsers?.join(',') || '', + validate: validateSlackUserId, + }); + + if (p.isCancel(users)) return undefined; + + if (users) { + return users.split(',').map(s => s.trim()).filter(Boolean); + } + } + + return undefined; +} From 36ee6d7bd5ab318b8c77741422b4827b71ea546c Mon Sep 17 00:00:00 2001 From: Caren Thomas Date: Thu, 29 Jan 2026 18:29:54 -0800 Subject: [PATCH 2/2] chore: update package-lock after merge --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index 2362fb1..9244e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "bin": { "lettabot": "dist/cli.js", "lettabot-message": "dist/cli/message.js", + "lettabot-react": "dist/cli/react.js", "lettabot-schedule": "dist/cron/cli.js" }, "optionalDependencies": {