@deepdream314/remodex 1.3.9 → 1.3.11
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/bin/remodex.js +158 -22
- package/package.json +1 -1
- package/src/account-status.js +39 -0
- package/src/bridge.js +10 -4
- package/src/daemon-state.js +1 -1
- package/src/index.js +18 -2
- package/src/linux-systemd.js +435 -0
- package/src/macos-launch-agent.js +2 -2
- package/src/voice-handler.js +97 -12
package/bin/remodex.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// FILE: remodex.js
|
|
3
|
-
// Purpose: CLI surface for foreground bridge runs, pairing reset, thread resume, and
|
|
3
|
+
// Purpose: CLI surface for foreground bridge runs, pairing reset, thread resume, and local service control.
|
|
4
4
|
// Layer: CLI binary
|
|
5
5
|
// Exports: none
|
|
6
6
|
// Depends on: ../src
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
|
+
getLinuxBridgeServiceStatus,
|
|
9
10
|
getMacOSBridgeServiceStatus,
|
|
11
|
+
printLinuxBridgePairingQr,
|
|
10
12
|
printMacOSBridgePairingQr,
|
|
13
|
+
printLinuxBridgeServiceStatus,
|
|
11
14
|
printMacOSBridgeServiceStatus,
|
|
12
15
|
readBridgeConfig,
|
|
16
|
+
resetLinuxBridgePairing,
|
|
13
17
|
resetMacOSBridgePairing,
|
|
18
|
+
runLinuxBridgeService,
|
|
14
19
|
runMacOSBridgeService,
|
|
15
20
|
startBridge,
|
|
21
|
+
startLinuxBridgeService,
|
|
16
22
|
startMacOSBridgeService,
|
|
23
|
+
stopLinuxBridgeService,
|
|
17
24
|
stopMacOSBridgeService,
|
|
18
25
|
resetBridgePairing,
|
|
19
26
|
openLastActiveThread,
|
|
@@ -22,14 +29,21 @@ const {
|
|
|
22
29
|
const { version } = require("../package.json");
|
|
23
30
|
|
|
24
31
|
const defaultDeps = {
|
|
32
|
+
getLinuxBridgeServiceStatus,
|
|
25
33
|
getMacOSBridgeServiceStatus,
|
|
34
|
+
printLinuxBridgePairingQr,
|
|
26
35
|
printMacOSBridgePairingQr,
|
|
36
|
+
printLinuxBridgeServiceStatus,
|
|
27
37
|
printMacOSBridgeServiceStatus,
|
|
28
38
|
readBridgeConfig,
|
|
39
|
+
resetLinuxBridgePairing,
|
|
29
40
|
resetMacOSBridgePairing,
|
|
41
|
+
runLinuxBridgeService,
|
|
30
42
|
runMacOSBridgeService,
|
|
31
43
|
startBridge,
|
|
44
|
+
startLinuxBridgeService,
|
|
32
45
|
startMacOSBridgeService,
|
|
46
|
+
stopLinuxBridgeService,
|
|
33
47
|
stopMacOSBridgeService,
|
|
34
48
|
resetBridgePairing,
|
|
35
49
|
openLastActiveThread,
|
|
@@ -67,6 +81,16 @@ async function main({
|
|
|
67
81
|
return;
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
if (platform === "linux") {
|
|
85
|
+
const result = await deps.startLinuxBridgeService({
|
|
86
|
+
waitForPairing: true,
|
|
87
|
+
});
|
|
88
|
+
deps.printLinuxBridgePairingQr({
|
|
89
|
+
pairingSession: result.pairingSession,
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
deps.startBridge();
|
|
71
95
|
return;
|
|
72
96
|
}
|
|
@@ -77,28 +101,41 @@ async function main({
|
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
if (command === "run-service") {
|
|
80
|
-
|
|
104
|
+
if (platform === "darwin") {
|
|
105
|
+
deps.runMacOSBridgeService();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (platform === "linux") {
|
|
110
|
+
deps.runLinuxBridgeService();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
deps.startBridge();
|
|
81
115
|
return;
|
|
82
116
|
}
|
|
83
117
|
|
|
84
118
|
if (command === "start") {
|
|
85
|
-
|
|
119
|
+
deps.readBridgeConfig();
|
|
120
|
+
const result = await startManagedBridgeService({
|
|
121
|
+
commandName: command,
|
|
86
122
|
platform,
|
|
123
|
+
deps,
|
|
87
124
|
consoleImpl,
|
|
88
125
|
exitImpl,
|
|
89
126
|
});
|
|
90
|
-
deps.readBridgeConfig();
|
|
91
|
-
const result = await deps.startMacOSBridgeService({
|
|
92
|
-
waitForPairing: false,
|
|
93
|
-
});
|
|
94
127
|
emitResult({
|
|
95
128
|
payload: {
|
|
96
129
|
ok: true,
|
|
97
130
|
currentVersion: version,
|
|
131
|
+
platform,
|
|
98
132
|
plistPath: result?.plistPath,
|
|
133
|
+
unitPath: result?.unitPath,
|
|
99
134
|
pairingSession: result?.pairingSession,
|
|
100
135
|
},
|
|
101
|
-
message:
|
|
136
|
+
message: platform === "darwin"
|
|
137
|
+
? "[remodex] macOS bridge service is running."
|
|
138
|
+
: "[remodex] Linux bridge service is running.",
|
|
102
139
|
jsonOutput,
|
|
103
140
|
consoleImpl,
|
|
104
141
|
});
|
|
@@ -106,23 +143,26 @@ async function main({
|
|
|
106
143
|
}
|
|
107
144
|
|
|
108
145
|
if (command === "restart") {
|
|
109
|
-
|
|
146
|
+
deps.readBridgeConfig();
|
|
147
|
+
const result = await startManagedBridgeService({
|
|
148
|
+
commandName: command,
|
|
110
149
|
platform,
|
|
150
|
+
deps,
|
|
111
151
|
consoleImpl,
|
|
112
152
|
exitImpl,
|
|
113
153
|
});
|
|
114
|
-
deps.readBridgeConfig();
|
|
115
|
-
const result = await deps.startMacOSBridgeService({
|
|
116
|
-
waitForPairing: false,
|
|
117
|
-
});
|
|
118
154
|
emitResult({
|
|
119
155
|
payload: {
|
|
120
156
|
ok: true,
|
|
121
157
|
currentVersion: version,
|
|
158
|
+
platform,
|
|
122
159
|
plistPath: result?.plistPath,
|
|
160
|
+
unitPath: result?.unitPath,
|
|
123
161
|
pairingSession: result?.pairingSession,
|
|
124
162
|
},
|
|
125
|
-
message:
|
|
163
|
+
message: platform === "darwin"
|
|
164
|
+
? "[remodex] macOS bridge service restarted."
|
|
165
|
+
: "[remodex] Linux bridge service restarted.",
|
|
126
166
|
jsonOutput,
|
|
127
167
|
consoleImpl,
|
|
128
168
|
});
|
|
@@ -130,18 +170,21 @@ async function main({
|
|
|
130
170
|
}
|
|
131
171
|
|
|
132
172
|
if (command === "stop") {
|
|
133
|
-
|
|
173
|
+
stopManagedBridgeService({
|
|
134
174
|
platform,
|
|
175
|
+
deps,
|
|
135
176
|
consoleImpl,
|
|
136
177
|
exitImpl,
|
|
137
178
|
});
|
|
138
|
-
deps.stopMacOSBridgeService();
|
|
139
179
|
emitResult({
|
|
140
180
|
payload: {
|
|
141
181
|
ok: true,
|
|
142
182
|
currentVersion: version,
|
|
183
|
+
platform,
|
|
143
184
|
},
|
|
144
|
-
message:
|
|
185
|
+
message: platform === "darwin"
|
|
186
|
+
? "[remodex] macOS bridge service stopped."
|
|
187
|
+
: "[remodex] Linux bridge service stopped.",
|
|
145
188
|
jsonOutput,
|
|
146
189
|
consoleImpl,
|
|
147
190
|
});
|
|
@@ -149,19 +192,25 @@ async function main({
|
|
|
149
192
|
}
|
|
150
193
|
|
|
151
194
|
if (command === "status") {
|
|
152
|
-
|
|
195
|
+
const status = getManagedBridgeServiceStatus({
|
|
153
196
|
platform,
|
|
197
|
+
deps,
|
|
154
198
|
consoleImpl,
|
|
155
199
|
exitImpl,
|
|
156
200
|
});
|
|
157
201
|
if (jsonOutput) {
|
|
158
202
|
emitJson({
|
|
159
|
-
...
|
|
203
|
+
...status,
|
|
160
204
|
currentVersion: version,
|
|
161
205
|
});
|
|
162
206
|
return;
|
|
163
207
|
}
|
|
164
|
-
|
|
208
|
+
printManagedBridgeServiceStatus({
|
|
209
|
+
platform,
|
|
210
|
+
deps,
|
|
211
|
+
consoleImpl,
|
|
212
|
+
exitImpl,
|
|
213
|
+
});
|
|
165
214
|
return;
|
|
166
215
|
}
|
|
167
216
|
|
|
@@ -179,6 +228,18 @@ async function main({
|
|
|
179
228
|
jsonOutput,
|
|
180
229
|
consoleImpl,
|
|
181
230
|
});
|
|
231
|
+
} else if (platform === "linux") {
|
|
232
|
+
deps.resetLinuxBridgePairing();
|
|
233
|
+
emitResult({
|
|
234
|
+
payload: {
|
|
235
|
+
ok: true,
|
|
236
|
+
currentVersion: version,
|
|
237
|
+
platform,
|
|
238
|
+
},
|
|
239
|
+
message: "[remodex] Stopped the Linux bridge service and cleared the saved pairing state. Run `remodex up` to pair again.",
|
|
240
|
+
jsonOutput,
|
|
241
|
+
consoleImpl,
|
|
242
|
+
});
|
|
182
243
|
} else {
|
|
183
244
|
deps.resetBridgePairing();
|
|
184
245
|
emitResult({
|
|
@@ -296,14 +357,89 @@ function assertMacOSCommand(name, {
|
|
|
296
357
|
consoleImpl = console,
|
|
297
358
|
exitImpl = process.exit,
|
|
298
359
|
} = {}) {
|
|
299
|
-
if (platform === "darwin") {
|
|
360
|
+
if (platform === "darwin" || platform === "linux") {
|
|
300
361
|
return;
|
|
301
362
|
}
|
|
302
363
|
|
|
303
|
-
consoleImpl.error(`[remodex] \`${name}\` is only available on macOS. Use \`remodex up\` or \`remodex run\` for the foreground bridge on this OS.`);
|
|
364
|
+
consoleImpl.error(`[remodex] \`${name}\` is only available on macOS and Linux. Use \`remodex up\` or \`remodex run\` for the foreground bridge on this OS.`);
|
|
304
365
|
exitImpl(1);
|
|
305
366
|
}
|
|
306
367
|
|
|
368
|
+
async function startManagedBridgeService({
|
|
369
|
+
commandName = "start",
|
|
370
|
+
platform = process.platform,
|
|
371
|
+
deps = defaultDeps,
|
|
372
|
+
consoleImpl = console,
|
|
373
|
+
exitImpl = process.exit,
|
|
374
|
+
} = {}) {
|
|
375
|
+
assertMacOSCommand(commandName, {
|
|
376
|
+
platform,
|
|
377
|
+
consoleImpl,
|
|
378
|
+
exitImpl,
|
|
379
|
+
});
|
|
380
|
+
if (platform === "darwin") {
|
|
381
|
+
return deps.startMacOSBridgeService({
|
|
382
|
+
waitForPairing: false,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return deps.startLinuxBridgeService({
|
|
386
|
+
waitForPairing: false,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function stopManagedBridgeService({
|
|
391
|
+
platform = process.platform,
|
|
392
|
+
deps = defaultDeps,
|
|
393
|
+
consoleImpl = console,
|
|
394
|
+
exitImpl = process.exit,
|
|
395
|
+
} = {}) {
|
|
396
|
+
assertMacOSCommand("stop", {
|
|
397
|
+
platform,
|
|
398
|
+
consoleImpl,
|
|
399
|
+
exitImpl,
|
|
400
|
+
});
|
|
401
|
+
if (platform === "darwin") {
|
|
402
|
+
deps.stopMacOSBridgeService();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
deps.stopLinuxBridgeService();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function getManagedBridgeServiceStatus({
|
|
409
|
+
platform = process.platform,
|
|
410
|
+
deps = defaultDeps,
|
|
411
|
+
consoleImpl = console,
|
|
412
|
+
exitImpl = process.exit,
|
|
413
|
+
} = {}) {
|
|
414
|
+
assertMacOSCommand("status", {
|
|
415
|
+
platform,
|
|
416
|
+
consoleImpl,
|
|
417
|
+
exitImpl,
|
|
418
|
+
});
|
|
419
|
+
if (platform === "darwin") {
|
|
420
|
+
return deps.getMacOSBridgeServiceStatus();
|
|
421
|
+
}
|
|
422
|
+
return deps.getLinuxBridgeServiceStatus();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function printManagedBridgeServiceStatus({
|
|
426
|
+
platform = process.platform,
|
|
427
|
+
deps = defaultDeps,
|
|
428
|
+
consoleImpl = console,
|
|
429
|
+
exitImpl = process.exit,
|
|
430
|
+
} = {}) {
|
|
431
|
+
assertMacOSCommand("status", {
|
|
432
|
+
platform,
|
|
433
|
+
consoleImpl,
|
|
434
|
+
exitImpl,
|
|
435
|
+
});
|
|
436
|
+
if (platform === "darwin") {
|
|
437
|
+
deps.printMacOSBridgeServiceStatus();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
deps.printLinuxBridgeServiceStatus();
|
|
441
|
+
}
|
|
442
|
+
|
|
307
443
|
function isVersionCommand(value) {
|
|
308
444
|
return value === "-v" || value === "--v" || value === "-V" || value === "--version" || value === "version";
|
|
309
445
|
}
|
package/package.json
CHANGED
package/src/account-status.js
CHANGED
|
@@ -80,6 +80,44 @@ function redactAuthStatus(authStatus = null, extras = {}) {
|
|
|
80
80
|
|
|
81
81
|
// ─── Settled snapshot helpers ───────────────────────────────
|
|
82
82
|
|
|
83
|
+
function applyLocalAuthFallbackToSettledAuthStatus(
|
|
84
|
+
authStatusResult = null,
|
|
85
|
+
localAuthResult = null
|
|
86
|
+
) {
|
|
87
|
+
const localAuthStatus = localAuthResult?.status === "fulfilled" ? localAuthResult.value : null;
|
|
88
|
+
if (!localAuthStatus?.token || !localAuthStatus?.isChatGPT) {
|
|
89
|
+
return authStatusResult;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (authStatusResult?.status === "fulfilled") {
|
|
93
|
+
const authStatus = authStatusResult.value;
|
|
94
|
+
if (normalizeString(authStatus?.authToken)) {
|
|
95
|
+
return authStatusResult;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
status: "fulfilled",
|
|
100
|
+
value: {
|
|
101
|
+
...authStatus,
|
|
102
|
+
authMethod: normalizeString(localAuthStatus.authMethod)
|
|
103
|
+
|| normalizeString(authStatus?.authMethod)
|
|
104
|
+
|| "chatgpt",
|
|
105
|
+
authToken: localAuthStatus.token,
|
|
106
|
+
requiresOpenaiAuth: localAuthStatus.requiresOpenaiAuth ?? authStatus?.requiresOpenaiAuth,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
status: "fulfilled",
|
|
113
|
+
value: {
|
|
114
|
+
authMethod: normalizeString(localAuthStatus.authMethod) || "chatgpt",
|
|
115
|
+
authToken: localAuthStatus.token,
|
|
116
|
+
requiresOpenaiAuth: localAuthStatus.requiresOpenaiAuth,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
83
121
|
// Collapses settled bridge RPC results into one safe snapshot, even if one side fails.
|
|
84
122
|
// Input: Promise.allSettled-style results → Output: sanitized account status object
|
|
85
123
|
// Throws if both the account read and auth status fail, so the bridge can surface a real error.
|
|
@@ -141,6 +179,7 @@ function parseBoolean(value) {
|
|
|
141
179
|
}
|
|
142
180
|
|
|
143
181
|
module.exports = {
|
|
182
|
+
applyLocalAuthFallbackToSettledAuthStatus,
|
|
144
183
|
composeAccountStatus,
|
|
145
184
|
composeSanitizedAuthStatusFromSettledResults,
|
|
146
185
|
redactAuthStatus,
|
package/src/bridge.js
CHANGED
|
@@ -22,8 +22,13 @@ const { handleGitRequest } = require("./git-handler");
|
|
|
22
22
|
const { handleThreadContextRequest } = require("./thread-context-handler");
|
|
23
23
|
const { handleWorkspaceRequest } = require("./workspace-handler");
|
|
24
24
|
const { createNotificationsHandler } = require("./notifications-handler");
|
|
25
|
-
const { createVoiceHandler, resolveVoiceAuth } = require("./voice-handler");
|
|
26
25
|
const {
|
|
26
|
+
createVoiceHandler,
|
|
27
|
+
readLocalChatGPTAuthTokenFromDisk,
|
|
28
|
+
resolveVoiceAuth,
|
|
29
|
+
} = require("./voice-handler");
|
|
30
|
+
const {
|
|
31
|
+
applyLocalAuthFallbackToSettledAuthStatus,
|
|
27
32
|
composeSanitizedAuthStatusFromSettledResults,
|
|
28
33
|
} = require("./account-status");
|
|
29
34
|
const { createBridgePackageVersionStatusReader } = require("./package-version-status");
|
|
@@ -528,7 +533,7 @@ function startBridge({
|
|
|
528
533
|
case "account/login/openOnMac":
|
|
529
534
|
return openPendingAuthLoginOnMac(params);
|
|
530
535
|
case "voice/resolveAuth":
|
|
531
|
-
return resolveVoiceAuth(sendCodexRequest);
|
|
536
|
+
return resolveVoiceAuth(sendCodexRequest, params);
|
|
532
537
|
default:
|
|
533
538
|
throw new Error(`Unsupported bridge-managed account method: ${method}`);
|
|
534
539
|
}
|
|
@@ -537,7 +542,7 @@ function startBridge({
|
|
|
537
542
|
// Combines account/read + getAuthStatus into one safe snapshot for the phone UI.
|
|
538
543
|
// The two RPCs are settled independently so one transient failure does not hide the other.
|
|
539
544
|
async function readSanitizedAuthStatus() {
|
|
540
|
-
const [accountReadResult, authStatusResult, bridgeVersionInfoResult] = await Promise.allSettled([
|
|
545
|
+
const [accountReadResult, authStatusResult, bridgeVersionInfoResult, localAuthResult] = await Promise.allSettled([
|
|
541
546
|
sendCodexRequest("account/read", {
|
|
542
547
|
refreshToken: false,
|
|
543
548
|
}),
|
|
@@ -546,6 +551,7 @@ function startBridge({
|
|
|
546
551
|
refreshToken: false,
|
|
547
552
|
}),
|
|
548
553
|
readBridgePackageVersionStatus(),
|
|
554
|
+
readLocalChatGPTAuthTokenFromDisk(),
|
|
549
555
|
]);
|
|
550
556
|
|
|
551
557
|
return composeSanitizedAuthStatusFromSettledResults({
|
|
@@ -555,7 +561,7 @@ function startBridge({
|
|
|
555
561
|
value: normalizeAccountRead(accountReadResult.value),
|
|
556
562
|
}
|
|
557
563
|
: accountReadResult,
|
|
558
|
-
authStatusResult,
|
|
564
|
+
authStatusResult: applyLocalAuthFallbackToSettledAuthStatus(authStatusResult, localAuthResult),
|
|
559
565
|
loginInFlight: Boolean(pendingAuthLogin.loginId),
|
|
560
566
|
bridgeVersionInfo: bridgeVersionInfoResult.status === "fulfilled"
|
|
561
567
|
? bridgeVersionInfoResult.value
|
package/src/daemon-state.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FILE: daemon-state.js
|
|
2
|
-
// Purpose: Persists
|
|
2
|
+
// Purpose: Persists bridge service config/runtime state outside the repo for local daemon flows.
|
|
3
3
|
// Layer: CLI helper
|
|
4
4
|
// Exports: path resolvers plus read/write helpers for daemon config, pairing payloads, and service status.
|
|
5
5
|
// Depends on: fs, os, path
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
// FILE: index.js
|
|
2
2
|
// Purpose: Small entrypoint wrapper for bridge lifecycle commands.
|
|
3
3
|
// Layer: CLI entry
|
|
4
|
-
// Exports: bridge lifecycle, pairing reset, thread resume/watch, and
|
|
5
|
-
// Depends on: ./bridge, ./secure-device-state, ./session-state, ./rollout-watch, ./macos-launch-agent
|
|
4
|
+
// Exports: bridge lifecycle, pairing reset, thread resume/watch, and platform service helpers.
|
|
5
|
+
// Depends on: ./bridge, ./secure-device-state, ./session-state, ./rollout-watch, ./linux-systemd, ./macos-launch-agent
|
|
6
6
|
|
|
7
7
|
const { startBridge } = require("./bridge");
|
|
8
8
|
const { resetBridgeDeviceState } = require("./secure-device-state");
|
|
9
9
|
const { openLastActiveThread } = require("./session-state");
|
|
10
10
|
const { watchThreadRollout } = require("./rollout-watch");
|
|
11
11
|
const { readBridgeConfig } = require("./codex-desktop-refresher");
|
|
12
|
+
const {
|
|
13
|
+
getLinuxBridgeServiceStatus,
|
|
14
|
+
printLinuxBridgePairingQr,
|
|
15
|
+
printLinuxBridgeServiceStatus,
|
|
16
|
+
resetLinuxBridgePairing,
|
|
17
|
+
runLinuxBridgeService,
|
|
18
|
+
startLinuxBridgeService,
|
|
19
|
+
stopLinuxBridgeService,
|
|
20
|
+
} = require("./linux-systemd");
|
|
12
21
|
const {
|
|
13
22
|
getMacOSBridgeServiceStatus,
|
|
14
23
|
printMacOSBridgePairingQr,
|
|
@@ -20,14 +29,21 @@ const {
|
|
|
20
29
|
} = require("./macos-launch-agent");
|
|
21
30
|
|
|
22
31
|
module.exports = {
|
|
32
|
+
getLinuxBridgeServiceStatus,
|
|
23
33
|
getMacOSBridgeServiceStatus,
|
|
34
|
+
printLinuxBridgePairingQr,
|
|
24
35
|
printMacOSBridgePairingQr,
|
|
36
|
+
printLinuxBridgeServiceStatus,
|
|
25
37
|
printMacOSBridgeServiceStatus,
|
|
26
38
|
readBridgeConfig,
|
|
39
|
+
resetLinuxBridgePairing,
|
|
27
40
|
resetMacOSBridgePairing,
|
|
41
|
+
runLinuxBridgeService,
|
|
28
42
|
startBridge,
|
|
29
43
|
runMacOSBridgeService,
|
|
44
|
+
startLinuxBridgeService,
|
|
30
45
|
startMacOSBridgeService,
|
|
46
|
+
stopLinuxBridgeService,
|
|
31
47
|
stopMacOSBridgeService,
|
|
32
48
|
resetBridgePairing: resetBridgeDeviceState,
|
|
33
49
|
openLastActiveThread,
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// FILE: linux-systemd.js
|
|
2
|
+
// Purpose: Owns Linux-only systemd user-service install/start/stop/status helpers for the background Remodex bridge.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: start/stop/status helpers plus the systemd service runner used by `remodex up`.
|
|
5
|
+
// Depends on: child_process, fs, os, path, ./bridge, ./daemon-state, ./codex-desktop-refresher, ./qr, ./secure-device-state
|
|
6
|
+
|
|
7
|
+
const { execFileSync } = require("child_process");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { startBridge } = require("./bridge");
|
|
12
|
+
const { readBridgeConfig } = require("./codex-desktop-refresher");
|
|
13
|
+
const { printQR } = require("./qr");
|
|
14
|
+
const { resetBridgeDeviceState } = require("./secure-device-state");
|
|
15
|
+
const {
|
|
16
|
+
clearBridgeStatus,
|
|
17
|
+
clearPairingSession,
|
|
18
|
+
ensureRemodexLogsDir,
|
|
19
|
+
ensureRemodexStateDir,
|
|
20
|
+
readBridgeStatus,
|
|
21
|
+
readDaemonConfig,
|
|
22
|
+
readPairingSession,
|
|
23
|
+
resolveBridgeStderrLogPath,
|
|
24
|
+
resolveBridgeStdoutLogPath,
|
|
25
|
+
resolveRemodexStateDir,
|
|
26
|
+
writeBridgeStatus,
|
|
27
|
+
writeDaemonConfig,
|
|
28
|
+
writePairingSession,
|
|
29
|
+
} = require("./daemon-state");
|
|
30
|
+
|
|
31
|
+
const SERVICE_NAME = "com.remodex.bridge.service";
|
|
32
|
+
const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
|
|
33
|
+
const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
|
|
34
|
+
|
|
35
|
+
function runLinuxBridgeService({ env = process.env, platform = process.platform } = {}) {
|
|
36
|
+
assertLinuxPlatform(platform);
|
|
37
|
+
const config = readDaemonConfig({ env });
|
|
38
|
+
if (!config?.relayUrl) {
|
|
39
|
+
const message = "No relay URL configured for the Linux bridge service.";
|
|
40
|
+
clearPairingSession({ env });
|
|
41
|
+
writeBridgeStatus({
|
|
42
|
+
state: "error",
|
|
43
|
+
connectionStatus: "error",
|
|
44
|
+
pid: process.pid,
|
|
45
|
+
lastError: message,
|
|
46
|
+
}, { env });
|
|
47
|
+
console.error(`[remodex] ${message}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
startBridge({
|
|
52
|
+
config,
|
|
53
|
+
printPairingQr: false,
|
|
54
|
+
onPairingPayload(pairingPayload) {
|
|
55
|
+
writePairingSession(pairingPayload, { env });
|
|
56
|
+
},
|
|
57
|
+
onBridgeStatus(status) {
|
|
58
|
+
writeBridgeStatus(status, { env });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function startLinuxBridgeService({
|
|
64
|
+
env = process.env,
|
|
65
|
+
platform = process.platform,
|
|
66
|
+
fsImpl = fs,
|
|
67
|
+
execFileSyncImpl = execFileSync,
|
|
68
|
+
osImpl = os,
|
|
69
|
+
nodePath = process.execPath,
|
|
70
|
+
cliPath = path.resolve(__dirname, "..", "bin", "remodex.js"),
|
|
71
|
+
waitForPairing = false,
|
|
72
|
+
pairingTimeoutMs = DEFAULT_PAIRING_WAIT_TIMEOUT_MS,
|
|
73
|
+
pairingPollIntervalMs = DEFAULT_PAIRING_WAIT_INTERVAL_MS,
|
|
74
|
+
} = {}) {
|
|
75
|
+
assertLinuxPlatform(platform);
|
|
76
|
+
const config = readBridgeConfig({ env });
|
|
77
|
+
assertRelayConfigured(config);
|
|
78
|
+
const startedAt = Date.now();
|
|
79
|
+
|
|
80
|
+
writeDaemonConfig(config, { env, fsImpl });
|
|
81
|
+
clearPairingSession({ env, fsImpl });
|
|
82
|
+
clearBridgeStatus({ env, fsImpl });
|
|
83
|
+
ensureRemodexStateDir({ env, fsImpl, osImpl });
|
|
84
|
+
ensureRemodexLogsDir({ env, fsImpl, osImpl });
|
|
85
|
+
|
|
86
|
+
const unitPath = writeUserServiceUnit({
|
|
87
|
+
env,
|
|
88
|
+
fsImpl,
|
|
89
|
+
osImpl,
|
|
90
|
+
nodePath,
|
|
91
|
+
cliPath,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
runSystemctl(["--user", "daemon-reload"], { execFileSyncImpl });
|
|
95
|
+
runSystemctl(["--user", "enable", SERVICE_NAME], { execFileSyncImpl });
|
|
96
|
+
runSystemctl(["--user", "restart", SERVICE_NAME], { execFileSyncImpl });
|
|
97
|
+
|
|
98
|
+
if (!waitForPairing) {
|
|
99
|
+
return {
|
|
100
|
+
unitPath,
|
|
101
|
+
pairingSession: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pairingSession = await waitForFreshPairingSession({
|
|
106
|
+
env,
|
|
107
|
+
fsImpl,
|
|
108
|
+
startedAt,
|
|
109
|
+
timeoutMs: pairingTimeoutMs,
|
|
110
|
+
intervalMs: pairingPollIntervalMs,
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
unitPath,
|
|
114
|
+
pairingSession,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stopLinuxBridgeService({
|
|
119
|
+
env = process.env,
|
|
120
|
+
platform = process.platform,
|
|
121
|
+
execFileSyncImpl = execFileSync,
|
|
122
|
+
fsImpl = fs,
|
|
123
|
+
} = {}) {
|
|
124
|
+
assertLinuxPlatform(platform);
|
|
125
|
+
try {
|
|
126
|
+
runSystemctl(["--user", "disable", "--now", SERVICE_NAME], {
|
|
127
|
+
execFileSyncImpl,
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (!isMissingSystemdUnitError(error)) {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
clearPairingSession({ env, fsImpl });
|
|
135
|
+
clearBridgeStatus({ env, fsImpl });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resetLinuxBridgePairing({
|
|
139
|
+
env = process.env,
|
|
140
|
+
platform = process.platform,
|
|
141
|
+
execFileSyncImpl = execFileSync,
|
|
142
|
+
fsImpl = fs,
|
|
143
|
+
resetBridgePairingImpl = resetBridgeDeviceState,
|
|
144
|
+
} = {}) {
|
|
145
|
+
assertLinuxPlatform(platform);
|
|
146
|
+
stopLinuxBridgeService({
|
|
147
|
+
env,
|
|
148
|
+
platform,
|
|
149
|
+
execFileSyncImpl,
|
|
150
|
+
fsImpl,
|
|
151
|
+
});
|
|
152
|
+
return resetBridgePairingImpl();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getLinuxBridgeServiceStatus({
|
|
156
|
+
env = process.env,
|
|
157
|
+
platform = process.platform,
|
|
158
|
+
execFileSyncImpl = execFileSync,
|
|
159
|
+
fsImpl = fs,
|
|
160
|
+
osImpl = os,
|
|
161
|
+
} = {}) {
|
|
162
|
+
assertLinuxPlatform(platform);
|
|
163
|
+
const unitPath = resolveUserServiceUnitPath({ env, osImpl });
|
|
164
|
+
const serviceState = readSystemdServiceState({
|
|
165
|
+
execFileSyncImpl,
|
|
166
|
+
missingAsStopped: true,
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
label: SERVICE_NAME,
|
|
170
|
+
manager: "systemd-user",
|
|
171
|
+
platform: "linux",
|
|
172
|
+
installed: fsImpl.existsSync(unitPath),
|
|
173
|
+
unitPath,
|
|
174
|
+
unitLoaded: serviceState.loadState === "loaded",
|
|
175
|
+
activeState: serviceState.activeState,
|
|
176
|
+
subState: serviceState.subState,
|
|
177
|
+
servicePid: serviceState.execMainPid,
|
|
178
|
+
unitFileState: serviceState.unitFileState,
|
|
179
|
+
daemonConfig: readDaemonConfig({ env, fsImpl }),
|
|
180
|
+
bridgeStatus: readBridgeStatus({ env, fsImpl }),
|
|
181
|
+
pairingSession: readPairingSession({ env, fsImpl }),
|
|
182
|
+
stdoutLogPath: resolveBridgeStdoutLogPath({ env }),
|
|
183
|
+
stderrLogPath: resolveBridgeStderrLogPath({ env }),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function printLinuxBridgeServiceStatus(options = {}) {
|
|
188
|
+
const status = getLinuxBridgeServiceStatus(options);
|
|
189
|
+
const bridgeState = status.bridgeStatus?.state || "unknown";
|
|
190
|
+
const connectionStatus = status.bridgeStatus?.connectionStatus || "unknown";
|
|
191
|
+
const pairingCreatedAt = status.pairingSession?.createdAt || "none";
|
|
192
|
+
console.log(`[remodex] Service label: ${status.label}`);
|
|
193
|
+
console.log("[remodex] Service manager: systemd --user");
|
|
194
|
+
console.log(`[remodex] Installed: ${status.installed ? "yes" : "no"}`);
|
|
195
|
+
console.log(`[remodex] Loaded: ${status.unitLoaded ? "yes" : "no"}`);
|
|
196
|
+
console.log(`[remodex] Active state: ${status.activeState}`);
|
|
197
|
+
console.log(`[remodex] Sub-state: ${status.subState}`);
|
|
198
|
+
console.log(`[remodex] PID: ${status.servicePid || status.bridgeStatus?.pid || "unknown"}`);
|
|
199
|
+
console.log(`[remodex] Bridge state: ${bridgeState}`);
|
|
200
|
+
console.log(`[remodex] Connection: ${connectionStatus}`);
|
|
201
|
+
console.log(`[remodex] Pairing payload: ${pairingCreatedAt}`);
|
|
202
|
+
console.log(`[remodex] Unit file: ${status.unitPath}`);
|
|
203
|
+
console.log(`[remodex] Stdout log: ${status.stdoutLogPath}`);
|
|
204
|
+
console.log(`[remodex] Stderr log: ${status.stderrLogPath}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function printLinuxBridgePairingQr({ pairingSession = null, env = process.env, fsImpl = fs } = {}) {
|
|
208
|
+
const nextPairingSession = pairingSession || readPairingSession({ env, fsImpl });
|
|
209
|
+
const pairingPayload = nextPairingSession?.pairingPayload;
|
|
210
|
+
if (!pairingPayload) {
|
|
211
|
+
throw new Error("The Linux bridge service did not publish a pairing payload yet.");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
printQR(pairingPayload);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function writeUserServiceUnit({
|
|
218
|
+
env = process.env,
|
|
219
|
+
fsImpl = fs,
|
|
220
|
+
osImpl = os,
|
|
221
|
+
nodePath = process.execPath,
|
|
222
|
+
cliPath = path.resolve(__dirname, "..", "bin", "remodex.js"),
|
|
223
|
+
} = {}) {
|
|
224
|
+
const unitPath = resolveUserServiceUnitPath({ env, osImpl });
|
|
225
|
+
const stateDir = resolveRemodexStateDir({ env, osImpl });
|
|
226
|
+
const stdoutLogPath = resolveBridgeStdoutLogPath({ env, osImpl });
|
|
227
|
+
const stderrLogPath = resolveBridgeStderrLogPath({ env, osImpl });
|
|
228
|
+
const homeDir = env.HOME || osImpl.homedir();
|
|
229
|
+
const workingDirectory = path.resolve(__dirname, "..");
|
|
230
|
+
const serialized = buildUserServiceUnit({
|
|
231
|
+
homeDir,
|
|
232
|
+
pathEnv: env.PATH || "",
|
|
233
|
+
stateDir,
|
|
234
|
+
stdoutLogPath,
|
|
235
|
+
stderrLogPath,
|
|
236
|
+
workingDirectory,
|
|
237
|
+
nodePath,
|
|
238
|
+
cliPath,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
fsImpl.mkdirSync(path.dirname(unitPath), { recursive: true });
|
|
242
|
+
fsImpl.writeFileSync(unitPath, serialized, "utf8");
|
|
243
|
+
return unitPath;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildUserServiceUnit({
|
|
247
|
+
homeDir,
|
|
248
|
+
pathEnv,
|
|
249
|
+
stateDir,
|
|
250
|
+
stdoutLogPath,
|
|
251
|
+
stderrLogPath,
|
|
252
|
+
workingDirectory,
|
|
253
|
+
nodePath,
|
|
254
|
+
cliPath,
|
|
255
|
+
}) {
|
|
256
|
+
return `[Unit]
|
|
257
|
+
Description=Remodex bridge
|
|
258
|
+
After=default.target
|
|
259
|
+
|
|
260
|
+
[Service]
|
|
261
|
+
Type=simple
|
|
262
|
+
WorkingDirectory=${quoteSystemdPath(workingDirectory)}
|
|
263
|
+
ExecStart=${quoteSystemdExec(nodePath, cliPath, "run-service")}
|
|
264
|
+
Restart=on-failure
|
|
265
|
+
RestartSec=2
|
|
266
|
+
Environment=${quoteSystemdEnv("HOME", homeDir)}
|
|
267
|
+
Environment=${quoteSystemdEnv("PATH", pathEnv)}
|
|
268
|
+
Environment=${quoteSystemdEnv("REMODEX_DEVICE_STATE_DIR", stateDir)}
|
|
269
|
+
StandardOutput=append:${quoteSystemdPath(stdoutLogPath)}
|
|
270
|
+
StandardError=append:${quoteSystemdPath(stderrLogPath)}
|
|
271
|
+
|
|
272
|
+
[Install]
|
|
273
|
+
WantedBy=default.target
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function resolveUserServiceUnitPath({ env = process.env, osImpl = os } = {}) {
|
|
278
|
+
const configHome = normalizeNonEmptyString(env.XDG_CONFIG_HOME)
|
|
279
|
+
|| path.join(env.HOME || osImpl.homedir(), ".config");
|
|
280
|
+
return path.join(configHome, "systemd", "user", SERVICE_NAME);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function readSystemdServiceState({
|
|
284
|
+
execFileSyncImpl = execFileSync,
|
|
285
|
+
missingAsStopped = false,
|
|
286
|
+
} = {}) {
|
|
287
|
+
try {
|
|
288
|
+
const output = runSystemctl([
|
|
289
|
+
"--user",
|
|
290
|
+
"show",
|
|
291
|
+
"--property=LoadState,ActiveState,SubState,ExecMainPID,UnitFileState",
|
|
292
|
+
SERVICE_NAME,
|
|
293
|
+
], {
|
|
294
|
+
execFileSyncImpl,
|
|
295
|
+
encoding: "utf8",
|
|
296
|
+
});
|
|
297
|
+
return parseSystemdShowOutput(output);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (missingAsStopped && isMissingSystemdUnitError(error)) {
|
|
300
|
+
return {
|
|
301
|
+
loadState: "not-found",
|
|
302
|
+
activeState: "inactive",
|
|
303
|
+
subState: "dead",
|
|
304
|
+
execMainPid: null,
|
|
305
|
+
unitFileState: "disabled",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function runSystemctl(args, {
|
|
313
|
+
execFileSyncImpl = execFileSync,
|
|
314
|
+
encoding = "utf8",
|
|
315
|
+
} = {}) {
|
|
316
|
+
try {
|
|
317
|
+
return execFileSyncImpl("systemctl", args, {
|
|
318
|
+
encoding,
|
|
319
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error?.code === "ENOENT") {
|
|
323
|
+
throw new Error("`systemctl --user` is required for Linux bridge service management.");
|
|
324
|
+
}
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function parseSystemdShowOutput(output) {
|
|
330
|
+
const values = Object.create(null);
|
|
331
|
+
for (const line of String(output || "").split(/\r?\n/)) {
|
|
332
|
+
if (!line || !line.includes("=")) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const separatorIndex = line.indexOf("=");
|
|
336
|
+
const key = line.slice(0, separatorIndex);
|
|
337
|
+
const value = line.slice(separatorIndex + 1);
|
|
338
|
+
values[key] = value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const pidValue = Number.parseInt(values.ExecMainPID || "", 10);
|
|
342
|
+
return {
|
|
343
|
+
loadState: values.LoadState || "unknown",
|
|
344
|
+
activeState: values.ActiveState || "unknown",
|
|
345
|
+
subState: values.SubState || "unknown",
|
|
346
|
+
execMainPid: Number.isFinite(pidValue) && pidValue > 0 ? pidValue : null,
|
|
347
|
+
unitFileState: values.UnitFileState || "unknown",
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function waitForFreshPairingSession({
|
|
352
|
+
env = process.env,
|
|
353
|
+
fsImpl = fs,
|
|
354
|
+
startedAt,
|
|
355
|
+
timeoutMs,
|
|
356
|
+
intervalMs,
|
|
357
|
+
} = {}) {
|
|
358
|
+
const deadline = Date.now() + timeoutMs;
|
|
359
|
+
while (Date.now() < deadline) {
|
|
360
|
+
const pairingSession = readPairingSession({ env, fsImpl });
|
|
361
|
+
const createdAt = Date.parse(pairingSession?.createdAt || "");
|
|
362
|
+
if (pairingSession?.pairingPayload && Number.isFinite(createdAt) && createdAt >= startedAt) {
|
|
363
|
+
return pairingSession;
|
|
364
|
+
}
|
|
365
|
+
await sleep(intervalMs);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Timed out waiting for the Linux bridge service to publish a pairing QR. `
|
|
370
|
+
+ `Check ${resolveBridgeStdoutLogPath({ env })} and ${resolveBridgeStderrLogPath({ env })}.`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function isMissingSystemdUnitError(error) {
|
|
375
|
+
const combined = [
|
|
376
|
+
error?.message,
|
|
377
|
+
error?.stderr?.toString?.("utf8"),
|
|
378
|
+
error?.stdout?.toString?.("utf8"),
|
|
379
|
+
].filter(Boolean).join("\n").toLowerCase();
|
|
380
|
+
return combined.includes("unit com.remodex.bridge.service could not be found")
|
|
381
|
+
|| combined.includes("unit com.remodex.bridge.service not found")
|
|
382
|
+
|| combined.includes("not loaded");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function assertLinuxPlatform(platform = process.platform) {
|
|
386
|
+
if (platform !== "linux") {
|
|
387
|
+
throw new Error("Linux bridge service management is only available on Linux.");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function assertRelayConfigured(config) {
|
|
392
|
+
if (typeof config?.relayUrl === "string" && config.relayUrl.trim()) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
throw new Error("No relay URL configured. Run ./run-local-remodex.sh or set REMODEX_RELAY before enabling the Linux bridge service.");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function quoteSystemdValue(value) {
|
|
399
|
+
return `"${String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function quoteSystemdPath(value) {
|
|
403
|
+
return String(value)
|
|
404
|
+
.replaceAll("\\", "\\\\")
|
|
405
|
+
.replaceAll(" ", "\\x20")
|
|
406
|
+
.replaceAll(":", "\\:");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function quoteSystemdEnv(name, value) {
|
|
410
|
+
return `"${String(name)}=${String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function quoteSystemdExec(...parts) {
|
|
414
|
+
return parts.map(quoteSystemdValue).join(" ");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function normalizeNonEmptyString(value) {
|
|
418
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function sleep(ms) {
|
|
422
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = {
|
|
426
|
+
buildUserServiceUnit,
|
|
427
|
+
getLinuxBridgeServiceStatus,
|
|
428
|
+
printLinuxBridgePairingQr,
|
|
429
|
+
printLinuxBridgeServiceStatus,
|
|
430
|
+
resetLinuxBridgePairing,
|
|
431
|
+
resolveUserServiceUnitPath,
|
|
432
|
+
runLinuxBridgeService,
|
|
433
|
+
startLinuxBridgeService,
|
|
434
|
+
stopLinuxBridgeService,
|
|
435
|
+
};
|
|
@@ -33,8 +33,8 @@ const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
|
|
|
33
33
|
const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
|
|
34
34
|
|
|
35
35
|
// Runs the bridge inside launchd while keeping QR rendering in the foreground CLI command.
|
|
36
|
-
function runMacOSBridgeService({ env = process.env } = {}) {
|
|
37
|
-
assertDarwinPlatform();
|
|
36
|
+
function runMacOSBridgeService({ env = process.env, platform = process.platform } = {}) {
|
|
37
|
+
assertDarwinPlatform(platform);
|
|
38
38
|
const config = readDaemonConfig({ env });
|
|
39
39
|
if (!config?.relayUrl) {
|
|
40
40
|
const message = "No relay URL configured for the macOS bridge service.";
|
package/src/voice-handler.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
// Exports: createVoiceHandler
|
|
5
5
|
// Depends on: global fetch/FormData/Blob, local codex app-server auth via sendCodexRequest
|
|
6
6
|
|
|
7
|
+
const fs = require("fs/promises");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
7
11
|
const CHATGPT_TRANSCRIPTIONS_URL = "https://chatgpt.com/backend-api/transcribe";
|
|
8
12
|
const MAX_AUDIO_BYTES = 10 * 1024 * 1024;
|
|
9
13
|
const MAX_DURATION_MS = 60_000;
|
|
@@ -13,6 +17,7 @@ function createVoiceHandler({
|
|
|
13
17
|
fetchImpl = globalThis.fetch,
|
|
14
18
|
FormDataImpl = globalThis.FormData,
|
|
15
19
|
BlobImpl = globalThis.Blob,
|
|
20
|
+
readLocalAuthToken = readLocalChatGPTAuthTokenFromDisk,
|
|
16
21
|
logPrefix = "[remodex]",
|
|
17
22
|
} = {}) {
|
|
18
23
|
function handleVoiceRequest(rawMessage, sendResponse) {
|
|
@@ -36,6 +41,7 @@ function createVoiceHandler({
|
|
|
36
41
|
fetchImpl,
|
|
37
42
|
FormDataImpl,
|
|
38
43
|
BlobImpl,
|
|
44
|
+
readLocalAuthToken,
|
|
39
45
|
})
|
|
40
46
|
.then((result) => {
|
|
41
47
|
sendResponse(JSON.stringify({ id, result }));
|
|
@@ -67,7 +73,7 @@ function createVoiceHandler({
|
|
|
67
73
|
// Validates iPhone-owned audio input and proxies it to the official transcription endpoint.
|
|
68
74
|
async function transcribeVoice(
|
|
69
75
|
params,
|
|
70
|
-
{ sendCodexRequest, fetchImpl, FormDataImpl, BlobImpl }
|
|
76
|
+
{ sendCodexRequest, fetchImpl, FormDataImpl, BlobImpl, readLocalAuthToken }
|
|
71
77
|
) {
|
|
72
78
|
if (typeof sendCodexRequest !== "function") {
|
|
73
79
|
throw voiceError("bridge_not_ready", "Voice transcription is not available right now.");
|
|
@@ -99,7 +105,7 @@ async function transcribeVoice(
|
|
|
99
105
|
throw voiceError("audio_too_large", "Voice messages are limited to 10 MB.");
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
const authContext = await loadAuthContext(sendCodexRequest);
|
|
108
|
+
const authContext = await loadAuthContext(sendCodexRequest, { readLocalAuthToken });
|
|
103
109
|
return requestTranscription({
|
|
104
110
|
authContext,
|
|
105
111
|
audioBuffer,
|
|
@@ -108,6 +114,7 @@ async function transcribeVoice(
|
|
|
108
114
|
FormDataImpl,
|
|
109
115
|
BlobImpl,
|
|
110
116
|
sendCodexRequest,
|
|
117
|
+
readLocalAuthToken,
|
|
111
118
|
});
|
|
112
119
|
}
|
|
113
120
|
|
|
@@ -119,6 +126,7 @@ async function requestTranscription({
|
|
|
119
126
|
FormDataImpl,
|
|
120
127
|
BlobImpl,
|
|
121
128
|
sendCodexRequest,
|
|
129
|
+
readLocalAuthToken,
|
|
122
130
|
}) {
|
|
123
131
|
const makeAttempt = async (activeAuthContext) => {
|
|
124
132
|
const formData = new FormDataImpl();
|
|
@@ -137,7 +145,10 @@ async function requestTranscription({
|
|
|
137
145
|
|
|
138
146
|
let response = await makeAttempt(authContext);
|
|
139
147
|
if (response.status === 401) {
|
|
140
|
-
const refreshedAuthContext = await loadAuthContext(sendCodexRequest
|
|
148
|
+
const refreshedAuthContext = await loadAuthContext(sendCodexRequest, {
|
|
149
|
+
forceRefresh: true,
|
|
150
|
+
readLocalAuthToken,
|
|
151
|
+
});
|
|
141
152
|
response = await makeAttempt(refreshedAuthContext);
|
|
142
153
|
}
|
|
143
154
|
|
|
@@ -170,8 +181,17 @@ async function requestTranscription({
|
|
|
170
181
|
}
|
|
171
182
|
|
|
172
183
|
// Reads the current bridge-owned auth state from the local codex app-server and refreshes if needed.
|
|
173
|
-
async function loadAuthContext(
|
|
174
|
-
|
|
184
|
+
async function loadAuthContext(
|
|
185
|
+
sendCodexRequest,
|
|
186
|
+
{
|
|
187
|
+
forceRefresh = false,
|
|
188
|
+
readLocalAuthToken = readLocalChatGPTAuthTokenFromDisk,
|
|
189
|
+
} = {}
|
|
190
|
+
) {
|
|
191
|
+
const { authMethod, token, isChatGPT } = await resolveCurrentOrRefreshedAuthStatus(sendCodexRequest, {
|
|
192
|
+
forceRefresh,
|
|
193
|
+
readLocalAuthToken,
|
|
194
|
+
});
|
|
175
195
|
|
|
176
196
|
if (!token) {
|
|
177
197
|
throw voiceError("not_authenticated", "Sign in with ChatGPT before using voice transcription.");
|
|
@@ -277,8 +297,15 @@ function voiceError(errorCode, userMessage) {
|
|
|
277
297
|
|
|
278
298
|
// Returns an ephemeral ChatGPT token so the phone can call the transcription API directly.
|
|
279
299
|
// Uses its own token resolution instead of loadAuthContext so errors are specific and actionable.
|
|
280
|
-
async function resolveVoiceAuth(
|
|
300
|
+
async function resolveVoiceAuth(
|
|
301
|
+
sendCodexRequest,
|
|
302
|
+
params = null,
|
|
303
|
+
{ readLocalAuthToken = readLocalChatGPTAuthTokenFromDisk } = {}
|
|
304
|
+
) {
|
|
305
|
+
const forceRefresh = Boolean(params?.forceRefresh);
|
|
281
306
|
const { authMethod, token, isChatGPT, requiresOpenaiAuth } = await resolveCurrentOrRefreshedAuthStatus(sendCodexRequest, {
|
|
307
|
+
forceRefresh,
|
|
308
|
+
readLocalAuthToken,
|
|
282
309
|
rpcErrorCode: "auth_unavailable",
|
|
283
310
|
rpcErrorMessage: "Could not read ChatGPT session from the Mac runtime. Is the bridge running?",
|
|
284
311
|
});
|
|
@@ -301,25 +328,82 @@ async function resolveVoiceAuth(sendCodexRequest) {
|
|
|
301
328
|
async function resolveCurrentOrRefreshedAuthStatus(
|
|
302
329
|
sendCodexRequest,
|
|
303
330
|
{
|
|
331
|
+
forceRefresh = false,
|
|
332
|
+
readLocalAuthToken = readLocalChatGPTAuthTokenFromDisk,
|
|
304
333
|
rpcErrorCode = "not_authenticated",
|
|
305
334
|
rpcErrorMessage = "Sign in with ChatGPT before using voice transcription.",
|
|
306
335
|
} = {}
|
|
307
336
|
) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
337
|
+
if (forceRefresh) {
|
|
338
|
+
const refreshedStatus = await readAuthStatus(sendCodexRequest, {
|
|
339
|
+
refreshToken: true,
|
|
340
|
+
rpcErrorCode,
|
|
341
|
+
rpcErrorMessage,
|
|
342
|
+
});
|
|
343
|
+
return withLocalAuthFallback(refreshedStatus, readLocalAuthToken);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const currentStatus = await withLocalAuthFallback(
|
|
347
|
+
await readAuthStatus(sendCodexRequest, {
|
|
348
|
+
refreshToken: false,
|
|
349
|
+
rpcErrorCode,
|
|
350
|
+
rpcErrorMessage,
|
|
351
|
+
}),
|
|
352
|
+
readLocalAuthToken
|
|
353
|
+
);
|
|
313
354
|
|
|
314
355
|
if (currentStatus.token) {
|
|
315
356
|
return currentStatus;
|
|
316
357
|
}
|
|
317
358
|
|
|
318
|
-
|
|
359
|
+
const refreshedStatus = await readAuthStatus(sendCodexRequest, {
|
|
319
360
|
refreshToken: true,
|
|
320
361
|
rpcErrorCode,
|
|
321
362
|
rpcErrorMessage,
|
|
322
363
|
});
|
|
364
|
+
|
|
365
|
+
return withLocalAuthFallback(refreshedStatus, readLocalAuthToken);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function withLocalAuthFallback(status, readLocalAuthToken) {
|
|
369
|
+
if (status?.token || typeof readLocalAuthToken !== "function") {
|
|
370
|
+
return status;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const localAuthStatus = await readLocalAuthToken().catch(() => null);
|
|
374
|
+
if (!localAuthStatus?.token || !localAuthStatus?.isChatGPT) {
|
|
375
|
+
return status;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
authMethod: localAuthStatus.authMethod || status.authMethod || "chatgpt",
|
|
380
|
+
token: localAuthStatus.token,
|
|
381
|
+
isChatGPT: true,
|
|
382
|
+
requiresOpenaiAuth: localAuthStatus.requiresOpenaiAuth ?? status.requiresOpenaiAuth,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function readLocalChatGPTAuthTokenFromDisk({
|
|
387
|
+
readFileImpl = fs.readFile,
|
|
388
|
+
codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"),
|
|
389
|
+
} = {}) {
|
|
390
|
+
const authFile = path.join(codexHome, "auth.json");
|
|
391
|
+
const contents = await readFileImpl(authFile, "utf8");
|
|
392
|
+
const auth = JSON.parse(contents);
|
|
393
|
+
const authMethod = readString(auth?.auth_mode);
|
|
394
|
+
const tokenContainer = auth?.tokens && typeof auth.tokens === "object" ? auth.tokens : auth;
|
|
395
|
+
const token = readString(tokenContainer?.access_token);
|
|
396
|
+
const isChatGPT = token != null
|
|
397
|
+
&& authMethod !== "apikey"
|
|
398
|
+
&& authMethod !== "api_key"
|
|
399
|
+
&& authMethod !== "apiKey";
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
authMethod: authMethod || (isChatGPT ? "chatgpt" : null),
|
|
403
|
+
token,
|
|
404
|
+
isChatGPT,
|
|
405
|
+
requiresOpenaiAuth: isChatGPT,
|
|
406
|
+
};
|
|
323
407
|
}
|
|
324
408
|
|
|
325
409
|
async function readAuthStatus(
|
|
@@ -348,5 +432,6 @@ async function readAuthStatus(
|
|
|
348
432
|
|
|
349
433
|
module.exports = {
|
|
350
434
|
createVoiceHandler,
|
|
435
|
+
readLocalChatGPTAuthTokenFromDisk,
|
|
351
436
|
resolveVoiceAuth,
|
|
352
437
|
};
|