@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/voice-handler.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// FILE: voice-handler.js
|
|
2
2
|
// Purpose: Handles bridge-owned voice transcription requests without exposing auth tokens to iPhone.
|
|
3
3
|
// Layer: Bridge handler
|
|
4
|
-
// Exports: createVoiceHandler
|
|
4
|
+
// Exports: createVoiceHandler, resolveVoiceAuth
|
|
5
5
|
// Depends on: global fetch/FormData/Blob, local codex app-server auth via sendCodexRequest
|
|
6
6
|
|
|
7
|
+
const OPENAI_TRANSCRIPTIONS_URL = "https://api.openai.com/v1/audio/transcriptions";
|
|
7
8
|
const CHATGPT_TRANSCRIPTIONS_URL = "https://chatgpt.com/backend-api/transcribe";
|
|
9
|
+
const DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe";
|
|
8
10
|
const MAX_AUDIO_BYTES = 10 * 1024 * 1024;
|
|
9
11
|
const MAX_DURATION_MS = 120_000;
|
|
10
12
|
|
|
@@ -14,6 +16,7 @@ function createVoiceHandler({
|
|
|
14
16
|
FormDataImpl = globalThis.FormData,
|
|
15
17
|
BlobImpl = globalThis.Blob,
|
|
16
18
|
logPrefix = "[remodex]",
|
|
19
|
+
env = process.env,
|
|
17
20
|
} = {}) {
|
|
18
21
|
function handleVoiceRequest(rawMessage, sendResponse) {
|
|
19
22
|
let parsed;
|
|
@@ -36,6 +39,7 @@ function createVoiceHandler({
|
|
|
36
39
|
fetchImpl,
|
|
37
40
|
FormDataImpl,
|
|
38
41
|
BlobImpl,
|
|
42
|
+
env,
|
|
39
43
|
})
|
|
40
44
|
.then((result) => {
|
|
41
45
|
sendResponse(JSON.stringify({ id, result }));
|
|
@@ -67,7 +71,7 @@ function createVoiceHandler({
|
|
|
67
71
|
// Validates iPhone-owned audio input and proxies it to the official transcription endpoint.
|
|
68
72
|
async function transcribeVoice(
|
|
69
73
|
params,
|
|
70
|
-
{ sendCodexRequest, fetchImpl, FormDataImpl, BlobImpl }
|
|
74
|
+
{ sendCodexRequest, fetchImpl, FormDataImpl, BlobImpl, env = process.env }
|
|
71
75
|
) {
|
|
72
76
|
if (typeof sendCodexRequest !== "function") {
|
|
73
77
|
throw voiceError("bridge_not_ready", "Voice transcription is not available right now.");
|
|
@@ -98,8 +102,18 @@ async function transcribeVoice(
|
|
|
98
102
|
if (audioBuffer.length > MAX_AUDIO_BYTES) {
|
|
99
103
|
throw voiceError("audio_too_large", "Voice messages are limited to 10 MB.");
|
|
100
104
|
}
|
|
105
|
+
const wavInfo = readWavInfo(audioBuffer);
|
|
106
|
+
if (!wavInfo) {
|
|
107
|
+
throw voiceError("invalid_audio", "The recorded audio is not a valid WAV file.");
|
|
108
|
+
}
|
|
109
|
+
if (wavInfo.audioFormat !== 1
|
|
110
|
+
|| wavInfo.channelCount !== 1
|
|
111
|
+
|| wavInfo.sampleRateHz !== 24_000
|
|
112
|
+
|| wavInfo.bitsPerSample !== 16) {
|
|
113
|
+
throw voiceError("unsupported_sample_rate", "Voice transcription requires 24 kHz mono WAV audio.");
|
|
114
|
+
}
|
|
101
115
|
|
|
102
|
-
const authContext = await loadAuthContext(sendCodexRequest);
|
|
116
|
+
const authContext = await loadAuthContext(sendCodexRequest, { env });
|
|
103
117
|
return requestTranscription({
|
|
104
118
|
authContext,
|
|
105
119
|
audioBuffer,
|
|
@@ -108,6 +122,7 @@ async function transcribeVoice(
|
|
|
108
122
|
FormDataImpl,
|
|
109
123
|
BlobImpl,
|
|
110
124
|
sendCodexRequest,
|
|
125
|
+
env,
|
|
111
126
|
});
|
|
112
127
|
}
|
|
113
128
|
|
|
@@ -119,10 +134,14 @@ async function requestTranscription({
|
|
|
119
134
|
FormDataImpl,
|
|
120
135
|
BlobImpl,
|
|
121
136
|
sendCodexRequest,
|
|
137
|
+
env,
|
|
122
138
|
}) {
|
|
123
139
|
const makeAttempt = async (activeAuthContext) => {
|
|
124
140
|
const formData = new FormDataImpl();
|
|
125
141
|
formData.append("file", new BlobImpl([audioBuffer], { type: mimeType }), "voice.wav");
|
|
142
|
+
if (!activeAuthContext.isChatGPT) {
|
|
143
|
+
formData.append("model", DEFAULT_TRANSCRIPTION_MODEL);
|
|
144
|
+
}
|
|
126
145
|
|
|
127
146
|
const headers = {
|
|
128
147
|
Authorization: `Bearer ${activeAuthContext.token}`,
|
|
@@ -135,10 +154,20 @@ async function requestTranscription({
|
|
|
135
154
|
});
|
|
136
155
|
};
|
|
137
156
|
|
|
138
|
-
let
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
let activeAuthContext = authContext;
|
|
158
|
+
let response = await makeAttempt(activeAuthContext);
|
|
159
|
+
if (response.status === 401 || response.status === 403) {
|
|
160
|
+
activeAuthContext = await loadAuthContext(sendCodexRequest, { env });
|
|
161
|
+
response = await makeAttempt(activeAuthContext);
|
|
162
|
+
if (!response.ok
|
|
163
|
+
&& (response.status === 401 || response.status === 403)
|
|
164
|
+
&& activeAuthContext.isChatGPT) {
|
|
165
|
+
const apiKeyContext = loadEnvApiKeyAuthContext(env);
|
|
166
|
+
if (apiKeyContext) {
|
|
167
|
+
activeAuthContext = apiKeyContext;
|
|
168
|
+
response = await makeAttempt(activeAuthContext);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
142
171
|
}
|
|
143
172
|
|
|
144
173
|
if (!response.ok) {
|
|
@@ -154,7 +183,10 @@ async function requestTranscription({
|
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
if (response.status === 401 || response.status === 403) {
|
|
157
|
-
|
|
186
|
+
const message = activeAuthContext.isChatGPT
|
|
187
|
+
? "Your ChatGPT login has expired. Sign in again."
|
|
188
|
+
: "Your OpenAI API key was rejected. Update the API key on the Mac, then try again.";
|
|
189
|
+
throw voiceError("auth_rejected", message);
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
throw voiceError("transcription_failed", errorMessage);
|
|
@@ -170,32 +202,55 @@ async function requestTranscription({
|
|
|
170
202
|
}
|
|
171
203
|
|
|
172
204
|
// Reads the current bridge-owned auth state from the local codex app-server and refreshes if needed.
|
|
173
|
-
async function loadAuthContext(sendCodexRequest) {
|
|
174
|
-
const authStatus = await sendCodexRequest
|
|
175
|
-
includeToken: true,
|
|
176
|
-
refreshToken: true,
|
|
177
|
-
});
|
|
205
|
+
async function loadAuthContext(sendCodexRequest, { env = process.env } = {}) {
|
|
206
|
+
const authStatus = await readVoiceAuthStatus(sendCodexRequest);
|
|
178
207
|
|
|
179
208
|
const authMethod = readString(authStatus?.authMethod);
|
|
180
|
-
const token =
|
|
181
|
-
const isChatGPT = authMethod
|
|
209
|
+
const token = normalizeBearerToken(authStatus?.authToken);
|
|
210
|
+
const isChatGPT = isChatGPTAuthMethod(authMethod);
|
|
182
211
|
|
|
183
212
|
if (!token) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
const apiKeyContext = loadEnvApiKeyAuthContext(env);
|
|
214
|
+
if (apiKeyContext) {
|
|
215
|
+
return apiKeyContext;
|
|
216
|
+
}
|
|
217
|
+
throw voiceError("not_authenticated", "Sign in with ChatGPT or configure an OpenAI API key before using voice transcription.");
|
|
188
218
|
}
|
|
189
219
|
|
|
190
220
|
return {
|
|
191
221
|
authMethod,
|
|
192
222
|
token,
|
|
193
223
|
isChatGPT,
|
|
194
|
-
transcriptionURL: CHATGPT_TRANSCRIPTIONS_URL,
|
|
195
|
-
chatgptAccountId: readChatGPTAccountIdFromToken(token),
|
|
224
|
+
transcriptionURL: isChatGPT ? CHATGPT_TRANSCRIPTIONS_URL : OPENAI_TRANSCRIPTIONS_URL,
|
|
196
225
|
};
|
|
197
226
|
}
|
|
198
227
|
|
|
228
|
+
function loadEnvApiKeyAuthContext(env = process.env) {
|
|
229
|
+
const token = normalizeBearerToken(env?.OPENAI_API_KEY);
|
|
230
|
+
if (!token) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
authMethod: "apiKey",
|
|
236
|
+
token,
|
|
237
|
+
isChatGPT: false,
|
|
238
|
+
transcriptionURL: OPENAI_TRANSCRIPTIONS_URL,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function readVoiceAuthStatus(sendCodexRequest) {
|
|
243
|
+
try {
|
|
244
|
+
return await sendCodexRequest("getAuthStatus", {
|
|
245
|
+
includeToken: true,
|
|
246
|
+
refreshToken: true,
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(`[remodex] voice auth: getAuthStatus RPC failed: ${err.message}`);
|
|
250
|
+
throw voiceError("auth_unavailable", "Could not read OpenAI auth from the Mac runtime. Is the bridge running?");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
199
254
|
function decodeAudioBase64(value) {
|
|
200
255
|
const normalized = normalizeBase64(value);
|
|
201
256
|
if (!normalized) {
|
|
@@ -215,7 +270,7 @@ function decodeAudioBase64(value) {
|
|
|
215
270
|
throw voiceError("invalid_audio", "The recorded audio could not be decoded.");
|
|
216
271
|
}
|
|
217
272
|
|
|
218
|
-
if (!
|
|
273
|
+
if (!hasRiffWaveHeader(audioBuffer)) {
|
|
219
274
|
throw voiceError("invalid_audio", "The recorded audio is not a valid WAV file.");
|
|
220
275
|
}
|
|
221
276
|
|
|
@@ -231,45 +286,68 @@ function isLikelyBase64(value) {
|
|
|
231
286
|
return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
|
|
232
287
|
}
|
|
233
288
|
|
|
234
|
-
function
|
|
289
|
+
function hasRiffWaveHeader(buffer) {
|
|
235
290
|
return buffer.length >= 44
|
|
236
291
|
&& buffer.toString("ascii", 0, 4) === "RIFF"
|
|
237
292
|
&& buffer.toString("ascii", 8, 12) === "WAVE";
|
|
238
293
|
}
|
|
239
294
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return readString(
|
|
244
|
-
authClaim?.chatgpt_account_id
|
|
245
|
-
|| authClaim?.chatgptAccountId
|
|
246
|
-
|| payload?.chatgpt_account_id
|
|
247
|
-
|| payload?.chatgptAccountId
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function decodeJWTPayload(token) {
|
|
252
|
-
const segments = typeof token === "string" ? token.split(".") : [];
|
|
253
|
-
if (segments.length < 2) {
|
|
295
|
+
// Parses chunked WAV metadata so extra chunks before fmt/data do not break valid clips.
|
|
296
|
+
function readWavInfo(buffer) {
|
|
297
|
+
if (!hasRiffWaveHeader(buffer)) {
|
|
254
298
|
return null;
|
|
255
299
|
}
|
|
256
300
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
301
|
+
let offset = 12;
|
|
302
|
+
let info = null;
|
|
303
|
+
let hasData = false;
|
|
304
|
+
while (offset + 8 <= buffer.length) {
|
|
305
|
+
const chunkId = buffer.toString("ascii", offset, offset + 4);
|
|
306
|
+
const chunkSize = buffer.readUInt32LE(offset + 4);
|
|
307
|
+
const payloadStart = offset + 8;
|
|
308
|
+
const payloadEnd = payloadStart + chunkSize;
|
|
309
|
+
if (payloadEnd > buffer.length) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
261
312
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
313
|
+
if (chunkId === "fmt ") {
|
|
314
|
+
if (chunkSize < 16) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
info = {
|
|
318
|
+
audioFormat: buffer.readUInt16LE(payloadStart),
|
|
319
|
+
channelCount: buffer.readUInt16LE(payloadStart + 2),
|
|
320
|
+
sampleRateHz: buffer.readUInt32LE(payloadStart + 4),
|
|
321
|
+
bitsPerSample: buffer.readUInt16LE(payloadStart + 14),
|
|
322
|
+
};
|
|
323
|
+
} else if (chunkId === "data") {
|
|
324
|
+
hasData = chunkSize > 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
offset = payloadEnd + (chunkSize % 2);
|
|
266
328
|
}
|
|
329
|
+
|
|
330
|
+
return info && hasData ? info : null;
|
|
267
331
|
}
|
|
268
332
|
|
|
269
333
|
function readString(value) {
|
|
270
334
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
271
335
|
}
|
|
272
336
|
|
|
337
|
+
function normalizeBearerToken(value) {
|
|
338
|
+
const token = readString(value);
|
|
339
|
+
if (!token) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const match = token.match(/^bearer\s+(.+)$/i);
|
|
343
|
+
return match ? match[1].trim() : token;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isChatGPTAuthMethod(value) {
|
|
347
|
+
const normalized = readString(value)?.toLowerCase().replace(/[^a-z0-9]/g, "") || "";
|
|
348
|
+
return normalized.includes("chatgpt");
|
|
349
|
+
}
|
|
350
|
+
|
|
273
351
|
function readPositiveNumber(value) {
|
|
274
352
|
const numericValue = typeof value === "number" ? value : Number(value);
|
|
275
353
|
return Number.isFinite(numericValue) && numericValue >= 0 ? numericValue : 0;
|
|
@@ -282,33 +360,22 @@ function voiceError(errorCode, userMessage) {
|
|
|
282
360
|
return error;
|
|
283
361
|
}
|
|
284
362
|
|
|
285
|
-
//
|
|
286
|
-
// Uses its own token resolution instead of loadAuthContext so errors are specific and actionable.
|
|
363
|
+
// Serves older phone builds that upload directly to ChatGPT with a Mac-owned token.
|
|
287
364
|
async function resolveVoiceAuth(sendCodexRequest) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
authStatus = await sendCodexRequest("getAuthStatus", {
|
|
291
|
-
includeToken: true,
|
|
292
|
-
refreshToken: true,
|
|
293
|
-
});
|
|
294
|
-
} catch (err) {
|
|
295
|
-
console.error(`[remodex] voice/resolveAuth: getAuthStatus RPC failed: ${err.message}`);
|
|
296
|
-
throw voiceError("auth_unavailable", "Could not read ChatGPT session from the Mac runtime. Is the bridge running?");
|
|
365
|
+
if (typeof sendCodexRequest !== "function") {
|
|
366
|
+
throw voiceError("bridge_not_ready", "Voice transcription is not available right now.");
|
|
297
367
|
}
|
|
298
368
|
|
|
369
|
+
const authStatus = await readVoiceAuthStatus(sendCodexRequest);
|
|
299
370
|
const authMethod = readString(authStatus?.authMethod);
|
|
300
|
-
const token =
|
|
301
|
-
const isChatGPT = authMethod
|
|
371
|
+
const token = normalizeBearerToken(authStatus?.authToken);
|
|
372
|
+
const isChatGPT = isChatGPTAuthMethod(authMethod);
|
|
302
373
|
|
|
303
|
-
// Check for a usable ChatGPT token first. The runtime may set requiresOpenaiAuth
|
|
304
|
-
// even when a valid ChatGPT session is present (the flag is about the runtime's
|
|
305
|
-
// preferred auth mode, not whether ChatGPT tokens are actually available).
|
|
306
374
|
if (isChatGPT && token) {
|
|
307
375
|
return { token };
|
|
308
376
|
}
|
|
309
377
|
|
|
310
378
|
if (!token) {
|
|
311
|
-
console.error(`[remodex] voice/resolveAuth: no token. authMethod=${authMethod || "none"} requiresOpenaiAuth=${authStatus?.requiresOpenaiAuth}`);
|
|
312
379
|
throw voiceError("token_missing", "No ChatGPT session token available. Sign in to ChatGPT on the Mac.");
|
|
313
380
|
}
|
|
314
381
|
|