commit bcb3233ecb123e9805ea72487b29fbbf74f733ac Author: alexanxin Date: Wed Mar 25 15:22:55 2026 +0100 Initial release: opencode-dispatch Control opencode from Telegram — like Claude's Dispatch feature. - Python (bot.py) and Node.js (bot.js) implementations - Connects to opencode server API via POST /session/:id/message - Queue system for handling concurrent messages - /start, /help, /status, /working, /clear commands - Workspace scoping via cd into project directory - Password protection support via OPENCODE_SERVER_PASSWORD diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e675222 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Telegram Bot Token +# Get this from @BotFather on Telegram +# 1. Open Telegram and search for @BotFather +# 2. Send /newbot +# 3. Follow the prompts to create your bot +# 4. Copy the token it gives you +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# opencode API URL +# Must match the --port flag you use when starting opencode serve +# Example: opencode serve --port 5050 +OPENCODE_API_URL=http://127.0.0.1:5050 + +# Optional: password protect the opencode server +# If set, you must also run: OPENCODE_SERVER_PASSWORD=your-secret opencode serve --port 5050 +# OPENCODE_SERVER_PASSWORD=your-secret diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82831d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Environment variables (contains secrets) +.env + +# Dependencies +node_modules/ +__pycache__/ +*.pyc + +# Cache +.ruff_cache/ +.pytest_cache/ +.mypy_cache/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbedca0 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# opencode-dispatch + +**Control opencode from Telegram — like Claude users do with Dispatch.** + +> I deeply appreciate what the opencode team is building. This project is my small contribution to their already awesome work — making opencode accessible from anywhere, just like Claude's Dispatch. + +opencode-dispatch bridges your Telegram bot to the opencode CLI. Send messages from your phone, and opencode processes them just like it would in a terminal. Perfect for when you're away from your desk but still want AI assistance. + +## What You Need + +- **[opencode CLI](https://opencode.ai)** installed — verify with `opencode --version` +- A Telegram account +- Python 3.10+ or Node.js +- A Telegram bot token (free from [@BotFather](https://t.me/BotFather)) + +## Quick Setup + +### Step 1: Create a Telegram Bot + +1. Open Telegram and search for **@BotFather** +2. Send `/newbot` +3. Give it a name (e.g., "My opencode Bot") +4. Give it a username ending in `bot` (e.g., `my_opencode_bot`) +5. Copy the token BotFather gives you + +### Step 2: Install Dependencies + +Choose Python or Node.js (both work the same): + +**Python:** +```bash +pip install -r requirements.txt +``` + +**Node.js:** +```bash +npm install +``` + +### Step 3: Configure + +```bash +cp .env.example .env +``` + +Edit `.env` and add your Telegram bot token: + +```env +TELEGRAM_BOT_TOKEN=your_bot_token_here +OPENCODE_API_URL=http://127.0.0.1:5050 +``` + +### Step 4: Start opencode + +```bash +cd ~/your-project +opencode serve --port 5050 +``` + +> **Important:** Always `cd` into a specific project folder first. This limits opencode's access to that folder and its subfolders. + +### Step 5: Run the Bot + +**Python:** +```bash +python bot.py +``` + +**Node.js:** +```bash +npm start +``` + +### Step 6: Chat + +Open Telegram, find your bot, and send a message. opencode will receive it and respond through the bot. + +## Bot Commands + +| Command | Description | +|---------|-------------| +| `/start` | Welcome message and server info | +| `/help` | Usage instructions | +| `/status` | Server health, session ID, queue size | +| `/working` | Check what task is currently being processed | +| `/clear` | Clear pending messages from queue | + +## How It Works + +``` +Your Phone → Telegram → bot.py → opencode API (port 5050) → response → Telegram → Your Phone +``` + +1. You send a message to your Telegram bot +2. The bot forwards it to opencode via the server API +3. opencode processes it in your project context +4. The response comes back through the bot to your phone + +## Use Cases + +### 1. Ask About Your Codebase + +``` +You: "What does the auth middleware in @middleware.ts do?" +opencode: "The auth middleware in middleware.ts checks for a valid JWT token + in the Authorization header. It extracts the user ID from the token + and attaches it to the request object. If the token is missing or + expired, it returns a 401 response." +``` + +### 2. Start a Task + +``` +You: "Add input validation to the signup form" +opencode: "I'll add validation to the signup form. I found the form in + src/components/Signup.tsx. I'll add email format validation, + password length requirements, and field presence checks." +``` + +### 3. Check Project Status + +``` +You: "Are there any failing tests?" +opencode: "I ran the test suite. 47 passing, 2 failing: + - test_checkout_flow: assertion error in calculateTotal + - test_user_profile: timeout waiting for API response" +``` + +### 4. Get Commit Summaries + +``` +You: "What changed in the last 3 commits?" +opencode: "Last 3 commits: + 1. Add user profile API endpoints (2 files changed) + 2. Fix payment timeout issue (1 file changed) + 3. Update test fixtures for new schema (4 files changed)" +``` + +### 5. Quick Lookups + +``` +You: "What environment variables does this project need?" +opencode: "Required env vars from .env.example: + - DATABASE_URL: PostgreSQL connection string + - JWT_SECRET: Secret for token signing + - STRIPE_KEY: Stripe API key for payments" +``` + +## Security + +### Limit workspace access + +Always start opencode from a specific project folder: + +```bash +cd ~/my-project # Only this folder and subfolders +opencode serve --port 5050 +``` + +**Never** run from your home directory (`~`) or root (`/`). opencode can access all files in the directory it's started from. + +### Password protect (recommended) + +Set a password to prevent unauthorized local access: + +```bash +OPENCODE_SERVER_PASSWORD=your-secret opencode serve --port 5050 +``` + +Add to `.env`: +```env +OPENCODE_SERVER_PASSWORD=your-secret +``` + +### Network safety + +The server only listens on `127.0.0.1` (localhost) by default. It's not accessible from other machines on your network. Never use `--hostname 0.0.0.0` unless you know what you're doing. + +## What's Included + +| File | Purpose | +|------|---------| +| `bot.py` | Python bot — receives Telegram messages and forwards to opencode | +| `bot.js` | Node.js bot — same as bot.py but for Node users | +| `requirements.txt` | Python dependencies | +| `package.json` | Node.js dependencies | +| `.env.example` | Template for your configuration | + +## Troubleshooting + +**"Can't connect to opencode"** +- Make sure `opencode serve --port 5050` is running in a terminal +- Verify with: `curl http://127.0.0.1:5050/global/health` + +**"Bot isn't responding"** +- Check your Telegram bot token in `.env` +- Make sure the bot is running (`python bot.py` or `npm start`) + +**"Port already in use"** +- Another process is using port 5050 +- Pick a different port: `opencode serve --port 5051` +- Update `OPENCODE_API_URL` in `.env` to match + +**"opencode command not found"** +- Install the CLI: `curl -fsSL https://opencode.ai/install | bash` +- Then restart your terminal or run: `source ~/.zshrc` + +## Tips for Best Results + +- **Be specific**: Instead of "fix my code," say "fix the null pointer error in UserService.java" +- **One task at a time**: For complex requests, break them into smaller steps +- **Keep context**: Mention relevant files or features so opencode understands what you're referring to +- **Use /status**: Check if opencode is healthy before sending important tasks + +## Contributing + +Found a bug? Have an improvement? Open an issue or submit a pull request! + +## License + +MIT diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..6825cdc --- /dev/null +++ b/bot.js @@ -0,0 +1,214 @@ +import { Telegraf } from 'telegraf'; +import { config } from 'dotenv'; +import axios from 'axios'; + +config(); + +const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; +const OPENCODE_API_URL = process.env.OPENCODE_API_URL || 'http://127.0.0.1:5050'; +const ALLOWED_CHAT_ID = process.env.TELEGRAM_ALLOWED_CHAT_ID ? String(process.env.TELEGRAM_ALLOWED_CHAT_ID) : null; +const messageQueue = []; +let isProcessing = false; +let currentTask = 'Idle'; + +if (!BOT_TOKEN) { + console.error('Error: TELEGRAM_BOT_TOKEN not set in .env file'); + process.exit(1); +} + +async function getSession() { + try { + const r = await axios.get(`${OPENCODE_API_URL}/session`, { timeout: 10000 }); + if (r.data && r.data.length > 0) { + return r.data[0].id; + } + } catch (e) {} + try { + const r = await axios.post(`${OPENCODE_API_URL}/session`, {}, { timeout: 10000 }); + if (r.data && r.data.id) { + return r.data.id; + } + } catch (e) {} + return null; +} + +async function sendToOpencode(message) { + const sessionId = await getSession(); + if (!sessionId) { + return "Error: Could not connect to opencode session."; + } + + try { + const r = await axios.post( + `${OPENCODE_API_URL}/session/${sessionId}/message`, + { parts: [{ type: "text", text: `[Telegram] ${message}` }] }, + { timeout: 1200000 } + ); + if (r.data && r.data.parts) { + const textParts = r.data.parts + .filter(p => p.type === "text" && p.text) + .map(p => p.text); + return textParts.join("\n") || "Message sent, no text response."; + } + return "Message sent."; + } catch (error) { + if (error.code === 'ECONNREFUSED') { + return "Can't connect to opencode. Is it running?"; + } else if (error.code === 'ETIMEDOUT') { + return "opencode took too long to respond. Please try again."; + } + return `Error: ${error.message}`; + } +} + +async function processQueue() { + while (messageQueue.length > 0) { + const item = messageQueue.shift(); + isProcessing = true; + try { + const reply = await sendToOpencode(item.message); + await item.ctx.telegram.editMessageText( + item.chatId, + item.messageId, + null, + `🔄 Processed from queue\n\n${reply.substring(0, 4000)}` + ); + } catch (e) { + await item.ctx.telegram.editMessageText( + item.chatId, + item.messageId, + null, + `Error: ${e.message}` + ); + } + isProcessing = false; + } +} + +const bot = new Telegraf(BOT_TOKEN); + +bot.start((ctx) => { + ctx.reply( + 'opencode-dispatch bot\n\n' + + 'Send any message and opencode will process it.\n' + + `Server: ${OPENCODE_API_URL}` + ); +}); + +bot.help((ctx) => { + ctx.reply( + 'How to use:\n\n' + + '1. Make sure opencode is running\n' + + '2. Send me any message\n' + + '3. I\'ll forward it to opencode and relay the response\n\n' + + 'Commands: /start, /help, /status, /working, /clear' + ); +}); + +bot.command('status', async (ctx) => { + const sessionId = await getSession(); + let healthy = false; + try { + const r = await axios.get(`${OPENCODE_API_URL}/global/health`, { timeout: 5000 }); + healthy = r.ok; + } catch (e) {} + ctx.reply( + `Server: ${OPENCODE_API_URL}\n` + + `opencode: ${healthy ? '✅' : '❌'}\n` + + `Session: ${sessionId || 'none'}\n` + + `Queue: ${messageQueue.length} messages` + ); +}); + +bot.command('clear', (ctx) => { + if (isProcessing) { + ctx.reply('❌ Can\'t clear queue while processing. Wait for current task to finish.'); + } else { + messageQueue.length = 0; + ctx.reply('✅ Queue cleared.'); + } +}); + +bot.command('working', (ctx) => { + if (isProcessing) { + ctx.reply(`🔄 Currently working on:\n"${currentTask}"\n\nQueue: ${messageQueue.length} messages`); + } else { + ctx.reply('✅ Currently idle. No task in progress.'); + } +}); + +bot.on('text', async (ctx) => { + const chatId = String(ctx.chat.id); + if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) { + ctx.reply('❌ This bot is not authorized to respond to you.'); + return; + } + + const userMessage = ctx.message.text; + + if (isProcessing) { + const sent = await ctx.reply( + '⏳ opencode is busy. Your message has been added to the queue.\n' + + 'I\'ll respond when ready. Use /status to check queue position.' + ); + messageQueue.push({ + message: userMessage, + chatId: ctx.chat.id, + messageId: sent.message_id, + ctx: ctx + }); + } else { + currentTask = userMessage.length > 50 ? userMessage.substring(0, 50) + '...' : userMessage; + const sent = await ctx.reply('🔄 Processing...'); + isProcessing = true; + try { + const reply = await sendToOpencode(userMessage); + await ctx.telegram.editMessageText( + ctx.chat.id, + sent.message_id, + null, + reply.substring(0, 4000) + ); + } catch (e) { + await ctx.telegram.editMessageText( + ctx.chat.id, + sent.message_id, + null, + `Error: ${e.message}` + ); + } + isProcessing = false; + currentTask = 'Idle'; + + if (messageQueue.length > 0) { + processQueue(); + } + } +}); + +bot.on('voice', (ctx) => { + const chatId = String(ctx.chat.id); + if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return; + ctx.reply('Voice messages not yet supported. Send text.'); +}); + +bot.on('document', (ctx) => { + const chatId = String(ctx.chat.id); + if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return; + ctx.reply('File handling not yet supported. Send text.'); +}); + +bot.on('photo', (ctx) => { + const chatId = String(ctx.chat.id); + if (ALLOWED_CHAT_ID && chatId !== ALLOWED_CHAT_ID) return; + ctx.reply('Image handling not yet supported. Send text.'); +}); + +console.log('opencode-dispatch bot starting...'); +console.log(`Connecting to opencode at: ${OPENCODE_API_URL}`); +console.log('Press Ctrl+C to stop'); + +bot.launch(); + +process.once('SIGINT', () => bot.stop('SIGINT')); +process.once('SIGTERM', () => bot.stop('SIGTERM')); diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..69ba722 --- /dev/null +++ b/bot.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +import os +import threading +import queue +import requests +from dotenv import load_dotenv +from telegram import Update +from telegram.ext import ( + Application, + CommandHandler, + MessageHandler, + filters, + ContextTypes, +) + +load_dotenv() + +OPENCODE_API_URL = os.getenv("OPENCODE_API_URL", "http://127.0.0.1:5050") +BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") +ALLOWED_CHAT_ID = os.getenv("TELEGRAM_ALLOWED_CHAT_ID") +SESSION_ID = None +message_queue = queue.Queue() +is_processing = False +current_task = "Idle" +processing_lock = threading.Lock() + + +def get_session(): + """Get or create a session.""" + global SESSION_ID + if SESSION_ID: + return SESSION_ID + try: + r = requests.get(f"{OPENCODE_API_URL}/session", timeout=10) + if r.ok: + sessions = r.json() + if sessions: + SESSION_ID = sessions[0]["id"] + return SESSION_ID + except Exception: + pass + try: + r = requests.post(f"{OPENCODE_API_URL}/session", json={}, timeout=10) + if r.ok: + SESSION_ID = r.json()["id"] + return SESSION_ID + except Exception: + pass + return None + + +def send_to_opencode(message): + """Send message to opencode and return response.""" + session_id = get_session() + if not session_id: + return "Error: Could not connect to opencode session." + + try: + r = requests.post( + f"{OPENCODE_API_URL}/session/{session_id}/message", + json={"parts": [{"type": "text", "text": f"[Telegram] {message}"}]}, + timeout=1200, + ) + if r.ok: + data = r.json() + parts = data.get("parts", []) + text_parts = [ + p["text"] for p in parts if p.get("type") == "text" and p.get("text") + ] + return ( + "\n".join(text_parts) + if text_parts + else "Message sent, no text response." + ) + else: + return f"opencode returned {r.status_code}: {r.text[:200]}" + except requests.exceptions.ConnectionError: + return "Can't connect to opencode. Is it running?" + except requests.exceptions.Timeout: + return "opencode took too long to respond. Please try again." + except Exception as e: + return f"Error: {str(e)}" + + +def process_queue(bot, chat_id): + """Process messages in the queue one at a time.""" + global is_processing + while True: + try: + item = message_queue.get(timeout=1) + if item is None: + break + user_id, message_id, user_message = item + with processing_lock: + is_processing = True + try: + reply = send_to_opencode(user_message) + + async def send_reply(): + await bot.edit_message_text( + chat_id=chat_id, + message_id=message_id, + text=f"🔄 Processing...\n\n{reply[:4000]}", + ) + + import asyncio + + asyncio.run(send_reply()) + finally: + with processing_lock: + is_processing = False + message_queue.task_done() + except queue.Empty: + continue + + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await update.message.reply_text( + "opencode-dispatch bot\n\n" + "Send any message and opencode will process it.\n" + f"Server: {OPENCODE_API_URL}" + ) + + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + await update.message.reply_text( + "How to use:\n\n" + "1. Make sure opencode is running\n" + "2. Send me any message\n" + "3. I'll forward it to opencode and relay the response\n\n" + "Commands: /start, /help, /status, /working, /clear" + ) + + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + session_id = get_session() + try: + r = requests.get(f"{OPENCODE_API_URL}/global/health", timeout=5) + healthy = r.ok + except Exception: + healthy = False + queue_size = message_queue.qsize() + await update.message.reply_text( + f"Server: {OPENCODE_API_URL}\n" + f"opencode: {'✅' if healthy else '❌'}\n" + f"Session: {session_id or 'none'}\n" + f"Queue: {queue_size} messages" + ) + + +async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + with processing_lock: + if is_processing: + await update.message.reply_text( + "❌ Can't clear queue while processing. Wait for current task to finish." + ) + else: + while not message_queue.empty(): + try: + message_queue.get_nowait() + except queue.Empty: + break + await update.message.reply_text("✅ Queue cleared.") + + +async def working_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + global current_task + if is_processing: + await update.message.reply_text( + f'🔄 Currently working on:\n"{current_task}"\n\nQueue: {message_queue.qsize()} messages' + ) + else: + await update.message.reply_text("✅ Currently idle. No task in progress.") + + +async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): + global is_processing, current_task + chat_id = str(update.message.chat.id) + if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: + await update.message.reply_text( + "❌ This bot is not authorized to respond to you." + ) + return + user_message = update.message.text + user_id = update.effective_user.id if update.effective_user else "unknown" + + with processing_lock: + currently_processing = is_processing + + if currently_processing: + sent = await update.message.reply_text( + "⏳ opencode is busy. Your message has been added to the queue.\n" + "I'll respond when ready. Use /status to check queue position." + ) + message_queue.put((user_id, sent.message_id, user_message)) + else: + current_task = ( + user_message[:50] + "..." if len(user_message) > 50 else user_message + ) + await update.message.chat.send_action("typing") + sent = await update.message.reply_text("🔄 Processing...") + with processing_lock: + is_processing = True + try: + reply = send_to_opencode(user_message) + await sent.edit_text(reply[:4000]) + except Exception as e: + await sent.edit_text(f"Error: {str(e)}") + finally: + with processing_lock: + is_processing = False + current_task = "Idle" + + +async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): + chat_id = str(update.message.chat.id) + if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: + return + await update.message.reply_text("Voice messages not yet supported. Send text.") + + +async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE): + chat_id = str(update.message.chat.id) + if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: + return + await update.message.reply_text("File handling not yet supported. Send text.") + + +async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): + chat_id = str(update.message.chat.id) + if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID: + return + await update.message.reply_text("Image handling not yet supported. Send text.") + + +def main(): + if not BOT_TOKEN: + print("Error: TELEGRAM_BOT_TOKEN not set in .env file") + return + + app = Application.builder().token(BOT_TOKEN).build() + + app.add_handler(CommandHandler("start", start_command)) + app.add_handler(CommandHandler("help", help_command)) + app.add_handler(CommandHandler("status", status_command)) + app.add_handler(CommandHandler("working", working_command)) + app.add_handler(CommandHandler("clear", clear_command)) + app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) + app.add_handler(MessageHandler(filters.VOICE, handle_voice)) + app.add_handler(MessageHandler(filters.Document.ALL, handle_document)) + app.add_handler(MessageHandler(filters.PHOTO, handle_photo)) + + print(f"opencode-dispatch bot starting...") + print(f"Connecting to opencode at: {OPENCODE_API_URL}") + print("Press Ctrl+C to stop") + + app.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fb9da7c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,481 @@ +{ + "name": "opencode-dispatch", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opencode-dispatch", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^1.7.9", + "dotenv": "^16.4.5", + "telegraf": "^4.16.3" + } + }, + "node_modules/@telegraf/types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz", + "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/p-timeout": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", + "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/safe-compare": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", + "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==", + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.2.0" + } + }, + "node_modules/sandwich-stream": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", + "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/telegraf": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", + "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", + "license": "MIT", + "dependencies": { + "@telegraf/types": "^7.1.0", + "abort-controller": "^3.0.0", + "debug": "^4.3.4", + "mri": "^1.2.0", + "node-fetch": "^2.7.0", + "p-timeout": "^4.1.0", + "safe-compare": "^1.1.4", + "sandwich-stream": "^2.0.2" + }, + "bin": { + "telegraf": "lib/cli.mjs" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ebafdc5 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "opencode-dispatch", + "version": "1.0.0", + "description": "Control opencode from Telegram - like Claude users do with Dispatch", + "main": "bot.js", + "type": "module", + "scripts": { + "start": "node bot.js" + }, + "dependencies": { + "telegraf": "^4.16.3", + "dotenv": "^16.4.5", + "axios": "^1.7.9" + }, + "keywords": [ + "opencode", + "telegram", + "bot", + "ai", + "cli" + ], + "author": "", + "license": "MIT" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8943bcc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +python-telegram-bot==21.6 +requests==2.32.3 +python-dotenv==1.0.1