codex-telegram 0.5.0__tar.gz → 0.5.2__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.
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/PKG-INFO +29 -1
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/README.md +28 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/app_runtime/bootstrap.py +2 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/callbacks.py +28 -1
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/handlers.py +17 -3
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/projects.py +9 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/threads.py +18 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/event_forwarding.py +67 -4
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram/__init__.py +1 -1
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram/cli.py +50 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram.egg-info/PKG-INFO +29 -1
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram.egg-info/SOURCES.txt +6 -2
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/models/state.py +1 -0
- codex_telegram-0.5.2/models/telegram_bridge.py +77 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/models/user.py +66 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_callbacks_results.py +91 -1
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_handlers_results.py +50 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_main_approval_flow.py +205 -21
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_web_runtime.py +6 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_web_server_local_command.py +362 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/routes.py +118 -4
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/runtime.py +12 -0
- codex_telegram-0.5.2/web/static/dist/assets/index-DIOICBNe.js +251 -0
- codex_telegram-0.5.2/web/static/dist/assets/index-DW1AcoLg.css +1 -0
- codex_telegram-0.5.2/web/static/dist/favicon.ico +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/static/dist/index.html +3 -2
- codex_telegram-0.5.2/web/telegram_sync.py +79 -0
- codex_telegram-0.5.2/web/telegram_thread_selection.py +182 -0
- codex_telegram-0.5.0/web/static/dist/assets/index-CAl6dA4Q.js +0 -53
- codex_telegram-0.5.0/web/static/dist/assets/index-C_g51sA5.css +0 -1
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/LICENSE +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/MANIFEST.in +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/TELEGRAM.md +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/WEB.md +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/app_runtime/__init__.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/app_runtime/telegram_app.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/app_runtime/web_server.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/__init__.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/features_ui.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/keyboard.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/projects_ui.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/skills_ui.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/bot/thread_ui.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/__init__.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/approval_flow.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/approval_guardian.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/client.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/client_pool.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/collaboration_mode.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/__init__.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/common.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/context.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/contracts.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/core.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/review.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/command_router/system.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/commands.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/events.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex/protocol.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram.egg-info/dependency_links.txt +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram.egg-info/entry_points.txt +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram.egg-info/requires.txt +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/codex_telegram.egg-info/top_level.txt +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/conf.toml.example +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/main.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/models/__init__.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/models/thread.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/pyproject.toml +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/setup.cfg +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_approval_guardian.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_approval_policy.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_bootstrap_update_check.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_cli_entrypoint.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_codex_client.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_codex_client_pool.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_command_router.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_guardian_config.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_keyboard.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_normalize.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_pypi_check.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_single_instance.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_telegram_bot_config.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/tests/test_thread_history.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/__init__.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/approval_policy.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/config.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/local_command.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/logger.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/normalize.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/pypi_check.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/single_instance.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/utils/web_config.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/__init__.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/dependencies.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/server.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/static/dist/assets/codex-telegram-icon-black.svg +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/static/dist/assets/codex-telegram-icon-ivory.svg +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/static/dist/assets/codex-telegram-login-image.png +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/thread_history.py +0 -0
- {codex_telegram-0.5.0 → codex_telegram-0.5.2}/web/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-telegram
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
|
|
5
5
|
Author: heungtae
|
|
6
6
|
License: Apache License
|
|
@@ -292,6 +292,34 @@ python -m venv .venv && source .venv/bin/activate
|
|
|
292
292
|
pip install codex-telegram
|
|
293
293
|
```
|
|
294
294
|
|
|
295
|
+
### Update
|
|
296
|
+
|
|
297
|
+
The CLI checks for updates automatically on startup. When a newer version is available on PyPI, you'll see:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
✨ Update available! 0.5.1 -> 0.5.2
|
|
301
|
+
|
|
302
|
+
Release notes: https://github.com/heungtae/codex-telegram/releases/latest
|
|
303
|
+
|
|
304
|
+
1. Update now and exit (runs `pip install -U codex-telegram`)
|
|
305
|
+
2. Skip this run
|
|
306
|
+
3. Skip this version
|
|
307
|
+
|
|
308
|
+
Choose [1/2/3] (default: 2):
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
You can also update manually:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
pip install -U codex-telegram
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Or with system packages flag:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
pip install -U codex-telegram --break-system-packages
|
|
321
|
+
```
|
|
322
|
+
|
|
295
323
|
### 2. Configure
|
|
296
324
|
|
|
297
325
|
Create `~/.config/codex-telegram/conf.toml`:
|
|
@@ -67,6 +67,34 @@ python -m venv .venv && source .venv/bin/activate
|
|
|
67
67
|
pip install codex-telegram
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
### Update
|
|
71
|
+
|
|
72
|
+
The CLI checks for updates automatically on startup. When a newer version is available on PyPI, you'll see:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
✨ Update available! 0.5.1 -> 0.5.2
|
|
76
|
+
|
|
77
|
+
Release notes: https://github.com/heungtae/codex-telegram/releases/latest
|
|
78
|
+
|
|
79
|
+
1. Update now and exit (runs `pip install -U codex-telegram`)
|
|
80
|
+
2. Skip this run
|
|
81
|
+
3. Skip this version
|
|
82
|
+
|
|
83
|
+
Choose [1/2/3] (default: 2):
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can also update manually:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -U codex-telegram
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or with system packages flag:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install -U codex-telegram --break-system-packages
|
|
96
|
+
```
|
|
97
|
+
|
|
70
98
|
### 2. Configure
|
|
71
99
|
|
|
72
100
|
Create `~/.config/codex-telegram/conf.toml`:
|
|
@@ -56,6 +56,7 @@ async def post_init(
|
|
|
56
56
|
match_approval_policy: Callable[..., Any],
|
|
57
57
|
to_thread: Callable[..., Awaitable[Any]],
|
|
58
58
|
) -> None:
|
|
59
|
+
state.telegram_app = app
|
|
59
60
|
state.codex_client = await setup_codex_fn()
|
|
60
61
|
state.command_router = command_router_factory(state.codex_client)
|
|
61
62
|
state.approval_guardian = approval_guardian_factory()
|
|
@@ -95,6 +96,7 @@ async def post_shutdown(
|
|
|
95
96
|
*,
|
|
96
97
|
state_module=state,
|
|
97
98
|
) -> None:
|
|
99
|
+
state_module.telegram_app = None
|
|
98
100
|
if state_module.codex_client:
|
|
99
101
|
await state_module.codex_client.stop()
|
|
100
102
|
state_module.codex_client = None
|
|
@@ -14,6 +14,10 @@ from models import state
|
|
|
14
14
|
from models.user import user_manager
|
|
15
15
|
from codex.commands import CommandResult
|
|
16
16
|
from web.runtime import event_hub
|
|
17
|
+
from web.telegram_thread_selection import (
|
|
18
|
+
format_telegram_active_thread_changed_message,
|
|
19
|
+
preview_from_read_text,
|
|
20
|
+
)
|
|
17
21
|
|
|
18
22
|
logger = logging.getLogger("codex-telegram.bot")
|
|
19
23
|
GUARDIAN_WEB_ONLY_TEXT = "Guardian settings and rules can be edited in Web UI only."
|
|
@@ -227,6 +231,12 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
227
231
|
if command == "start":
|
|
228
232
|
logger.info("Executing callback action user_id=%s data=%s", user_id, data)
|
|
229
233
|
result = await run_callback_command("/start", user_id)
|
|
234
|
+
logger.info(
|
|
235
|
+
"Telegram callback /start result user_id=%s kind=%s thread_id=%s",
|
|
236
|
+
user_id,
|
|
237
|
+
result.kind,
|
|
238
|
+
result.meta.get("thread_id"),
|
|
239
|
+
)
|
|
230
240
|
await context.bot.send_message(chat_id=chat_id, text=result.text, reply_markup=_main_menu_markup(user_id))
|
|
231
241
|
elif command == "menu":
|
|
232
242
|
logger.info("Executing callback action user_id=%s data=%s", user_id, data)
|
|
@@ -400,7 +410,24 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
400
410
|
thread_id = data[7:]
|
|
401
411
|
logger.info("Executing callback action user_id=%s data=%s", user_id, data)
|
|
402
412
|
result = await state.command_router.route("/resume", [thread_id], user_id)
|
|
403
|
-
|
|
413
|
+
if result.kind == "error":
|
|
414
|
+
await edit_with_log(
|
|
415
|
+
query,
|
|
416
|
+
context,
|
|
417
|
+
result.text,
|
|
418
|
+
user_id,
|
|
419
|
+
reply_markup=_main_menu_markup(user_id),
|
|
420
|
+
)
|
|
421
|
+
return
|
|
422
|
+
resolved_thread_id = result.meta.get("thread_id") or thread_id
|
|
423
|
+
read_result = await state.command_router.route("/read", [resolved_thread_id], user_id)
|
|
424
|
+
preview = preview_from_read_text(resolved_thread_id, read_result.text)
|
|
425
|
+
message = format_telegram_active_thread_changed_message(
|
|
426
|
+
preview,
|
|
427
|
+
project_key=user_manager.get_thread_project(resolved_thread_id),
|
|
428
|
+
resume_status="ok",
|
|
429
|
+
)
|
|
430
|
+
await edit_with_log(query, context, message, user_id, reply_markup=_main_menu_markup(user_id))
|
|
404
431
|
|
|
405
432
|
elif data.startswith("fork:"):
|
|
406
433
|
thread_id = data[5:]
|
|
@@ -21,6 +21,7 @@ from bot.features_ui import features_keyboard, features_panel_text
|
|
|
21
21
|
from models import state
|
|
22
22
|
from utils.single_instance import find_local_conflict_candidates
|
|
23
23
|
from utils.local_command import run_bang_command
|
|
24
|
+
from web.telegram_sync import bind_telegram_thread_to_active_web
|
|
24
25
|
|
|
25
26
|
logger = logging.getLogger("codex-telegram.bot")
|
|
26
27
|
_last_conflict_log_at = 0.0
|
|
@@ -182,12 +183,15 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
182
183
|
)
|
|
183
184
|
return
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
active_thread_running_turn_id = state_user.get_turn_for_thread(state_user.active_thread_id)
|
|
187
|
+
if not active_thread_running_turn_id and state_user.active_turn_id and not state_user.active_turn_id_by_thread:
|
|
188
|
+
active_thread_running_turn_id = state_user.active_turn_id
|
|
189
|
+
if active_thread_running_turn_id:
|
|
186
190
|
await send_reply(
|
|
187
191
|
update,
|
|
188
192
|
(
|
|
189
193
|
"A turn is already running.\n"
|
|
190
|
-
f"turnId: {
|
|
194
|
+
f"turnId: {active_thread_running_turn_id}\n"
|
|
191
195
|
"Use /interrupt (or the Interrupt button) to stop it first."
|
|
192
196
|
),
|
|
193
197
|
user_id,
|
|
@@ -205,6 +209,12 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
205
209
|
"threadId": state_user.active_thread_id,
|
|
206
210
|
"input": [{"type": "text", "text": text}],
|
|
207
211
|
}
|
|
212
|
+
bound_user_ids = await bind_telegram_thread_to_active_web(
|
|
213
|
+
user_id,
|
|
214
|
+
state_user.active_thread_id,
|
|
215
|
+
project_key=user_manager.get_thread_project(state_user.active_thread_id),
|
|
216
|
+
notify_web=False,
|
|
217
|
+
)
|
|
208
218
|
collaboration_mode = await _require_turn_collaboration_mode(state_user)
|
|
209
219
|
params["collaborationMode"] = collaboration_mode
|
|
210
220
|
logger.info(
|
|
@@ -220,7 +230,11 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
|
220
230
|
turn = result.get("turn", {})
|
|
221
231
|
turn_id = turn.get("id", "unknown")
|
|
222
232
|
if isinstance(turn_id, str) and turn_id and turn_id != "unknown":
|
|
223
|
-
state_user.set_turn(turn_id)
|
|
233
|
+
state_user.set_turn(turn_id, state_user.active_thread_id)
|
|
234
|
+
user_manager.bind_turn(user_id, turn_id, state_user.active_thread_id)
|
|
235
|
+
for bound_user_id in bound_user_ids:
|
|
236
|
+
if bound_user_id != user_id:
|
|
237
|
+
user_manager.bind_turn_subscriber(bound_user_id, turn_id, state_user.active_thread_id)
|
|
224
238
|
|
|
225
239
|
if state_user.selected_project_path:
|
|
226
240
|
await send_reply(update, f"Turn started: {turn_id}\nWorkspace: {state_user.selected_project_path}", user_id)
|
|
@@ -210,6 +210,15 @@ class ProjectCommands:
|
|
|
210
210
|
if isinstance(new_thread_id, str) and new_thread_id:
|
|
211
211
|
user_manager.set_active_thread(user_id, new_thread_id, project_key=selected["key"])
|
|
212
212
|
state.clear_turn()
|
|
213
|
+
if user_id > 0:
|
|
214
|
+
from web.telegram_sync import bind_telegram_thread_to_active_web
|
|
215
|
+
|
|
216
|
+
await bind_telegram_thread_to_active_web(
|
|
217
|
+
user_id,
|
|
218
|
+
new_thread_id,
|
|
219
|
+
project_key=selected["key"],
|
|
220
|
+
notify_web=True,
|
|
221
|
+
)
|
|
213
222
|
else:
|
|
214
223
|
user_manager.clear_active_thread(user_id)
|
|
215
224
|
thread_start_note = "\nProject switched, but failed to create a new thread. Run /start."
|
|
@@ -126,6 +126,15 @@ class ThreadCommands:
|
|
|
126
126
|
|
|
127
127
|
if thread_id:
|
|
128
128
|
user_manager.set_active_thread(user_id, thread_id, project_key=(project or {}).get("key"))
|
|
129
|
+
if user_id > 0:
|
|
130
|
+
from web.telegram_sync import bind_telegram_thread_to_active_web
|
|
131
|
+
|
|
132
|
+
await bind_telegram_thread_to_active_web(
|
|
133
|
+
user_id,
|
|
134
|
+
thread_id,
|
|
135
|
+
project_key=(project or {}).get("key"),
|
|
136
|
+
notify_web=True,
|
|
137
|
+
)
|
|
129
138
|
|
|
130
139
|
if project:
|
|
131
140
|
return text_result(
|
|
@@ -149,6 +158,15 @@ class ThreadCommands:
|
|
|
149
158
|
|
|
150
159
|
await self.ctx.codex.call("thread/resume", {"threadId": thread_id})
|
|
151
160
|
user_manager.set_active_thread(user_id, thread_id)
|
|
161
|
+
if user_id > 0:
|
|
162
|
+
from web.telegram_sync import bind_telegram_thread_to_active_web
|
|
163
|
+
|
|
164
|
+
await bind_telegram_thread_to_active_web(
|
|
165
|
+
user_id,
|
|
166
|
+
thread_id,
|
|
167
|
+
project_key=user_manager.get_thread_project(thread_id),
|
|
168
|
+
notify_web=True,
|
|
169
|
+
)
|
|
152
170
|
return text_result(f"Thread resumed: {thread_id}", thread_id=thread_id)
|
|
153
171
|
|
|
154
172
|
async def fork(self, args: list[str], user_id: int) -> CommandResult:
|
|
@@ -977,10 +977,16 @@ def truncate_telegram_text(text: str, footer: str) -> str:
|
|
|
977
977
|
return text[:head_len] + suffix + footer
|
|
978
978
|
|
|
979
979
|
|
|
980
|
+
def telegram_turn_footer(turn_id: str | None) -> str:
|
|
981
|
+
if isinstance(turn_id, str) and turn_id:
|
|
982
|
+
return f"\n\nturnId: {turn_id}"
|
|
983
|
+
return ""
|
|
984
|
+
|
|
985
|
+
|
|
980
986
|
async def send_telegram_message(app, user_id: int, text: str, turn_id: str | None) -> None:
|
|
981
987
|
if user_id <= 0 or app is None or not text.strip():
|
|
982
988
|
return
|
|
983
|
-
footer =
|
|
989
|
+
footer = telegram_turn_footer(turn_id)
|
|
984
990
|
try:
|
|
985
991
|
await app.bot.send_message(
|
|
986
992
|
chat_id=user_id,
|
|
@@ -1020,7 +1026,7 @@ async def send_telegram_file_change(app, user_id: int, payload: dict[str, Any])
|
|
|
1020
1026
|
try:
|
|
1021
1027
|
last_index = len(lines) - 1
|
|
1022
1028
|
for index, line in enumerate(lines):
|
|
1023
|
-
footer =
|
|
1029
|
+
footer = telegram_turn_footer(turn_id) if index == last_index else ""
|
|
1024
1030
|
await app.bot.send_message(chat_id=user_id, text=truncate_telegram_text(line, footer))
|
|
1025
1031
|
if index < last_index:
|
|
1026
1032
|
await asyncio.sleep(FILE_CHANGE_LINE_DELAY_SECONDS)
|
|
@@ -1034,12 +1040,60 @@ async def send_telegram_plan(app, user_id: int, payload: dict[str, Any]) -> None
|
|
|
1034
1040
|
await send_telegram_message(app, user_id, f"Plan proposal\n\n{plan_text}", payload.get("turn_id"))
|
|
1035
1041
|
|
|
1036
1042
|
|
|
1043
|
+
def filter_telegram_targets_to_active_thread(
|
|
1044
|
+
method: str,
|
|
1045
|
+
target_user_ids: set[int],
|
|
1046
|
+
thread_id: str | None,
|
|
1047
|
+
turn_id: str | None,
|
|
1048
|
+
params: dict | None,
|
|
1049
|
+
) -> set[int]:
|
|
1050
|
+
if not target_user_ids:
|
|
1051
|
+
return set()
|
|
1052
|
+
payload_thread = (params or {}).get("thread")
|
|
1053
|
+
parent_thread_id = (
|
|
1054
|
+
payload_thread.get("forkedFromId")
|
|
1055
|
+
if isinstance(payload_thread, dict) and isinstance(payload_thread.get("forkedFromId"), str)
|
|
1056
|
+
else None
|
|
1057
|
+
)
|
|
1058
|
+
filtered: set[int] = set()
|
|
1059
|
+
for user_id in target_user_ids:
|
|
1060
|
+
if user_id <= 0:
|
|
1061
|
+
filtered.add(user_id)
|
|
1062
|
+
continue
|
|
1063
|
+
active_thread_id = user_manager.get(user_id).active_thread_id
|
|
1064
|
+
if (
|
|
1065
|
+
isinstance(thread_id, str)
|
|
1066
|
+
and thread_id
|
|
1067
|
+
and (active_thread_id == thread_id or active_thread_id == parent_thread_id)
|
|
1068
|
+
):
|
|
1069
|
+
filtered.add(user_id)
|
|
1070
|
+
continue
|
|
1071
|
+
if not thread_id and method in {"turn/completed", "turn/failed", "turn/cancelled"} and active_thread_id:
|
|
1072
|
+
filtered.add(user_id)
|
|
1073
|
+
continue
|
|
1074
|
+
logger.info(
|
|
1075
|
+
"Telegram forwarding skipped method=%s thread_id=%s turn_id=%s telegram_user_id=%s active_thread_id=%s reason=active_thread_mismatch",
|
|
1076
|
+
method,
|
|
1077
|
+
thread_id,
|
|
1078
|
+
turn_id,
|
|
1079
|
+
user_id,
|
|
1080
|
+
active_thread_id,
|
|
1081
|
+
)
|
|
1082
|
+
return filtered
|
|
1083
|
+
|
|
1084
|
+
|
|
1037
1085
|
def build_event_forwarder(app, config: ForwardingConfig):
|
|
1038
1086
|
async def forward_event(method: str, params: dict | None):
|
|
1039
1087
|
thread_id = extract_thread_id(method, params)
|
|
1040
1088
|
turn_id = extract_turn_id(method, params)
|
|
1041
1089
|
if thread_id is None and turn_id:
|
|
1042
1090
|
thread_id = user_manager.get_turn_thread(turn_id)
|
|
1091
|
+
if not turn_id and thread_id:
|
|
1092
|
+
for user_id in user_manager.find_user_ids_by_thread(thread_id):
|
|
1093
|
+
inferred_turn_id = user_manager.get(user_id).get_turn_for_thread(thread_id)
|
|
1094
|
+
if inferred_turn_id:
|
|
1095
|
+
turn_id = inferred_turn_id
|
|
1096
|
+
break
|
|
1043
1097
|
target_user_ids = set()
|
|
1044
1098
|
if turn_id:
|
|
1045
1099
|
target_user_ids.update(user_manager.find_user_ids_by_turn(turn_id))
|
|
@@ -1050,6 +1104,7 @@ def build_event_forwarder(app, config: ForwardingConfig):
|
|
|
1050
1104
|
fallback_owner = user_manager.find_single_active_turn_owner()
|
|
1051
1105
|
if fallback_owner is not None:
|
|
1052
1106
|
target_user_ids.add(fallback_owner)
|
|
1107
|
+
target_user_ids = filter_telegram_targets_to_active_thread(method, target_user_ids, thread_id, turn_id, params)
|
|
1053
1108
|
|
|
1054
1109
|
if method == "turn/started" and turn_id and target_user_ids:
|
|
1055
1110
|
actual_mode = normalize_mode_kind((params or {}).get("collaboration_mode_kind") or (params or {}).get("collaborationModeKind"))
|
|
@@ -1076,7 +1131,7 @@ def build_event_forwarder(app, config: ForwardingConfig):
|
|
|
1076
1131
|
try:
|
|
1077
1132
|
await app.bot.send_message(
|
|
1078
1133
|
chat_id=uid,
|
|
1079
|
-
text=truncate_telegram_text(message,
|
|
1134
|
+
text=truncate_telegram_text(message, telegram_turn_footer(turn_id)),
|
|
1080
1135
|
)
|
|
1081
1136
|
except Exception:
|
|
1082
1137
|
logger.exception("Failed to forward app-server error to Telegram")
|
|
@@ -1235,12 +1290,20 @@ def build_event_forwarder(app, config: ForwardingConfig):
|
|
|
1235
1290
|
if event_level(method, params) < config.threshold:
|
|
1236
1291
|
return
|
|
1237
1292
|
telegram_user_ids = [uid for uid in target_user_ids if uid > 0]
|
|
1293
|
+
logger.info(
|
|
1294
|
+
"Telegram forwarding targets method=%s thread_id=%s turn_id=%s target_user_ids=%s telegram_user_ids=%s",
|
|
1295
|
+
method,
|
|
1296
|
+
thread_id,
|
|
1297
|
+
turn_id,
|
|
1298
|
+
sorted(target_user_ids),
|
|
1299
|
+
sorted(telegram_user_ids),
|
|
1300
|
+
)
|
|
1238
1301
|
if not telegram_user_ids or app is None:
|
|
1239
1302
|
return
|
|
1240
1303
|
message = format_event(method, params, config.rules)
|
|
1241
1304
|
if message is None or not message.strip():
|
|
1242
1305
|
return
|
|
1243
|
-
footer =
|
|
1306
|
+
footer = telegram_turn_footer(turn_id)
|
|
1244
1307
|
for user_id in telegram_user_ids:
|
|
1245
1308
|
logger.info(
|
|
1246
1309
|
"Forwarding app-server event to Telegram user_id=%s method=%s message=%s",
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import json
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
5
8
|
from importlib.util import module_from_spec, spec_from_file_location
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from types import ModuleType
|
|
@@ -13,6 +16,44 @@ def _get_version() -> str:
|
|
|
13
16
|
return __version__
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
def _check_for_update(current_version: str) -> str | None:
|
|
20
|
+
"""Check PyPI for the latest version. Returns latest version if newer, else None."""
|
|
21
|
+
url = "https://pypi.org/pypi/codex-telegram/json"
|
|
22
|
+
try:
|
|
23
|
+
req = urllib.request.Request(url, headers={"User-Agent": "codex-telegram"})
|
|
24
|
+
with urllib.request.urlopen(req, timeout=3) as resp:
|
|
25
|
+
data = json.load(resp)
|
|
26
|
+
latest = data["info"]["version"]
|
|
27
|
+
if _version_gt(latest, current_version):
|
|
28
|
+
return latest
|
|
29
|
+
except (urllib.error.URLError, json.JSONDecodeError, KeyError, TimeoutError):
|
|
30
|
+
pass
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _version_gt(v1: str, v2: str) -> bool:
|
|
35
|
+
"""Compare version strings (simple tuple comparison)."""
|
|
36
|
+
def parse(v: str):
|
|
37
|
+
return tuple(int(x) for x in v.split(".") if x.isdigit())
|
|
38
|
+
return parse(v1) > parse(v2)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _prompt_update(latest: str) -> bool:
|
|
42
|
+
"""Prompt user to update. Returns True if user chooses to update."""
|
|
43
|
+
print(f"\n✨ Update available! {_get_version()} -> {latest}")
|
|
44
|
+
print(" Release notes: https://github.com/heungtae/codex-telegram/releases/latest")
|
|
45
|
+
print()
|
|
46
|
+
print(" 1. Update now and exit (runs `pip install -U codex-telegram`)")
|
|
47
|
+
print(" 2. Skip this run")
|
|
48
|
+
print(" 3. Skip this version")
|
|
49
|
+
print()
|
|
50
|
+
try:
|
|
51
|
+
choice = input(" Choose [1/2/3] (default: 2): ").strip() or "2"
|
|
52
|
+
except (EOFError, KeyboardInterrupt):
|
|
53
|
+
return False
|
|
54
|
+
return choice == "1"
|
|
55
|
+
|
|
56
|
+
|
|
16
57
|
def _load_root_main_module() -> ModuleType:
|
|
17
58
|
root_main = Path(__file__).resolve().parent.parent / "main.py"
|
|
18
59
|
spec = spec_from_file_location("codex_telegram_root_main", root_main)
|
|
@@ -24,6 +65,15 @@ def _load_root_main_module() -> ModuleType:
|
|
|
24
65
|
|
|
25
66
|
|
|
26
67
|
def main() -> int:
|
|
68
|
+
# Check for updates (skip if --version or --validate-config or --debug)
|
|
69
|
+
if "--version" not in sys.argv and "--validate-config" not in sys.argv and "--debug" not in sys.argv:
|
|
70
|
+
current = _get_version()
|
|
71
|
+
latest = _check_for_update(current)
|
|
72
|
+
if latest and _prompt_update(latest):
|
|
73
|
+
print("Running: pip install -U codex-telegram")
|
|
74
|
+
os.execvpe(sys.executable, [sys.executable, "-m", "pip", "install", "-U", "codex-telegram"], os.environ)
|
|
75
|
+
return 0
|
|
76
|
+
|
|
27
77
|
parser = argparse.ArgumentParser(
|
|
28
78
|
prog="codex-telegram",
|
|
29
79
|
description="Codex Telegram Bot - Bridges Codex App Server with Telegram and Web UI.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-telegram
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
|
|
5
5
|
Author: heungtae
|
|
6
6
|
License: Apache License
|
|
@@ -292,6 +292,34 @@ python -m venv .venv && source .venv/bin/activate
|
|
|
292
292
|
pip install codex-telegram
|
|
293
293
|
```
|
|
294
294
|
|
|
295
|
+
### Update
|
|
296
|
+
|
|
297
|
+
The CLI checks for updates automatically on startup. When a newer version is available on PyPI, you'll see:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
✨ Update available! 0.5.1 -> 0.5.2
|
|
301
|
+
|
|
302
|
+
Release notes: https://github.com/heungtae/codex-telegram/releases/latest
|
|
303
|
+
|
|
304
|
+
1. Update now and exit (runs `pip install -U codex-telegram`)
|
|
305
|
+
2. Skip this run
|
|
306
|
+
3. Skip this version
|
|
307
|
+
|
|
308
|
+
Choose [1/2/3] (default: 2):
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
You can also update manually:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
pip install -U codex-telegram
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Or with system packages flag:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
pip install -U codex-telegram --break-system-packages
|
|
321
|
+
```
|
|
322
|
+
|
|
295
323
|
### 2. Configure
|
|
296
324
|
|
|
297
325
|
Create `~/.config/codex-telegram/conf.toml`:
|
|
@@ -47,6 +47,7 @@ codex_telegram.egg-info/requires.txt
|
|
|
47
47
|
codex_telegram.egg-info/top_level.txt
|
|
48
48
|
models/__init__.py
|
|
49
49
|
models/state.py
|
|
50
|
+
models/telegram_bridge.py
|
|
50
51
|
models/thread.py
|
|
51
52
|
models/user.py
|
|
52
53
|
tests/test_approval_guardian.py
|
|
@@ -82,11 +83,14 @@ web/dependencies.py
|
|
|
82
83
|
web/routes.py
|
|
83
84
|
web/runtime.py
|
|
84
85
|
web/server.py
|
|
86
|
+
web/telegram_sync.py
|
|
87
|
+
web/telegram_thread_selection.py
|
|
85
88
|
web/thread_history.py
|
|
86
89
|
web/workspace.py
|
|
90
|
+
web/static/dist/favicon.ico
|
|
87
91
|
web/static/dist/index.html
|
|
88
92
|
web/static/dist/assets/codex-telegram-icon-black.svg
|
|
89
93
|
web/static/dist/assets/codex-telegram-icon-ivory.svg
|
|
90
94
|
web/static/dist/assets/codex-telegram-login-image.png
|
|
91
|
-
web/static/dist/assets/index-
|
|
92
|
-
web/static/dist/assets/index-
|
|
95
|
+
web/static/dist/assets/index-DIOICBNe.js
|
|
96
|
+
web/static/dist/assets/index-DW1AcoLg.css
|
|
@@ -7,5 +7,6 @@ from codex.approval_guardian import ApprovalGuardianService
|
|
|
7
7
|
codex_client: Any | None = None
|
|
8
8
|
command_router: CommandRouter | None = None
|
|
9
9
|
approval_guardian: ApprovalGuardianService | None = None
|
|
10
|
+
telegram_app: Any | None = None
|
|
10
11
|
codex_ready = asyncio.Event()
|
|
11
12
|
update_notified: bool = False
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from utils.config import get
|
|
2
|
+
|
|
3
|
+
from .user import user_manager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TelegramBridgeConfigError(ValueError):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def single_allowed_telegram_user_id() -> int | None:
|
|
11
|
+
allowed_ids = get("users.allowed_ids", [])
|
|
12
|
+
if not isinstance(allowed_ids, list) or len(allowed_ids) != 1:
|
|
13
|
+
return None
|
|
14
|
+
try:
|
|
15
|
+
user_id = int(allowed_ids[0])
|
|
16
|
+
except (TypeError, ValueError):
|
|
17
|
+
return None
|
|
18
|
+
if user_id <= 0:
|
|
19
|
+
return None
|
|
20
|
+
return user_id
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_single_allowed_telegram_user_id() -> int:
|
|
24
|
+
allowed_ids = get("users.allowed_ids", [])
|
|
25
|
+
if not isinstance(allowed_ids, list) or len(allowed_ids) != 1:
|
|
26
|
+
raise TelegramBridgeConfigError(
|
|
27
|
+
"Open in Telegram requires exactly one Telegram user in users.allowed_ids."
|
|
28
|
+
)
|
|
29
|
+
try:
|
|
30
|
+
user_id = int(allowed_ids[0])
|
|
31
|
+
except (TypeError, ValueError) as exc:
|
|
32
|
+
raise TelegramBridgeConfigError(
|
|
33
|
+
"Open in Telegram requires users.allowed_ids to contain one numeric Telegram user id."
|
|
34
|
+
) from exc
|
|
35
|
+
if user_id <= 0:
|
|
36
|
+
raise TelegramBridgeConfigError(
|
|
37
|
+
"Open in Telegram requires a positive Telegram user id in users.allowed_ids."
|
|
38
|
+
)
|
|
39
|
+
return user_id
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def bind_thread_counterparts(
|
|
43
|
+
user_id: int,
|
|
44
|
+
thread_id: str | None,
|
|
45
|
+
*,
|
|
46
|
+
project_key: str | None = None,
|
|
47
|
+
web_user_ids: list[int] | tuple[int, ...] | set[int] | None = None,
|
|
48
|
+
) -> set[int]:
|
|
49
|
+
if not isinstance(thread_id, str) or not thread_id:
|
|
50
|
+
return set()
|
|
51
|
+
|
|
52
|
+
user_manager.bind_thread_subscriber(user_id, thread_id)
|
|
53
|
+
if isinstance(project_key, str) and project_key:
|
|
54
|
+
user_manager.bind_thread_project(thread_id, project_key)
|
|
55
|
+
|
|
56
|
+
bound_user_ids = {user_id}
|
|
57
|
+
telegram_user_id = single_allowed_telegram_user_id()
|
|
58
|
+
if telegram_user_id is None:
|
|
59
|
+
return bound_user_ids
|
|
60
|
+
|
|
61
|
+
if user_id > 0:
|
|
62
|
+
if user_id != telegram_user_id:
|
|
63
|
+
return bound_user_ids
|
|
64
|
+
user_manager.keep_user_thread_subscription_only(user_id, thread_id)
|
|
65
|
+
for web_user_id in web_user_ids or ():
|
|
66
|
+
if isinstance(web_user_id, int) and web_user_id < 0:
|
|
67
|
+
user_manager.bind_thread_subscriber(web_user_id, thread_id)
|
|
68
|
+
bound_user_ids.add(web_user_id)
|
|
69
|
+
return bound_user_ids
|
|
70
|
+
|
|
71
|
+
if user_id < 0:
|
|
72
|
+
telegram_state = user_manager.get(telegram_user_id)
|
|
73
|
+
if telegram_state.active_thread_id == thread_id:
|
|
74
|
+
user_manager.keep_user_thread_subscription_only(telegram_user_id, thread_id)
|
|
75
|
+
bound_user_ids.add(telegram_user_id)
|
|
76
|
+
|
|
77
|
+
return bound_user_ids
|