telegram-opencode-bridge-bot 0.1.5__tar.gz → 0.1.6__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.
Files changed (27) hide show
  1. {telegram_opencode_bridge_bot-0.1.5/telegram_opencode_bridge_bot.egg-info → telegram_opencode_bridge_bot-0.1.6}/PKG-INFO +7 -3
  2. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/README.md +6 -2
  3. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/bot.py +21 -0
  4. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/handlers/commands.py +440 -0
  5. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/handlers/messages.py +1 -1
  6. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/opencode/client.py +12 -0
  7. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/pyproject.toml +1 -1
  8. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6/telegram_opencode_bridge_bot.egg-info}/PKG-INFO +7 -3
  9. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/.env.example +0 -0
  10. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/MANIFEST.in +0 -0
  11. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/config.py +0 -0
  12. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/handlers/__init__.py +0 -0
  13. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/list_session_models.py +0 -0
  14. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/opencode/__init__.py +0 -0
  15. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/opencode/server.py +0 -0
  16. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/requirements.txt +0 -0
  17. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/sessions/__init__.py +0 -0
  18. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/sessions/manager.py +0 -0
  19. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/setup.cfg +0 -0
  20. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/telegram_opencode_bridge_bot.egg-info/SOURCES.txt +0 -0
  21. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/telegram_opencode_bridge_bot.egg-info/dependency_links.txt +0 -0
  22. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/telegram_opencode_bridge_bot.egg-info/entry_points.txt +0 -0
  23. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/telegram_opencode_bridge_bot.egg-info/requires.txt +0 -0
  24. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/telegram_opencode_bridge_bot.egg-info/top_level.txt +0 -0
  25. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/utils/__init__.py +0 -0
  26. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/utils/formatting.py +0 -0
  27. {telegram_opencode_bridge_bot-0.1.5 → telegram_opencode_bridge_bot-0.1.6}/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.5
3
+ Version: 0.1.6
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
@@ -23,7 +23,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
23
23
 
24
24
  - **Direct OpenCode integration** — routes your messages to OpenCode's HTTP API
25
25
  - **Persistent sessions** — conversations maintain context across messages
26
- - **Session management** — create, switch, list, and share sessions
26
+ - **Session management** — create, delete, switch, list, and share sessions
27
27
  - **Model switching** — change AI models on the fly (`/model`)
28
28
  - **Plan/Build modes** — toggle between read-only analysis and full execution
29
29
  - **Smart formatting** — code blocks with syntax highlighting in Telegram
@@ -31,6 +31,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
31
31
  - **Security** — whitelist-based access control + rate limiting
32
32
  - **Workspace Switching** — Switching from one workspace to another
33
33
  - **Uploading from Mobile to WorkSpace** - U can upload the documents/images from your mobile to the opencode agent workspace
34
+ - **Workspace Management** — Creation and deletion of the workspace/folder
34
35
 
35
36
  ## 📋 Prerequisites
36
37
 
@@ -46,7 +47,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
46
47
 
47
48
  ### Quick Installation:
48
49
  ```bash
49
- pip install telegram-opencode-bridge-bot==0.1.5
50
+ pip install telegram-opencode-bridge-bot==0.1.6
50
51
  telegram-opencode-bot
51
52
  telegram-opencode-bot --env (use --env flag if u want to re-configure later anytime)
52
53
  ```
@@ -88,6 +89,9 @@ Open Telegram, find your bot, and start asking! 🎉
88
89
  | `/project` | To view the current workspace, sub folder workspaces and to change the workspace |
89
90
  | `/enable` | To enable the streaming |
90
91
  | `/disable` | To disable the streaming |
92
+ | `/create_project` | To create new project/folder/workspace |
93
+ | `/delete_project` | To create new project/folder/workspace |
94
+ | `/delete` | To delete a conversation |
91
95
 
92
96
  ## 🏗️ Architecture
93
97
 
@@ -6,7 +6,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
6
6
 
7
7
  - **Direct OpenCode integration** — routes your messages to OpenCode's HTTP API
8
8
  - **Persistent sessions** — conversations maintain context across messages
9
- - **Session management** — create, switch, list, and share sessions
9
+ - **Session management** — create, delete, switch, list, and share sessions
10
10
  - **Model switching** — change AI models on the fly (`/model`)
11
11
  - **Plan/Build modes** — toggle between read-only analysis and full execution
12
12
  - **Smart formatting** — code blocks with syntax highlighting in Telegram
@@ -14,6 +14,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
14
14
  - **Security** — whitelist-based access control + rate limiting
15
15
  - **Workspace Switching** — Switching from one workspace to another
16
16
  - **Uploading from Mobile to WorkSpace** - U can upload the documents/images from your mobile to the opencode agent workspace
17
+ - **Workspace Management** — Creation and deletion of the workspace/folder
17
18
 
18
19
  ## 📋 Prerequisites
19
20
 
@@ -29,7 +30,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
29
30
 
30
31
  ### Quick Installation:
31
32
  ```bash
32
- pip install telegram-opencode-bridge-bot==0.1.5
33
+ pip install telegram-opencode-bridge-bot==0.1.6
33
34
  telegram-opencode-bot
34
35
  telegram-opencode-bot --env (use --env flag if u want to re-configure later anytime)
35
36
  ```
@@ -71,6 +72,9 @@ Open Telegram, find your bot, and start asking! 🎉
71
72
  | `/project` | To view the current workspace, sub folder workspaces and to change the workspace |
72
73
  | `/enable` | To enable the streaming |
73
74
  | `/disable` | To disable the streaming |
75
+ | `/create_project` | To create new project/folder/workspace |
76
+ | `/delete_project` | To create new project/folder/workspace |
77
+ | `/delete` | To delete a conversation |
74
78
 
75
79
  ## 🏗️ Architecture
76
80
 
@@ -41,6 +41,7 @@ from handlers.commands import (
41
41
  help_command,
42
42
  new_command,
43
43
  sessions_command,
44
+ delete_command,
44
45
  plan_command,
45
46
  build_command,
46
47
  share_command,
@@ -49,6 +50,8 @@ from handlers.commands import (
49
50
  models_command,
50
51
  stop_command,
51
52
  project_command,
53
+ create_project_command,
54
+ delete_project_command,
52
55
  enable_command,
53
56
  disable_command,
54
57
  set_bot_commands,
@@ -105,6 +108,10 @@ def build_authorized_handlers(authorizer: UserAuthorizer, rate_limiter: RateLimi
105
108
  async def _sessions(update, context):
106
109
  await sessions_command(update, context)
107
110
 
111
+ @authorized(authorizer, rate_limiter)
112
+ async def _delete(update, context):
113
+ await delete_command(update, context)
114
+
108
115
  @authorized(authorizer, rate_limiter)
109
116
  async def _plan(update, context):
110
117
  await plan_command(update, context)
@@ -137,6 +144,14 @@ def build_authorized_handlers(authorizer: UserAuthorizer, rate_limiter: RateLimi
137
144
  async def _project(update, context):
138
145
  await project_command(update, context)
139
146
 
147
+ @authorized(authorizer, rate_limiter)
148
+ async def _create_project(update, context):
149
+ await create_project_command(update, context)
150
+
151
+ @authorized(authorizer, rate_limiter)
152
+ async def _delete_project(update, context):
153
+ await delete_project_command(update, context)
154
+
140
155
  @authorized(authorizer, rate_limiter)
141
156
  async def _enable(update, context):
142
157
  await enable_command(update, context)
@@ -162,9 +177,12 @@ def build_authorized_handlers(authorizer: UserAuthorizer, rate_limiter: RateLimi
162
177
  "help": _help,
163
178
  "new": _new,
164
179
  "sessions": _sessions,
180
+ "delete": _delete,
165
181
  "models": _models,
166
182
  "stop": _stop,
167
183
  "project": _project,
184
+ "create_project": _create_project,
185
+ "delete_project": _delete_project,
168
186
  "enable": _enable,
169
187
  "disable": _disable,
170
188
  "plan": _plan,
@@ -451,9 +469,12 @@ def main():
451
469
  application.add_handler(CommandHandler("help", handlers["help"], block=False))
452
470
  application.add_handler(CommandHandler("new", handlers["new"], block=False))
453
471
  application.add_handler(CommandHandler("sessions", handlers["sessions"], block=False))
472
+ application.add_handler(CommandHandler("delete", handlers["delete"], block=False))
454
473
  application.add_handler(CommandHandler("models", handlers["models"], block=False))
455
474
  application.add_handler(CommandHandler("stop", handlers["stop"], block=False))
456
475
  application.add_handler(CommandHandler("project", handlers["project"], block=False))
476
+ application.add_handler(CommandHandler("create_project", handlers["create_project"], block=False))
477
+ application.add_handler(CommandHandler("delete_project", handlers["delete_project"], block=False))
457
478
  application.add_handler(CommandHandler("enable", handlers["enable"], block=False))
458
479
  application.add_handler(CommandHandler("disable", handlers["disable"], block=False))
459
480
  application.add_handler(CommandHandler("plan", handlers["plan"], block=False))
@@ -56,9 +56,12 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
56
56
  "/new — Start a fresh conversation (clears current session)\n"
57
57
  "/stop — Stop/abort the current active task\n"
58
58
  "/project — View & switch active project directories\n"
59
+ "/create_project — Create a new workspace folder\n"
60
+ "/delete_project — Delete a workspace folder\n"
59
61
  "/enable — Enable live tool call & progress streaming\n"
60
62
  "/disable — Disable live progress streaming\n"
61
63
  "/sessions — List your recent sessions (tap to switch)\n"
64
+ "/delete — Permanently delete a session\n"
62
65
  "/models — List all available models (tap to change)\n"
63
66
  "/plan — Switch to plan mode (read-only)\n"
64
67
  "/build — Switch to build mode (read, write, execute)\n"
@@ -251,6 +254,88 @@ async def sessions_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -
251
254
  await update.message.reply_text("\n".join(lines), reply_markup=reply_markup, parse_mode="HTML")
252
255
 
253
256
 
257
+ # ──────────────────────────────────────────────
258
+ # Command: /delete
259
+ # ──────────────────────────────────────────────
260
+ async def delete_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
261
+ """List recent sessions for this user in their active workspace for deletion."""
262
+ user_id = update.effective_user.id
263
+ session_mgr = context.bot_data["session_manager"]
264
+ oc_client = context.bot_data["opencode_client"]
265
+ config = context.bot_data["config"]
266
+
267
+ # Ensure OpenCode server is running dynamically
268
+ from handlers.messages import ensure_server_running
269
+ if not await ensure_server_running(update, context, user_id):
270
+ return
271
+
272
+ # Resolve workspace
273
+ base_dir = os.path.abspath(config.opencode_work_dir)
274
+ current_dir = await session_mgr.get_user_work_dir(user_id, base_dir)
275
+ current_dir = os.path.abspath(current_dir)
276
+
277
+ def norm(p):
278
+ if not p:
279
+ return ""
280
+ return os.path.normcase(os.path.normpath(os.path.abspath(p)))
281
+
282
+ current_dir_norm = norm(current_dir)
283
+
284
+ # Fetch server sessions
285
+ server_sessions = []
286
+ try:
287
+ server_sessions = await oc_client.list_sessions()
288
+ except Exception as e:
289
+ logger.warning(f"Could not fetch sessions from server during delete call: {e}")
290
+ await update.message.reply_text(
291
+ "⚠️ Could not reach the OpenCode server to list sessions.\n"
292
+ "Make sure <code>opencode serve</code> is running.",
293
+ parse_mode="HTML",
294
+ )
295
+ return
296
+
297
+ # Filter to current workspace
298
+ workspace_sessions = []
299
+ for s in server_sessions:
300
+ s_dir = s.get("directory", "")
301
+ if norm(s_dir) == current_dir_norm:
302
+ workspace_sessions.append(s)
303
+
304
+ if not workspace_sessions:
305
+ folder_name = os.path.basename(current_dir) or "Root"
306
+ await update.message.reply_text(
307
+ f"📭 No sessions found in project <b>{html.escape(folder_name)}</b> to delete.",
308
+ parse_mode="HTML",
309
+ )
310
+ return
311
+
312
+ workspace_sessions.sort(
313
+ key=lambda s: s.get("time", {}).get("updated", 0),
314
+ reverse=True,
315
+ )
316
+
317
+ active_sid = await session_mgr.get_active_session(user_id)
318
+
319
+ folder_name = os.path.basename(current_dir) or "Root"
320
+ text = (
321
+ f"🗑️ <b>Delete Session (Workspace: {html.escape(folder_name)})</b>\n\n"
322
+ f"Choose a session below to permanently delete it from your local machine and the server.\n\n"
323
+ f"⚠️ <b>WARNING:</b> This cannot be undone!"
324
+ )
325
+
326
+ keyboard = []
327
+ for s in workspace_sessions:
328
+ s_id = s.get("id", "")
329
+ s_title = s.get("title", "") or s_id[:8]
330
+ is_active = (s_id == active_sid)
331
+ marker = "🔹 (Current)" if is_active else "📄"
332
+ button_text = f"❌ Delete {s_title} {marker}"
333
+ keyboard.append([InlineKeyboardButton(button_text, callback_data=f"delsess:{s_id}")])
334
+
335
+ reply_markup = InlineKeyboardMarkup(keyboard)
336
+ await update.message.reply_text(text, reply_markup=reply_markup, parse_mode="HTML")
337
+
338
+
254
339
  # ──────────────────────────────────────────────
255
340
  # Command: /switch <session_id>
256
341
  # ──────────────────────────────────────────────
@@ -867,9 +952,12 @@ async def set_bot_commands(app) -> None:
867
952
  BotCommand("new", "Start a fresh conversation"),
868
953
  BotCommand("stop", "Stop/abort the current active task"),
869
954
  BotCommand("project", "View & switch project folders"),
955
+ BotCommand("create_project", "Create a new workspace folder"),
956
+ BotCommand("delete_project", "Delete an existing workspace folder"),
870
957
  BotCommand("enable", "Enable live progress streaming"),
871
958
  BotCommand("disable", "Disable live progress streaming"),
872
959
  BotCommand("sessions", "List your sessions"),
960
+ BotCommand("delete", "Permanently delete a session"),
873
961
  BotCommand("models", "List all available models"),
874
962
  BotCommand("plan", "Switch to plan mode (read-only)"),
875
963
  BotCommand("build", "Switch to build mode (read, write, execute)"),
@@ -1004,6 +1092,58 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -
1004
1092
  parse_mode="HTML",
1005
1093
  )
1006
1094
 
1095
+ # 1.3 Delete Session tap
1096
+ elif data.startswith("delsess:"):
1097
+ target_id = data[len("delsess:"):]
1098
+
1099
+ from handlers.messages import ensure_server_running
1100
+ if not await ensure_server_running(update, context, user_id):
1101
+ return
1102
+
1103
+ resolved_id = None
1104
+ server_sessions = []
1105
+ try:
1106
+ server_sessions = await oc_client.list_sessions()
1107
+ for s in server_sessions:
1108
+ s_id = s.get("id", "")
1109
+ if s_id.lower().startswith(target_id.lower()):
1110
+ resolved_id = s_id
1111
+ break
1112
+ except Exception as e:
1113
+ logger.warning(f"Could not verify session list on server during delete callback: {e}")
1114
+
1115
+ if not resolved_id:
1116
+ resolved_id = target_id
1117
+
1118
+ try:
1119
+ # 1. Delete on OpenCode Server
1120
+ await oc_client.delete_session(resolved_id)
1121
+ except Exception as e:
1122
+ logger.warning(f"Failed to delete session {resolved_id} on server, proceeding locally: {e}")
1123
+
1124
+ # 2. Delete locally in SQLite DB
1125
+ try:
1126
+ await session_mgr._db.execute(
1127
+ "DELETE FROM sessions WHERE user_id = ? AND opencode_session_id = ?",
1128
+ (user_id, resolved_id)
1129
+ )
1130
+ await session_mgr._db.commit()
1131
+ except Exception as e:
1132
+ logger.error(f"Failed to delete session {resolved_id} in local database: {e}")
1133
+
1134
+ # 3. If active session was deleted, clear active cache and reset
1135
+ active_sid = await session_mgr.get_active_session(user_id)
1136
+ if active_sid == resolved_id:
1137
+ if user_id in session_mgr._active_sessions:
1138
+ del session_mgr._active_sessions[user_id]
1139
+ # Try to auto-resolve first remaining session or let the bot create a new one lazily
1140
+ await session_mgr.clear_session(user_id)
1141
+
1142
+ await query.edit_message_text(
1143
+ f"🗑️ <b>Deleted:</b> Session <code>{html.escape(resolved_id[:8])}</code> has been permanently removed.",
1144
+ parse_mode="HTML",
1145
+ )
1146
+
1007
1147
  # 1.5 Handle Sensitive Operations / Tool Permissions
1008
1148
  elif data.startswith("perm:"):
1009
1149
  parts = data.split(":")
@@ -1200,3 +1340,303 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -
1200
1340
  logger.error(f"Error navigating subfolder: {e}", exc_info=True)
1201
1341
  await query.edit_message_text(f"❌ Navigation error: {e}")
1202
1342
 
1343
+ # 4. Workspace Deletion callback query flows
1344
+ elif data.startswith("delproj_ask:"):
1345
+ target_folder = data[len("delproj_ask:"):]
1346
+
1347
+ # Verify it's not active
1348
+ base_dir = os.path.abspath(config.opencode_work_dir)
1349
+ current_dir = await session_mgr.get_user_work_dir(user_id, base_dir)
1350
+ current_dir = os.path.abspath(current_dir)
1351
+
1352
+ target_path = os.path.abspath(os.path.join(base_dir, target_folder))
1353
+
1354
+ if os.path.normcase(target_path) == os.path.normcase(current_dir):
1355
+ await query.edit_message_text(
1356
+ f"❌ <b>Cannot Delete Active Workspace:</b>\n"
1357
+ f"You cannot delete your currently active workspace directory <code>{html.escape(target_folder)}</code>.\n"
1358
+ f"Please switch to another workspace folder first using /project, then attempt deletion.",
1359
+ parse_mode="HTML"
1360
+ )
1361
+ return
1362
+
1363
+ warning_text = (
1364
+ f"⚠️ <b>WARNING: Permanent Deletion!</b>\n\n"
1365
+ f"Are you absolutely sure you want to permanently delete the folder <code>{html.escape(target_folder)}</code>?\n\n"
1366
+ f"This will physically **erase all source files and directories** inside this workspace on your computer!\n\n"
1367
+ f"🚨 <b>THIS ACTION CANNOT BE UNDONE!</b>"
1368
+ )
1369
+
1370
+ keyboard = [
1371
+ [
1372
+ InlineKeyboardButton("💥 Yes, Erase Folder", callback_data=f"delproj_confirm:{target_folder}"),
1373
+ InlineKeyboardButton("↩️ Cancel", callback_data="delproj_cancel")
1374
+ ]
1375
+ ]
1376
+
1377
+ await query.edit_message_text(warning_text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="HTML")
1378
+
1379
+ elif data.startswith("delproj_confirm:"):
1380
+ target_folder = data[len("delproj_confirm:"):]
1381
+ base_dir = os.path.abspath(config.opencode_work_dir)
1382
+ target_path = os.path.abspath(os.path.join(base_dir, target_folder))
1383
+
1384
+ # 1. Security Check: Path traversal exploit guard
1385
+ if not os.path.normcase(target_path).startswith(os.path.normcase(base_dir)) or target_path == base_dir:
1386
+ await query.edit_message_text(
1387
+ "❌ <b>Security Violation:</b> Deletion path lies outside your parent workspace directory.",
1388
+ parse_mode="HTML"
1389
+ )
1390
+ return
1391
+
1392
+ # 2. Check if currently active (double check)
1393
+ current_dir = await session_mgr.get_user_work_dir(user_id, base_dir)
1394
+ current_dir = os.path.abspath(current_dir)
1395
+ if os.path.normcase(target_path) == os.path.normcase(current_dir):
1396
+ await query.edit_message_text(
1397
+ "❌ <b>Cannot Delete Active Workspace:</b> Please switch to another workspace first.",
1398
+ parse_mode="HTML"
1399
+ )
1400
+ return
1401
+
1402
+ # 3. Perform physical deletion & SQLite sync
1403
+ import shutil
1404
+ try:
1405
+ if os.path.exists(target_path):
1406
+ # Clean up local SQLite DB session history inside the workspace
1407
+ # Let's delete all sessions associated with this path
1408
+ # Since workspace paths are normalized, let's normalize this target path
1409
+ norm_target_path = os.path.normcase(target_path)
1410
+
1411
+ # Fetch all sessions in the DB to match work_dir
1412
+ cursor = await session_mgr._db.execute("SELECT opencode_session_id, work_dir FROM sessions WHERE user_id = ?", (user_id,))
1413
+ rows = await cursor.fetchall()
1414
+ stale_sids = []
1415
+
1416
+ def norm(p):
1417
+ if not p:
1418
+ return ""
1419
+ return os.path.normcase(os.path.normpath(os.path.abspath(p)))
1420
+
1421
+ for row in rows:
1422
+ sid, wd = row
1423
+ if norm(wd) == norm_target_path:
1424
+ stale_sids.append(sid)
1425
+
1426
+ if stale_sids:
1427
+ for sid in stale_sids:
1428
+ await session_mgr._db.execute(
1429
+ "DELETE FROM sessions WHERE user_id = ? AND opencode_session_id = ?",
1430
+ (user_id, sid)
1431
+ )
1432
+ await session_mgr._db.commit()
1433
+
1434
+ # Clear session mgr active cache if active
1435
+ active_sid = await session_mgr.get_active_session(user_id)
1436
+ if active_sid in stale_sids:
1437
+ if user_id in session_mgr._active_sessions:
1438
+ del session_mgr._active_sessions[user_id]
1439
+
1440
+ # Physical rmtree
1441
+ shutil.rmtree(target_path)
1442
+
1443
+ await query.edit_message_text(
1444
+ f"🗑️ <b>Folder Deleted Successfully!</b>\n\n"
1445
+ f"The workspace folder <code>{html.escape(target_folder)}</code> and all its files have been permanently erased from your hard drive, "
1446
+ f"and all session logs are cleared.",
1447
+ parse_mode="HTML"
1448
+ )
1449
+ else:
1450
+ await query.edit_message_text(
1451
+ f"❌ <b>Folder not found:</b> The folder <code>{html.escape(target_folder)}</code> no longer exists on disk.",
1452
+ parse_mode="HTML"
1453
+ )
1454
+ except Exception as e:
1455
+ logger.error(f"Failed to delete folder {target_path}: {e}", exc_info=True)
1456
+ await query.edit_message_text(
1457
+ f"❌ <b>Deletion Failed:</b> An error occurred while erasing the directory:\n"
1458
+ f"<code>{html.escape(str(e))}</code>",
1459
+ parse_mode="HTML"
1460
+ )
1461
+
1462
+ elif data == "delproj_cancel":
1463
+ await query.edit_message_text(
1464
+ "↩️ <b>Deletion Cancelled.</b>\n\n"
1465
+ "Your workspace folder and code remain completely untouched.",
1466
+ parse_mode="HTML"
1467
+ )
1468
+
1469
+
1470
+ # ──────────────────────────────────────────────
1471
+ # Commands: /create_project and /delete_project
1472
+ # ──────────────────────────────────────────────
1473
+
1474
+ async def create_project_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
1475
+ """Create a new workspace folder inside the base directory."""
1476
+ import os
1477
+ import html
1478
+
1479
+ user_id = update.effective_user.id
1480
+ config = context.bot_data["config"]
1481
+
1482
+ if not context.args:
1483
+ await update.message.reply_text(
1484
+ "⚠️ <b>Usage:</b> /create_project <code>&lt;new_folder_name&gt;</code>\n\n"
1485
+ "Example: <code>/create_project backend-api</code>",
1486
+ parse_mode="HTML"
1487
+ )
1488
+ return
1489
+
1490
+ name = " ".join(context.args).strip()
1491
+
1492
+ # Strict filename sanitization (keep alphanumeric, space, dash, underscore)
1493
+ cleaned_name = "".join(c for c in name if c.isalnum() or c in ("-", "_", " ")).strip()
1494
+
1495
+ if not cleaned_name:
1496
+ await update.message.reply_text(
1497
+ "⚠️ <b>Invalid folder name.</b> Only alphanumeric characters, dashes, underscores, and spaces are allowed.",
1498
+ parse_mode="HTML"
1499
+ )
1500
+ return
1501
+
1502
+ base_dir = os.path.abspath(config.opencode_work_dir)
1503
+ target_path = os.path.abspath(os.path.join(base_dir, cleaned_name))
1504
+
1505
+ # Security traversal check
1506
+ if not os.path.normcase(target_path).startswith(os.path.normcase(base_dir)):
1507
+ await update.message.reply_text(
1508
+ "❌ <b>Security Violation:</b> Target path lies outside your parent workspace directory.",
1509
+ parse_mode="HTML"
1510
+ )
1511
+ return
1512
+
1513
+ # Check existence
1514
+ if os.path.exists(target_path):
1515
+ if os.path.isdir(target_path):
1516
+ await update.message.reply_text(
1517
+ f"ℹ️ <b>Folder already exists.</b> Switching you to <code>{html.escape(cleaned_name)}</code>...",
1518
+ parse_mode="HTML"
1519
+ )
1520
+ await execute_project_switch(update, context, user_id, target_path)
1521
+ else:
1522
+ await update.message.reply_text(
1523
+ f"❌ <b>File conflict:</b> A file named <code>{html.escape(cleaned_name)}</code> already exists at that path.",
1524
+ parse_mode="HTML"
1525
+ )
1526
+ return
1527
+
1528
+ # Physical directory creation
1529
+ try:
1530
+ os.makedirs(target_path, exist_ok=True)
1531
+ await execute_project_switch(update, context, user_id, target_path)
1532
+ except Exception as e:
1533
+ logger.error(f"Failed to create workspace folder {target_path}: {e}", exc_info=True)
1534
+ await update.message.reply_text(
1535
+ f"❌ <b>Folder Creation Failed:</b>\n<code>{html.escape(str(e))}</code>",
1536
+ parse_mode="HTML"
1537
+ )
1538
+
1539
+
1540
+ async def delete_project_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
1541
+ """List workspaces in base directory for secure deletion."""
1542
+ import os
1543
+ import html
1544
+
1545
+ user_id = update.effective_user.id
1546
+ session_mgr = context.bot_data["session_manager"]
1547
+ config = context.bot_data["config"]
1548
+
1549
+ base_dir = os.path.abspath(config.opencode_work_dir)
1550
+
1551
+ # If the user passed a direct folder name in context.args
1552
+ if context.args:
1553
+ arg_folder = " ".join(context.args).strip()
1554
+ cleaned_arg = "".join(c for c in arg_folder if c.isalnum() or c in ("-", "_", " ")).strip()
1555
+
1556
+ target_path = os.path.abspath(os.path.join(base_dir, cleaned_arg))
1557
+ if not os.path.normcase(target_path).startswith(os.path.normcase(base_dir)) or target_path == base_dir:
1558
+ await update.message.reply_text(
1559
+ "❌ <b>Security Violation:</b> Target path lies outside your parent workspace directory.",
1560
+ parse_mode="HTML"
1561
+ )
1562
+ return
1563
+
1564
+ if not os.path.exists(target_path) or not os.path.isdir(target_path):
1565
+ await update.message.reply_text(
1566
+ f"❌ <b>Workspace not found:</b> The folder <code>{html.escape(cleaned_arg)}</code> does not exist.",
1567
+ parse_mode="HTML"
1568
+ )
1569
+ return
1570
+
1571
+ # Verify it's not active
1572
+ current_dir = await session_mgr.get_user_work_dir(user_id, base_dir)
1573
+ current_dir = os.path.abspath(current_dir)
1574
+ if os.path.normcase(target_path) == os.path.normcase(current_dir):
1575
+ await update.message.reply_text(
1576
+ f"❌ <b>Cannot Delete Active Workspace:</b>\n"
1577
+ f"You cannot delete your active workspace. Please switch to another workspace first using /project.",
1578
+ parse_mode="HTML"
1579
+ )
1580
+ return
1581
+
1582
+ warning_text = (
1583
+ f"⚠️ <b>WARNING: Permanent Deletion!</b>\n\n"
1584
+ f"Are you absolutely sure you want to permanently delete the folder <code>{html.escape(cleaned_arg)}</code>?\n\n"
1585
+ f"This will physically **erase all source files and directories** inside this workspace on your computer!\n\n"
1586
+ f"🚨 <b>THIS ACTION CANNOT BE UNDONE!</b>"
1587
+ )
1588
+
1589
+ keyboard = [
1590
+ [
1591
+ InlineKeyboardButton("💥 Yes, Erase Folder", callback_data=f"delproj_confirm:{cleaned_arg}"),
1592
+ InlineKeyboardButton("↩️ Cancel", callback_data="delproj_cancel")
1593
+ ]
1594
+ ]
1595
+
1596
+ await update.message.reply_text(warning_text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="HTML")
1597
+ return
1598
+
1599
+ # Otherwise list all subfolders for interactive selection
1600
+ subdirs = get_subdirectories(base_dir)
1601
+
1602
+ if not subdirs:
1603
+ await update.message.reply_text(
1604
+ "📭 <b>No workspace subdirectories found to delete.</b>",
1605
+ parse_mode="HTML"
1606
+ )
1607
+ return
1608
+
1609
+ current_dir = await session_mgr.get_user_work_dir(user_id, base_dir)
1610
+ current_dir = os.path.abspath(current_dir)
1611
+
1612
+ text = (
1613
+ "🗑️ <b>Delete Workspace Folder</b>\n\n"
1614
+ "Select a folder below to permanently delete it from your local machine. "
1615
+ "Stale session histories will be synced automatically.\n\n"
1616
+ "⚠️ <b>WARNING:</b> This erases physical code files!"
1617
+ )
1618
+
1619
+ keyboard = []
1620
+ for name in subdirs:
1621
+ target_path = os.path.abspath(os.path.join(base_dir, name))
1622
+ is_active = (os.path.normcase(target_path) == os.path.normcase(current_dir))
1623
+
1624
+ # Don't show delete button for the currently active workspace
1625
+ if is_active:
1626
+ continue
1627
+
1628
+ button_text = f"❌ Delete {name}"
1629
+ keyboard.append([InlineKeyboardButton(button_text, callback_data=f"delproj_ask:{name}")])
1630
+
1631
+ if not keyboard:
1632
+ await update.message.reply_text(
1633
+ "📭 <b>No other workspace folders found to delete.</b>\n"
1634
+ "<i>(You cannot delete your currently active workspace)</i>",
1635
+ parse_mode="HTML"
1636
+ )
1637
+ return
1638
+
1639
+ keyboard.append([InlineKeyboardButton("↩️ Cancel", callback_data="delproj_cancel")])
1640
+
1641
+ await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="HTML")
1642
+
@@ -415,7 +415,7 @@ async def _listen_and_stream_events(
415
415
  except Exception as e:
416
416
  logger.debug(f"Failed to update status message: {e}")
417
417
 
418
- async with aiohttp.ClientSession() as sse_session:
418
+ async with aiohttp.ClientSession(read_bufsize=100 * 1024 * 1024) as sse_session:
419
419
  try:
420
420
  async with sse_session.get(url, headers={"Accept": "text/event-stream"}) as resp:
421
421
  async for line in resp.content:
@@ -133,6 +133,7 @@ class OpenCodeClient:
133
133
  self._session = aiohttp.ClientSession(
134
134
  timeout=self.timeout,
135
135
  auth=self._auth,
136
+ read_bufsize=100 * 1024 * 1024,
136
137
  )
137
138
  return self._session
138
139
 
@@ -489,3 +490,14 @@ class OpenCodeClient:
489
490
  )
490
491
  raise
491
492
 
493
+ async def delete_session(self, session_id: str) -> bool:
494
+ """Permanently delete a session and all its context on the OpenCode server."""
495
+ try:
496
+ result = await self._request("DELETE", f"/session/{session_id}")
497
+ if isinstance(result, dict):
498
+ return result.get("success", True)
499
+ return True
500
+ except Exception as e:
501
+ logger.error(f"Failed to delete session {session_id} on server: {e}")
502
+ raise
503
+
@@ -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.5"
7
+ version = "0.1.6"
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.5
3
+ Version: 0.1.6
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
@@ -23,7 +23,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
23
23
 
24
24
  - **Direct OpenCode integration** — routes your messages to OpenCode's HTTP API
25
25
  - **Persistent sessions** — conversations maintain context across messages
26
- - **Session management** — create, switch, list, and share sessions
26
+ - **Session management** — create, delete, switch, list, and share sessions
27
27
  - **Model switching** — change AI models on the fly (`/model`)
28
28
  - **Plan/Build modes** — toggle between read-only analysis and full execution
29
29
  - **Smart formatting** — code blocks with syntax highlighting in Telegram
@@ -31,6 +31,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
31
31
  - **Security** — whitelist-based access control + rate limiting
32
32
  - **Workspace Switching** — Switching from one workspace to another
33
33
  - **Uploading from Mobile to WorkSpace** - U can upload the documents/images from your mobile to the opencode agent workspace
34
+ - **Workspace Management** — Creation and deletion of the workspace/folder
34
35
 
35
36
  ## 📋 Prerequisites
36
37
 
@@ -46,7 +47,7 @@ A lightweight Python bot that bridges your Telegram messages directly to [OpenCo
46
47
 
47
48
  ### Quick Installation:
48
49
  ```bash
49
- pip install telegram-opencode-bridge-bot==0.1.5
50
+ pip install telegram-opencode-bridge-bot==0.1.6
50
51
  telegram-opencode-bot
51
52
  telegram-opencode-bot --env (use --env flag if u want to re-configure later anytime)
52
53
  ```
@@ -88,6 +89,9 @@ Open Telegram, find your bot, and start asking! 🎉
88
89
  | `/project` | To view the current workspace, sub folder workspaces and to change the workspace |
89
90
  | `/enable` | To enable the streaming |
90
91
  | `/disable` | To disable the streaming |
92
+ | `/create_project` | To create new project/folder/workspace |
93
+ | `/delete_project` | To create new project/folder/workspace |
94
+ | `/delete` | To delete a conversation |
91
95
 
92
96
  ## 🏗️ Architecture
93
97