telegram-opencode-bridge-bot 0.1.6__tar.gz → 0.1.7__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.6/telegram_opencode_bridge_bot.egg-info → telegram_opencode_bridge_bot-0.1.7}/PKG-INFO +2 -2
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/README.md +1 -1
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/bot.py +65 -5
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/handlers/commands.py +3 -1
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/handlers/messages.py +221 -41
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/opencode/client.py +14 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/opencode/server.py +19 -16
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/pyproject.toml +1 -1
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/sessions/manager.py +31 -1
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7/telegram_opencode_bridge_bot.egg-info}/PKG-INFO +2 -2
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/utils/formatting.py +151 -1
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/.env.example +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/MANIFEST.in +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/config.py +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/handlers/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/list_session_models.py +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/opencode/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/requirements.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/sessions/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/setup.cfg +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/telegram_opencode_bridge_bot.egg-info/SOURCES.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/telegram_opencode_bridge_bot.egg-info/dependency_links.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/telegram_opencode_bridge_bot.egg-info/entry_points.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/telegram_opencode_bridge_bot.egg-info/requires.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/telegram_opencode_bridge_bot.egg-info/top_level.txt +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/utils/__init__.py +0 -0
- {telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/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.7
|
|
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
|
|
@@ -47,7 +47,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
|
|
|
47
47
|
|
|
48
48
|
### Quick Installation:
|
|
49
49
|
```bash
|
|
50
|
-
pip install telegram-opencode-bridge-bot==0.1.
|
|
50
|
+
pip install telegram-opencode-bridge-bot==0.1.7
|
|
51
51
|
telegram-opencode-bot
|
|
52
52
|
telegram-opencode-bot --env (use --env flag if u want to re-configure later anytime)
|
|
53
53
|
```
|
|
@@ -30,7 +30,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
|
|
|
30
30
|
|
|
31
31
|
### Quick Installation:
|
|
32
32
|
```bash
|
|
33
|
-
pip install telegram-opencode-bridge-bot==0.1.
|
|
33
|
+
pip install telegram-opencode-bridge-bot==0.1.7
|
|
34
34
|
telegram-opencode-bot
|
|
35
35
|
telegram-opencode-bot --env (use --env flag if u want to re-configure later anytime)
|
|
36
36
|
```
|
|
@@ -19,6 +19,10 @@ import logging
|
|
|
19
19
|
import sys
|
|
20
20
|
import os
|
|
21
21
|
|
|
22
|
+
# Switch to Selector Event Loop on Windows for robust signal handling and clean shutdowns
|
|
23
|
+
if sys.platform == 'win32':
|
|
24
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
25
|
+
|
|
22
26
|
from telegram import Update
|
|
23
27
|
from telegram.ext import (
|
|
24
28
|
ApplicationBuilder,
|
|
@@ -67,6 +71,43 @@ logging.basicConfig(
|
|
|
67
71
|
)
|
|
68
72
|
logger = logging.getLogger("opencode-telegram-bot")
|
|
69
73
|
|
|
74
|
+
_lock_file = None
|
|
75
|
+
|
|
76
|
+
def acquire_bot_lock():
|
|
77
|
+
"""Acquire an exclusive lock file to prevent multiple instances from running concurrently."""
|
|
78
|
+
global _lock_file
|
|
79
|
+
lock_path = os.path.join(os.path.abspath("."), "bot.lock")
|
|
80
|
+
try:
|
|
81
|
+
_lock_file = open(lock_path, "w")
|
|
82
|
+
if os.name == 'nt':
|
|
83
|
+
import msvcrt
|
|
84
|
+
try:
|
|
85
|
+
_lock_file.seek(0)
|
|
86
|
+
msvcrt.locking(_lock_file.fileno(), msvcrt.LK_NBLCK, 1)
|
|
87
|
+
_lock_file.write(str(os.getpid()))
|
|
88
|
+
_lock_file.flush()
|
|
89
|
+
except (OSError, IOError):
|
|
90
|
+
print("\n" + "="*65)
|
|
91
|
+
print("❌ ERROR: Another instance of the Telegram bot is already running!")
|
|
92
|
+
print("Please close the other terminal or kill the stray Python process.")
|
|
93
|
+
print("="*65 + "\n")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
else:
|
|
96
|
+
import fcntl
|
|
97
|
+
try:
|
|
98
|
+
fcntl.flock(_lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
99
|
+
_lock_file.write(str(os.getpid()))
|
|
100
|
+
_lock_file.flush()
|
|
101
|
+
except (OSError, IOError):
|
|
102
|
+
print("\n" + "="*65)
|
|
103
|
+
print("❌ ERROR: Another instance of the Telegram bot is already running!")
|
|
104
|
+
print("Please close the other terminal or kill the stray Python process.")
|
|
105
|
+
print("="*65 + "\n")
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.warning(f"Could not acquire bot lock: {e}")
|
|
109
|
+
|
|
110
|
+
|
|
70
111
|
|
|
71
112
|
class RetryingHTTPXRequest(HTTPXRequest):
|
|
72
113
|
"""Custom HTTPXRequest that automatically retries failed requests on connection timeouts/errors."""
|
|
@@ -198,8 +239,17 @@ def build_authorized_handlers(authorizer: UserAuthorizer, rate_limiter: RateLimi
|
|
|
198
239
|
|
|
199
240
|
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
200
241
|
"""Log the error and send a Telegram message to notify the user."""
|
|
201
|
-
#
|
|
202
|
-
|
|
242
|
+
# Suppress full traceback for common transient network / timeout errors to keep logs clean
|
|
243
|
+
from telegram.error import NetworkError, TimedOut
|
|
244
|
+
|
|
245
|
+
err = context.error
|
|
246
|
+
err_str = str(err).lower()
|
|
247
|
+
if isinstance(err, (NetworkError, TimedOut)) or "httpx" in err_str or "httpcore" in err_str or "read error" in err_str:
|
|
248
|
+
logger.warning(f"📡 Transient Telegram network/timeout error: {err}")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Log unexpected errors with traceback
|
|
252
|
+
logger.error("Exception while handling an update:", exc_info=err)
|
|
203
253
|
|
|
204
254
|
# Notify the user if the update is a Telegram Update with a message
|
|
205
255
|
if isinstance(update, Update) and update.effective_message:
|
|
@@ -233,19 +283,25 @@ async def post_shutdown(application) -> None:
|
|
|
233
283
|
# Stop the background opencode server process if running
|
|
234
284
|
try:
|
|
235
285
|
from opencode.server import stop_server
|
|
236
|
-
await stop_server()
|
|
286
|
+
await asyncio.wait_for(stop_server(), timeout=8.0)
|
|
237
287
|
except Exception as e:
|
|
238
288
|
logger.warning(f"Failed to stop background OpenCode server: {e}")
|
|
239
289
|
|
|
240
290
|
# Close session manager DB
|
|
241
291
|
session_mgr = application.bot_data.get("session_manager")
|
|
242
292
|
if session_mgr:
|
|
243
|
-
|
|
293
|
+
try:
|
|
294
|
+
await asyncio.wait_for(session_mgr.close(), timeout=3.0)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.warning(f"Failed to close session manager: {e}")
|
|
244
297
|
|
|
245
298
|
# Close HTTP client
|
|
246
299
|
oc_client = application.bot_data.get("opencode_client")
|
|
247
300
|
if oc_client:
|
|
248
|
-
|
|
301
|
+
try:
|
|
302
|
+
await asyncio.wait_for(oc_client.close(), timeout=3.0)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning(f"Failed to close HTTP client: {e}")
|
|
249
305
|
|
|
250
306
|
logger.info("Goodbye!")
|
|
251
307
|
|
|
@@ -412,6 +468,9 @@ def main():
|
|
|
412
468
|
logger.error("Copy .env.example → .env and fill in your values.")
|
|
413
469
|
sys.exit(1)
|
|
414
470
|
|
|
471
|
+
# ── Acquire exclusive bot lock to prevent concurrent instances ───────
|
|
472
|
+
acquire_bot_lock()
|
|
473
|
+
|
|
415
474
|
logger.info("=" * 50)
|
|
416
475
|
logger.info(" OpenCode Telegram Bot")
|
|
417
476
|
logger.info("=" * 50)
|
|
@@ -447,6 +506,7 @@ def main():
|
|
|
447
506
|
ApplicationBuilder()
|
|
448
507
|
.token(config.telegram_bot_token)
|
|
449
508
|
.request(request)
|
|
509
|
+
.get_updates_request(request)
|
|
450
510
|
.post_init(post_init)
|
|
451
511
|
.post_shutdown(post_shutdown)
|
|
452
512
|
.build()
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/handlers/commands.py
RENAMED
|
@@ -828,7 +828,9 @@ async def execute_project_switch(update_or_query, context: ContextTypes.DEFAULT_
|
|
|
828
828
|
if not session_id:
|
|
829
829
|
raise ValueError("Response did not contain a session ID.")
|
|
830
830
|
|
|
831
|
-
|
|
831
|
+
# Fetch user preferred model, falling back to config model
|
|
832
|
+
preferred_model = await session_mgr.get_user_preferred_model(user_id, config.opencode_model)
|
|
833
|
+
await session_mgr.set_active_session(user_id, session_id, preferred_model, work_dir=target_path)
|
|
832
834
|
|
|
833
835
|
await update_status(
|
|
834
836
|
f"🔄 <b>Switched project to:</b> <code>{html.escape(target_folder)}</code>\n"
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/handlers/messages.py
RENAMED
|
@@ -146,6 +146,8 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
146
146
|
parse_mode="HTML"
|
|
147
147
|
)
|
|
148
148
|
|
|
149
|
+
status_msg_holder = [status_msg]
|
|
150
|
+
|
|
149
151
|
# Always spawn the SSE event stream listener so we can handle interactive permission prompts
|
|
150
152
|
# (e.g. for sensitive files like .env) even if the user has disabled regular tool-call progress.
|
|
151
153
|
sse_task = asyncio.create_task(
|
|
@@ -155,14 +157,24 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
155
157
|
session_id=session_id,
|
|
156
158
|
server_url=config.opencode_server_url,
|
|
157
159
|
is_streaming=bool(is_streaming == 1),
|
|
158
|
-
|
|
160
|
+
status_msg_holder=status_msg_holder
|
|
159
161
|
)
|
|
160
162
|
)
|
|
161
163
|
|
|
162
164
|
typing_task = asyncio.create_task(
|
|
163
165
|
_keep_typing(update, config.response_timeout)
|
|
164
166
|
)
|
|
167
|
+
before_ids = set()
|
|
168
|
+
sent_message_ids = context.user_data.setdefault("sent_message_ids", set())
|
|
169
|
+
sent_message_ids.clear()
|
|
165
170
|
try:
|
|
171
|
+
# Fetch message IDs before sending the prompt
|
|
172
|
+
try:
|
|
173
|
+
before_messages = await oc_client.list_messages(session_id)
|
|
174
|
+
before_ids = {m.get("info", {}).get("id") for m in before_messages if m.get("info", {}).get("id")}
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.warning(f"Failed to fetch messages before prompt: {e}")
|
|
177
|
+
|
|
166
178
|
session_info = await session_mgr.get_session_info(user_id)
|
|
167
179
|
session_model = (session_info or {}).get("model", config.opencode_model) or config.opencode_model
|
|
168
180
|
|
|
@@ -276,9 +288,9 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
276
288
|
if sse_task:
|
|
277
289
|
sse_task.cancel()
|
|
278
290
|
# Clean up by deleting the temporary live phase status message
|
|
279
|
-
if
|
|
291
|
+
if status_msg_holder and status_msg_holder[0]:
|
|
280
292
|
try:
|
|
281
|
-
await
|
|
293
|
+
await status_msg_holder[0].delete()
|
|
282
294
|
except Exception:
|
|
283
295
|
pass
|
|
284
296
|
|
|
@@ -286,40 +298,78 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
286
298
|
await session_mgr.increment_message_count(user_id, prompt=message_text)
|
|
287
299
|
|
|
288
300
|
# ── 5. Format and send response ───────────────────────
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
# Fetch messages after prompt completes to get all multi-step assistant messages
|
|
302
|
+
response_texts = []
|
|
303
|
+
try:
|
|
304
|
+
after_messages = await oc_client.list_messages(session_id)
|
|
305
|
+
new_messages = [
|
|
306
|
+
m for m in after_messages
|
|
307
|
+
if m.get("info", {}).get("id") not in before_ids
|
|
308
|
+
and m.get("info", {}).get("id") not in sent_message_ids
|
|
309
|
+
and m.get("info", {}).get("role") == "assistant"
|
|
310
|
+
]
|
|
311
|
+
for m in new_messages:
|
|
312
|
+
parts = m.get("parts", [])
|
|
313
|
+
content_text = ""
|
|
314
|
+
if isinstance(parts, list):
|
|
315
|
+
text_parts = [
|
|
316
|
+
p.get("text", "")
|
|
317
|
+
for p in parts
|
|
318
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
319
|
+
]
|
|
320
|
+
content_text = "".join(text_parts)
|
|
321
|
+
if content_text.strip():
|
|
322
|
+
response_texts.append(content_text)
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.warning(f"Failed to fetch messages after prompt: {e}")
|
|
325
|
+
|
|
326
|
+
# Fallback to standard response if no intermediate texts were retrieved
|
|
327
|
+
all_responses = response_texts if response_texts else ([response_text] if response_text else [])
|
|
328
|
+
|
|
329
|
+
if not all_responses:
|
|
330
|
+
if response_text == "ABORTED":
|
|
331
|
+
return
|
|
332
|
+
# Send a user-friendly status message to prevent getting stuck silently
|
|
333
|
+
await update.message.reply_text(
|
|
334
|
+
"ℹ️ <b>OpenCode finished execution.</b>\n<i>(No conversational text response was returned)</i>",
|
|
335
|
+
parse_mode="HTML"
|
|
336
|
+
)
|
|
291
337
|
return
|
|
292
338
|
|
|
293
|
-
|
|
294
|
-
|
|
339
|
+
for resp in all_responses:
|
|
340
|
+
if not resp or resp == "ABORTED":
|
|
341
|
+
continue
|
|
295
342
|
|
|
296
|
-
|
|
297
|
-
|
|
343
|
+
# Format OpenCode output for Telegram
|
|
344
|
+
formatted = format_opencode_response(resp)
|
|
298
345
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
parse_mode="HTML",
|
|
304
|
-
disable_web_page_preview=True,
|
|
305
|
-
)
|
|
306
|
-
except Exception as e:
|
|
307
|
-
# If HTML parsing fails, try sending as plain text
|
|
308
|
-
logger.warning(f"HTML parse failed for chunk {i+1}, falling back to plain text: {e}")
|
|
346
|
+
# Split into chunks if too long
|
|
347
|
+
chunks = split_message(formatted, config.max_message_length)
|
|
348
|
+
|
|
349
|
+
for i, chunk in enumerate(chunks):
|
|
309
350
|
try:
|
|
310
|
-
# Strip HTML tags for plain text fallback
|
|
311
|
-
import re
|
|
312
|
-
plain = re.sub(r'<[^>]+>', '', chunk)
|
|
313
351
|
await update.message.reply_text(
|
|
314
|
-
|
|
352
|
+
chunk,
|
|
353
|
+
parse_mode="HTML",
|
|
315
354
|
disable_web_page_preview=True,
|
|
316
355
|
)
|
|
317
|
-
except Exception as
|
|
318
|
-
|
|
356
|
+
except Exception as e:
|
|
357
|
+
# If HTML parsing fails, try sending as plain text
|
|
358
|
+
logger.warning(f"HTML parse failed for chunk {i+1}, falling back to plain text: {e}")
|
|
359
|
+
try:
|
|
360
|
+
# Strip HTML tags for plain text fallback
|
|
361
|
+
import re
|
|
362
|
+
plain = re.sub(r'<[^>]+>', '', chunk)
|
|
363
|
+
await update.message.reply_text(
|
|
364
|
+
plain,
|
|
365
|
+
disable_web_page_preview=True,
|
|
366
|
+
)
|
|
367
|
+
except Exception as e2:
|
|
368
|
+
logger.error(f"Failed to send chunk {i+1} even as plain text: {e2}")
|
|
319
369
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
370
|
+
# Small delay between chunks to respect rate limits
|
|
371
|
+
if i < len(chunks) - 1:
|
|
372
|
+
await asyncio.sleep(0.5)
|
|
323
373
|
|
|
324
374
|
|
|
325
375
|
async def _create_session(oc_client, user_id, session_mgr, config):
|
|
@@ -339,7 +389,10 @@ async def _create_session(oc_client, user_id, session_mgr, config):
|
|
|
339
389
|
if not session_id:
|
|
340
390
|
raise ValueError(f"OpenCode server response did not contain a session ID: {result}")
|
|
341
391
|
|
|
342
|
-
|
|
392
|
+
# Fetch user preferred model, falling back to config model
|
|
393
|
+
preferred_model = await session_mgr.get_user_preferred_model(user_id, config.opencode_model)
|
|
394
|
+
|
|
395
|
+
await session_mgr.set_active_session(user_id, session_id, preferred_model, work_dir=work_dir)
|
|
343
396
|
return session_id
|
|
344
397
|
|
|
345
398
|
|
|
@@ -382,7 +435,7 @@ async def _listen_and_stream_events(
|
|
|
382
435
|
session_id: str,
|
|
383
436
|
server_url: str,
|
|
384
437
|
is_streaming: bool,
|
|
385
|
-
|
|
438
|
+
status_msg_holder = None
|
|
386
439
|
):
|
|
387
440
|
"""Listens to global OpenCode events via SSE and handles tool progress/permission requests."""
|
|
388
441
|
import aiohttp
|
|
@@ -396,6 +449,7 @@ async def _listen_and_stream_events(
|
|
|
396
449
|
notified_calls = set()
|
|
397
450
|
completed_calls = set()
|
|
398
451
|
last_update_time = [0.0]
|
|
452
|
+
last_status_text = ["🧠 <b>Thinking...</b>\n<i>Analyzing request and preparing a plan...</i>"]
|
|
399
453
|
|
|
400
454
|
def truncate(text, max_len=500):
|
|
401
455
|
if not text:
|
|
@@ -407,10 +461,11 @@ async def _listen_and_stream_events(
|
|
|
407
461
|
|
|
408
462
|
async def update_status(text: str):
|
|
409
463
|
now = time.time()
|
|
464
|
+
last_status_text[0] = text
|
|
410
465
|
# Throttling to respect Telegram API rate limits (minimum 1.5 seconds between message edits)
|
|
411
|
-
if
|
|
466
|
+
if status_msg_holder and status_msg_holder[0] and (now - last_update_time[0] >= 1.5):
|
|
412
467
|
try:
|
|
413
|
-
await
|
|
468
|
+
await status_msg_holder[0].edit_text(text, parse_mode="HTML")
|
|
414
469
|
last_update_time[0] = now
|
|
415
470
|
except Exception as e:
|
|
416
471
|
logger.debug(f"Failed to update status message: {e}")
|
|
@@ -434,14 +489,82 @@ async def _listen_and_stream_events(
|
|
|
434
489
|
if not isinstance(properties, dict):
|
|
435
490
|
continue
|
|
436
491
|
|
|
437
|
-
event_session_id =
|
|
492
|
+
event_session_id = (
|
|
493
|
+
properties.get("sessionID")
|
|
494
|
+
or properties.get("sessionId")
|
|
495
|
+
or properties.get("session_id")
|
|
496
|
+
or payload.get("sessionID")
|
|
497
|
+
or payload.get("sessionId")
|
|
498
|
+
or payload.get("session_id")
|
|
499
|
+
or ""
|
|
500
|
+
)
|
|
438
501
|
if event_session_id != session_id:
|
|
439
502
|
continue
|
|
440
503
|
|
|
441
504
|
event_type = payload.get("type", "")
|
|
442
505
|
|
|
443
|
-
# A. Handle
|
|
444
|
-
if event_type == "
|
|
506
|
+
# A. Handle Intermediate Assistant Message Completion (Real-time Streaming)
|
|
507
|
+
if event_type == "message.updated":
|
|
508
|
+
info = properties.get("info", {})
|
|
509
|
+
msg_id = info.get("id")
|
|
510
|
+
role = info.get("role")
|
|
511
|
+
completed = info.get("time", {}).get("completed")
|
|
512
|
+
|
|
513
|
+
if role == "assistant" and completed:
|
|
514
|
+
sent_message_ids = context.user_data.setdefault("sent_message_ids", set())
|
|
515
|
+
if msg_id not in sent_message_ids:
|
|
516
|
+
sent_message_ids.add(msg_id)
|
|
517
|
+
try:
|
|
518
|
+
oc_client = context.bot_data["opencode_client"]
|
|
519
|
+
messages = await oc_client.list_messages(session_id)
|
|
520
|
+
target_msg = next((m for m in messages if m.get("info", {}).get("id") == msg_id), None)
|
|
521
|
+
if target_msg:
|
|
522
|
+
parts = target_msg.get("parts", [])
|
|
523
|
+
content_text = ""
|
|
524
|
+
if isinstance(parts, list):
|
|
525
|
+
text_parts = [
|
|
526
|
+
p.get("text", "")
|
|
527
|
+
for p in parts
|
|
528
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
529
|
+
]
|
|
530
|
+
content_text = "".join(text_parts)
|
|
531
|
+
|
|
532
|
+
if content_text.strip():
|
|
533
|
+
# Delete the old status message at the top
|
|
534
|
+
if status_msg_holder and status_msg_holder[0]:
|
|
535
|
+
try:
|
|
536
|
+
await status_msg_holder[0].delete()
|
|
537
|
+
except Exception:
|
|
538
|
+
pass
|
|
539
|
+
status_msg_holder[0] = None
|
|
540
|
+
|
|
541
|
+
from utils.formatting import format_opencode_response, split_message
|
|
542
|
+
formatted = format_opencode_response(content_text)
|
|
543
|
+
chunks = split_message(formatted, context.bot_data["config"].max_message_length)
|
|
544
|
+
for i, chunk in enumerate(chunks):
|
|
545
|
+
await update.message.reply_text(
|
|
546
|
+
chunk,
|
|
547
|
+
parse_mode="HTML",
|
|
548
|
+
disable_web_page_preview=True,
|
|
549
|
+
)
|
|
550
|
+
if i < len(chunks) - 1:
|
|
551
|
+
await asyncio.sleep(0.5)
|
|
552
|
+
|
|
553
|
+
# Recreate the status indicator at the very bottom
|
|
554
|
+
if status_msg_holder:
|
|
555
|
+
try:
|
|
556
|
+
status_msg_holder[0] = await update.message.reply_text(
|
|
557
|
+
last_status_text[0],
|
|
558
|
+
parse_mode="HTML"
|
|
559
|
+
)
|
|
560
|
+
last_update_time[0] = time.time()
|
|
561
|
+
except Exception as e:
|
|
562
|
+
logger.warning(f"Failed to recreate status message at bottom: {e}")
|
|
563
|
+
except Exception as e:
|
|
564
|
+
logger.warning(f"Failed to stream intermediate message {msg_id}: {e}")
|
|
565
|
+
|
|
566
|
+
# B. Handle Permission Requested Popup (Always Enabled)
|
|
567
|
+
elif event_type == "permission.asked":
|
|
445
568
|
perm_id = properties.get("id") or properties.get("permissionID") or payload.get("id")
|
|
446
569
|
perm_type = properties.get("permission") or properties.get("type") or "execute"
|
|
447
570
|
patterns = properties.get("patterns", [])
|
|
@@ -514,7 +637,7 @@ async def _listen_and_stream_events(
|
|
|
514
637
|
metadata = {}
|
|
515
638
|
|
|
516
639
|
# ── 1. Update In-Place Status Message (Always Active) ──
|
|
517
|
-
if status in ("pending", "running") and
|
|
640
|
+
if status in ("pending", "running") and status_msg_holder and status_msg_holder[0]:
|
|
518
641
|
status_text = ""
|
|
519
642
|
if tool_name == "bash":
|
|
520
643
|
cmd = input_data.get("command") or input_data.get("content") or ""
|
|
@@ -734,6 +857,8 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
734
857
|
"🧠 <b>Thinking...</b>\n<i>Analyzing request and preparing a plan...</i>",
|
|
735
858
|
parse_mode="HTML"
|
|
736
859
|
)
|
|
860
|
+
|
|
861
|
+
status_msg_holder = [status_msg]
|
|
737
862
|
|
|
738
863
|
sse_task = asyncio.create_task(
|
|
739
864
|
_listen_and_stream_events(
|
|
@@ -742,7 +867,7 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
742
867
|
session_id=session_id,
|
|
743
868
|
server_url=config.opencode_server_url,
|
|
744
869
|
is_streaming=bool(is_streaming == 1),
|
|
745
|
-
|
|
870
|
+
status_msg_holder=status_msg_holder
|
|
746
871
|
)
|
|
747
872
|
)
|
|
748
873
|
|
|
@@ -750,10 +875,20 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
750
875
|
_keep_typing(update, config.response_timeout)
|
|
751
876
|
)
|
|
752
877
|
|
|
878
|
+
before_ids = set()
|
|
879
|
+
sent_message_ids = context.user_data.setdefault("sent_message_ids", set())
|
|
880
|
+
sent_message_ids.clear()
|
|
753
881
|
try:
|
|
754
882
|
session_info = await session_mgr.get_session_info(user_id)
|
|
755
883
|
session_model = (session_info or {}).get("model", config.opencode_model) or config.opencode_model
|
|
756
884
|
|
|
885
|
+
# Fetch message IDs before sending the prompt
|
|
886
|
+
try:
|
|
887
|
+
before_messages = await oc_client.list_messages(session_id)
|
|
888
|
+
before_ids = {m.get("info", {}).get("id") for m in before_messages if m.get("info", {}).get("id")}
|
|
889
|
+
except Exception as e:
|
|
890
|
+
logger.warning(f"Failed to fetch messages before document prompt: {e}")
|
|
891
|
+
|
|
757
892
|
response_text = await _send_to_opencode(
|
|
758
893
|
oc_client=oc_client,
|
|
759
894
|
session_id=session_id,
|
|
@@ -765,9 +900,54 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
765
900
|
await session_mgr.increment_message_count(user_id, prompt=caption)
|
|
766
901
|
|
|
767
902
|
# Send response back to user
|
|
768
|
-
|
|
769
|
-
|
|
903
|
+
# Fetch messages after prompt completes to get all multi-step assistant messages
|
|
904
|
+
response_texts = []
|
|
905
|
+
try:
|
|
906
|
+
after_messages = await oc_client.list_messages(session_id)
|
|
907
|
+
new_messages = [
|
|
908
|
+
m for m in after_messages
|
|
909
|
+
if m.get("info", {}).get("id") not in before_ids
|
|
910
|
+
and m.get("info", {}).get("id") not in sent_message_ids
|
|
911
|
+
and m.get("info", {}).get("role") == "assistant"
|
|
912
|
+
]
|
|
913
|
+
for m in new_messages:
|
|
914
|
+
parts = m.get("parts", [])
|
|
915
|
+
content_text = ""
|
|
916
|
+
if isinstance(parts, list):
|
|
917
|
+
text_parts = [
|
|
918
|
+
p.get("text", "")
|
|
919
|
+
for p in parts
|
|
920
|
+
if isinstance(p, dict) and p.get("type") == "text"
|
|
921
|
+
]
|
|
922
|
+
content_text = "".join(text_parts)
|
|
923
|
+
if content_text.strip():
|
|
924
|
+
response_texts.append(content_text)
|
|
925
|
+
except Exception as e:
|
|
926
|
+
logger.warning(f"Failed to fetch messages after document prompt: {e}")
|
|
927
|
+
|
|
928
|
+
# Fallback to standard response if no intermediate texts were retrieved
|
|
929
|
+
all_responses = response_texts if response_texts else ([response_text] if response_text else [])
|
|
930
|
+
|
|
931
|
+
if not all_responses:
|
|
932
|
+
if response_text == "ABORTED":
|
|
933
|
+
return
|
|
934
|
+
# Send a user-friendly status message to prevent getting stuck silently
|
|
935
|
+
await update.message.reply_text(
|
|
936
|
+
"ℹ️ <b>OpenCode finished execution.</b>\n<i>(No conversational text response was returned)</i>",
|
|
937
|
+
parse_mode="HTML"
|
|
938
|
+
)
|
|
939
|
+
return
|
|
940
|
+
|
|
941
|
+
for resp in all_responses:
|
|
942
|
+
if not resp or resp == "ABORTED":
|
|
943
|
+
continue
|
|
944
|
+
|
|
945
|
+
# Format OpenCode output for Telegram
|
|
946
|
+
formatted = format_opencode_response(resp)
|
|
947
|
+
|
|
948
|
+
# Split into chunks if too long
|
|
770
949
|
chunks = split_message(formatted, config.max_message_length)
|
|
950
|
+
|
|
771
951
|
for i, chunk in enumerate(chunks):
|
|
772
952
|
try:
|
|
773
953
|
await update.message.reply_text(
|
|
@@ -796,9 +976,9 @@ async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|
|
796
976
|
typing_task.cancel()
|
|
797
977
|
if sse_task:
|
|
798
978
|
sse_task.cancel()
|
|
799
|
-
if
|
|
979
|
+
if status_msg_holder and status_msg_holder[0]:
|
|
800
980
|
try:
|
|
801
|
-
await
|
|
981
|
+
await status_msg_holder[0].delete()
|
|
802
982
|
except Exception:
|
|
803
983
|
pass
|
|
804
984
|
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/opencode/client.py
RENAMED
|
@@ -274,6 +274,20 @@ class OpenCodeClient:
|
|
|
274
274
|
return result.get("sessions", result.get("data", []))
|
|
275
275
|
return []
|
|
276
276
|
|
|
277
|
+
async def list_messages(self, session_id: str) -> List[Dict[str, Any]]:
|
|
278
|
+
"""List all messages in an OpenCode session.
|
|
279
|
+
|
|
280
|
+
Parameters:
|
|
281
|
+
session_id: The session identifier.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
A list of message dictionaries.
|
|
285
|
+
"""
|
|
286
|
+
result = await self._request("GET", f"/session/{session_id}/message")
|
|
287
|
+
if isinstance(result, list):
|
|
288
|
+
return result
|
|
289
|
+
return []
|
|
290
|
+
|
|
277
291
|
async def create_session(self, directory: Optional[str] = None) -> Dict[str, Any]:
|
|
278
292
|
"""Create a new OpenCode session.
|
|
279
293
|
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/opencode/server.py
RENAMED
|
@@ -103,34 +103,37 @@ async def stop_server() -> None:
|
|
|
103
103
|
# Also kill any stray opencode serve processes on our port
|
|
104
104
|
if platform.system() == "Windows":
|
|
105
105
|
try:
|
|
106
|
-
# Query netstat to find process ID listening on the port
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
stdout=
|
|
111
|
-
stderr=
|
|
106
|
+
# Query netstat to find process ID listening on the port synchronously
|
|
107
|
+
proc = subprocess.run(
|
|
108
|
+
f'netstat -ano | findstr LISTENING | findstr :{_server_port}',
|
|
109
|
+
shell=True,
|
|
110
|
+
stdout=subprocess.PIPE,
|
|
111
|
+
stderr=subprocess.DEVNULL,
|
|
112
|
+
text=True,
|
|
113
|
+
timeout=5
|
|
112
114
|
)
|
|
113
|
-
stdout
|
|
114
|
-
lines = stdout.
|
|
115
|
+
stdout = proc.stdout
|
|
116
|
+
lines = stdout.strip().split('\n')
|
|
115
117
|
for line in lines:
|
|
116
118
|
parts = line.strip().split()
|
|
117
119
|
if len(parts) >= 5:
|
|
118
120
|
pid = parts[-1]
|
|
119
121
|
if pid.isdigit() and int(pid) > 0:
|
|
120
|
-
|
|
122
|
+
subprocess.run(f"taskkill /F /T /PID {pid}", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)
|
|
121
123
|
logger.info(f"Killed stray Windows PID={pid} on port {_server_port}")
|
|
122
124
|
except Exception as e:
|
|
123
125
|
logger.warning(f"Failed to kill stray Windows process: {e}")
|
|
124
126
|
else:
|
|
125
|
-
# Unix lsof implementation
|
|
127
|
+
# Unix lsof implementation synchronously
|
|
126
128
|
try:
|
|
127
|
-
proc =
|
|
128
|
-
"lsof", "-ti", f":{_server_port}", "-sTCP:LISTEN",
|
|
129
|
-
stdout=
|
|
130
|
-
stderr=
|
|
129
|
+
proc = subprocess.run(
|
|
130
|
+
["lsof", "-ti", f":{_server_port}", "-sTCP:LISTEN"],
|
|
131
|
+
stdout=subprocess.PIPE,
|
|
132
|
+
stderr=subprocess.DEVNULL,
|
|
133
|
+
text=True,
|
|
134
|
+
timeout=5
|
|
131
135
|
)
|
|
132
|
-
|
|
133
|
-
pids = stdout.decode().strip().split()
|
|
136
|
+
pids = proc.stdout.strip().split()
|
|
134
137
|
for pid in pids:
|
|
135
138
|
if pid.isdigit():
|
|
136
139
|
try:
|
|
@@ -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.7"
|
|
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"
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/sessions/manager.py
RENAMED
|
@@ -65,6 +65,11 @@ class SessionManager:
|
|
|
65
65
|
await self._db.execute("ALTER TABLE user_settings ADD COLUMN streaming INTEGER DEFAULT 0")
|
|
66
66
|
except aiosqlite.OperationalError:
|
|
67
67
|
pass
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
await self._db.execute("ALTER TABLE user_settings ADD COLUMN model TEXT DEFAULT ''")
|
|
71
|
+
except aiosqlite.OperationalError:
|
|
72
|
+
pass
|
|
68
73
|
|
|
69
74
|
await self._db.execute("""
|
|
70
75
|
CREATE INDEX IF NOT EXISTS idx_user_active
|
|
@@ -174,7 +179,11 @@ class SessionManager:
|
|
|
174
179
|
await self._db.commit()
|
|
175
180
|
|
|
176
181
|
async def set_model(self, user_id: int, model: str) -> None:
|
|
177
|
-
"""Set the model for a user's active session."""
|
|
182
|
+
"""Set the model for a user's active session and also save as preferred settings."""
|
|
183
|
+
# 1. Save in user_settings
|
|
184
|
+
await self.set_user_preferred_model(user_id, model)
|
|
185
|
+
|
|
186
|
+
# 2. Update active session if exists
|
|
178
187
|
if user_id in self._active_sessions:
|
|
179
188
|
self._active_sessions[user_id]["model"] = model
|
|
180
189
|
await self._db.execute(
|
|
@@ -182,6 +191,27 @@ class SessionManager:
|
|
|
182
191
|
(model, user_id)
|
|
183
192
|
)
|
|
184
193
|
await self._db.commit()
|
|
194
|
+
|
|
195
|
+
async def get_user_preferred_model(self, user_id: int, default_model: str) -> str:
|
|
196
|
+
"""Get the preferred model for a specific user, falling back to default."""
|
|
197
|
+
async with self._db.execute(
|
|
198
|
+
"SELECT model FROM user_settings WHERE user_id = ?",
|
|
199
|
+
(user_id,)
|
|
200
|
+
) as cursor:
|
|
201
|
+
row = await cursor.fetchone()
|
|
202
|
+
if row and row[0]:
|
|
203
|
+
return row[0]
|
|
204
|
+
return default_model
|
|
205
|
+
|
|
206
|
+
async def set_user_preferred_model(self, user_id: int, model: str) -> None:
|
|
207
|
+
"""Save or update the preferred model for a user in their settings."""
|
|
208
|
+
await self._db.execute(
|
|
209
|
+
"INSERT INTO user_settings (user_id, work_dir, model) VALUES (?, '', ?) "
|
|
210
|
+
"ON CONFLICT(user_id) DO UPDATE SET model = excluded.model",
|
|
211
|
+
(user_id, model)
|
|
212
|
+
)
|
|
213
|
+
await self._db.commit()
|
|
214
|
+
logger.info(f"Set preferred model for user {user_id}: {model}")
|
|
185
215
|
|
|
186
216
|
async def clear_session(self, user_id: int) -> None:
|
|
187
217
|
"""Deactivate the current session for a user (they'll get a new one on next message)."""
|
|
@@ -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.7
|
|
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
|
|
@@ -47,7 +47,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
|
|
|
47
47
|
|
|
48
48
|
### Quick Installation:
|
|
49
49
|
```bash
|
|
50
|
-
pip install telegram-opencode-bridge-bot==0.1.
|
|
50
|
+
pip install telegram-opencode-bridge-bot==0.1.7
|
|
51
51
|
telegram-opencode-bot
|
|
52
52
|
telegram-opencode-bot --env (use --env flag if u want to re-configure later anytime)
|
|
53
53
|
```
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/utils/formatting.py
RENAMED
|
@@ -35,7 +35,9 @@ def format_opencode_response(text: str) -> str:
|
|
|
35
35
|
formatted_parts.append(f'<pre>{escaped}</pre>')
|
|
36
36
|
else:
|
|
37
37
|
# Regular text — convert markdown to HTML
|
|
38
|
-
|
|
38
|
+
html_converted = _markdown_to_html(content)
|
|
39
|
+
# Parse and convert markdown tables inside the HTML-converted text
|
|
40
|
+
formatted_parts.append(_convert_markdown_tables(html_converted))
|
|
39
41
|
|
|
40
42
|
return "\n".join(formatted_parts)
|
|
41
43
|
|
|
@@ -98,6 +100,154 @@ def _markdown_to_html(text: str) -> str:
|
|
|
98
100
|
return text
|
|
99
101
|
|
|
100
102
|
|
|
103
|
+
def _convert_markdown_tables(text: str) -> str:
|
|
104
|
+
"""Detect and convert Markdown tables to Telegram-friendly HTML cards or lists."""
|
|
105
|
+
lines = text.split("\n")
|
|
106
|
+
processed_lines = []
|
|
107
|
+
|
|
108
|
+
in_table = False
|
|
109
|
+
table_headers = []
|
|
110
|
+
table_rows = []
|
|
111
|
+
|
|
112
|
+
def flush_table():
|
|
113
|
+
nonlocal in_table, table_headers, table_rows
|
|
114
|
+
if not table_headers:
|
|
115
|
+
in_table = False
|
|
116
|
+
table_rows = []
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
formatted_table = []
|
|
120
|
+
num_cols = len(table_headers)
|
|
121
|
+
|
|
122
|
+
if num_cols == 2:
|
|
123
|
+
# 2 Columns (Checklist / Key-Value Card)
|
|
124
|
+
header_left = table_headers[0].strip()
|
|
125
|
+
header_right = table_headers[1].strip()
|
|
126
|
+
|
|
127
|
+
# Strip tags for clean header title
|
|
128
|
+
h_left_raw = re.sub(r'<[^>]+>', '', header_left).strip()
|
|
129
|
+
formatted_table.append(f"<b>📋 {h_left_raw}</b>\n")
|
|
130
|
+
|
|
131
|
+
for row in table_rows:
|
|
132
|
+
if len(row) >= 2:
|
|
133
|
+
key = row[0].strip()
|
|
134
|
+
val = row[1].strip()
|
|
135
|
+
|
|
136
|
+
# Strip any redundant bold tags from key for clean styling
|
|
137
|
+
clean_key = key
|
|
138
|
+
if clean_key.startswith("<b>") and clean_key.endswith("</b>"):
|
|
139
|
+
clean_key = clean_key[3:-4]
|
|
140
|
+
|
|
141
|
+
formatted_table.append(f"• <b>{clean_key}:</b> {val}")
|
|
142
|
+
formatted_table.append("") # Empty line after table
|
|
143
|
+
|
|
144
|
+
else:
|
|
145
|
+
# 3+ Columns (API Card Layout)
|
|
146
|
+
method_idx = -1
|
|
147
|
+
path_idx = -1
|
|
148
|
+
other_idxs = []
|
|
149
|
+
|
|
150
|
+
for idx, h in enumerate(table_headers):
|
|
151
|
+
h_clean = h.strip().lower()
|
|
152
|
+
h_raw = re.sub(r'<[^>]+>', '', h_clean)
|
|
153
|
+
if "method" in h_raw:
|
|
154
|
+
method_idx = idx
|
|
155
|
+
elif "path" in h_raw or "endpoint" in h_raw:
|
|
156
|
+
path_idx = idx
|
|
157
|
+
else:
|
|
158
|
+
other_idxs.append(idx)
|
|
159
|
+
|
|
160
|
+
for row in table_rows:
|
|
161
|
+
method_val = row[method_idx].strip() if method_idx >= 0 and method_idx < len(row) else ""
|
|
162
|
+
path_val = row[path_idx].strip() if path_idx >= 0 and path_idx < len(row) else ""
|
|
163
|
+
|
|
164
|
+
# Clean method and path from any HTML tags
|
|
165
|
+
method_raw = re.sub(r'<[^>]+>', '', method_val).upper().strip()
|
|
166
|
+
path_raw = re.sub(r'<[^>]+>', '', path_val).strip()
|
|
167
|
+
|
|
168
|
+
# Format method badge with colored emojis
|
|
169
|
+
method_html = ""
|
|
170
|
+
if method_raw:
|
|
171
|
+
emoji = "⚪"
|
|
172
|
+
if "GET" in method_raw and "POST" in method_raw:
|
|
173
|
+
emoji = "🟢/🔵"
|
|
174
|
+
elif "GET" in method_raw:
|
|
175
|
+
emoji = "🟢"
|
|
176
|
+
elif "POST" in method_raw:
|
|
177
|
+
emoji = "🔵"
|
|
178
|
+
elif "PUT" in method_raw:
|
|
179
|
+
emoji = "🟡"
|
|
180
|
+
elif "DELETE" in method_raw:
|
|
181
|
+
emoji = "🔴"
|
|
182
|
+
elif "PATCH" in method_raw:
|
|
183
|
+
emoji = "🟣"
|
|
184
|
+
|
|
185
|
+
method_html = f"{emoji} <b>{method_raw}</b>"
|
|
186
|
+
|
|
187
|
+
# Format path in monospaced code blocks
|
|
188
|
+
path_html = ""
|
|
189
|
+
if path_raw:
|
|
190
|
+
path_html = f" <code>{path_raw}</code>"
|
|
191
|
+
|
|
192
|
+
card_title = f"{method_html}{path_html}".strip()
|
|
193
|
+
if card_title:
|
|
194
|
+
formatted_table.append(card_title)
|
|
195
|
+
|
|
196
|
+
# Add other columns as key-value bullets under this card
|
|
197
|
+
for idx in other_idxs:
|
|
198
|
+
if idx < len(row):
|
|
199
|
+
col_name = table_headers[idx].strip()
|
|
200
|
+
col_val = row[idx].strip()
|
|
201
|
+
|
|
202
|
+
col_name_raw = re.sub(r'<[^>]+>', '', col_name).strip()
|
|
203
|
+
|
|
204
|
+
if col_val:
|
|
205
|
+
formatted_table.append(f"• <b>{col_name_raw}:</b> {col_val}")
|
|
206
|
+
|
|
207
|
+
formatted_table.append("") # Empty line between cards
|
|
208
|
+
|
|
209
|
+
# Reset state
|
|
210
|
+
in_table = False
|
|
211
|
+
table_headers = []
|
|
212
|
+
table_rows = []
|
|
213
|
+
|
|
214
|
+
return formatted_table
|
|
215
|
+
|
|
216
|
+
i = 0
|
|
217
|
+
while i < len(lines):
|
|
218
|
+
line = lines[i]
|
|
219
|
+
line_stripped = line.strip()
|
|
220
|
+
|
|
221
|
+
if line_stripped.startswith("|"):
|
|
222
|
+
cells = [c.strip() for c in line_stripped.split("|")[1:-1]]
|
|
223
|
+
|
|
224
|
+
is_separator = False
|
|
225
|
+
if cells:
|
|
226
|
+
is_separator = all(re.match(r'^:?-+:?$', c) for c in cells)
|
|
227
|
+
|
|
228
|
+
if is_separator:
|
|
229
|
+
i += 1
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if not in_table:
|
|
233
|
+
in_table = True
|
|
234
|
+
table_headers = cells
|
|
235
|
+
else:
|
|
236
|
+
table_rows.append(cells)
|
|
237
|
+
else:
|
|
238
|
+
if in_table:
|
|
239
|
+
processed_lines.extend(flush_table())
|
|
240
|
+
processed_lines.append(line)
|
|
241
|
+
|
|
242
|
+
i += 1
|
|
243
|
+
|
|
244
|
+
if in_table:
|
|
245
|
+
processed_lines.extend(flush_table())
|
|
246
|
+
|
|
247
|
+
return "\n".join(processed_lines)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
101
251
|
def split_message(text: str, max_length: int = DEFAULT_MAX_LENGTH) -> List[str]:
|
|
102
252
|
"""Split a long message into chunks that fit Telegram's limit.
|
|
103
253
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/handlers/__init__.py
RENAMED
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/list_session_models.py
RENAMED
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/opencode/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{telegram_opencode_bridge_bot-0.1.6 → telegram_opencode_bridge_bot-0.1.7}/sessions/__init__.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
|
|
File without changes
|