open-codex-ui 0.1.4__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.
Files changed (40) hide show
  1. open_codex_ui-0.1.4.dist-info/METADATA +249 -0
  2. open_codex_ui-0.1.4.dist-info/RECORD +40 -0
  3. open_codex_ui-0.1.4.dist-info/WHEEL +5 -0
  4. open_codex_ui-0.1.4.dist-info/entry_points.txt +7 -0
  5. open_codex_ui-0.1.4.dist-info/top_level.txt +1 -0
  6. yier_web/__init__.py +6 -0
  7. yier_web/app.py +223 -0
  8. yier_web/auth.py +269 -0
  9. yier_web/cli.py +172 -0
  10. yier_web/codex/__init__.py +3 -0
  11. yier_web/codex/ipc_manager.py +1761 -0
  12. yier_web/codex/session_events.py +110 -0
  13. yier_web/codex/ws_commands.py +299 -0
  14. yier_web/config.py +651 -0
  15. yier_web/directory_picker.py +93 -0
  16. yier_web/event_stream.py +29 -0
  17. yier_web/frontend.py +204 -0
  18. yier_web/routes/__init__.py +17 -0
  19. yier_web/routes/codex.py +573 -0
  20. yier_web/routes/core.py +281 -0
  21. yier_web/schemas.py +534 -0
  22. yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js +1 -0
  23. yier_web/static/assets/CodexView-wpI61iXa.js +606 -0
  24. yier_web/static/assets/LoginView-CELCom2O.js +101 -0
  25. yier_web/static/assets/api-CeihACIV.js +1099 -0
  26. yier_web/static/assets/index-BdFqJ-Kl.css +1 -0
  27. yier_web/static/assets/index-CjVNk6ja.js +183 -0
  28. yier_web/static/assets/index-mSBvq1p8.js +79 -0
  29. yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg +99 -0
  30. yier_web/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
  31. yier_web/static/assets/primeicons-DMOk5skT.eot +0 -0
  32. yier_web/static/assets/primeicons-Dr5RGzOO.svg +345 -0
  33. yier_web/static/assets/primeicons-MpK4pl85.ttf +0 -0
  34. yier_web/static/assets/primeicons-WjwUDZjB.woff +0 -0
  35. yier_web/static/assets/useCodexWorkspace-6QenDzvb.css +1 -0
  36. yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js +1128 -0
  37. yier_web/static/brand/open-codex-ui-logo.svg +85 -0
  38. yier_web/static/favicon.ico +0 -0
  39. yier_web/static/favicon.svg +99 -0
  40. yier_web/static/index.html +24 -0
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ from pathlib import Path
6
+
7
+
8
+ class LocalDirectoryPickerService:
9
+ def select_directory(self, initial_path: str | None = None) -> str | None:
10
+ normalized_initial_path = self._normalize_initial_path(initial_path)
11
+
12
+ if sys.platform == "darwin":
13
+ handled, selected_path = self._select_directory_with_osascript(
14
+ normalized_initial_path
15
+ )
16
+ if handled:
17
+ return selected_path
18
+
19
+ return self._select_directory_with_tk(normalized_initial_path)
20
+
21
+ def _normalize_initial_path(self, initial_path: str | None) -> Path | None:
22
+ if not isinstance(initial_path, str) or not initial_path.strip():
23
+ return None
24
+
25
+ candidate = Path(initial_path).expanduser()
26
+ if candidate.is_dir():
27
+ return candidate.resolve()
28
+ if candidate.exists():
29
+ return candidate.parent.resolve()
30
+ if candidate.parent.exists():
31
+ return candidate.parent.resolve()
32
+ return None
33
+
34
+ def _select_directory_with_osascript(
35
+ self, initial_path: Path | None
36
+ ) -> tuple[bool, str | None]:
37
+ script = [
38
+ 'set chosenFolder to choose folder with prompt "Select a project folder"'
39
+ ]
40
+ if initial_path is not None:
41
+ escaped_path = self._escape_applescript_text(str(initial_path))
42
+ script = [
43
+ f'set chosenFolder to choose folder with prompt "Select a project folder" default location POSIX file "{escaped_path}"'
44
+ ]
45
+ script.append("POSIX path of chosenFolder")
46
+
47
+ result = subprocess.run(
48
+ ["osascript", *sum([["-e", line] for line in script], [])],
49
+ capture_output=True,
50
+ text=True,
51
+ check=False,
52
+ )
53
+ if result.returncode == 0:
54
+ selected_path = result.stdout.strip()
55
+ if not selected_path:
56
+ return True, None
57
+ return True, str(Path(selected_path).expanduser().resolve())
58
+
59
+ stderr = result.stderr.strip()
60
+ if "User canceled" in stderr or "(-128)" in stderr:
61
+ return True, None
62
+ return False, None
63
+
64
+ def _select_directory_with_tk(self, initial_path: Path | None) -> str | None:
65
+ try:
66
+ import tkinter as tk
67
+ from tkinter import filedialog
68
+ except Exception:
69
+ return None
70
+
71
+ root = tk.Tk()
72
+ root.withdraw()
73
+ try:
74
+ root.attributes("-topmost", True)
75
+ except Exception:
76
+ pass
77
+
78
+ try:
79
+ selected_path = filedialog.askdirectory(
80
+ initialdir=str(initial_path) if initial_path is not None else None,
81
+ mustexist=True,
82
+ title="Select a project folder",
83
+ parent=root,
84
+ )
85
+ finally:
86
+ root.destroy()
87
+
88
+ if not selected_path:
89
+ return None
90
+ return str(Path(selected_path).expanduser().resolve())
91
+
92
+ def _escape_applescript_text(self, value: str) -> str:
93
+ return value.replace("\\", "\\\\").replace('"', '\\"')
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class EventStreamItem:
10
+ event: str
11
+ data: dict[str, Any]
12
+
13
+
14
+ class EventStreamBroker:
15
+ def __init__(self) -> None:
16
+ self._subscribers: set[asyncio.Queue[EventStreamItem]] = set()
17
+
18
+ def subscribe(self) -> asyncio.Queue[EventStreamItem]:
19
+ queue: asyncio.Queue[EventStreamItem] = asyncio.Queue()
20
+ self._subscribers.add(queue)
21
+ return queue
22
+
23
+ def unsubscribe(self, queue: asyncio.Queue[EventStreamItem]) -> None:
24
+ self._subscribers.discard(queue)
25
+
26
+ async def publish(self, event: str, data: dict[str, Any]) -> None:
27
+ item = EventStreamItem(event=event, data=data)
28
+ for subscriber in list(self._subscribers):
29
+ await subscriber.put(item)
yier_web/frontend.py ADDED
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import mimetypes
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import httpx
8
+ from litestar.connection import WebSocket
9
+ from litestar import Request
10
+ from litestar.response import Response
11
+
12
+ from yier_web.schemas import FrontendHealth
13
+
14
+
15
+ class FrontendService:
16
+ def __init__(
17
+ self,
18
+ dist_root: Path | None = None,
19
+ vite_origin: str = "http://127.0.0.1:5173",
20
+ debug: bool = False,
21
+ ) -> None:
22
+ self.dist_root = (
23
+ dist_root or Path(__file__).resolve().parent / "static"
24
+ ).resolve()
25
+ self.vite_origin = vite_origin.rstrip("/")
26
+ self.debug = debug
27
+
28
+ async def get_status(self) -> FrontendHealth:
29
+ if await self._should_proxy_to_vite():
30
+ return FrontendHealth(
31
+ ready=True, mode="proxy", detail=f"Proxying {self.vite_origin}"
32
+ )
33
+ if (self.dist_root / "index.html").exists():
34
+ return FrontendHealth(
35
+ ready=True, mode="static", detail=f"Serving {self.dist_root}"
36
+ )
37
+ return FrontendHealth(
38
+ ready=False,
39
+ mode="missing",
40
+ detail="Start Vite dev server or build the frontend bundle first.",
41
+ )
42
+
43
+ async def handle_request(
44
+ self, request: Request[Any, Any, Any], path: str
45
+ ) -> Response:
46
+ if await self._should_proxy_to_vite():
47
+ return await self._proxy_request(request)
48
+
49
+ resolved_path = self._resolve_dist_path(path)
50
+ if resolved_path is not None and resolved_path.exists():
51
+ return self._build_static_response(resolved_path)
52
+
53
+ index_path = self.dist_root / "index.html"
54
+ if index_path.exists():
55
+ return self._build_static_response(index_path)
56
+
57
+ return Response(
58
+ content="Frontend is unavailable. Start `pnpm dev` in `web` or build the frontend.",
59
+ media_type="text/plain",
60
+ status_code=503,
61
+ )
62
+
63
+ async def handle_websocket(self, socket: WebSocket, _path: str) -> None:
64
+ await socket.accept()
65
+ if await self._should_proxy_to_vite():
66
+ await socket.close(
67
+ code=1013,
68
+ reason="Frontend dev WebSocket is served by Vite on port 5173.",
69
+ )
70
+ return
71
+
72
+ await socket.close(
73
+ code=1008,
74
+ reason="Frontend WebSocket is unavailable for the static bundle.",
75
+ )
76
+
77
+ async def _should_proxy_to_vite(self) -> bool:
78
+ if not self.debug:
79
+ return False
80
+ return await self._vite_available()
81
+
82
+ async def _proxy_request(self, request: Request[Any, Any, Any]) -> Response:
83
+ request_headers = {
84
+ key: value
85
+ for key, value in request.headers.items()
86
+ if key.lower() not in {"host", "connection", "content-length"}
87
+ }
88
+
89
+ async with self._create_vite_client(
90
+ timeout=10.0, follow_redirects=False
91
+ ) as client:
92
+ upstream = await self._proxy_to_vite(
93
+ client,
94
+ request=request,
95
+ headers=request_headers,
96
+ path=request.url.path,
97
+ query=request.url.query,
98
+ )
99
+
100
+ if self._should_fallback_to_vite_index(request, upstream.status_code):
101
+ upstream = await self._proxy_to_vite(
102
+ client,
103
+ request=request,
104
+ headers=request_headers,
105
+ path="/",
106
+ query="",
107
+ )
108
+
109
+ response_headers = {
110
+ key: value
111
+ for key, value in upstream.headers.items()
112
+ if key.lower() not in {"connection", "content-length", "transfer-encoding"}
113
+ }
114
+ media_type = upstream.headers.get("content-type")
115
+ return Response(
116
+ content=upstream.content,
117
+ status_code=upstream.status_code,
118
+ media_type=media_type,
119
+ headers=response_headers,
120
+ )
121
+
122
+ async def _proxy_to_vite(
123
+ self,
124
+ client: httpx.AsyncClient,
125
+ *,
126
+ request: Request[Any, Any, Any],
127
+ headers: dict[str, str],
128
+ path: str,
129
+ query: str,
130
+ ) -> httpx.Response:
131
+ target_url = f"{self.vite_origin}{path}"
132
+ if query:
133
+ target_url = f"{target_url}?{query}"
134
+ return await client.request(
135
+ request.method,
136
+ target_url,
137
+ headers=headers,
138
+ content=await request.body(),
139
+ )
140
+
141
+ def _should_fallback_to_vite_index(
142
+ self,
143
+ request: Request[Any, Any, Any],
144
+ upstream_status_code: int,
145
+ ) -> bool:
146
+ if upstream_status_code != 404:
147
+ return False
148
+ if request.method.upper() not in {"GET", "HEAD"}:
149
+ return False
150
+ if Path(request.url.path).suffix:
151
+ return False
152
+
153
+ accept = request.headers.get("accept", "").lower()
154
+ return "text/html" in accept or accept in {"", "*/*"}
155
+
156
+ def _resolve_dist_path(self, path: str) -> Path | None:
157
+ normalized_path = path.lstrip("/")
158
+
159
+ if not normalized_path:
160
+ candidate = self.dist_root / "index.html"
161
+ return candidate if candidate.exists() else None
162
+
163
+ candidate = (self.dist_root / normalized_path).resolve()
164
+ try:
165
+ candidate.relative_to(self.dist_root.resolve())
166
+ except ValueError:
167
+ return None
168
+
169
+ if candidate.exists() and candidate.is_file():
170
+ return candidate
171
+ if Path(normalized_path).suffix:
172
+ return None
173
+ return None
174
+
175
+ async def _vite_available(self) -> bool:
176
+ try:
177
+ async with self._create_vite_client(timeout=0.35) as client:
178
+ response = await client.get(self.vite_origin)
179
+ except httpx.HTTPError:
180
+ return False
181
+ return response.status_code < 500
182
+
183
+ def _create_vite_client(
184
+ self,
185
+ *,
186
+ timeout: float,
187
+ follow_redirects: bool = False,
188
+ ) -> httpx.AsyncClient:
189
+ return httpx.AsyncClient(
190
+ follow_redirects=follow_redirects,
191
+ timeout=timeout,
192
+ trust_env=False,
193
+ )
194
+
195
+ def _build_static_response(self, path: Path) -> Response:
196
+ media_type, encoding = mimetypes.guess_type(path.name)
197
+ headers: dict[str, str] = {}
198
+ if encoding:
199
+ headers["content-encoding"] = encoding
200
+ return Response(
201
+ content=path.read_bytes(),
202
+ media_type=media_type or "application/octet-stream",
203
+ headers=headers or None,
204
+ )
@@ -0,0 +1,17 @@
1
+ from yier_web.routes.codex import CodexController
2
+ from yier_web.routes.core import (
3
+ AuthController,
4
+ ConfigController,
5
+ EventsController,
6
+ HealthController,
7
+ SystemController,
8
+ )
9
+
10
+ __all__ = [
11
+ "AuthController",
12
+ "CodexController",
13
+ "ConfigController",
14
+ "EventsController",
15
+ "HealthController",
16
+ "SystemController",
17
+ ]