@makerbi/remodex 1.3.10 → 1.4.1

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 CHANGED
@@ -58,6 +58,7 @@ async function main({
58
58
 
59
59
  if (command === "up") {
60
60
  if (platform === "darwin") {
61
+ consoleImpl.log("[remodex] Starting bridge and pairing QR...");
61
62
  const result = await deps.startMacOSBridgeService({
62
63
  waitForPairing: true,
63
64
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makerbi/remodex",
3
- "version": "1.3.10",
3
+ "version": "1.4.1",
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,7 +24,7 @@
24
24
  "cli"
25
25
  ],
26
26
  "author": "Emanuele Di Pietro",
27
- "license": "ISC",
27
+ "license": "Apache-2.0",
28
28
  "type": "commonjs",
29
29
  "dependencies": {
30
30
  "qrcode-terminal": "^0.12.0",
package/src/bridge.js CHANGED
@@ -24,6 +24,7 @@ const { handleGitRequest } = require("./git-handler");
24
24
  const { handleThreadContextRequest } = require("./thread-context-handler");
25
25
  const { handleWorkspaceRequest } = require("./workspace-handler");
26
26
  const { handleProjectRequest } = require("./project-handler");
27
+ const { handlePetRequest } = require("./pet-handler");
27
28
  const { createNotificationsHandler } = require("./notifications-handler");
28
29
  const { createVoiceHandler, resolveVoiceAuth } = require("./voice-handler");
29
30
  const {
@@ -40,6 +41,10 @@ const {
40
41
  } = require("./secure-device-state");
41
42
  const { createBridgeSecureTransport } = require("./secure-transport");
42
43
  const { createRolloutLiveMirrorController } = require("./rollout-live-mirror");
44
+ const {
45
+ createDesktopIpcActionFollower,
46
+ seedConversationStateFromThreadRead,
47
+ } = require("./desktop-ipc-action-follower");
43
48
  const { version: bridgePackageVersion = "" } = require("../package.json");
44
49
  const {
45
50
  MINIMUM_SUPPORTED_IOS_APP_VERSION,
@@ -198,6 +203,13 @@ function startBridge({
198
203
  sendApplicationResponse,
199
204
  })
200
205
  : null;
206
+ const desktopIpcActionFollower = !config.codexEndpoint
207
+ ? createDesktopIpcActionFollower({
208
+ sendApplicationResponse,
209
+ readConversationState: readDesktopConversationState,
210
+ socketPath: config.desktopIpcSocketPath || undefined,
211
+ })
212
+ : null;
201
213
  let contextUsageWatcher = null;
202
214
  let watchedContextUsageKey = null;
203
215
 
@@ -443,6 +455,7 @@ function startBridge({
443
455
  }
444
456
  stopContextUsageWatcher();
445
457
  rolloutLiveMirror?.stopAll();
458
+ desktopIpcActionFollower?.stopAll();
446
459
  desktopRefresher.handleTransportReset();
447
460
  scheduleRelayReconnect(code, closeReason);
448
461
  });
@@ -498,6 +511,7 @@ function startBridge({
498
511
  clearReconnectTimer();
499
512
  stopContextUsageWatcher();
500
513
  rolloutLiveMirror?.stopAll();
514
+ desktopIpcActionFollower?.stopAll();
501
515
  desktopRefresher.handleTransportReset();
502
516
  failBridgeManagedCodexRequests(new Error("Codex transport closed before the bridge request completed."));
503
517
  forwardedRequestMethodsById.clear();
@@ -541,6 +555,9 @@ function startBridge({
541
555
  if (handleProjectRequest(rawMessage, sendApplicationResponse)) {
542
556
  return;
543
557
  }
558
+ if (handlePetRequest(rawMessage, sendApplicationResponse)) {
559
+ return;
560
+ }
544
561
  if (notificationsHandler.handleNotificationsRequest(rawMessage, sendApplicationResponse)) {
545
562
  return;
546
563
  }
@@ -560,6 +577,9 @@ function startBridge({
560
577
  }
561
578
  desktopRefresher.handleInbound(rawMessage);
562
579
  rolloutLiveMirror?.observeInbound(rawMessage);
580
+ if (desktopIpcActionFollower?.observeInbound(rawMessage)) {
581
+ return;
582
+ }
563
583
  rememberForwardedRequestMethod(rawMessage);
564
584
  rememberThreadFromMessage("phone", rawMessage);
565
585
  codex.send(rawMessage);
@@ -592,6 +612,15 @@ function startBridge({
592
612
  }));
593
613
  }
594
614
 
615
+ // Seeds the desktop IPC follower when it receives patches before a full snapshot.
616
+ async function readDesktopConversationState(threadId) {
617
+ const result = await sendCodexRequest("thread/read", {
618
+ threadId,
619
+ includeTurns: true,
620
+ });
621
+ return seedConversationStateFromThreadRead(result);
622
+ }
623
+
595
624
  // ─── Bridge-owned auth snapshot ─────────────────────────────
596
625
 
597
626
  // Handles the bridge-owned auth status wrappers without exposing tokens to the phone.
@@ -587,6 +587,7 @@ function readBridgeConfig({
587
587
  ? (persistedKeepMacAwakeEnabled == null ? false : persistedKeepMacAwakeEnabled)
588
588
  : explicitKeepMacAwakeEnabled,
589
589
  codexEndpoint,
590
+ desktopIpcSocketPath: readFirstDefinedEnv(["REMODEX_DESKTOP_IPC_SOCKET"], "", env),
590
591
  refreshCommand,
591
592
  codexBundleId: readFirstDefinedEnv(["REMODEX_CODEX_BUNDLE_ID"], DEFAULT_BUNDLE_ID, env),
592
593
  codexAppPath: DEFAULT_APP_PATH,
@@ -20,6 +20,8 @@ const DEFAULT_APP_BOOT_WAIT_MS = 1_200;
20
20
  const DEFAULT_THREAD_MATERIALIZE_WAIT_MS = 4_000;
21
21
  const DEFAULT_THREAD_MATERIALIZE_POLL_MS = 250;
22
22
  const DEFAULT_WAKE_DISPLAY_DURATION_SECONDS = 30;
23
+ const WINDOWS_BOUNCE_URL = "codex://settings";
24
+ const DESKTOP_THREAD_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
23
25
 
24
26
  function handleDesktopRequest(rawMessage, sendResponse, options = {}) {
25
27
  let parsed;
@@ -71,16 +73,39 @@ async function handleDesktopMethod(method, params, options = {}) {
71
73
  const threadMaterializeWaitMs = options.threadMaterializeWaitMs ?? DEFAULT_THREAD_MATERIALIZE_WAIT_MS;
72
74
  const threadMaterializePollMs = options.threadMaterializePollMs ?? DEFAULT_THREAD_MATERIALIZE_POLL_MS;
73
75
 
74
- if (platform !== "darwin") {
75
- throw desktopError(
76
- "unsupported_platform",
77
- "Mac handoff is only available when the bridge is running on macOS."
78
- );
79
- }
80
-
81
76
  switch (method) {
77
+ case "desktop/continueOnDesktop":
78
+ if (platform !== "darwin" && platform !== "win32") {
79
+ throw desktopError(
80
+ "unsupported_platform",
81
+ "Desktop handoff is only available when the bridge is running on macOS or Windows."
82
+ );
83
+ }
84
+
85
+ return continueOnDesktop(params, {
86
+ platform,
87
+ bundleId,
88
+ appPath,
89
+ executor,
90
+ env,
91
+ fsModule,
92
+ isAppRunning,
93
+ sleepFn,
94
+ appBootWaitMs,
95
+ relaunchWaitMs,
96
+ threadMaterializeWaitMs,
97
+ threadMaterializePollMs,
98
+ });
82
99
  case "desktop/continueOnMac":
83
- return continueOnMac(params, {
100
+ if (platform !== "darwin") {
101
+ throw desktopError(
102
+ "unsupported_platform",
103
+ "Mac handoff is only available when the bridge is running on macOS."
104
+ );
105
+ }
106
+
107
+ return continueOnDesktop(params, {
108
+ platform,
84
109
  bundleId,
85
110
  appPath,
86
111
  executor,
@@ -106,10 +131,11 @@ async function handleDesktopMethod(method, params, options = {}) {
106
131
  }
107
132
  }
108
133
 
109
- // Waits for fresh phone-authored chats to materialize locally before deep-linking them on Mac.
110
- async function continueOnMac(
134
+ // Waits for fresh phone-authored chats to materialize locally before deep-linking them on desktop.
135
+ async function continueOnDesktop(
111
136
  params,
112
137
  {
138
+ platform,
113
139
  bundleId,
114
140
  appPath,
115
141
  executor,
@@ -125,11 +151,53 @@ async function continueOnMac(
125
151
  ) {
126
152
  const threadId = resolveThreadId(params);
127
153
  if (!threadId) {
128
- throw desktopError("missing_thread_id", "A thread id is required to continue on Mac.");
154
+ throw desktopError("missing_thread_id", "A thread id is required to continue on desktop.");
155
+ }
156
+ if (!isValidDesktopThreadId(threadId)) {
157
+ throw desktopError("invalid_thread_id", "The requested desktop thread id is not valid.");
129
158
  }
130
159
 
131
160
  const targetUrl = `codex://threads/${threadId}`;
132
161
  const desktopKnown = isThreadLikelyKnownOnDesktop(threadId, { env, fsModule });
162
+
163
+ if (platform === "win32") {
164
+ try {
165
+ if (desktopKnown) {
166
+ await refreshWindowsCodex(targetUrl, {
167
+ executor,
168
+ env,
169
+ sleepFn,
170
+ settleMs: relaunchWaitMs,
171
+ });
172
+ } else {
173
+ await openWindowsDeepLink(WINDOWS_BOUNCE_URL, { executor, env });
174
+ await sleepFn(appBootWaitMs);
175
+ await waitForThreadMaterialization(threadId, {
176
+ env,
177
+ fsModule,
178
+ sleepFn,
179
+ timeoutMs: threadMaterializeWaitMs,
180
+ pollMs: threadMaterializePollMs,
181
+ });
182
+ await openWindowsDeepLink(targetUrl, { executor, env });
183
+ }
184
+ } catch (error) {
185
+ throw desktopError(
186
+ "handoff_failed",
187
+ "Could not open Codex on this PC.",
188
+ error
189
+ );
190
+ }
191
+
192
+ return {
193
+ success: true,
194
+ relaunched: false,
195
+ targetUrl,
196
+ threadId,
197
+ desktopKnown,
198
+ };
199
+ }
200
+
133
201
  const appRunning = typeof isAppRunning === "function"
134
202
  ? await isAppRunning(appPath)
135
203
  : await detectRunningCodexApp(appPath, executor);
@@ -311,6 +379,11 @@ function resolveThreadId(params) {
311
379
  return "";
312
380
  }
313
381
 
382
+ // Keeps desktop deep links to a single safe route segment before handing them to OS launchers.
383
+ function isValidDesktopThreadId(threadId) {
384
+ return typeof threadId === "string" && DESKTOP_THREAD_ID_PATTERN.test(threadId);
385
+ }
386
+
314
387
  function desktopError(errorCode, userMessage, cause = null) {
315
388
  const error = new Error(userMessage);
316
389
  error.errorCode = errorCode;
@@ -372,6 +445,22 @@ async function openCodexApp({ bundleId, appPath, executor }) {
372
445
  }
373
446
  }
374
447
 
448
+ async function openWindowsDeepLink(targetUrl, { executor, env }) {
449
+ await executor(env?.SystemRoot ? path.join(env.SystemRoot, "System32", "rundll32.exe") : "rundll32.exe", [
450
+ "url.dll,FileProtocolHandler",
451
+ targetUrl,
452
+ ], {
453
+ timeout: HANDOFF_TIMEOUT_MS,
454
+ windowsHide: true,
455
+ });
456
+ }
457
+
458
+ async function refreshWindowsCodex(targetUrl, { executor, env, sleepFn, settleMs }) {
459
+ await openWindowsDeepLink(WINDOWS_BOUNCE_URL, { executor, env });
460
+ await sleepFn(settleMs);
461
+ await openWindowsDeepLink(targetUrl, { executor, env });
462
+ }
463
+
375
464
  // Gives the desktop a short window to materialize the requested thread before the final deep link.
376
465
  async function openWhenThreadReady(
377
466
  threadId,