codex-telegram 0.4.6__tar.gz → 0.4.8__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.4.6 → codex_telegram-0.4.8}/PKG-INFO +6 -2
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/client.py +24 -9
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram/__init__.py +1 -1
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram.egg-info/PKG-INFO +6 -2
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_codex_client.py +57 -1
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/LICENSE +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/MANIFEST.in +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/README.md +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/TELEGRAM.md +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/WEB.md +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/app_runtime/__init__.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/app_runtime/bootstrap.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/app_runtime/telegram_app.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/app_runtime/web_server.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/__init__.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/callbacks.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/features_ui.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/handlers.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/keyboard.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/projects_ui.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/skills_ui.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/bot/thread_ui.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/__init__.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/approval_flow.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/approval_guardian.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/collaboration_mode.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/__init__.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/common.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/context.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/contracts.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/core.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/projects.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/review.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/system.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/command_router/threads.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/commands.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/event_forwarding.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/events.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex/protocol.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram/cli.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram.egg-info/SOURCES.txt +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram.egg-info/dependency_links.txt +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram.egg-info/entry_points.txt +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram.egg-info/requires.txt +1 -1
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/codex_telegram.egg-info/top_level.txt +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/conf.toml.example +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/main.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/models/__init__.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/models/state.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/models/thread.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/models/user.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/pyproject.toml +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/setup.cfg +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_approval_guardian.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_approval_policy.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_callbacks_results.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_cli_entrypoint.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_command_router.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_guardian_config.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_handlers_results.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_keyboard.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_main_approval_flow.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_normalize.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_pypi_check.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_single_instance.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_telegram_bot_config.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_thread_history.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_web_runtime.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/tests/test_web_server_local_command.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/__init__.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/approval_policy.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/config.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/local_command.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/logger.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/normalize.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/pypi_check.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/single_instance.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/utils/web_config.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/__init__.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/dependencies.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/routes.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/runtime.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/server.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/static/dist/assets/index-B6IQl7AZ.js +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/static/dist/assets/index-Czx9yv3Y.css +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/static/dist/icons-tab-add.svg +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/static/dist/icons-tab-close.svg +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/static/dist/index.html +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/thread_history.py +0 -0
- {codex-telegram-0.4.6 → codex_telegram-0.4.8}/web/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-telegram
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
|
|
5
5
|
Author: heungtae
|
|
6
6
|
License: Apache License
|
|
@@ -218,6 +218,10 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
218
218
|
Requires-Python: >=3.11
|
|
219
219
|
Description-Content-Type: text/markdown
|
|
220
220
|
License-File: LICENSE
|
|
221
|
+
Requires-Dist: python-telegram-bot>=20.0
|
|
222
|
+
Requires-Dist: fastapi>=0.111.0
|
|
223
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
224
|
+
Dynamic: license-file
|
|
221
225
|
|
|
222
226
|
# Codex Telegram + Web
|
|
223
227
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import inspect
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from typing import Any, Callable
|
|
4
5
|
import logging
|
|
5
6
|
|
|
@@ -10,6 +11,12 @@ from utils.config import get
|
|
|
10
11
|
logger = logging.getLogger("codex-telegram.codex")
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class _ApprovalWaiter:
|
|
16
|
+
future: asyncio.Future[str]
|
|
17
|
+
loop: asyncio.AbstractEventLoop
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
class CodexClient:
|
|
14
21
|
def __init__(self):
|
|
15
22
|
self.protocol = Protocol()
|
|
@@ -17,7 +24,7 @@ class CodexClient:
|
|
|
17
24
|
self._reader_task: asyncio.Task | None = None
|
|
18
25
|
self._stderr_task: asyncio.Task | None = None
|
|
19
26
|
self._pending: dict[int, asyncio.Future[JSONRPCResponse]] = {}
|
|
20
|
-
self._pending_approvals: dict[int,
|
|
27
|
+
self._pending_approvals: dict[int, _ApprovalWaiter] = {}
|
|
21
28
|
self._event_handlers: dict[str, list[Callable]] = {}
|
|
22
29
|
self._any_event_handlers: list[Callable] = []
|
|
23
30
|
self._approval_handlers: list[Callable] = []
|
|
@@ -114,10 +121,18 @@ class CodexClient:
|
|
|
114
121
|
self._approval_handlers.append(handler)
|
|
115
122
|
|
|
116
123
|
def submit_approval_decision(self, request_id: int, decision: str) -> bool:
|
|
117
|
-
|
|
118
|
-
if
|
|
124
|
+
waiter = self._pending_approvals.get(request_id)
|
|
125
|
+
if waiter is None or waiter.future.done():
|
|
119
126
|
return False
|
|
120
|
-
|
|
127
|
+
|
|
128
|
+
def _resolve() -> None:
|
|
129
|
+
if not waiter.future.done():
|
|
130
|
+
waiter.future.set_result(decision)
|
|
131
|
+
|
|
132
|
+
if waiter.loop.is_running():
|
|
133
|
+
waiter.loop.call_soon_threadsafe(_resolve)
|
|
134
|
+
else:
|
|
135
|
+
_resolve()
|
|
121
136
|
return True
|
|
122
137
|
|
|
123
138
|
def _write(self, msg: JSONRPCRequest | JSONRPCNotification | JSONRPCResponse):
|
|
@@ -125,7 +140,6 @@ class CodexClient:
|
|
|
125
140
|
logger.debug("app-server stdin: %s", data)
|
|
126
141
|
if self._proc and self._proc.stdin:
|
|
127
142
|
self._proc.stdin.write(data.encode() + b"\n")
|
|
128
|
-
self._proc.stdin.flush()
|
|
129
143
|
|
|
130
144
|
def _fail_pending(self, exc: Exception) -> None:
|
|
131
145
|
for future in self._pending.values():
|
|
@@ -133,8 +147,8 @@ class CodexClient:
|
|
|
133
147
|
future.set_exception(exc)
|
|
134
148
|
self._pending.clear()
|
|
135
149
|
for future in self._pending_approvals.values():
|
|
136
|
-
if not future.done():
|
|
137
|
-
future.set_result("deny")
|
|
150
|
+
if not future.future.done():
|
|
151
|
+
future.future.set_result("deny")
|
|
138
152
|
self._pending_approvals.clear()
|
|
139
153
|
|
|
140
154
|
async def _read_stdout_stream(self):
|
|
@@ -500,8 +514,9 @@ class CodexClient:
|
|
|
500
514
|
)
|
|
501
515
|
return
|
|
502
516
|
|
|
503
|
-
|
|
504
|
-
|
|
517
|
+
loop = asyncio.get_running_loop()
|
|
518
|
+
future: asyncio.Future[str] = loop.create_future()
|
|
519
|
+
self._pending_approvals[req_id] = _ApprovalWaiter(future=future, loop=loop)
|
|
505
520
|
await self._emit_approval_request(method, req_id, params)
|
|
506
521
|
|
|
507
522
|
decision_choice = self._default_choice(auto_mode)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-telegram
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
|
|
5
5
|
Author: heungtae
|
|
6
6
|
License: Apache License
|
|
@@ -218,6 +218,10 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
218
218
|
Requires-Python: >=3.11
|
|
219
219
|
Description-Content-Type: text/markdown
|
|
220
220
|
License-File: LICENSE
|
|
221
|
+
Requires-Dist: python-telegram-bot>=20.0
|
|
222
|
+
Requires-Dist: fastapi>=0.111.0
|
|
223
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
224
|
+
Dynamic: license-file
|
|
221
225
|
|
|
222
226
|
# Codex Telegram + Web
|
|
223
227
|
|
|
@@ -1,11 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import unittest
|
|
3
|
+
import threading
|
|
2
4
|
from unittest.mock import patch
|
|
5
|
+
from types import SimpleNamespace
|
|
3
6
|
|
|
4
7
|
from codex.client import CodexClient
|
|
5
|
-
from codex.protocol import JSONRPCRequest
|
|
8
|
+
from codex.protocol import JSONRPCRequest, JSONRPCNotification
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class CodexClientServerRequestTests(unittest.IsolatedAsyncioTestCase):
|
|
12
|
+
def test_write_does_not_require_flush_on_asyncio_stream_writer(self):
|
|
13
|
+
client = CodexClient()
|
|
14
|
+
written = []
|
|
15
|
+
|
|
16
|
+
class _DummyStdin:
|
|
17
|
+
def write(self, data):
|
|
18
|
+
written.append(data)
|
|
19
|
+
|
|
20
|
+
client._proc = SimpleNamespace(stdin=_DummyStdin())
|
|
21
|
+
|
|
22
|
+
client._write(JSONRPCNotification(method="initialized"))
|
|
23
|
+
|
|
24
|
+
self.assertEqual([b'{"method": "initialized"}\n'], written)
|
|
25
|
+
|
|
9
26
|
async def test_tool_request_user_input_auto_approve_maps_to_approve_once(self):
|
|
10
27
|
client = CodexClient()
|
|
11
28
|
written = []
|
|
@@ -74,6 +91,45 @@ class CodexClientServerRequestTests(unittest.IsolatedAsyncioTestCase):
|
|
|
74
91
|
answers = ((response.get("result") or {}).get("answers") or {})
|
|
75
92
|
self.assertEqual({"answers": ["Approve this Session"]}, answers.get("q2"))
|
|
76
93
|
|
|
94
|
+
async def test_submit_approval_decision_is_thread_safe(self):
|
|
95
|
+
client = CodexClient()
|
|
96
|
+
written = []
|
|
97
|
+
client._write = lambda msg: written.append(msg) # type: ignore[method-assign]
|
|
98
|
+
req = JSONRPCRequest(
|
|
99
|
+
method="item/commandExecution/requestApproval",
|
|
100
|
+
id=42,
|
|
101
|
+
params={"threadId": "thread-1"},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
with patch(
|
|
105
|
+
"codex.client.get",
|
|
106
|
+
side_effect=lambda key, default=None: "interactive" if key == "approval.mode" else "approve" if key == "approval.auto_response" else default,
|
|
107
|
+
):
|
|
108
|
+
task = asyncio.create_task(client._handle_server_request(req))
|
|
109
|
+
for _ in range(100):
|
|
110
|
+
if 42 in client._pending_approvals:
|
|
111
|
+
break
|
|
112
|
+
await asyncio.sleep(0.01)
|
|
113
|
+
else:
|
|
114
|
+
self.fail("approval request was not registered")
|
|
115
|
+
|
|
116
|
+
accepted = {}
|
|
117
|
+
|
|
118
|
+
def approve_from_thread():
|
|
119
|
+
accepted["value"] = client.submit_approval_decision(42, "approve")
|
|
120
|
+
|
|
121
|
+
thread = threading.Thread(target=approve_from_thread)
|
|
122
|
+
thread.start()
|
|
123
|
+
thread.join()
|
|
124
|
+
|
|
125
|
+
await asyncio.wait_for(task, timeout=1.0)
|
|
126
|
+
|
|
127
|
+
self.assertTrue(accepted["value"])
|
|
128
|
+
self.assertEqual(1, len(written))
|
|
129
|
+
response = written[0].to_dict()
|
|
130
|
+
self.assertEqual(42, response.get("id"))
|
|
131
|
+
self.assertEqual({"decision": "accept"}, response.get("result"))
|
|
132
|
+
|
|
77
133
|
async def test_tool_request_user_input_prefers_option_value_when_present(self):
|
|
78
134
|
client = CodexClient()
|
|
79
135
|
written = []
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|