diff --git a/src/cli/setup/automation.ts b/src/cli/setup/automation.ts index 3c58db4..80293c5 100644 --- a/src/cli/setup/automation.ts +++ b/src/cli/setup/automation.ts @@ -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'); diff --git a/src/cli/setup/channels.ts b/src/cli/setup/channels.ts index 58d79c5..f2d3005 100644 --- a/src/cli/setup/channels.ts +++ b/src/cli/setup/channels.ts @@ -2,6 +2,9 @@ import type { Prompter } from './prompts.js'; import type { ConfigBuilder } from './config.js'; async function setupTelegram(p: Prompter, builder: ConfigBuilder): Promise { + 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 } async function setupDiscord(p: Prompter, builder: ConfigBuilder): Promise { + 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 } async function setupSlack(p: Prompter, builder: ConfigBuilder): Promise { + 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'); diff --git a/src/cli/setup/gateway.ts b/src/cli/setup/gateway.ts index 7a99184..8c670f5 100644 --- a/src/cli/setup/gateway.ts +++ b/src/cli/setup/gateway.ts @@ -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 { + 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); diff --git a/src/cli/setup/memory.ts b/src/cli/setup/memory.ts index 3e5860c..83c577b 100644 --- a/src/cli/setup/memory.ts +++ b/src/cli/setup/memory.ts @@ -11,9 +11,13 @@ const EMBEDDING_PROVIDERS = [ const REUSABLE_PROVIDERS = ['openai', 'gemini']; export async function setupMemory(p: Prompter, builder: ConfigBuilder): Promise { + 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(); diff --git a/src/cli/setup/providers.ts b/src/cli/setup/providers.ts index 5fc0801..75441fd 100644 --- a/src/cli/setup/providers.ts +++ b/src/cli/setup/providers.ts @@ -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 = { + 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 = { 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); diff --git a/src/cli/setup/security.ts b/src/cli/setup/security.ts index 5cb9623..3db82cf 100644 --- a/src/cli/setup/security.ts +++ b/src/cli/setup/security.ts @@ -9,18 +9,29 @@ const TOOL_PROFILES = [ ]; export async function setupSecurity(p: Prompter, builder: ConfigBuilder): Promise { + 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); }