feat: import
perso/opencode-dispatch/pipeline/head This commit looks good

This commit is contained in:
Julien Cabillot
2026-03-27 16:55:22 -04:00
parent c0e82369cd
commit be3edc0e14
11 changed files with 249 additions and 830 deletions
+9 -9
View File
@@ -1,4 +1,4 @@
# Telegram Bot Token # Telegram Bot Token (required)
# Get this from @BotFather on Telegram # Get this from @BotFather on Telegram
# 1. Open Telegram and search for @BotFather # 1. Open Telegram and search for @BotFather
# 2. Send /newbot # 2. Send /newbot
@@ -6,16 +6,16 @@
# 4. Copy the token it gives you # 4. Copy the token it gives you
TELEGRAM_BOT_TOKEN=your_bot_token_here TELEGRAM_BOT_TOKEN=your_bot_token_here
# Restrict bot to your Telegram chat (required)
# Get your chat ID by messaging @userinfobot on Telegram
# The bot will refuse to start without this value.
TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here
# opencode API URL # opencode API URL
# Must match the --port flag you use when starting opencode serve # Must match the --port flag you use when starting opencode serve
# Example: opencode serve --port 5050 # Example: opencode serve --port 4096
OPENCODE_API_URL=http://127.0.0.1:5050 OPENCODE_API_URL=http://127.0.0.1:4096
# Optional: password protect the opencode server # Optional: password protect the opencode server
# If set, you must also run: OPENCODE_SERVER_PASSWORD=your-secret opencode serve --port 5050 # If set, you must also run: OPENCODE_SERVER_PASSWORD=your-secret opencode serve --port 4096
# OPENCODE_SERVER_PASSWORD=your-secret # OPENCODE_SERVER_PASSWORD=your-secret
# Optional: restrict bot to a single Telegram user
# Get your chat ID by messaging @userinfobot on Telegram
# If not set, the bot responds to anyone
# TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here
-1
View File
@@ -2,7 +2,6 @@
.env .env
# Dependencies # Dependencies
node_modules/
__pycache__/ __pycache__/
*.pyc *.pyc
+98
View File
@@ -0,0 +1,98 @@
# AGENTS.md
Instructions for AI agents working on this repository.
## Project overview
`opencode-dispatch` is a Telegram bot that acts as a secure remote bridge to a locally-running `opencode` instance. It forwards messages from a single authorized Telegram chat to the opencode REST API and relays the responses back.
Single entry point: `bot.py`. Python only.
## Architecture
```
Telegram (user) ──► bot.py ──► opencode REST API (opencode serve)
◄──────────────────────────────
```
**Concurrency model:**
- `python-telegram-bot` runs an asyncio event loop
- `send_to_opencode()` is synchronous (blocking HTTP, up to 1200s timeout) and is always called via `loop.run_in_executor()` to avoid blocking the event loop
- A `queue.Queue` serializes requests when a message arrives while one is already processing
- A `threading.Lock` (`processing_lock`) guards `is_processing` and `current_task`
**opencode API endpoints used:**
| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/global/health` | Health check (`/status` command) |
| `GET` | `/session` | List existing sessions |
| `POST` | `/session` | Create a new session |
| `POST` | `/session/{id}/message` | Send a message and get the response |
## Environment variables
Defined in `.env` (copy from `.env.example`).
| Variable | Required | Description |
|----------|----------|-------------|
| `TELEGRAM_BOT_TOKEN` | **Yes** | Bot token from @BotFather |
| `TELEGRAM_ALLOWED_CHAT_ID` | **Yes** | Telegram chat ID to allow. Bot refuses to start if unset. |
| `OPENCODE_API_URL` | No | Default: `http://127.0.0.1:5050` |
| `OPENCODE_SERVER_PASSWORD` | No | If set on the opencode server, must match |
> **Note:** `OPENCODE_SERVER_PASSWORD` is read from the env but not yet forwarded in HTTP requests. If you implement password support, add it as a header or query param to `get_session()` and `send_to_opencode()`.
## Security invariants — do not break
1. **`ALLOWED_CHAT_ID` is enforced.** `main()` exits if `TELEGRAM_ALLOWED_CHAT_ID` is not set.
2. **`is_authorized()` is called at the top of every handler** — including all command handlers (`/start`, `/help`, `/status`, `/working`, `/clear`) and all message handlers. Unauthorized requests are silently dropped (no reply).
3. **No internal info is leaked to Telegram.** Error messages sent to the user are generic strings. Detailed errors go to `logger` only.
4. **No user-visible stack traces.** Use `logger.exception()` server-side, return a static string to the user.
## Adding a new bot command
1. Write an `async def my_command(update, context)` function.
2. Call `if not is_authorized(update): return` as the first line.
3. Register it in `main()` with `app.add_handler(CommandHandler("my_command", my_command))`.
4. Add it to the help text in `help_command()`.
## Adding a new message type handler (e.g. video)
Same pattern as `handle_voice`, `handle_document`, `handle_photo`:
1. Implement the handler with an `is_authorized` guard.
2. Register with `app.add_handler(MessageHandler(filters.VIDEO, handle_video))` in `main()`.
## Running locally
```bash
# Install dependencies
pip install -r requirements.txt
# Configure
cp .env.example .env
# Edit .env: fill TELEGRAM_BOT_TOKEN and TELEGRAM_ALLOWED_CHAT_ID
# Start opencode in your target project folder
cd ~/your-project
opencode serve --port 5050
# Run the bot
python bot.py
```
## Dependencies
```
python-telegram-bot==21.6 # async Telegram bot framework
requests==2.32.3 # synchronous HTTP client for opencode API
python-dotenv==1.0.1 # .env file loading
```
No linting or test runner is currently configured. The `.gitignore` includes `.ruff_cache/` and `.pytest_cache/` anticipating future setup.
## What is not implemented yet
- `OPENCODE_SERVER_PASSWORD` forwarding in HTTP requests
- Voice, photo, and document handling (handlers exist but reply "not supported")
- Queued message processing: messages added to the queue while the bot is busy are stored but never drained — the queue worker (`process_queue`) was removed. Queued items are currently orphaned.
+16
View File
@@ -0,0 +1,16 @@
FROM python:3.14-slim
WORKDIR /app
# Install dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy bot
COPY bot.py .
# Non-root user
RUN useradd --no-create-home --shell /bin/false botuser
USER botuser
CMD ["python", "bot.py"]
Vendored
+38
View File
@@ -0,0 +1,38 @@
pipeline {
environment {
registry = 'https://registry.hub.docker.com'
registryCredential = 'dockerhub_jcabillot'
dockerImage = 'jcabillot/opencode-dispatch'
}
agent any
triggers {
cron('@midnight')
}
stages {
stage('Clone repository') {
steps{
checkout scm
}
}
stage('Build image') {
steps{
sh 'docker build --force-rm=true --no-cache=true --pull -t ${dockerImage} .'
}
}
stage('Deploy Image') {
steps{
script {
withCredentials([usernamePassword(credentialsId: 'dockerhub_jcabillot', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh 'docker login --username ${DOCKER_USER} --password ${DOCKER_PASS}'
sh 'docker push ${dockerImage}'
}
}
}
}
}
}
+18 -27
View File
@@ -1,3 +1,5 @@
> **Fork** of [alexanxin/opencode-dispatch](https://github.com/alexanxin/opencode-dispatch) — security hardening and Python-only rewrite.
# Beyond the Terminal: The Road to a Truly Autonomous AI Agent # Beyond the Terminal: The Road to a Truly Autonomous AI Agent
**opencode-dispatch** is a secure Telegram bridge for the [opencode CLI](https://opencode.ai). It brings the power of a 120K-star, self-hosted AI agent to your pocket—giving you "Dispatch-style" remote access without the corporate subscription or vendor lock-in. **opencode-dispatch** is a secure Telegram bridge for the [opencode CLI](https://opencode.ai). It brings the power of a 120K-star, self-hosted AI agent to your pocket—giving you "Dispatch-style" remote access without the corporate subscription or vendor lock-in.
@@ -37,7 +39,7 @@ This repository is more than a bridge; it's a foundational layer for a persisten
- **[opencode CLI](https://opencode.ai)** installed — verify with `opencode --version` - **[opencode CLI](https://opencode.ai)** installed — verify with `opencode --version`
- A Telegram account - A Telegram account
- Python 3.10+ or Node.js - Python 3.10+
- A Telegram bot token (free from [@BotFather](https://t.me/BotFather)) - A Telegram bot token (free from [@BotFather](https://t.me/BotFather))
## Quick Setup ## Quick Setup
@@ -49,31 +51,26 @@ This repository is more than a bridge; it's a foundational layer for a persisten
### Step 2: Install Dependencies ### Step 2: Install Dependencies
Choose Python or Node.js (both work the same):
**Python:**
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
``` ```
**Node.js:**
```bash
npm install
```
### Step 3: Configure ### Step 3: Configure
```bash ```bash
cp .env.example .env cp .env.example .env
``` ```
Edit `.env` and add your Telegram bot token: Edit `.env` and set your Telegram bot token and chat ID:
```env ```env
TELEGRAM_BOT_TOKEN=your_bot_token_here TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here
OPENCODE_API_URL=http://127.0.0.1:5050 OPENCODE_API_URL=http://127.0.0.1:5050
``` ```
> Get your chat ID by messaging [@userinfobot](https://t.me/userinfobot) on Telegram.
### Step 4: Start opencode ### Step 4: Start opencode
```bash ```bash
@@ -85,8 +82,9 @@ opencode serve --port 5050
### Step 5: Run the Bot ### Step 5: Run the Bot
- **Python:** `python bot.py` ```bash
- **Node.js:** `npm start` python bot.py
```
### Bot Commands ### Bot Commands
@@ -97,13 +95,17 @@ opencode serve --port 5050
| `/working` | Check what task is currently being processed | | `/working` | Check what task is currently being processed |
| `/clear` | Clear pending messages from queue | | `/clear` | Clear pending messages from queue |
## Security Recommendations ## Security
### 1. Limit workspace access ### 1. Chat ID locking (Required)
The bot **requires** `TELEGRAM_ALLOWED_CHAT_ID` to be set and will refuse to start without it. This ensures only your Telegram account can interact with the bot.
### 2. Limit workspace access
Never run from your home directory (`~`) or root (`/`). opencode can access all files in the directory it's started from. Never run from your home directory (`~`) or root (`/`). opencode can access all files in the directory it's started from.
### 2. Password protect (Recommended) ### 3. Password protect (Recommended)
Set a password to prevent unauthorized local access: Set a password to prevent unauthorized local access:
@@ -117,17 +119,6 @@ Then add to `.env`:
OPENCODE_SERVER_PASSWORD=your-secret OPENCODE_SERVER_PASSWORD=your-secret
``` ```
### 3. Restrict to your Telegram account (Highly Recommended)
To lock the bot so only YOU can use it:
1. Get your chat ID from [@userinfobot](https://t.me/userinfobot)
2. Add to `.env`:
```env
TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here
```
## Troubleshooting ## Troubleshooting
**"Can't connect to opencode"** **"Can't connect to opencode"**
@@ -136,7 +127,7 @@ TELEGRAM_ALLOWED_CHAT_ID=your_chat_id_here
**"Bot isn't responding"** **"Bot isn't responding"**
- Check your Telegram bot token in `.env` - Check your Telegram bot token in `.env`
- Make sure the bot is running (`python bot.py` or `npm start`) - Make sure the bot is running (`python bot.py`)
**"Port already in use"** **"Port already in use"**
- Pick a different port: `opencode serve --port 5051` - Pick a different port: `opencode serve --port 5051`
-214
View File
@@ -1,214 +0,0 @@
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'));
+67 -71
View File
@@ -1,23 +1,33 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import asyncio
import logging
import os import os
import threading
import queue import queue
import threading
import requests import requests
from dotenv import load_dotenv from dotenv import load_dotenv
from telegram import Update from telegram import Update
from telegram.ext import ( from telegram.ext import (
Application, Application,
CommandHandler, CommandHandler,
ContextTypes,
MessageHandler, MessageHandler,
filters, filters,
ContextTypes,
) )
load_dotenv() load_dotenv()
OPENCODE_API_URL = os.getenv("OPENCODE_API_URL", "http://127.0.0.1:5050") logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)
OPENCODE_API_URL = os.getenv("OPENCODE_API_URL", "http://127.0.0.1:4096")
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
ALLOWED_CHAT_ID = os.getenv("TELEGRAM_ALLOWED_CHAT_ID") ALLOWED_CHAT_ID = os.getenv("TELEGRAM_ALLOWED_CHAT_ID")
SESSION_ID = None SESSION_ID = None
message_queue = queue.Queue() message_queue = queue.Queue()
is_processing = False is_processing = False
@@ -25,6 +35,12 @@ current_task = "Idle"
processing_lock = threading.Lock() processing_lock = threading.Lock()
def is_authorized(update: Update) -> bool:
"""Check if the update comes from the allowed chat."""
chat_id = str(update.message.chat.id)
return chat_id == ALLOWED_CHAT_ID
def get_session(): def get_session():
"""Get or create a session.""" """Get or create a session."""
global SESSION_ID global SESSION_ID
@@ -38,14 +54,14 @@ def get_session():
SESSION_ID = sessions[0]["id"] SESSION_ID = sessions[0]["id"]
return SESSION_ID return SESSION_ID
except Exception: except Exception:
pass logger.exception("Failed to fetch existing sessions")
try: try:
r = requests.post(f"{OPENCODE_API_URL}/session", json={}, timeout=10) r = requests.post(f"{OPENCODE_API_URL}/session", json={}, timeout=10)
if r.ok: if r.ok:
SESSION_ID = r.json()["id"] SESSION_ID = r.json()["id"]
return SESSION_ID return SESSION_ID
except Exception: except Exception:
pass logger.exception("Failed to create new session")
return None return None
@@ -53,7 +69,7 @@ def send_to_opencode(message):
"""Send message to opencode and return response.""" """Send message to opencode and return response."""
session_id = get_session() session_id = get_session()
if not session_id: if not session_id:
return "Error: Could not connect to opencode session." return "Could not connect to opencode session."
try: try:
r = requests.post( r = requests.post(
@@ -73,56 +89,32 @@ def send_to_opencode(message):
else "Message sent, no text response." else "Message sent, no text response."
) )
else: else:
return f"opencode returned {r.status_code}: {r.text[:200]}" logger.error("opencode returned %d: %s", r.status_code, r.text[:500])
return "opencode returned an error. Check server logs."
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logger.error("Connection error to opencode at %s", OPENCODE_API_URL)
return "Can't connect to opencode. Is it running?" return "Can't connect to opencode. Is it running?"
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
logger.warning("opencode request timed out")
return "opencode took too long to respond. Please try again." return "opencode took too long to respond. Please try again."
except Exception as e: except Exception:
return f"Error: {str(e)}" logger.exception("Unexpected error sending message to opencode")
return "An unexpected error occurred. Check server logs."
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): async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_authorized(update):
return
await update.message.reply_text( await update.message.reply_text(
"opencode-dispatch bot\n\n" "opencode-dispatch bot\n\n"
"Send any message and opencode will process it.\n" "Send any message and opencode will process it.\n"
f"Server: {OPENCODE_API_URL}" "Commands: /start, /help, /status, /working, /clear"
) )
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_authorized(update):
return
await update.message.reply_text( await update.message.reply_text(
"How to use:\n\n" "How to use:\n\n"
"1. Make sure opencode is running\n" "1. Make sure opencode is running\n"
@@ -133,7 +125,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
session_id = get_session() if not is_authorized(update):
return
try: try:
r = requests.get(f"{OPENCODE_API_URL}/global/health", timeout=5) r = requests.get(f"{OPENCODE_API_URL}/global/health", timeout=5)
healthy = r.ok healthy = r.ok
@@ -141,18 +134,19 @@ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
healthy = False healthy = False
queue_size = message_queue.qsize() queue_size = message_queue.qsize()
await update.message.reply_text( await update.message.reply_text(
f"Server: {OPENCODE_API_URL}\n" f"opencode: {'connected' if healthy else 'unreachable'}\n"
f"opencode: {'' if healthy else ''}\n" f"Session: {'active' if SESSION_ID else 'none'}\n"
f"Session: {session_id or 'none'}\n"
f"Queue: {queue_size} messages" f"Queue: {queue_size} messages"
) )
async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not is_authorized(update):
return
with processing_lock: with processing_lock:
if is_processing: if is_processing:
await update.message.reply_text( await update.message.reply_text(
"❌ Can't clear queue while processing. Wait for current task to finish." "Can't clear queue while processing. Wait for current task to finish."
) )
else: else:
while not message_queue.empty(): while not message_queue.empty():
@@ -160,26 +154,23 @@ async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
message_queue.get_nowait() message_queue.get_nowait()
except queue.Empty: except queue.Empty:
break break
await update.message.reply_text("Queue cleared.") await update.message.reply_text("Queue cleared.")
async def working_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def working_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
global current_task if not is_authorized(update):
return
if is_processing: if is_processing:
await update.message.reply_text( await update.message.reply_text(
f'🔄 Currently working on:\n"{current_task}"\n\nQueue: {message_queue.qsize()} messages' f'Currently working on:\n"{current_task}"\n\nQueue: {message_queue.qsize()} messages'
) )
else: else:
await update.message.reply_text("Currently idle. No task in progress.") await update.message.reply_text("Currently idle. No task in progress.")
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
global is_processing, current_task global is_processing, current_task
chat_id = str(update.message.chat.id) if not is_authorized(update):
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 return
user_message = update.message.text user_message = update.message.text
user_id = update.effective_user.id if update.effective_user else "unknown" user_id = update.effective_user.id if update.effective_user else "unknown"
@@ -189,7 +180,7 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
if currently_processing: if currently_processing:
sent = await update.message.reply_text( sent = await update.message.reply_text(
"opencode is busy. Your message has been added to the queue.\n" "opencode is busy. Your message has been added to the queue.\n"
"I'll respond when ready. Use /status to check queue position." "I'll respond when ready. Use /status to check queue position."
) )
message_queue.put((user_id, sent.message_id, user_message)) message_queue.put((user_id, sent.message_id, user_message))
@@ -198,14 +189,16 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_message[:50] + "..." if len(user_message) > 50 else user_message user_message[:50] + "..." if len(user_message) > 50 else user_message
) )
await update.message.chat.send_action("typing") await update.message.chat.send_action("typing")
sent = await update.message.reply_text("🔄 Processing...") sent = await update.message.reply_text("Processing...")
with processing_lock: with processing_lock:
is_processing = True is_processing = True
try: try:
reply = send_to_opencode(user_message) loop = asyncio.get_event_loop()
reply = await loop.run_in_executor(None, send_to_opencode, user_message)
await sent.edit_text(reply[:4000]) await sent.edit_text(reply[:4000])
except Exception as e: except Exception:
await sent.edit_text(f"Error: {str(e)}") logger.exception("Error processing message")
await sent.edit_text("An error occurred. Check server logs.")
finally: finally:
with processing_lock: with processing_lock:
is_processing = False is_processing = False
@@ -213,29 +206,33 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = str(update.message.chat.id) if not is_authorized(update):
if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID:
return return
await update.message.reply_text("Voice messages not yet supported. Send text.") await update.message.reply_text("Voice messages not yet supported. Send text.")
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = str(update.message.chat.id) if not is_authorized(update):
if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID:
return return
await update.message.reply_text("File handling not yet supported. Send text.") await update.message.reply_text("File handling not yet supported. Send text.")
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = str(update.message.chat.id) if not is_authorized(update):
if ALLOWED_CHAT_ID and chat_id != ALLOWED_CHAT_ID:
return return
await update.message.reply_text("Image handling not yet supported. Send text.") await update.message.reply_text("Image handling not yet supported. Send text.")
def main(): def main():
if not BOT_TOKEN: if not BOT_TOKEN:
print("Error: TELEGRAM_BOT_TOKEN not set in .env file") logger.error("TELEGRAM_BOT_TOKEN not set in .env file")
return
if not ALLOWED_CHAT_ID:
logger.error(
"TELEGRAM_ALLOWED_CHAT_ID not set in .env file. "
"Refusing to start without access control."
)
return return
app = Application.builder().token(BOT_TOKEN).build() app = Application.builder().token(BOT_TOKEN).build()
@@ -250,9 +247,8 @@ def main():
app.add_handler(MessageHandler(filters.Document.ALL, handle_document)) app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
app.add_handler(MessageHandler(filters.PHOTO, handle_photo)) app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
print(f"opencode-dispatch bot starting...") logger.info("opencode-dispatch bot starting...")
print(f"Connecting to opencode at: {OPENCODE_API_URL}") logger.info("Press Ctrl+C to stop")
print("Press Ctrl+C to stop")
app.run_polling(allowed_updates=Update.ALL_TYPES) app.run_polling(allowed_updates=Update.ALL_TYPES)
-481
View File
@@ -1,481 +0,0 @@
{
"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
@@ -1,24 +0,0 @@
{
"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 -3
View File
@@ -1,3 +1,3 @@
python-telegram-bot==21.6 python-telegram-bot==22.7
requests==2.32.3 requests==2.33.0
python-dotenv==1.0.1 python-dotenv==1.2.2