@makerbi/remodex 2.4.0 → 3.1.0

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.
@@ -1,7 +1,7 @@
1
1
  // FILE: macos-launch-agent.js
2
- // Purpose: Owns macOS-only launchd install/start/stop/status helpers for the background Remodex bridge.
2
+ // Purpose: Owns macOS-only launchd install/start/stop/uninstall/status helpers for the background Remodex bridge.
3
3
  // Layer: CLI helper
4
- // Exports: start/stop/status helpers plus the launchd service runner used by `remodex up`.
4
+ // Exports: start/stop/uninstall/status helpers plus the launchd service runner used by `remodex up`.
5
5
  // Depends on: child_process, fs, os, path, ./bridge, ./daemon-state, ./codex-desktop-refresher, ./qr, ./secure-device-state
6
6
 
7
7
  const { execFileSync } = require("child_process");
@@ -33,6 +33,23 @@ const SERVICE_LABEL = "com.remodex.bridge";
33
33
  const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
34
34
  const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
35
35
 
36
+ // If the saved Node binary or CLI entrypoint disappears (npm uninstall, deleted
37
+ // checkout), exit 0 so launchd's KeepAlive.SuccessfulExit=false stops rescheduling
38
+ // the job; `exec` keeps genuine daemon failures non-zero so they still restart.
39
+ const LAUNCH_AGENT_GUARD_SCRIPT = 'if [ ! -x "$1" ] || [ ! -f "$2" ]; then exit 0; fi; exec "$1" "$2" run-service';
40
+
41
+ // Keeps the guard script constant: the installed paths are shell positionals, never interpolated source.
42
+ function buildLaunchAgentProgramArguments({ nodePath, cliPath }) {
43
+ return [
44
+ "/bin/sh",
45
+ "-c",
46
+ LAUNCH_AGENT_GUARD_SCRIPT,
47
+ SERVICE_LABEL,
48
+ nodePath,
49
+ cliPath,
50
+ ];
51
+ }
52
+
36
53
  // Runs the bridge inside launchd while keeping QR rendering in the foreground CLI command.
37
54
  function runMacOSBridgeService({ env = process.env, platform = process.platform } = {}) {
38
55
  assertDarwinPlatform(platform);
@@ -123,17 +140,19 @@ async function startMacOSBridgeService({
123
140
  };
124
141
  }
125
142
 
126
- // Restarts the installed LaunchAgent without rewriting relay config, useful during local bridge development.
143
+ // Restarts the installed LaunchAgent without rewriting relay config, regenerating the plist so
144
+ // legacy launch definitions pick up the current Node/CLI paths and launch policy.
127
145
  async function restartMacOSBridgeService({
128
146
  env = process.env,
129
147
  platform = process.platform,
130
148
  fsImpl = fs,
131
149
  execFileSyncImpl = execFileSync,
132
150
  osImpl = os,
151
+ nodePath = process.execPath,
152
+ cliPath = path.resolve(__dirname, "..", "bin", "remodex.js"),
133
153
  waitForPairing = false,
134
154
  pairingTimeoutMs = DEFAULT_PAIRING_WAIT_TIMEOUT_MS,
135
155
  pairingPollIntervalMs = DEFAULT_PAIRING_WAIT_INTERVAL_MS,
136
- ...startOptions
137
156
  } = {}) {
138
157
  assertDarwinPlatform(platform);
139
158
  const plistPath = resolveLaunchAgentPlistPath({ env, osImpl });
@@ -144,10 +163,11 @@ async function restartMacOSBridgeService({
144
163
  fsImpl,
145
164
  execFileSyncImpl,
146
165
  osImpl,
166
+ nodePath,
167
+ cliPath,
147
168
  waitForPairing,
148
169
  pairingTimeoutMs,
149
170
  pairingPollIntervalMs,
150
- ...startOptions,
151
171
  });
152
172
  }
153
173
 
@@ -156,7 +176,16 @@ async function restartMacOSBridgeService({
156
176
  clearPairingSession({ env, fsImpl });
157
177
  }
158
178
 
159
- kickstartLaunchAgent({
179
+ ensureRemodexStateDir({ env, fsImpl, osImpl });
180
+ ensureRemodexLogsDir({ env, fsImpl, osImpl });
181
+ writeLaunchAgentPlist({
182
+ env,
183
+ fsImpl,
184
+ osImpl,
185
+ nodePath,
186
+ cliPath,
187
+ });
188
+ restartLaunchAgent({
160
189
  env,
161
190
  execFileSyncImpl,
162
191
  plistPath,
@@ -204,6 +233,31 @@ function stopMacOSBridgeService({
204
233
  clearBridgeStatus({ env, fsImpl });
205
234
  }
206
235
 
236
+ // Removes launchd ownership of the bridge (unload + plist) while preserving daemon config,
237
+ // logs, device trust, and pairing identity for a future reinstall.
238
+ function uninstallMacOSBridgeService({
239
+ env = process.env,
240
+ platform = process.platform,
241
+ execFileSyncImpl = execFileSync,
242
+ fsImpl = fs,
243
+ osImpl = os,
244
+ processImpl = process,
245
+ } = {}) {
246
+ assertDarwinPlatform(platform);
247
+ const plistPath = resolveLaunchAgentPlistPath({ env, osImpl });
248
+ const removed = fsImpl.existsSync(plistPath);
249
+ // Stop first: a real bootout failure throws here and leaves the plist on disk.
250
+ stopMacOSBridgeService({
251
+ env,
252
+ platform,
253
+ execFileSyncImpl,
254
+ fsImpl,
255
+ processImpl,
256
+ });
257
+ fsImpl.rmSync(plistPath, { force: true });
258
+ return { plistPath, removed };
259
+ }
260
+
207
261
  // Revokes pairing immediately on macOS by stopping the daemon before rotating identity/trust state.
208
262
  function resetMacOSBridgePairing({
209
263
  env = process.env,
@@ -374,9 +428,9 @@ function buildLaunchAgentPlist({
374
428
  <string>${escapeXml(SERVICE_LABEL)}</string>
375
429
  <key>ProgramArguments</key>
376
430
  <array>
377
- <string>${escapeXml(nodePath)}</string>
378
- <string>${escapeXml(cliPath)}</string>
379
- <string>run-service</string>
431
+ ${buildLaunchAgentProgramArguments({ nodePath, cliPath })
432
+ .map((argument) => ` <string>${escapeXml(argument)}</string>`)
433
+ .join("\n")}
380
434
  </array>
381
435
  <key>RunAtLoad</key>
382
436
  <true/>
@@ -455,31 +509,6 @@ function restartLaunchAgent({
455
509
  ], { stdio: ["ignore", "ignore", "pipe"] });
456
510
  }
457
511
 
458
- function kickstartLaunchAgent({
459
- env = process.env,
460
- execFileSyncImpl = execFileSync,
461
- plistPath,
462
- } = {}) {
463
- try {
464
- execFileSyncImpl("launchctl", [
465
- "kickstart",
466
- "-k",
467
- launchAgentLabelDomain(env),
468
- ], { stdio: ["ignore", "ignore", "pipe"] });
469
- } catch {
470
- execFileSyncImpl("launchctl", [
471
- "bootstrap",
472
- launchAgentDomain(env),
473
- plistPath,
474
- ], { stdio: ["ignore", "ignore", "pipe"] });
475
- execFileSyncImpl("launchctl", [
476
- "kickstart",
477
- "-k",
478
- launchAgentLabelDomain(env),
479
- ], { stdio: ["ignore", "ignore", "pipe"] });
480
- }
481
- }
482
-
483
512
  function bootoutLaunchAgent({
484
513
  env = process.env,
485
514
  execFileSyncImpl = execFileSync,
@@ -675,6 +704,7 @@ function shortFingerprint(value) {
675
704
  module.exports = {
676
705
  buildTrustedDeviceSummary,
677
706
  buildLaunchAgentPlist,
707
+ buildLaunchAgentProgramArguments,
678
708
  getMacOSBridgeServiceStatus,
679
709
  mergeBridgeStatusForDaemon,
680
710
  printMacOSBridgePairingQr,
@@ -685,4 +715,5 @@ module.exports = {
685
715
  runMacOSBridgeService,
686
716
  startMacOSBridgeService,
687
717
  stopMacOSBridgeService,
718
+ uninstallMacOSBridgeService,
688
719
  };
@@ -13,6 +13,10 @@ const {
13
13
  } = require("./rollout-watch");
14
14
  const { resolveCodexGeneratedImagesRoot } = require("./codex-home");
15
15
  const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
16
+ const {
17
+ expandExecWrapperToolCall,
18
+ isOrchestrationWaitCall,
19
+ } = require("./codex-tool-wrapper");
16
20
  const {
17
21
  TERMINAL_TASK_EVENT_TYPES,
18
22
  terminalEventClosesTrackedTurn,
@@ -56,6 +60,8 @@ function createRolloutLiveMirrorController({
56
60
  now = () => Date.now(),
57
61
  setIntervalFn = setInterval,
58
62
  clearIntervalFn = clearInterval,
63
+ setImmediateFn = setImmediate,
64
+ clearImmediateFn = clearImmediate,
59
65
  pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
60
66
  lookupTimeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS,
61
67
  idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
@@ -110,6 +116,8 @@ function createRolloutLiveMirrorController({
110
116
  now,
111
117
  setIntervalFn,
112
118
  clearIntervalFn,
119
+ setImmediateFn,
120
+ clearImmediateFn,
113
121
  pollIntervalMs,
114
122
  lookupTimeoutMs,
115
123
  idleTimeoutMs,
@@ -157,6 +165,8 @@ function createThreadRolloutLiveMirror({
157
165
  now,
158
166
  setIntervalFn,
159
167
  clearIntervalFn,
168
+ setImmediateFn,
169
+ clearImmediateFn,
160
170
  pollIntervalMs,
161
171
  lookupTimeoutMs,
162
172
  idleTimeoutMs,
@@ -166,6 +176,7 @@ function createThreadRolloutLiveMirror({
166
176
  onStop = () => {},
167
177
  }) {
168
178
  const startedAt = now();
179
+ let lookupStartedAt = startedAt;
169
180
  const state = createMirrorState(threadId);
170
181
 
171
182
  let isStopped = false;
@@ -182,7 +193,10 @@ function createThreadRolloutLiveMirror({
182
193
  let wasSuppressed = false;
183
194
 
184
195
  const intervalId = setIntervalFn(tick, pollIntervalMs);
185
- tick();
196
+ let initialTickId = setImmediateFn(() => {
197
+ initialTickId = null;
198
+ tick();
199
+ });
186
200
 
187
201
  function tick() {
188
202
  if (isStopped) {
@@ -191,9 +205,30 @@ function createThreadRolloutLiveMirror({
191
205
 
192
206
  try {
193
207
  const currentTime = now();
208
+ const suppressedBeforeScan = isSuppressed();
209
+ if (suppressedBeforeScan) {
210
+ if (!wasSuppressed) {
211
+ rolloutPath = null;
212
+ lastSize = 0;
213
+ partialLine = "";
214
+ didBootstrap = false;
215
+ resetRunState(state);
216
+ }
217
+ wasSuppressed = true;
218
+ return;
219
+ }
220
+ if (wasSuppressed) {
221
+ rolloutPath = null;
222
+ lastSize = 0;
223
+ partialLine = "";
224
+ didBootstrap = false;
225
+ resetRunState(state);
226
+ lookupStartedAt = currentTime;
227
+ wasSuppressed = false;
228
+ }
194
229
 
195
230
  if (!rolloutPath) {
196
- if (currentTime - startedAt >= lookupTimeoutMs) {
231
+ if (currentTime - lookupStartedAt >= lookupTimeoutMs) {
197
232
  stop();
198
233
  return;
199
234
  }
@@ -209,20 +244,21 @@ function createThreadRolloutLiveMirror({
209
244
 
210
245
  const rolloutStat = fsModule.statSync(rolloutPath);
211
246
  const fileSize = rolloutStat.size;
212
- // While another live source streams this thread the tail keeps consuming
213
- // rollout lines with its emissions muted. Compare per-thread activity so
214
- // a quiet Desktop turn stays owned, while newer rollout growth can recover
215
- // from a stale connected snapshot.
247
+ // Re-check ownership with the rollout's activity time before bootstrapping.
248
+ // If another source owns the thread, leave the file untouched until that
249
+ // ownership expires.
216
250
  const suppressed = isSuppressed({
217
251
  fallbackActivityAt: Number(rolloutStat.mtimeMs) || 0,
218
252
  });
219
- if (wasSuppressed && !suppressed && didBootstrap) {
253
+ if (suppressed) {
254
+ rolloutPath = null;
220
255
  lastSize = 0;
221
256
  partialLine = "";
222
257
  didBootstrap = false;
223
258
  resetRunState(state);
259
+ wasSuppressed = true;
260
+ return;
224
261
  }
225
- wasSuppressed = suppressed;
226
262
  if (!didBootstrap) {
227
263
  didBootstrap = true;
228
264
  bootstrapFromExistingRollout({
@@ -354,6 +390,10 @@ function createThreadRolloutLiveMirror({
354
390
  // final partial-line flush must never leak the poll interval.
355
391
  isStopped = true;
356
392
  clearIntervalFn(intervalId);
393
+ if (initialTickId != null) {
394
+ clearImmediateFn(initialTickId);
395
+ initialTickId = null;
396
+ }
357
397
  if (partialLine) {
358
398
  const flushLine = partialLine;
359
399
  partialLine = "";
@@ -369,8 +409,8 @@ function createThreadRolloutLiveMirror({
369
409
  // Only a healthy, actively-tailed run with a real id counts: synthetic ids
370
410
  // are not actionable app-server turn ids, and suppressed/awaiting states
371
411
  // mean the mirror does not actually know what is running. While another live
372
- // source owns the thread the tail keeps parsing with its emissions muted, so
373
- // reporting that turn id would resurrect exactly the state the bridge muted.
412
+ // source owns the thread the mirror has no parsed file state, so reporting a
413
+ // turn id would resurrect exactly the state the bridge muted.
374
414
  function getActiveTurnId() {
375
415
  if (
376
416
  isStopped
@@ -950,6 +990,7 @@ function synthesizeNotificationsFromRolloutEntry(entry, state, { nowMs = Date.no
950
990
  state.commandCalls.clear();
951
991
  state.applyPatchCalls.clear();
952
992
  state.emittedPatchApplyEndCalls.clear();
993
+ state.wrappedExecCallIdsByOuterId.clear();
953
994
 
954
995
  const startedParams = {
955
996
  threadId: state.threadId,
@@ -1081,12 +1122,12 @@ function synthesizeNotificationsFromRolloutEntry(entry, state, { nowMs = Date.no
1081
1122
  }
1082
1123
 
1083
1124
  if (itemType === "functioncall") {
1084
- notifications.push(...toolStartNotifications(state, payload));
1125
+ notifications.push(...projectedToolStartNotifications(state, payload));
1085
1126
  return notifications;
1086
1127
  }
1087
1128
 
1088
1129
  if (itemType === "customtoolcall") {
1089
- notifications.push(...customToolStartNotifications(state, payload));
1130
+ notifications.push(...projectedToolStartNotifications(state, payload));
1090
1131
  return notifications;
1091
1132
  }
1092
1133
 
@@ -1326,6 +1367,29 @@ function extractResponseItemMessageText(payload) {
1326
1367
  return responseItemMessageText(payload);
1327
1368
  }
1328
1369
 
1370
+ function projectedToolStartNotifications(state, payload) {
1371
+ if (isOrchestrationWaitCall(payload)) {
1372
+ return [];
1373
+ }
1374
+
1375
+ const projectedPayloads = expandExecWrapperToolCall(payload);
1376
+ const outerCallId = projectedPayloads[0]?.remodexWrappedExecCallId;
1377
+ if (outerCallId && projectedPayloads.length > 1) {
1378
+ state.wrappedExecCallIdsByOuterId.set(
1379
+ outerCallId,
1380
+ projectedPayloads.map((projectedPayload) => (
1381
+ readString(projectedPayload.call_id) || readString(projectedPayload.callId)
1382
+ )).filter(Boolean)
1383
+ );
1384
+ }
1385
+
1386
+ return projectedPayloads.flatMap((projectedPayload) => (
1387
+ normalizeRolloutItemType(projectedPayload.type) === "customtoolcall"
1388
+ ? customToolStartNotifications(state, projectedPayload)
1389
+ : toolStartNotifications(state, projectedPayload)
1390
+ ));
1391
+ }
1392
+
1329
1393
  function toolStartNotifications(state, payload) {
1330
1394
  if (!state.activeTurnId) {
1331
1395
  return [];
@@ -1383,6 +1447,7 @@ function toolStartNotifications(state, payload) {
1383
1447
  toolName,
1384
1448
  command: resolveToolCommand(toolName, argumentsObject),
1385
1449
  cwd: resolveToolWorkingDirectory(argumentsObject, state),
1450
+ wrappedExecCall: Boolean(payload.remodexWrappedExecCallId),
1386
1451
  });
1387
1452
 
1388
1453
  if (isCommandToolName(toolName)) {
@@ -1411,6 +1476,8 @@ function toolStartNotifications(state, payload) {
1411
1476
  threadId: state.threadId,
1412
1477
  turnId: state.activeTurnId,
1413
1478
  call_id: callId,
1479
+ itemId: callId,
1480
+ status: "inProgress",
1414
1481
  message: activityMessage,
1415
1482
  }),
1416
1483
  ];
@@ -1461,6 +1528,7 @@ function customToolStartNotifications(state, payload) {
1461
1528
  toolName,
1462
1529
  command: toolName,
1463
1530
  cwd: readString(state.sessionMeta?.cwd) || "",
1531
+ wrappedExecCall: Boolean(payload.remodexWrappedExecCallId),
1464
1532
  });
1465
1533
  }
1466
1534
 
@@ -1470,6 +1538,10 @@ function customToolStartNotifications(state, payload) {
1470
1538
  threadId: state.threadId,
1471
1539
  turnId: state.activeTurnId,
1472
1540
  call_id: callId,
1541
+ ...(!state.applyPatchCalls.has(callId) ? {
1542
+ itemId: callId,
1543
+ status: "inProgress",
1544
+ } : {}),
1473
1545
  message: activityMessage,
1474
1546
  }),
1475
1547
  ];
@@ -1544,8 +1616,28 @@ function toolOutputNotifications(state, payload) {
1544
1616
  return [];
1545
1617
  }
1546
1618
 
1619
+ const wrappedCallIds = state.wrappedExecCallIdsByOuterId.get(callId);
1620
+ if (Array.isArray(wrappedCallIds) && wrappedCallIds.length > 0) {
1621
+ state.wrappedExecCallIdsByOuterId.delete(callId);
1622
+ const outputRecipientId = wrappedCallIds.find((nestedCallId) => (
1623
+ isCommandToolName(state.commandCalls.get(nestedCallId)?.toolName)
1624
+ )) || wrappedCallIds[0];
1625
+ return wrappedCallIds.flatMap((nestedCallId) => toolOutputNotifications(state, {
1626
+ ...payload,
1627
+ call_id: nestedCallId,
1628
+ callId: nestedCallId,
1629
+ output: nestedCallId === outputRecipientId ? payload.output : "",
1630
+ }));
1631
+ }
1632
+
1547
1633
  const toolCall = state.commandCalls.get(callId);
1548
1634
  if (!toolCall) {
1635
+ if (state.applyPatchCalls.has(callId)) {
1636
+ return patchApplyEndNotifications(state, {
1637
+ ...payload,
1638
+ status: readString(payload.status) || "completed",
1639
+ });
1640
+ }
1549
1641
  return [];
1550
1642
  }
1551
1643
 
@@ -1555,13 +1647,16 @@ function toolOutputNotifications(state, payload) {
1555
1647
  threadId: state.threadId,
1556
1648
  turnId: state.activeTurnId,
1557
1649
  call_id: callId,
1650
+ itemId: callId,
1651
+ status: "completed",
1558
1652
  message: genericToolCompletionMessage(toolCall.toolName),
1559
1653
  }));
1560
1654
  state.commandCalls.delete(callId);
1561
1655
  return notifications;
1562
1656
  }
1563
1657
 
1564
- const output = readString(payload.output);
1658
+ const rawOutput = extractToolOutputText(payload.output);
1659
+ const output = toolCall.wrappedExecCall ? stripExecOutputEnvelope(rawOutput) : rawOutput;
1565
1660
  const notifications = [...ensureThinkingNotifications(state)];
1566
1661
  if (output) {
1567
1662
  notifications.push(createNotification("codex/event/exec_command_output_delta", {
@@ -1587,6 +1682,38 @@ function toolOutputNotifications(state, payload) {
1587
1682
  return notifications;
1588
1683
  }
1589
1684
 
1685
+ function extractToolOutputText(value) {
1686
+ if (typeof value === "string") {
1687
+ return value;
1688
+ }
1689
+ if (Array.isArray(value)) {
1690
+ return value.map(extractToolOutputText).join("");
1691
+ }
1692
+ if (!value || typeof value !== "object") {
1693
+ return "";
1694
+ }
1695
+
1696
+ for (const key of ["text", "output_text", "outputText"]) {
1697
+ if (typeof value[key] === "string") {
1698
+ return value[key];
1699
+ }
1700
+ }
1701
+ for (const key of ["content", "output", "result"]) {
1702
+ const text = extractToolOutputText(value[key]);
1703
+ if (text) {
1704
+ return text;
1705
+ }
1706
+ }
1707
+ return "";
1708
+ }
1709
+
1710
+ function stripExecOutputEnvelope(output) {
1711
+ return readString(output).replace(
1712
+ /^Script [^\n]*\nWall time [^\n]*\nOutput:\n?/,
1713
+ ""
1714
+ );
1715
+ }
1716
+
1590
1717
  function imageGenerationNotifications(state, payload, { preferCallId = false } = {}) {
1591
1718
  if (!state.activeTurnId) {
1592
1719
  return [];
@@ -1759,6 +1886,7 @@ function createMirrorState(threadId) {
1759
1886
  commandCalls: new Map(),
1760
1887
  applyPatchCalls: new Map(),
1761
1888
  emittedPatchApplyEndCalls: new Set(),
1889
+ wrappedExecCallIdsByOuterId: new Map(),
1762
1890
  emittedAgentMessageKeys: new Set(),
1763
1891
  agentMessageOccurrencesByBaseKey: new Map(),
1764
1892
  pendingEventAgentMessageOccurrencesByBaseKey: new Map(),
@@ -2091,6 +2219,7 @@ function resetRunState(state) {
2091
2219
  state.commandCalls.clear();
2092
2220
  state.applyPatchCalls.clear();
2093
2221
  state.emittedPatchApplyEndCalls.clear();
2222
+ state.wrappedExecCallIdsByOuterId.clear();
2094
2223
  state.emittedAgentMessageKeys.clear();
2095
2224
  state.agentMessageOccurrencesByBaseKey.clear();
2096
2225
  state.pendingEventAgentMessageOccurrencesByBaseKey.clear();