telegram-opencode-bridge-bot 0.1.0__py3-none-any.whl

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.
sessions/manager.py ADDED
@@ -0,0 +1,342 @@
1
+ import aiosqlite
2
+ import logging
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class SessionManager:
10
+ """Manages OpenCode sessions per Telegram user with SQLite persistence."""
11
+
12
+ def __init__(self, db_path: str = "sessions.db"):
13
+ self.db_path = db_path
14
+ self._db: Optional[aiosqlite.Connection] = None
15
+ # In-memory cache for fast lookups
16
+ self._active_sessions: Dict[int, Dict[str, Any]] = {}
17
+
18
+ async def initialize(self) -> None:
19
+ """Initialize the database and create tables if needed."""
20
+ self._db = await aiosqlite.connect(self.db_path)
21
+
22
+ # 1. Create sessions table with work_dir support
23
+ await self._db.execute("""
24
+ CREATE TABLE IF NOT EXISTS sessions (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ user_id INTEGER NOT NULL,
27
+ opencode_session_id TEXT NOT NULL,
28
+ model TEXT DEFAULT '',
29
+ mode TEXT DEFAULT 'build',
30
+ message_count INTEGER DEFAULT 0,
31
+ created_at TEXT NOT NULL,
32
+ last_active TEXT NOT NULL,
33
+ is_active INTEGER DEFAULT 1,
34
+ name TEXT DEFAULT '',
35
+ work_dir TEXT DEFAULT ''
36
+ )
37
+ """)
38
+
39
+ # 2. Create user settings table for preferred workspace directories and scan depth
40
+ await self._db.execute("""
41
+ CREATE TABLE IF NOT EXISTS user_settings (
42
+ user_id INTEGER PRIMARY KEY,
43
+ work_dir TEXT NOT NULL,
44
+ scan_depth INTEGER DEFAULT 2
45
+ )
46
+ """)
47
+
48
+ # 3. Safely migrate existing databases to add columns if missing
49
+ try:
50
+ await self._db.execute("ALTER TABLE sessions ADD COLUMN name TEXT DEFAULT ''")
51
+ except aiosqlite.OperationalError:
52
+ pass
53
+
54
+ try:
55
+ await self._db.execute("ALTER TABLE sessions ADD COLUMN work_dir TEXT DEFAULT ''")
56
+ except aiosqlite.OperationalError:
57
+ pass
58
+
59
+ try:
60
+ await self._db.execute("ALTER TABLE user_settings ADD COLUMN scan_depth INTEGER DEFAULT 2")
61
+ except aiosqlite.OperationalError:
62
+ pass
63
+
64
+ try:
65
+ await self._db.execute("ALTER TABLE user_settings ADD COLUMN streaming INTEGER DEFAULT 0")
66
+ except aiosqlite.OperationalError:
67
+ pass
68
+
69
+ await self._db.execute("""
70
+ CREATE INDEX IF NOT EXISTS idx_user_active
71
+ ON sessions(user_id, is_active)
72
+ """)
73
+ await self._db.commit()
74
+
75
+ # 4. Load active sessions into memory
76
+ async with self._db.execute(
77
+ "SELECT user_id, opencode_session_id, model, mode, message_count, created_at, last_active, name, work_dir "
78
+ "FROM sessions WHERE is_active = 1"
79
+ ) as cursor:
80
+ async for row in cursor:
81
+ self._active_sessions[row[0]] = {
82
+ "session_id": row[1],
83
+ "model": row[2],
84
+ "mode": row[3],
85
+ "message_count": row[4],
86
+ "created_at": row[5],
87
+ "last_active": row[6],
88
+ "name": row[7] if len(row) > 7 else "",
89
+ "work_dir": row[8] if len(row) > 8 else "",
90
+ }
91
+
92
+ logger.info(f"Session manager initialized. {len(self._active_sessions)} active sessions loaded.")
93
+
94
+ async def get_active_session(self, user_id: int) -> Optional[str]:
95
+ """Get the active OpenCode session ID for a user, or None."""
96
+ session = self._active_sessions.get(user_id)
97
+ return session["session_id"] if session else None
98
+
99
+ async def get_session_info(self, user_id: int) -> Optional[Dict[str, Any]]:
100
+ """Get full session info for a user."""
101
+ return self._active_sessions.get(user_id)
102
+
103
+ async def set_active_session(
104
+ self, user_id: int, opencode_session_id: str, model: str = "", work_dir: str = ""
105
+ ) -> None:
106
+ """Set or create an active session for a user."""
107
+ now = datetime.utcnow().isoformat()
108
+
109
+ # Deactivate any existing active session
110
+ if user_id in self._active_sessions:
111
+ await self._db.execute(
112
+ "UPDATE sessions SET is_active = 0, last_active = ? WHERE user_id = ? AND is_active = 1",
113
+ (now, user_id)
114
+ )
115
+
116
+ # Insert new active session
117
+ await self._db.execute(
118
+ "INSERT INTO sessions (user_id, opencode_session_id, model, created_at, last_active, name, work_dir) VALUES (?, ?, ?, ?, ?, ?, ?)",
119
+ (user_id, opencode_session_id, model, now, now, "", work_dir)
120
+ )
121
+ await self._db.commit()
122
+
123
+ # Update cache
124
+ self._active_sessions[user_id] = {
125
+ "session_id": opencode_session_id,
126
+ "model": model,
127
+ "mode": "build",
128
+ "message_count": 0,
129
+ "created_at": now,
130
+ "last_active": now,
131
+ "name": "",
132
+ "work_dir": work_dir,
133
+ }
134
+
135
+ logger.info(f"New session for user {user_id}: {opencode_session_id}")
136
+
137
+ async def increment_message_count(self, user_id: int, prompt: Optional[str] = None) -> None:
138
+ """Increment the message count for a user's active session and optionally set name."""
139
+ if user_id in self._active_sessions:
140
+ self._active_sessions[user_id]["message_count"] += 1
141
+ now = datetime.utcnow().isoformat()
142
+ self._active_sessions[user_id]["last_active"] = now
143
+
144
+ name_to_set = None
145
+ if prompt and (not self._active_sessions[user_id].get("name") or self._active_sessions[user_id]["message_count"] == 1):
146
+ # Clean up prompt for a nice title (strip newlines/spaces)
147
+ clean_prompt = " ".join(prompt.split())
148
+ short_name = clean_prompt[:35] + "..." if len(clean_prompt) > 35 else clean_prompt
149
+ self._active_sessions[user_id]["name"] = short_name
150
+ name_to_set = short_name
151
+
152
+ if name_to_set:
153
+ await self._db.execute(
154
+ "UPDATE sessions SET message_count = message_count + 1, last_active = ?, name = ? "
155
+ "WHERE user_id = ? AND is_active = 1",
156
+ (now, name_to_set, user_id)
157
+ )
158
+ else:
159
+ await self._db.execute(
160
+ "UPDATE sessions SET message_count = message_count + 1, last_active = ? "
161
+ "WHERE user_id = ? AND is_active = 1",
162
+ (now, user_id)
163
+ )
164
+ await self._db.commit()
165
+
166
+ async def set_mode(self, user_id: int, mode: str) -> None:
167
+ """Set the mode (build/plan) for a user's active session."""
168
+ if user_id in self._active_sessions:
169
+ self._active_sessions[user_id]["mode"] = mode
170
+ await self._db.execute(
171
+ "UPDATE sessions SET mode = ? WHERE user_id = ? AND is_active = 1",
172
+ (mode, user_id)
173
+ )
174
+ await self._db.commit()
175
+
176
+ async def set_model(self, user_id: int, model: str) -> None:
177
+ """Set the model for a user's active session."""
178
+ if user_id in self._active_sessions:
179
+ self._active_sessions[user_id]["model"] = model
180
+ await self._db.execute(
181
+ "UPDATE sessions SET model = ? WHERE user_id = ? AND is_active = 1",
182
+ (model, user_id)
183
+ )
184
+ await self._db.commit()
185
+
186
+ async def clear_session(self, user_id: int) -> None:
187
+ """Deactivate the current session for a user (they'll get a new one on next message)."""
188
+ if user_id in self._active_sessions:
189
+ now = datetime.utcnow().isoformat()
190
+ await self._db.execute(
191
+ "UPDATE sessions SET is_active = 0, last_active = ? WHERE user_id = ? AND is_active = 1",
192
+ (now, user_id)
193
+ )
194
+ await self._db.commit()
195
+ del self._active_sessions[user_id]
196
+ logger.info(f"Session cleared for user {user_id}")
197
+
198
+ async def list_user_sessions(self, user_id: int) -> List[Dict[str, Any]]:
199
+ """List all sessions (active and archived) for a user."""
200
+ sessions = []
201
+ async with self._db.execute(
202
+ "SELECT opencode_session_id, model, mode, message_count, created_at, last_active, is_active, name, work_dir "
203
+ "FROM sessions WHERE user_id = ? ORDER BY created_at DESC LIMIT 20",
204
+ (user_id,)
205
+ ) as cursor:
206
+ async for row in cursor:
207
+ sessions.append({
208
+ "session_id": row[0],
209
+ "model": row[1],
210
+ "mode": row[2],
211
+ "message_count": row[3],
212
+ "created_at": row[4],
213
+ "last_active": row[5],
214
+ "is_active": bool(row[6]),
215
+ "name": row[7] if len(row) > 7 else "",
216
+ "work_dir": row[8] if len(row) > 8 else "",
217
+ })
218
+ return sessions
219
+
220
+ async def switch_session(self, user_id: int, session_id: str) -> bool:
221
+ """Switch a user to a different existing session."""
222
+ # Check if session exists for this user
223
+ async with self._db.execute(
224
+ "SELECT opencode_session_id, model, mode, message_count, created_at, name, work_dir "
225
+ "FROM sessions WHERE user_id = ? AND opencode_session_id = ?",
226
+ (user_id, session_id)
227
+ ) as cursor:
228
+ row = await cursor.fetchone()
229
+ if not row:
230
+ return False
231
+
232
+ now = datetime.utcnow().isoformat()
233
+
234
+ # Deactivate current
235
+ await self._db.execute(
236
+ "UPDATE sessions SET is_active = 0 WHERE user_id = ? AND is_active = 1",
237
+ (user_id,)
238
+ )
239
+
240
+ # Activate target
241
+ await self._db.execute(
242
+ "UPDATE sessions SET is_active = 1, last_active = ? WHERE user_id = ? AND opencode_session_id = ?",
243
+ (now, user_id, session_id)
244
+ )
245
+ await self._db.commit()
246
+
247
+ # Update cache
248
+ self._active_sessions[user_id] = {
249
+ "session_id": row[0],
250
+ "model": row[1],
251
+ "mode": row[2],
252
+ "message_count": row[3],
253
+ "created_at": row[4],
254
+ "last_active": now,
255
+ "name": row[5] or "",
256
+ "work_dir": row[6] or "",
257
+ }
258
+
259
+ logger.info(f"User {user_id} switched to session {session_id}")
260
+ return True
261
+
262
+ async def get_user_work_dir(self, user_id: int, default_dir: str) -> str:
263
+ """Get the active workspace/project directory for a specific user, falling back to default."""
264
+ async with self._db.execute(
265
+ "SELECT work_dir FROM user_settings WHERE user_id = ?",
266
+ (user_id,)
267
+ ) as cursor:
268
+ row = await cursor.fetchone()
269
+ if row and row[0]:
270
+ return row[0]
271
+ return default_dir
272
+
273
+ async def set_user_work_dir(self, user_id: int, work_dir: str) -> None:
274
+ """Save or update the preferred workspace directory for a user, preserving scan_depth."""
275
+ await self._db.execute(
276
+ "INSERT INTO user_settings (user_id, work_dir, scan_depth) VALUES (?, ?, 2) "
277
+ "ON CONFLICT(user_id) DO UPDATE SET work_dir = excluded.work_dir",
278
+ (user_id, work_dir)
279
+ )
280
+ await self._db.commit()
281
+ logger.info(f"Set preferred project directory for user {user_id}: {work_dir}")
282
+
283
+ async def get_user_scan_depth(self, user_id: int, default_depth: int = 2) -> int:
284
+ """Get the preferred recursive scan depth for a specific user, falling back to default."""
285
+ async with self._db.execute(
286
+ "SELECT scan_depth FROM user_settings WHERE user_id = ?",
287
+ (user_id,)
288
+ ) as cursor:
289
+ row = await cursor.fetchone()
290
+ if row and row[0] is not None:
291
+ return int(row[0])
292
+ return default_depth
293
+
294
+ async def set_user_scan_depth(self, user_id: int, depth: int) -> None:
295
+ """Save or update the preferred recursive scan depth for a user, preserving work_dir."""
296
+ await self._db.execute(
297
+ "INSERT INTO user_settings (user_id, work_dir, scan_depth) VALUES (?, '', ?) "
298
+ "ON CONFLICT(user_id) DO UPDATE SET scan_depth = excluded.scan_depth",
299
+ (user_id, depth)
300
+ )
301
+ await self._db.commit()
302
+ logger.info(f"Set preferred project scan depth for user {user_id}: {depth}")
303
+
304
+ async def get_user_streaming(self, user_id: int, default_val: int = 0) -> int:
305
+ """Get the user's streaming preference (0 = disabled, 1 = enabled)."""
306
+ async with self._db.execute(
307
+ "SELECT streaming FROM user_settings WHERE user_id = ?",
308
+ (user_id,)
309
+ ) as cursor:
310
+ row = await cursor.fetchone()
311
+ if row and row[0] is not None:
312
+ return int(row[0])
313
+ return default_val
314
+
315
+ async def set_user_streaming(self, user_id: int, enabled: int) -> None:
316
+ """Save or update the user's streaming preference."""
317
+ await self._db.execute(
318
+ "INSERT INTO user_settings (user_id, work_dir, scan_depth, streaming) VALUES (?, '', 2, ?) "
319
+ "ON CONFLICT(user_id) DO UPDATE SET streaming = excluded.streaming",
320
+ (user_id, enabled)
321
+ )
322
+ await self._db.commit()
323
+ logger.info(f"Set preferred streaming for user {user_id}: {enabled}")
324
+
325
+ async def get_last_session_in_workspace(self, user_id: int, work_dir: str) -> Optional[str]:
326
+ """Find the most recently active session ID for a user in a specific workspace."""
327
+ async with self._db.execute(
328
+ "SELECT opencode_session_id FROM sessions "
329
+ "WHERE user_id = ? AND work_dir = ? "
330
+ "ORDER BY last_active DESC LIMIT 1",
331
+ (user_id, work_dir)
332
+ ) as cursor:
333
+ row = await cursor.fetchone()
334
+ if row:
335
+ return row[0]
336
+ return None
337
+
338
+ async def close(self) -> None:
339
+ """Close the database connection."""
340
+ if self._db:
341
+ await self._db.close()
342
+ logger.info("Session manager closed.")
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: telegram-opencode-bridge-bot
3
+ Version: 0.1.0
4
+ Summary: A Telegram bot that bridges messages directly to OpenCode — an AI coding agent running on your machine
5
+ Author-email: MaheshNagabhairava <your@email.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/MaheshNagabhairava/telegram-opencode-bridge-bot
8
+ Keywords: telegram,bot,opencode,ai,coding,bridge
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: python-telegram-bot[ext]>=21.0
14
+ Requires-Dist: aiohttp>=3.9.0
15
+ Requires-Dist: python-dotenv>=1.0.0
16
+ Requires-Dist: aiosqlite>=0.19.0
17
+
18
+ # 🤖 Telegram → OpenCode Bridge Bot
19
+
20
+ A lightweight Python bot that bridges your Telegram messages directly to [OpenCode](https://opencode.ai) — an AI coding agent running on your machine. Think of it as having Claude/GPT-powered coding assistance right in your pocket via Telegram.
21
+
22
+ ## ✨ Features
23
+
24
+ - **Direct OpenCode integration** — routes your messages to OpenCode's HTTP API
25
+ - **Persistent sessions** — conversations maintain context across messages
26
+ - **Session management** — create, switch, list, and share sessions
27
+ - **Model switching** — change AI models on the fly (`/model`)
28
+ - **Plan/Build modes** — toggle between read-only analysis and full execution
29
+ - **Smart formatting** — code blocks with syntax highlighting in Telegram
30
+ - **Auto message splitting** — handles responses longer than Telegram's 4096 char limit
31
+ - **Security** — whitelist-based access control + rate limiting
32
+ - **Workspace Switching** — Switching from one workspace to another
33
+
34
+ ## 📋 Prerequisites
35
+
36
+ 1. **Python 3.10+**
37
+ 2. **OpenCode CLI** — install via:
38
+ ```bash
39
+ npm install -g opencode-ai
40
+ # or
41
+ curl -fsSL https://opencode.ai/install | bash
42
+ ```
43
+ 3. **Telegram Bot Token** — get one from [@BotFather](https://t.me/BotFather)
44
+ 4. **Your Telegram User ID** — send `/id` to the bot after setup, or use [@userinfobot](https://t.me/userinfobot)
45
+
46
+ ## 🚀 Quick Start
47
+
48
+ ### 1. Clone & Install
49
+
50
+ ```bash
51
+ cd telegram-opencode-bot
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ ### 2. Start OpenCode Server
56
+
57
+ In a separate terminal:
58
+
59
+ ```bash
60
+ opencode serve --port 4096 --hostname 127.0.0.1
61
+ ```
62
+
63
+ This starts the OpenCode HTTP API on `localhost:4096`.
64
+
65
+ ### 3. Run the Bot
66
+
67
+ ```bash
68
+ python bot.py
69
+ python bot.py --env (if u want to setup ur configuration later)
70
+ ```
71
+ > **💡 Tip:** Don't know your Telegram user ID? Start the bot with `AUTHORIZED_USERS=0` temporarily, send `/id` to the bot, then update the `.env` with your real ID.
72
+
73
+ ### 4. Chat!
74
+
75
+ Open Telegram, find your bot, and start asking! 🎉
76
+
77
+ ## 📱 Commands
78
+
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `/start` | Welcome message & connection check |
82
+ | `/help` | Show all commands |
83
+ | `/new` | Start a fresh conversation |
84
+ | `/sessions` | List your recent sessions |
85
+ | `/switch <id>` | Switch to a different session |
86
+ | `/model <name>` | Change AI model(ex: /model opencode/deepseek-v4-flash-free) |
87
+ | `/mode <plan\|build>` | Toggle plan/build mode |
88
+ | `/share` | Share current session (public URL) |
89
+ | `/status` | Check connection & session details |
90
+ | `/id` | Show your Telegram user ID |
91
+ | `/stop` | Abort active model processing |
92
+ | `/models` | List all available models |
93
+ | `/project` | To view the current workspace, sub folder workspaces and to change the workspace (ex: /project 2)|
94
+ | `/project depth <1to5>` | Depth level recursive check for subfolders (ex: if u give depth 2, only 2 sub folders it will show from root) |
95
+ | `/enable` | To enable the streaming |
96
+ | `/disable` | To disable the streaming |
97
+
98
+ ## 🏗️ Architecture
99
+
100
+ ```
101
+ Telegram User
102
+
103
+
104
+ Telegram Bot (Python)
105
+
106
+ ├──► OpenCode HTTP API (localhost:4096) ← primary
107
+
108
+ ├──► LLM Provider (Claude/GPT/Gemini)
109
+ └──► Local Filesystem & Shell
110
+ ```
111
+
112
+ ## ⚙️ Configuration
113
+
114
+ | Variable | Description | Default |
115
+ |----------|-------------|---------|
116
+ | `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | **Required** |
117
+ | `AUTHORIZED_USERS` | Comma-separated Telegram user IDs | **Required** |
118
+ | `OPENCODE_SERVER_URL` | OpenCode HTTP API URL | `http://localhost:4096` |
119
+ | `OPENCODE_SERVER_USERNAME` | OpenCode auth username | *(empty)* |
120
+ | `OPENCODE_SERVER_PASSWORD` | OpenCode auth password | *(empty)* |
121
+ | `OPENCODE_MODEL` | Default AI model | `anthropic/claude-sonnet-4` |
122
+ | `OPENCODE_WORK_DIR` | Working directory for OpenCode | `.` |
123
+ | `MAX_MESSAGE_LENGTH` | Max Telegram message length | `4000` |
124
+ | `RESPONSE_TIMEOUT` | Max wait for response (seconds) | `0` |
125
+ | `DB_PATH` | SQLite database path | `sessions.db` |
126
+
127
+ ## 🔒 Security
128
+
129
+ - **User whitelist** — only Telegram user IDs in `AUTHORIZED_USERS` can use the bot
130
+ - **Rate limiting** — 20 requests per minute per user (configurable)
131
+ - **No public exposure** — designed to run on your local machine
132
+
133
+ > ⚠️ **Warning:** This bot executes AI-driven code on your machine. Only authorize trusted users.
134
+
135
+ ## 📁 Project Structure
136
+
137
+ ```
138
+ telegram-opencode-bot/
139
+ ├── bot.py # Main entry point
140
+ ├── config.py # Environment configuration
141
+ ├── handlers/
142
+ │ ├── commands.py # Slash command handlers
143
+ │ └── messages.py # Text message → OpenCode bridge
144
+ ├── opencode/
145
+ │ ├── client.py # OpenCode HTTP API client
146
+
147
+ ├── sessions/
148
+ │ └── manager.py # Per-user session tracking (SQLite)
149
+ ├── utils/
150
+ │ ├── formatting.py # Telegram message formatting
151
+ │ └── security.py # Auth, rate limiting, sanitization
152
+ ├── .env.example # Environment template
153
+ ├── requirements.txt # Python dependencies
154
+ └── README.md # This file
155
+ ```
156
+
@@ -0,0 +1,16 @@
1
+ handlers/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ handlers/commands.py,sha256=maetTTDVF9L-5OwdkA-ntXKYrGWJisFKPiy3yX8oo7A,43432
3
+ handlers/messages.py,sha256=KHfCdUXjH5LY14bPD0oX2U1qdPNIhaJOFhVpssNpbxk,21361
4
+ opencode/__init__.py,sha256=N8XuBa44IAcAx8JFoAK-Sho1xczreGDaG9pj0_wwaAw,224
5
+ opencode/client.py,sha256=o15SXko9KOnHP_kgMExSwqAp515XsR2xMrDulqpJ8jw,16729
6
+ opencode/server.py,sha256=VP_BCaVPwm9X_Z7qZ_6m5LLIacMHEnlrTrwDithdyHc,5362
7
+ sessions/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
8
+ sessions/manager.py,sha256=7PHHTVRAjDhOOfU24ezrmq6uCwv-HIiRsQ6m_s-kknA,14833
9
+ utils/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
+ utils/formatting.py,sha256=UD7zvl6TjoBVP-rYtup4G73v3lTKpjE8pJHSPYIxBsA,7137
11
+ utils/security.py,sha256=QDa4mIaFL7mUQ8OmCfFXBq-tUzZvdraQ1lumjWfUIdY,3931
12
+ telegram_opencode_bridge_bot-0.1.0.dist-info/METADATA,sha256=E6t5hIGpeGjeKPO7AYWTagYP3O9ApjbFZdVXJ1Y3dsg,6061
13
+ telegram_opencode_bridge_bot-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ telegram_opencode_bridge_bot-0.1.0.dist-info/entry_points.txt,sha256=y4R7q1zs1w1VqiGXaF71kU_2p4lLbJzfTvnlBcvnQ10,51
15
+ telegram_opencode_bridge_bot-0.1.0.dist-info/top_level.txt,sha256=k31OhktT-XbhuLxDtdPxwn96ACQNqs9o3dj-I-1BfgE,33
16
+ telegram_opencode_bridge_bot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ telegram-opencode-bot = bot:main
@@ -0,0 +1,4 @@
1
+ handlers
2
+ opencode
3
+ sessions
4
+ utils
utils/__init__.py ADDED
@@ -0,0 +1 @@
1
+