@makerbi/remodex 1.4.0 → 1.5.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.
- package/package.json +6 -1
- package/src/bridge.js +425 -68
- package/src/desktop-handler.js +100 -11
- package/src/desktop-ipc-action-follower.js +42 -12
- package/src/git-handler.js +48 -4
- package/src/macos-launch-agent.js +2 -2
- package/src/pet-handler.js +537 -0
- package/src/qr.js +21 -2
- package/src/rollout-watch.js +17 -7
- package/src/workspace-handler.js +312 -4
- package/src/private-defaults.json +0 -4
package/src/desktop-handler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
110
|
-
async function
|
|
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
|
|
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,
|
|
@@ -77,7 +77,6 @@ function createDesktopIpcActionFollower({
|
|
|
77
77
|
|
|
78
78
|
activeThreadIds.add(threadId);
|
|
79
79
|
ipc.ensureConnected();
|
|
80
|
-
recoverThreadBaseline(threadId);
|
|
81
80
|
return false;
|
|
82
81
|
}
|
|
83
82
|
|
|
@@ -111,6 +110,19 @@ function createDesktopIpcActionFollower({
|
|
|
111
110
|
const nextState = applyConversationStateChange(previousState, params.change);
|
|
112
111
|
if (!nextState) {
|
|
113
112
|
if (isPatchChange(params.change)) {
|
|
113
|
+
const emptyState = createEmptyConversationState();
|
|
114
|
+
const speculativeState = applyConversationStateChange(emptyState, params.change);
|
|
115
|
+
const speculativeActions = projectPendingDesktopActions(threadId, speculativeState);
|
|
116
|
+
if (speculativeActions.length > 0) {
|
|
117
|
+
rawStatesByThreadId.set(threadId, speculativeState);
|
|
118
|
+
syncProjectedActions(threadId, speculativeActions);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof readConversationState !== "function") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
queueThreadChange(threadId, params.change);
|
|
115
127
|
recoverThreadBaseline(threadId);
|
|
116
128
|
}
|
|
@@ -219,8 +231,7 @@ function createDesktopIpcActionFollower({
|
|
|
219
231
|
}
|
|
220
232
|
|
|
221
233
|
function recoverThreadBaseline(threadId) {
|
|
222
|
-
if (
|
|
223
|
-
|| recoveringThreadIds.has(threadId)
|
|
234
|
+
if (recoveringThreadIds.has(threadId)
|
|
224
235
|
|| rawStatesByThreadId.has(threadId)) {
|
|
225
236
|
return;
|
|
226
237
|
}
|
|
@@ -230,27 +241,39 @@ function createDesktopIpcActionFollower({
|
|
|
230
241
|
.then(() => readConversationState(threadId))
|
|
231
242
|
.then((baselineState) => {
|
|
232
243
|
if (!baselineState || typeof baselineState !== "object") {
|
|
244
|
+
recoverThreadBaselineFromQueuedChanges(threadId, null);
|
|
233
245
|
return;
|
|
234
246
|
}
|
|
235
247
|
|
|
236
|
-
|
|
237
|
-
const queuedChanges = queuedChangesByThreadId.get(threadId) || [];
|
|
238
|
-
queuedChangesByThreadId.delete(threadId);
|
|
239
|
-
for (const change of queuedChanges) {
|
|
240
|
-
nextState = applyConversationStateChange(nextState, change) || nextState;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
rawStatesByThreadId.set(threadId, nextState);
|
|
244
|
-
syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
|
|
248
|
+
recoverThreadBaselineFromQueuedChanges(threadId, baselineState);
|
|
245
249
|
})
|
|
246
250
|
.catch((error) => {
|
|
247
251
|
console.warn(`${logPrefix} desktop IPC baseline recovery failed for ${threadId}: ${error.message}`);
|
|
252
|
+
recoverThreadBaselineFromQueuedChanges(threadId, null);
|
|
248
253
|
})
|
|
249
254
|
.finally(() => {
|
|
250
255
|
recoveringThreadIds.delete(threadId);
|
|
251
256
|
});
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
function recoverThreadBaselineFromQueuedChanges(threadId, baselineState) {
|
|
260
|
+
const queuedChanges = queuedChangesByThreadId.get(threadId) || [];
|
|
261
|
+
if (queuedChanges.length === 0) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
queuedChangesByThreadId.delete(threadId);
|
|
266
|
+
let nextState = baselineState && typeof baselineState === "object"
|
|
267
|
+
? cloneJSON(baselineState)
|
|
268
|
+
: createEmptyConversationState();
|
|
269
|
+
for (const change of queuedChanges) {
|
|
270
|
+
nextState = applyConversationStateChange(nextState, change) || nextState;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
rawStatesByThreadId.set(threadId, nextState);
|
|
274
|
+
syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
|
|
275
|
+
}
|
|
276
|
+
|
|
254
277
|
return {
|
|
255
278
|
observeInbound,
|
|
256
279
|
stopAll,
|
|
@@ -555,6 +578,13 @@ function seedConversationStateFromThreadRead(response) {
|
|
|
555
578
|
};
|
|
556
579
|
}
|
|
557
580
|
|
|
581
|
+
function createEmptyConversationState() {
|
|
582
|
+
return {
|
|
583
|
+
turns: [],
|
|
584
|
+
requests: [],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
558
588
|
function applyImmerPatch(target, patch) {
|
|
559
589
|
const patchPath = Array.isArray(patch?.path) ? patch.path : [];
|
|
560
590
|
const op = readString(patch?.op).toLowerCase();
|
package/src/git-handler.js
CHANGED
|
@@ -13,6 +13,8 @@ const { promisify } = require("util");
|
|
|
13
13
|
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
15
|
const GIT_TIMEOUT_MS = 30_000;
|
|
16
|
+
/** Node defaults maxBuffer to 1 MiB; large repo diffs exceed it ("stdout maxBuffer length exceeded"). */
|
|
17
|
+
const GIT_EXEC_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
16
18
|
const GIT_DRAFT_TIMEOUT_MS = 120_000;
|
|
17
19
|
const GITHUB_CLI_TIMEOUT_MS = 120_000;
|
|
18
20
|
const GIT_DRAFT_PATCH_MAX_BYTES = 80_000;
|
|
@@ -49,7 +51,20 @@ function handleGitRequest(rawMessage, sendResponse, options = {}) {
|
|
|
49
51
|
const id = parsed.id;
|
|
50
52
|
const params = parsed.params || {};
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
// Lets long-running git flows push interim progress events to the phone.
|
|
55
|
+
const sendNotification = (notificationMethod, notificationParams) => {
|
|
56
|
+
if (typeof notificationMethod !== "string" || !notificationMethod) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
sendResponse(JSON.stringify({
|
|
60
|
+
method: notificationMethod,
|
|
61
|
+
params: notificationParams ?? {},
|
|
62
|
+
}));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const methodOptions = { ...options, sendNotification };
|
|
66
|
+
|
|
67
|
+
handleGitMethod(method, params, methodOptions)
|
|
53
68
|
.then((result) => {
|
|
54
69
|
sendResponse(JSON.stringify({ id, result }));
|
|
55
70
|
if (method === "thread/name/set") {
|
|
@@ -977,8 +992,26 @@ async function gitRunStackedAction(cwd, params, options = {}) {
|
|
|
977
992
|
const wantsCommit = action === "commit" || action === "commit_push" || action === "commit_push_pr";
|
|
978
993
|
const wantsPr = action === "create_pr" || action === "commit_push_pr";
|
|
979
994
|
|
|
995
|
+
// Emits phase progress events on the same wire used by JSON-RPC responses
|
|
996
|
+
// so the iOS toast can reflect the live step (commit/push/PR) of a stacked action.
|
|
997
|
+
const progressId = typeof params.progressId === "string" && params.progressId.trim()
|
|
998
|
+
? params.progressId.trim()
|
|
999
|
+
: null;
|
|
1000
|
+
const emitPhase = (phase, status) => {
|
|
1001
|
+
if (!progressId || typeof options.sendNotification !== "function") {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
options.sendNotification("git/stackedAction/progress", {
|
|
1005
|
+
progressId,
|
|
1006
|
+
phase,
|
|
1007
|
+
status,
|
|
1008
|
+
});
|
|
1009
|
+
};
|
|
1010
|
+
|
|
980
1011
|
if (params.featureBranch === true) {
|
|
1012
|
+
emitPhase("branch", "started");
|
|
981
1013
|
await gitCreateFeatureBranch(cwd, params);
|
|
1014
|
+
emitPhase("branch", "completed");
|
|
982
1015
|
}
|
|
983
1016
|
|
|
984
1017
|
const branch = await currentBranchName(cwd);
|
|
@@ -1004,6 +1037,7 @@ async function gitRunStackedAction(cwd, params, options = {}) {
|
|
|
1004
1037
|
if (wantsCommit) {
|
|
1005
1038
|
const statusBeforeCommit = await gitStatus(cwd);
|
|
1006
1039
|
if (statusBeforeCommit.dirty) {
|
|
1040
|
+
emitPhase("commit", "started");
|
|
1007
1041
|
const commitResult = await gitCommit(cwd, {
|
|
1008
1042
|
message: params.commitMessage || params.message,
|
|
1009
1043
|
});
|
|
@@ -1013,10 +1047,12 @@ async function gitRunStackedAction(cwd, params, options = {}) {
|
|
|
1013
1047
|
commitSha: commitResult.hash,
|
|
1014
1048
|
subject: firstCommitMessageLine(params.commitMessage || params.message),
|
|
1015
1049
|
};
|
|
1050
|
+
emitPhase("commit", "completed");
|
|
1016
1051
|
} else if (action === "commit") {
|
|
1017
1052
|
throw gitError("nothing_to_commit", "Nothing to commit.");
|
|
1018
1053
|
} else {
|
|
1019
1054
|
result.commit = { status: "skipped_clean" };
|
|
1055
|
+
emitPhase("commit", "skipped");
|
|
1020
1056
|
}
|
|
1021
1057
|
}
|
|
1022
1058
|
|
|
@@ -1037,17 +1073,21 @@ async function gitRunStackedAction(cwd, params, options = {}) {
|
|
|
1037
1073
|
if (statusBeforePush.dirty) {
|
|
1038
1074
|
throw gitError("dirty_worktree", "Commit or stash local changes before pushing.");
|
|
1039
1075
|
}
|
|
1076
|
+
emitPhase("push", "started");
|
|
1040
1077
|
result.push = {
|
|
1041
1078
|
state: "pushed",
|
|
1042
1079
|
...(await gitPush(cwd)),
|
|
1043
1080
|
};
|
|
1081
|
+
emitPhase("push", "completed");
|
|
1044
1082
|
}
|
|
1045
1083
|
|
|
1046
1084
|
if (wantsPr) {
|
|
1085
|
+
emitPhase("createPR", "started");
|
|
1047
1086
|
result.pr = await gitCreatePullRequest(cwd, {
|
|
1048
1087
|
...params,
|
|
1049
1088
|
pushBeforeCreate: false,
|
|
1050
1089
|
}, options);
|
|
1090
|
+
emitPhase("createPR", "completed");
|
|
1051
1091
|
}
|
|
1052
1092
|
|
|
1053
1093
|
result.status = await gitStatus(cwd);
|
|
@@ -2387,7 +2427,7 @@ async function gitDiffNoIndexNumstat(cwd, filePath) {
|
|
|
2387
2427
|
const { stdout } = await execFileAsync(
|
|
2388
2428
|
"git",
|
|
2389
2429
|
["diff", "--no-index", "--numstat", "--", "/dev/null", filePath],
|
|
2390
|
-
{ cwd, timeout: GIT_TIMEOUT_MS }
|
|
2430
|
+
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES }
|
|
2391
2431
|
);
|
|
2392
2432
|
return stdout;
|
|
2393
2433
|
} catch (err) {
|
|
@@ -2413,7 +2453,7 @@ async function gitDiffNoIndexPatch(cwd, filePath) {
|
|
|
2413
2453
|
const { stdout } = await execFileAsync(
|
|
2414
2454
|
"git",
|
|
2415
2455
|
["diff", "--no-index", "--binary", "--", "/dev/null", filePath],
|
|
2416
|
-
{ cwd, timeout: GIT_TIMEOUT_MS }
|
|
2456
|
+
{ cwd, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES }
|
|
2417
2457
|
);
|
|
2418
2458
|
return stdout;
|
|
2419
2459
|
} catch (err) {
|
|
@@ -2428,7 +2468,11 @@ async function gitDiffNoIndexPatch(cwd, filePath) {
|
|
|
2428
2468
|
// ─── Helpers ──────────────────────────────────────────────────
|
|
2429
2469
|
|
|
2430
2470
|
function git(cwd, ...args) {
|
|
2431
|
-
return execFileAsync("git", args, {
|
|
2471
|
+
return execFileAsync("git", args, {
|
|
2472
|
+
cwd,
|
|
2473
|
+
timeout: GIT_TIMEOUT_MS,
|
|
2474
|
+
maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
|
|
2475
|
+
})
|
|
2432
2476
|
.then(({ stdout }) => stdout)
|
|
2433
2477
|
.catch((err) => {
|
|
2434
2478
|
const msg = (err.stderr || err.message || "").trim();
|
|
@@ -33,8 +33,8 @@ const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
|
|
|
33
33
|
const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
|
|
34
34
|
|
|
35
35
|
// Runs the bridge inside launchd while keeping QR rendering in the foreground CLI command.
|
|
36
|
-
function runMacOSBridgeService({ env = process.env } = {}) {
|
|
37
|
-
assertDarwinPlatform();
|
|
36
|
+
function runMacOSBridgeService({ env = process.env, platform = process.platform } = {}) {
|
|
37
|
+
assertDarwinPlatform(platform);
|
|
38
38
|
const config = readDaemonConfig({ env });
|
|
39
39
|
if (!config?.relayUrl) {
|
|
40
40
|
const message = "No relay URL configured for the macOS bridge service.";
|