@makerbi/remodex 1.4.0 → 1.5.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/package.json +6 -1
- package/src/bridge.js +425 -68
- package/src/desktop-handler.js +100 -11
- package/src/desktop-ipc-action-follower.js +42 -12
- package/src/git-handler.js +48 -4
- package/src/macos-launch-agent.js +2 -2
- package/src/pet-handler.js +537 -0
- package/src/qr.js +21 -2
- package/src/rollout-watch.js +17 -7
- package/src/workspace-handler.js +312 -4
- package/src/private-defaults.json +0 -4
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
// FILE: pet-handler.js
|
|
2
|
+
// Purpose: Lists Codex-compatible local pet packages for the mobile companion overlay.
|
|
3
|
+
// Layer: Bridge handler
|
|
4
|
+
// Exports: handlePetRequest, handlePetMethod
|
|
5
|
+
// Depends on: fs, path, ./codex-home
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { resolveCodexHome } = require("./codex-home");
|
|
10
|
+
|
|
11
|
+
const ATLAS_WIDTH = 1536;
|
|
12
|
+
const ATLAS_HEIGHT = 1872;
|
|
13
|
+
const MAX_SPRITESHEET_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
const IMAGE_MIME_TYPES_BY_EXTENSION = new Map([
|
|
15
|
+
[".png", "image/png"],
|
|
16
|
+
[".webp", "image/webp"],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
function handlePetRequest(rawMessage, sendResponse) {
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(rawMessage);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
|
|
28
|
+
if (!isPetMethod(method)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const id = parsed.id;
|
|
33
|
+
const params = parsed.params || {};
|
|
34
|
+
handlePetMethod(method, params)
|
|
35
|
+
.then((result) => {
|
|
36
|
+
sendResponse(JSON.stringify({ id, result }));
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
const errorCode = err.errorCode || "pet_error";
|
|
40
|
+
const message = err.userMessage || err.message || "Unable to load Codex pets.";
|
|
41
|
+
sendResponse(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
id,
|
|
44
|
+
error: {
|
|
45
|
+
code: -32000,
|
|
46
|
+
message,
|
|
47
|
+
data: { errorCode },
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function handlePetMethod(method, params = {}) {
|
|
57
|
+
if (!isPetMethod(method)) {
|
|
58
|
+
throw petError("pet_method_unknown", "Unknown Codex pet method.");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (method === "pet/read" || method === "custom-avatar/read") {
|
|
62
|
+
return readPet(params);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const codexHome = resolveCodexHome();
|
|
66
|
+
const includeData = params.includeData !== false && params.metadataOnly !== true;
|
|
67
|
+
const results = [];
|
|
68
|
+
const errors = [];
|
|
69
|
+
|
|
70
|
+
for (const directory of petDirectories(codexHome)) {
|
|
71
|
+
const discovered = await loadPetDirectory(directory, { includeData });
|
|
72
|
+
results.push(...discovered.avatars);
|
|
73
|
+
errors.push(...discovered.errors);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const avatars = mergeCustomAvatars(results);
|
|
77
|
+
return {
|
|
78
|
+
avatarDirectory: path.join(codexHome, "pets"),
|
|
79
|
+
petDirectory: path.join(codexHome, "pets"),
|
|
80
|
+
avatars,
|
|
81
|
+
pets: avatars,
|
|
82
|
+
errors,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Reads a single selected pet so mobile does not have to embed every atlas in pet/list.
|
|
87
|
+
async function readPet(params = {}) {
|
|
88
|
+
const codexHome = resolveCodexHome();
|
|
89
|
+
const folderName = petFolderNameFromID(params.id || params.folderName);
|
|
90
|
+
let lastError = null;
|
|
91
|
+
|
|
92
|
+
for (const directory of petDirectories(codexHome)) {
|
|
93
|
+
try {
|
|
94
|
+
const avatar = await loadCustomAvatar(directory, folderName, { includeData: true });
|
|
95
|
+
if (avatar) {
|
|
96
|
+
return avatar;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
lastError = error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (lastError) {
|
|
104
|
+
throw lastError;
|
|
105
|
+
}
|
|
106
|
+
throw petError("pet_not_found", "The selected Codex pet could not be found.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPetMethod(method) {
|
|
110
|
+
return method === "pet/list"
|
|
111
|
+
|| method === "custom-avatars"
|
|
112
|
+
|| method === "pet/read"
|
|
113
|
+
|| method === "custom-avatar/read";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Mirrors Codex desktop's pets-first custom avatar lookup while keeping legacy avatars usable.
|
|
117
|
+
function petDirectories(codexHome) {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
root: path.join(codexHome, "pets"),
|
|
121
|
+
manifestName: "pet.json",
|
|
122
|
+
kind: "pet",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
root: path.join(codexHome, "avatars"),
|
|
126
|
+
manifestName: "avatar.json",
|
|
127
|
+
kind: "avatar",
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Accept only a local package id/folder name; the spritesheet path is validated separately.
|
|
133
|
+
function petFolderNameFromID(rawID) {
|
|
134
|
+
if (typeof rawID !== "string") {
|
|
135
|
+
throw petError("pet_id_invalid", "A pet id is required.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const folderName = rawID.startsWith("custom:") ? rawID.slice("custom:".length) : rawID;
|
|
139
|
+
if (
|
|
140
|
+
!folderName
|
|
141
|
+
|| folderName === "."
|
|
142
|
+
|| folderName === ".."
|
|
143
|
+
|| path.isAbsolute(folderName)
|
|
144
|
+
|| folderName.includes("/")
|
|
145
|
+
|| folderName.includes("\\")
|
|
146
|
+
) {
|
|
147
|
+
throw petError("pet_id_invalid", "The selected pet id is invalid.");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return folderName;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function loadPetDirectory(directory, { includeData }) {
|
|
154
|
+
const avatars = [];
|
|
155
|
+
const errors = [];
|
|
156
|
+
let entries = [];
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
entries = await fs.promises.readdir(directory.root, { withFileTypes: true });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error && error.code === "ENOENT") {
|
|
162
|
+
return { avatars, errors };
|
|
163
|
+
}
|
|
164
|
+
throw petError("pet_directory_unreadable", "Could not read local Codex pet folders.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (!entry.isDirectory()) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const avatar = await loadCustomAvatar(directory, entry.name, { includeData });
|
|
174
|
+
if (avatar) {
|
|
175
|
+
avatars.push(avatar);
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
errors.push({
|
|
179
|
+
folderName: entry.name,
|
|
180
|
+
kind: directory.kind,
|
|
181
|
+
message: error.userMessage || error.message || "Invalid pet package.",
|
|
182
|
+
errorCode: error.errorCode || "invalid_pet",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { avatars, errors };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function loadCustomAvatar(directory, folderName, { includeData }) {
|
|
191
|
+
const petRoot = path.join(directory.root, folderName);
|
|
192
|
+
const manifestPath = path.join(petRoot, directory.manifestName);
|
|
193
|
+
const manifest = await readManifest(manifestPath);
|
|
194
|
+
if (!manifest) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const spritesheetPath = resolveSpritesheetPath(
|
|
199
|
+
petRoot,
|
|
200
|
+
typeof manifest.spritesheetPath === "string" ? manifest.spritesheetPath : "spritesheet.webp"
|
|
201
|
+
);
|
|
202
|
+
const image = await readValidatedSpritesheet(spritesheetPath, { includeData });
|
|
203
|
+
const displayName = firstNonEmptyString([manifest.displayName, manifest.name, displayFromSlug(folderName)]);
|
|
204
|
+
const description = firstNonEmptyString([manifest.description]) || "A custom Codex pet.";
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: `custom:${folderName}`,
|
|
208
|
+
folderName,
|
|
209
|
+
kind: directory.kind,
|
|
210
|
+
displayName,
|
|
211
|
+
description,
|
|
212
|
+
spritesheetPath,
|
|
213
|
+
spritesheetMimeType: image.mimeType,
|
|
214
|
+
spritesheetByteLength: image.byteLength,
|
|
215
|
+
spritesheetDataUrl: image.dataUrl,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function readManifest(manifestPath) {
|
|
220
|
+
let contents;
|
|
221
|
+
try {
|
|
222
|
+
contents = await fs.promises.readFile(manifestPath, "utf8");
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error && error.code === "ENOENT") {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
throw petError("pet_manifest_unreadable", "Could not read the pet manifest.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const manifest = JSON.parse(contents);
|
|
232
|
+
return manifest && typeof manifest === "object" ? manifest : {};
|
|
233
|
+
} catch {
|
|
234
|
+
throw petError("pet_manifest_invalid", "The pet manifest is not valid JSON.");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveSpritesheetPath(petRoot, rawSpritesheetPath) {
|
|
239
|
+
const trimmedPath = rawSpritesheetPath.trim() || "spritesheet.webp";
|
|
240
|
+
if (path.isAbsolute(trimmedPath)) {
|
|
241
|
+
throw petError("pet_spritesheet_path_invalid", "Pet spritesheet paths must be relative.");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const candidate = path.resolve(petRoot, trimmedPath);
|
|
245
|
+
const relative = path.relative(petRoot, candidate);
|
|
246
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
247
|
+
throw petError("pet_spritesheet_path_invalid", "Pet spritesheet paths cannot escape the pet folder.");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return candidate;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function readValidatedSpritesheet(spritesheetPath, { includeData }) {
|
|
254
|
+
const extension = path.extname(spritesheetPath).toLowerCase();
|
|
255
|
+
const mimeType = IMAGE_MIME_TYPES_BY_EXTENSION.get(extension);
|
|
256
|
+
if (!mimeType) {
|
|
257
|
+
throw petError("pet_spritesheet_type_invalid", "Pet spritesheets must be PNG or WebP files.");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!includeData) {
|
|
261
|
+
return readValidatedSpritesheetMetadata(spritesheetPath, mimeType);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const stat = await readSpritesheetStat(spritesheetPath);
|
|
265
|
+
assertSpritesheetByteLength(stat.size);
|
|
266
|
+
|
|
267
|
+
let data;
|
|
268
|
+
try {
|
|
269
|
+
data = await fs.promises.readFile(spritesheetPath);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (error && error.code === "ENOENT") {
|
|
272
|
+
throw petError("pet_spritesheet_missing", "The pet spritesheet file does not exist.");
|
|
273
|
+
}
|
|
274
|
+
throw petError("pet_spritesheet_unreadable", "Could not read the pet spritesheet file.");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const dimensions = imageDimensions(data, mimeType);
|
|
278
|
+
if (!dimensions || dimensions.width !== ATLAS_WIDTH || dimensions.height !== ATLAS_HEIGHT) {
|
|
279
|
+
throw petError("pet_spritesheet_dimensions_invalid", "Pet spritesheets must be exactly 1536x1872 pixels.");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
mimeType,
|
|
284
|
+
byteLength: data.byteLength,
|
|
285
|
+
dataUrl: includeData ? `data:${mimeType};base64,${data.toString("base64")}` : undefined,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function readValidatedSpritesheetMetadata(spritesheetPath, mimeType) {
|
|
290
|
+
let file;
|
|
291
|
+
try {
|
|
292
|
+
file = await fs.promises.open(spritesheetPath, "r");
|
|
293
|
+
const stat = await file.stat();
|
|
294
|
+
assertSpritesheetByteLength(stat.size);
|
|
295
|
+
const dimensions = await readImageDimensionsFromFile(file, mimeType, stat.size);
|
|
296
|
+
if (!dimensions || dimensions.width !== ATLAS_WIDTH || dimensions.height !== ATLAS_HEIGHT) {
|
|
297
|
+
throw petError("pet_spritesheet_dimensions_invalid", "Pet spritesheets must be exactly 1536x1872 pixels.");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
mimeType,
|
|
302
|
+
byteLength: stat.size,
|
|
303
|
+
dataUrl: undefined,
|
|
304
|
+
};
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error && error.errorCode) {
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
if (error && error.code === "ENOENT") {
|
|
310
|
+
throw petError("pet_spritesheet_missing", "The pet spritesheet file does not exist.");
|
|
311
|
+
}
|
|
312
|
+
throw petError("pet_spritesheet_unreadable", "Could not read the pet spritesheet file.");
|
|
313
|
+
} finally {
|
|
314
|
+
await file?.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Rejects oversized local packages before base64 expansion can bloat relay payloads.
|
|
319
|
+
async function readSpritesheetStat(spritesheetPath) {
|
|
320
|
+
try {
|
|
321
|
+
return await fs.promises.stat(spritesheetPath);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error && error.code === "ENOENT") {
|
|
324
|
+
throw petError("pet_spritesheet_missing", "The pet spritesheet file does not exist.");
|
|
325
|
+
}
|
|
326
|
+
throw petError("pet_spritesheet_unreadable", "Could not read the pet spritesheet file.");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function assertSpritesheetByteLength(byteLength) {
|
|
331
|
+
if (byteLength > MAX_SPRITESHEET_BYTES) {
|
|
332
|
+
throw petError("pet_spritesheet_too_large", "Pet spritesheets must be 16 MB or smaller.");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function readImageDimensionsFromFile(file, mimeType, fileSize) {
|
|
337
|
+
if (mimeType === "image/png") {
|
|
338
|
+
return pngDimensions(await readFileSlice(file, 0, 24));
|
|
339
|
+
}
|
|
340
|
+
if (mimeType === "image/webp") {
|
|
341
|
+
return readWebPDimensionsFromFile(file, fileSize);
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function readWebPDimensionsFromFile(file, fileSize) {
|
|
347
|
+
const riffHeader = await readFileSlice(file, 0, 12);
|
|
348
|
+
if (
|
|
349
|
+
riffHeader.length < 12
|
|
350
|
+
|| riffHeader.toString("ascii", 0, 4) !== "RIFF"
|
|
351
|
+
|| riffHeader.toString("ascii", 8, 12) !== "WEBP"
|
|
352
|
+
) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let offset = 12;
|
|
357
|
+
while (offset + 8 <= fileSize) {
|
|
358
|
+
const chunkHeader = await readFileSlice(file, offset, 8);
|
|
359
|
+
if (chunkHeader.length < 8) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const chunkType = chunkHeader.toString("ascii", 0, 4);
|
|
364
|
+
const chunkSize = chunkHeader.readUInt32LE(4);
|
|
365
|
+
const payloadOffset = offset + 8;
|
|
366
|
+
if (payloadOffset + chunkSize > fileSize) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const dimensionsPayloadLength = webpDimensionsPayloadLength(chunkType);
|
|
371
|
+
if (dimensionsPayloadLength > 0) {
|
|
372
|
+
const payload = await readFileSlice(file, payloadOffset, Math.min(chunkSize, dimensionsPayloadLength));
|
|
373
|
+
const chunkBuffer = Buffer.concat([chunkHeader, payload]);
|
|
374
|
+
const dimensions = webpChunkDimensions(chunkBuffer, chunkType, 8, chunkSize);
|
|
375
|
+
if (dimensions) {
|
|
376
|
+
return dimensions;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
offset = payloadOffset + chunkSize + (chunkSize % 2);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function webpDimensionsPayloadLength(chunkType) {
|
|
387
|
+
if (chunkType === "VP8X") {
|
|
388
|
+
return 10;
|
|
389
|
+
}
|
|
390
|
+
if (chunkType === "VP8L") {
|
|
391
|
+
return 5;
|
|
392
|
+
}
|
|
393
|
+
if (chunkType === "VP8 ") {
|
|
394
|
+
return 10;
|
|
395
|
+
}
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function readFileSlice(file, offset, length) {
|
|
400
|
+
const buffer = Buffer.alloc(length);
|
|
401
|
+
const { bytesRead } = await file.read(buffer, 0, length, offset);
|
|
402
|
+
return buffer.subarray(0, bytesRead);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function imageDimensions(data, mimeType) {
|
|
406
|
+
if (mimeType === "image/png") {
|
|
407
|
+
return pngDimensions(data);
|
|
408
|
+
}
|
|
409
|
+
if (mimeType === "image/webp") {
|
|
410
|
+
return webpDimensions(data);
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function pngDimensions(data) {
|
|
416
|
+
if (data.length < 24 || data.readUInt32BE(0) !== 0x89504e47 || data.readUInt32BE(4) !== 0x0d0a1a0a) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
width: data.readUInt32BE(16),
|
|
421
|
+
height: data.readUInt32BE(20),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function webpDimensions(data) {
|
|
426
|
+
if (
|
|
427
|
+
data.length < 30
|
|
428
|
+
|| data.toString("ascii", 0, 4) !== "RIFF"
|
|
429
|
+
|| data.toString("ascii", 8, 12) !== "WEBP"
|
|
430
|
+
) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let offset = 12;
|
|
435
|
+
while (offset + 8 <= data.length) {
|
|
436
|
+
const chunkType = data.toString("ascii", offset, offset + 4);
|
|
437
|
+
const chunkSize = data.readUInt32LE(offset + 4);
|
|
438
|
+
const payloadOffset = offset + 8;
|
|
439
|
+
if (payloadOffset + chunkSize > data.length) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const dimensions = webpChunkDimensions(data, chunkType, payloadOffset, chunkSize);
|
|
444
|
+
if (dimensions) {
|
|
445
|
+
return dimensions;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
offset = payloadOffset + chunkSize + (chunkSize % 2);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function webpChunkDimensions(data, chunkType, payloadOffset, chunkSize) {
|
|
455
|
+
if (chunkType === "VP8X" && chunkSize >= 10) {
|
|
456
|
+
return {
|
|
457
|
+
width: readUInt24LE(data, payloadOffset + 4) + 1,
|
|
458
|
+
height: readUInt24LE(data, payloadOffset + 7) + 1,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (chunkType === "VP8L" && chunkSize >= 5 && data[payloadOffset] === 0x2f) {
|
|
463
|
+
const bits = data.readUInt32LE(payloadOffset + 1);
|
|
464
|
+
return {
|
|
465
|
+
width: (bits & 0x3fff) + 1,
|
|
466
|
+
height: ((bits >> 14) & 0x3fff) + 1,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (chunkType === "VP8 " && chunkSize >= 10) {
|
|
471
|
+
const startCodeOffset = payloadOffset + 3;
|
|
472
|
+
if (
|
|
473
|
+
data[startCodeOffset] !== 0x9d
|
|
474
|
+
|| data[startCodeOffset + 1] !== 0x01
|
|
475
|
+
|| data[startCodeOffset + 2] !== 0x2a
|
|
476
|
+
) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
width: data.readUInt16LE(payloadOffset + 6) & 0x3fff,
|
|
482
|
+
height: data.readUInt16LE(payloadOffset + 8) & 0x3fff,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function readUInt24LE(data, offset) {
|
|
490
|
+
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Keeps ~/.codex/pets entries authoritative when legacy ~/.codex/avatars has the same folder.
|
|
494
|
+
function mergeCustomAvatars(avatars) {
|
|
495
|
+
const byID = new Map();
|
|
496
|
+
for (const avatar of avatars) {
|
|
497
|
+
const existing = byID.get(avatar.id);
|
|
498
|
+
if (!existing || avatar.kind === "pet") {
|
|
499
|
+
byID.set(avatar.id, avatar);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return Array.from(byID.values()).sort((left, right) => left.displayName.localeCompare(right.displayName));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function firstNonEmptyString(candidates) {
|
|
506
|
+
for (const candidate of candidates) {
|
|
507
|
+
if (typeof candidate !== "string") {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const trimmed = candidate.trim();
|
|
511
|
+
if (trimmed) {
|
|
512
|
+
return trimmed;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function displayFromSlug(slug) {
|
|
519
|
+
return slug
|
|
520
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
521
|
+
.filter(Boolean)
|
|
522
|
+
.map((word) => word.slice(0, 1).toUpperCase() + word.slice(1))
|
|
523
|
+
.join(" ") || slug;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function petError(errorCode, userMessage) {
|
|
527
|
+
const error = new Error(userMessage);
|
|
528
|
+
error.errorCode = errorCode;
|
|
529
|
+
error.userMessage = userMessage;
|
|
530
|
+
return error;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
module.exports = {
|
|
534
|
+
handlePetMethod,
|
|
535
|
+
handlePetRequest,
|
|
536
|
+
imageDimensions,
|
|
537
|
+
};
|
package/src/qr.js
CHANGED
|
@@ -40,9 +40,12 @@ function normalizePairingSession(pairingSessionOrPayload) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function printQR(pairingSessionOrPayload) {
|
|
43
|
+
function printQR(pairingSessionOrPayload, options = {}) {
|
|
44
44
|
const { pairingPayload, pairingCode } = normalizePairingSession(pairingSessionOrPayload);
|
|
45
45
|
const payload = JSON.stringify(pairingPayload);
|
|
46
|
+
const sessionId = typeof pairingPayload?.sessionId === "string" ? pairingPayload.sessionId.trim() : "";
|
|
47
|
+
const sessionIdShort = sessionId.length > 12 ? `${sessionId.slice(0, 8)}…` : sessionId;
|
|
48
|
+
const env = options.env || process.env;
|
|
46
49
|
|
|
47
50
|
console.log("\nScan this QR with the iPhone:\n");
|
|
48
51
|
qrcode.generate(payload, { small: true });
|
|
@@ -50,9 +53,24 @@ function printQR(pairingSessionOrPayload) {
|
|
|
50
53
|
console.log("Or paste this pairing code in the iPhone app:\n");
|
|
51
54
|
console.log(pairingCode);
|
|
52
55
|
}
|
|
53
|
-
console.log(`\nSession ID: ${
|
|
56
|
+
console.log(`\nSession ID: ${sessionIdShort || "(none)"}`);
|
|
54
57
|
console.log(`Device ID: ${pairingPayload.macDeviceId}`);
|
|
55
58
|
console.log(`Expires: ${new Date(pairingPayload.expiresAt).toISOString()}\n`);
|
|
59
|
+
|
|
60
|
+
if (shouldPrintPairingJson({ env, explicitValue: options.printPairingJson })) {
|
|
61
|
+
// Opt-in only: this is the same bearer-like payload as the QR scan target.
|
|
62
|
+
console.log("Pairing JSON (debug only; same sensitive bytes as the QR):\n");
|
|
63
|
+
console.log(`${payload}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function shouldPrintPairingJson({ env = process.env, explicitValue } = {}) {
|
|
68
|
+
if (typeof explicitValue === "boolean") {
|
|
69
|
+
return explicitValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rawValue = env?.REMODEX_PRINT_PAIRING_JSON || env?.PHODEX_PRINT_PAIRING_JSON || "";
|
|
73
|
+
return ["1", "true", "yes", "on"].includes(String(rawValue).trim().toLowerCase());
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
module.exports = {
|
|
@@ -60,4 +78,5 @@ module.exports = {
|
|
|
60
78
|
SHORT_PAIRING_CODE_LENGTH,
|
|
61
79
|
createShortPairingCode,
|
|
62
80
|
printQR,
|
|
81
|
+
shouldPrintPairingJson,
|
|
63
82
|
};
|
package/src/rollout-watch.js
CHANGED
|
@@ -441,12 +441,12 @@ function findNewestRolloutFileForThread(root, threadId, { fsModule = fs } = {})
|
|
|
441
441
|
continue;
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
444
|
+
const candidate = {
|
|
445
|
+
filePath: fullPath,
|
|
446
|
+
mtimeMs: fsModule.statSync(fullPath).mtimeMs,
|
|
447
|
+
};
|
|
448
|
+
if (!newestMatch || compareRolloutFileOrder(candidate, newestMatch) < 0) {
|
|
449
|
+
newestMatch = candidate;
|
|
450
450
|
}
|
|
451
451
|
}
|
|
452
452
|
}
|
|
@@ -498,10 +498,20 @@ function collectRecentRolloutFiles(
|
|
|
498
498
|
}
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
-
candidates.sort(
|
|
501
|
+
candidates.sort(compareRolloutFileOrder);
|
|
502
502
|
return candidates.slice(0, candidateLimit);
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
+
// Keeps rollout selection deterministic when filesystem timestamp resolution
|
|
506
|
+
// reports equal mtimes for several rollout candidates.
|
|
507
|
+
function compareRolloutFileOrder(lhs, rhs) {
|
|
508
|
+
if (lhs.mtimeMs !== rhs.mtimeMs) {
|
|
509
|
+
return rhs.mtimeMs - lhs.mtimeMs;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return rhs.filePath.localeCompare(lhs.filePath);
|
|
513
|
+
}
|
|
514
|
+
|
|
505
515
|
function rolloutFileContainsTurnId(
|
|
506
516
|
filePath,
|
|
507
517
|
turnId,
|