feat(setup): add contextual help text to all wizard flows

Each setup section now explains what's needed before prompting:
- Providers: links to API key consoles (Anthropic, OpenAI, Gemini, etc.)
- Channels: step-by-step bot creation (Telegram @BotFather, Discord dev
  portal, Slack app setup, WhatsApp QR)
- Gmail: Google Cloud Console OAuth setup walkthrough
- Memory: explains what vector search does and key reuse
- Security: describes each option (sandbox, pairing, tool profiles)
- Gateway: explains auth token, Tailscale Serve, lock mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-10 10:08:44 -08:00
parent f9446a4d67
commit f4b9c850ab
6 changed files with 60 additions and 2 deletions
+11 -2
View File
@@ -5,18 +5,27 @@ export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Prom
const cron = await p.confirm('Enable cron scheduler?', false);
if (cron) {
builder.setCronEnabled();
p.println('✓ Cron enabled — add jobs to config.yaml later');
p.println('✓ Cron enabled — add jobs to config.yaml under automation.cron.jobs[]');
}
const webhooks = await p.confirm('Enable webhook receiver?', false);
if (webhooks) {
p.println(' Webhooks accept HTTP POST at /webhooks/:name on the gateway port.');
p.println(' Set a shared secret to verify requests via HMAC signature.');
const secret = await p.ask('Webhook shared secret (optional)', '');
builder.setWebhooksEnabled(secret || undefined);
p.println('✓ Webhooks enabled');
p.println('✓ Webhooks enabled — define triggers in config.yaml under automation.webhooks[]');
}
const gmail = await p.confirm('Enable Gmail watcher?', false);
if (gmail) {
p.println(' To set up Gmail access:');
p.println(' 1. Go to https://console.cloud.google.com → create or select a project');
p.println(' 2. Enable the Gmail API and Cloud Pub/Sub API');
p.println(' 3. Go to APIs & Services → Credentials → Create Credentials → OAuth client ID');
p.println(' 4. Choose "Desktop app", download the JSON file');
p.println(' 5. Save it as ~/.config/flynn/gmail-credentials.json');
p.println(' On first run, Flynn will open a browser for OAuth consent and save the token.');
const creds = await p.ask('OAuth credentials file', '~/.config/flynn/gmail-credentials.json');
builder.setGmailEnabled(creds, 'webchat', 'gmail');
p.println('✓ Gmail watcher enabled');
+13
View File
@@ -2,6 +2,9 @@ import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
async function setupTelegram(p: Prompter, builder: ConfigBuilder): Promise<void> {
p.println(' 1. Message @BotFather on Telegram and use /newbot to create a bot');
p.println(' 2. Copy the bot token it gives you');
p.println(' 3. To find your chat ID, message @userinfobot or @RawDataBot');
const botToken = await p.password('Bot token (from @BotFather)');
const chatIdsRaw = await p.ask('Allowed chat IDs (comma-separated)');
const chatIds = chatIdsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
@@ -14,6 +17,11 @@ async function setupTelegram(p: Prompter, builder: ConfigBuilder): Promise<void>
}
async function setupDiscord(p: Prompter, builder: ConfigBuilder): Promise<void> {
p.println(' 1. Go to https://discord.com/developers/applications');
p.println(' 2. Create an application → Bot → copy the bot token');
p.println(' 3. Enable MESSAGE CONTENT intent under Bot settings');
p.println(' 4. Invite bot to your server with OAuth2 URL Generator (bot scope + Send Messages)');
p.println(' 5. Guild ID: right-click your server → Copy Server ID (enable Developer Mode in settings)');
const botToken = await p.password('Bot token');
const guildIdsRaw = await p.ask('Allowed guild IDs (comma-separated, or * for all)');
const guildIds = guildIdsRaw === '*' ? [] : guildIdsRaw.split(',').map(s => s.trim()).filter(Boolean);
@@ -22,6 +30,11 @@ async function setupDiscord(p: Prompter, builder: ConfigBuilder): Promise<void>
}
async function setupSlack(p: Prompter, builder: ConfigBuilder): Promise<void> {
p.println(' 1. Go to https://api.slack.com/apps and create a new app');
p.println(' 2. Enable Socket Mode → generate an App Token (xapp-...)');
p.println(' 3. Under OAuth & Permissions, install to workspace → copy Bot Token (xoxb-...)');
p.println(' 4. Under Basic Information → copy Signing Secret');
p.println(' 5. Channel IDs: right-click a channel → View channel details → copy the ID at bottom');
const botToken = await p.password('Bot token (xoxb-...)');
const appToken = await p.password('App token (xapp-...)');
const signingSecret = await p.password('Signing secret');
+7
View File
@@ -2,9 +2,12 @@ import type { Prompter } from './prompts.js';
import type { ConfigBuilder } from './config.js';
export async function setupGateway(p: Prompter, builder: ConfigBuilder): Promise<void> {
p.println(' The gateway serves the web dashboard and WebSocket API.');
const port = await p.ask('Gateway port', '18800');
builder.setGatewayPort(parseInt(port, 10) || 18800);
p.println();
p.println(' An auth token protects gateway access (required for remote/Tailscale use).');
const wantAuth = await p.confirm('Set auth token?', false);
if (wantAuth) {
const token = await p.password('Auth token');
@@ -12,12 +15,16 @@ export async function setupGateway(p: Prompter, builder: ConfigBuilder): Promise
p.println('✓ Gateway auth token set');
}
p.println();
p.println(' Tailscale Serve exposes the gateway on your tailnet (requires tailscale CLI).');
const tailscale = await p.confirm('Enable Tailscale Serve?', false);
if (tailscale) {
builder.setTailscaleServe(true);
p.println('✓ Tailscale Serve enabled');
}
p.println();
p.println(' Gateway lock limits connections to one client at a time.');
const lock = await p.confirm('Enable gateway lock (single client)?', false);
if (lock) {
builder.setGatewayLock(true);
+4
View File
@@ -11,9 +11,13 @@ const EMBEDDING_PROVIDERS = [
const REUSABLE_PROVIDERS = ['openai', 'gemini'];
export async function setupMemory(p: Prompter, builder: ConfigBuilder): Promise<void> {
p.println(' Vector search enables semantic memory — Flynn remembers and retrieves');
p.println(' information based on meaning, not just keywords.');
const enable = await p.confirm('Enable vector search for semantic memory?', false);
if (!enable) return;
p.println(' Pick a provider to generate embeddings (vector representations of text).');
p.println(' If you already configured OpenAI or Gemini as a model, you can reuse that key.');
const provider = await p.choose('Embedding provider:', EMBEDDING_PROVIDERS);
const config = builder.build();
+14
View File
@@ -26,9 +26,23 @@ const SECOND_TIER: ProviderDef[] = [
{ name: 'GitHub Models', provider: 'github', defaultModel: 'claude-sonnet-4-20250514', needsApiKey: false, needsEndpoint: false },
];
const PROVIDER_HELP: Record<string, string> = {
anthropic: 'Get your API key at https://console.anthropic.com/settings/keys',
openai: 'Get your API key at https://platform.openai.com/api-keys',
ollama: 'Ollama runs locally — install from https://ollama.com and run: ollama serve',
gemini: 'Get your API key at https://aistudio.google.com/apikey',
openrouter: 'Get your API key at https://openrouter.ai/keys (supports 200+ models)',
xai: 'Get your API key at https://console.x.ai',
bedrock: 'Uses AWS credentials from environment (~/.aws/credentials or IAM role)',
github: 'Uses GitHub Copilot — authenticate via OAuth device flow on first use',
};
async function configureProvider(p: Prompter, def: ProviderDef): Promise<{
provider: string; model: string; api_key?: string; endpoint?: string;
}> {
const help = PROVIDER_HELP[def.provider];
if (help) p.println(` ${help}`);
const config: Record<string, string> = { provider: def.provider };
if (def.needsApiKey) config.api_key = await p.password(def.apiKeyLabel ?? 'API key');
if (def.needsEndpoint) config.endpoint = await p.ask('Host', def.defaultEndpoint);
+11
View File
@@ -9,18 +9,29 @@ const TOOL_PROFILES = [
];
export async function setupSecurity(p: Prompter, builder: ConfigBuilder): Promise<void> {
p.println(' Docker sandboxing runs tool commands in isolated containers.');
p.println(' Requires Docker installed and running.');
const sandbox = await p.confirm('Enable Docker sandboxing?', false);
if (sandbox) {
builder.setSandboxEnabled(true);
p.println('✓ Docker sandboxing enabled');
}
p.println();
p.println(' DM pairing requires unknown senders to enter a code before chatting.');
p.println(' Generate codes via the gateway or TUI /pair command.');
const pairing = await p.confirm('Enable DM pairing for unknown senders?', false);
if (pairing) {
builder.setPairingEnabled(true);
p.println('✓ DM pairing enabled');
}
p.println();
p.println(' Tool profiles control which tools the agent can use:');
p.println(' full — all tools available (file, shell, web, memory, messaging)');
p.println(' coding — file system + shell + sessions + memory (no messaging/web)');
p.println(' messaging — send messages only (no file/shell access)');
p.println(' minimal — status checks only (read-only, safest)');
const profile = await p.choose('Tool policy profile:', TOOL_PROFILES);
builder.setToolProfile(profile);
}