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:
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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'));
|
||||||
@@ -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()
|
||||||
Generated
+481
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
python-telegram-bot==21.6
|
||||||
|
requests==2.32.3
|
||||||
|
python-dotenv==1.0.1
|
||||||
Reference in New Issue
Block a user