@makerbi/remodex 2.4.0 → 2.5.6

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)) {
@@ -1461,6 +1526,7 @@ function customToolStartNotifications(state, payload) {
1461
1526
  toolName,
1462
1527
  command: toolName,
1463
1528
  cwd: readString(state.sessionMeta?.cwd) || "",
1529
+ wrappedExecCall: Boolean(payload.remodexWrappedExecCallId),
1464
1530
  });
1465
1531
  }
1466
1532
 
@@ -1544,8 +1610,28 @@ function toolOutputNotifications(state, payload) {
1544
1610
  return [];
1545
1611
  }
1546
1612
 
1613
+ const wrappedCallIds = state.wrappedExecCallIdsByOuterId.get(callId);
1614
+ if (Array.isArray(wrappedCallIds) && wrappedCallIds.length > 0) {
1615
+ state.wrappedExecCallIdsByOuterId.delete(callId);
1616
+ const outputRecipientId = wrappedCallIds.find((nestedCallId) => (
1617
+ isCommandToolName(state.commandCalls.get(nestedCallId)?.toolName)
1618
+ )) || wrappedCallIds[0];
1619
+ return wrappedCallIds.flatMap((nestedCallId) => toolOutputNotifications(state, {
1620
+ ...payload,
1621
+ call_id: nestedCallId,
1622
+ callId: nestedCallId,
1623
+ output: nestedCallId === outputRecipientId ? payload.output : "",
1624
+ }));
1625
+ }
1626
+
1547
1627
  const toolCall = state.commandCalls.get(callId);
1548
1628
  if (!toolCall) {
1629
+ if (state.applyPatchCalls.has(callId)) {
1630
+ return patchApplyEndNotifications(state, {
1631
+ ...payload,
1632
+ status: readString(payload.status) || "completed",
1633
+ });
1634
+ }
1549
1635
  return [];
1550
1636
  }
1551
1637
 
@@ -1561,7 +1647,8 @@ function toolOutputNotifications(state, payload) {
1561
1647
  return notifications;
1562
1648
  }
1563
1649
 
1564
- const output = readString(payload.output);
1650
+ const rawOutput = extractToolOutputText(payload.output);
1651
+ const output = toolCall.wrappedExecCall ? stripExecOutputEnvelope(rawOutput) : rawOutput;
1565
1652
  const notifications = [...ensureThinkingNotifications(state)];
1566
1653
  if (output) {
1567
1654
  notifications.push(createNotification("codex/event/exec_command_output_delta", {
@@ -1587,6 +1674,38 @@ function toolOutputNotifications(state, payload) {
1587
1674
  return notifications;
1588
1675
  }
1589
1676
 
1677
+ function extractToolOutputText(value) {
1678
+ if (typeof value === "string") {
1679
+ return value;
1680
+ }
1681
+ if (Array.isArray(value)) {
1682
+ return value.map(extractToolOutputText).join("");
1683
+ }
1684
+ if (!value || typeof value !== "object") {
1685
+ return "";
1686
+ }
1687
+
1688
+ for (const key of ["text", "output_text", "outputText"]) {
1689
+ if (typeof value[key] === "string") {
1690
+ return value[key];
1691
+ }
1692
+ }
1693
+ for (const key of ["content", "output", "result"]) {
1694
+ const text = extractToolOutputText(value[key]);
1695
+ if (text) {
1696
+ return text;
1697
+ }
1698
+ }
1699
+ return "";
1700
+ }
1701
+
1702
+ function stripExecOutputEnvelope(output) {
1703
+ return readString(output).replace(
1704
+ /^Script [^\n]*\nWall time [^\n]*\nOutput:\n?/,
1705
+ ""
1706
+ );
1707
+ }
1708
+
1590
1709
  function imageGenerationNotifications(state, payload, { preferCallId = false } = {}) {
1591
1710
  if (!state.activeTurnId) {
1592
1711
  return [];
@@ -1759,6 +1878,7 @@ function createMirrorState(threadId) {
1759
1878
  commandCalls: new Map(),
1760
1879
  applyPatchCalls: new Map(),
1761
1880
  emittedPatchApplyEndCalls: new Set(),
1881
+ wrappedExecCallIdsByOuterId: new Map(),
1762
1882
  emittedAgentMessageKeys: new Set(),
1763
1883
  agentMessageOccurrencesByBaseKey: new Map(),
1764
1884
  pendingEventAgentMessageOccurrencesByBaseKey: new Map(),
@@ -2091,6 +2211,7 @@ function resetRunState(state) {
2091
2211
  state.commandCalls.clear();
2092
2212
  state.applyPatchCalls.clear();
2093
2213
  state.emittedPatchApplyEndCalls.clear();
2214
+ state.wrappedExecCallIdsByOuterId.clear();
2094
2215
  state.emittedAgentMessageKeys.clear();
2095
2216
  state.agentMessageOccurrencesByBaseKey.clear();
2096
2217
  state.pendingEventAgentMessageOccurrencesByBaseKey.clear();