@makerbi/remodex 1.5.3 → 1.5.8

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.
@@ -13,6 +13,7 @@ function createCodexTransport({
13
13
  endpoint = "",
14
14
  env = process.env,
15
15
  appPath = "",
16
+ platform = process.platform,
16
17
  spawnImpl = spawn,
17
18
  WebSocketImpl = WebSocket,
18
19
  } = {}) {
@@ -20,11 +21,11 @@ function createCodexTransport({
20
21
  return createWebSocketTransport({ endpoint, WebSocketImpl });
21
22
  }
22
23
 
23
- return createSpawnTransport({ env, appPath, spawnImpl });
24
+ return createSpawnTransport({ env, appPath, platform, spawnImpl });
24
25
  }
25
26
 
26
- function createSpawnTransport({ env, appPath, spawnImpl = spawn }) {
27
- const launchPlans = createCodexLaunchPlans({ env, appPath });
27
+ function createSpawnTransport({ env, appPath, platform, spawnImpl = spawn }) {
28
+ const launchPlans = createCodexLaunchPlans({ env, appPath, platform });
28
29
  let launchIndex = -1;
29
30
  let activeLaunch = null;
30
31
  let codex = null;
@@ -154,13 +155,12 @@ function createSpawnTransport({ env, appPath, spawnImpl = spawn }) {
154
155
  return;
155
156
  }
156
157
  stdoutBuffer += chunk.toString("utf8");
157
- const lines = stdoutBuffer.split("\n");
158
- stdoutBuffer = lines.pop() || "";
159
-
160
- for (const line of lines) {
161
- const trimmedLine = line.trim();
162
- if (trimmedLine) {
163
- listeners.emitMessage(trimmedLine);
158
+ let newlineIndex;
159
+ while ((newlineIndex = stdoutBuffer.indexOf("\n")) !== -1) {
160
+ const line = stdoutBuffer.substring(0, newlineIndex).trim();
161
+ stdoutBuffer = stdoutBuffer.substring(newlineIndex + 1);
162
+ if (line) {
163
+ listeners.emitMessage(line);
164
164
  }
165
165
  }
166
166
  });
@@ -254,7 +254,32 @@ function shutdownCodexProcess(codex) {
254
254
  function createCodexCloseError({ code, signal, stderrBuffer, launchDescription }) {
255
255
  const details = stderrBuffer.trim();
256
256
  const reason = details || `Process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}.`;
257
- return new Error(`Codex launcher ${launchDescription} failed: ${reason}`);
257
+ return new Error(formatCodexLaunchFailure({
258
+ launchDescription,
259
+ reason,
260
+ }));
261
+ }
262
+
263
+ // Turns common Codex auth/config failures into recovery guidance without handling secrets in Remodex.
264
+ function formatCodexLaunchFailure({ launchDescription, reason }) {
265
+ const message = `Codex launcher ${launchDescription} failed: ${reason}`;
266
+ const missingEnvVar = extractMissingEnvironmentVariable(reason);
267
+ if (!missingEnvVar) {
268
+ return message;
269
+ }
270
+
271
+ const guidance = [
272
+ `Codex is asking for ${missingEnvVar}, which usually means your Codex config forces API-key auth or a custom provider env var.`,
273
+ "Remodex does not store or forward OpenAI API keys.",
274
+ "Recommended fix: run `codex login` on this Mac, then restart Remodex.",
275
+ "If you intentionally use API-key auth, run `printenv OPENAI_API_KEY | codex login --with-api-key` or make that env var available to the Remodex daemon yourself.",
276
+ ];
277
+ return `${message}\n${guidance.join("\n")}`;
278
+ }
279
+
280
+ function extractMissingEnvironmentVariable(reason) {
281
+ const match = String(reason || "").match(/Missing environment variable:\s*`?([A-Za-z_][A-Za-z0-9_]*)`?/i);
282
+ return match ? match[1] : "";
258
283
  }
259
284
 
260
285
  function appendOutputBuffer(buffer, chunk) {
@@ -350,4 +375,6 @@ function createListenerBag() {
350
375
  module.exports = {
351
376
  createCodexLaunchPlans,
352
377
  createCodexTransport,
378
+ extractMissingEnvironmentVariable,
379
+ formatCodexLaunchFailure,
353
380
  };
@@ -1,5 +1,5 @@
1
1
  // FILE: desktop-handler.js
2
- // Purpose: Handles explicit desktop handoff, display wake, and bridge preference RPCs for Codex.app.
2
+ // Purpose: Handles explicit desktop handoff, display wake, bridge update, and preference RPCs for Codex.app.
3
3
  // Layer: Bridge handler
4
4
  // Exports: handleDesktopRequest
5
5
  // Depends on: child_process, fs, os, path, ./rollout-watch
@@ -126,6 +126,8 @@ async function handleDesktopMethod(method, params, options = {}) {
126
126
  return readBridgePreferences(options);
127
127
  case "desktop/preferences/update":
128
128
  return updateBridgePreferences(params, options);
129
+ case "desktop/bridge/updateAndRestart":
130
+ return updateBridgePackageAndRestart(options);
129
131
  default:
130
132
  throw desktopError("unknown_method", `Unknown desktop method: ${method}`);
131
133
  }
@@ -360,6 +362,17 @@ async function updateBridgePreferences(params, options = {}) {
360
362
  });
361
363
  }
362
364
 
365
+ async function updateBridgePackageAndRestart(options = {}) {
366
+ if (typeof options.updateBridgePackageAndRestart !== "function") {
367
+ throw desktopError(
368
+ "unsupported_bridge_update",
369
+ "This bridge does not support iPhone-triggered bridge updates yet."
370
+ );
371
+ }
372
+
373
+ return options.updateBridgePackageAndRestart();
374
+ }
375
+
363
376
  function resolveThreadId(params) {
364
377
  if (!params || typeof params !== "object") {
365
378
  return "";
@@ -11,17 +11,20 @@ const path = require("path");
11
11
  const FRAME_HEADER_BYTES = 4;
12
12
  const MAX_FRAME_BYTES = 256 * 1024 * 1024;
13
13
  const REQUEST_TIMEOUT_MS = 10_000;
14
+ const DESKTOP_IPC_ACTION_SOURCE = "desktop-ipc-action-follower";
14
15
  const DESKTOP_RESUME_METHODS = new Set(["thread/read", "thread/resume"]);
15
16
  const ACTION_METHODS = new Set([
16
17
  "item/commandExecution/requestApproval",
17
18
  "item/fileChange/requestApproval",
18
19
  "item/fileRead/requestApproval",
20
+ "item/permissions/requestApproval",
19
21
  "item/tool/requestUserInput",
20
22
  ]);
21
23
  const REPLY_METHOD_BY_ACTION_METHOD = new Map([
22
24
  ["item/commandExecution/requestApproval", "thread-follower-command-approval-decision"],
23
25
  ["item/fileChange/requestApproval", "thread-follower-file-approval-decision"],
24
26
  ["item/fileRead/requestApproval", "thread-follower-file-approval-decision"],
27
+ ["item/permissions/requestApproval", "thread-follower-file-approval-decision"],
25
28
  ["item/tool/requestUserInput", "thread-follower-submit-user-input"],
26
29
  ]);
27
30
  const METHOD_VERSION_BY_NAME = new Map([
@@ -52,6 +55,7 @@ function createDesktopIpcActionFollower({
52
55
  onDisconnect,
53
56
  });
54
57
  const rawStatesByThreadId = new Map();
58
+ const assistantMessageTextsByThreadId = new Map();
55
59
  const pendingRoutesByRequestId = new Map();
56
60
  const activeThreadIds = new Set();
57
61
  const recoveringThreadIds = new Set();
@@ -82,6 +86,7 @@ function createDesktopIpcActionFollower({
82
86
 
83
87
  function stopAll() {
84
88
  rawStatesByThreadId.clear();
89
+ assistantMessageTextsByThreadId.clear();
85
90
  pendingRoutesByRequestId.clear();
86
91
  activeThreadIds.clear();
87
92
  recoveringThreadIds.clear();
@@ -130,11 +135,13 @@ function createDesktopIpcActionFollower({
130
135
  }
131
136
 
132
137
  rawStatesByThreadId.set(threadId, nextState);
138
+ syncProjectedAssistantDeltas(threadId, previousState, nextState);
133
139
  syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
134
140
  }
135
141
 
136
142
  function onDisconnect() {
137
143
  rawStatesByThreadId.clear();
144
+ assistantMessageTextsByThreadId.clear();
138
145
  pendingRoutesByRequestId.clear();
139
146
  recoveringThreadIds.clear();
140
147
  queuedChangesByThreadId.clear();
@@ -271,9 +278,34 @@ function createDesktopIpcActionFollower({
271
278
  }
272
279
 
273
280
  rawStatesByThreadId.set(threadId, nextState);
281
+ syncProjectedAssistantDeltas(threadId, baselineState, nextState);
274
282
  syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
275
283
  }
276
284
 
285
+ function syncProjectedAssistantDeltas(threadId, previousState, nextState) {
286
+ const previousTexts = assistantMessageTextsByThreadId.get(threadId);
287
+ if (!previousTexts && !previousState) {
288
+ assistantMessageTextsByThreadId.set(threadId, snapshotAssistantMessageTexts(nextState));
289
+ return;
290
+ }
291
+
292
+ const notifications = projectDesktopAssistantDeltaNotifications(
293
+ threadId,
294
+ previousState,
295
+ nextState,
296
+ previousTexts || snapshotAssistantMessageTexts(previousState)
297
+ );
298
+ if (notifications.length === 0) {
299
+ assistantMessageTextsByThreadId.set(threadId, snapshotAssistantMessageTexts(nextState));
300
+ return;
301
+ }
302
+
303
+ for (const notification of notifications) {
304
+ sendApplicationResponse(JSON.stringify(notification));
305
+ }
306
+ assistantMessageTextsByThreadId.set(threadId, snapshotAssistantMessageTexts(nextState));
307
+ }
308
+
277
309
  return {
278
310
  observeInbound,
279
311
  stopAll,
@@ -483,7 +515,7 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
483
515
  };
484
516
  }
485
517
 
486
- const decision = readString(responseMessage?.result?.decision);
518
+ const decision = desktopApprovalDecisionForResponse(route.method, responseMessage?.result);
487
519
  if (!APPROVAL_DECISIONS.has(decision)) {
488
520
  return null;
489
521
  }
@@ -498,6 +530,47 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
498
530
  };
499
531
  }
500
532
 
533
+ function desktopApprovalDecisionForResponse(method, result) {
534
+ const explicitDecision = readString(result?.decision);
535
+ if (explicitDecision) {
536
+ return explicitDecision;
537
+ }
538
+
539
+ if (method !== "item/permissions/requestApproval") {
540
+ return "";
541
+ }
542
+
543
+ // Permission approvals use a grant payload on app-server, while Desktop IPC
544
+ // currently exposes only decision-style follower replies.
545
+ return hasGrantedPermission(result?.permissions) ? "accept" : "decline";
546
+ }
547
+
548
+ function hasGrantedPermission(value) {
549
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
550
+ return false;
551
+ }
552
+
553
+ if (Object.keys(value).length === 0) {
554
+ return false;
555
+ }
556
+
557
+ return Object.values(value).some((entry) => {
558
+ if (entry == null) {
559
+ return false;
560
+ }
561
+ if (typeof entry === "boolean") {
562
+ return entry;
563
+ }
564
+ if (Array.isArray(entry)) {
565
+ return entry.length > 0;
566
+ }
567
+ if (typeof entry === "object") {
568
+ return Object.keys(entry).length > 0;
569
+ }
570
+ return true;
571
+ });
572
+ }
573
+
501
574
  function projectPendingDesktopActions(threadId, conversationState) {
502
575
  const requests = Array.isArray(conversationState?.requests) ? conversationState.requests : [];
503
576
  return requests
@@ -507,6 +580,93 @@ function projectPendingDesktopActions(threadId, conversationState) {
507
580
  .filter(Boolean);
508
581
  }
509
582
 
583
+ // Desktop IPC exposes full conversation snapshots/patches, not app-server assistant delta events.
584
+ // Mirror only suffix growth for assistant rows so phones can render the same live text progression.
585
+ function projectDesktopAssistantDeltaNotifications(
586
+ threadId,
587
+ previousState,
588
+ nextState,
589
+ previousTexts = snapshotAssistantMessageTexts(previousState)
590
+ ) {
591
+ const nextMessages = collectAssistantMessages(nextState);
592
+ const notifications = [];
593
+
594
+ for (const message of nextMessages) {
595
+ const previousText = previousTexts.get(message.key) || "";
596
+ if (!message.text || !message.text.startsWith(previousText) || message.text.length <= previousText.length) {
597
+ continue;
598
+ }
599
+
600
+ const delta = message.text.slice(previousText.length);
601
+ notifications.push({
602
+ method: "item/agentMessage/delta",
603
+ params: {
604
+ threadId,
605
+ turnId: message.turnId,
606
+ itemId: message.itemId,
607
+ delta,
608
+ },
609
+ });
610
+ }
611
+
612
+ return notifications;
613
+ }
614
+
615
+ function snapshotAssistantMessageTexts(conversationState) {
616
+ return new Map(collectAssistantMessages(conversationState).map((message) => [message.key, message.text]));
617
+ }
618
+
619
+ function collectAssistantMessages(conversationState) {
620
+ const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : [];
621
+ const messages = [];
622
+ for (const turn of turns) {
623
+ const turnId = readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
624
+ const items = Array.isArray(turn?.items) ? turn.items : [];
625
+ for (const item of items) {
626
+ if (!isAssistantMessageItem(item)) {
627
+ continue;
628
+ }
629
+
630
+ const itemId = readString(item?.id) || readString(item?.itemId) || readString(item?.item_id);
631
+ const text = assistantMessageText(item);
632
+ if (!turnId || !itemId) {
633
+ continue;
634
+ }
635
+
636
+ messages.push({
637
+ key: `${turnId}:${itemId}`,
638
+ turnId,
639
+ itemId,
640
+ text,
641
+ });
642
+ }
643
+ }
644
+ return messages;
645
+ }
646
+
647
+ function isAssistantMessageItem(item) {
648
+ const type = normalizeToken(item?.type);
649
+ if (type === "agentmessage" || type === "assistantmessage") {
650
+ return true;
651
+ }
652
+ return type === "message" && normalizeToken(item?.role) === "assistant";
653
+ }
654
+
655
+ function assistantMessageText(item) {
656
+ const directText = readString(item?.text) || readString(item?.message);
657
+ if (directText) {
658
+ return directText;
659
+ }
660
+
661
+ const content = Array.isArray(item?.content) ? item.content : [];
662
+ return content
663
+ .map((entry) => entry && typeof entry === "object" ? entry : null)
664
+ .filter(Boolean)
665
+ .map((entry) => readString(entry.text) || readString(entry?.data?.text))
666
+ .filter(Boolean)
667
+ .join("");
668
+ }
669
+
510
670
  function projectPendingDesktopAction(threadId, request) {
511
671
  const requestId = requestIdKey(request.id);
512
672
  const method = readString(request.method);
@@ -529,6 +689,7 @@ function projectPendingDesktopAction(threadId, request) {
529
689
  method,
530
690
  params: {
531
691
  ...params,
692
+ remodexActionSource: DESKTOP_IPC_ACTION_SOURCE,
532
693
  threadId: readString(params.threadId) || readString(params.thread_id) || threadId,
533
694
  },
534
695
  };
@@ -631,6 +792,10 @@ function writeFrame(socket, payload, callback) {
631
792
  }
632
793
 
633
794
  function resolveDefaultIpcSocketPath() {
795
+ if (process.platform === "win32") {
796
+ return "\\\\.\\pipe\\codex-ipc";
797
+ }
798
+
634
799
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
635
800
  return path.join(os.tmpdir(), "codex-ipc", `ipc-${uid}.sock`);
636
801
  }
@@ -656,6 +821,12 @@ function readString(value) {
656
821
  return typeof value === "string" && value.trim() ? value.trim() : "";
657
822
  }
658
823
 
824
+ function normalizeToken(value) {
825
+ return typeof value === "string"
826
+ ? value.toLowerCase().replace(/[_-\s]+/g, "")
827
+ : "";
828
+ }
829
+
659
830
  function cloneJSON(value) {
660
831
  return JSON.parse(JSON.stringify(value));
661
832
  }
@@ -672,6 +843,7 @@ module.exports = {
672
843
  applyConversationStateChange,
673
844
  createDesktopIpcActionFollower,
674
845
  desktopFollowerPayloadForResponse,
846
+ projectDesktopAssistantDeltaNotifications,
675
847
  projectPendingDesktopActions,
676
848
  resolveDefaultIpcSocketPath,
677
849
  seedConversationStateFromThreadRead,
package/src/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // Depends on: ./bridge, ./secure-device-state, ./session-state, ./rollout-watch, ./macos-launch-agent
6
6
 
7
7
  const { startBridge } = require("./bridge");
8
- const { readBridgeDeviceState, resetBridgeDeviceState } = require("./secure-device-state");
8
+ const { readBridgeDeviceState, resetBridgeTrustState } = 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");
@@ -14,6 +14,7 @@ const {
14
14
  printMacOSBridgePairingQr,
15
15
  printMacOSBridgeServiceStatus,
16
16
  resetMacOSBridgePairing,
17
+ restartMacOSBridgeService,
17
18
  runMacOSBridgeService,
18
19
  startMacOSBridgeService,
19
20
  stopMacOSBridgeService,
@@ -26,11 +27,12 @@ module.exports = {
26
27
  readBridgeConfig,
27
28
  readBridgeDeviceState,
28
29
  resetMacOSBridgePairing,
30
+ restartMacOSBridgeService,
29
31
  startBridge,
30
32
  runMacOSBridgeService,
31
33
  startMacOSBridgeService,
32
34
  stopMacOSBridgeService,
33
- resetBridgePairing: resetBridgeDeviceState,
35
+ resetBridgePairing: resetBridgeTrustState,
34
36
  openLastActiveThread,
35
37
  watchThreadRollout,
36
38
  };
@@ -11,7 +11,7 @@ const path = require("path");
11
11
  const { startBridge } = require("./bridge");
12
12
  const { readBridgeConfig } = require("./codex-desktop-refresher");
13
13
  const { printQR } = require("./qr");
14
- const { resetBridgeDeviceState } = require("./secure-device-state");
14
+ const { resetBridgeTrustState } = require("./secure-device-state");
15
15
  const {
16
16
  clearBridgeStatus,
17
17
  clearPairingSession,
@@ -122,6 +122,65 @@ async function startMacOSBridgeService({
122
122
  };
123
123
  }
124
124
 
125
+ // Restarts the installed LaunchAgent without rewriting relay config, useful during local bridge development.
126
+ async function restartMacOSBridgeService({
127
+ env = process.env,
128
+ platform = process.platform,
129
+ fsImpl = fs,
130
+ execFileSyncImpl = execFileSync,
131
+ osImpl = os,
132
+ waitForPairing = false,
133
+ pairingTimeoutMs = DEFAULT_PAIRING_WAIT_TIMEOUT_MS,
134
+ pairingPollIntervalMs = DEFAULT_PAIRING_WAIT_INTERVAL_MS,
135
+ ...startOptions
136
+ } = {}) {
137
+ assertDarwinPlatform(platform);
138
+ const plistPath = resolveLaunchAgentPlistPath({ env, osImpl });
139
+ if (!fsImpl.existsSync(plistPath)) {
140
+ return startMacOSBridgeService({
141
+ env,
142
+ platform,
143
+ fsImpl,
144
+ execFileSyncImpl,
145
+ osImpl,
146
+ waitForPairing,
147
+ pairingTimeoutMs,
148
+ pairingPollIntervalMs,
149
+ ...startOptions,
150
+ });
151
+ }
152
+
153
+ const startedAt = Date.now();
154
+ if (waitForPairing) {
155
+ clearPairingSession({ env, fsImpl });
156
+ }
157
+
158
+ kickstartLaunchAgent({
159
+ env,
160
+ execFileSyncImpl,
161
+ plistPath,
162
+ });
163
+
164
+ if (waitForPairing) {
165
+ const pairingSession = await waitForFreshPairingSession({
166
+ env,
167
+ fsImpl,
168
+ startedAt,
169
+ timeoutMs: pairingTimeoutMs,
170
+ intervalMs: pairingPollIntervalMs,
171
+ });
172
+ return {
173
+ plistPath,
174
+ pairingSession,
175
+ };
176
+ }
177
+
178
+ return {
179
+ plistPath,
180
+ pairingSession: null,
181
+ };
182
+ }
183
+
125
184
  function stopMacOSBridgeService({
126
185
  env = process.env,
127
186
  platform = process.platform,
@@ -150,7 +209,7 @@ function resetMacOSBridgePairing({
150
209
  platform = process.platform,
151
210
  execFileSyncImpl = execFileSync,
152
211
  fsImpl = fs,
153
- resetBridgePairingImpl = resetBridgeDeviceState,
212
+ resetBridgePairingImpl = resetBridgeTrustState,
154
213
  } = {}) {
155
214
  assertDarwinPlatform(platform);
156
215
  stopMacOSBridgeService({
@@ -388,6 +447,31 @@ function restartLaunchAgent({
388
447
  ], { stdio: ["ignore", "ignore", "pipe"] });
389
448
  }
390
449
 
450
+ function kickstartLaunchAgent({
451
+ env = process.env,
452
+ execFileSyncImpl = execFileSync,
453
+ plistPath,
454
+ } = {}) {
455
+ try {
456
+ execFileSyncImpl("launchctl", [
457
+ "kickstart",
458
+ "-k",
459
+ launchAgentLabelDomain(env),
460
+ ], { stdio: ["ignore", "ignore", "pipe"] });
461
+ } catch {
462
+ execFileSyncImpl("launchctl", [
463
+ "bootstrap",
464
+ launchAgentDomain(env),
465
+ plistPath,
466
+ ], { stdio: ["ignore", "ignore", "pipe"] });
467
+ execFileSyncImpl("launchctl", [
468
+ "kickstart",
469
+ "-k",
470
+ launchAgentLabelDomain(env),
471
+ ], { stdio: ["ignore", "ignore", "pipe"] });
472
+ }
473
+ }
474
+
391
475
  function bootoutLaunchAgent({
392
476
  env = process.env,
393
477
  execFileSyncImpl = execFileSync,
@@ -552,6 +636,7 @@ module.exports = {
552
636
  printMacOSBridgeServiceStatus,
553
637
  resetMacOSBridgePairing,
554
638
  resolveLaunchAgentPlistPath,
639
+ restartMacOSBridgeService,
555
640
  runMacOSBridgeService,
556
641
  startMacOSBridgeService,
557
642
  stopMacOSBridgeService,