codex-webstrapper 0.1.0
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.
- package/LICENSE.md +21 -0
- package/README.md +239 -0
- package/bin/codex-webstrap.sh +63 -0
- package/package.json +27 -0
- package/src/app-server.mjs +289 -0
- package/src/assets.mjs +190 -0
- package/src/auth.mjs +166 -0
- package/src/bridge-shim.js +669 -0
- package/src/ipc-uds.mjs +320 -0
- package/src/message-router.mjs +1857 -0
- package/src/server.mjs +363 -0
- package/src/util.mjs +95 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 codex-webstrap contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# codex-webstrap
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
`codex-webstrap` is a macOS wrapper that lets you run the Codex desktop client UI in a browser while keeping backend execution local.
|
|
6
|
+
|
|
7
|
+
This started as a personal project to remotely access the Codex desktop experience; it is open sourced so others can use and improve it.
|
|
8
|
+
|
|
9
|
+
## What It Is
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
https://github.com/user-attachments/assets/24e023ee-6a74-448c-892d-9fc1964bd10c
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Codex desktop is not a pure web app. The renderer expects Electron preload APIs, IPC, local process control, worker threads, and desktop-only integrations.
|
|
16
|
+
|
|
17
|
+
`codex-webstrap` makes browser access possible by:
|
|
18
|
+
|
|
19
|
+
1. Serving Codex's bundled web assets.
|
|
20
|
+
2. Injecting a browser shim that emulates key `electronBridge` preload methods.
|
|
21
|
+
3. Bridging renderer messages over WebSocket to local backend handlers.
|
|
22
|
+
4. Forwarding app protocol traffic to local `codex app-server` and UDS IPC where available.
|
|
23
|
+
|
|
24
|
+
Default endpoint: `http://127.0.0.1:8080`
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
### Core Components
|
|
29
|
+
|
|
30
|
+
- `bin/codex-webstrap.sh`
|
|
31
|
+
- CLI entrypoint and env/arg normalization.
|
|
32
|
+
- `src/server.mjs`
|
|
33
|
+
- HTTP + WS host, auth gating, startup orchestration.
|
|
34
|
+
- `src/auth.mjs`
|
|
35
|
+
- Persistent token bootstrap + `cw_session` cookie sessions.
|
|
36
|
+
- `src/assets.mjs`
|
|
37
|
+
- Discovers Codex app bundle, extracts/caches `app.asar` assets, patches `index.html`.
|
|
38
|
+
- `src/bridge-shim.js`
|
|
39
|
+
- Browser-side Electron preload compatibility layer (`window.electronBridge`).
|
|
40
|
+
- `src/ipc-uds.mjs`
|
|
41
|
+
- Framed UDS client (`length-prefix + JSON`) for `codex-ipc`.
|
|
42
|
+
- `src/app-server.mjs`
|
|
43
|
+
- Local `codex app-server` process manager over stdio JSON-RPC.
|
|
44
|
+
- `src/message-router.mjs`
|
|
45
|
+
- Message dispatch, terminal lifecycle, worker bridge, unsupported fallbacks.
|
|
46
|
+
|
|
47
|
+
### Runtime Flow
|
|
48
|
+
|
|
49
|
+
1. Wrapper starts Node server and loads config.
|
|
50
|
+
2. Auth token is created/read from token file.
|
|
51
|
+
3. Codex app assets are extracted to a versioned cache directory.
|
|
52
|
+
4. Patched `index.html` is served with shim injection.
|
|
53
|
+
5. Browser opens `/`, shim connects to `ws://<host>/__webstrapper/bridge`.
|
|
54
|
+
6. Renderer messages are routed to:
|
|
55
|
+
- app-server JSON-RPC (`thread/*`, turns, config, etc.)
|
|
56
|
+
- UDS broadcast forwarding where relevant
|
|
57
|
+
- terminal sessions (`spawn`, `write`, `close`)
|
|
58
|
+
- git worker bridge path
|
|
59
|
+
7. Results are sent back as bridge envelopes and posted to the renderer via `window.postMessage`.
|
|
60
|
+
|
|
61
|
+
## Why This Is Not "Native Web"
|
|
62
|
+
|
|
63
|
+
Codex desktop behavior depends on Electron/main-process features unavailable to normal browser JavaScript, including:
|
|
64
|
+
|
|
65
|
+
- preload-only bridge APIs
|
|
66
|
+
- local privileged process orchestration
|
|
67
|
+
- desktop IPC channels
|
|
68
|
+
- local worker/protocol assumptions
|
|
69
|
+
|
|
70
|
+
This project provides near-parity by emulation/bridging, not by removing those dependencies.
|
|
71
|
+
|
|
72
|
+
## API Surface
|
|
73
|
+
|
|
74
|
+
### CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
codex-webstrap [--port <n>] [--bind <ip>] [--open] [--token-file <path>] [--codex-app <path>]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Environment Overrides
|
|
81
|
+
|
|
82
|
+
- `CODEX_WEBSTRAP_PORT`
|
|
83
|
+
- `CODEX_WEBSTRAP_BIND`
|
|
84
|
+
- `CODEX_WEBSTRAP_TOKEN_FILE`
|
|
85
|
+
- `CODEX_WEBSTRAP_CODEX_APP`
|
|
86
|
+
- `CODEX_WEBSTRAP_INTERNAL_WS_PORT`
|
|
87
|
+
|
|
88
|
+
### HTTP Endpoints
|
|
89
|
+
|
|
90
|
+
- `GET /`
|
|
91
|
+
- `GET /__webstrapper/shim.js`
|
|
92
|
+
- `GET /__webstrapper/healthz`
|
|
93
|
+
- `GET /__webstrapper/auth?token=...`
|
|
94
|
+
|
|
95
|
+
### WebSocket Endpoint
|
|
96
|
+
|
|
97
|
+
- `GET /__webstrapper/bridge`
|
|
98
|
+
|
|
99
|
+
### Bridge Envelope Types
|
|
100
|
+
|
|
101
|
+
- `view-message`
|
|
102
|
+
- `main-message`
|
|
103
|
+
- `worker-message`
|
|
104
|
+
- `worker-event`
|
|
105
|
+
- `bridge-error`
|
|
106
|
+
- `bridge-ready`
|
|
107
|
+
|
|
108
|
+
## Setup
|
|
109
|
+
|
|
110
|
+
### Prerequisites
|
|
111
|
+
|
|
112
|
+
- macOS
|
|
113
|
+
- Node.js 20+
|
|
114
|
+
- Installed Codex app bundle at `/Applications/Codex.app` (or pass `--codex-app`)
|
|
115
|
+
- `codex` CLI available in `PATH` (or set `CODEX_CLI_PATH`)
|
|
116
|
+
|
|
117
|
+
### Install
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Global CLI install:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install -g codex-webstrapper
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Run
|
|
130
|
+
|
|
131
|
+
With global install:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
codex-webstrap --port 8080 --bind 127.0.0.1
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
From local checkout:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
./bin/codex-webstrap.sh --port 8080 --bind 127.0.0.1
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Optional auto-open:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
codex-webstrap --open
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Authentication Model
|
|
150
|
+
|
|
151
|
+
1. On first run, a random token is persisted at `~/.codex-webstrap/token` (default path).
|
|
152
|
+
2. You authenticate once via:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
open "http://127.0.0.1:8080/__webstrapper/auth?token=$(cat ~/.codex-webstrap/token)"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
3. Server sets `cw_session` cookie (`HttpOnly`, `SameSite=Lax`, scoped to `/`).
|
|
159
|
+
4. UI and bridge endpoints require a valid session cookie.
|
|
160
|
+
|
|
161
|
+
## Security Risks and Recommendations
|
|
162
|
+
|
|
163
|
+
This project can expose powerful local capabilities if misconfigured. Treat it as sensitive software.
|
|
164
|
+
|
|
165
|
+
### Primary Risks
|
|
166
|
+
|
|
167
|
+
- Remote users with valid session can operate Codex UI features and local workflows.
|
|
168
|
+
- Token bootstrap URL can be leaked via shell history, logs, screenshots, or shared links.
|
|
169
|
+
- Binding to non-local interfaces increases attack surface.
|
|
170
|
+
- No built-in TLS termination. Plain HTTP should not be exposed directly to the public internet.
|
|
171
|
+
|
|
172
|
+
### Recommended Safe Usage
|
|
173
|
+
|
|
174
|
+
- Keep default bind: `127.0.0.1` unless remote access is required.
|
|
175
|
+
- If remote access is needed, use a private overlay network (for example Tailscale/WireGuard) and not public port-forwarding.
|
|
176
|
+
- Do not share token values in chat, screenshots, logs, issue reports, or commit history.
|
|
177
|
+
- Rotate token file if exposure is suspected:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
rm -f ~/.codex-webstrap/token
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Then restart wrapper to generate a new token.
|
|
184
|
+
|
|
185
|
+
- Consider external TLS/auth proxy if you must serve beyond localhost.
|
|
186
|
+
|
|
187
|
+
## Functional Coverage Notes
|
|
188
|
+
|
|
189
|
+
Implemented coverage includes:
|
|
190
|
+
|
|
191
|
+
- core message routing (`ready`, `fetch`, `mcp-*`, `terminal-*`, `persisted-atom-*`, `shared-object-*`)
|
|
192
|
+
- thread lifecycle actions including archive/unarchive pathing
|
|
193
|
+
- worker message support (including git worker bridge)
|
|
194
|
+
- browser equivalents for desktop-only UX events (open links, diff/plan summaries)
|
|
195
|
+
- graceful unsupported handling for non-web-native desktop actions
|
|
196
|
+
|
|
197
|
+
Unknown message types produce structured `bridge-error` responses and do not crash the session.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
### Run Tests
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm test
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Worktree Bootstrap
|
|
208
|
+
|
|
209
|
+
Bootstrap env/secrets from another worktree checkout:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
./scripts/worktree-bootstrap.sh --dry-run
|
|
213
|
+
./scripts/worktree-bootstrap.sh --mode symlink
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Core paths are configured via:
|
|
217
|
+
|
|
218
|
+
- `scripts/worktree-secrets.manifest`
|
|
219
|
+
|
|
220
|
+
Codex setup-script compatible command:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
./scripts/worktree-bootstrap.sh --mode symlink --overwrite backup --extras on --install on --checks on
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Typical Troubleshooting
|
|
227
|
+
|
|
228
|
+
- `401 unauthorized`
|
|
229
|
+
- Authenticate first via `/__webstrapper/auth?token=...`.
|
|
230
|
+
- UI loads but actions fail
|
|
231
|
+
- Check `GET /__webstrapper/healthz` for app-server/UDS readiness.
|
|
232
|
+
- Codex app not found
|
|
233
|
+
- Pass `--codex-app /path/to/Codex.app`.
|
|
234
|
+
- `codex` CLI spawn failures
|
|
235
|
+
- Ensure `codex` is on `PATH` or set `CODEX_CLI_PATH`.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT. See `LICENSE.md`.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
PORT="${CODEX_WEBSTRAP_PORT:-8080}"
|
|
8
|
+
BIND="${CODEX_WEBSTRAP_BIND:-127.0.0.1}"
|
|
9
|
+
OPEN_FLAG="0"
|
|
10
|
+
TOKEN_FILE="${CODEX_WEBSTRAP_TOKEN_FILE:-}"
|
|
11
|
+
CODEX_APP="${CODEX_WEBSTRAP_CODEX_APP:-}"
|
|
12
|
+
INTERNAL_WS_PORT="${CODEX_WEBSTRAP_INTERNAL_WS_PORT:-38080}"
|
|
13
|
+
|
|
14
|
+
while [[ $# -gt 0 ]]; do
|
|
15
|
+
case "$1" in
|
|
16
|
+
--port)
|
|
17
|
+
PORT="$2"
|
|
18
|
+
shift 2
|
|
19
|
+
;;
|
|
20
|
+
--bind)
|
|
21
|
+
BIND="$2"
|
|
22
|
+
shift 2
|
|
23
|
+
;;
|
|
24
|
+
--open)
|
|
25
|
+
OPEN_FLAG="1"
|
|
26
|
+
shift
|
|
27
|
+
;;
|
|
28
|
+
--token-file)
|
|
29
|
+
TOKEN_FILE="$2"
|
|
30
|
+
shift 2
|
|
31
|
+
;;
|
|
32
|
+
--codex-app)
|
|
33
|
+
CODEX_APP="$2"
|
|
34
|
+
shift 2
|
|
35
|
+
;;
|
|
36
|
+
--help|-h)
|
|
37
|
+
cat <<USAGE
|
|
38
|
+
Usage: $(basename "$0") [--port <n>] [--bind <ip>] [--open] [--token-file <path>] [--codex-app <path>]
|
|
39
|
+
|
|
40
|
+
Env overrides:
|
|
41
|
+
CODEX_WEBSTRAP_PORT
|
|
42
|
+
CODEX_WEBSTRAP_BIND
|
|
43
|
+
CODEX_WEBSTRAP_TOKEN_FILE
|
|
44
|
+
CODEX_WEBSTRAP_CODEX_APP
|
|
45
|
+
CODEX_WEBSTRAP_INTERNAL_WS_PORT
|
|
46
|
+
USAGE
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
echo "Unknown argument: $1" >&2
|
|
51
|
+
exit 1
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
export CODEX_WEBSTRAP_PORT="$PORT"
|
|
57
|
+
export CODEX_WEBSTRAP_BIND="$BIND"
|
|
58
|
+
export CODEX_WEBSTRAP_TOKEN_FILE="$TOKEN_FILE"
|
|
59
|
+
export CODEX_WEBSTRAP_CODEX_APP="$CODEX_APP"
|
|
60
|
+
export CODEX_WEBSTRAP_INTERNAL_WS_PORT="$INTERNAL_WS_PORT"
|
|
61
|
+
export CODEX_WEBSTRAP_OPEN="$OPEN_FLAG"
|
|
62
|
+
|
|
63
|
+
exec node "${ROOT_DIR}/src/server.mjs"
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-webstrapper",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Web wrapper for Codex desktop assets with bridge + token auth",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"codex-webstrap": "./bin/codex-webstrap.sh"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"LICENSE.md",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/server.mjs",
|
|
18
|
+
"test": "node --test"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@electron/asar": "^4.0.1",
|
|
25
|
+
"ws": "^8.18.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
import { createLogger, toErrorMessage } from "./util.mjs";
|
|
5
|
+
|
|
6
|
+
export class AppServerManager extends EventEmitter {
|
|
7
|
+
constructor({
|
|
8
|
+
internalPort = 38080,
|
|
9
|
+
codexCliPath = process.env.CODEX_CLI_PATH || "codex",
|
|
10
|
+
logger
|
|
11
|
+
} = {}) {
|
|
12
|
+
super();
|
|
13
|
+
this.internalPort = internalPort;
|
|
14
|
+
this.codexCliPath = codexCliPath;
|
|
15
|
+
this.logger = logger || createLogger("app-server");
|
|
16
|
+
|
|
17
|
+
this.proc = null;
|
|
18
|
+
this.connected = false;
|
|
19
|
+
this.initialized = false;
|
|
20
|
+
this.transportKind = "stdio";
|
|
21
|
+
|
|
22
|
+
this.nextId = 1;
|
|
23
|
+
this.pending = new Map();
|
|
24
|
+
this.connectingPromise = null;
|
|
25
|
+
this.stopped = false;
|
|
26
|
+
this.stdoutBuffer = "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getState() {
|
|
30
|
+
return {
|
|
31
|
+
connected: this.connected,
|
|
32
|
+
initialized: this.initialized,
|
|
33
|
+
transportKind: this.transportKind,
|
|
34
|
+
wsUrl: null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async start() {
|
|
39
|
+
this.stopped = false;
|
|
40
|
+
|
|
41
|
+
if (this.connected && this.initialized) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.connectingPromise) {
|
|
46
|
+
return this.connectingPromise;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.connectingPromise = this._startInternal().finally(() => {
|
|
50
|
+
this.connectingPromise = null;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return this.connectingPromise;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async _startInternal() {
|
|
57
|
+
if (this.proc && !this.proc.killed && this.connected) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await this._spawnStdioProcess();
|
|
62
|
+
await this._initializeProtocol();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
stop() {
|
|
66
|
+
this.stopped = true;
|
|
67
|
+
|
|
68
|
+
this._rejectAllPending(new Error("App server stopped"));
|
|
69
|
+
|
|
70
|
+
if (this.proc && !this.proc.killed) {
|
|
71
|
+
this.proc.kill();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.proc = null;
|
|
75
|
+
this.connected = false;
|
|
76
|
+
this.initialized = false;
|
|
77
|
+
this.stdoutBuffer = "";
|
|
78
|
+
this._emitConnectionChanged();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async sendRequest(method, params, options = {}) {
|
|
82
|
+
if (!options.skipReadyCheck) {
|
|
83
|
+
await this._ensureReady();
|
|
84
|
+
} else if (!(this.connected && this.proc && this.proc.stdin && !this.proc.stdin.destroyed)) {
|
|
85
|
+
throw new Error("App server stdio is not connected");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const id = options.id ?? this.nextId++;
|
|
89
|
+
const payload = { id, method, params };
|
|
90
|
+
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
this.pending.delete(id);
|
|
94
|
+
reject(new Error(`app-server request timeout: ${method}`));
|
|
95
|
+
}, options.timeoutMs || 15000);
|
|
96
|
+
|
|
97
|
+
this.pending.set(id, { resolve, reject, timer, method });
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
this._sendJson(payload);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
this.pending.delete(id);
|
|
104
|
+
reject(error);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async sendNotification(method, params) {
|
|
110
|
+
await this._ensureReady();
|
|
111
|
+
this._sendJson({ method, params });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async sendRaw(message) {
|
|
115
|
+
if (!message || typeof message !== "object") {
|
|
116
|
+
throw new Error("sendRaw expects an object");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (Object.prototype.hasOwnProperty.call(message, "id") && message.method) {
|
|
120
|
+
return this.sendRequest(message.method, message.params, {
|
|
121
|
+
id: message.id,
|
|
122
|
+
timeoutMs: 15000
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await this._ensureReady();
|
|
127
|
+
this._sendJson(message);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async _ensureReady() {
|
|
132
|
+
if (this.connected && this.initialized && this.proc && this.proc.stdin && !this.proc.stdin.destroyed) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await this.start();
|
|
137
|
+
|
|
138
|
+
if (!(this.connected && this.initialized)) {
|
|
139
|
+
throw new Error("App server is not connected");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async _spawnStdioProcess() {
|
|
144
|
+
if (this.proc && !this.proc.killed) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const args = ["app-server", "--analytics-default-enabled"];
|
|
149
|
+
|
|
150
|
+
this.logger.info("Starting codex app-server (stdio)", {
|
|
151
|
+
codexCliPath: this.codexCliPath,
|
|
152
|
+
args
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const proc = spawn(this.codexCliPath, args, {
|
|
156
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
157
|
+
env: process.env
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
proc.stderr?.on("data", (chunk) => {
|
|
161
|
+
const line = chunk.toString("utf8").trim();
|
|
162
|
+
if (line) {
|
|
163
|
+
this.logger.debug("app-server stderr", { line });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
proc.stdout?.on("data", (chunk) => {
|
|
168
|
+
this._handleStdoutChunk(chunk.toString("utf8"));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
proc.on("error", (error) => {
|
|
172
|
+
this.logger.error("app-server spawn failed", { error: toErrorMessage(error) });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
proc.on("exit", (code, signal) => {
|
|
176
|
+
this.logger.warn("app-server exited", { code, signal });
|
|
177
|
+
this.proc = null;
|
|
178
|
+
this.connected = false;
|
|
179
|
+
this.initialized = false;
|
|
180
|
+
this.stdoutBuffer = "";
|
|
181
|
+
this._rejectAllPending(new Error("App server process exited"));
|
|
182
|
+
this._emitConnectionChanged();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
this.proc = proc;
|
|
186
|
+
this.connected = true;
|
|
187
|
+
this.initialized = false;
|
|
188
|
+
this._emitConnectionChanged();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_handleStdoutChunk(chunk) {
|
|
192
|
+
this.stdoutBuffer += chunk;
|
|
193
|
+
|
|
194
|
+
for (;;) {
|
|
195
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
196
|
+
if (newlineIndex < 0) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
201
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
202
|
+
|
|
203
|
+
if (!line) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let payload;
|
|
208
|
+
try {
|
|
209
|
+
payload = JSON.parse(line);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
this.logger.warn("Dropping non-JSON app-server line", {
|
|
212
|
+
line,
|
|
213
|
+
error: toErrorMessage(error)
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this._handleIncoming(payload);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_handleIncoming(payload) {
|
|
223
|
+
this.emit("message", payload);
|
|
224
|
+
|
|
225
|
+
if (payload.id != null) {
|
|
226
|
+
const pending = this.pending.get(payload.id);
|
|
227
|
+
if (pending) {
|
|
228
|
+
clearTimeout(pending.timer);
|
|
229
|
+
this.pending.delete(payload.id);
|
|
230
|
+
pending.resolve(payload);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (payload.method) {
|
|
236
|
+
if (payload.id != null) {
|
|
237
|
+
this.emit("request", payload);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.emit("notification", payload);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async _initializeProtocol() {
|
|
245
|
+
const initializeResponse = await this.sendRequest(
|
|
246
|
+
"initialize",
|
|
247
|
+
{
|
|
248
|
+
clientInfo: {
|
|
249
|
+
name: "codex_webstrapper",
|
|
250
|
+
title: "Codex Webstrapper",
|
|
251
|
+
version: "0.1.0"
|
|
252
|
+
},
|
|
253
|
+
capabilities: {
|
|
254
|
+
experimentalApi: true
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{ skipReadyCheck: true }
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (initializeResponse?.error) {
|
|
261
|
+
throw new Error(`App server initialize failed: ${JSON.stringify(initializeResponse.error)}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this._sendJson({ method: "initialized", params: {} });
|
|
265
|
+
this.initialized = true;
|
|
266
|
+
this._emitConnectionChanged();
|
|
267
|
+
this.emit("initialized");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_sendJson(payload) {
|
|
271
|
+
if (!this.proc || !this.proc.stdin || this.proc.stdin.destroyed) {
|
|
272
|
+
throw new Error("App server stdin is not writable");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.proc.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_rejectAllPending(error) {
|
|
279
|
+
for (const pending of this.pending.values()) {
|
|
280
|
+
clearTimeout(pending.timer);
|
|
281
|
+
pending.reject(error);
|
|
282
|
+
}
|
|
283
|
+
this.pending.clear();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_emitConnectionChanged() {
|
|
287
|
+
this.emit("connection-changed", this.getState());
|
|
288
|
+
}
|
|
289
|
+
}
|