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:
@@ -5,18 +5,27 @@ export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Prom
|
|||||||
const cron = await p.confirm('Enable cron scheduler?', false);
|
const cron = await p.confirm('Enable cron scheduler?', false);
|
||||||
if (cron) {
|
if (cron) {
|
||||||
builder.setCronEnabled();
|
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);
|
const webhooks = await p.confirm('Enable webhook receiver?', false);
|
||||||
if (webhooks) {
|
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)', '');
|
const secret = await p.ask('Webhook shared secret (optional)', '');
|
||||||
builder.setWebhooksEnabled(secret || undefined);
|
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);
|
const gmail = await p.confirm('Enable Gmail watcher?', false);
|
||||||
if (gmail) {
|
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');
|
const creds = await p.ask('OAuth credentials file', '~/.config/flynn/gmail-credentials.json');
|
||||||
builder.setGmailEnabled(creds, 'webchat', 'gmail');
|
builder.setGmailEnabled(creds, 'webchat', 'gmail');
|
||||||
p.println('✓ Gmail watcher enabled');
|
p.println('✓ Gmail watcher enabled');
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type { Prompter } from './prompts.js';
|
|||||||
import type { ConfigBuilder } from './config.js';
|
import type { ConfigBuilder } from './config.js';
|
||||||
|
|
||||||
async function setupTelegram(p: Prompter, builder: ConfigBuilder): Promise<void> {
|
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 botToken = await p.password('Bot token (from @BotFather)');
|
||||||
const chatIdsRaw = await p.ask('Allowed chat IDs (comma-separated)');
|
const chatIdsRaw = await p.ask('Allowed chat IDs (comma-separated)');
|
||||||
const chatIds = chatIdsRaw.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n));
|
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> {
|
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 botToken = await p.password('Bot token');
|
||||||
const guildIdsRaw = await p.ask('Allowed guild IDs (comma-separated, or * for all)');
|
const guildIdsRaw = await p.ask('Allowed guild IDs (comma-separated, or * for all)');
|
||||||
const guildIds = guildIdsRaw === '*' ? [] : guildIdsRaw.split(',').map(s => s.trim()).filter(Boolean);
|
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> {
|
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 botToken = await p.password('Bot token (xoxb-...)');
|
||||||
const appToken = await p.password('App token (xapp-...)');
|
const appToken = await p.password('App token (xapp-...)');
|
||||||
const signingSecret = await p.password('Signing secret');
|
const signingSecret = await p.password('Signing secret');
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import type { Prompter } from './prompts.js';
|
|||||||
import type { ConfigBuilder } from './config.js';
|
import type { ConfigBuilder } from './config.js';
|
||||||
|
|
||||||
export async function setupGateway(p: Prompter, builder: ConfigBuilder): Promise<void> {
|
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');
|
const port = await p.ask('Gateway port', '18800');
|
||||||
builder.setGatewayPort(parseInt(port, 10) || 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);
|
const wantAuth = await p.confirm('Set auth token?', false);
|
||||||
if (wantAuth) {
|
if (wantAuth) {
|
||||||
const token = await p.password('Auth token');
|
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('✓ 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);
|
const tailscale = await p.confirm('Enable Tailscale Serve?', false);
|
||||||
if (tailscale) {
|
if (tailscale) {
|
||||||
builder.setTailscaleServe(true);
|
builder.setTailscaleServe(true);
|
||||||
p.println('✓ Tailscale Serve enabled');
|
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);
|
const lock = await p.confirm('Enable gateway lock (single client)?', false);
|
||||||
if (lock) {
|
if (lock) {
|
||||||
builder.setGatewayLock(true);
|
builder.setGatewayLock(true);
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ const EMBEDDING_PROVIDERS = [
|
|||||||
const REUSABLE_PROVIDERS = ['openai', 'gemini'];
|
const REUSABLE_PROVIDERS = ['openai', 'gemini'];
|
||||||
|
|
||||||
export async function setupMemory(p: Prompter, builder: ConfigBuilder): Promise<void> {
|
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);
|
const enable = await p.confirm('Enable vector search for semantic memory?', false);
|
||||||
if (!enable) return;
|
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 provider = await p.choose('Embedding provider:', EMBEDDING_PROVIDERS);
|
||||||
|
|
||||||
const config = builder.build();
|
const config = builder.build();
|
||||||
|
|||||||
@@ -26,9 +26,23 @@ const SECOND_TIER: ProviderDef[] = [
|
|||||||
{ name: 'GitHub Models', provider: 'github', defaultModel: 'claude-sonnet-4-20250514', needsApiKey: false, needsEndpoint: false },
|
{ 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<{
|
async function configureProvider(p: Prompter, def: ProviderDef): Promise<{
|
||||||
provider: string; model: string; api_key?: string; endpoint?: string;
|
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 };
|
const config: Record<string, string> = { provider: def.provider };
|
||||||
if (def.needsApiKey) config.api_key = await p.password(def.apiKeyLabel ?? 'API key');
|
if (def.needsApiKey) config.api_key = await p.password(def.apiKeyLabel ?? 'API key');
|
||||||
if (def.needsEndpoint) config.endpoint = await p.ask('Host', def.defaultEndpoint);
|
if (def.needsEndpoint) config.endpoint = await p.ask('Host', def.defaultEndpoint);
|
||||||
|
|||||||
@@ -9,18 +9,29 @@ const TOOL_PROFILES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export async function setupSecurity(p: Prompter, builder: ConfigBuilder): Promise<void> {
|
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);
|
const sandbox = await p.confirm('Enable Docker sandboxing?', false);
|
||||||
if (sandbox) {
|
if (sandbox) {
|
||||||
builder.setSandboxEnabled(true);
|
builder.setSandboxEnabled(true);
|
||||||
p.println('✓ Docker sandboxing enabled');
|
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);
|
const pairing = await p.confirm('Enable DM pairing for unknown senders?', false);
|
||||||
if (pairing) {
|
if (pairing) {
|
||||||
builder.setPairingEnabled(true);
|
builder.setPairingEnabled(true);
|
||||||
p.println('✓ DM pairing enabled');
|
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);
|
const profile = await p.choose('Tool policy profile:', TOOL_PROFILES);
|
||||||
builder.setToolProfile(profile);
|
builder.setToolProfile(profile);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user