telegram-opencode-bridge-bot 0.1.8__tar.gz → 0.1.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {telegram_opencode_bridge_bot-0.1.8/telegram_opencode_bridge_bot.egg-info → telegram_opencode_bridge_bot-0.1.9}/PKG-INFO +3 -3
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/README.md +2 -2
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/bot.py +7 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/handlers/commands.py +124 -1
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/handlers/messages.py +249 -235
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/opencode/server.py +55 -35
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/pyproject.toml +1 -1
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9/telegram_opencode_bridge_bot.egg-info}/PKG-INFO +3 -3
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/.env.example +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/MANIFEST.in +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/config.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/handlers/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/list_session_models.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/opencode/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/opencode/client.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/requirements.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/sessions/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/sessions/manager.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/setup.cfg +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/telegram_opencode_bridge_bot.egg-info/SOURCES.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/telegram_opencode_bridge_bot.egg-info/dependency_links.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/telegram_opencode_bridge_bot.egg-info/entry_points.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/telegram_opencode_bridge_bot.egg-info/requires.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/telegram_opencode_bridge_bot.egg-info/top_level.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/utils/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/utils/formatting.py +0 -0
- {telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/utils/security.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telegram-opencode-bridge-bot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: A Telegram bot that bridges messages directly to OpenCode — an AI coding agent running on your machine
|
|
5
5
|
Author-email: MaheshNagabhairava <maheshnagabhirava12345@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -58,7 +58,7 @@ A premium, feature-rich Python bot that bridges your Telegram client directly to
|
|
|
58
58
|
### Option A: Standard PyPI Installation
|
|
59
59
|
Install and run the bot wrapper directly from the terminal:
|
|
60
60
|
```bash
|
|
61
|
-
pip install telegram-opencode-bridge-bot==0.1.
|
|
61
|
+
pip install telegram-opencode-bridge-bot==0.1.9
|
|
62
62
|
telegram-opencode-bot
|
|
63
63
|
```
|
|
64
64
|
> **💡 Tip:** Use the `--env` flag anytime to reconfigure your variables:
|
|
@@ -106,7 +106,7 @@ telegram-opencode-bot
|
|
|
106
106
|
| **`/enable`** | — | Enables real-time streaming of tool calls, shell executions, and file edits. |
|
|
107
107
|
| **`/disable`** | — | Disables streaming; the bot will only report the final LLM response. |
|
|
108
108
|
| **`/share`** | — | Fetches a public, shareable web preview URL of the active conversation. |
|
|
109
|
-
|
|
109
|
+
| **`/update`** | — | Update to a new available version. |
|
|
110
110
|
---
|
|
111
111
|
|
|
112
112
|
## ⚙️ Configuration Variables
|
|
@@ -41,7 +41,7 @@ A premium, feature-rich Python bot that bridges your Telegram client directly to
|
|
|
41
41
|
### Option A: Standard PyPI Installation
|
|
42
42
|
Install and run the bot wrapper directly from the terminal:
|
|
43
43
|
```bash
|
|
44
|
-
pip install telegram-opencode-bridge-bot==0.1.
|
|
44
|
+
pip install telegram-opencode-bridge-bot==0.1.9
|
|
45
45
|
telegram-opencode-bot
|
|
46
46
|
```
|
|
47
47
|
> **💡 Tip:** Use the `--env` flag anytime to reconfigure your variables:
|
|
@@ -89,7 +89,7 @@ telegram-opencode-bot
|
|
|
89
89
|
| **`/enable`** | — | Enables real-time streaming of tool calls, shell executions, and file edits. |
|
|
90
90
|
| **`/disable`** | — | Disables streaming; the bot will only report the final LLM response. |
|
|
91
91
|
| **`/share`** | — | Fetches a public, shareable web preview URL of the active conversation. |
|
|
92
|
-
|
|
92
|
+
| **`/update`** | — | Update to a new available version. |
|
|
93
93
|
---
|
|
94
94
|
|
|
95
95
|
## ⚙️ Configuration Variables
|
|
@@ -60,6 +60,7 @@ from handlers.commands import (
|
|
|
60
60
|
enable_command,
|
|
61
61
|
disable_command,
|
|
62
62
|
history_command,
|
|
63
|
+
update_command,
|
|
63
64
|
set_bot_commands,
|
|
64
65
|
callback_handler,
|
|
65
66
|
)
|
|
@@ -211,6 +212,10 @@ def build_authorized_handlers(authorizer: UserAuthorizer, rate_limiter: RateLimi
|
|
|
211
212
|
async def _history(update, context):
|
|
212
213
|
await history_command(update, context)
|
|
213
214
|
|
|
215
|
+
@authorized(authorizer, rate_limiter)
|
|
216
|
+
async def _update(update, context):
|
|
217
|
+
await update_command(update, context)
|
|
218
|
+
|
|
214
219
|
@authorized(authorizer)
|
|
215
220
|
async def _callback(update, context):
|
|
216
221
|
await callback_handler(update, context)
|
|
@@ -237,6 +242,7 @@ def build_authorized_handlers(authorizer: UserAuthorizer, rate_limiter: RateLimi
|
|
|
237
242
|
"enable": _enable,
|
|
238
243
|
"disable": _disable,
|
|
239
244
|
"history": _history,
|
|
245
|
+
"update": _update,
|
|
240
246
|
"plan": _plan,
|
|
241
247
|
"build": _build,
|
|
242
248
|
"mode": _mode,
|
|
@@ -550,6 +556,7 @@ def main():
|
|
|
550
556
|
application.add_handler(CommandHandler("enable", handlers["enable"], block=False))
|
|
551
557
|
application.add_handler(CommandHandler("disable", handlers["disable"], block=False))
|
|
552
558
|
application.add_handler(CommandHandler("history", handlers["history"], block=False))
|
|
559
|
+
application.add_handler(CommandHandler("update", handlers["update"], block=False))
|
|
553
560
|
application.add_handler(CommandHandler("mode", handlers["mode"], block=False))
|
|
554
561
|
application.add_handler(CommandHandler("plan", handlers["plan"], block=False))
|
|
555
562
|
application.add_handler(CommandHandler("build", handlers["build"], block=False))
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/handlers/commands.py
RENAMED
|
@@ -69,7 +69,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
|
|
69
69
|
"/build — Switch to build mode (read, write, execute)\n"
|
|
70
70
|
"/share — Share current session (get public URL)\n"
|
|
71
71
|
"/status — Show bot & connection status\n"
|
|
72
|
-
"/id — Show your Telegram user ID\n
|
|
72
|
+
"/id — Show your Telegram user ID\n"
|
|
73
|
+
"/update — Check and install updates\n\n"
|
|
73
74
|
"<b>💡 Tips:</b>\n"
|
|
74
75
|
"• Just type normally to chat with OpenCode\n"
|
|
75
76
|
"• Use <code>@filename</code> in prompts to reference files\n"
|
|
@@ -1158,6 +1159,127 @@ async def history_command(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
1158
1159
|
await asyncio.sleep(0.5)
|
|
1159
1160
|
|
|
1160
1161
|
|
|
1162
|
+
# ──────────────────────────────────────────────
|
|
1163
|
+
# Command: /update
|
|
1164
|
+
# ──────────────────────────────────────────────
|
|
1165
|
+
async def update_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
1166
|
+
"""Check for and apply bot updates, then restart the process in-place."""
|
|
1167
|
+
import asyncio
|
|
1168
|
+
import os
|
|
1169
|
+
import sys
|
|
1170
|
+
import html
|
|
1171
|
+
from utils.formatting import format_error
|
|
1172
|
+
|
|
1173
|
+
user_id = update.effective_user.id
|
|
1174
|
+
|
|
1175
|
+
# 1. Send initial status message
|
|
1176
|
+
status_msg = await update.message.reply_text(
|
|
1177
|
+
"⏳ <b>Checking for bot updates...</b>",
|
|
1178
|
+
parse_mode="HTML"
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
async def update_status(text: str):
|
|
1182
|
+
try:
|
|
1183
|
+
await status_msg.edit_text(text, parse_mode="HTML")
|
|
1184
|
+
except Exception:
|
|
1185
|
+
await update.message.reply_text(text, parse_mode="HTML")
|
|
1186
|
+
|
|
1187
|
+
is_git = os.path.exists(".git")
|
|
1188
|
+
|
|
1189
|
+
def run_cmd_sync(command: str) -> tuple[int, str, str]:
|
|
1190
|
+
"""Helper to run a shell command synchronously."""
|
|
1191
|
+
import subprocess
|
|
1192
|
+
proc = subprocess.run(
|
|
1193
|
+
command,
|
|
1194
|
+
shell=True,
|
|
1195
|
+
stdout=subprocess.PIPE,
|
|
1196
|
+
stderr=subprocess.PIPE,
|
|
1197
|
+
text=True
|
|
1198
|
+
)
|
|
1199
|
+
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
|
|
1200
|
+
|
|
1201
|
+
async def run_cmd(command: str) -> tuple[int, str, str]:
|
|
1202
|
+
"""Helper to run a shell command in a background thread."""
|
|
1203
|
+
return await asyncio.to_thread(run_cmd_sync, command)
|
|
1204
|
+
|
|
1205
|
+
if is_git:
|
|
1206
|
+
await update_status("🔍 <b>Detected Git installation. Fetching remote changes...</b>")
|
|
1207
|
+
|
|
1208
|
+
# 1. Git fetch
|
|
1209
|
+
code, stdout, stderr = await run_cmd("git fetch origin")
|
|
1210
|
+
if code != 0:
|
|
1211
|
+
await update_status(
|
|
1212
|
+
f"❌ <b>Git fetch failed:</b>\n"
|
|
1213
|
+
f"<pre>{html.escape(stderr or stdout)}</pre>"
|
|
1214
|
+
)
|
|
1215
|
+
return
|
|
1216
|
+
|
|
1217
|
+
# 2. Check diff
|
|
1218
|
+
code_local, local_hash, _ = await run_cmd("git rev-parse HEAD")
|
|
1219
|
+
code_remote, remote_hash, _ = await run_cmd("git rev-parse @{u}")
|
|
1220
|
+
|
|
1221
|
+
if code_local != 0 or code_remote != 0:
|
|
1222
|
+
await update_status("❌ <b>Failed to resolve Git commit hashes.</b>")
|
|
1223
|
+
return
|
|
1224
|
+
|
|
1225
|
+
if local_hash == remote_hash:
|
|
1226
|
+
await update_status(
|
|
1227
|
+
f"✨ <b>Your bot is already up to date!</b>\n"
|
|
1228
|
+
f"Commit: <code>{local_hash[:8]}</code>"
|
|
1229
|
+
)
|
|
1230
|
+
return
|
|
1231
|
+
|
|
1232
|
+
await update_status("🔄 <b>New updates found. Pulling latest code changes...</b>")
|
|
1233
|
+
|
|
1234
|
+
# 3. Git pull
|
|
1235
|
+
code_pull, pull_stdout, pull_stderr = await run_cmd("git pull")
|
|
1236
|
+
if code_pull != 0:
|
|
1237
|
+
await update_status(
|
|
1238
|
+
f"❌ <b>Git pull failed:</b>\n"
|
|
1239
|
+
f"<pre>{html.escape(pull_stderr or pull_stdout)}</pre>"
|
|
1240
|
+
)
|
|
1241
|
+
return
|
|
1242
|
+
|
|
1243
|
+
# 4. Pip install requirements if updated
|
|
1244
|
+
if os.path.exists("requirements.txt"):
|
|
1245
|
+
await update_status("📦 <b>Updating dependencies from requirements.txt...</b>")
|
|
1246
|
+
await run_cmd(f"{sys.executable} -m pip install -r requirements.txt")
|
|
1247
|
+
|
|
1248
|
+
else:
|
|
1249
|
+
# Pip package installation
|
|
1250
|
+
await update_status("🔍 <b>Detected Pip installation. Checking PyPI for updates...</b>")
|
|
1251
|
+
|
|
1252
|
+
# Run pip install --upgrade
|
|
1253
|
+
code, stdout, stderr = await run_cmd(f"{sys.executable} -m pip install --upgrade telegram-opencode-bridge-bot")
|
|
1254
|
+
if code != 0:
|
|
1255
|
+
await update_status(
|
|
1256
|
+
f"❌ <b>Pip upgrade failed:</b>\n"
|
|
1257
|
+
f"<pre>{html.escape(stderr or stdout)}</pre>"
|
|
1258
|
+
)
|
|
1259
|
+
return
|
|
1260
|
+
|
|
1261
|
+
# Check if it was already up to date
|
|
1262
|
+
if "Requirement already satisfied" in stdout and "Successfully installed" not in stdout:
|
|
1263
|
+
await update_status("✨ <b>Your bot is already up to date!</b>")
|
|
1264
|
+
return
|
|
1265
|
+
|
|
1266
|
+
# Trigger restart
|
|
1267
|
+
await update_status("✅ <b>Update complete! Restarting bot in-place...</b>")
|
|
1268
|
+
|
|
1269
|
+
# Wait 1 second to let Telegram send the message completely
|
|
1270
|
+
await asyncio.sleep(1)
|
|
1271
|
+
|
|
1272
|
+
# Close any running connections or servers nicely
|
|
1273
|
+
try:
|
|
1274
|
+
from opencode.server import stop_server
|
|
1275
|
+
await stop_server()
|
|
1276
|
+
except Exception:
|
|
1277
|
+
pass
|
|
1278
|
+
|
|
1279
|
+
# Exec process replacement
|
|
1280
|
+
os.execv(sys.executable, [sys.executable] + sys.argv)
|
|
1281
|
+
|
|
1282
|
+
|
|
1161
1283
|
# ──────────────────────────────────────────────
|
|
1162
1284
|
# Register bot commands for Telegram menu
|
|
1163
1285
|
# ──────────────────────────────────────────────
|
|
@@ -1183,6 +1305,7 @@ async def set_bot_commands(app) -> None:
|
|
|
1183
1305
|
BotCommand("share", "Share current session"),
|
|
1184
1306
|
BotCommand("status", "Bot & connection status"),
|
|
1185
1307
|
BotCommand("id", "Show your Telegram user ID"),
|
|
1308
|
+
BotCommand("update", "Check and install updates"),
|
|
1186
1309
|
]
|
|
1187
1310
|
await app.bot.set_my_commands(commands)
|
|
1188
1311
|
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/handlers/messages.py
RENAMED
|
@@ -450,7 +450,9 @@ async def _listen_and_stream_events(
|
|
|
450
450
|
is_streaming: bool,
|
|
451
451
|
status_msg_holder = None
|
|
452
452
|
):
|
|
453
|
-
"""Listens to global OpenCode events via SSE and handles tool progress/permission requests.
|
|
453
|
+
"""Listens to global OpenCode events via SSE and handles tool progress/permission requests.
|
|
454
|
+
Includes an automatic reconnect loop with exponential back-off to prevent getting stuck.
|
|
455
|
+
"""
|
|
454
456
|
import aiohttp
|
|
455
457
|
import json
|
|
456
458
|
import html
|
|
@@ -483,261 +485,273 @@ async def _listen_and_stream_events(
|
|
|
483
485
|
except Exception as e:
|
|
484
486
|
logger.debug(f"Failed to update status message: {e}")
|
|
485
487
|
|
|
486
|
-
|
|
488
|
+
retry_delay = 1.0
|
|
489
|
+
while True:
|
|
487
490
|
try:
|
|
488
|
-
async with
|
|
489
|
-
async
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
continue
|
|
491
|
+
async with aiohttp.ClientSession(read_bufsize=100 * 1024 * 1024) as sse_session:
|
|
492
|
+
async with sse_session.get(url, headers={"Accept": "text/event-stream"}) as resp:
|
|
493
|
+
# Connection successful, reset retry delay
|
|
494
|
+
retry_delay = 1.0
|
|
493
495
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
payload = event_obj.get("payload", {})
|
|
498
|
-
if not isinstance(payload, dict):
|
|
499
|
-
continue
|
|
500
|
-
|
|
501
|
-
properties = payload.get("properties", {})
|
|
502
|
-
if not isinstance(properties, dict):
|
|
496
|
+
async for line in resp.content:
|
|
497
|
+
line_str = line.decode('utf-8').strip()
|
|
498
|
+
if not line_str or not line_str.startswith("data:"):
|
|
503
499
|
continue
|
|
504
500
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
or payload.get("session_id")
|
|
512
|
-
or ""
|
|
513
|
-
)
|
|
514
|
-
if event_session_id != session_id:
|
|
515
|
-
continue
|
|
516
|
-
|
|
517
|
-
event_type = payload.get("type", "")
|
|
518
|
-
|
|
519
|
-
# A. Handle Intermediate Assistant Message Completion (Real-time Streaming)
|
|
520
|
-
if event_type == "message.updated":
|
|
521
|
-
info = properties.get("info", {})
|
|
522
|
-
msg_id = info.get("id")
|
|
523
|
-
role = info.get("role")
|
|
524
|
-
completed = info.get("time", {}).get("completed")
|
|
501
|
+
data_content = line_str[5:].strip()
|
|
502
|
+
try:
|
|
503
|
+
event_obj = json.loads(data_content)
|
|
504
|
+
payload = event_obj.get("payload", {})
|
|
505
|
+
if not isinstance(payload, dict):
|
|
506
|
+
continue
|
|
525
507
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if msg_id not in sent_message_ids:
|
|
529
|
-
sent_message_ids.add(msg_id)
|
|
530
|
-
try:
|
|
531
|
-
oc_client = context.bot_data["opencode_client"]
|
|
532
|
-
messages = await oc_client.list_messages(session_id)
|
|
533
|
-
target_msg = next((m for m in messages if m.get("info", {}).get("id") == msg_id), None)
|
|
534
|
-
if target_msg:
|
|
535
|
-
parts = target_msg.get("parts", [])
|
|
536
|
-
content_text = ""
|
|
537
|
-
if isinstance(parts, list):
|
|
538
|
-
text_parts = [
|
|
539
|
-
p.get("text", "")
|
|
540
|
-
for p in parts
|
|
541
|
-
if isinstance(p, dict) and p.get("type") == "text"
|
|
542
|
-
]
|
|
543
|
-
content_text = "".join(text_parts)
|
|
544
|
-
|
|
545
|
-
if content_text.strip():
|
|
546
|
-
# Delete the old status message at the top
|
|
547
|
-
if status_msg_holder and status_msg_holder[0]:
|
|
548
|
-
try:
|
|
549
|
-
await status_msg_holder[0].delete()
|
|
550
|
-
except Exception:
|
|
551
|
-
pass
|
|
552
|
-
status_msg_holder[0] = None
|
|
553
|
-
|
|
554
|
-
from utils.formatting import format_opencode_response, split_message
|
|
555
|
-
formatted = format_opencode_response(content_text)
|
|
556
|
-
chunks = split_message(formatted, context.bot_data["config"].max_message_length)
|
|
557
|
-
for i, chunk in enumerate(chunks):
|
|
558
|
-
await update.message.reply_text(
|
|
559
|
-
chunk,
|
|
560
|
-
parse_mode="HTML",
|
|
561
|
-
disable_web_page_preview=True,
|
|
562
|
-
)
|
|
563
|
-
if i < len(chunks) - 1:
|
|
564
|
-
await asyncio.sleep(0.5)
|
|
565
|
-
|
|
566
|
-
# Recreate the status indicator at the very bottom
|
|
567
|
-
if status_msg_holder:
|
|
568
|
-
try:
|
|
569
|
-
status_msg_holder[0] = await update.message.reply_text(
|
|
570
|
-
last_status_text[0],
|
|
571
|
-
parse_mode="HTML"
|
|
572
|
-
)
|
|
573
|
-
last_update_time[0] = time.time()
|
|
574
|
-
except Exception as e:
|
|
575
|
-
logger.warning(f"Failed to recreate status message at bottom: {e}")
|
|
576
|
-
except Exception as e:
|
|
577
|
-
logger.warning(f"Failed to stream intermediate message {msg_id}: {e}")
|
|
578
|
-
|
|
579
|
-
# B. Handle Permission Requested Popup (Always Enabled)
|
|
580
|
-
elif event_type == "permission.asked":
|
|
581
|
-
perm_id = properties.get("id") or properties.get("permissionID") or payload.get("id")
|
|
582
|
-
perm_type = properties.get("permission") or properties.get("type") or "execute"
|
|
583
|
-
patterns = properties.get("patterns", [])
|
|
584
|
-
|
|
585
|
-
if not perm_id:
|
|
586
|
-
logger.warning("Received permission.asked event but no permission ID was found.")
|
|
508
|
+
properties = payload.get("properties", {})
|
|
509
|
+
if not isinstance(properties, dict):
|
|
587
510
|
continue
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
"session_id"
|
|
596
|
-
"
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
patterns_text = ""
|
|
600
|
-
if patterns:
|
|
601
|
-
pat_list = "\n".join([f"• <code>{html.escape(str(p))}</code>" for p in patterns])
|
|
602
|
-
patterns_text = f"\n<b>Target Resource(s):</b>\n{pat_list}"
|
|
603
|
-
|
|
604
|
-
tool_name = ""
|
|
605
|
-
tool_info = properties.get("tool", {})
|
|
606
|
-
if isinstance(tool_info, dict):
|
|
607
|
-
tool_name = tool_info.get("name", "")
|
|
608
|
-
if not tool_name:
|
|
609
|
-
tool_name = perm_type
|
|
610
|
-
|
|
611
|
-
msg = (
|
|
612
|
-
f"🛡️ <b>OpenCode Permission Requested</b>\n\n"
|
|
613
|
-
f"The agent is asking for confirmation to use the tool <code>{html.escape(tool_name)}</code>.\n"
|
|
614
|
-
f"{patterns_text}\n\n"
|
|
615
|
-
f"Do you want to allow this operation?"
|
|
511
|
+
|
|
512
|
+
event_session_id = (
|
|
513
|
+
properties.get("sessionID")
|
|
514
|
+
or properties.get("sessionId")
|
|
515
|
+
or properties.get("session_id")
|
|
516
|
+
or payload.get("sessionID")
|
|
517
|
+
or payload.get("sessionId")
|
|
518
|
+
or payload.get("session_id")
|
|
519
|
+
or ""
|
|
616
520
|
)
|
|
521
|
+
if event_session_id != session_id:
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
event_type = payload.get("type", "")
|
|
525
|
+
|
|
526
|
+
# A. Handle Intermediate Assistant Message Completion (Real-time Streaming)
|
|
527
|
+
if event_type == "message.updated":
|
|
528
|
+
info = properties.get("info", {})
|
|
529
|
+
msg_id = info.get("id")
|
|
530
|
+
role = info.get("role")
|
|
531
|
+
completed = info.get("time", {}).get("completed")
|
|
532
|
+
|
|
533
|
+
if role == "assistant" and completed:
|
|
534
|
+
sent_message_ids = context.user_data.setdefault("sent_message_ids", set())
|
|
535
|
+
if msg_id not in sent_message_ids:
|
|
536
|
+
sent_message_ids.add(msg_id)
|
|
537
|
+
try:
|
|
538
|
+
oc_client = context.bot_data["opencode_client"]
|
|
539
|
+
messages = await oc_client.list_messages(session_id)
|
|
540
|
+
target_msg = next((m for m in messages if m.get("info", {}).get("id") == msg_id), None)
|
|
541
|
+
if target_msg:
|
|
542
|
+
parts = target_msg.get("parts", [])
|
|
543
|
+
content_text = ""
|
|
544
|
+
if isinstance(parts, list):
|
|
545
|
+
text_parts = [
|
|
546
|
+
p.get("text", "")
|
|
547
|
+
for p in parts
|
|
548
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
549
|
+
]
|
|
550
|
+
content_text = "".join(text_parts)
|
|
551
|
+
|
|
552
|
+
if content_text.strip():
|
|
553
|
+
# Delete the old status message at the top
|
|
554
|
+
if status_msg_holder and status_msg_holder[0]:
|
|
555
|
+
try:
|
|
556
|
+
await status_msg_holder[0].delete()
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
status_msg_holder[0] = None
|
|
560
|
+
|
|
561
|
+
from utils.formatting import format_opencode_response, split_message
|
|
562
|
+
formatted = format_opencode_response(content_text)
|
|
563
|
+
chunks = split_message(formatted, context.bot_data["config"].max_message_length)
|
|
564
|
+
for i, chunk in enumerate(chunks):
|
|
565
|
+
await update.message.reply_text(
|
|
566
|
+
chunk,
|
|
567
|
+
parse_mode="HTML",
|
|
568
|
+
disable_web_page_preview=True,
|
|
569
|
+
)
|
|
570
|
+
if i < len(chunks) - 1:
|
|
571
|
+
await asyncio.sleep(0.5)
|
|
572
|
+
|
|
573
|
+
# Recreate the status indicator at the very bottom
|
|
574
|
+
if status_msg_holder:
|
|
575
|
+
try:
|
|
576
|
+
status_msg_holder[0] = await update.message.reply_text(
|
|
577
|
+
last_status_text[0],
|
|
578
|
+
parse_mode="HTML"
|
|
579
|
+
)
|
|
580
|
+
last_update_time[0] = time.time()
|
|
581
|
+
except Exception as e:
|
|
582
|
+
logger.warning(f"Failed to recreate status message at bottom: {e}")
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logger.warning(f"Failed to stream intermediate message {msg_id}: {e}")
|
|
585
|
+
|
|
586
|
+
# B. Handle Permission Requested Popup (Always Enabled)
|
|
587
|
+
elif event_type == "permission.asked":
|
|
588
|
+
perm_id = properties.get("id") or properties.get("permissionID") or payload.get("id")
|
|
589
|
+
perm_type = properties.get("permission") or properties.get("type") or "execute"
|
|
590
|
+
patterns = properties.get("patterns", [])
|
|
591
|
+
|
|
592
|
+
if not perm_id:
|
|
593
|
+
logger.warning("Received permission.asked event but no permission ID was found.")
|
|
594
|
+
continue
|
|
617
595
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
596
|
+
# Register pending permission in-memory lookup to avoid Telegram 64-char callback limit
|
|
597
|
+
if "pending_permissions" not in context.bot_data:
|
|
598
|
+
context.bot_data["pending_permissions"] = {}
|
|
599
|
+
|
|
600
|
+
short_key = uuid.uuid4().hex[:8]
|
|
601
|
+
context.bot_data["pending_permissions"][short_key] = {
|
|
602
|
+
"session_id": session_id,
|
|
603
|
+
"permission_id": perm_id
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
patterns_text = ""
|
|
607
|
+
if patterns:
|
|
608
|
+
pat_list = "\n".join([f"• <code>{html.escape(str(p))}</code>" for p in patterns])
|
|
609
|
+
patterns_text = f"\n<b>Target Resource(s):</b>\n{pat_list}"
|
|
610
|
+
|
|
611
|
+
tool_name = ""
|
|
612
|
+
tool_info = properties.get("tool", {})
|
|
613
|
+
if isinstance(tool_info, dict):
|
|
614
|
+
tool_name = tool_info.get("name", "")
|
|
615
|
+
if not tool_name:
|
|
616
|
+
tool_name = perm_type
|
|
617
|
+
|
|
618
|
+
msg = (
|
|
619
|
+
f"🛡️ <b>OpenCode Permission Requested</b>\n\n"
|
|
620
|
+
f"The agent is asking for confirmation to use the tool <code>{html.escape(tool_name)}</code>.\n"
|
|
621
|
+
f"{patterns_text}\n\n"
|
|
622
|
+
f"Do you want to allow this operation?"
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
keyboard = [
|
|
626
|
+
[
|
|
627
|
+
InlineKeyboardButton("✅ Yes, Allow", callback_data=f"perm:allow:{short_key}"),
|
|
628
|
+
InlineKeyboardButton("❌ No, Deny", callback_data=f"perm:deny:{short_key}")
|
|
629
|
+
]
|
|
622
630
|
]
|
|
623
|
-
]
|
|
624
631
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
632
|
+
await update.message.reply_text(
|
|
633
|
+
msg,
|
|
634
|
+
parse_mode="HTML",
|
|
635
|
+
reply_markup=InlineKeyboardMarkup(keyboard)
|
|
636
|
+
)
|
|
630
637
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
continue
|
|
636
|
-
|
|
637
|
-
part_type = part.get("type", "")
|
|
638
|
-
if part_type == "tool":
|
|
639
|
-
tool_name = part.get("tool", "unknown")
|
|
640
|
-
call_id = part.get("callID", "unknown")
|
|
641
|
-
state = part.get("state", {})
|
|
642
|
-
if not isinstance(state, dict):
|
|
638
|
+
# B. Handle Tool Execution Progress
|
|
639
|
+
elif event_type == "message.part.updated":
|
|
640
|
+
part = properties.get("part", {})
|
|
641
|
+
if not isinstance(part, dict):
|
|
643
642
|
continue
|
|
644
643
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
# ── 1. Update In-Place Status Message (Always Active) ──
|
|
653
|
-
if status in ("pending", "running") and status_msg_holder and status_msg_holder[0]:
|
|
654
|
-
status_text = ""
|
|
655
|
-
if tool_name == "bash":
|
|
656
|
-
cmd = input_data.get("command") or input_data.get("content") or ""
|
|
657
|
-
cmd_truncated = truncate(cmd, 60)
|
|
658
|
-
status_text = f"💻 <b>Running shell command...</b>\n<code>{html.escape(cmd_truncated)}</code>"
|
|
659
|
-
elif tool_name in ("edit", "write", "save"):
|
|
660
|
-
path = input_data.get("path") or input_data.get("target") or input_data.get("filepath") or ""
|
|
661
|
-
path_truncated = truncate(os.path.basename(path) if path else "", 60)
|
|
662
|
-
status_text = f"📝 <b>Modifying file...</b>\n<code>{html.escape(path_truncated)}</code>"
|
|
663
|
-
elif tool_name in ("read", "view", "show"):
|
|
664
|
-
path = input_data.get("path") or input_data.get("target") or input_data.get("filepath") or ""
|
|
665
|
-
path_truncated = truncate(os.path.basename(path) if path else "", 60)
|
|
666
|
-
status_text = f"🔍 <b>Reading file...</b>\n<code>{html.escape(path_truncated)}</code>"
|
|
667
|
-
elif tool_name in ("webfetch", "websearch", "search"):
|
|
668
|
-
query = input_data.get("query") or input_data.get("url") or ""
|
|
669
|
-
query_truncated = truncate(query, 60)
|
|
670
|
-
status_text = f"🌐 <b>Searching web...</b>\n<code>{html.escape(query_truncated)}</code>"
|
|
671
|
-
else:
|
|
672
|
-
status_text = f"⚙️ <b>Executing tool <code>{html.escape(tool_name)}</code>...</b>"
|
|
644
|
+
part_type = part.get("type", "")
|
|
645
|
+
if part_type == "tool":
|
|
646
|
+
tool_name = part.get("tool", "unknown")
|
|
647
|
+
call_id = part.get("callID", "unknown")
|
|
648
|
+
state = part.get("state", {})
|
|
649
|
+
if not isinstance(state, dict):
|
|
650
|
+
continue
|
|
673
651
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
652
|
+
status = state.get("status", "")
|
|
653
|
+
input_data = state.get("input", {})
|
|
654
|
+
output_data = state.get("output", "")
|
|
655
|
+
metadata = state.get("metadata", {})
|
|
656
|
+
if not isinstance(metadata, dict):
|
|
657
|
+
metadata = {}
|
|
658
|
+
|
|
659
|
+
# ── 1. Update In-Place Status Message (Always Active) ──
|
|
660
|
+
if status in ("pending", "running") and status_msg_holder and status_msg_holder[0]:
|
|
661
|
+
status_text = ""
|
|
662
|
+
if tool_name == "bash":
|
|
663
|
+
cmd = input_data.get("command") or input_data.get("content") or ""
|
|
664
|
+
cmd_truncated = truncate(cmd, 60)
|
|
665
|
+
status_text = f"💻 <b>Running shell command...</b>\n<code>{html.escape(cmd_truncated)}</code>"
|
|
666
|
+
elif tool_name in ("edit", "write", "save"):
|
|
667
|
+
path = input_data.get("path") or input_data.get("target") or input_data.get("filepath") or ""
|
|
668
|
+
path_truncated = truncate(os.path.basename(path) if path else "", 60)
|
|
669
|
+
status_text = f"📝 <b>Modifying file...</b>\n<code>{html.escape(path_truncated)}</code>"
|
|
670
|
+
elif tool_name in ("read", "view", "show"):
|
|
671
|
+
path = input_data.get("path") or input_data.get("target") or input_data.get("filepath") or ""
|
|
672
|
+
path_truncated = truncate(os.path.basename(path) if path else "", 60)
|
|
673
|
+
status_text = f"🔍 <b>Reading file...</b>\n<code>{html.escape(path_truncated)}</code>"
|
|
674
|
+
elif tool_name in ("webfetch", "websearch", "search"):
|
|
675
|
+
query = input_data.get("query") or input_data.get("url") or ""
|
|
676
|
+
query_truncated = truncate(query, 60)
|
|
677
|
+
status_text = f"🌐 <b>Searching web...</b>\n<code>{html.escape(query_truncated)}</code>"
|
|
678
|
+
else:
|
|
679
|
+
status_text = f"⚙️ <b>Executing tool <code>{html.escape(tool_name)}</code>...</b>"
|
|
692
680
|
|
|
693
|
-
|
|
694
|
-
f"🛠️ <b>Calling Tool <code>{html.escape(tool_name)}</code></b>{desc_text}\n"
|
|
695
|
-
)
|
|
696
|
-
if args_text:
|
|
697
|
-
msg += f"{args_text}\n"
|
|
698
|
-
|
|
699
|
-
await update.message.reply_text(msg, parse_mode="HTML")
|
|
681
|
+
await update_status(status_text)
|
|
700
682
|
|
|
701
|
-
# 2. Tool
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
output_cleaned = truncate(str(output_data))
|
|
707
|
-
|
|
708
|
-
msg = (
|
|
709
|
-
f"✅ <b>Tool <code>{html.escape(tool_name)}</code> Completed</b> (Exit <code>{exit_code}</code>)\n"
|
|
710
|
-
)
|
|
711
|
-
if output_cleaned.strip():
|
|
712
|
-
msg += f"<pre>{html.escape(output_cleaned)}</pre>"
|
|
713
|
-
else:
|
|
714
|
-
msg += f"<i>(No output returned)</i>"
|
|
683
|
+
# ── 2. Stream Full Tool Logs (Only if is_streaming is True) ──
|
|
684
|
+
if is_streaming:
|
|
685
|
+
# 1. Tool Call Started / Running
|
|
686
|
+
if status in ("pending", "running") and call_id not in notified_calls:
|
|
687
|
+
notified_calls.add(call_id)
|
|
715
688
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
# 3. Tool Failed
|
|
719
|
-
elif status in ("failed", "error") and call_id not in completed_calls:
|
|
720
|
-
completed_calls.add(call_id)
|
|
721
|
-
|
|
722
|
-
output_cleaned = truncate(str(output_data))
|
|
723
|
-
|
|
724
|
-
msg = (
|
|
725
|
-
f"❌ <b>Tool <code>{html.escape(tool_name)}</code> Failed</b>\n"
|
|
726
|
-
)
|
|
727
|
-
if output_cleaned.strip():
|
|
728
|
-
msg += f"<pre>{html.escape(output_cleaned)}</pre>"
|
|
729
|
-
else:
|
|
730
|
-
msg += f"<i>(No error description returned)</i>"
|
|
689
|
+
desc = input_data.get("description", "") if isinstance(input_data, dict) else ""
|
|
690
|
+
desc_text = f" — <i>\"{html.escape(desc)}\"</i>" if desc else ""
|
|
731
691
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
692
|
+
# Format arguments
|
|
693
|
+
arg_lines = []
|
|
694
|
+
if isinstance(input_data, dict):
|
|
695
|
+
for k, v in input_data.items():
|
|
696
|
+
if k not in ("description", "content"):
|
|
697
|
+
arg_lines.append(f"<b>{html.escape(str(k))}:</b> {html.escape(truncate(str(v)))}")
|
|
698
|
+
args_text = "\n".join(arg_lines)
|
|
699
|
+
|
|
700
|
+
msg = (
|
|
701
|
+
f"🛠️ <b>Calling Tool <code>{html.escape(tool_name)}</code></b>{desc_text}\n"
|
|
702
|
+
)
|
|
703
|
+
if args_text:
|
|
704
|
+
msg += f"{args_text}\n"
|
|
705
|
+
|
|
706
|
+
await update.message.reply_text(msg, parse_mode="HTML")
|
|
707
|
+
|
|
708
|
+
# 2. Tool Completed
|
|
709
|
+
elif status == "completed" and call_id not in completed_calls:
|
|
710
|
+
completed_calls.add(call_id)
|
|
711
|
+
|
|
712
|
+
exit_code = metadata.get("exit", 0)
|
|
713
|
+
output_cleaned = truncate(str(output_data))
|
|
714
|
+
|
|
715
|
+
msg = (
|
|
716
|
+
f"✅ <b>Tool <code>{html.escape(tool_name)}</code> Completed</b> (Exit <code>{exit_code}</code>)\n"
|
|
717
|
+
)
|
|
718
|
+
if output_cleaned.strip():
|
|
719
|
+
msg += f"<pre>{html.escape(output_cleaned)}</pre>"
|
|
720
|
+
else:
|
|
721
|
+
msg += f"<i>(No output returned)</i>"
|
|
722
|
+
|
|
723
|
+
await update.message.reply_text(msg, parse_mode="HTML")
|
|
724
|
+
|
|
725
|
+
# 3. Tool Failed
|
|
726
|
+
elif status in ("failed", "error") and call_id not in completed_calls:
|
|
727
|
+
completed_calls.add(call_id)
|
|
728
|
+
|
|
729
|
+
output_cleaned = truncate(str(output_data))
|
|
730
|
+
|
|
731
|
+
msg = (
|
|
732
|
+
f"❌ <b>Tool <code>{html.escape(tool_name)}</code> Failed</b>\n"
|
|
733
|
+
)
|
|
734
|
+
if output_cleaned.strip():
|
|
735
|
+
msg += f"<pre>{html.escape(output_cleaned)}</pre>"
|
|
736
|
+
else:
|
|
737
|
+
msg += f"<i>(No error description returned)</i>"
|
|
738
|
+
|
|
739
|
+
await update.message.reply_text(msg, parse_mode="HTML")
|
|
740
|
+
|
|
741
|
+
except Exception as e:
|
|
742
|
+
logger.debug(f"Error parsing SSE event in listener: {e}")
|
|
736
743
|
|
|
737
744
|
except asyncio.CancelledError:
|
|
738
|
-
|
|
745
|
+
logger.debug("SSE streaming task listener cancelled by parent task.")
|
|
746
|
+
break
|
|
739
747
|
except Exception as e:
|
|
740
|
-
|
|
748
|
+
err_name = e or type(e).__name__
|
|
749
|
+
logger.warning(f"Error in SSE streaming task listener: {err_name}. Reconnecting in {retry_delay}s...")
|
|
750
|
+
try:
|
|
751
|
+
await asyncio.sleep(retry_delay)
|
|
752
|
+
except asyncio.CancelledError:
|
|
753
|
+
break
|
|
754
|
+
retry_delay = min(retry_delay * 2, 10.0)
|
|
741
755
|
|
|
742
756
|
|
|
743
757
|
async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/opencode/server.py
RENAMED
|
@@ -41,45 +41,65 @@ async def restart_server(directory: str, port: int = 8080, hostname: str = "127.
|
|
|
41
41
|
await stop_server()
|
|
42
42
|
await asyncio.sleep(1)
|
|
43
43
|
|
|
44
|
-
# 2. Start a new one from the target directory
|
|
45
44
|
binary = get_opencode_binary()
|
|
46
45
|
cmd = [binary, "serve", "--port", str(port), "--hostname", hostname]
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
creationflags = 0x00000200
|
|
55
|
-
|
|
56
|
-
_server_process = subprocess.Popen(
|
|
57
|
-
cmd,
|
|
58
|
-
cwd=directory,
|
|
59
|
-
stdout=subprocess.DEVNULL,
|
|
60
|
-
stderr=subprocess.DEVNULL,
|
|
61
|
-
stdin=subprocess.DEVNULL,
|
|
62
|
-
creationflags=creationflags,
|
|
63
|
-
start_new_session=(platform.system() != "Windows"),
|
|
64
|
-
)
|
|
65
|
-
except Exception as e:
|
|
66
|
-
logger.error(f"Failed to start opencode serve: {e}")
|
|
67
|
-
return False
|
|
68
|
-
|
|
69
|
-
# 3. Wait until the server is reachable (max 30s)
|
|
70
|
-
import aiohttp
|
|
71
|
-
for attempt in range(30):
|
|
72
|
-
await asyncio.sleep(1)
|
|
47
|
+
creationflags = 0
|
|
48
|
+
if platform.system() == "Windows":
|
|
49
|
+
creationflags = 0x00000200 # CREATE_NEW_PROCESS_GROUP
|
|
50
|
+
|
|
51
|
+
for run_attempt in range(1, 3):
|
|
52
|
+
logger.info(f"Starting opencode serve (attempt {run_attempt}/2) from {directory}: {' '.join(cmd)}")
|
|
73
53
|
try:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
54
|
+
_server_process = subprocess.Popen(
|
|
55
|
+
cmd,
|
|
56
|
+
cwd=directory,
|
|
57
|
+
stdout=subprocess.DEVNULL,
|
|
58
|
+
stderr=subprocess.DEVNULL,
|
|
59
|
+
stdin=subprocess.DEVNULL,
|
|
60
|
+
creationflags=creationflags,
|
|
61
|
+
start_new_session=(platform.system() != "Windows"),
|
|
62
|
+
)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Failed to start opencode serve on attempt {run_attempt}: {e}")
|
|
65
|
+
if run_attempt == 2:
|
|
66
|
+
return False
|
|
67
|
+
await asyncio.sleep(2)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
# 3. Wait until the server is reachable (max 15 attempts, 1s sleep + 1s timeout)
|
|
71
|
+
import aiohttp
|
|
72
|
+
for attempt in range(15):
|
|
73
|
+
# Check if the process exited prematurely
|
|
74
|
+
poll_code = _server_process.poll()
|
|
75
|
+
if poll_code is not None:
|
|
76
|
+
logger.warning(f"opencode serve process exited prematurely with code {poll_code} on attempt {run_attempt}")
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
await asyncio.sleep(1)
|
|
80
|
+
try:
|
|
81
|
+
async with aiohttp.ClientSession() as session:
|
|
82
|
+
async with session.get(f"http://{hostname}:{port}/session", timeout=aiohttp.ClientTimeout(total=1)) as resp:
|
|
83
|
+
if resp.status < 500:
|
|
84
|
+
logger.info(f"opencode serve is up after {attempt + 1}s (pid={_server_process.pid})")
|
|
85
|
+
return True
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# Cleanup failed process
|
|
90
|
+
if _server_process:
|
|
91
|
+
try:
|
|
92
|
+
_server_process.terminate()
|
|
93
|
+
_server_process.wait(timeout=2)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
_server_process = None
|
|
97
|
+
|
|
98
|
+
if run_attempt == 1:
|
|
99
|
+
logger.warning("First startup attempt failed or port was busy. Retrying in 2 seconds...")
|
|
100
|
+
await asyncio.sleep(2)
|
|
101
|
+
|
|
102
|
+
logger.error("opencode serve did not become reachable after 2 attempts")
|
|
83
103
|
return False
|
|
84
104
|
|
|
85
105
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "telegram-opencode-bridge-bot"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "A Telegram bot that bridges messages directly to OpenCode — an AI coding agent running on your machine"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telegram-opencode-bridge-bot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: A Telegram bot that bridges messages directly to OpenCode — an AI coding agent running on your machine
|
|
5
5
|
Author-email: MaheshNagabhairava <maheshnagabhirava12345@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -58,7 +58,7 @@ A premium, feature-rich Python bot that bridges your Telegram client directly to
|
|
|
58
58
|
### Option A: Standard PyPI Installation
|
|
59
59
|
Install and run the bot wrapper directly from the terminal:
|
|
60
60
|
```bash
|
|
61
|
-
pip install telegram-opencode-bridge-bot==0.1.
|
|
61
|
+
pip install telegram-opencode-bridge-bot==0.1.9
|
|
62
62
|
telegram-opencode-bot
|
|
63
63
|
```
|
|
64
64
|
> **💡 Tip:** Use the `--env` flag anytime to reconfigure your variables:
|
|
@@ -106,7 +106,7 @@ telegram-opencode-bot
|
|
|
106
106
|
| **`/enable`** | — | Enables real-time streaming of tool calls, shell executions, and file edits. |
|
|
107
107
|
| **`/disable`** | — | Disables streaming; the bot will only report the final LLM response. |
|
|
108
108
|
| **`/share`** | — | Fetches a public, shareable web preview URL of the active conversation. |
|
|
109
|
-
|
|
109
|
+
| **`/update`** | — | Update to a new available version. |
|
|
110
110
|
---
|
|
111
111
|
|
|
112
112
|
## ⚙️ Configuration Variables
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/handlers/__init__.py
RENAMED
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/list_session_models.py
RENAMED
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/opencode/__init__.py
RENAMED
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/opencode/client.py
RENAMED
|
File without changes
|
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/sessions/__init__.py
RENAMED
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/sessions/manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.8 → telegram_opencode_bridge_bot-0.1.9}/utils/formatting.py
RENAMED
|
File without changes
|
|
File without changes
|