remodex-windows-fix 1.0.2

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/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # remodex-windows-fix
2
+
3
+ Windows-friendly mirror of `remodex` with a launcher fix for `codex app-server`.
4
+
5
+ ## What Changed
6
+
7
+ - Fixes Windows startup when `remodex up` tries to spawn `codex` and fails with `spawn codex ENOENT`.
8
+ - Prefers `codex.cmd` or `codex.bat` on Windows and runs them with shell support.
9
+ - Falls back to `codex.exe` when a wrapper script is not present.
10
+ - Adds `REMODEX_CODEX_BIN` and `PHODEX_CODEX_BIN` overrides for explicit Codex binary selection.
11
+ - Prevents immediate uncaught startup crashes by routing launcher failures through Remodex error handling.
12
+
13
+ ## Why This Exists
14
+
15
+ On Windows, `codex` is often installed through npm as a `.cmd` shim. A plain Node `spawn("codex", ["app-server"])` call can fail even when `codex` works in PowerShell. This fork resolves the executable explicitly before launching the bridge.
16
+
17
+ ## Install
18
+
19
+ Install from this GitHub repository:
20
+
21
+ ```bash
22
+ npm install -g github:needitem/remodex-windows-fix
23
+ ```
24
+
25
+ Global CLI command:
26
+
27
+ ```bash
28
+ remodex-windows-fix
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ Start the bridge:
34
+
35
+ ```bash
36
+ remodex-windows-fix up
37
+ ```
38
+
39
+ Resume the last active thread:
40
+
41
+ ```bash
42
+ remodex-windows-fix resume
43
+ ```
44
+
45
+ Watch rollout output:
46
+
47
+ ```bash
48
+ remodex-windows-fix watch [threadId]
49
+ ```
50
+
51
+ ## Windows Override
52
+
53
+ If you want to force a specific Codex binary, set one of these environment variables before running `remodex up`:
54
+
55
+ ```powershell
56
+ $env:REMODEX_CODEX_BIN = "C:\Users\th072\AppData\Roaming\npm\codex.cmd"
57
+ ```
58
+
59
+ Legacy alias:
60
+
61
+ ```powershell
62
+ $env:PHODEX_CODEX_BIN = "C:\Users\th072\AppData\Roaming\npm\codex.cmd"
63
+ ```
64
+
65
+ ## Self-Hosted Relay
66
+
67
+ This repository now includes the upstream-compatible relay code and a local runner.
68
+
69
+ Start your own relay:
70
+
71
+ ```bash
72
+ npm run relay
73
+ ```
74
+
75
+ Or use the bundled binary directly:
76
+
77
+ ```bash
78
+ remodex-relay
79
+ ```
80
+
81
+ Optional relay host/port overrides:
82
+
83
+ ```powershell
84
+ $env:REMODEX_RELAY_HOST = "0.0.0.0"
85
+ $env:REMODEX_RELAY_PORT = "9000"
86
+ ```
87
+
88
+ Point the bridge at your relay:
89
+
90
+ ```powershell
91
+ $env:REMODEX_RELAY = "ws://YOUR_HOST:9000/relay"
92
+ remodex up
93
+ ```
94
+
95
+ For TLS/reverse-proxy setups, use the public `wss://YOUR_DOMAIN/relay` URL instead.
96
+
97
+ Health endpoint:
98
+
99
+ ```text
100
+ GET /health
101
+ ```
102
+
103
+ ## Cloudflare Deploy
104
+
105
+ This repository also includes a Cloudflare Workers relay implementation in [cloudflare/worker.mjs](cloudflare/worker.mjs) with Durable Objects configured in [wrangler.toml](wrangler.toml).
106
+
107
+ GitHub itself still does not host persistent WebSocket servers, but Cloudflare Workers can deploy this relay directly from your GitHub repository without running anything locally.
108
+
109
+ Import this repository in Cloudflare:
110
+
111
+ 1. Open Cloudflare Workers & Pages.
112
+ 2. Create or import a Worker from your GitHub repository.
113
+ 3. Use the worker name `remodex-relay` so it matches [wrangler.toml](wrangler.toml).
114
+ 4. Deploy the repository as-is.
115
+ 5. After deploy, use the public Worker URL as the relay base:
116
+
117
+ ```powershell
118
+ $env:REMODEX_RELAY = "wss://YOUR-WORKER.YOUR-SUBDOMAIN.workers.dev/relay"
119
+ remodex-windows-fix up
120
+ ```
121
+
122
+ Current deployed example for this repository:
123
+
124
+ ```powershell
125
+ $env:REMODEX_RELAY = "wss://remodex-relay.th07290828.workers.dev/relay"
126
+ remodex-windows-fix up
127
+ ```
128
+
129
+ If you are using `cmd.exe` instead of PowerShell:
130
+
131
+ ```cmd
132
+ set REMODEX_RELAY=wss://remodex-relay.th07290828.workers.dev/relay
133
+ remodex-windows-fix up
134
+ ```
135
+
136
+ Health check:
137
+
138
+ ```text
139
+ https://remodex-relay.th07290828.workers.dev/health
140
+ ```
141
+
142
+ Cloudflare health endpoint:
143
+
144
+ ```text
145
+ GET /health
146
+ ```
147
+
148
+ ## Alternative GitHub Deploy
149
+
150
+ If you prefer a normal Node web service instead of Workers, this repository still includes the Render-compatible runner in [render.yaml](render.yaml).
151
+
152
+ ```powershell
153
+ $env:REMODEX_RELAY = "wss://YOUR-SERVICE.onrender.com/relay"
154
+ remodex-windows-fix up
155
+ ```
156
+
157
+ ## Validation
158
+
159
+ Typical local verification:
160
+
161
+ ```bash
162
+ codex --version
163
+ codex app-server --help
164
+ remodex-windows-fix up
165
+ ```
166
+
167
+ Expected Windows resolution after the fix:
168
+
169
+ ```text
170
+ C:\Users\th072\AppData\Roaming\npm\codex.cmd app-server
171
+ ```
172
+
173
+ ## Project Layout
174
+
175
+ ```text
176
+ bin/
177
+ src/
178
+ package.json
179
+ ```
180
+
181
+ ## Attribution
182
+
183
+ This repository preserves the upstream Remodex package structure and credits the original package author in `package.json`. This fork adds a Windows launcher compatibility patch and accompanying documentation.
package/bin/phodex.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // FILE: phodex.js
3
+ // Purpose: Backward-compatible wrapper that forwards legacy `phodex up` usage to `remodex up`.
4
+ // Layer: CLI binary
5
+ // Exports: none
6
+ // Depends on: ./remodex
7
+
8
+ require("./remodex");
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // FILE: remodex-relay.js
3
+ // Purpose: CLI entrypoint for running the bundled Remodex relay locally.
4
+ // Layer: CLI binary
5
+ // Exports: none
6
+ // Depends on: ../src/relay-server
7
+
8
+ const { startRelayServer } = require("../src/relay-server");
9
+
10
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
11
+ console.log("Usage: remodex-relay [port]");
12
+ console.log("Environment: REMODEX_RELAY_HOST, REMODEX_RELAY_PORT");
13
+ process.exit(0);
14
+ }
15
+
16
+ const portArg = process.argv[2];
17
+ startRelayServer({
18
+ port: portArg ? Number.parseInt(portArg, 10) : undefined,
19
+ });
package/bin/remodex.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // FILE: remodex.js
3
+ // Purpose: CLI surface for starting the local Remodex bridge, reopening the latest active thread, and tailing its rollout file.
4
+ // Layer: CLI binary
5
+ // Exports: none
6
+ // Depends on: ../src
7
+
8
+ const { startBridge, openLastActiveThread, watchThreadRollout } = require("../src");
9
+
10
+ const command = process.argv[2] || "up";
11
+
12
+ if (command === "up") {
13
+ startBridge();
14
+ return;
15
+ }
16
+
17
+ if (command === "resume") {
18
+ try {
19
+ const state = openLastActiveThread();
20
+ console.log(
21
+ `[remodex] Opened last active thread: ${state.threadId} (${state.source || "unknown"})`
22
+ );
23
+ } catch (error) {
24
+ console.error(`[remodex] ${(error && error.message) || "Failed to reopen the last thread."}`);
25
+ process.exit(1);
26
+ }
27
+ return;
28
+ }
29
+
30
+ if (command === "watch") {
31
+ try {
32
+ watchThreadRollout(process.argv[3] || "");
33
+ } catch (error) {
34
+ console.error(`[remodex] ${(error && error.message) || "Failed to watch the thread rollout."}`);
35
+ process.exit(1);
36
+ }
37
+ return;
38
+ }
39
+
40
+ if (command !== "up") {
41
+ console.error(`Unknown command: ${command}`);
42
+ console.error("Usage: remodex up | remodex resume | remodex watch [threadId]");
43
+ process.exit(1);
44
+ }
@@ -0,0 +1,16 @@
1
+ # Cloudflare Relay
2
+
3
+ This folder contains a Cloudflare Workers + Durable Objects implementation of the Remodex relay protocol.
4
+
5
+ It preserves the same session path and headers used by the bridge and iPhone client:
6
+
7
+ - path: `/relay/{sessionId}`
8
+ - required header: `x-role: mac` or `x-role: iphone`
9
+ - close code `4000`: invalid session or role
10
+ - close code `4001`: previous Mac connection replaced
11
+ - close code `4002`: session unavailable / Mac disconnected
12
+ - close code `4003`: previous iPhone connection replaced
13
+
14
+ The public health endpoint is:
15
+
16
+ - `GET /health`
@@ -0,0 +1,221 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+
3
+ const MAX_HISTORY = 500;
4
+ const CLOSE_CODE_INVALID_SESSION = 4000;
5
+ const CLOSE_CODE_MAC_REPLACED = 4001;
6
+ const CLOSE_CODE_SESSION_UNAVAILABLE = 4002;
7
+ const CLOSE_CODE_IPHONE_REPLACED = 4003;
8
+
9
+ export default {
10
+ async fetch(request, env) {
11
+ const url = new URL(request.url);
12
+
13
+ if (url.pathname === "/health") {
14
+ return jsonResponse({
15
+ ok: true,
16
+ service: "remodex-relay",
17
+ runtime: "cloudflare-workers",
18
+ });
19
+ }
20
+
21
+ const match = url.pathname.match(/^\/relay\/([^/?]+)/);
22
+ if (!match) {
23
+ return new Response("not found", { status: 404 });
24
+ }
25
+
26
+ const sessionId = match[1];
27
+ const stub = env.SESSION_RELAY.get(env.SESSION_RELAY.idFromName(sessionId));
28
+ return stub.fetch(request);
29
+ },
30
+ };
31
+
32
+ export class SessionRelay extends DurableObject {
33
+ constructor(ctx, env) {
34
+ super(ctx, env);
35
+ this.ctx = ctx;
36
+ this.env = env;
37
+ this.mac = null;
38
+ this.clients = new Set();
39
+ this.history = [];
40
+
41
+ this.ctx.blockConcurrencyWhile(async () => {
42
+ this.history = (await this.ctx.storage.get("history")) || [];
43
+ for (const socket of this.ctx.getWebSockets()) {
44
+ const metadata = socket.deserializeAttachment() || {};
45
+ if (metadata.role === "mac") {
46
+ this.mac = socket;
47
+ } else if (metadata.role === "iphone") {
48
+ this.clients.add(socket);
49
+ }
50
+ }
51
+ });
52
+ }
53
+
54
+ async fetch(request) {
55
+ const upgrade = request.headers.get("Upgrade");
56
+ if (!upgrade || upgrade.toLowerCase() !== "websocket") {
57
+ return new Response("Expected WebSocket upgrade", { status: 426 });
58
+ }
59
+
60
+ const url = new URL(request.url);
61
+ const sessionId = url.pathname.match(/^\/relay\/([^/?]+)/)?.[1];
62
+ const role = request.headers.get("x-role");
63
+
64
+ if (!sessionId || (role !== "mac" && role !== "iphone")) {
65
+ return closeWebSocketResponse(
66
+ CLOSE_CODE_INVALID_SESSION,
67
+ "Missing sessionId or invalid x-role header"
68
+ );
69
+ }
70
+
71
+ if (role === "iphone" && !this.isSocketOpen(this.mac)) {
72
+ return closeWebSocketResponse(
73
+ CLOSE_CODE_SESSION_UNAVAILABLE,
74
+ "Mac session not available"
75
+ );
76
+ }
77
+
78
+ const pair = new WebSocketPair();
79
+ const [clientSocket, serverSocket] = Object.values(pair);
80
+
81
+ this.ctx.acceptWebSocket(serverSocket);
82
+ serverSocket.serializeAttachment({ role });
83
+
84
+ if (role === "mac") {
85
+ if (this.isSocketOpen(this.mac)) {
86
+ this.safeClose(
87
+ this.mac,
88
+ CLOSE_CODE_MAC_REPLACED,
89
+ "Replaced by new Mac connection"
90
+ );
91
+ }
92
+ this.mac = serverSocket;
93
+ console.log(`[relay] Mac connected -> session ${sessionId}`);
94
+ } else {
95
+ for (const existingClient of this.clients) {
96
+ if (existingClient === serverSocket) {
97
+ continue;
98
+ }
99
+ this.safeClose(
100
+ existingClient,
101
+ CLOSE_CODE_IPHONE_REPLACED,
102
+ "Replaced by newer iPhone connection"
103
+ );
104
+ }
105
+ this.clients = new Set([serverSocket]);
106
+ console.log(`[relay] iPhone connected -> session ${sessionId} (1 client)`);
107
+
108
+ for (const msg of this.history) {
109
+ this.safeSend(serverSocket, msg);
110
+ }
111
+ }
112
+
113
+ return new Response(null, { status: 101, webSocket: clientSocket });
114
+ }
115
+
116
+ async webSocketMessage(socket, message) {
117
+ const metadata = socket.deserializeAttachment() || {};
118
+ const text = normalizeMessage(message);
119
+
120
+ if (metadata.role === "mac") {
121
+ this.history.push(text);
122
+ if (this.history.length > MAX_HISTORY) {
123
+ this.history.shift();
124
+ }
125
+ await this.ctx.storage.put("history", this.history);
126
+
127
+ for (const client of this.clients) {
128
+ this.safeSend(client, text);
129
+ }
130
+ return;
131
+ }
132
+
133
+ if (this.isSocketOpen(this.mac)) {
134
+ this.safeSend(this.mac, text);
135
+ }
136
+ }
137
+
138
+ async webSocketClose(socket) {
139
+ await this.dropSocket(socket);
140
+ }
141
+
142
+ async webSocketError(socket, error) {
143
+ console.error("[relay] WebSocket error:", error?.message || String(error));
144
+ await this.dropSocket(socket);
145
+ }
146
+
147
+ async dropSocket(socket) {
148
+ const metadata = socket.deserializeAttachment() || {};
149
+
150
+ if (metadata.role === "mac") {
151
+ if (this.mac === socket) {
152
+ this.mac = null;
153
+ for (const client of this.clients) {
154
+ this.safeClose(client, CLOSE_CODE_SESSION_UNAVAILABLE, "Mac disconnected");
155
+ }
156
+ this.clients.clear();
157
+ }
158
+ } else if (metadata.role === "iphone") {
159
+ this.clients.delete(socket);
160
+ }
161
+
162
+ if (!this.isSocketOpen(this.mac) && this.clients.size === 0) {
163
+ this.history = [];
164
+ await this.ctx.storage.deleteAll();
165
+ }
166
+ }
167
+
168
+ isSocketOpen(socket) {
169
+ return !!socket && socket.readyState === 1;
170
+ }
171
+
172
+ safeSend(socket, message) {
173
+ if (!this.isSocketOpen(socket)) {
174
+ return;
175
+ }
176
+
177
+ try {
178
+ socket.send(message);
179
+ } catch {}
180
+ }
181
+
182
+ safeClose(socket, code, reason) {
183
+ if (!socket) {
184
+ return;
185
+ }
186
+
187
+ try {
188
+ socket.close(code, reason);
189
+ } catch {}
190
+ }
191
+ }
192
+
193
+ function normalizeMessage(message) {
194
+ if (typeof message === "string") {
195
+ return message;
196
+ }
197
+
198
+ if (message instanceof ArrayBuffer) {
199
+ return new TextDecoder().decode(message);
200
+ }
201
+
202
+ return String(message);
203
+ }
204
+
205
+ function closeWebSocketResponse(code, reason) {
206
+ const pair = new WebSocketPair();
207
+ const [clientSocket, serverSocket] = Object.values(pair);
208
+ serverSocket.accept();
209
+ serverSocket.close(code, reason);
210
+ return new Response(null, { status: 101, webSocket: clientSocket });
211
+ }
212
+
213
+ function jsonResponse(value) {
214
+ const payload = JSON.stringify(value);
215
+ return new Response(payload, {
216
+ status: 200,
217
+ headers: {
218
+ "content-type": "application/json; charset=utf-8",
219
+ },
220
+ });
221
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "remodex-windows-fix",
3
+ "version": "1.0.2",
4
+ "description": "Local bridge between Codex and the Remodex mobile app with a Windows-safe Codex launcher.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "remodex-windows-fix": "bin/remodex.js",
8
+ "remodex-relay": "bin/remodex-relay.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "cloudflare/",
13
+ "relay/",
14
+ "render.yaml",
15
+ "src/",
16
+ "wrangler.toml"
17
+ ],
18
+ "scripts": {
19
+ "start": "node ./bin/remodex.js up",
20
+ "relay": "node ./bin/remodex-relay.js"
21
+ },
22
+ "keywords": [
23
+ "remodex",
24
+ "codex",
25
+ "bridge",
26
+ "cli",
27
+ "windows"
28
+ ],
29
+ "author": "Emanuele Di Pietro",
30
+ "license": "ISC",
31
+ "type": "commonjs",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/needitem/remodex-windows-fix.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/needitem/remodex-windows-fix/issues"
41
+ },
42
+ "homepage": "https://github.com/needitem/remodex-windows-fix#readme",
43
+ "dependencies": {
44
+ "qrcode-terminal": "^0.12.0",
45
+ "uuid": "^13.0.0",
46
+ "ws": "^8.19.0"
47
+ }
48
+ }
@@ -0,0 +1,39 @@
1
+ # Relay
2
+
3
+ This folder contains the thin WebSocket relay used by the default hosted Remodex pairing flow.
4
+
5
+ The implementation is copied from the upstream open-source relay so you can run the same session routing yourself.
6
+
7
+ ## What It Does
8
+
9
+ - accepts WebSocket connections at `/relay/{sessionId}`
10
+ - pairs one Mac host with one live iPhone client for a session
11
+ - forwards JSON-RPC traffic between Mac and iPhone
12
+ - replays a small in-memory history buffer to a reconnecting iPhone client
13
+ - exposes lightweight stats for a health endpoint
14
+
15
+ ## What It Does Not Do
16
+
17
+ - it does not run Codex
18
+ - it does not execute git commands
19
+ - it does not contain your repository checkout
20
+ - it does not persist the local workspace on the server
21
+
22
+ Codex, git, and local file operations still run on the user's machine.
23
+
24
+ ## Local Runner
25
+
26
+ This repository includes a local relay runner:
27
+
28
+ ```bash
29
+ npm run relay
30
+ ```
31
+
32
+ Environment variables:
33
+
34
+ - `REMODEX_RELAY_HOST` defaults to `0.0.0.0`
35
+ - `REMODEX_RELAY_PORT` defaults to `9000`
36
+
37
+ Health endpoint:
38
+
39
+ - `GET /health`