From 1d66f42dad4f731d959a6fb7e66b8a1a59b865e4 Mon Sep 17 00:00:00 2001 From: cthomas Date: Fri, 30 Jan 2026 16:14:29 -0800 Subject: [PATCH] feat: add non-interactive onboarding and SKILL.md (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add non-interactive onboarding and SKILL.md Add agent-friendly setup flow: - lettabot onboard --non-interactive flag - Reads all config from environment variables - SKILL.md documents env-based setup for agents - Supports all channels (Telegram, Slack, Discord, WhatsApp, Signal) - No prompts - ideal for coding agents automating setup 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * fix: address non-interactive setup issues - Add SLACK_APP_NAME for customizable app name (defaults to LETTA_AGENT_NAME or LettaBot) - Clarify WhatsApp requires WHATSAPP_ENABLED and WHATSAPP_SELF_CHAT to be explicit - Document all 5 channels supported (Telegram, Slack, Discord, WhatsApp, Signal) - Fix WhatsApp selfChat default to be explicit false * docs: recommend non-interactive setup as primary method Update README per review feedback to show env-based setup first. This is simpler for most users and ideal for automation. * docs: rewrite setup to be AI-first per feedback Make recommended setup AI-focused: - Show prompt to paste into AI coding assistants - AI handles clone/install/config autonomously - Manual wizard becomes Option 2 for human users --------- Co-authored-by: Letta --- README.md | 24 +++- SKILL.md | 295 ++++++++++++++++++++++++++++++++++++++ src/cli.ts | 3 +- src/onboard.ts | 156 +++++++++++++++++++- src/setup/slack-wizard.ts | 5 +- 5 files changed, 478 insertions(+), 5 deletions(-) create mode 100644 SKILL.md diff --git a/README.md b/README.md index c44d278..9d03157 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,29 @@ See the [documentation](https://docs.letta.com/guides/docker/) for more details ### Setup -Run the interactive onboarding wizard: +**Option 1: AI-Assisted Setup (Recommended)** + +Paste this into Letta Code, Claude Code, Codex CLI, or any AI coding assistant: + +``` +Clone https://github.com/letta-ai/lettabot, read the SKILL.md +for setup instructions, and help me configure Telegram. +``` + +You'll need: +- A Letta API key from [app.letta.com](https://app.letta.com) (or a [Letta Docker server](https://docs.letta.com/guides/docker/)) +- A Telegram bot token from [@BotFather](https://t.me/BotFather) + +The AI will handle the rest: cloning, installing dependencies, reading setup docs, and configuring your bot. + +**Option 2: Interactive Wizard** + +For manual step-by-step setup: ```bash +git clone https://github.com/letta-ai/lettabot.git +cd lettabot +npm install && npm run build && npm link lettabot onboard ``` @@ -64,6 +84,8 @@ lettabot server That's it! Message your bot on Telegram. +> **Note:** For detailed environment variable reference and multi-channel setup, see [SKILL.md](./SKILL.md) + ## Skills LettaBot is compatible with [skills.sh](https://skills.sh) and [Clawdhub](https://clawdhub.com/). diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..2ba6b22 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,295 @@ +--- +name: lettabot +description: Set up and run LettaBot - a multi-channel AI assistant for Telegram, Slack, Discord, WhatsApp, and Signal. Supports both interactive wizard and non-interactive (agent-friendly) configuration. +--- + +# LettaBot Setup + +Multi-channel AI assistant with persistent memory across Telegram, Slack, Discord, WhatsApp, and Signal. + +## Quick Setup (Agent-Friendly) + +For non-interactive setup (ideal for coding agents): + +```bash +# 1. Clone and install +git clone https://github.com/letta-ai/lettabot.git +cd lettabot +npm install +npm run build +npm link + +# 2. Configure via environment variables +export LETTA_API_KEY="letta_..." # From app.letta.com +export LETTA_BASE_URL="https://api.letta.com" # Or self-hosted +export LETTA_AGENT_ID="agent-..." # Optional: use existing agent + +# 3. Configure channel (example: Telegram) +export TELEGRAM_BOT_TOKEN="123456:ABC-DEF..." # From @BotFather +export TELEGRAM_DM_POLICY="pairing" # Optional: pairing | allowlist | open + +# 4. Run non-interactive setup +lettabot onboard --non-interactive + +# 5. Start the bot +lettabot server +``` + +## Interactive Setup + +For human-friendly setup with wizard: + +```bash +lettabot onboard +``` + +The wizard will guide you through: +- Letta API authentication (OAuth or API key) +- Agent selection/creation +- Channel configuration (Telegram, Slack, Discord, WhatsApp, Signal) + +## Environment Variables + +### Required + +| Variable | Description | +|----------|-------------| +| `LETTA_API_KEY` | API key from app.letta.com (or skip for OAuth) | +| `LETTA_BASE_URL` | API endpoint (default: https://api.letta.com) | + +### Agent Selection + +| Variable | Description | +|----------|-------------| +| `LETTA_AGENT_ID` | Use existing agent (skip agent creation) | +| `LETTA_AGENT_NAME` | Name for new agent (default: "lettabot") | +| `LETTA_MODEL` | Model for new agent (default: "claude-sonnet-4") | + +### Telegram + +| Variable | Description | Required | +|----------|-------------|----------| +| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | ✅ | +| `TELEGRAM_DM_POLICY` | Access control: `pairing` \| `allowlist` \| `open` | ❌ (default: pairing) | +| `TELEGRAM_ALLOWED_USERS` | Comma-separated user IDs (if dmPolicy=allowlist) | ❌ | + +### Slack (Socket Mode) + +| Variable | Description | Required | +|----------|-------------|----------| +| `SLACK_BOT_TOKEN` | Bot User OAuth Token (xoxb-...) | ✅ | +| `SLACK_APP_TOKEN` | App-Level Token (xapp-...) for Socket Mode | ✅ | +| `SLACK_APP_NAME` | Custom app name (default: LETTA_AGENT_NAME or "LettaBot") | ❌ | +| `SLACK_DM_POLICY` | Access control: `pairing` \| `allowlist` \| `open` | ❌ (default: pairing) | +| `SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs (if dmPolicy=allowlist) | ❌ | + +**Setup Slack app:** See [Slack Setup Wizard](./src/setup/slack-wizard.ts) or run `lettabot onboard` for guided setup. + +### Discord + +| Variable | Description | Required | +|----------|-------------|----------| +| `DISCORD_BOT_TOKEN` | Bot token from discord.com/developers/applications | ✅ | +| `DISCORD_DM_POLICY` | Access control: `pairing` \| `allowlist` \| `open` | ❌ (default: pairing) | +| `DISCORD_ALLOWED_USERS` | Comma-separated Discord user IDs (if dmPolicy=allowlist) | ❌ | + +**Setup Discord bot:** See [docs/discord-setup.md](./docs/discord-setup.md) + +### WhatsApp + +| Variable | Description | Required | +|----------|-------------|----------| +| `WHATSAPP_ENABLED` | Enable WhatsApp: `true` \| `false` | ✅ Must be explicit | +| `WHATSAPP_SELF_CHAT` | Self-chat mode: `true` (personal number) \| `false` (dedicated bot number) | ✅ Must be explicit | +| `WHATSAPP_DM_POLICY` | Access control: `pairing` \| `allowlist` \| `open` | ❌ (default: pairing) | +| `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers with + (if dmPolicy=allowlist) | ❌ | + +**Important:** +- `WHATSAPP_SELF_CHAT=false` (dedicated bot number): Responds to ALL incoming messages +- `WHATSAPP_SELF_CHAT=true` (personal number): Only responds to "Message Yourself" chat +- QR code appears on first run - scan with WhatsApp app + +### Signal + +| Variable | Description | Required | +|----------|-------------|----------| +| `SIGNAL_PHONE_NUMBER` | Your phone number (with +) | ✅ | +| `SIGNAL_DM_POLICY` | Access control: `pairing` \| `allowlist` \| `open` | ❌ (default: pairing) | +| `SIGNAL_ALLOWED_USERS` | Comma-separated phone numbers with + (if dmPolicy=allowlist) | ❌ | + +**Setup:** Requires Signal CLI - see [signal-cli documentation](https://github.com/AsamK/signal-cli). + +## Channel-Specific Setup + +### Telegram Bot Setup + +1. Message [@BotFather](https://t.me/BotFather) on Telegram +2. Send `/newbot` and follow prompts +3. Copy the token (format: `123456:ABC-DEF...`) +4. Set `TELEGRAM_BOT_TOKEN` environment variable + +### Slack App Setup (Interactive) + +For Socket Mode (required for real-time messages): + +```bash +lettabot onboard +# Select "Slack" → "Guided setup" +``` + +This uses a manifest to pre-configure: +- Socket Mode +- 5 bot scopes (app_mentions:read, chat:write, im:*) +- 2 event subscriptions (app_mention, message.im) + +### Slack App Setup (Manual) + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Create app from manifest (see `src/setup/slack-wizard.ts` for manifest YAML) +3. Install to workspace → copy Bot Token (`xoxb-...`) +4. Enable Socket Mode → generate App Token (`xapp-...`) +5. Set both tokens in environment + +## Access Control + +Each channel supports three DM policies: + +- **`pairing`** (recommended): Users get a code, you approve via `lettabot pairing approve ` +- **`allowlist`**: Only specified user IDs can message +- **`open`**: Anyone can message (not recommended) + +## Configuration File + +After onboarding, config is saved to `~/.config/lettabot/config.yaml`: + +```yaml +server: + baseUrl: https://api.letta.com + apiKey: letta_... + agentId: agent-... + +telegram: + enabled: true + botToken: 123456:ABC-DEF... + dmPolicy: pairing + +slack: + enabled: true + botToken: xoxb-... + appToken: xapp-... + dmPolicy: pairing +``` + +Edit this file directly or re-run `lettabot onboard` to reconfigure. + +## Commands + +```bash +# Setup +lettabot onboard # Interactive wizard +lettabot onboard --non-interactive # Env-based setup (agent-friendly) + +# Run +lettabot server # Start bot server + +# Manage +lettabot pairing list # List pending pairing requests +lettabot pairing approve # Approve user +lettabot skills # Enable/disable skills + +# Scheduling +lettabot cron list # List scheduled tasks +lettabot cron add "Daily standup at 9am" "0 9 * * *" # Add cron job +``` + +## Troubleshooting + +### "Module not found" errors + +Make sure you've run `npm run build` after installing or pulling updates. + +### Telegram bot not responding + +1. Check token is correct: `curl https://api.telegram.org/bot/getMe` +2. Ensure bot is started: `lettabot server` should show "Connected to Telegram" +3. Check access control: User may need pairing approval + +### Slack not receiving messages + +1. Verify Socket Mode is enabled in Slack app settings +2. Check both tokens are set: `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` +3. Ensure event subscriptions are configured (app_mention, message.im) + +### WhatsApp QR code not appearing + +1. Make sure Signal Desktop is closed (conflicts with baileys) +2. Delete `~/.wwebjs_auth` if previously used different library +3. Check no other WhatsApp Web sessions are active + +## Example: Agent Setup Flow + +For coding agents helping users set up LettaBot: + +```bash +# 1. Clone and build +git clone https://github.com/letta-ai/lettabot.git +cd lettabot +npm install && npm run build && npm link + +# 2. Get Letta API key +# Guide user to app.letta.com → API Keys → Create Key + +# 3. Get Telegram bot token +# Guide user to @BotFather → /newbot → follow prompts + +# 4. Set environment variables +export LETTA_API_KEY="letta_..." +export TELEGRAM_BOT_TOKEN="123456:ABC-DEF..." + +# 5. Run non-interactive setup +lettabot onboard --non-interactive + +# 6. Start server +lettabot server +``` + +The agent can verify success by checking: +- `lettabot server` output shows "Connected to Telegram" +- Config file exists at `~/.config/lettabot/config.yaml` +- User can message bot on Telegram + +## Self-Hosted Letta + +To use a self-hosted Letta server: + +```bash +# Run Letta Docker +docker run -v ~/.letta/.persist/pgdata:/var/lib/postgresql/data \ + -p 8283:8283 \ + -e OPENAI_API_KEY="your_openai_api_key" \ + letta/letta:latest + +# Configure LettaBot +export LETTA_BASE_URL="http://localhost:8283" +export LETTA_API_KEY="sk-..." # From Letta admin panel + +lettabot onboard --non-interactive +``` + +## Skills Integration + +LettaBot supports loading skills from: +- **Clawdhub** ([clawdhub.com](https://clawdhub.com)) +- **skills.sh** repositories +- Local `.skills/` directory + +```bash +# Install skill from Clawdhub +npx molthub@latest install sonoscli + +# Connect to LettaBot +lettabot skills +# Space to toggle, Enter to confirm + +# Skills will be available to agent +``` diff --git a/src/cli.ts b/src/cli.ts index 5741fe3..9896820 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -206,7 +206,8 @@ async function main() { case 'onboard': case 'setup': case 'init': - await onboard(); + const nonInteractive = args.includes('--non-interactive') || args.includes('-n'); + await onboard({ nonInteractive }); break; case 'server': diff --git a/src/onboard.ts b/src/onboard.ts index f8e3545..c61528f 100644 --- a/src/onboard.ts +++ b/src/onboard.ts @@ -9,6 +9,119 @@ import * as p from '@clack/prompts'; import { saveConfig, syncProviders } from './config/index.js'; import type { LettaBotConfig, ProviderConfig } from './config/types.js'; +// ============================================================================ +// Non-Interactive Helpers +// ============================================================================ + +function readConfigFromEnv(existingConfig: any): any { + return { + baseUrl: process.env.LETTA_BASE_URL || existingConfig.server?.baseUrl || 'https://api.letta.com', + apiKey: process.env.LETTA_API_KEY || existingConfig.server?.apiKey, + agentId: process.env.LETTA_AGENT_ID || existingConfig.agent?.id, + agentName: process.env.LETTA_AGENT_NAME || existingConfig.agent?.name || 'lettabot', + model: process.env.LETTA_MODEL || existingConfig.agent?.model || 'claude-sonnet-4', + + telegram: { + enabled: !!process.env.TELEGRAM_BOT_TOKEN, + botToken: process.env.TELEGRAM_BOT_TOKEN || existingConfig.channels?.telegram?.token, + dmPolicy: process.env.TELEGRAM_DM_POLICY || existingConfig.channels?.telegram?.dmPolicy || 'pairing', + allowedUsers: process.env.TELEGRAM_ALLOWED_USERS?.split(',').map(s => s.trim()) || existingConfig.channels?.telegram?.allowedUsers, + }, + + slack: { + enabled: !!(process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN), + botToken: process.env.SLACK_BOT_TOKEN || existingConfig.channels?.slack?.botToken, + appToken: process.env.SLACK_APP_TOKEN || existingConfig.channels?.slack?.appToken, + dmPolicy: process.env.SLACK_DM_POLICY || existingConfig.channels?.slack?.dmPolicy || 'pairing', + allowedUsers: process.env.SLACK_ALLOWED_USERS?.split(',').map(s => s.trim()) || existingConfig.channels?.slack?.allowedUsers, + }, + + discord: { + enabled: !!process.env.DISCORD_BOT_TOKEN, + botToken: process.env.DISCORD_BOT_TOKEN || existingConfig.channels?.discord?.token, + dmPolicy: process.env.DISCORD_DM_POLICY || existingConfig.channels?.discord?.dmPolicy || 'pairing', + allowedUsers: process.env.DISCORD_ALLOWED_USERS?.split(',').map(s => s.trim()) || existingConfig.channels?.discord?.allowedUsers, + }, + + whatsapp: { + enabled: process.env.WHATSAPP_ENABLED === 'true' || !!existingConfig.channels?.whatsapp?.enabled, + selfChat: process.env.WHATSAPP_SELF_CHAT === 'true' || !!existingConfig.channels?.whatsapp?.selfChat || false, + dmPolicy: process.env.WHATSAPP_DM_POLICY || existingConfig.channels?.whatsapp?.dmPolicy || 'pairing', + allowedUsers: process.env.WHATSAPP_ALLOWED_USERS?.split(',').map(s => s.trim()) || existingConfig.channels?.whatsapp?.allowedUsers, + }, + + signal: { + enabled: !!process.env.SIGNAL_PHONE_NUMBER, + phoneNumber: process.env.SIGNAL_PHONE_NUMBER || existingConfig.channels?.signal?.phoneNumber, + dmPolicy: process.env.SIGNAL_DM_POLICY || existingConfig.channels?.signal?.dmPolicy || 'pairing', + allowedUsers: process.env.SIGNAL_ALLOWED_USERS?.split(',').map(s => s.trim()) || existingConfig.channels?.signal?.allowedUsers, + }, + }; +} + +async function saveConfigFromEnv(config: any, configPath: string): Promise { + const { saveConfig } = await import('./config/index.js'); + + const lettabotConfig: LettaBotConfig = { + server: { + mode: config.baseUrl?.includes('localhost') ? 'selfhosted' : 'cloud', + baseUrl: config.baseUrl, + apiKey: config.apiKey, + }, + agent: { + id: config.agentId, + name: config.agentName, + model: config.model, + }, + channels: { + telegram: config.telegram.enabled ? { + enabled: true, + token: config.telegram.botToken, + dmPolicy: config.telegram.dmPolicy, + allowedUsers: config.telegram.allowedUsers, + } : { enabled: false }, + + slack: config.slack.enabled ? { + enabled: true, + botToken: config.slack.botToken, + appToken: config.slack.appToken, + allowedUsers: config.slack.allowedUsers, + } : { enabled: false }, + + discord: config.discord.enabled ? { + enabled: true, + token: config.discord.botToken, + dmPolicy: config.discord.dmPolicy, + allowedUsers: config.discord.allowedUsers, + } : { enabled: false }, + + whatsapp: config.whatsapp.enabled ? { + enabled: true, + selfChat: config.whatsapp.selfChat, + dmPolicy: config.whatsapp.dmPolicy, + allowedUsers: config.whatsapp.allowedUsers, + } : { enabled: false }, + + signal: config.signal.enabled ? { + enabled: true, + phone: config.signal.phoneNumber, + selfChat: config.signal.selfChat, + dmPolicy: config.signal.dmPolicy, + allowedUsers: config.signal.allowedUsers, + } : { enabled: false }, + }, + features: { + cron: false, + heartbeat: { + enabled: false, + intervalMin: 60, + }, + }, + }; + + saveConfig(lettabotConfig); +} + // ============================================================================ // Config Types // ============================================================================ @@ -1173,7 +1286,8 @@ async function reviewLoop(config: OnboardConfig, env: Record): P // Main Onboard Function // ============================================================================ -export async function onboard(): Promise { +export async function onboard(options?: { nonInteractive?: boolean }): Promise { + const nonInteractive = options?.nonInteractive || false; // Temporary storage for wizard values const env: Record = {}; @@ -1183,6 +1297,46 @@ export async function onboard(): Promise { const configPath = resolveConfigPath(); const hasExistingConfig = existsSync(configPath); + // Non-interactive mode: read all config from env vars + if (nonInteractive) { + console.log('🤖 LettaBot Non-Interactive Setup\n'); + console.log('Reading configuration from environment variables...\n'); + + const config = readConfigFromEnv(existingConfig); + + // Validate required fields + if (!config.baseUrl) { + console.error('❌ Error: LETTA_BASE_URL is required'); + process.exit(1); + } + + if (!config.apiKey && !config.baseUrl?.includes('localhost')) { + console.error('❌ Error: LETTA_API_KEY is required (or use self-hosted with LETTA_BASE_URL)'); + process.exit(1); + } + + // Test server connection + console.log(`Connecting to ${config.baseUrl}...`); + try { + const res = await fetch(`${config.baseUrl}/v1/health`, { signal: AbortSignal.timeout(5000) }); + if (res.ok) { + console.log('✅ Connected to server\n'); + } else { + console.error(`❌ Server returned status ${res.status}`); + process.exit(1); + } + } catch (e) { + console.error(`❌ Could not connect to ${config.baseUrl}`); + process.exit(1); + } + + // Save config and exit + await saveConfigFromEnv(config, configPath); + console.log(`✅ Configuration saved to ${configPath}\n`); + console.log('Run "lettabot server" to start the bot.'); + return; + } + p.intro('🤖 LettaBot Setup'); if (hasExistingConfig) { diff --git a/src/setup/slack-wizard.ts b/src/setup/slack-wizard.ts index 0001ef4..d2a3a49 100644 --- a/src/setup/slack-wizard.ts +++ b/src/setup/slack-wizard.ts @@ -85,13 +85,14 @@ async function stepCreateApp(): Promise { p.log.step('Step 1/3: Create Slack App from Manifest'); // Inline manifest for Socket Mode configuration + const appName = process.env.SLACK_APP_NAME || process.env.LETTA_AGENT_NAME || 'LettaBot'; const manifest = `display_information: - name: LettaBot + name: ${appName} description: AI assistant with Socket Mode for real-time conversations background_color: "#2c2d30" features: bot_user: - display_name: LettaBot + display_name: ${appName} always_online: false oauth_config: scopes: