@makerbi/remodex 1.5.8 → 2.0.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/bin/remodex.js +56 -5
- package/package.json +1 -1
- package/src/bridge.js +85 -2
- package/src/git-handler.js +92 -28
- package/src/ios-app-compatibility.js +7 -7
- package/src/macos-launch-agent.js +46 -1
- package/src/qr.js +2 -5
- package/src/secure-device-state.js +41 -2
- package/src/secure-transport.js +7 -0
- package/src/session-jsonl-history.js +54 -1
- package/src/voice-handler.js +33 -3
package/bin/remodex.js
CHANGED
|
@@ -120,7 +120,7 @@ async function main({
|
|
|
120
120
|
ok: true,
|
|
121
121
|
currentVersion: version,
|
|
122
122
|
plistPath: result?.plistPath,
|
|
123
|
-
pairingSession: result?.pairingSession,
|
|
123
|
+
pairingSession: sanitizePairingSessionForOutput(result?.pairingSession),
|
|
124
124
|
},
|
|
125
125
|
message: "[remodex] macOS bridge service is running.",
|
|
126
126
|
jsonOutput,
|
|
@@ -141,7 +141,7 @@ async function main({
|
|
|
141
141
|
ok: true,
|
|
142
142
|
currentVersion: version,
|
|
143
143
|
plistPath: result?.plistPath,
|
|
144
|
-
pairingSession: result?.pairingSession,
|
|
144
|
+
pairingSession: sanitizePairingSessionForOutput(result?.pairingSession),
|
|
145
145
|
},
|
|
146
146
|
message: "[remodex] macOS bridge service restarted.",
|
|
147
147
|
jsonOutput,
|
|
@@ -156,10 +156,20 @@ async function main({
|
|
|
156
156
|
consoleImpl,
|
|
157
157
|
exitImpl,
|
|
158
158
|
});
|
|
159
|
-
consoleImpl.log("[remodex] Refreshing bridge pairing QR...");
|
|
160
159
|
const result = await deps.startMacOSBridgeService({
|
|
161
160
|
waitForPairing: true,
|
|
162
161
|
});
|
|
162
|
+
if (jsonOutput) {
|
|
163
|
+
emitJson({
|
|
164
|
+
ok: true,
|
|
165
|
+
currentVersion: version,
|
|
166
|
+
plistPath: result?.plistPath,
|
|
167
|
+
pairingSession: result?.pairingSession,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
consoleImpl.log("[remodex] Refreshing bridge pairing QR...");
|
|
163
173
|
deps.printMacOSBridgePairingQr({
|
|
164
174
|
pairingSession: result.pairingSession,
|
|
165
175
|
});
|
|
@@ -193,7 +203,7 @@ async function main({
|
|
|
193
203
|
});
|
|
194
204
|
if (jsonOutput) {
|
|
195
205
|
emitJson({
|
|
196
|
-
...deps.getMacOSBridgeServiceStatus(),
|
|
206
|
+
...sanitizeBridgeServiceStatusForOutput(deps.getMacOSBridgeServiceStatus()),
|
|
197
207
|
currentVersion: version,
|
|
198
208
|
});
|
|
199
209
|
return;
|
|
@@ -272,7 +282,7 @@ async function main({
|
|
|
272
282
|
"Usage: remodex up | remodex run [--extra-device|--extra-devices=N] | remodex start | remodex restart | "
|
|
273
283
|
+ "remodex qr | remodex pair | remodex stop | remodex status | "
|
|
274
284
|
+ "remodex reset-pairing | remodex resume | remodex watch [threadId] | remodex --version | "
|
|
275
|
-
+ "append --json to start/restart/stop/status/reset-pairing/resume for machine-readable output"
|
|
285
|
+
+ "append --json to start/restart/qr/pair/stop/status/reset-pairing/resume for machine-readable output"
|
|
276
286
|
);
|
|
277
287
|
exitImpl(1);
|
|
278
288
|
}
|
|
@@ -361,6 +371,47 @@ function emitJson(payload) {
|
|
|
361
371
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
362
372
|
}
|
|
363
373
|
|
|
374
|
+
// Keeps machine-readable CLI output useful without exposing relay endpoints or live pairing payloads.
|
|
375
|
+
function sanitizeBridgeServiceStatusForOutput(status = {}) {
|
|
376
|
+
const sanitized = {
|
|
377
|
+
...status,
|
|
378
|
+
daemonConfig: sanitizeDaemonConfigForOutput(status.daemonConfig),
|
|
379
|
+
pairingSession: sanitizePairingSessionForOutput(status.pairingSession),
|
|
380
|
+
};
|
|
381
|
+
return sanitized;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function sanitizeDaemonConfigForOutput(config) {
|
|
385
|
+
if (!config || typeof config !== "object") {
|
|
386
|
+
return config || null;
|
|
387
|
+
}
|
|
388
|
+
const { relayUrl, pushServiceUrl, ...rest } = config;
|
|
389
|
+
return {
|
|
390
|
+
...rest,
|
|
391
|
+
relayConfigured: Boolean(relayUrl),
|
|
392
|
+
pushServiceConfigured: Boolean(pushServiceUrl),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function sanitizePairingSessionForOutput(pairingSession) {
|
|
397
|
+
if (!pairingSession || typeof pairingSession !== "object") {
|
|
398
|
+
return pairingSession || null;
|
|
399
|
+
}
|
|
400
|
+
const payload = pairingSession.pairingPayload || {};
|
|
401
|
+
return {
|
|
402
|
+
createdAt: pairingSession.createdAt,
|
|
403
|
+
pairingCode: pairingSession.pairingCode,
|
|
404
|
+
pairingPayload: {
|
|
405
|
+
v: payload.v,
|
|
406
|
+
expiresAt: payload.expiresAt,
|
|
407
|
+
hasRelay: Boolean(payload.relay),
|
|
408
|
+
hasSessionId: Boolean(payload.sessionId),
|
|
409
|
+
hasMacIdentityPublicKey: Boolean(payload.macIdentityPublicKey),
|
|
410
|
+
displayName: payload.displayName,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
364
415
|
function assertMacOSCommand(name, {
|
|
365
416
|
platform = process.platform,
|
|
366
417
|
consoleImpl = console,
|
package/package.json
CHANGED
package/src/bridge.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Depends on: ws, crypto, os, ./bridge-status, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler
|
|
6
6
|
|
|
7
7
|
const WebSocket = require("ws");
|
|
8
|
-
const { randomBytes, randomUUID } = require("crypto");
|
|
8
|
+
const { createHash, randomBytes, randomUUID } = require("crypto");
|
|
9
9
|
const { execFile, spawn } = require("child_process");
|
|
10
10
|
const fs = require("fs");
|
|
11
11
|
const path = require("path");
|
|
@@ -46,6 +46,7 @@ const { createPushNotificationTracker } = require("./push-notification-tracker")
|
|
|
46
46
|
const { resolveCodexGeneratedImagesRoot } = require("./codex-home");
|
|
47
47
|
const {
|
|
48
48
|
loadOrCreateBridgeDeviceState,
|
|
49
|
+
rememberLastSeenClientDeviceKind,
|
|
49
50
|
rememberLastSeenPhoneAppVersion,
|
|
50
51
|
resolveBridgeRelaySession,
|
|
51
52
|
} = require("./secure-device-state");
|
|
@@ -238,6 +239,7 @@ function startBridge({
|
|
|
238
239
|
requestId: null,
|
|
239
240
|
startedAt: 0,
|
|
240
241
|
};
|
|
242
|
+
let activePhoneSummary = null;
|
|
241
243
|
const secureTransport = createBridgeSecureTransport({
|
|
242
244
|
sessionId,
|
|
243
245
|
relayUrl: relayBaseUrl,
|
|
@@ -247,6 +249,13 @@ function startBridge({
|
|
|
247
249
|
deviceState = nextDeviceState;
|
|
248
250
|
sendRelayRegistrationUpdate(nextDeviceState);
|
|
249
251
|
},
|
|
252
|
+
onSecureSessionReady(session) {
|
|
253
|
+
activePhoneSummary = buildActivePhoneSummary(session, deviceState);
|
|
254
|
+
const lastPublishedBridgeStatus = bridgeStatusPublisher.latest();
|
|
255
|
+
if (lastPublishedBridgeStatus) {
|
|
256
|
+
publishBridgeStatus(lastPublishedBridgeStatus);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
250
259
|
});
|
|
251
260
|
let primaryRelayChannel = null;
|
|
252
261
|
// Keeps one stable sender identity across reconnects so buffered replay state
|
|
@@ -420,6 +429,9 @@ function startBridge({
|
|
|
420
429
|
|
|
421
430
|
lastConnectionStatus = status;
|
|
422
431
|
lastConnectionError = lastError;
|
|
432
|
+
if (status !== "connected") {
|
|
433
|
+
activePhoneSummary = null;
|
|
434
|
+
}
|
|
423
435
|
publishBridgeStatus({
|
|
424
436
|
state: "running",
|
|
425
437
|
connectionStatus: status,
|
|
@@ -1529,6 +1541,20 @@ function startBridge({
|
|
|
1529
1541
|
function bridgeManagedInitializeCompatibilityError(params) {
|
|
1530
1542
|
const clientInfo = params && typeof params === "object" ? params.clientInfo : null;
|
|
1531
1543
|
const clientName = normalizeNonEmptyString(clientInfo?.name);
|
|
1544
|
+
const clientDeviceKind = classifyClientDeviceKind(clientName);
|
|
1545
|
+
if (clientDeviceKind) {
|
|
1546
|
+
deviceState = rememberLastSeenClientDeviceKind(deviceState, clientDeviceKind);
|
|
1547
|
+
if (activePhoneSummary?.connected) {
|
|
1548
|
+
activePhoneSummary = {
|
|
1549
|
+
...activePhoneSummary,
|
|
1550
|
+
deviceKind: clientDeviceKind,
|
|
1551
|
+
};
|
|
1552
|
+
const lastPublishedBridgeStatus = bridgeStatusPublisher.latest();
|
|
1553
|
+
if (lastPublishedBridgeStatus) {
|
|
1554
|
+
publishBridgeStatus(lastPublishedBridgeStatus);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1532
1558
|
if (clientName !== "codexmobile_ios") {
|
|
1533
1559
|
return null;
|
|
1534
1560
|
}
|
|
@@ -1697,7 +1723,11 @@ function startBridge({
|
|
|
1697
1723
|
}
|
|
1698
1724
|
|
|
1699
1725
|
function publishBridgeStatus(status) {
|
|
1700
|
-
bridgeStatusPublisher.publish(
|
|
1726
|
+
bridgeStatusPublisher.publish({
|
|
1727
|
+
...status,
|
|
1728
|
+
activeDevice: activePhoneSummary,
|
|
1729
|
+
activePhone: activePhoneSummary,
|
|
1730
|
+
});
|
|
1701
1731
|
}
|
|
1702
1732
|
|
|
1703
1733
|
// Refreshes the relay's trusted-mac index after the QR bootstrap locks in a phone identity.
|
|
@@ -1950,6 +1980,47 @@ function buildMacRegistration(deviceState, pairingSession) {
|
|
|
1950
1980
|
};
|
|
1951
1981
|
}
|
|
1952
1982
|
|
|
1983
|
+
function buildActivePhoneSummary(session, deviceState = null) {
|
|
1984
|
+
const phoneFingerprint = shortFingerprint(session?.phoneDeviceId);
|
|
1985
|
+
if (!phoneFingerprint) {
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return {
|
|
1990
|
+
connected: true,
|
|
1991
|
+
phoneFingerprint,
|
|
1992
|
+
deviceKind: normalizeNonEmptyString(deviceState?.lastSeenDeviceKind) || null,
|
|
1993
|
+
handshakeMode: normalizeNonEmptyString(session?.handshakeMode) || null,
|
|
1994
|
+
keyEpoch: Number.isFinite(session?.keyEpoch) ? session.keyEpoch : null,
|
|
1995
|
+
updatedAt: new Date().toISOString(),
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function classifyClientDeviceKind(clientName) {
|
|
2000
|
+
const normalized = normalizeNonEmptyString(clientName).toLowerCase();
|
|
2001
|
+
if (!normalized) {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
if (normalized.includes("android")) {
|
|
2005
|
+
return "android";
|
|
2006
|
+
}
|
|
2007
|
+
if (normalized.includes("ios") || normalized.includes("iphone")) {
|
|
2008
|
+
return "iphone";
|
|
2009
|
+
}
|
|
2010
|
+
if (normalized.includes("macos") || normalized.includes("mac")) {
|
|
2011
|
+
return "mac";
|
|
2012
|
+
}
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function shortFingerprint(value) {
|
|
2017
|
+
const normalized = normalizeNonEmptyString(value);
|
|
2018
|
+
if (!normalized) {
|
|
2019
|
+
return null;
|
|
2020
|
+
}
|
|
2021
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1953
2024
|
function shutdown(codex, getSocket, beforeExit = () => {}, { exitCode = 0 } = {}) {
|
|
1954
2025
|
beforeExit();
|
|
1955
2026
|
|
|
@@ -2553,6 +2624,10 @@ function compactEmergencySingleTurnForRelay(turn, maxChars, maxItems) {
|
|
|
2553
2624
|
"created_at",
|
|
2554
2625
|
"completedAt",
|
|
2555
2626
|
"completed_at",
|
|
2627
|
+
"timeZoneIdentifier",
|
|
2628
|
+
"timeZone",
|
|
2629
|
+
"timezone",
|
|
2630
|
+
"time_zone",
|
|
2556
2631
|
"status",
|
|
2557
2632
|
"role",
|
|
2558
2633
|
"kind",
|
|
@@ -3822,8 +3897,16 @@ function compactHistoryItemForRelay(item, maxChars) {
|
|
|
3822
3897
|
turn_id: typeof item?.turn_id === "string" ? item.turn_id : undefined,
|
|
3823
3898
|
createdAt: relayScalarHistoryMetadata(item?.createdAt),
|
|
3824
3899
|
created_at: relayScalarHistoryMetadata(item?.created_at),
|
|
3900
|
+
startedAt: relayScalarHistoryMetadata(item?.startedAt),
|
|
3901
|
+
started_at: relayScalarHistoryMetadata(item?.started_at),
|
|
3902
|
+
completedAt: relayScalarHistoryMetadata(item?.completedAt),
|
|
3903
|
+
completed_at: relayScalarHistoryMetadata(item?.completed_at),
|
|
3825
3904
|
timestamp: relayScalarHistoryMetadata(item?.timestamp),
|
|
3826
3905
|
time: relayScalarHistoryMetadata(item?.time),
|
|
3906
|
+
timeZoneIdentifier: relayScalarHistoryMetadata(item?.timeZoneIdentifier),
|
|
3907
|
+
timeZone: relayScalarHistoryMetadata(item?.timeZone),
|
|
3908
|
+
timezone: relayScalarHistoryMetadata(item?.timezone),
|
|
3909
|
+
time_zone: relayScalarHistoryMetadata(item?.time_zone),
|
|
3827
3910
|
relayPayloadTruncated: true,
|
|
3828
3911
|
};
|
|
3829
3912
|
const tailText = maxChars > 0 ? firstRelayTextTail(item, maxChars) : "";
|
package/src/git-handler.js
CHANGED
|
@@ -20,9 +20,11 @@ const GITHUB_CLI_TIMEOUT_MS = 120_000;
|
|
|
20
20
|
const GIT_DRAFT_PATCH_MAX_BYTES = 80_000;
|
|
21
21
|
const EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
22
22
|
const DEFAULT_GIT_WRITER_MODEL = "gpt-5.4-mini";
|
|
23
|
+
const STATUS_UPSTREAM_FETCH_TTL_MS = 15_000;
|
|
23
24
|
|
|
24
25
|
let runStructuredCodexJsonImpl = runStructuredCodexJson;
|
|
25
26
|
let runGitHubCliImpl = runGitHubCli;
|
|
27
|
+
const statusUpstreamFetchCache = new Map();
|
|
26
28
|
|
|
27
29
|
function resolveGitWriterModel(rawModel) {
|
|
28
30
|
const trimmed = typeof rawModel === "string" ? rawModel.trim() : "";
|
|
@@ -172,9 +174,43 @@ async function gitStatus(cwd) {
|
|
|
172
174
|
return nonRepositoryStatus(cwd);
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
const
|
|
177
|
+
const snapshot = await readGitStatusSnapshot(cwd);
|
|
178
|
+
const { ahead, behind } = await freshBranchInfoForStatus(cwd, snapshot);
|
|
179
|
+
const dirty = snapshot.files.length > 0;
|
|
180
|
+
const noUpstream = snapshot.tracking === null && !snapshot.detached;
|
|
181
|
+
const hasHeadCommit = await refExists(cwd, "HEAD").catch(() => false);
|
|
182
|
+
const hasPushRemote = await pushRemoteAvailable(cwd, snapshot.tracking).catch(() => false);
|
|
183
|
+
const publishedToRemote = !snapshot.detached && !!snapshot.branch && await remoteBranchExists(cwd, snapshot.branch).catch(() => false);
|
|
184
|
+
const localOnlyCommitCount = await countLocalOnlyCommits(cwd, { detached: snapshot.detached }).catch(() => 0);
|
|
185
|
+
const state = computeState(dirty, ahead, behind, snapshot.detached, noUpstream);
|
|
186
|
+
const canPush = hasPushRemote && hasHeadCommit && (ahead > 0 || noUpstream) && !snapshot.detached;
|
|
187
|
+
const diff = await repoDiffTotals(cwd, {
|
|
188
|
+
tracking: snapshot.tracking,
|
|
189
|
+
fileLines: snapshot.fileLines,
|
|
190
|
+
}).catch(() => ({ additions: 0, deletions: 0, binaryFiles: 0 }));
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
isRepo: true,
|
|
194
|
+
repoRoot: snapshot.repoRoot,
|
|
195
|
+
branch: snapshot.branch,
|
|
196
|
+
tracking: snapshot.tracking,
|
|
197
|
+
dirty,
|
|
198
|
+
hasHeadCommit,
|
|
199
|
+
hasPushRemote,
|
|
200
|
+
ahead,
|
|
201
|
+
behind,
|
|
202
|
+
localOnlyCommitCount,
|
|
203
|
+
state,
|
|
204
|
+
canPush,
|
|
205
|
+
publishedToRemote,
|
|
206
|
+
files: snapshot.files,
|
|
207
|
+
diff,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function readGitStatusSnapshot(cwd) {
|
|
212
|
+
const [porcelain, repoRoot] = await Promise.all([
|
|
176
213
|
git(cwd, "status", "--porcelain=v1", "-b"),
|
|
177
|
-
revListCounts(cwd).catch(() => ({ ahead: 0, behind: 0 })),
|
|
178
214
|
resolveRepoRoot(cwd).catch(() => null),
|
|
179
215
|
]);
|
|
180
216
|
|
|
@@ -184,45 +220,27 @@ async function gitStatus(cwd) {
|
|
|
184
220
|
|
|
185
221
|
const branch = parseBranchFromStatus(branchLine);
|
|
186
222
|
const tracking = parseTrackingFromStatus(branchLine);
|
|
223
|
+
const detached = branchLine.includes("HEAD detached") || branchLine.includes("no branch");
|
|
187
224
|
const files = fileLines.map((line) => ({
|
|
188
225
|
path: line.substring(3).trim(),
|
|
189
226
|
status: line.substring(0, 2).trim(),
|
|
190
227
|
}));
|
|
191
228
|
|
|
192
|
-
const dirty = files.length > 0;
|
|
193
|
-
const { ahead, behind } = branchInfo;
|
|
194
|
-
const detached = branchLine.includes("HEAD detached") || branchLine.includes("no branch");
|
|
195
|
-
const noUpstream = tracking === null && !detached;
|
|
196
|
-
const hasHeadCommit = await refExists(cwd, "HEAD").catch(() => false);
|
|
197
|
-
const hasPushRemote = await pushRemoteAvailable(cwd, tracking).catch(() => false);
|
|
198
|
-
const publishedToRemote = !detached && !!branch && await remoteBranchExists(cwd, branch).catch(() => false);
|
|
199
|
-
const localOnlyCommitCount = await countLocalOnlyCommits(cwd, { detached }).catch(() => 0);
|
|
200
|
-
const state = computeState(dirty, ahead, behind, detached, noUpstream);
|
|
201
|
-
const canPush = hasPushRemote && hasHeadCommit && (ahead > 0 || noUpstream) && !detached;
|
|
202
|
-
const diff = await repoDiffTotals(cwd, {
|
|
203
|
-
tracking,
|
|
204
|
-
fileLines,
|
|
205
|
-
}).catch(() => ({ additions: 0, deletions: 0, binaryFiles: 0 }));
|
|
206
|
-
|
|
207
229
|
return {
|
|
208
|
-
isRepo: true,
|
|
209
230
|
repoRoot,
|
|
210
231
|
branch,
|
|
211
232
|
tracking,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
hasPushRemote,
|
|
215
|
-
ahead,
|
|
216
|
-
behind,
|
|
217
|
-
localOnlyCommitCount,
|
|
218
|
-
state,
|
|
219
|
-
canPush,
|
|
220
|
-
publishedToRemote,
|
|
233
|
+
detached,
|
|
234
|
+
fileLines,
|
|
221
235
|
files,
|
|
222
|
-
diff,
|
|
223
236
|
};
|
|
224
237
|
}
|
|
225
238
|
|
|
239
|
+
async function freshBranchInfoForStatus(cwd, snapshot) {
|
|
240
|
+
await refreshStatusUpstreamIfNeeded(cwd, snapshot.tracking, snapshot.repoRoot).catch(() => false);
|
|
241
|
+
return await revListCounts(cwd).catch(() => ({ ahead: 0, behind: 0 }));
|
|
242
|
+
}
|
|
243
|
+
|
|
226
244
|
async function gitInit(cwd) {
|
|
227
245
|
if (await isInsideGitWorkTree(cwd)) {
|
|
228
246
|
throw gitError("already_git_repository", "This folder is already inside a Git repository.");
|
|
@@ -2506,6 +2524,52 @@ async function revListCounts(cwd) {
|
|
|
2506
2524
|
};
|
|
2507
2525
|
}
|
|
2508
2526
|
|
|
2527
|
+
// Keeps Update eligibility based on the current upstream ref, not stale local fetch data.
|
|
2528
|
+
async function refreshStatusUpstreamIfNeeded(cwd, tracking, repoRoot) {
|
|
2529
|
+
const parsedTracking = parseTrackingRef(tracking);
|
|
2530
|
+
if (!parsedTracking) {
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
const cacheKey = `${repoRoot || cwd}\0${parsedTracking.remote}\0${parsedTracking.branch}`;
|
|
2535
|
+
const now = Date.now();
|
|
2536
|
+
const lastFetchAt = statusUpstreamFetchCache.get(cacheKey) || 0;
|
|
2537
|
+
if (now - lastFetchAt < STATUS_UPSTREAM_FETCH_TTL_MS) {
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
statusUpstreamFetchCache.set(cacheKey, now);
|
|
2542
|
+
if (statusUpstreamFetchCache.size > 200) {
|
|
2543
|
+
statusUpstreamFetchCache.clear();
|
|
2544
|
+
statusUpstreamFetchCache.set(cacheKey, now);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
await git(
|
|
2548
|
+
cwd,
|
|
2549
|
+
"fetch",
|
|
2550
|
+
"--quiet",
|
|
2551
|
+
parsedTracking.remote,
|
|
2552
|
+
`+refs/heads/${parsedTracking.branch}:refs/remotes/${parsedTracking.remote}/${parsedTracking.branch}`
|
|
2553
|
+
);
|
|
2554
|
+
return true;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
function parseTrackingRef(tracking) {
|
|
2558
|
+
if (typeof tracking !== "string") {
|
|
2559
|
+
return null;
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
const separatorIndex = tracking.indexOf("/");
|
|
2563
|
+
if (separatorIndex <= 0 || separatorIndex === tracking.length - 1) {
|
|
2564
|
+
return null;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
return {
|
|
2568
|
+
remote: tracking.slice(0, separatorIndex),
|
|
2569
|
+
branch: tracking.slice(separatorIndex + 1),
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2509
2573
|
function parseBranchFromStatus(line) {
|
|
2510
2574
|
// "## main...origin/main" or "## main" or "## HEAD (no branch)"
|
|
2511
2575
|
const match = line.match(/^## (.+?)(?:\.{3}|$)/);
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// Exports: version comparison + bridge/iPhone compatibility helpers
|
|
5
5
|
// Depends on: none
|
|
6
6
|
|
|
7
|
-
const MINIMUM_SUPPORTED_IOS_APP_VERSION = "
|
|
8
|
-
const IOS_APP_COMPATIBILITY_GATE_BRIDGE_VERSION = "
|
|
9
|
-
const
|
|
10
|
-
const LEGACY_BRIDGE_DOWNGRADE_COMMAND = `npm install -g remodex@${
|
|
7
|
+
const MINIMUM_SUPPORTED_IOS_APP_VERSION = "2.0";
|
|
8
|
+
const IOS_APP_COMPATIBILITY_GATE_BRIDGE_VERSION = "2.0.0";
|
|
9
|
+
const LEGACY_BRIDGE_VERSION_FOR_IOS_1_X = "1.5.1";
|
|
10
|
+
const LEGACY_BRIDGE_DOWNGRADE_COMMAND = `npm install -g @makerbi/remodex@${LEGACY_BRIDGE_VERSION_FOR_IOS_1_X}`;
|
|
11
11
|
const NOTICE_BOX_WIDTH = 74;
|
|
12
12
|
|
|
13
13
|
function buildIOSAppCompatibilitySnapshot({
|
|
@@ -80,7 +80,7 @@ function buildSnapshot({
|
|
|
80
80
|
isCompatible,
|
|
81
81
|
requiresAppUpdate,
|
|
82
82
|
minimumSupportedIOSAppVersion: MINIMUM_SUPPORTED_IOS_APP_VERSION,
|
|
83
|
-
legacyBridgeVersion:
|
|
83
|
+
legacyBridgeVersion: LEGACY_BRIDGE_VERSION_FOR_IOS_1_X,
|
|
84
84
|
downgradeCommand: LEGACY_BRIDGE_DOWNGRADE_COMMAND,
|
|
85
85
|
message,
|
|
86
86
|
};
|
|
@@ -108,7 +108,7 @@ function buildLegacyIOSAppCompatibilityMessage({
|
|
|
108
108
|
return `Remodex bridge ${normalizedBridgeVersion} requires Remodex iPhone `
|
|
109
109
|
+ `${MINIMUM_SUPPORTED_IOS_APP_VERSION} or later. `
|
|
110
110
|
+ `Update the iPhone app from the App Store first, or install Remodex bridge `
|
|
111
|
-
+ `${
|
|
111
|
+
+ `${LEGACY_BRIDGE_VERSION_FOR_IOS_1_X} to keep using iPhone ${normalizedIOSAppVersion}.`;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
function buildCachedIOSAppCompatibilityWarning({
|
|
@@ -232,7 +232,7 @@ function splitVersionParts(value) {
|
|
|
232
232
|
|
|
233
233
|
module.exports = {
|
|
234
234
|
LEGACY_BRIDGE_DOWNGRADE_COMMAND,
|
|
235
|
-
|
|
235
|
+
LEGACY_BRIDGE_VERSION_FOR_IOS_1_X,
|
|
236
236
|
IOS_APP_COMPATIBILITY_GATE_BRIDGE_VERSION,
|
|
237
237
|
MINIMUM_SUPPORTED_IOS_APP_VERSION,
|
|
238
238
|
buildCachedIOSAppCompatibilityWarning,
|
|
@@ -5,13 +5,14 @@
|
|
|
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");
|
|
8
|
+
const { createHash } = require("crypto");
|
|
8
9
|
const fs = require("fs");
|
|
9
10
|
const os = require("os");
|
|
10
11
|
const path = require("path");
|
|
11
12
|
const { startBridge } = require("./bridge");
|
|
12
13
|
const { readBridgeConfig } = require("./codex-desktop-refresher");
|
|
13
14
|
const { printQR } = require("./qr");
|
|
14
|
-
const { resetBridgeTrustState } = require("./secure-device-state");
|
|
15
|
+
const { readBridgeDeviceState, resetBridgeTrustState } = require("./secure-device-state");
|
|
15
16
|
const {
|
|
16
17
|
clearBridgeStatus,
|
|
17
18
|
clearPairingSession,
|
|
@@ -276,6 +277,7 @@ function getMacOSBridgeServiceStatus({
|
|
|
276
277
|
daemonConfig: sanitizedDaemonConfigForStatus(readDaemonConfig({ env, fsImpl })),
|
|
277
278
|
bridgeStatus: readBridgeStatus({ env, fsImpl }),
|
|
278
279
|
pairingSession: readPairingSession({ env, fsImpl }),
|
|
280
|
+
trustedDevice: buildTrustedDeviceSummary(readBridgeDeviceState()),
|
|
279
281
|
stdoutLogPath: resolveBridgeStdoutLogPath({ env }),
|
|
280
282
|
stderrLogPath: resolveBridgeStderrLogPath({ env }),
|
|
281
283
|
};
|
|
@@ -301,12 +303,18 @@ function printMacOSBridgeServiceStatus(options = {}) {
|
|
|
301
303
|
const bridgeState = status.bridgeStatus?.state || "unknown";
|
|
302
304
|
const connectionStatus = status.bridgeStatus?.connectionStatus || "unknown";
|
|
303
305
|
const pairingCreatedAt = status.pairingSession?.createdAt || "none";
|
|
306
|
+
const activeDevice = status.bridgeStatus?.activeDevice || status.bridgeStatus?.activePhone;
|
|
307
|
+
const trustedPhoneCount = status.trustedDevice?.trustedPhoneCount || 0;
|
|
308
|
+
const activeDeviceName = formatDeviceKind(activeDevice?.deviceKind) || "device";
|
|
309
|
+
const trustedDeviceName = formatDeviceKind(status.trustedDevice?.lastSeenDeviceKind) || "device";
|
|
304
310
|
console.log(`[remodex] Service label: ${status.label}`);
|
|
305
311
|
console.log(`[remodex] Installed: ${status.installed ? "yes" : "no"}`);
|
|
306
312
|
console.log(`[remodex] Launchd loaded: ${status.launchdLoaded ? "yes" : "no"}`);
|
|
307
313
|
console.log(`[remodex] PID: ${status.launchdPid || status.bridgeStatus?.pid || "unknown"}`);
|
|
308
314
|
console.log(`[remodex] Bridge state: ${bridgeState}`);
|
|
309
315
|
console.log(`[remodex] Connection: ${connectionStatus}`);
|
|
316
|
+
console.log(`[remodex] Active ${activeDeviceName}: ${activeDevice?.connected ? activeDevice.phoneFingerprint || "yes" : "no"}`);
|
|
317
|
+
console.log(`[remodex] Trusted ${trustedDeviceName}: ${trustedPhoneCount > 0 ? "yes" : "no"}`);
|
|
310
318
|
console.log(`[remodex] Pairing payload: ${pairingCreatedAt}`);
|
|
311
319
|
console.log(`[remodex] Stdout log: ${status.stdoutLogPath}`);
|
|
312
320
|
console.log(`[remodex] Stderr log: ${status.stderrLogPath}`);
|
|
@@ -628,7 +636,44 @@ function normalizeNonEmptyString(value) {
|
|
|
628
636
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
629
637
|
}
|
|
630
638
|
|
|
639
|
+
function buildTrustedDeviceSummary(deviceState) {
|
|
640
|
+
const trustedPhoneEntries = Object.entries(deviceState?.trustedPhones || {})
|
|
641
|
+
.filter(([phoneDeviceId, publicKey]) => normalizeNonEmptyString(phoneDeviceId) && normalizeNonEmptyString(publicKey));
|
|
642
|
+
const firstTrustedPhoneId = trustedPhoneEntries[0]?.[0] || "";
|
|
643
|
+
const lastSeenPhoneAppVersion = normalizeNonEmptyString(deviceState?.lastSeenPhoneAppVersion) || null;
|
|
644
|
+
return {
|
|
645
|
+
macDeviceFingerprint: shortFingerprint(deviceState?.macDeviceId),
|
|
646
|
+
trustedPhoneCount: trustedPhoneEntries.length,
|
|
647
|
+
trustedPhoneFingerprint: shortFingerprint(firstTrustedPhoneId),
|
|
648
|
+
lastSeenDeviceKind: normalizeNonEmptyString(deviceState?.lastSeenDeviceKind) || (lastSeenPhoneAppVersion ? "iphone" : null),
|
|
649
|
+
lastSeenPhoneAppVersion,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function formatDeviceKind(deviceKind) {
|
|
654
|
+
const normalized = normalizeNonEmptyString(deviceKind).toLowerCase();
|
|
655
|
+
if (normalized === "iphone") {
|
|
656
|
+
return "iPhone";
|
|
657
|
+
}
|
|
658
|
+
if (normalized === "android") {
|
|
659
|
+
return "Android";
|
|
660
|
+
}
|
|
661
|
+
if (normalized === "mac") {
|
|
662
|
+
return "Mac";
|
|
663
|
+
}
|
|
664
|
+
return "";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function shortFingerprint(value) {
|
|
668
|
+
const normalized = normalizeNonEmptyString(value);
|
|
669
|
+
if (!normalized) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
673
|
+
}
|
|
674
|
+
|
|
631
675
|
module.exports = {
|
|
676
|
+
buildTrustedDeviceSummary,
|
|
632
677
|
buildLaunchAgentPlist,
|
|
633
678
|
getMacOSBridgeServiceStatus,
|
|
634
679
|
mergeBridgeStatusForDaemon,
|
package/src/qr.js
CHANGED
|
@@ -9,7 +9,6 @@ const qrcode = require("qrcode-terminal");
|
|
|
9
9
|
|
|
10
10
|
const SHORT_PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
11
11
|
const SHORT_PAIRING_CODE_LENGTH = 10;
|
|
12
|
-
|
|
13
12
|
// Generates a short-lived human-friendly pairing token for reconnect flows.
|
|
14
13
|
function createShortPairingCode({
|
|
15
14
|
length = SHORT_PAIRING_CODE_LENGTH,
|
|
@@ -50,7 +49,7 @@ function printQR(pairingSessionOrPayload, options = {}) {
|
|
|
50
49
|
console.log("\nScan this QR with the iPhone:\n");
|
|
51
50
|
qrcode.generate(payload, { small: true });
|
|
52
51
|
if (pairingCode) {
|
|
53
|
-
console.log("Or
|
|
52
|
+
console.log("Or enter this pairing code in the iPhone app:\n");
|
|
54
53
|
console.log(pairingCode);
|
|
55
54
|
}
|
|
56
55
|
console.log(`\nSession ID: ${sessionIdShort || "(none)"}`);
|
|
@@ -58,9 +57,7 @@ function printQR(pairingSessionOrPayload, options = {}) {
|
|
|
58
57
|
console.log(`Expires: ${new Date(pairingPayload.expiresAt).toISOString()}\n`);
|
|
59
58
|
|
|
60
59
|
if (shouldPrintPairingJson({ env, explicitValue: options.printPairingJson })) {
|
|
61
|
-
|
|
62
|
-
console.log("Pairing JSON (debug only; same sensitive bytes as the QR):\n");
|
|
63
|
-
console.log(`${payload}\n`);
|
|
60
|
+
console.log("Pairing JSON debug output is disabled because the payload contains private relay metadata.\n");
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
63
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// FILE: secure-device-state.js
|
|
2
|
-
// Purpose: Persists canonical bridge identity, trusted
|
|
2
|
+
// Purpose: Persists canonical bridge identity, trusted mobile state, and last seen companion metadata for local QR pairing.
|
|
3
3
|
// Layer: CLI helper
|
|
4
|
-
// Exports: loadOrCreateBridgeDeviceState, readBridgeDeviceState, resetBridgeDeviceState, resetBridgeTrustState, rememberTrustedPhone, rememberLastSeenPhoneAppVersion, getTrustedPhonePublicKey, resolveBridgeRelaySession
|
|
4
|
+
// Exports: loadOrCreateBridgeDeviceState, readBridgeDeviceState, resetBridgeDeviceState, resetBridgeTrustState, rememberTrustedPhone, rememberLastSeenPhoneAppVersion, rememberLastSeenClientDeviceKind, getTrustedPhonePublicKey, resolveBridgeRelaySession
|
|
5
5
|
// Depends on: fs, os, path, crypto, child_process
|
|
6
6
|
|
|
7
7
|
const fs = require("fs");
|
|
@@ -153,6 +153,22 @@ function rememberLastSeenPhoneAppVersion(state, phoneAppVersion, { persist = tru
|
|
|
153
153
|
return nextState;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
function rememberLastSeenClientDeviceKind(state, deviceKind, { persist = true } = {}) {
|
|
157
|
+
const normalizedDeviceKind = normalizeDeviceKind(deviceKind);
|
|
158
|
+
if (!normalizedDeviceKind) {
|
|
159
|
+
return state;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const nextState = normalizeBridgeDeviceState({
|
|
163
|
+
...state,
|
|
164
|
+
lastSeenDeviceKind: normalizedDeviceKind,
|
|
165
|
+
});
|
|
166
|
+
if (persist) {
|
|
167
|
+
writeBridgeDeviceState(nextState);
|
|
168
|
+
}
|
|
169
|
+
return nextState;
|
|
170
|
+
}
|
|
171
|
+
|
|
156
172
|
function getTrustedPhonePublicKey(state, phoneDeviceId) {
|
|
157
173
|
const normalizedDeviceId = normalizeNonEmptyString(phoneDeviceId);
|
|
158
174
|
if (!normalizedDeviceId) {
|
|
@@ -176,6 +192,7 @@ function createBridgeDeviceState() {
|
|
|
176
192
|
macIdentityPublicKey: base64UrlToBase64(publicJwk.x),
|
|
177
193
|
macIdentityPrivateKey: base64UrlToBase64(privateJwk.d),
|
|
178
194
|
trustedPhones: {},
|
|
195
|
+
lastSeenDeviceKind: null,
|
|
179
196
|
lastSeenPhoneAppVersion: null,
|
|
180
197
|
};
|
|
181
198
|
}
|
|
@@ -385,6 +402,8 @@ function normalizeBridgeDeviceState(rawState) {
|
|
|
385
402
|
const macIdentityPublicKey = normalizeNonEmptyString(rawState?.macIdentityPublicKey);
|
|
386
403
|
const macIdentityPrivateKey = normalizeNonEmptyString(rawState?.macIdentityPrivateKey);
|
|
387
404
|
const lastSeenPhoneAppVersion = normalizeNonEmptyString(rawState?.lastSeenPhoneAppVersion) || null;
|
|
405
|
+
const lastSeenDeviceKind = normalizeDeviceKind(rawState?.lastSeenDeviceKind)
|
|
406
|
+
|| inferLegacyDeviceKind({ lastSeenPhoneAppVersion });
|
|
388
407
|
|
|
389
408
|
if (!macDeviceId || !macIdentityPublicKey || !macIdentityPrivateKey) {
|
|
390
409
|
throw new Error("Bridge device state is incomplete");
|
|
@@ -408,6 +427,7 @@ function normalizeBridgeDeviceState(rawState) {
|
|
|
408
427
|
macIdentityPublicKey,
|
|
409
428
|
macIdentityPrivateKey,
|
|
410
429
|
trustedPhones,
|
|
430
|
+
lastSeenDeviceKind,
|
|
411
431
|
lastSeenPhoneAppVersion,
|
|
412
432
|
};
|
|
413
433
|
}
|
|
@@ -430,6 +450,24 @@ function recoverBridgeDeviceIdentity(state, { fallbackState = null } = {}) {
|
|
|
430
450
|
return nextState;
|
|
431
451
|
}
|
|
432
452
|
|
|
453
|
+
function normalizeDeviceKind(value) {
|
|
454
|
+
const normalized = normalizeNonEmptyString(value).toLowerCase();
|
|
455
|
+
if (normalized === "ios" || normalized === "iphone") {
|
|
456
|
+
return "iphone";
|
|
457
|
+
}
|
|
458
|
+
if (normalized === "android") {
|
|
459
|
+
return "android";
|
|
460
|
+
}
|
|
461
|
+
if (normalized === "mac" || normalized === "macos" || normalized === "darwin") {
|
|
462
|
+
return "mac";
|
|
463
|
+
}
|
|
464
|
+
return normalized || null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function inferLegacyDeviceKind({ lastSeenPhoneAppVersion } = {}) {
|
|
468
|
+
return lastSeenPhoneAppVersion ? "iphone" : null;
|
|
469
|
+
}
|
|
470
|
+
|
|
433
471
|
function bridgeStatesEqual(left, right) {
|
|
434
472
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
435
473
|
}
|
|
@@ -475,6 +513,7 @@ module.exports = {
|
|
|
475
513
|
getTrustedPhonePublicKey,
|
|
476
514
|
loadOrCreateBridgeDeviceState,
|
|
477
515
|
readBridgeDeviceState,
|
|
516
|
+
rememberLastSeenClientDeviceKind,
|
|
478
517
|
rememberLastSeenPhoneAppVersion,
|
|
479
518
|
rememberTrustedPhone,
|
|
480
519
|
resetBridgeDeviceState,
|
package/src/secure-transport.js
CHANGED
|
@@ -39,6 +39,7 @@ function createBridgeSecureTransport({
|
|
|
39
39
|
deviceState,
|
|
40
40
|
displayName = "",
|
|
41
41
|
onTrustedPhoneUpdate = null,
|
|
42
|
+
onSecureSessionReady = null,
|
|
42
43
|
persistTrustedPhone = true,
|
|
43
44
|
}) {
|
|
44
45
|
let currentDeviceState = deviceState;
|
|
@@ -382,7 +383,13 @@ function createBridgeSecureTransport({
|
|
|
382
383
|
activeSession.firstOutboundSeq = nextBridgeOutboundSeq;
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
const completedHandshakeMode = pendingHandshake.handshakeMode;
|
|
385
387
|
pendingHandshake = null;
|
|
388
|
+
onSecureSessionReady?.({
|
|
389
|
+
phoneDeviceId: activeSession.phoneDeviceId,
|
|
390
|
+
handshakeMode: completedHandshakeMode,
|
|
391
|
+
keyEpoch: activeSession.keyEpoch,
|
|
392
|
+
});
|
|
386
393
|
sendControlMessage({
|
|
387
394
|
kind: "secureReady",
|
|
388
395
|
sessionId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// FILE: session-jsonl-history.js
|
|
2
|
-
// Purpose: Reconstructs a small thread/turns/list page from local Codex session JSONL files
|
|
2
|
+
// Purpose: Reconstructs a small thread/turns/list page from local Codex session JSONL files,
|
|
3
|
+
// including desktop-local timestamp metadata for mobile history rendering.
|
|
3
4
|
|
|
4
5
|
const fs = require("fs");
|
|
5
6
|
const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
|
|
@@ -83,6 +84,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
83
84
|
let activeTurnId = "";
|
|
84
85
|
let sessionThreadId = normalizeString(threadId);
|
|
85
86
|
let sessionCwd = "";
|
|
87
|
+
let sessionTimeZone = "";
|
|
86
88
|
const skippedCallIds = new Set();
|
|
87
89
|
const toolCallsByCallId = new Map();
|
|
88
90
|
const pendingUserMessages = [];
|
|
@@ -115,6 +117,26 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
115
117
|
|| normalizeString(payload?.thread_id)
|
|
116
118
|
|| normalizeString(payload?.threadId);
|
|
117
119
|
sessionCwd ||= normalizeString(payload?.cwd);
|
|
120
|
+
sessionTimeZone ||= normalizeString(payload?.timezone)
|
|
121
|
+
|| normalizeString(payload?.timeZone)
|
|
122
|
+
|| normalizeString(payload?.time_zone);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (entry?.type === "turn_context") {
|
|
127
|
+
const payload = objectValue(entry.payload);
|
|
128
|
+
sessionCwd = normalizeString(payload?.cwd) || sessionCwd;
|
|
129
|
+
sessionTimeZone = normalizeString(payload?.timezone)
|
|
130
|
+
|| normalizeString(payload?.timeZone)
|
|
131
|
+
|| normalizeString(payload?.time_zone)
|
|
132
|
+
|| sessionTimeZone;
|
|
133
|
+
activeTurnId = normalizeString(payload?.turn_id)
|
|
134
|
+
|| normalizeString(payload?.turnId)
|
|
135
|
+
|| activeTurnId;
|
|
136
|
+
if (activeTurnId) {
|
|
137
|
+
const turn = ensureTurn(turns, turnsById, activeTurnId, sessionThreadId, entry.timestamp);
|
|
138
|
+
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
139
|
+
}
|
|
118
140
|
continue;
|
|
119
141
|
}
|
|
120
142
|
|
|
@@ -127,6 +149,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
127
149
|
|| activeTurnId
|
|
128
150
|
|| `turn-line-${index + 1}`;
|
|
129
151
|
const turn = ensureTurn(turns, turnsById, activeTurnId, sessionThreadId, entry.timestamp);
|
|
152
|
+
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
130
153
|
flushPendingUserMessagesToTurn(turn, pendingUserMessages);
|
|
131
154
|
continue;
|
|
132
155
|
}
|
|
@@ -139,6 +162,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
139
162
|
sessionThreadId,
|
|
140
163
|
entry.timestamp
|
|
141
164
|
);
|
|
165
|
+
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
142
166
|
turn.status = "completed";
|
|
143
167
|
activeTurnId = "";
|
|
144
168
|
continue;
|
|
@@ -162,6 +186,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
162
186
|
toolCallsByCallId,
|
|
163
187
|
});
|
|
164
188
|
if (item) {
|
|
189
|
+
applyHistoryTimeZone(item, sessionTimeZone);
|
|
165
190
|
turn.items.push(item);
|
|
166
191
|
}
|
|
167
192
|
continue;
|
|
@@ -170,6 +195,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
170
195
|
if (eventType === "user_message") {
|
|
171
196
|
const explicitTurnId = normalizeString(payload?.turn_id) || normalizeString(payload?.turnId);
|
|
172
197
|
const item = createUserMessageHistoryItem(payload, index + 1, entry.timestamp);
|
|
198
|
+
applyHistoryTimeZone(item, sessionTimeZone);
|
|
173
199
|
if (!explicitTurnId && !activeTurnId) {
|
|
174
200
|
pushPendingUserMessage(pendingUserMessages, item);
|
|
175
201
|
continue;
|
|
@@ -182,6 +208,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
182
208
|
sessionThreadId,
|
|
183
209
|
entry.timestamp
|
|
184
210
|
);
|
|
211
|
+
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
185
212
|
addHistoryItemToTurn(turn, item);
|
|
186
213
|
continue;
|
|
187
214
|
}
|
|
@@ -207,6 +234,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
207
234
|
sessionThreadId,
|
|
208
235
|
entry.timestamp
|
|
209
236
|
);
|
|
237
|
+
applyHistoryTimeZone(turn, sessionTimeZone);
|
|
210
238
|
const item = normalizeResponseItemForHistory(payload, index + 1, {
|
|
211
239
|
cwd: sessionCwd,
|
|
212
240
|
toolCallsByCallId,
|
|
@@ -222,6 +250,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
|
222
250
|
if (itemTimestamp && !item.timestamp) {
|
|
223
251
|
item.timestamp = itemTimestamp;
|
|
224
252
|
}
|
|
253
|
+
applyHistoryTimeZone(item, sessionTimeZone);
|
|
225
254
|
addHistoryItemToTurn(turn, item);
|
|
226
255
|
}
|
|
227
256
|
}
|
|
@@ -336,6 +365,12 @@ function historyItemTimestamp(item, fallbackTimestamp = "") {
|
|
|
336
365
|
return firstNonEmptyString([
|
|
337
366
|
normalizeString(item?.createdAt),
|
|
338
367
|
normalizeString(item?.created_at),
|
|
368
|
+
normalizeString(item?.startedAt),
|
|
369
|
+
normalizeString(item?.started_at),
|
|
370
|
+
normalizeString(item?.completedAt),
|
|
371
|
+
normalizeString(item?.completed_at),
|
|
372
|
+
normalizeString(item?.endedAt),
|
|
373
|
+
normalizeString(item?.ended_at),
|
|
339
374
|
normalizeString(item?.timestamp),
|
|
340
375
|
normalizeString(item?.time),
|
|
341
376
|
normalizeString(fallbackTimestamp),
|
|
@@ -379,6 +414,7 @@ function flushPendingUserMessagesToTurn(turn, pendingUserMessages) {
|
|
|
379
414
|
}
|
|
380
415
|
|
|
381
416
|
for (const item of pendingUserMessages.splice(0)) {
|
|
417
|
+
applyHistoryTimeZone(item, normalizeString(turn.timeZone) || normalizeString(turn.timezone));
|
|
382
418
|
addHistoryItemToTurn(turn, item);
|
|
383
419
|
}
|
|
384
420
|
}
|
|
@@ -403,6 +439,23 @@ function ensureTurn(turns, turnsById, turnId, threadId, timestamp) {
|
|
|
403
439
|
return turn;
|
|
404
440
|
}
|
|
405
441
|
|
|
442
|
+
function applyHistoryTimeZone(target, timeZone) {
|
|
443
|
+
const normalizedTimeZone = normalizeString(timeZone);
|
|
444
|
+
if (!target || !normalizedTimeZone) {
|
|
445
|
+
return target;
|
|
446
|
+
}
|
|
447
|
+
if (!target.timeZoneIdentifier) {
|
|
448
|
+
target.timeZoneIdentifier = normalizedTimeZone;
|
|
449
|
+
}
|
|
450
|
+
if (!target.timeZone) {
|
|
451
|
+
target.timeZone = normalizedTimeZone;
|
|
452
|
+
}
|
|
453
|
+
if (!target.timezone) {
|
|
454
|
+
target.timezone = normalizedTimeZone;
|
|
455
|
+
}
|
|
456
|
+
return target;
|
|
457
|
+
}
|
|
458
|
+
|
|
406
459
|
function normalizeResponseItemForHistory(payload, lineNumber, { cwd = "", toolCallsByCallId = new Map() } = {}) {
|
|
407
460
|
const type = normalizeHistoryItemType(payload.type);
|
|
408
461
|
if (!type) {
|
package/src/voice-handler.js
CHANGED
|
@@ -8,7 +8,8 @@ const OPENAI_TRANSCRIPTIONS_URL = "https://api.openai.com/v1/audio/transcription
|
|
|
8
8
|
const CHATGPT_TRANSCRIPTIONS_URL = "https://chatgpt.com/backend-api/transcribe";
|
|
9
9
|
const DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe";
|
|
10
10
|
const MAX_AUDIO_BYTES = 10 * 1024 * 1024;
|
|
11
|
-
const
|
|
11
|
+
const MAX_DURATION_SECONDS = 150;
|
|
12
|
+
const MAX_DURATION_MS = MAX_DURATION_SECONDS * 1_000;
|
|
12
13
|
|
|
13
14
|
function createVoiceHandler({
|
|
14
15
|
sendCodexRequest,
|
|
@@ -95,7 +96,7 @@ async function transcribeVoice(
|
|
|
95
96
|
throw voiceError("invalid_duration", "Voice messages must include a positive duration.");
|
|
96
97
|
}
|
|
97
98
|
if (durationMs > MAX_DURATION_MS) {
|
|
98
|
-
throw voiceError("duration_too_long",
|
|
99
|
+
throw voiceError("duration_too_long", `Voice messages are limited to ${MAX_DURATION_SECONDS} seconds.`);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
const audioBuffer = decodeAudioBase64(params.audioBase64);
|
|
@@ -283,7 +284,36 @@ function normalizeBase64(value) {
|
|
|
283
284
|
}
|
|
284
285
|
|
|
285
286
|
function isLikelyBase64(value) {
|
|
286
|
-
|
|
287
|
+
if (typeof value !== "string" || value.length === 0 || value.length % 4 !== 0) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const paddingStart = value.indexOf("=");
|
|
292
|
+
if (paddingStart !== -1) {
|
|
293
|
+
const paddingLength = value.length - paddingStart;
|
|
294
|
+
if (paddingLength > 2) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
for (let i = paddingStart; i < value.length; i += 1) {
|
|
298
|
+
if (value[i] !== "=") {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Avoid one giant regex: V8 can overflow its stack on multi-MB voice clips.
|
|
305
|
+
const dataEnd = paddingStart === -1 ? value.length : paddingStart;
|
|
306
|
+
for (let i = 0; i < dataEnd; i += 1) {
|
|
307
|
+
const code = value.charCodeAt(i);
|
|
308
|
+
const isUppercase = code >= 65 && code <= 90;
|
|
309
|
+
const isLowercase = code >= 97 && code <= 122;
|
|
310
|
+
const isDigit = code >= 48 && code <= 57;
|
|
311
|
+
if (!isUppercase && !isLowercase && !isDigit && value[i] !== "+" && value[i] !== "/") {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return true;
|
|
287
317
|
}
|
|
288
318
|
|
|
289
319
|
function hasRiffWaveHeader(buffer) {
|