@makerbi/remodex 1.5.4 → 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.
- package/bin/remodex.js +22 -9
- package/package.json +1 -1
- package/src/account-status.js +7 -1
- package/src/apply-patch-changes.js +185 -0
- package/src/bootstrap-codex-cli.js +1 -1
- package/src/bridge-status.js +3 -2
- package/src/bridge.js +752 -71
- package/src/codex-transport.js +10 -10
- package/src/desktop-handler.js +14 -1
- package/src/desktop-ipc-action-follower.js +129 -0
- package/src/index.js +4 -2
- package/src/macos-launch-agent.js +87 -2
- package/src/project-handler.js +162 -1
- package/src/push-notification-service-client.js +85 -37
- package/src/push-notification-tracker.js +15 -0
- package/src/rollout-live-mirror.js +331 -20
- package/src/rollout-watch.js +5 -1
- package/src/secure-device-state.js +26 -1
- package/src/secure-transport.js +40 -12
- package/src/session-jsonl-history.js +796 -15
- package/src/voice-handler.js +129 -62
- package/src/workspace-handler.js +327 -14
package/src/codex-transport.js
CHANGED
|
@@ -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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
});
|
package/src/desktop-handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FILE: desktop-handler.js
|
|
2
|
-
// Purpose: Handles explicit desktop handoff, display wake, and
|
|
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,6 +11,7 @@ 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",
|
|
@@ -54,6 +55,7 @@ function createDesktopIpcActionFollower({
|
|
|
54
55
|
onDisconnect,
|
|
55
56
|
});
|
|
56
57
|
const rawStatesByThreadId = new Map();
|
|
58
|
+
const assistantMessageTextsByThreadId = new Map();
|
|
57
59
|
const pendingRoutesByRequestId = new Map();
|
|
58
60
|
const activeThreadIds = new Set();
|
|
59
61
|
const recoveringThreadIds = new Set();
|
|
@@ -84,6 +86,7 @@ function createDesktopIpcActionFollower({
|
|
|
84
86
|
|
|
85
87
|
function stopAll() {
|
|
86
88
|
rawStatesByThreadId.clear();
|
|
89
|
+
assistantMessageTextsByThreadId.clear();
|
|
87
90
|
pendingRoutesByRequestId.clear();
|
|
88
91
|
activeThreadIds.clear();
|
|
89
92
|
recoveringThreadIds.clear();
|
|
@@ -132,11 +135,13 @@ function createDesktopIpcActionFollower({
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
rawStatesByThreadId.set(threadId, nextState);
|
|
138
|
+
syncProjectedAssistantDeltas(threadId, previousState, nextState);
|
|
135
139
|
syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
function onDisconnect() {
|
|
139
143
|
rawStatesByThreadId.clear();
|
|
144
|
+
assistantMessageTextsByThreadId.clear();
|
|
140
145
|
pendingRoutesByRequestId.clear();
|
|
141
146
|
recoveringThreadIds.clear();
|
|
142
147
|
queuedChangesByThreadId.clear();
|
|
@@ -273,9 +278,34 @@ function createDesktopIpcActionFollower({
|
|
|
273
278
|
}
|
|
274
279
|
|
|
275
280
|
rawStatesByThreadId.set(threadId, nextState);
|
|
281
|
+
syncProjectedAssistantDeltas(threadId, baselineState, nextState);
|
|
276
282
|
syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
|
|
277
283
|
}
|
|
278
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
|
+
|
|
279
309
|
return {
|
|
280
310
|
observeInbound,
|
|
281
311
|
stopAll,
|
|
@@ -550,6 +580,93 @@ function projectPendingDesktopActions(threadId, conversationState) {
|
|
|
550
580
|
.filter(Boolean);
|
|
551
581
|
}
|
|
552
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
|
+
|
|
553
670
|
function projectPendingDesktopAction(threadId, request) {
|
|
554
671
|
const requestId = requestIdKey(request.id);
|
|
555
672
|
const method = readString(request.method);
|
|
@@ -572,6 +689,7 @@ function projectPendingDesktopAction(threadId, request) {
|
|
|
572
689
|
method,
|
|
573
690
|
params: {
|
|
574
691
|
...params,
|
|
692
|
+
remodexActionSource: DESKTOP_IPC_ACTION_SOURCE,
|
|
575
693
|
threadId: readString(params.threadId) || readString(params.thread_id) || threadId,
|
|
576
694
|
},
|
|
577
695
|
};
|
|
@@ -674,6 +792,10 @@ function writeFrame(socket, payload, callback) {
|
|
|
674
792
|
}
|
|
675
793
|
|
|
676
794
|
function resolveDefaultIpcSocketPath() {
|
|
795
|
+
if (process.platform === "win32") {
|
|
796
|
+
return "\\\\.\\pipe\\codex-ipc";
|
|
797
|
+
}
|
|
798
|
+
|
|
677
799
|
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
678
800
|
return path.join(os.tmpdir(), "codex-ipc", `ipc-${uid}.sock`);
|
|
679
801
|
}
|
|
@@ -699,6 +821,12 @@ function readString(value) {
|
|
|
699
821
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
700
822
|
}
|
|
701
823
|
|
|
824
|
+
function normalizeToken(value) {
|
|
825
|
+
return typeof value === "string"
|
|
826
|
+
? value.toLowerCase().replace(/[_-\s]+/g, "")
|
|
827
|
+
: "";
|
|
828
|
+
}
|
|
829
|
+
|
|
702
830
|
function cloneJSON(value) {
|
|
703
831
|
return JSON.parse(JSON.stringify(value));
|
|
704
832
|
}
|
|
@@ -715,6 +843,7 @@ module.exports = {
|
|
|
715
843
|
applyConversationStateChange,
|
|
716
844
|
createDesktopIpcActionFollower,
|
|
717
845
|
desktopFollowerPayloadForResponse,
|
|
846
|
+
projectDesktopAssistantDeltaNotifications,
|
|
718
847
|
projectPendingDesktopActions,
|
|
719
848
|
resolveDefaultIpcSocketPath,
|
|
720
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,
|
|
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:
|
|
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 {
|
|
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 =
|
|
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,
|
package/src/project-handler.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Purpose: Serves safe Mac-local project folder discovery and creation requests from the iOS app.
|
|
3
3
|
// Layer: Bridge handler
|
|
4
4
|
// Exports: handleProjectRequest plus testable project filesystem helpers
|
|
5
|
-
// Depends on: fs, os, path
|
|
5
|
+
// Depends on: fs, os, path, ./codex-home
|
|
6
6
|
|
|
7
7
|
const fs = require("fs");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
const path = require("path");
|
|
10
|
+
const { resolveCodexHome } = require("./codex-home");
|
|
10
11
|
|
|
11
12
|
const DEFAULT_DIRECTORY_LIMIT = 200;
|
|
12
13
|
const DEFAULT_DIRECTORY_SEARCH_LIMIT = 80;
|
|
@@ -14,6 +15,14 @@ const DEFAULT_DIRECTORY_SEARCH_MAX_DEPTH = 8;
|
|
|
14
15
|
const DEFAULT_DIRECTORY_SEARCH_MAX_VISITED = 5000;
|
|
15
16
|
const DEFAULT_HIDDEN_DIRECTORY_NAMES = new Set(["Library"]);
|
|
16
17
|
|
|
18
|
+
// Rootless chat slug rules mirror Codex Desktop's `~/Documents/Codex/<DATE>/<slug>`
|
|
19
|
+
// convention so iOS-created Quick Chats land in the same bucket Desktop classifies
|
|
20
|
+
// as projectless.
|
|
21
|
+
const ROOTLESS_CHAT_SLUG_MAX_TOKENS = 6;
|
|
22
|
+
const ROOTLESS_CHAT_SLUG_MAX_LENGTH = 60;
|
|
23
|
+
const ROOTLESS_CHAT_SLUG_FALLBACK = "new-chat";
|
|
24
|
+
const ROOTLESS_CHAT_DEDUP_LIMIT = 50;
|
|
25
|
+
|
|
17
26
|
// ─── ENTRY POINT ─────────────────────────────────────────────
|
|
18
27
|
|
|
19
28
|
function handleProjectRequest(rawMessage, sendResponse) {
|
|
@@ -58,6 +67,8 @@ async function handleProjectMethod(method, params, options = {}) {
|
|
|
58
67
|
switch (method) {
|
|
59
68
|
case "project/quickLocations":
|
|
60
69
|
return projectQuickLocations(options);
|
|
70
|
+
case "project/projectlessRoots":
|
|
71
|
+
return projectProjectlessRoots(options);
|
|
61
72
|
case "project/listDirectory":
|
|
62
73
|
return projectListDirectory(params, options);
|
|
63
74
|
case "project/searchDirectories":
|
|
@@ -66,6 +77,8 @@ async function handleProjectMethod(method, params, options = {}) {
|
|
|
66
77
|
return projectValidatePath(params, options);
|
|
67
78
|
case "project/createDirectory":
|
|
68
79
|
return projectCreateDirectory(params, options);
|
|
80
|
+
case "project/createRootlessChatRoot":
|
|
81
|
+
return projectCreateRootlessChatRoot(params, options);
|
|
69
82
|
default:
|
|
70
83
|
throw projectError("unknown_method", `Unknown project method: ${method}`);
|
|
71
84
|
}
|
|
@@ -99,6 +112,24 @@ async function projectQuickLocations(options = {}) {
|
|
|
99
112
|
return { locations };
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
async function projectProjectlessRoots(options = {}) {
|
|
116
|
+
const homeDir = resolveHomeDir(options);
|
|
117
|
+
const codexHome = path.resolve(readString(options.codexHome) || resolveCodexHome());
|
|
118
|
+
const documentedThreadsRoot = path.join(codexHome, "threads");
|
|
119
|
+
const desktopDocumentsRoot = path.join(homeDir, "Documents", "Codex");
|
|
120
|
+
const roots = uniqueExistingOrCandidatePaths([
|
|
121
|
+
documentedThreadsRoot,
|
|
122
|
+
desktopDocumentsRoot,
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
codexHome,
|
|
127
|
+
roots,
|
|
128
|
+
documentedThreadsRoot,
|
|
129
|
+
desktopDocumentsRoot,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
102
133
|
async function projectListDirectory(params, options = {}) {
|
|
103
134
|
const requestedPath = readString(params.path) || resolveHomeDir(options);
|
|
104
135
|
const directory = await requireUsableDirectory(requestedPath, options);
|
|
@@ -184,6 +215,116 @@ async function projectCreateDirectory(params, options = {}) {
|
|
|
184
215
|
};
|
|
185
216
|
}
|
|
186
217
|
|
|
218
|
+
// Mirrors the Codex Desktop "Chats" convention by materializing a dated rootless
|
|
219
|
+
// chat folder under `~/Documents/Codex/<DATE>/<slug>` (or its Windows equivalent
|
|
220
|
+
// `%USERPROFILE%\Documents\Codex\<DATE>\<slug>`) before the iOS client issues
|
|
221
|
+
// `thread/start`. Without an explicit cwd the app-server falls back to its own
|
|
222
|
+
// process working directory (often the user's home), which would otherwise show
|
|
223
|
+
// up in the sidebar as a project named after the user account.
|
|
224
|
+
async function projectCreateRootlessChatRoot(params = {}, options = {}) {
|
|
225
|
+
const homeDir = resolveHomeDir(options);
|
|
226
|
+
const desktopDocumentsRoot = path.join(homeDir, "Documents", "Codex");
|
|
227
|
+
const dateFolder = readString(params.dateFolder) || formatRootlessChatDate(new Date());
|
|
228
|
+
if (!isISODateFolderName(dateFolder)) {
|
|
229
|
+
throw projectError("invalid_date_folder", "The chat date folder must be in YYYY-MM-DD format.");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const slugBase = rootlessChatSlugFromPromptHint(params.promptHint);
|
|
233
|
+
const dateRootPath = path.join(desktopDocumentsRoot, dateFolder);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await fs.promises.mkdir(dateRootPath, { recursive: true });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw projectError("create_failed", error?.message || "Unable to prepare the Codex chats folder.");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const targetPath = await reserveUniqueRootlessChatPath(dateRootPath, slugBase);
|
|
242
|
+
try {
|
|
243
|
+
await fs.promises.mkdir(targetPath, { recursive: false });
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (error?.code !== "EEXIST") {
|
|
246
|
+
throw projectError("create_failed", error?.message || "Unable to create the rootless chat folder.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const resolvedPath = await safeRealpath(targetPath);
|
|
251
|
+
return {
|
|
252
|
+
path: resolvedPath,
|
|
253
|
+
parentPath: dateRootPath,
|
|
254
|
+
name: path.basename(resolvedPath),
|
|
255
|
+
slug: slugBase,
|
|
256
|
+
dateFolder,
|
|
257
|
+
root: desktopDocumentsRoot,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Codex Desktop generates kebab-case slugs from the first words of the prompt
|
|
262
|
+
// (e.g. "mi-dici-la-pwd-corrente" from "mi dici la pwd corrente?"). We mirror
|
|
263
|
+
// the same shape so that side-by-side users with the desktop app see a
|
|
264
|
+
// consistent chat folder naming scheme.
|
|
265
|
+
function rootlessChatSlugFromPromptHint(rawPromptHint) {
|
|
266
|
+
const hint = typeof rawPromptHint === "string" ? rawPromptHint.normalize("NFKD") : "";
|
|
267
|
+
const sanitized = hint
|
|
268
|
+
.toLowerCase()
|
|
269
|
+
.replace(/[^a-z0-9]+/gu, " ")
|
|
270
|
+
.trim();
|
|
271
|
+
if (!sanitized) {
|
|
272
|
+
return ROOTLESS_CHAT_SLUG_FALLBACK;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const tokens = sanitized
|
|
276
|
+
.split(/\s+/)
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
.slice(0, ROOTLESS_CHAT_SLUG_MAX_TOKENS);
|
|
279
|
+
if (!tokens.length) {
|
|
280
|
+
return ROOTLESS_CHAT_SLUG_FALLBACK;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let slug = tokens.join("-");
|
|
284
|
+
if (slug.length > ROOTLESS_CHAT_SLUG_MAX_LENGTH) {
|
|
285
|
+
slug = slug.slice(0, ROOTLESS_CHAT_SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
286
|
+
}
|
|
287
|
+
return slug || ROOTLESS_CHAT_SLUG_FALLBACK;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Appends "-2", "-3", … so two rapid-fire chats with the same first words
|
|
291
|
+
// keep distinct folders instead of fighting over the same path.
|
|
292
|
+
async function reserveUniqueRootlessChatPath(parentDirectory, slugBase) {
|
|
293
|
+
for (let attempt = 0; attempt < ROOTLESS_CHAT_DEDUP_LIMIT; attempt += 1) {
|
|
294
|
+
const candidateSlug = attempt === 0 ? slugBase : `${slugBase}-${attempt + 1}`;
|
|
295
|
+
const candidatePath = path.join(parentDirectory, candidateSlug);
|
|
296
|
+
try {
|
|
297
|
+
await fs.promises.access(candidatePath, fs.constants.F_OK);
|
|
298
|
+
} catch {
|
|
299
|
+
return candidatePath;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return path.join(parentDirectory, `${slugBase}-${Date.now()}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function formatRootlessChatDate(date) {
|
|
307
|
+
const year = date.getFullYear().toString().padStart(4, "0");
|
|
308
|
+
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
309
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
310
|
+
return `${year}-${month}-${day}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function isISODateFolderName(value) {
|
|
314
|
+
if (typeof value !== "string" || value.length !== 10) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return /^\d{4}-\d{2}-\d{2}$/u.test(value);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function safeRealpath(candidatePath) {
|
|
321
|
+
try {
|
|
322
|
+
return await fs.promises.realpath(candidatePath);
|
|
323
|
+
} catch {
|
|
324
|
+
return path.resolve(candidatePath);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
187
328
|
// ─── Filesystem Helpers ──────────────────────────────────────
|
|
188
329
|
|
|
189
330
|
async function readDirectoryEntries(directoryPath, options = {}) {
|
|
@@ -462,6 +603,23 @@ function resolveHomeDir(options = {}) {
|
|
|
462
603
|
return options.homeDir || os.homedir();
|
|
463
604
|
}
|
|
464
605
|
|
|
606
|
+
function uniqueExistingOrCandidatePaths(paths) {
|
|
607
|
+
const seen = new Set();
|
|
608
|
+
const result = [];
|
|
609
|
+
|
|
610
|
+
for (const candidatePath of paths) {
|
|
611
|
+
const normalizedPath = path.resolve(candidatePath);
|
|
612
|
+
const realPath = realpathSyncIfAvailable(normalizedPath) || normalizedPath;
|
|
613
|
+
if (seen.has(realPath)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
seen.add(realPath);
|
|
617
|
+
result.push(realPath);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
|
|
465
623
|
function realpathSyncIfAvailable(candidatePath) {
|
|
466
624
|
try {
|
|
467
625
|
return fs.realpathSync(candidatePath);
|
|
@@ -485,9 +643,12 @@ module.exports = {
|
|
|
485
643
|
handleProjectRequest,
|
|
486
644
|
handleProjectMethod,
|
|
487
645
|
projectQuickLocations,
|
|
646
|
+
projectProjectlessRoots,
|
|
488
647
|
projectListDirectory,
|
|
489
648
|
projectSearchDirectories,
|
|
490
649
|
projectValidatePath,
|
|
491
650
|
projectCreateDirectory,
|
|
651
|
+
projectCreateRootlessChatRoot,
|
|
492
652
|
validateDirectory,
|
|
653
|
+
rootlessChatSlugFromPromptHint,
|
|
493
654
|
};
|