@makerbi/remodex 1.3.10 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: ${pairingPayload.sessionId}`);
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
  };
@@ -37,6 +37,8 @@ const IMAGE_MIME_TYPES_BY_EXTENSION = new Map([
37
37
  [".heic", "image/heic"],
38
38
  [".heif", "image/heif"],
39
39
  ]);
40
+ /** Match git-handler.js: Node default maxBuffer is 1 MiB. */
41
+ const GIT_EXEC_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
40
42
  const repoMutationLocks = new Map();
41
43
 
42
44
  function handleWorkspaceRequest(rawMessage, sendResponse) {
@@ -132,12 +134,12 @@ async function workspaceReadImage(params) {
132
134
  throw workspaceError("image_not_found", "The image file no longer exists on this Mac.");
133
135
  }
134
136
 
135
- const [realRepoRoot, realTempRoots] = await Promise.all([
136
- cwd ? resolveRepoRoot(cwd).then(realpathOrNull).catch(() => null) : null,
137
+ const [realWorkspaceRoot, realTempRoots] = await Promise.all([
138
+ cwd ? resolveImageWorkspaceRoot(cwd) : null,
137
139
  realTemporaryImageRoots(),
138
140
  ]);
139
141
  const isAllowed =
140
- (realRepoRoot && isPathInside(realImagePath, realRepoRoot))
142
+ (realWorkspaceRoot && isPathInside(realImagePath, realWorkspaceRoot))
141
143
  || (realGeneratedImagesRoot && isPathInside(realImagePath, realGeneratedImagesRoot))
142
144
  || realTempRoots.some((tempRoot) => isPathInside(realImagePath, tempRoot));
143
145
  if (!isAllowed) {
@@ -212,6 +214,26 @@ async function realTemporaryImageRoots() {
212
214
  return Array.from(new Set(roots.filter(Boolean)));
213
215
  }
214
216
 
217
+ // Image previews are read-only, so non-git Codex scratch workspaces can be scoped to their cwd.
218
+ async function resolveImageWorkspaceRoot(cwd) {
219
+ const realRepoRoot = await resolveRepoRoot(cwd).then(realpathOrNull).catch(() => null);
220
+ if (realRepoRoot) {
221
+ return realRepoRoot;
222
+ }
223
+
224
+ const realCwd = await realpathOrNull(cwd);
225
+ if (!realCwd || isBroadWorkspaceRoot(realCwd)) {
226
+ return null;
227
+ }
228
+ return realCwd;
229
+ }
230
+
231
+ function isBroadWorkspaceRoot(candidatePath) {
232
+ const normalized = path.resolve(candidatePath);
233
+ return normalized === path.parse(normalized).root
234
+ || normalized === path.resolve(os.homedir());
235
+ }
236
+
215
237
  async function readPreviewImageData(imagePath, maxPixelDimension, originalByteLength) {
216
238
  if (!usesSipsImagePreview()) {
217
239
  if (originalByteLength <= MAX_IMAGE_PREVIEW_READ_BYTES) {
@@ -572,6 +594,7 @@ async function runGitApply(cwd, args, patchText) {
572
594
  const { stdout, stderr } = await execFileAsync("git", [...args, tempPatchPath], {
573
595
  cwd,
574
596
  timeout: GIT_TIMEOUT_MS,
597
+ maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
575
598
  });
576
599
  return { ok: true, stdout, stderr };
577
600
  } catch (err) {
@@ -730,7 +753,11 @@ function workspaceError(errorCode, userMessage) {
730
753
  }
731
754
 
732
755
  function git(cwd, ...args) {
733
- return execFileAsync("git", args, { cwd, timeout: GIT_TIMEOUT_MS })
756
+ return execFileAsync("git", args, {
757
+ cwd,
758
+ timeout: GIT_TIMEOUT_MS,
759
+ maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
760
+ })
734
761
  .then(({ stdout }) => stdout)
735
762
  .catch((err) => {
736
763
  const msg = (err.stderr || err.message || "").trim();
@@ -1,4 +0,0 @@
1
- {
2
- "relayUrl": "wss://remodex.vectorvein.com/relay",
3
- "pushServiceUrl": ""
4
- }