@makerbi/remodex 1.3.10 → 1.4.1
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 +1 -0
- package/package.json +2 -2
- package/src/bridge.js +29 -0
- package/src/codex-desktop-refresher.js +1 -0
- package/src/desktop-handler.js +100 -11
- package/src/desktop-ipc-action-follower.js +648 -0
- package/src/git-handler.js +322 -9
- package/src/pet-handler.js +537 -0
- package/src/qr.js +21 -2
- package/src/workspace-handler.js +31 -4
- package/src/private-defaults.json +0 -4
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
// FILE: desktop-ipc-action-follower.js
|
|
2
|
+
// Purpose: Mirrors live Codex Desktop IPC pending actions to the phone and routes replies back to the desktop runtime.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: createDesktopIpcActionFollower, projectPendingDesktopActions
|
|
5
|
+
// Depends on: net, os, path
|
|
6
|
+
|
|
7
|
+
const net = require("net");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const FRAME_HEADER_BYTES = 4;
|
|
12
|
+
const MAX_FRAME_BYTES = 256 * 1024 * 1024;
|
|
13
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
14
|
+
const DESKTOP_RESUME_METHODS = new Set(["thread/read", "thread/resume"]);
|
|
15
|
+
const ACTION_METHODS = new Set([
|
|
16
|
+
"item/commandExecution/requestApproval",
|
|
17
|
+
"item/fileChange/requestApproval",
|
|
18
|
+
"item/fileRead/requestApproval",
|
|
19
|
+
"item/tool/requestUserInput",
|
|
20
|
+
]);
|
|
21
|
+
const REPLY_METHOD_BY_ACTION_METHOD = new Map([
|
|
22
|
+
["item/commandExecution/requestApproval", "thread-follower-command-approval-decision"],
|
|
23
|
+
["item/fileChange/requestApproval", "thread-follower-file-approval-decision"],
|
|
24
|
+
["item/fileRead/requestApproval", "thread-follower-file-approval-decision"],
|
|
25
|
+
["item/tool/requestUserInput", "thread-follower-submit-user-input"],
|
|
26
|
+
]);
|
|
27
|
+
const METHOD_VERSION_BY_NAME = new Map([
|
|
28
|
+
["initialize", 1],
|
|
29
|
+
["thread-follower-command-approval-decision", 1],
|
|
30
|
+
["thread-follower-file-approval-decision", 1],
|
|
31
|
+
["thread-follower-submit-user-input", 1],
|
|
32
|
+
]);
|
|
33
|
+
const APPROVAL_DECISIONS = new Set(["accept", "acceptForSession", "decline", "cancel"]);
|
|
34
|
+
|
|
35
|
+
// Opens the Desktop IPC bus on demand and exposes Mac-owned pending actions as normal app-server requests.
|
|
36
|
+
function createDesktopIpcActionFollower({
|
|
37
|
+
sendApplicationResponse,
|
|
38
|
+
readConversationState = null,
|
|
39
|
+
logPrefix = "[remodex]",
|
|
40
|
+
socketPath = resolveDefaultIpcSocketPath(),
|
|
41
|
+
netModule = net,
|
|
42
|
+
now = () => Date.now(),
|
|
43
|
+
requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const ipc = createDesktopIpcClient({
|
|
46
|
+
socketPath,
|
|
47
|
+
netModule,
|
|
48
|
+
now,
|
|
49
|
+
requestTimeoutMs,
|
|
50
|
+
logPrefix,
|
|
51
|
+
onEnvelope,
|
|
52
|
+
onDisconnect,
|
|
53
|
+
});
|
|
54
|
+
const rawStatesByThreadId = new Map();
|
|
55
|
+
const pendingRoutesByRequestId = new Map();
|
|
56
|
+
const activeThreadIds = new Set();
|
|
57
|
+
const recoveringThreadIds = new Set();
|
|
58
|
+
const queuedChangesByThreadId = new Map();
|
|
59
|
+
|
|
60
|
+
function observeInbound(rawMessage) {
|
|
61
|
+
const message = safeParseJSON(rawMessage);
|
|
62
|
+
const responseRoute = desktopRouteForResponse(message);
|
|
63
|
+
if (responseRoute) {
|
|
64
|
+
submitDesktopActionResponse(responseRoute, message);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const method = readString(message?.method);
|
|
69
|
+
if (!DESKTOP_RESUME_METHODS.has(method)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const threadId = readThreadId(message?.params);
|
|
74
|
+
if (!threadId) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
activeThreadIds.add(threadId);
|
|
79
|
+
ipc.ensureConnected();
|
|
80
|
+
recoverThreadBaseline(threadId);
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stopAll() {
|
|
85
|
+
rawStatesByThreadId.clear();
|
|
86
|
+
pendingRoutesByRequestId.clear();
|
|
87
|
+
activeThreadIds.clear();
|
|
88
|
+
recoveringThreadIds.clear();
|
|
89
|
+
queuedChangesByThreadId.clear();
|
|
90
|
+
ipc.close();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Desktop broadcasts carry the live conversation state Litter projects from.
|
|
94
|
+
function onEnvelope(envelope) {
|
|
95
|
+
if (envelope?.type !== "broadcast" || envelope.method !== "thread-stream-state-changed") {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const params = envelope.params || {};
|
|
100
|
+
const threadId = readString(params.conversationId) || readString(params.conversation_id);
|
|
101
|
+
if (!threadId || !activeThreadIds.has(threadId)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (recoveringThreadIds.has(threadId)) {
|
|
106
|
+
queueThreadChange(threadId, params.change);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const previousState = rawStatesByThreadId.get(threadId) || null;
|
|
111
|
+
const nextState = applyConversationStateChange(previousState, params.change);
|
|
112
|
+
if (!nextState) {
|
|
113
|
+
if (isPatchChange(params.change)) {
|
|
114
|
+
queueThreadChange(threadId, params.change);
|
|
115
|
+
recoverThreadBaseline(threadId);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
rawStatesByThreadId.set(threadId, nextState);
|
|
121
|
+
syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onDisconnect() {
|
|
125
|
+
rawStatesByThreadId.clear();
|
|
126
|
+
pendingRoutesByRequestId.clear();
|
|
127
|
+
recoveringThreadIds.clear();
|
|
128
|
+
queuedChangesByThreadId.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function syncProjectedActions(threadId, actions) {
|
|
132
|
+
const nextRequestIds = new Set(actions.map((action) => action.id));
|
|
133
|
+
for (const [requestId, route] of Array.from(pendingRoutesByRequestId.entries())) {
|
|
134
|
+
if (route.threadId !== threadId || nextRequestIds.has(requestId)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pendingRoutesByRequestId.delete(requestId);
|
|
139
|
+
sendApplicationResponse(JSON.stringify({
|
|
140
|
+
method: "serverRequest/resolved",
|
|
141
|
+
params: {
|
|
142
|
+
threadId,
|
|
143
|
+
requestId,
|
|
144
|
+
},
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const action of actions) {
|
|
149
|
+
if (pendingRoutesByRequestId.has(action.id)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pendingRoutesByRequestId.set(action.id, {
|
|
154
|
+
requestId: action.id,
|
|
155
|
+
method: action.method,
|
|
156
|
+
threadId,
|
|
157
|
+
});
|
|
158
|
+
sendApplicationResponse(JSON.stringify({
|
|
159
|
+
id: action.id,
|
|
160
|
+
method: action.method,
|
|
161
|
+
params: action.params,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function desktopRouteForResponse(message) {
|
|
167
|
+
if (!message || typeof message !== "object" || message.method) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const requestId = requestIdKey(message.id);
|
|
172
|
+
return requestId ? pendingRoutesByRequestId.get(requestId) || null : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function submitDesktopActionResponse(route, responseMessage) {
|
|
176
|
+
const payload = desktopFollowerPayloadForResponse(route, responseMessage);
|
|
177
|
+
if (!payload) {
|
|
178
|
+
sendApplicationResponse(JSON.stringify({
|
|
179
|
+
id: responseMessage?.id ?? route.requestId,
|
|
180
|
+
error: {
|
|
181
|
+
code: -32602,
|
|
182
|
+
message: "Invalid desktop action response.",
|
|
183
|
+
},
|
|
184
|
+
}));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
ipc.sendRequest(payload.method, payload.params)
|
|
189
|
+
.then(() => {
|
|
190
|
+
pendingRoutesByRequestId.delete(route.requestId);
|
|
191
|
+
sendApplicationResponse(JSON.stringify({
|
|
192
|
+
method: "serverRequest/resolved",
|
|
193
|
+
params: {
|
|
194
|
+
threadId: route.threadId,
|
|
195
|
+
requestId: route.requestId,
|
|
196
|
+
},
|
|
197
|
+
}));
|
|
198
|
+
})
|
|
199
|
+
.catch((error) => {
|
|
200
|
+
console.warn(`${logPrefix} desktop action reply failed for ${route.threadId}: ${error.message}`);
|
|
201
|
+
sendApplicationResponse(JSON.stringify({
|
|
202
|
+
id: responseMessage.id,
|
|
203
|
+
error: {
|
|
204
|
+
code: -32000,
|
|
205
|
+
message: "Could not send this action to Codex on the Mac.",
|
|
206
|
+
},
|
|
207
|
+
}));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function queueThreadChange(threadId, change) {
|
|
212
|
+
if (!change || typeof change !== "object") {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const queuedChanges = queuedChangesByThreadId.get(threadId) || [];
|
|
217
|
+
queuedChanges.push(change);
|
|
218
|
+
queuedChangesByThreadId.set(threadId, queuedChanges);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function recoverThreadBaseline(threadId) {
|
|
222
|
+
if (typeof readConversationState !== "function"
|
|
223
|
+
|| recoveringThreadIds.has(threadId)
|
|
224
|
+
|| rawStatesByThreadId.has(threadId)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
recoveringThreadIds.add(threadId);
|
|
229
|
+
Promise.resolve()
|
|
230
|
+
.then(() => readConversationState(threadId))
|
|
231
|
+
.then((baselineState) => {
|
|
232
|
+
if (!baselineState || typeof baselineState !== "object") {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let nextState = cloneJSON(baselineState);
|
|
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));
|
|
245
|
+
})
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
console.warn(`${logPrefix} desktop IPC baseline recovery failed for ${threadId}: ${error.message}`);
|
|
248
|
+
})
|
|
249
|
+
.finally(() => {
|
|
250
|
+
recoveringThreadIds.delete(threadId);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
observeInbound,
|
|
256
|
+
stopAll,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Minimal IPC client for Litter's length-prefixed Codex desktop bus.
|
|
261
|
+
function createDesktopIpcClient({
|
|
262
|
+
socketPath,
|
|
263
|
+
netModule,
|
|
264
|
+
now,
|
|
265
|
+
requestTimeoutMs,
|
|
266
|
+
logPrefix,
|
|
267
|
+
onEnvelope,
|
|
268
|
+
onDisconnect,
|
|
269
|
+
}) {
|
|
270
|
+
let socket = null;
|
|
271
|
+
let clientId = "";
|
|
272
|
+
let isConnecting = false;
|
|
273
|
+
let readBuffer = Buffer.alloc(0);
|
|
274
|
+
const pendingRequests = new Map();
|
|
275
|
+
|
|
276
|
+
function ensureConnected() {
|
|
277
|
+
if (socket || isConnecting) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
isConnecting = true;
|
|
282
|
+
const nextSocket = netModule.createConnection(socketPath);
|
|
283
|
+
socket = nextSocket;
|
|
284
|
+
|
|
285
|
+
nextSocket.on("connect", () => {
|
|
286
|
+
isConnecting = false;
|
|
287
|
+
sendRequest("initialize", { clientType: "remodex-bridge" })
|
|
288
|
+
.then((result) => {
|
|
289
|
+
clientId = readString(result?.clientId) || clientId;
|
|
290
|
+
})
|
|
291
|
+
.catch((error) => {
|
|
292
|
+
console.warn(`${logPrefix} desktop IPC initialize failed: ${error.message}`);
|
|
293
|
+
close();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
nextSocket.on("data", handleData);
|
|
297
|
+
nextSocket.on("close", handleClose);
|
|
298
|
+
nextSocket.on("error", (error) => {
|
|
299
|
+
if (error?.code !== "ENOENT" && error?.code !== "ECONNREFUSED") {
|
|
300
|
+
console.warn(`${logPrefix} desktop IPC connection failed: ${error.message}`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function sendRequest(method, params) {
|
|
306
|
+
ensureConnected();
|
|
307
|
+
if (!socket || socket.destroyed) {
|
|
308
|
+
return Promise.reject(new Error("Desktop IPC is not connected."));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const requestId = `remodex-${now().toString(36)}-${Math.random().toString(16).slice(2)}`;
|
|
312
|
+
const envelope = {
|
|
313
|
+
type: "request",
|
|
314
|
+
requestId,
|
|
315
|
+
sourceClientId: method === "initialize" ? "initializing-client" : clientId || "remodex-bridge",
|
|
316
|
+
version: METHOD_VERSION_BY_NAME.get(method) || 1,
|
|
317
|
+
method,
|
|
318
|
+
params: params || {},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
const timeout = setTimeout(() => {
|
|
323
|
+
pendingRequests.delete(requestId);
|
|
324
|
+
reject(new Error(`Desktop IPC request timed out: ${method}`));
|
|
325
|
+
}, requestTimeoutMs);
|
|
326
|
+
timeout.unref?.();
|
|
327
|
+
|
|
328
|
+
pendingRequests.set(requestId, {
|
|
329
|
+
method,
|
|
330
|
+
resolve,
|
|
331
|
+
reject,
|
|
332
|
+
timeout,
|
|
333
|
+
});
|
|
334
|
+
writeFrame(socket, JSON.stringify(envelope), (error) => {
|
|
335
|
+
if (!error) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
clearTimeout(timeout);
|
|
340
|
+
pendingRequests.delete(requestId);
|
|
341
|
+
reject(error);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function handleData(chunk) {
|
|
347
|
+
readBuffer = Buffer.concat([readBuffer, chunk]);
|
|
348
|
+
while (readBuffer.length >= FRAME_HEADER_BYTES) {
|
|
349
|
+
const frameLength = readBuffer.readUInt32LE(0);
|
|
350
|
+
if (frameLength > MAX_FRAME_BYTES) {
|
|
351
|
+
close();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (readBuffer.length < FRAME_HEADER_BYTES + frameLength) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const payload = readBuffer.slice(FRAME_HEADER_BYTES, FRAME_HEADER_BYTES + frameLength).toString("utf8");
|
|
359
|
+
readBuffer = readBuffer.slice(FRAME_HEADER_BYTES + frameLength);
|
|
360
|
+
const envelope = safeParseJSON(payload);
|
|
361
|
+
if (envelope) {
|
|
362
|
+
dispatchEnvelope(envelope);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function dispatchEnvelope(envelope) {
|
|
368
|
+
if (envelope.type === "client-discovery-request") {
|
|
369
|
+
writeEnvelope({
|
|
370
|
+
type: "client-discovery-response",
|
|
371
|
+
requestId: envelope.requestId,
|
|
372
|
+
response: {
|
|
373
|
+
canHandle: false,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (envelope.type === "response") {
|
|
380
|
+
const requestId = requestIdKey(envelope.requestId);
|
|
381
|
+
const waiter = requestId ? pendingRequests.get(requestId) : null;
|
|
382
|
+
if (!waiter) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
pendingRequests.delete(requestId);
|
|
387
|
+
clearTimeout(waiter.timeout);
|
|
388
|
+
if (envelope.resultType === "error") {
|
|
389
|
+
waiter.reject(new Error(envelope.error || `Desktop IPC request failed: ${waiter.method}`));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
waiter.resolve(envelope.result ?? null);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
onEnvelope(envelope);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function handleClose() {
|
|
401
|
+
socket = null;
|
|
402
|
+
clientId = "";
|
|
403
|
+
isConnecting = false;
|
|
404
|
+
readBuffer = Buffer.alloc(0);
|
|
405
|
+
for (const waiter of pendingRequests.values()) {
|
|
406
|
+
clearTimeout(waiter.timeout);
|
|
407
|
+
waiter.reject(new Error("Desktop IPC connection closed."));
|
|
408
|
+
}
|
|
409
|
+
pendingRequests.clear();
|
|
410
|
+
onDisconnect();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function close() {
|
|
414
|
+
if (!socket) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const nextSocket = socket;
|
|
419
|
+
socket = null;
|
|
420
|
+
nextSocket.destroy();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function writeEnvelope(envelope, callback = () => {}) {
|
|
424
|
+
if (!socket || socket.destroyed) {
|
|
425
|
+
callback(new Error("Desktop IPC is not connected."));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
writeFrame(socket, JSON.stringify(envelope), callback);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
ensureConnected,
|
|
434
|
+
sendRequest,
|
|
435
|
+
close,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
440
|
+
const method = REPLY_METHOD_BY_ACTION_METHOD.get(route.method);
|
|
441
|
+
if (!method || responseMessage?.error) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (route.method === "item/tool/requestUserInput") {
|
|
446
|
+
const answers = responseMessage?.result?.answers;
|
|
447
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
method,
|
|
453
|
+
params: {
|
|
454
|
+
conversationId: route.threadId,
|
|
455
|
+
requestId: route.requestId,
|
|
456
|
+
response: {
|
|
457
|
+
answers,
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const decision = readString(responseMessage?.result?.decision);
|
|
464
|
+
if (!APPROVAL_DECISIONS.has(decision)) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
method,
|
|
470
|
+
params: {
|
|
471
|
+
conversationId: route.threadId,
|
|
472
|
+
requestId: route.requestId,
|
|
473
|
+
decision,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function projectPendingDesktopActions(threadId, conversationState) {
|
|
479
|
+
const requests = Array.isArray(conversationState?.requests) ? conversationState.requests : [];
|
|
480
|
+
return requests
|
|
481
|
+
.filter((request) => request && request.completed !== true)
|
|
482
|
+
.filter((request) => ACTION_METHODS.has(readString(request.method)))
|
|
483
|
+
.map((request) => projectPendingDesktopAction(threadId, request))
|
|
484
|
+
.filter(Boolean);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function projectPendingDesktopAction(threadId, request) {
|
|
488
|
+
const requestId = requestIdKey(request.id);
|
|
489
|
+
const method = readString(request.method);
|
|
490
|
+
const params = request.params && typeof request.params === "object" && !Array.isArray(request.params)
|
|
491
|
+
? request.params
|
|
492
|
+
: {};
|
|
493
|
+
if (!requestId || !method) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (method === "item/tool/requestUserInput") {
|
|
498
|
+
const questions = Array.isArray(params.questions) ? params.questions : [];
|
|
499
|
+
if (questions.length === 0) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
id: requestId,
|
|
506
|
+
method,
|
|
507
|
+
params: {
|
|
508
|
+
...params,
|
|
509
|
+
threadId: readString(params.threadId) || readString(params.thread_id) || threadId,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function applyConversationStateChange(previousState, change) {
|
|
515
|
+
if (!change || typeof change !== "object") {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (change.type === "snapshot" || change.type === "Snapshot") {
|
|
520
|
+
return cloneJSON(change.conversationState || change.conversation_state || {});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (change.type !== "patches" && change.type !== "Patches") {
|
|
524
|
+
return previousState || null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const patches = Array.isArray(change.patches) ? change.patches : [];
|
|
528
|
+
if (!previousState || patches.length === 0) {
|
|
529
|
+
return previousState || null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const nextState = cloneJSON(previousState);
|
|
533
|
+
for (const patch of patches) {
|
|
534
|
+
applyImmerPatch(nextState, patch);
|
|
535
|
+
}
|
|
536
|
+
return nextState;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isPatchChange(change) {
|
|
540
|
+
return change?.type === "patches" || change?.type === "Patches";
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function seedConversationStateFromThreadRead(response) {
|
|
544
|
+
const conversationState = response?.conversationState || response?.conversation_state;
|
|
545
|
+
if (conversationState && typeof conversationState === "object" && !Array.isArray(conversationState)) {
|
|
546
|
+
return cloneJSON(conversationState);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const thread = response?.thread && typeof response.thread === "object" && !Array.isArray(response.thread)
|
|
550
|
+
? response.thread
|
|
551
|
+
: {};
|
|
552
|
+
return {
|
|
553
|
+
turns: Array.isArray(thread.turns) ? cloneJSON(thread.turns) : [],
|
|
554
|
+
requests: Array.isArray(thread.requests) ? cloneJSON(thread.requests) : [],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function applyImmerPatch(target, patch) {
|
|
559
|
+
const patchPath = Array.isArray(patch?.path) ? patch.path : [];
|
|
560
|
+
const op = readString(patch?.op).toLowerCase();
|
|
561
|
+
if (!op || patchPath.length === 0) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
let parent = target;
|
|
566
|
+
for (let index = 0; index < patchPath.length - 1; index += 1) {
|
|
567
|
+
parent = parent?.[patchPath[index]];
|
|
568
|
+
if (parent == null) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const key = patchPath[patchPath.length - 1];
|
|
574
|
+
if (op === "remove") {
|
|
575
|
+
if (Array.isArray(parent) && Number.isInteger(key)) {
|
|
576
|
+
parent.splice(key, 1);
|
|
577
|
+
} else if (parent && typeof parent === "object") {
|
|
578
|
+
delete parent[key];
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (op === "add" || op === "replace") {
|
|
584
|
+
if (Array.isArray(parent) && Number.isInteger(key)) {
|
|
585
|
+
if (op === "add") {
|
|
586
|
+
parent.splice(key, 0, patch.value);
|
|
587
|
+
} else {
|
|
588
|
+
parent[key] = patch.value;
|
|
589
|
+
}
|
|
590
|
+
} else if (parent && typeof parent === "object") {
|
|
591
|
+
parent[key] = patch.value;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function writeFrame(socket, payload, callback) {
|
|
597
|
+
const body = Buffer.from(payload, "utf8");
|
|
598
|
+
const header = Buffer.alloc(FRAME_HEADER_BYTES);
|
|
599
|
+
header.writeUInt32LE(body.length, 0);
|
|
600
|
+
socket.write(Buffer.concat([header, body]), callback);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function resolveDefaultIpcSocketPath() {
|
|
604
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
605
|
+
return path.join(os.tmpdir(), "codex-ipc", `ipc-${uid}.sock`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function readThreadId(params) {
|
|
609
|
+
return readString(params?.threadId)
|
|
610
|
+
|| readString(params?.thread_id)
|
|
611
|
+
|| readString(params?.conversationId)
|
|
612
|
+
|| readString(params?.conversation_id);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function requestIdKey(value) {
|
|
616
|
+
if (typeof value === "string" && value) {
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
620
|
+
return String(value);
|
|
621
|
+
}
|
|
622
|
+
return "";
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function readString(value) {
|
|
626
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function cloneJSON(value) {
|
|
630
|
+
return JSON.parse(JSON.stringify(value));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function safeParseJSON(value) {
|
|
634
|
+
try {
|
|
635
|
+
return JSON.parse(value);
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
module.exports = {
|
|
642
|
+
applyConversationStateChange,
|
|
643
|
+
createDesktopIpcActionFollower,
|
|
644
|
+
desktopFollowerPayloadForResponse,
|
|
645
|
+
projectPendingDesktopActions,
|
|
646
|
+
resolveDefaultIpcSocketPath,
|
|
647
|
+
seedConversationStateFromThreadRead,
|
|
648
|
+
};
|