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
This commit is contained in:
alexanxin
2026-03-25 15:22:55 +01:00
commit bcb3233ecb
8 changed files with 1245 additions and 0 deletions
+16
View File
@@ -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
+25
View File
@@ -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
+221
View File
@@ -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
+214
View File
@@ -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'));
+261
View File
@@ -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()
+481
View File
@@ -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"
}
}
}
}
+24
View File
@@ -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"
}
+3
View File
@@ -0,0 +1,3 @@
python-telegram-bot==21.6
requests==2.32.3
python-dotenv==1.0.1