@deepdream314/remodex 1.3.8 → 1.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepdream314/remodex",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "description": "Local bridge between Codex and the Remodex mobile app. Run `remodex up` to start.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -24,8 +24,11 @@ function composeAccountStatus({
24
24
  ]) || null;
25
25
  const tokenReady = Boolean(authToken);
26
26
  const requiresOpenaiAuth = Boolean(accountRead?.requiresOpenaiAuth || authStatus?.requiresOpenaiAuth);
27
- const hasPriorLoginContext = hasAccountLogin || Boolean(authMethod);
28
- const needsReauth = !loginInFlight && requiresOpenaiAuth && hasPriorLoginContext;
27
+ const needsReauth = !loginInFlight
28
+ && requiresOpenaiAuth
29
+ && !tokenReady
30
+ && !hasAccountLogin
31
+ && Boolean(authMethod);
29
32
  const isAuthenticated = !needsReauth && (tokenReady || hasAccountLogin);
30
33
  const status = isAuthenticated
31
34
  ? "authenticated"
@@ -77,6 +80,44 @@ function redactAuthStatus(authStatus = null, extras = {}) {
77
80
 
78
81
  // ─── Settled snapshot helpers ───────────────────────────────
79
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
+
80
121
  // Collapses settled bridge RPC results into one safe snapshot, even if one side fails.
81
122
  // Input: Promise.allSettled-style results → Output: sanitized account status object
82
123
  // Throws if both the account read and auth status fail, so the bridge can surface a real error.
@@ -138,6 +179,7 @@ function parseBoolean(value) {
138
179
  }
139
180
 
140
181
  module.exports = {
182
+ applyLocalAuthFallbackToSettledAuthStatus,
141
183
  composeAccountStatus,
142
184
  composeSanitizedAuthStatusFromSettledResults,
143
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,15 +542,16 @@ 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
  }),
544
549
  sendCodexRequest("getAuthStatus", {
545
550
  includeToken: true,
546
- refreshToken: true,
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
@@ -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,16 +181,18 @@ 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(sendCodexRequest) {
174
- const authStatus = await sendCodexRequest("getAuthStatus", {
175
- includeToken: true,
176
- refreshToken: true,
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,
177
194
  });
178
195
 
179
- const authMethod = readString(authStatus?.authMethod);
180
- const token = readString(authStatus?.authToken);
181
- const isChatGPT = authMethod === "chatgpt" || authMethod === "chatgptAuthTokens";
182
-
183
196
  if (!token) {
184
197
  throw voiceError("not_authenticated", "Sign in with ChatGPT before using voice transcription.");
185
198
  }
@@ -284,21 +297,18 @@ function voiceError(errorCode, userMessage) {
284
297
 
285
298
  // Returns an ephemeral ChatGPT token so the phone can call the transcription API directly.
286
299
  // Uses its own token resolution instead of loadAuthContext so errors are specific and actionable.
287
- async function resolveVoiceAuth(sendCodexRequest) {
288
- let authStatus;
289
- try {
290
- authStatus = await sendCodexRequest("getAuthStatus", {
291
- includeToken: true,
292
- refreshToken: true,
293
- });
294
- } catch (err) {
295
- console.error(`[remodex] voice/resolveAuth: getAuthStatus RPC failed: ${err.message}`);
296
- throw voiceError("auth_unavailable", "Could not read ChatGPT session from the Mac runtime. Is the bridge running?");
297
- }
298
-
299
- const authMethod = readString(authStatus?.authMethod);
300
- const token = readString(authStatus?.authToken);
301
- const isChatGPT = authMethod === "chatgpt" || authMethod === "chatgptAuthTokens";
300
+ async function resolveVoiceAuth(
301
+ sendCodexRequest,
302
+ params = null,
303
+ { readLocalAuthToken = readLocalChatGPTAuthTokenFromDisk } = {}
304
+ ) {
305
+ const forceRefresh = Boolean(params?.forceRefresh);
306
+ const { authMethod, token, isChatGPT, requiresOpenaiAuth } = await resolveCurrentOrRefreshedAuthStatus(sendCodexRequest, {
307
+ forceRefresh,
308
+ readLocalAuthToken,
309
+ rpcErrorCode: "auth_unavailable",
310
+ rpcErrorMessage: "Could not read ChatGPT session from the Mac runtime. Is the bridge running?",
311
+ });
302
312
 
303
313
  // Check for a usable ChatGPT token first. The runtime may set requiresOpenaiAuth
304
314
  // even when a valid ChatGPT session is present (the flag is about the runtime's
@@ -308,14 +318,120 @@ async function resolveVoiceAuth(sendCodexRequest) {
308
318
  }
309
319
 
310
320
  if (!token) {
311
- console.error(`[remodex] voice/resolveAuth: no token. authMethod=${authMethod || "none"} requiresOpenaiAuth=${authStatus?.requiresOpenaiAuth}`);
321
+ console.error(`[remodex] voice/resolveAuth: no token. authMethod=${authMethod || "none"} requiresOpenaiAuth=${requiresOpenaiAuth}`);
312
322
  throw voiceError("token_missing", "No ChatGPT session token available. Sign in to ChatGPT on the Mac.");
313
323
  }
314
324
 
315
325
  throw voiceError("not_chatgpt", "Voice transcription requires a ChatGPT account.");
316
326
  }
317
327
 
328
+ async function resolveCurrentOrRefreshedAuthStatus(
329
+ sendCodexRequest,
330
+ {
331
+ forceRefresh = false,
332
+ readLocalAuthToken = readLocalChatGPTAuthTokenFromDisk,
333
+ rpcErrorCode = "not_authenticated",
334
+ rpcErrorMessage = "Sign in with ChatGPT before using voice transcription.",
335
+ } = {}
336
+ ) {
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
+ );
354
+
355
+ if (currentStatus.token) {
356
+ return currentStatus;
357
+ }
358
+
359
+ const refreshedStatus = await readAuthStatus(sendCodexRequest, {
360
+ refreshToken: true,
361
+ rpcErrorCode,
362
+ rpcErrorMessage,
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
+ };
407
+ }
408
+
409
+ async function readAuthStatus(
410
+ sendCodexRequest,
411
+ { refreshToken, rpcErrorCode, rpcErrorMessage }
412
+ ) {
413
+ let authStatus;
414
+ try {
415
+ authStatus = await sendCodexRequest("getAuthStatus", {
416
+ includeToken: true,
417
+ refreshToken,
418
+ });
419
+ } catch (err) {
420
+ throw voiceError(rpcErrorCode, rpcErrorMessage);
421
+ }
422
+
423
+ const authMethod = readString(authStatus?.authMethod);
424
+ const token = readString(authStatus?.authToken);
425
+ return {
426
+ authMethod,
427
+ token,
428
+ isChatGPT: authMethod === "chatgpt" || authMethod === "chatgptAuthTokens",
429
+ requiresOpenaiAuth: Boolean(authStatus?.requiresOpenaiAuth),
430
+ };
431
+ }
432
+
318
433
  module.exports = {
319
434
  createVoiceHandler,
435
+ readLocalChatGPTAuthTokenFromDisk,
320
436
  resolveVoiceAuth,
321
437
  };