@makerbi/remodex 1.5.0 → 1.5.2
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-jsonl-diagnose.js +501 -0
- package/bin/remodex.js +41 -2
- package/package.json +4 -2
- package/src/bridge.js +748 -52
- package/src/macos-launch-agent.js +44 -0
- package/src/secure-device-state.js +36 -8
- package/src/session-jsonl-history.js +197 -0
|
@@ -127,13 +127,19 @@ function stopMacOSBridgeService({
|
|
|
127
127
|
platform = process.platform,
|
|
128
128
|
execFileSyncImpl = execFileSync,
|
|
129
129
|
fsImpl = fs,
|
|
130
|
+
processImpl = process,
|
|
130
131
|
} = {}) {
|
|
131
132
|
assertDarwinPlatform(platform);
|
|
133
|
+
const previousStatus = readBridgeStatus({ env, fsImpl });
|
|
132
134
|
bootoutLaunchAgent({
|
|
133
135
|
env,
|
|
134
136
|
execFileSyncImpl,
|
|
135
137
|
ignoreMissing: true,
|
|
136
138
|
});
|
|
139
|
+
terminateRecordedBridgeProcess(previousStatus, {
|
|
140
|
+
execFileSyncImpl,
|
|
141
|
+
processImpl,
|
|
142
|
+
});
|
|
137
143
|
clearPairingSession({ env, fsImpl });
|
|
138
144
|
clearBridgeStatus({ env, fsImpl });
|
|
139
145
|
}
|
|
@@ -156,6 +162,44 @@ function resetMacOSBridgePairing({
|
|
|
156
162
|
return resetBridgePairingImpl();
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
// Stops orphaned run-service processes left behind when launchd reports the job missing.
|
|
166
|
+
function terminateRecordedBridgeProcess(status, {
|
|
167
|
+
execFileSyncImpl = execFileSync,
|
|
168
|
+
processImpl = process,
|
|
169
|
+
} = {}) {
|
|
170
|
+
const pid = Number(status?.pid);
|
|
171
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === processImpl.pid) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!isRecordedRemodexBridgeProcess(pid, { execFileSyncImpl })) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
processImpl.kill(pid, "SIGTERM");
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Checks the command line before killing so stale status files cannot target unrelated processes.
|
|
188
|
+
function isRecordedRemodexBridgeProcess(pid, { execFileSyncImpl = execFileSync } = {}) {
|
|
189
|
+
try {
|
|
190
|
+
const command = execFileSyncImpl("ps", [
|
|
191
|
+
"-p",
|
|
192
|
+
String(pid),
|
|
193
|
+
"-o",
|
|
194
|
+
"command=",
|
|
195
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
196
|
+
return command.includes("remodex")
|
|
197
|
+
&& command.includes("run-service");
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
159
203
|
function getMacOSBridgeServiceStatus({
|
|
160
204
|
env = process.env,
|
|
161
205
|
platform = process.platform,
|
|
@@ -14,6 +14,7 @@ const DEFAULT_STORE_DIR = path.join(os.homedir(), ".remodex");
|
|
|
14
14
|
const DEFAULT_STORE_FILE = path.join(DEFAULT_STORE_DIR, "device-state.json");
|
|
15
15
|
const KEYCHAIN_SERVICE = "com.remodex.bridge.device-state";
|
|
16
16
|
const KEYCHAIN_ACCOUNT = "default";
|
|
17
|
+
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
17
18
|
let hasLoggedKeychainMismatch = false;
|
|
18
19
|
|
|
19
20
|
// Loads the canonical bridge state or bootstraps a fresh one when no trusted state exists yet.
|
|
@@ -22,8 +23,11 @@ function loadOrCreateBridgeDeviceState() {
|
|
|
22
23
|
const keychainRecord = readKeychainStateRecord();
|
|
23
24
|
|
|
24
25
|
if (fileRecord.state) {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const canonicalState = recoverBridgeDeviceIdentity(fileRecord.state, {
|
|
27
|
+
fallbackState: keychainRecord.state,
|
|
28
|
+
});
|
|
29
|
+
reconcileLegacyKeychainMirror(canonicalState, keychainRecord);
|
|
30
|
+
return canonicalState;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
if (fileRecord.error) {
|
|
@@ -31,8 +35,9 @@ function loadOrCreateBridgeDeviceState() {
|
|
|
31
35
|
warnOnce(
|
|
32
36
|
"[remodex] Recovering the canonical device-state.json from the legacy Keychain pairing mirror."
|
|
33
37
|
);
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
const recoveredState = recoverBridgeDeviceIdentity(keychainRecord.state);
|
|
39
|
+
writeBridgeDeviceState(recoveredState);
|
|
40
|
+
return recoveredState;
|
|
36
41
|
}
|
|
37
42
|
throw corruptedStateError("device-state.json", fileRecord.error);
|
|
38
43
|
}
|
|
@@ -47,8 +52,9 @@ function loadOrCreateBridgeDeviceState() {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
if (keychainRecord.state) {
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
const recoveredState = recoverBridgeDeviceIdentity(keychainRecord.state);
|
|
56
|
+
writeBridgeDeviceState(recoveredState);
|
|
57
|
+
return recoveredState;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
const nextState = createBridgeDeviceState();
|
|
@@ -86,7 +92,7 @@ function resolveBridgeRelaySession(state, { persist = true } = {}) {
|
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
// Persists
|
|
95
|
+
// Persists trusted mobile identities so reconnects can be authenticated across paired iPhone/iPad clients.
|
|
90
96
|
function rememberTrustedPhone(state, phoneDeviceId, phoneIdentityPublicKey, { persist = true } = {}) {
|
|
91
97
|
const normalizedDeviceId = normalizeNonEmptyString(phoneDeviceId);
|
|
92
98
|
const normalizedPublicKey = normalizeNonEmptyString(phoneIdentityPublicKey);
|
|
@@ -94,10 +100,10 @@ function rememberTrustedPhone(state, phoneDeviceId, phoneIdentityPublicKey, { pe
|
|
|
94
100
|
return state;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
// Remodex supports one trusted iPhone per Mac, so a new trust record replaces old ones.
|
|
98
103
|
const nextState = normalizeBridgeDeviceState({
|
|
99
104
|
...state,
|
|
100
105
|
trustedPhones: {
|
|
106
|
+
...(state?.trustedPhones || {}),
|
|
101
107
|
[normalizedDeviceId]: normalizedPublicKey,
|
|
102
108
|
},
|
|
103
109
|
});
|
|
@@ -382,10 +388,32 @@ function normalizeBridgeDeviceState(rawState) {
|
|
|
382
388
|
};
|
|
383
389
|
}
|
|
384
390
|
|
|
391
|
+
function recoverBridgeDeviceIdentity(state, { fallbackState = null } = {}) {
|
|
392
|
+
if (isValidBridgeDeviceId(state?.macDeviceId)) {
|
|
393
|
+
return state;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (fallbackState && isValidBridgeDeviceId(fallbackState.macDeviceId)) {
|
|
397
|
+
warnOnce("[remodex] Recovering an invalid saved bridge device identity from the legacy Keychain mirror.");
|
|
398
|
+
writeBridgeDeviceState(fallbackState);
|
|
399
|
+
return fallbackState;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
warnOnce("[remodex] Rotating an invalid saved bridge device identity; scan the fresh QR to pair again.");
|
|
403
|
+
const nextState = createBridgeDeviceState();
|
|
404
|
+
nextState.lastSeenPhoneAppVersion = state?.lastSeenPhoneAppVersion || null;
|
|
405
|
+
writeBridgeDeviceState(nextState);
|
|
406
|
+
return nextState;
|
|
407
|
+
}
|
|
408
|
+
|
|
385
409
|
function bridgeStatesEqual(left, right) {
|
|
386
410
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
387
411
|
}
|
|
388
412
|
|
|
413
|
+
function isValidBridgeDeviceId(value) {
|
|
414
|
+
return UUID_V4_PATTERN.test(normalizeNonEmptyString(value));
|
|
415
|
+
}
|
|
416
|
+
|
|
389
417
|
function normalizeNonEmptyString(value) {
|
|
390
418
|
if (typeof value !== "string") {
|
|
391
419
|
return "";
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// FILE: session-jsonl-history.js
|
|
2
|
+
// Purpose: Reconstructs a small thread/turns/list page from local Codex session JSONL files.
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
function readThreadTurnsListPageFromSessionJsonl(filePath, {
|
|
7
|
+
threadId = "",
|
|
8
|
+
limit = 5,
|
|
9
|
+
maxLimit = 5,
|
|
10
|
+
cursor = null,
|
|
11
|
+
fsModule = fs,
|
|
12
|
+
} = {}) {
|
|
13
|
+
if (!filePath || cursor != null) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const content = fsModule.readFileSync(filePath, "utf8");
|
|
18
|
+
const turns = parseSessionJsonlTurns(content, { threadId });
|
|
19
|
+
if (turns.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const requestedLimit = Number.isInteger(limit) && limit > 0 ? limit : 5;
|
|
24
|
+
const requestedMaxLimit = Number.isInteger(maxLimit) && maxLimit > 0 ? maxLimit : 5;
|
|
25
|
+
const safeLimit = Math.min(requestedLimit, requestedMaxLimit, 5);
|
|
26
|
+
const pageTurns = turns.slice(-safeLimit).reverse();
|
|
27
|
+
return {
|
|
28
|
+
data: pageTurns,
|
|
29
|
+
nextCursor: turns.length > pageTurns.length ? "remodex-jsonl-fallback-older-unavailable" : null,
|
|
30
|
+
remodexJsonlFallback: true,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
|
|
35
|
+
const turns = [];
|
|
36
|
+
const turnsById = new Map();
|
|
37
|
+
let activeTurnId = "";
|
|
38
|
+
let sessionThreadId = normalizeString(threadId);
|
|
39
|
+
|
|
40
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
41
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
42
|
+
const line = lines[index].trim();
|
|
43
|
+
if (!line) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let entry;
|
|
48
|
+
try {
|
|
49
|
+
entry = JSON.parse(line);
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (entry?.type === "session_meta") {
|
|
55
|
+
const payload = objectValue(entry.payload);
|
|
56
|
+
sessionThreadId ||= normalizeString(payload?.id)
|
|
57
|
+
|| normalizeString(payload?.thread_id)
|
|
58
|
+
|| normalizeString(payload?.threadId);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (entry?.type === "event_msg") {
|
|
63
|
+
const payload = objectValue(entry.payload);
|
|
64
|
+
const eventType = normalizeString(payload?.type);
|
|
65
|
+
if (eventType === "task_started") {
|
|
66
|
+
activeTurnId = normalizeString(payload?.turn_id)
|
|
67
|
+
|| normalizeString(payload?.turnId)
|
|
68
|
+
|| activeTurnId
|
|
69
|
+
|| `turn-line-${index + 1}`;
|
|
70
|
+
ensureTurn(turns, turnsById, activeTurnId, sessionThreadId, entry.timestamp);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (eventType === "task_complete") {
|
|
75
|
+
const turn = ensureTurn(
|
|
76
|
+
turns,
|
|
77
|
+
turnsById,
|
|
78
|
+
normalizeString(payload?.turn_id) || normalizeString(payload?.turnId) || activeTurnId || `turn-line-${index + 1}`,
|
|
79
|
+
sessionThreadId,
|
|
80
|
+
entry.timestamp
|
|
81
|
+
);
|
|
82
|
+
turn.status = "completed";
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (eventType === "user_message") {
|
|
87
|
+
const turn = ensureTurn(
|
|
88
|
+
turns,
|
|
89
|
+
turnsById,
|
|
90
|
+
normalizeString(payload?.turn_id) || normalizeString(payload?.turnId) || activeTurnId || `turn-line-${index + 1}`,
|
|
91
|
+
sessionThreadId,
|
|
92
|
+
entry.timestamp
|
|
93
|
+
);
|
|
94
|
+
turn.items.push({
|
|
95
|
+
id: normalizeString(payload?.id) || `user-message-line-${index + 1}`,
|
|
96
|
+
type: "user_message",
|
|
97
|
+
role: "user",
|
|
98
|
+
text: normalizeString(payload?.message) || normalizeString(payload?.text),
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The final assistant text is usually present again as a response_item message.
|
|
104
|
+
// Skipping event agent_message avoids double-rendering streaming/final chunks.
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (entry?.type === "response_item") {
|
|
109
|
+
const payload = objectValue(entry.payload);
|
|
110
|
+
if (!payload) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const turn = ensureTurn(
|
|
114
|
+
turns,
|
|
115
|
+
turnsById,
|
|
116
|
+
normalizeString(payload.turn_id) || normalizeString(payload.turnId) || activeTurnId || `turn-line-${index + 1}`,
|
|
117
|
+
sessionThreadId,
|
|
118
|
+
entry.timestamp
|
|
119
|
+
);
|
|
120
|
+
const item = normalizeResponseItemForHistory(payload, index + 1);
|
|
121
|
+
if (item) {
|
|
122
|
+
turn.items.push(item);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return turns.filter((turn) => turn.items.length > 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ensureTurn(turns, turnsById, turnId, threadId, timestamp) {
|
|
131
|
+
const normalizedTurnId = normalizeString(turnId) || `turn-${turns.length + 1}`;
|
|
132
|
+
let turn = turnsById.get(normalizedTurnId);
|
|
133
|
+
if (!turn) {
|
|
134
|
+
turn = {
|
|
135
|
+
id: normalizedTurnId,
|
|
136
|
+
threadId: normalizeString(threadId) || undefined,
|
|
137
|
+
createdAt: normalizeString(timestamp) || undefined,
|
|
138
|
+
status: "running",
|
|
139
|
+
items: [],
|
|
140
|
+
};
|
|
141
|
+
turnsById.set(normalizedTurnId, turn);
|
|
142
|
+
turns.push(turn);
|
|
143
|
+
}
|
|
144
|
+
if (!turn.createdAt && timestamp) {
|
|
145
|
+
turn.createdAt = normalizeString(timestamp);
|
|
146
|
+
}
|
|
147
|
+
return turn;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeResponseItemForHistory(payload, lineNumber) {
|
|
151
|
+
const type = normalizeHistoryItemType(payload.type);
|
|
152
|
+
if (!type) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const item = {
|
|
157
|
+
...payload,
|
|
158
|
+
id: normalizeString(payload.id)
|
|
159
|
+
|| normalizeString(payload.item_id)
|
|
160
|
+
|| normalizeString(payload.itemId)
|
|
161
|
+
|| `response-item-line-${lineNumber}`,
|
|
162
|
+
type,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (type === "message" && !normalizeString(item.role)) {
|
|
166
|
+
item.role = "assistant";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return item;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeHistoryItemType(rawType) {
|
|
173
|
+
const normalized = normalizeString(rawType).toLowerCase().replace(/[\s_-]+/g, "");
|
|
174
|
+
if (!normalized) {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
if (normalized === "functioncall") {
|
|
178
|
+
return "tool_call";
|
|
179
|
+
}
|
|
180
|
+
if (normalized === "functioncalloutput") {
|
|
181
|
+
return "tool_call_output";
|
|
182
|
+
}
|
|
183
|
+
return rawType;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function objectValue(value) {
|
|
187
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeString(value) {
|
|
191
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
parseSessionJsonlTurns,
|
|
196
|
+
readThreadTurnsListPageFromSessionJsonl,
|
|
197
|
+
};
|