shotops-mcp 0.9.8 → 0.9.9

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/dist/local.js CHANGED
@@ -24,7 +24,7 @@
24
24
  }
25
25
  await visit('')
26
26
  return hash.digest('hex')
27
- })(sourceDirectory) !== "7345df8a318a3c0989195e1b9c4ce91f2eaf136e27afd8ee123620da60c23410") {
27
+ })(sourceDirectory) !== "726565c5a58fb0cf9d366865195f22440699f6a55c8789edcd92577fc682ebbf") {
28
28
  console.error('shotops-mcp: dist/local.js is stale; run npm run build in shotops-mcp.');
29
29
  }
30
30
  } catch { /* An unreadable checkout must not break an otherwise working CLI. */ }
@@ -140,10 +140,15 @@ var init_devices = __esm({
140
140
  });
141
141
 
142
142
  // ../mockup-engine/appstore/catalog/caption.ts
143
- var CAPTION_STYLE_BOUNDS;
143
+ var CAPTION_STYLE_DEFAULTS, CAPTION_STYLE_BOUNDS;
144
144
  var init_caption = __esm({
145
145
  "../mockup-engine/appstore/catalog/caption.ts"() {
146
146
  "use strict";
147
+ CAPTION_STYLE_DEFAULTS = {
148
+ lineHeight: 1.2,
149
+ bandInset: 0.06,
150
+ bandCenter: 0.5
151
+ };
147
152
  CAPTION_STYLE_BOUNDS = {
148
153
  sizePt: { min: 20, max: 120 },
149
154
  maxWidth: { min: 0.1, max: 1 },
@@ -1908,7 +1913,7 @@ var init_resultContract = __esm({
1908
1913
  { code: "render_failed", test: /^render failed|^renderer returned / },
1909
1914
  { code: "payload_too_large", test: /inline-response ceiling|is too large/ },
1910
1915
  { code: "storage_failed", test: /could not store the rendered panels|could not create share link/ },
1911
- { code: "asset_not_found", test: /failed to read panel|no preview image available|does not belong to this account/ },
1916
+ { code: "asset_not_found", test: /failed to read panel|no preview image available|does not belong to this account|no screenshot was ever uploaded for ref/ },
1912
1917
  { code: "persistence_failed", test: /could not build the project record|no project record was produced/ },
1913
1918
  { code: "quota_exhausted", test: /not enough credits|credit balance|wallet is empty/ },
1914
1919
  // Input-shape complaints raised by a HANDLER rather than by the schema. Last, and each anchored
@@ -3179,7 +3184,7 @@ function clampCaptionBounds(input) {
3179
3184
  }
3180
3185
  function captionClampNote(clamps) {
3181
3186
  if (clamps.length === 0) return void 0;
3182
- const said = clamps.map((clamp) => `${clamp.field} ${clamp.submitted} \u2192 ${clamp.stored}`).join("; ");
3187
+ const said = clamps.map((clamp2) => `${clamp2.field} ${clamp2.submitted} \u2192 ${clamp2.stored}`).join("; ");
3183
3188
  return `Caption values outside the supported range were stored at the bound (${said}) \u2014 sizePt is ${captionBoundText("sizePt")} and maxWidth ${captionBoundText("maxWidth")}, the same range the ShotOps caption controls offer, so nothing here is unreachable in the app.`;
3184
3189
  }
3185
3190
  function rangeIssueMessage(field, raw) {
@@ -4229,7 +4234,7 @@ var init_schemas = __esm({
4229
4234
  flatScreenshots = z5.array(screenshotEntry).min(1).max(10).describe("Preferred: one screenshot entry per App Store panel, in display order.");
4230
4235
  groupedScreenshots = z5.array(slot).min(1).max(10).describe("Advanced: one nested slot per panel; put several screenshot entries in a slot for a multi-phone panel.");
4231
4236
  screenshots = z5.union([flatScreenshots, groupedScreenshots]).describe(screenshotsDescription);
4232
- screenshotsForSaveDescription = `${screenshotsDescription} NOTE: save_project does not render \u2014 it uses only each slot's SHAPE (phones per panel) and each entry's \`name\` (source filename, for re-load matching). The image bytes are never fetched or stored, so pass the same entries you rendered with (esp. their \`name\`s).`;
4237
+ screenshotsForSaveDescription = `${screenshotsDescription} NOTE: save_project does not render and never fetches image bytes. It uses each slot's SHAPE (phones per panel), each entry's \`name\` (source filename, for re-load matching) and each { "ref" }, per-locale ones included, which it records on that entry's own frame so the project reopens with that screenshot. A ref that is not an upload under this account refuses the whole save, naming it, and nothing is written. Pass the same entries you rendered with (esp. their \`name\`s).`;
4233
4238
  screenshotsForSave = screenshots.describe(screenshotsForSaveDescription);
4234
4239
  screenshotsOptionalDescription = `${screenshotsDescription} Required unless \`panels\` (pre-rendered refs) is given instead.`;
4235
4240
  screenshotsOptional = screenshots.optional().describe(screenshotsOptionalDescription);
@@ -5465,11 +5470,11 @@ function previewUsage(deps, snapshot) {
5465
5470
  ...credits ? publicCreditBalance(credits) : {}
5466
5471
  };
5467
5472
  }
5468
- function statusMessage(deps, snapshot, step) {
5469
- if (step === "none") {
5473
+ function statusMessage(deps, snapshot, step2) {
5474
+ if (step2 === "none") {
5470
5475
  return "This connection can produce store-ready output: full-resolution render_strip and render_project, and every emit_bundle form." + (isLocal(deps) ? " Local rendering spends no cloud credits." : "");
5471
5476
  }
5472
- if (step === "sign_in" && !snapshot.authenticated && deps.anonymous) {
5477
+ if (step2 === "sign_in" && !snapshot.authenticated && deps.anonymous) {
5473
5478
  return `This connection is not signed in. It can import screenshots and render up to ${ANONYMOUS.attempts} preview strips of up to ${ANONYMOUS.maxPanels} panels each per ${Math.round(ANONYMOUS.windowSeconds / 86400)} days. ` + productionDenialMessage(snapshot.productionExport, deps);
5474
5479
  }
5475
5480
  return productionDenialMessage(snapshot.productionExport, deps);
@@ -5620,7 +5625,7 @@ function operationSupported(deps, tool, account) {
5620
5625
  async function handleAccountStatus(deps) {
5621
5626
  const snapshot = await readSnapshot(deps);
5622
5627
  const decision = snapshot.productionExport;
5623
- const step = accountNextStep(decision);
5628
+ const step2 = accountNextStep(decision);
5624
5629
  return textResult({
5625
5630
  ok: true,
5626
5631
  connection: isLocal(deps) ? "local" : "hosted",
@@ -5646,8 +5651,8 @@ async function handleAccountStatus(deps) {
5646
5651
  ...decision.allowed ? {} : { reason: decision.reason },
5647
5652
  ...decision.allowed || !decision.requiredPlan ? {} : { requiredPlan: decision.requiredPlan }
5648
5653
  },
5649
- nextStep: step,
5650
- message: statusMessage(deps, snapshot, step),
5654
+ nextStep: step2,
5655
+ message: statusMessage(deps, snapshot, step2),
5651
5656
  // #662 — the capability half: accepted inputs, delivery modes, retention classes, operation
5652
5657
  // support, the cost model and the closed failure catalog. Additive; every field above is
5653
5658
  // exactly what it was.
@@ -6167,6 +6172,20 @@ var init_panel = __esm({
6167
6172
  }
6168
6173
  });
6169
6174
 
6175
+ // ../mockup-engine/storagePath.ts
6176
+ function isSafeStoragePath(path) {
6177
+ if (path.length === 0 || path.startsWith("/") || UNSAFE_CHARACTER.test(path)) return false;
6178
+ const body = path.endsWith("/") ? path.slice(0, -1) : path;
6179
+ return body.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
6180
+ }
6181
+ var UNSAFE_CHARACTER;
6182
+ var init_storagePath = __esm({
6183
+ "../mockup-engine/storagePath.ts"() {
6184
+ "use strict";
6185
+ UNSAFE_CHARACTER = /[%\\?#\u0000-\u001f\u007f]/;
6186
+ }
6187
+ });
6188
+
6170
6189
  // ../mockup-engine/appstore/fingerprint.ts
6171
6190
  function isScreenshotFingerprint(value) {
6172
6191
  return typeof value === "string" && SCREENSHOT_FINGERPRINT.test(value);
@@ -6324,7 +6343,7 @@ function isShotManifestEntry(entry) {
6324
6343
  return typeof entry.shotId === "string";
6325
6344
  }
6326
6345
  function manifestRefUploader(ref) {
6327
- if (ref.includes("..")) return null;
6346
+ if (!isSafeStoragePath(ref)) return null;
6328
6347
  const match = UPLOAD_REF_RE.exec(ref);
6329
6348
  if (!match || !UUID_RE.test(match[1])) return null;
6330
6349
  return match[1];
@@ -6570,23 +6589,11 @@ function rebindManifestToShots(cells, shots) {
6570
6589
  }
6571
6590
  return { manifest, findings };
6572
6591
  }
6573
- function manifestFromNamedRefs(refs, shots, hashes = {}) {
6574
- const idsByFrameName = shotIdsByFrameName(shots);
6575
- const out = [];
6576
- for (const [name, ref] of Object.entries(refs)) {
6577
- const canonical = canonicalizeManifestRef(ref);
6578
- const hash = hashes[name];
6579
- const withHash = (entry) => hash === void 0 ? entry : { ...entry, hash };
6580
- const ids = idsByFrameName[name];
6581
- if (ids === void 0) out.push(withHash({ slot: name, variant: {}, ref: canonical }));
6582
- else for (const id of ids) out.push(withHash({ shotId: id, variant: {}, ref: canonical }));
6583
- }
6584
- return out;
6585
- }
6586
6592
  var UPLOAD_REF_RE, UUID_RE, TRUNCATED_NAMED_UPLOAD_REF, EMPTY;
6587
6593
  var init_screenManifest = __esm({
6588
6594
  "../mockup-engine/appstore/screenManifest.ts"() {
6589
6595
  "use strict";
6596
+ init_storagePath();
6590
6597
  init_variant();
6591
6598
  init_fingerprint();
6592
6599
  UPLOAD_REF_RE = /^uploads\/([^/]+)\/([^/]+(?:\/[^/]+)*)$/;
@@ -9832,7 +9839,360 @@ var init_agentTurnRepetition = __esm({
9832
9839
  }
9833
9840
  });
9834
9841
 
9842
+ // ../mockup-engine/appstore/agentTurnAdjustments.ts
9843
+ function step(field, from, to) {
9844
+ if (field !== "orbit") return to - from;
9845
+ return ((to - from) % 360 + 540) % 360 - 180;
9846
+ }
9847
+ function pushesFurther(field, start, now, proposed) {
9848
+ if (typeof start !== "number" || typeof now !== "number" || typeof proposed !== "number") return false;
9849
+ const moved = step(field, start, now);
9850
+ const next = step(field, now, proposed);
9851
+ return moved !== 0 && next !== 0 && Math.sign(moved) === Math.sign(next);
9852
+ }
9853
+ function deviceLooks(state) {
9854
+ const looks = {};
9855
+ for (const shot of state.shots) looks[shot.id] = aiShotPatchFromLook(shot.look);
9856
+ return looks;
9857
+ }
9858
+ function deviceRepeats(entry, deviceId, start, now) {
9859
+ const from = start[deviceId];
9860
+ const at = now[deviceId];
9861
+ if (!from || !at) return [];
9862
+ return ADJUSTABLE_DEVICE_FIELDS.filter((field) => pushesFurther(field, from[field], at[field], entry[field]));
9863
+ }
9864
+ function withoutFields(entry, fields2) {
9865
+ const kept = { ...entry };
9866
+ for (const field of fields2) delete kept[field];
9867
+ return kept;
9868
+ }
9869
+ function baseLayers(state, panelId) {
9870
+ return state.captionText[state.baseLocale]?.[panelId];
9871
+ }
9872
+ function effectiveCaptionValue(layer, field) {
9873
+ const value = layer[field];
9874
+ if (typeof value === "number") return value;
9875
+ switch (field) {
9876
+ case "lineHeight":
9877
+ case "bandInset":
9878
+ case "bandCenter":
9879
+ return CAPTION_STYLE_DEFAULTS[field];
9880
+ case "reserveLines":
9881
+ return layer.text.split("\n").length;
9882
+ default:
9883
+ return void 0;
9884
+ }
9885
+ }
9886
+ function captionRepeats(style2, start, now) {
9887
+ if (!start || !now || start.length !== now.length || now.length === 0) return [];
9888
+ return ADJUSTABLE_CAPTION_FIELDS.filter((field) => style2[field] !== void 0 && now.some((layer, index) => {
9889
+ const applied = captionLayerFromAiPatch(layer, { [field]: style2[field] });
9890
+ if (applied === layer) return false;
9891
+ return pushesFurther(
9892
+ field,
9893
+ effectiveCaptionValue(start[index], field),
9894
+ effectiveCaptionValue(layer, field),
9895
+ effectiveCaptionValue(applied, field)
9896
+ );
9897
+ }));
9898
+ }
9899
+ function dropRepeatedAdjustments(patch, context) {
9900
+ const dropped = [];
9901
+ const start = deviceLooks(context.start);
9902
+ const now = deviceLooks(context.now);
9903
+ const repairing = (panelId) => panelId !== void 0 && (context.repairing?.has(panelId) ?? false);
9904
+ const panelOfDevice = (deviceId) => context.now.shots.find((shot) => shot.id === deviceId)?.panelId;
9905
+ let next = patch;
9906
+ if (patch.devices) {
9907
+ const devices = [];
9908
+ let changed = false;
9909
+ for (const entry of patch.devices) {
9910
+ const repeats = repairing(panelOfDevice(entry.deviceId)) ? [] : deviceRepeats(entry.patch, entry.deviceId, start, now);
9911
+ if (repeats.length === 0) {
9912
+ devices.push(entry);
9913
+ continue;
9914
+ }
9915
+ changed = true;
9916
+ for (const field of repeats) dropped.push({ target: entry.deviceId, field });
9917
+ const kept = withoutFields(entry.patch, repeats);
9918
+ if (Object.keys(kept).length > 0) devices.push({ ...entry, patch: kept });
9919
+ }
9920
+ if (changed) {
9921
+ const { devices: _replaced, ...rest } = next;
9922
+ void _replaced;
9923
+ next = devices.length > 0 ? { ...rest, devices } : rest;
9924
+ }
9925
+ }
9926
+ if (patch.shots && patch.shots.length === context.panelIds.length) {
9927
+ let changed = false;
9928
+ const shots = patch.shots.map((entry, index) => {
9929
+ const panelId = context.panelIds[index];
9930
+ if (repairing(panelId)) return entry;
9931
+ const repeats = [];
9932
+ for (const shot of context.now.shots) {
9933
+ if (shot.panelId !== panelId) continue;
9934
+ for (const field of deviceRepeats(entry, shot.id, start, now)) {
9935
+ if (repeats.indexOf(field) < 0) repeats.push(field);
9936
+ }
9937
+ }
9938
+ if (repeats.length === 0) return entry;
9939
+ changed = true;
9940
+ for (const field of repeats) dropped.push({ target: `${panelId}#devices`, field });
9941
+ return withoutFields(entry, repeats);
9942
+ });
9943
+ if (changed) next = { ...next, shots };
9944
+ }
9945
+ const frames = patch.frames && patch.frames.length === context.panelIds.length ? patch.frames : void 0;
9946
+ if (frames) {
9947
+ let changed = false;
9948
+ const kept = frames.map((entry, index) => {
9949
+ const style2 = entry.captionStyle;
9950
+ const panelId = context.panelIds[index];
9951
+ if (!style2 || repairing(panelId)) return entry;
9952
+ const repeats = captionRepeats(style2, baseLayers(context.start, panelId), baseLayers(context.now, panelId));
9953
+ if (repeats.length === 0) return entry;
9954
+ changed = true;
9955
+ for (const field of repeats) dropped.push({ target: `${panelId}#captions`, field });
9956
+ const captionStyle = withoutFields(style2, repeats);
9957
+ const { captionStyle: _replaced, ...rest } = entry;
9958
+ void _replaced;
9959
+ return Object.keys(captionStyle).length > 0 ? { ...rest, captionStyle } : rest;
9960
+ });
9961
+ if (changed) next = { ...next, frames: kept };
9962
+ }
9963
+ if (patch.captionStyle && !context.panelIds.some(repairing)) {
9964
+ const stripWide = patch.captionStyle;
9965
+ const repeats = [];
9966
+ context.panelIds.forEach((panelId, index) => {
9967
+ const own = frames?.[index]?.captionStyle ?? {};
9968
+ const reaching = withoutFields(stripWide, Object.keys(own));
9969
+ const found = captionRepeats(reaching, baseLayers(context.start, panelId), baseLayers(context.now, panelId));
9970
+ for (const field of found) {
9971
+ if (repeats.indexOf(field) < 0) repeats.push(field);
9972
+ }
9973
+ });
9974
+ if (repeats.length > 0) {
9975
+ for (const field of repeats) dropped.push({ target: "strip#captions", field });
9976
+ const captionStyle = withoutFields(stripWide, repeats);
9977
+ const { captionStyle: _replaced, ...rest } = next;
9978
+ void _replaced;
9979
+ next = Object.keys(captionStyle).length > 0 ? { ...rest, captionStyle } : rest;
9980
+ }
9981
+ }
9982
+ return { patch: dropped.length > 0 ? next : patch, dropped };
9983
+ }
9984
+ function carriesSomething(value) {
9985
+ if (value === void 0 || value === null) return false;
9986
+ if (Array.isArray(value)) return value.some(carriesSomething);
9987
+ if (typeof value === "object") return Object.keys(value).some((key) => carriesSomething(value[key]));
9988
+ return true;
9989
+ }
9990
+ function patchChangesSomething(patch) {
9991
+ return Object.keys(patch).some((key) => key !== "summary" && carriesSomething(patch[key]));
9992
+ }
9993
+ var ADJUSTABLE_DEVICE_FIELDS, ADJUSTABLE_CAPTION_FIELDS;
9994
+ var init_agentTurnAdjustments = __esm({
9995
+ "../mockup-engine/appstore/agentTurnAdjustments.ts"() {
9996
+ "use strict";
9997
+ init_refinementValues();
9998
+ init_caption();
9999
+ ADJUSTABLE_DEVICE_FIELDS = [
10000
+ "orbit",
10001
+ "roll",
10002
+ "deviceSize",
10003
+ "hOffset",
10004
+ "vOffset",
10005
+ "finish",
10006
+ "clearcoat",
10007
+ "screenCornerRadius"
10008
+ ];
10009
+ ADJUSTABLE_CAPTION_FIELDS = ["sizePt", "maxWidth", "lineHeight", "bandInset", "bandCenter", "reserveLines"];
10010
+ }
10011
+ });
10012
+
10013
+ // ../mockup-engine/appstore/agentTurnLanded.ts
10014
+ function sameDeviceValue(field, proposed, applied) {
10015
+ if (field !== "orbit") return near(proposed, applied);
10016
+ const wrapped = ((proposed + 180) % 360 + 360) % 360 - 180;
10017
+ return near(wrapped, applied, 0.5) || near(Math.abs(wrapped), 180, 0.5) && near(Math.abs(applied), 180, 0.5);
10018
+ }
10019
+ function deviceDifference(field, proposed, applied, sharedOffset) {
10020
+ if (sameDeviceValue(field, proposed, applied)) return null;
10021
+ if (field === "hOffset" && sharedOffset) return "shared_offset";
10022
+ const range = field === "orbit" ? void 0 : SHOT_LOOK_RANGES[field];
10023
+ if (range && (proposed < range.min || proposed > range.max) && near(clamp(proposed, range), applied)) return "held_at_limit";
10024
+ return "set_differently";
10025
+ }
10026
+ function deviceDifferences(entry, applied, sharedOffset, out) {
10027
+ for (const field of DEVICE_NUMBERS) {
10028
+ const proposed = entry[field];
10029
+ const landed = applied[field];
10030
+ if (typeof proposed !== "number" || typeof landed !== "number") continue;
10031
+ const difference = deviceDifference(field, proposed, landed, sharedOffset);
10032
+ if (difference) out.add(difference);
10033
+ }
10034
+ }
10035
+ function expectedCaptionValue(field, proposed, current) {
10036
+ const raw = typeof proposed === "number" ? proposed : current === void 0 ? void 0 : current * clamp(proposed.scale, AI_SCALE_RANGE);
10037
+ return raw !== void 0 && field === "reserveLines" ? Math.round(raw) : raw;
10038
+ }
10039
+ function captionDifferences(style2, before, after, alignedByPosition, out) {
10040
+ const stated = CAPTION_NUMBERS.filter((field) => style2[field] !== void 0);
10041
+ if (stated.length === 0) return;
10042
+ if (!after || after.length === 0) {
10043
+ out.add("set_differently");
10044
+ return;
10045
+ }
10046
+ for (const field of stated) {
10047
+ const proposed = style2[field];
10048
+ if (typeof proposed !== "number" && !alignedByPosition) continue;
10049
+ after.forEach((layer, index) => {
10050
+ const expected = expectedCaptionValue(field, proposed, typeof proposed === "number" ? void 0 : before?.[index]?.[field]);
10051
+ const landed = layer[field];
10052
+ if (expected === void 0) {
10053
+ out.add("set_differently");
10054
+ return;
10055
+ }
10056
+ if (typeof landed === "number" && near(landed, expected)) return;
10057
+ const range = CAPTION_STYLE_BOUNDS[field];
10058
+ const limited = field === "reserveLines" ? Math.round(clamp(expected, range)) : clamp(expected, range);
10059
+ out.add(typeof landed === "number" && !near(limited, expected) && near(landed, limited) ? "held_at_limit" : "set_differently");
10060
+ });
10061
+ }
10062
+ }
10063
+ function landedDifferences(before, after, patch, panelIds) {
10064
+ const out = /* @__PURE__ */ new Set();
10065
+ const lookOf = (shotId) => {
10066
+ const shot = after.shots.find((candidate) => candidate.id === shotId);
10067
+ return shot ? aiShotPatchFromLook(shot.look) : null;
10068
+ };
10069
+ for (const entry of patch.devices ?? []) {
10070
+ const applied = lookOf(entry.deviceId);
10071
+ if (applied) deviceDifferences(entry.patch, applied, false, out);
10072
+ }
10073
+ if (patch.shots && patch.shots.length === panelIds.length) {
10074
+ patch.shots.forEach((entry, index) => {
10075
+ const shots = after.shots.filter((shot) => shot.panelId === panelIds[index]);
10076
+ for (const shot of shots) deviceDifferences(entry, aiShotPatchFromLook(shot.look), shots.length > 1, out);
10077
+ });
10078
+ }
10079
+ const frames = patch.frames && patch.frames.length === panelIds.length ? patch.frames : void 0;
10080
+ if (patch.captionStyle || frames) {
10081
+ const beforeSlots = normalizeCaptionSlotIds(before.captionSlotIds, before.captionText, before.panels.map((panel) => panel.id));
10082
+ const afterSlots = normalizeCaptionSlotIds(after.captionSlotIds, after.captionText, after.panels.map((panel) => panel.id));
10083
+ panelIds.forEach((panelId, index) => {
10084
+ const style2 = { ...patch.captionStyle, ...frames?.[index]?.captionStyle };
10085
+ const aligned = (beforeSlots[panelId] ?? []).join("\0") === (afterSlots[panelId] ?? []).join("\0");
10086
+ captionDifferences(
10087
+ style2,
10088
+ before.captionText[before.baseLocale]?.[panelId],
10089
+ after.captionText[after.baseLocale]?.[panelId],
10090
+ aligned,
10091
+ out
10092
+ );
10093
+ });
10094
+ }
10095
+ return out;
10096
+ }
10097
+ var DEVICE_NUMBERS, CAPTION_NUMBERS, clamp, near;
10098
+ var init_agentTurnLanded = __esm({
10099
+ "../mockup-engine/appstore/agentTurnLanded.ts"() {
10100
+ "use strict";
10101
+ init_caption();
10102
+ init_defaults();
10103
+ init_frameTemplates();
10104
+ init_refineIntent();
10105
+ init_refinementValues();
10106
+ DEVICE_NUMBERS = ["orbit", "roll", "deviceSize", "hOffset", "vOffset", "finish", "clearcoat", "screenCornerRadius"];
10107
+ CAPTION_NUMBERS = ["sizePt", "maxWidth", "lineHeight", "bandInset", "bandCenter", "reserveLines"];
10108
+ clamp = (value, range) => Math.min(Math.max(value, range.min), range.max);
10109
+ near = (a, b, tolerance = 1e-9) => Math.abs(a - b) <= tolerance;
10110
+ }
10111
+ });
10112
+
9835
10113
  // ../mockup-engine/appstore/agentTurnCoordinator.ts
10114
+ function passEffect(working, application, patch, panelIds, notApplied, focus) {
10115
+ const landed = landedDifferences(working, application.state, patch, panelIds);
10116
+ if (notApplied.size === 0 && landed.size === 0) return null;
10117
+ const applied = [...describeRefinementEffect(working, application.state), ...describeAssetBindings(application.assetBindings)];
10118
+ const lines = [applied.length > 0 ? `${applied.join(" \xB7 ")}.` : "Nothing changed in this pass."];
10119
+ if (notApplied.size > 0) lines.push(`Not applied: ${Array.from(notApplied).map((reason) => NOT_APPLIED[reason]).join("; ")}.`);
10120
+ if (landed.size > 0) lines.push(`Not applied as proposed: ${Array.from(landed).map((reason) => LANDED_DIFFERENTLY[reason]).join("; ")}.`);
10121
+ return {
10122
+ summary: lines.join(" "),
10123
+ reachesOutsideFocus: changedOutsideFocus(working, application.state, focus)
10124
+ };
10125
+ }
10126
+ function emptiedPassEffect(notApplied) {
10127
+ return {
10128
+ summary: `Nothing changed in this pass. Not applied: ${Array.from(notApplied).map((reason) => NOT_APPLIED[reason]).join("; ")}.`,
10129
+ reachesOutsideFocus: false
10130
+ };
10131
+ }
10132
+ function changedOutsideFocus(before, after, focus) {
10133
+ if (focus.kind === "project") return false;
10134
+ const frameId = focus.frameId;
10135
+ const ownsFrame = focus.kind === "frame";
10136
+ const slotsOf = (state) => normalizeCaptionSlotIds(state.captionSlotIds, state.captionText, state.panels.map((panel) => panel.id));
10137
+ const beforeSlots = slotsOf(before);
10138
+ const focusedSlot = focus.kind === "caption" ? beforeSlots[frameId]?.[focus.captionIndex] : void 0;
10139
+ const shotsOutside = (state) => state.shots.filter((shot) => !(ownsFrame && shot.panelId === frameId)).map((shot) => focus.kind === "device" && shot.id === focus.deviceId ? { id: shot.id, panelId: shot.panelId } : { id: shot.id, panelId: shot.panelId, frameName: shot.frameName, frameNodeId: shot.frameNodeId, look: shot.look });
10140
+ const bytesOutside = (state) => state.shots.filter((shot) => !(ownsFrame && shot.panelId === frameId) && !(focus.kind === "device" && shot.id === focus.deviceId)).map((shot) => shot.bytes);
10141
+ const perFrame = (map) => {
10142
+ if (!map || !ownsFrame) return map;
10143
+ const { [frameId]: _owned, ...rest } = map;
10144
+ void _owned;
10145
+ return rest;
10146
+ };
10147
+ const outside = (state) => {
10148
+ const {
10149
+ selection: _selection,
10150
+ activeLocale: _activeLocale,
10151
+ previewPresetId: _preview,
10152
+ outputScreens: _outputScreens,
10153
+ localeScreens: _localeScreens,
10154
+ selectedShotId: _selectedShotId,
10155
+ shots: _shots,
10156
+ captionSlotIds: _rawSlots,
10157
+ captionText,
10158
+ outputCaptions,
10159
+ panelColors,
10160
+ panelBackgrounds,
10161
+ staleCaptionTranslations,
10162
+ frameTemplates,
10163
+ ...rest
10164
+ } = state;
10165
+ void [_selection, _activeLocale, _preview, _outputScreens, _localeScreens, _selectedShotId, _shots, _rawSlots];
10166
+ const slots = slotsOf(state);
10167
+ const slotAt = (panelId, index) => slots[panelId]?.[index] ?? `${panelId}#${index}`;
10168
+ const owned = (panelId, index) => ownsFrame ? panelId === frameId : focusedSlot !== void 0 && slotAt(panelId, index) === focusedSlot;
10169
+ const keyed = (panelId, layers) => layers.flatMap((layer, index) => owned(panelId, index) ? [] : [[slotAt(panelId, index), layer]]);
10170
+ const byFrame = (frames) => frames && Object.keys(frames).map((panelId) => [panelId, keyed(panelId, frames[panelId])]).filter(([, entries]) => entries.length > 0);
10171
+ const byLocale = (locales) => locales && Object.keys(locales).map((locale2) => [locale2, byFrame(locales[locale2])]);
10172
+ return JSON.stringify({
10173
+ rest,
10174
+ shots: shotsOutside(state),
10175
+ captionText: byLocale(captionText),
10176
+ outputCaptions: outputCaptions && Object.keys(outputCaptions).map((family) => [family, byLocale(outputCaptions[family])]),
10177
+ captionSlots: Object.keys(slots).map((panelId) => [panelId, slots[panelId].filter((_, index) => !owned(panelId, index))]).filter(([, ids]) => ids.length > 0),
10178
+ staleCaptionTranslations: staleCaptionTranslations && Object.keys(staleCaptionTranslations).map((locale2) => [
10179
+ locale2,
10180
+ staleCaptionTranslations[locale2].flatMap((key) => {
10181
+ const cut = key.lastIndexOf(":");
10182
+ const panelId = key.slice(0, cut);
10183
+ const index = Number(key.slice(cut + 1));
10184
+ return owned(panelId, index) ? [] : [`${panelId}:${slotAt(panelId, index)}`];
10185
+ }).sort()
10186
+ ]),
10187
+ panelColors: perFrame(panelColors),
10188
+ panelBackgrounds: perFrame(panelBackgrounds),
10189
+ frameTemplates: perFrame(frameTemplates)
10190
+ });
10191
+ };
10192
+ const beforeBytes = bytesOutside(before);
10193
+ const afterBytes = bytesOutside(after);
10194
+ return outside(before) !== outside(after) || beforeBytes.length !== afterBytes.length || beforeBytes.some((bytes, index) => bytes !== afterBytes[index]);
10195
+ }
9836
10196
  function phaseFor(cycle) {
9837
10197
  return cycle === 1 ? "applying" : cycle === 2 ? "inspecting" : "adjusting";
9838
10198
  }
@@ -9901,6 +10261,7 @@ async function runAgentTurn(input, ports) {
9901
10261
  const appliedOperations = [];
9902
10262
  const touchedCapabilities = /* @__PURE__ */ new Set();
9903
10263
  const templatedPanels = /* @__PURE__ */ new Set();
10264
+ let instructionSatisfied = false;
9904
10265
  let terminalOutcome = null;
9905
10266
  let invalid2 = false;
9906
10267
  let cancelled = false;
@@ -9953,10 +10314,55 @@ async function runAgentTurn(input, ports) {
9953
10314
  }
9954
10315
  interpretation ||= authorization.interpretation?.trim() || instruction;
9955
10316
  inspection = authorization.inspection?.trim() || "Inspected the current composed result.";
10317
+ const notApplied = /* @__PURE__ */ new Set();
10318
+ if (!authorization.failed && !authorization.terminalOutcome) {
10319
+ if (authorization.patch && cycle > 1) {
10320
+ const guarded = dropRepeatedOperations(authorization.patch, appliedOperations);
10321
+ if (guarded.dropped.length > 0) {
10322
+ authorization = { ...authorization, patch: guarded.patch };
10323
+ notApplied.add("repeated_step");
10324
+ }
10325
+ }
10326
+ if (authorization.patch && instructionSatisfied) {
10327
+ const context = { start: before, now: working, panelIds: authorization.panelIds };
10328
+ let guarded = dropRepeatedAdjustments(authorization.patch, context);
10329
+ if (guarded.dropped.length > 0 && layoutRepairable && renderedState) {
10330
+ const repairing = /* @__PURE__ */ new Set();
10331
+ for (const index of ports.layoutCollisionFrames?.(rendered) ?? []) {
10332
+ const panelId = renderedState.panels[index]?.id;
10333
+ if (panelId) repairing.add(panelId);
10334
+ }
10335
+ if (repairing.size > 0) guarded = dropRepeatedAdjustments(authorization.patch, { ...context, repairing });
10336
+ }
10337
+ if (guarded.dropped.length > 0) {
10338
+ authorization = {
10339
+ ...authorization,
10340
+ patch: patchChangesSomething(guarded.patch) ? guarded.patch : void 0
10341
+ };
10342
+ notApplied.add("repeated_adjustment");
10343
+ }
10344
+ }
10345
+ if (authorization.patch && templatedPanels.size > 0) {
10346
+ const shots = working.shots;
10347
+ const guarded = dropTemplatePoses(
10348
+ authorization.patch,
10349
+ templatedPanels,
10350
+ authorization.panelIds,
10351
+ (deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
10352
+ );
10353
+ if (guarded.dropped) {
10354
+ authorization = { ...authorization, patch: guarded.patch };
10355
+ notApplied.add("template_pose");
10356
+ }
10357
+ }
10358
+ }
10359
+ const planned = !authorization.failed && !authorization.terminalOutcome;
10360
+ const application = planned && authorization.patch ? applyProjectRefinementWithAssets(working, authorization.patch, authorization.panelIds, input.assets ?? []) : null;
10361
+ const effect = !planned ? null : application && authorization.patch ? passEffect(working, application, authorization.patch, authorization.panelIds, notApplied, input.focus) : notApplied.size > 0 && !authorization.patch ? emptiedPassEffect(notApplied) : null;
9956
10362
  const pass = {
9957
10363
  phase,
9958
10364
  generationId: authorization.generationId,
9959
- summary: authorization.failed ? FAILED_PASS_SUMMARY[phase] : inspection,
10365
+ summary: authorization.failed ? FAILED_PASS_SUMMARY[phase] : effect?.summary ?? inspection,
9960
10366
  usage: authorization.usage,
9961
10367
  createdAt: now()
9962
10368
  };
@@ -9977,29 +10383,11 @@ async function runAgentTurn(input, ports) {
9977
10383
  break;
9978
10384
  }
9979
10385
  remainingMismatch = authorization.remainingMismatch?.trim() || authorization.followUp?.trim() || null;
9980
- for (const change of authorization.broaderChanges ?? []) broaderChanges.add(change);
9981
- if (authorization.patch && cycle > 1) {
9982
- const guarded = dropRepeatedOperations(authorization.patch, appliedOperations);
9983
- if (guarded.dropped.length > 0) authorization = { ...authorization, patch: guarded.patch };
9984
- }
9985
- if (authorization.patch && templatedPanels.size > 0) {
9986
- const shots = working.shots;
9987
- const guarded = dropTemplatePoses(
9988
- authorization.patch,
9989
- templatedPanels,
9990
- authorization.panelIds,
9991
- (deviceId) => shots.find((shot) => shot.id === deviceId)?.panelId
9992
- );
9993
- if (guarded.dropped) authorization = { ...authorization, patch: guarded.patch };
10386
+ if (authorization.patch && (!effect || effect.reachesOutsideFocus)) {
10387
+ for (const change of authorization.broaderChanges ?? []) broaderChanges.add(change);
9994
10388
  }
9995
10389
  if (authorization.patch) {
9996
10390
  for (const id of capabilitiesForPatch(authorization.patch)) touchedCapabilities.add(id);
9997
- const application = applyProjectRefinementWithAssets(
9998
- working,
9999
- authorization.patch,
10000
- authorization.panelIds,
10001
- input.assets ?? []
10002
- );
10003
10391
  if (!application) {
10004
10392
  invalid2 = true;
10005
10393
  break;
@@ -10025,6 +10413,7 @@ async function runAgentTurn(input, ports) {
10025
10413
  assetBindings: workingAssetBindings
10026
10414
  };
10027
10415
  selected = checkpoint;
10416
+ if (authorization.complete === true) instructionSatisfied = true;
10028
10417
  if (checkpoint.changed.length > 0 && (!best || checkpoint.quality >= best.quality)) best = checkpoint;
10029
10418
  await emit({
10030
10419
  type: "draft_applied",
@@ -10151,7 +10540,7 @@ async function runAgentTurn(input, ports) {
10151
10540
  void input.requestedCycle;
10152
10541
  return result;
10153
10542
  }
10154
- var AGENT_TURN_PASS_CAP, AgentTurnAuthorizationError, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY;
10543
+ var AGENT_TURN_PASS_CAP, AgentTurnAuthorizationError, EMPTY_USAGE, NO_CHANGE_SUMMARY, NEEDS_INPUT_SUMMARY, FAILED_PASS_SUMMARY, NOT_APPLIED, LANDED_DIFFERENTLY;
10155
10544
  var init_agentTurnCoordinator = __esm({
10156
10545
  "../mockup-engine/appstore/agentTurnCoordinator.ts"() {
10157
10546
  "use strict";
@@ -10163,8 +10552,11 @@ var init_agentTurnCoordinator = __esm({
10163
10552
  init_appStoreLocales();
10164
10553
  init_agentAssetCustody();
10165
10554
  init_agentTurnRepetition();
10555
+ init_agentTurnAdjustments();
10166
10556
  init_templatePoseGuard();
10167
10557
  init_layoutTemplates();
10558
+ init_frameTemplates();
10559
+ init_agentTurnLanded();
10168
10560
  AGENT_TURN_PASS_CAP = 3;
10169
10561
  AgentTurnAuthorizationError = class extends Error {
10170
10562
  constructor(outcome) {
@@ -10188,6 +10580,16 @@ var init_agentTurnCoordinator = __esm({
10188
10580
  inspecting: "The inspection pass did not complete.",
10189
10581
  adjusting: "The correction pass did not complete."
10190
10582
  };
10583
+ NOT_APPLIED = {
10584
+ repeated_step: "a step this turn had already taken",
10585
+ repeated_adjustment: "a further change to what this turn had already adjusted",
10586
+ template_pose: "a new pose for a device its layout template had placed"
10587
+ };
10588
+ LANDED_DIFFERENTLY = {
10589
+ held_at_limit: "a value past its limit, which was held at the limit",
10590
+ shared_offset: "a sideways offset for each phone of a frame that holds several, which stays shared",
10591
+ set_differently: "a value the project took differently"
10592
+ };
10191
10593
  }
10192
10594
  });
10193
10595
 
@@ -10955,10 +11357,14 @@ var init_supabaseRest = __esm({
10955
11357
  });
10956
11358
 
10957
11359
  // ../api/_lib/shareStorage.ts
11360
+ function safePath(path) {
11361
+ if (!isSafeStoragePath(path)) throw new Error(`storage: refusing unsafe object path ${JSON.stringify(path)}`);
11362
+ return path;
11363
+ }
10958
11364
  async function createSignedUploadUrl(path, options = {}) {
10959
11365
  const headers = serviceHeaders({ "Content-Type": "application/json" });
10960
11366
  if (options.upsert) headers["x-upsert"] = "true";
10961
- const res = await fetch(`${storageBase()}/object/upload/sign/${BUCKET}/${path}`, {
11367
+ const res = await fetch(`${storageBase()}/object/upload/sign/${BUCKET}/${safePath(path)}`, {
10962
11368
  method: "POST",
10963
11369
  headers,
10964
11370
  body: JSON.stringify({})
@@ -10971,7 +11377,7 @@ async function createSignedUploadUrl(path, options = {}) {
10971
11377
  return { uploadUrl: `${storageBase()}${data.url}` };
10972
11378
  }
10973
11379
  async function createSignedDownloadUrl(path, expiresInSeconds, downloadFilename) {
10974
- const res = await fetch(`${storageBase()}/object/sign/${BUCKET}/${path}`, {
11380
+ const res = await fetch(`${storageBase()}/object/sign/${BUCKET}/${safePath(path)}`, {
10975
11381
  method: "POST",
10976
11382
  headers: serviceHeaders({ "Content-Type": "application/json" }),
10977
11383
  body: JSON.stringify({ expiresIn: expiresInSeconds })
@@ -10996,7 +11402,7 @@ async function putBytes(uploadUrl, bytes, contentType) {
10996
11402
  }
10997
11403
  }
10998
11404
  async function downloadObject(path) {
10999
- const res = await fetch(`${storageBase()}/object/${BUCKET}/${path}`, { headers: serviceHeaders() });
11405
+ const res = await fetch(`${storageBase()}/object/${BUCKET}/${safePath(path)}`, { headers: serviceHeaders() });
11000
11406
  if (!res.ok) {
11001
11407
  const body = (await res.text().catch(() => "")).slice(0, 200);
11002
11408
  if (res.status === 404 || /"statusCode"\s*:\s*"?404"?/.test(body) || /not_found/.test(body)) {
@@ -11007,7 +11413,7 @@ async function downloadObject(path) {
11007
11413
  return new Uint8Array(await res.arrayBuffer());
11008
11414
  }
11009
11415
  async function getObjectInfo(path) {
11010
- const res = await fetch(`${storageBase()}/object/info/${BUCKET}/${path}`, { headers: serviceHeaders() });
11416
+ const res = await fetch(`${storageBase()}/object/info/${BUCKET}/${safePath(path)}`, { headers: serviceHeaders() });
11011
11417
  if (!res.ok) {
11012
11418
  const body = (await res.text().catch(() => "")).slice(0, 200);
11013
11419
  if (res.status === 404 || /"statusCode"\s*:\s*"?404"?/.test(body) || /not_found/.test(body)) {
@@ -11026,6 +11432,7 @@ var BUCKET, StorageObjectNotFoundError;
11026
11432
  var init_shareStorage = __esm({
11027
11433
  "../api/_lib/shareStorage.ts"() {
11028
11434
  "use strict";
11435
+ init_storagePath();
11029
11436
  init_supabaseRest();
11030
11437
  BUCKET = "share-bundles";
11031
11438
  StorageObjectNotFoundError = class extends Error {
@@ -11380,7 +11787,7 @@ function uploadCustody(userId) {
11380
11787
  }
11381
11788
  function refBelongsToUser(ref, userId, now = Date.now()) {
11382
11789
  const prefix = `uploads/${userId}/`;
11383
- if (!ref.startsWith(prefix) || ref.includes("..")) return false;
11790
+ if (!ref.startsWith(prefix) || !isSafeStoragePath(ref)) return false;
11384
11791
  if (!userId.startsWith(ANONYMOUS_USER_PREFIX)) return true;
11385
11792
  const dir = ref.slice(prefix.length).split("/")[0];
11386
11793
  const expiry = ANONYMOUS_DIR_EXPIRY_RE.exec(dir);
@@ -11394,17 +11801,16 @@ async function fetchUploadedRef(ref, userId) {
11394
11801
  await adoptLegacyAssetOnAuthorizedRead(ref, userId).catch(() => void 0);
11395
11802
  return await downloadObject(ref);
11396
11803
  } catch (err) {
11397
- if (err instanceof StorageObjectNotFoundError) {
11398
- throw new Error(
11399
- `no screenshot was ever uploaded for ref "${ref}". request_screenshot_upload only MINTS an upload slot \u2014 the raw PNG bytes must still be PUT to its uploadUrl before rendering (curl -T screenshot.png "$UPLOAD_URL"). If that PUT failed or was skipped, mint a fresh slot and retry it; slots are single-use and short-lived. Alternatively pass the screenshot as { "url": "https://\u2026" } and this server will fetch it for you. ` + // #180: on ChatGPT mobile the PUT can NEVER succeed (the sandbox cannot resolve the
11400
- // storage host), so "retry it" is not always advice an agent can take. Name the door
11401
- // that has no upload step at all.
11402
- BROWSER_FIRST_ROUTE
11403
- );
11404
- }
11804
+ if (err instanceof StorageObjectNotFoundError) throw new Error(missingUploadMessage(ref));
11405
11805
  throw err;
11406
11806
  }
11407
11807
  }
11808
+ function missingUploadMessage(ref) {
11809
+ return `no screenshot was ever uploaded for ref "${ref}". request_screenshot_upload only MINTS an upload slot \u2014 the raw PNG bytes must still be PUT to its uploadUrl before rendering (curl -T screenshot.png "$UPLOAD_URL"). If that PUT failed or was skipped, mint a fresh slot and retry it; slots are single-use and short-lived. Alternatively pass the screenshot as { "url": "https://\u2026" } and this server will fetch it for you. ` + // #180: on ChatGPT mobile the PUT can NEVER succeed (the sandbox cannot resolve the
11810
+ // storage host), so "retry it" is not always advice an agent can take. Name the door
11811
+ // that has no upload step at all.
11812
+ BROWSER_FIRST_ROUTE;
11813
+ }
11408
11814
  async function fetchPanelRef(ref, userId) {
11409
11815
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ref)) {
11410
11816
  return fetchUploadedRef(ref, userId);
@@ -11578,6 +11984,7 @@ var init_uploads = __esm({
11578
11984
  init_shareStorage();
11579
11985
  init_assetRegistry();
11580
11986
  init_supabaseRest();
11987
+ init_storagePath();
11581
11988
  init_retentionPolicy();
11582
11989
  init_toolContract();
11583
11990
  init_pngOptimizer();
@@ -11660,7 +12067,12 @@ async function readLocalPathAsBase64(path) {
11660
12067
  const { bytes } = await readLocalScreenshotFile(path);
11661
12068
  return (await optimizeScreenshotPng(bytes, `The file at "${path}"`)).toString("base64");
11662
12069
  }
11663
- async function resolvePanelBytes(entry, userId, allowLocalPath) {
12070
+ async function readOneUpload(readUploads, ref) {
12071
+ const bytes = (await readUploads([ref])).get(ref);
12072
+ if (!bytes) throw new Error(`the screenshot for ref "${ref}" could not be read.`);
12073
+ return bytes;
12074
+ }
12075
+ async function resolvePanelBytes(entry, userId, allowLocalPath, readUploads) {
11664
12076
  if ("path" in entry) {
11665
12077
  if (!allowLocalPath) {
11666
12078
  throw new Error(
@@ -11673,14 +12085,20 @@ async function resolvePanelBytes(entry, userId, allowLocalPath) {
11673
12085
  throw new Error(`could not read local panel at "${entry.path}": ${err.message}`);
11674
12086
  }
11675
12087
  }
12088
+ if (readUploads) {
12089
+ if (PANEL_ID_RE.test(entry.ref)) throw new HostedOnlyPanelError(entry.ref);
12090
+ return readOneUpload(readUploads, entry.ref);
12091
+ }
11676
12092
  return entry.ref.includes("/") ? fetchUploadedRef(entry.ref, userId) : fetchPanelRef(entry.ref, userId);
11677
12093
  }
11678
- async function resolveEntry(entry, userId, allowLocalPath) {
12094
+ async function resolveEntry(entry, userId, allowLocalPath, uploaded) {
11679
12095
  if (typeof entry === "string") {
11680
12096
  return (await optimizeScreenshotPng(Buffer.from(entry, "base64"), "Inline screenshot")).toString("base64");
11681
12097
  }
11682
12098
  if ("ref" in entry) {
11683
- const bytes = await fetchUploadedRef(entry.ref, userId);
12099
+ const read = uploaded?.get(entry.ref);
12100
+ if (uploaded && !read) throw new Error(`the screenshot for ref "${entry.ref}" could not be read.`);
12101
+ const bytes = read ?? await fetchUploadedRef(entry.ref, userId);
11684
12102
  return (await optimizeScreenshotPng(bytes, `Screenshot ref "${entry.ref}"`)).toString("base64");
11685
12103
  }
11686
12104
  if ("path" in entry) {
@@ -11696,10 +12114,13 @@ async function resolveEntry(entry, userId, allowLocalPath) {
11696
12114
  }
11697
12115
  return fetchUrlAsBase64(entry.url);
11698
12116
  }
11699
- async function resolveScreenshots(slots, userId, allowLocalPath = false) {
12117
+ async function resolveScreenshots(slots, userId, allowLocalPath = false, readUploads) {
11700
12118
  assertInlinePayloadSizeNested(slots);
11701
- return Promise.all(slots.map((slot2) => Promise.all(slot2.map((entry) => resolveEntry(entry, userId, allowLocalPath)))));
12119
+ const refs = Array.from(new Set(slots.flat().flatMap((entry) => entry && typeof entry === "object" && "ref" in entry && typeof entry.ref === "string" ? [entry.ref] : [])));
12120
+ const uploaded = readUploads && refs.length > 0 ? await readUploads(refs) : null;
12121
+ return Promise.all(slots.map((slot2) => Promise.all(slot2.map((entry) => resolveEntry(entry, userId, allowLocalPath, uploaded)))));
11702
12122
  }
12123
+ var PANEL_ID_RE, HostedOnlyPanelError;
11703
12124
  var init_screenshotInput = __esm({
11704
12125
  "src/render/screenshotInput.ts"() {
11705
12126
  "use strict";
@@ -11707,6 +12128,15 @@ var init_screenshotInput = __esm({
11707
12128
  init_uploads();
11708
12129
  init_pngOptimizer();
11709
12130
  init_toolContract();
12131
+ PANEL_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12132
+ HostedOnlyPanelError = class extends Error {
12133
+ constructor(ref) {
12134
+ super(
12135
+ `Rendered panel ids are hosted-only \u2014 this local server cannot read panel "${ref}". Pass the rendered PNG as { "path": "\u2026" } instead.`
12136
+ );
12137
+ this.name = "HostedOnlyPanelError";
12138
+ }
12139
+ };
11710
12140
  }
11711
12141
  });
11712
12142
 
@@ -11716,22 +12146,96 @@ function screenNamesFromRecord(record8) {
11716
12146
  if (!Array.isArray(shots)) return [];
11717
12147
  return shots.map((s) => s?.frameName).filter((n) => typeof n === "string" && n.length > 0);
11718
12148
  }
11719
- function handoffRefsFromScreenshots(screenshots2, frameNames, userId) {
11720
- if (!Array.isArray(screenshots2) || screenshots2.length === 0) return {};
11721
- const wanted = new Set(frameNames);
11722
- const out = {};
11723
- for (const slot2 of normalizeScreenshotSlots(screenshots2)) {
11724
- for (const entry of slot2) {
11725
- if (!entry || typeof entry !== "object" || !("ref" in entry) || typeof entry.ref !== "string") continue;
11726
- const ref = canonicalizeUploadRef(entry.ref);
11727
- if (!refBelongsToUser(ref, userId)) continue;
11728
- const name = entryName(entry);
11729
- if (!name || !wanted.has(name)) continue;
11730
- out[name] = ref;
12149
+ function refOf(entry) {
12150
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
12151
+ const ref = entry.ref;
12152
+ return typeof ref === "string" && ref.length > 0 ? canonicalizeUploadRef(ref) : null;
12153
+ }
12154
+ function introducedScreenCells(screenshots2, projectFile) {
12155
+ const out = { manifest: [], refs: [], unaddressed: [] };
12156
+ if (!Array.isArray(screenshots2) || screenshots2.length === 0) return out;
12157
+ const entries = normalizeScreenshotSlots(screenshots2).flat();
12158
+ const shots = shotRefsFromProjectFile(projectFile);
12159
+ const { baseLocale } = projectLocaleAxis(projectFile);
12160
+ entries.forEach((entry, i) => {
12161
+ const shot = entries.length === shots.length ? shots[i] : void 0;
12162
+ const cells = [];
12163
+ if (isLocaleVariants(entry)) {
12164
+ const base = refOf(resolveLocaleVariant(entry, baseLocale, baseLocale));
12165
+ if (base) cells.push({ ref: base, variant: {} });
12166
+ for (const [locale2, variant] of Object.entries(entry.locales)) {
12167
+ const ref = locale2 === baseLocale ? null : refOf(variant);
12168
+ if (ref) cells.push({ ref, variant: canonicalVariant({ locale: locale2 }, baseLocale), locale: locale2 });
12169
+ }
12170
+ } else {
12171
+ const ref = refOf(entry);
12172
+ if (ref) cells.push({ ref, variant: {} });
11731
12173
  }
11732
- }
12174
+ for (const cell of cells) {
12175
+ const named = {
12176
+ ref: cell.ref,
12177
+ frame: i + 1,
12178
+ frameName: shot?.frameName ?? null,
12179
+ ...cell.locale ? { locale: cell.locale } : {}
12180
+ };
12181
+ out.refs.push(named);
12182
+ if (!shot) out.unaddressed.push(named);
12183
+ else out.manifest.push({ shotId: shot.id, variant: cell.variant, ref: cell.ref });
12184
+ }
12185
+ });
11733
12186
  return out;
11734
12187
  }
12188
+ function describeUnsavedRefs(refs) {
12189
+ return refs.map((entry) => {
12190
+ const where = [
12191
+ `frame ${entry.frame}`,
12192
+ entry.frameName ? `"${entry.frameName}"` : null,
12193
+ entry.locale ? `locale ${entry.locale}` : null
12194
+ ].filter(Boolean).join(", ");
12195
+ return `${entry.ref} (${where})`;
12196
+ }).join("; ");
12197
+ }
12198
+ function refusedRefs(kind, refs) {
12199
+ const list = describeUnsavedRefs(refs);
12200
+ const details = { frames: refs.map(({ ref: _ref, ...where }) => where) };
12201
+ switch (kind) {
12202
+ case "no_account":
12203
+ return {
12204
+ code: "authentication_required",
12205
+ details,
12206
+ error: `This local server has no ShotOps account, so it cannot save { "ref" } screenshots \u2014 a ref points at an upload inside an account: ${list}. Pass each screenshot as a local { "path": "\u2026" } PNG, or sign in (npx shotops-mcp login) and save again. Nothing was saved.`
12207
+ };
12208
+ case "account_unknown":
12209
+ return {
12210
+ code: "upstream_unavailable",
12211
+ details,
12212
+ error: `Could not confirm which ShotOps account this server\u2019s token signs in as, so these { "ref" } screenshots could not be checked: ${list}. Nothing was saved \u2014 try again in a moment.`
12213
+ };
12214
+ case "foreign":
12215
+ return {
12216
+ code: "asset_not_found",
12217
+ details,
12218
+ error: `These { "ref" } screenshots do not belong to this account: ${list}. A ref has to be an upload under the account this save writes to. Pass those screenshots again from a source this account owns \u2014 a ref from import_screenshot or request_screenshot_upload, or a local { "path": "\u2026" } on the local server. Nothing was saved.`
12219
+ };
12220
+ case "unaddressed":
12221
+ return {
12222
+ code: "internal_error",
12223
+ details,
12224
+ error: `These { "ref" } screenshots could not be matched to a frame of the project being saved: ${list}. Nothing was saved.`
12225
+ };
12226
+ }
12227
+ }
12228
+ async function unsavableRefRefusal(deps, introduced) {
12229
+ if (introduced.refs.length === 0) return null;
12230
+ if (introduced.unaddressed.length > 0) return refusedRefs("unaddressed", introduced.unaddressed);
12231
+ if (deps.createPendingProjectClaim) return refusedRefs("no_account", introduced.refs);
12232
+ let owner = deps.userId;
12233
+ if (deps.accountUserId) owner = await deps.accountUserId().catch(() => null);
12234
+ if (!owner) return refusedRefs("account_unknown", introduced.refs);
12235
+ const account = owner;
12236
+ const foreign = introduced.refs.filter((entry) => !refBelongsToUser(entry.ref, account));
12237
+ return foreign.length > 0 ? refusedRefs("foreign", foreign) : null;
12238
+ }
11735
12239
  function projectShotRefsFromRecord(record8) {
11736
12240
  return shotRefsFromProjectFile(record8?.project);
11737
12241
  }
@@ -11779,17 +12283,16 @@ function projectSaveEnvelope(stored, projectFile, manifest, source, listing2) {
11779
12283
  async function persistProject(deps, args, record8, claimPolicy = "none", listing2) {
11780
12284
  const names = screenNamesFromRecord(record8);
11781
12285
  const source = names.length === 0 ? void 0 : args.sourceDir ? { kind: "local-path", ref: { dir: args.sourceDir, names } } : { kind: "agent", ref: { names } };
11782
- const introducedRefs = handoffRefsFromScreenshots(args.screenshots, names, deps.userId);
12286
+ const introduced = introducedScreenCells(args.screenshots, record8);
12287
+ const refusal = await unsavableRefRefusal(deps, introduced);
12288
+ if (refusal) return refusal;
11783
12289
  let screenNote;
11784
- const wrapWith = (handoffRefs, existingRecord) => {
12290
+ const wrapWith = (existingRecord) => {
11785
12291
  const preserved = preservedCells(existingRecord, record8);
11786
12292
  if (preserved.findings.length > 0) {
11787
12293
  screenNote = preserved.findings.map((finding) => `The stored screenshot filed under "${finding.frameName}" was not carried over: that filename names ${finding.shotIds.length} devices (${finding.shotIds.join(", ")}), so no single device owns it. Its object is retained; give those devices distinct filenames, or pass their screenshots explicitly.`).join(" ");
11788
12294
  }
11789
- const manifest = mergeManifest(
11790
- preserved.manifest,
11791
- manifestFromNamedRefs(handoffRefs, shotRefsFromProjectFile(record8))
11792
- );
12295
+ const manifest = mergeManifest(preserved.manifest, introduced.manifest);
11793
12296
  return projectSaveEnvelope(existingRecord, record8, manifest, source, listing2);
11794
12297
  };
11795
12298
  const ctx = {
@@ -11815,7 +12318,7 @@ async function persistProject(deps, args, record8, claimPolicy = "none", listing
11815
12318
  };
11816
12319
  }
11817
12320
  const name = args.projectName?.trim() || "ShotOps render";
11818
- return await deps.createPendingProjectClaim(name, wrapWith(introducedRefs), ctx);
12321
+ return await deps.createPendingProjectClaim(name, wrapWith(), ctx);
11819
12322
  }
11820
12323
  let projectId;
11821
12324
  if (args.project) {
@@ -11831,7 +12334,7 @@ async function persistProject(deps, args, record8, claimPolicy = "none", listing
11831
12334
  if (denial) return { error: denial };
11832
12335
  const existing = await deps.readProjectRecord(project2.id);
11833
12336
  if (!existing) return { error: `Project "${project2.name}" has no readable saved record.` };
11834
- await deps.updateProjectRecord(project2.id, wrapWith(introducedRefs, existing.record), existing.recordVersion ?? 1, ctx);
12337
+ await deps.updateProjectRecord(project2.id, wrapWith(existing.record), existing.recordVersion ?? 1, ctx);
11835
12338
  projectId = project2.id;
11836
12339
  } else {
11837
12340
  const name = args.projectName?.trim() || "ShotOps render";
@@ -11844,7 +12347,7 @@ async function persistProject(deps, args, record8, claimPolicy = "none", listing
11844
12347
  const denial = authorizationMessage(decision);
11845
12348
  if (denial) return { error: denial };
11846
12349
  }
11847
- const created = await deps.createProject(deps.userId, name, wrapWith(introducedRefs), ctx);
12350
+ const created = await deps.createProject(deps.userId, name, wrapWith(), ctx);
11848
12351
  projectId = created.id;
11849
12352
  }
11850
12353
  return { projectId, openUrl: `${deps.studioOrigin}/?project=${projectId}`, ...screenNote ? { screenNote } : {} };
@@ -11865,6 +12368,9 @@ var init_projectPersistence = __esm({
11865
12368
  init_screenManifest();
11866
12369
  init_recordEnvelope();
11867
12370
  init_appListing();
12371
+ init_localeAxis();
12372
+ init_localeVariants();
12373
+ init_variant();
11868
12374
  init_authorization();
11869
12375
  init_projectResolution();
11870
12376
  init_schemas();
@@ -12912,7 +13418,7 @@ var init_package = __esm({
12912
13418
  "package.json"() {
12913
13419
  package_default = {
12914
13420
  name: "shotops-mcp",
12915
- version: "0.9.8",
13421
+ version: "0.9.9",
12916
13422
  private: false,
12917
13423
  type: "module",
12918
13424
  description: "The bundle-emitting MCP server over the @engine/@sync spine. Exposes ShotOps tools to ChatGPT, Claude Code, Cursor, and CI over Streamable HTTP with OAuth or personal API tokens. Hosted and MCP tool paths never accept, store, or forward a store-signing credential; the explicit local release CLI validates the user's existing key directly with Apple and records only its path. Renders via headless Playwright Chromium, the engine's native non-browser habitat (same path as mockup-mcp). Also ships as a free LOCAL stdio server (`npx shotops-mcp`) \u2014 same render, on your own machine, no account needed.",
@@ -13382,7 +13888,7 @@ async function renderStrip(deps, args, scope, signal) {
13382
13888
  if (durableHostedProductionAvailable(deps) && args.preview !== true) {
13383
13889
  let durableScreenshots;
13384
13890
  try {
13385
- durableScreenshots = await resolveScreenshots(entries, deps.userId, false);
13891
+ durableScreenshots = await resolveScreenshots(entries, deps.userId, false, deps.readUploadedRefs);
13386
13892
  } catch (err) {
13387
13893
  return errorResult(err.message);
13388
13894
  }
@@ -13435,7 +13941,7 @@ async function renderStrip(deps, args, scope, signal) {
13435
13941
  if (refused) return errorResult(refused.error);
13436
13942
  let screenshots2;
13437
13943
  try {
13438
- screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
13944
+ screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
13439
13945
  } catch (err) {
13440
13946
  return errorResult(err.message);
13441
13947
  }
@@ -13714,7 +14220,7 @@ async function resolveProjectScreens(deps, loaded, supplied) {
13714
14220
  const nested = supplied.map((e) => [e]);
13715
14221
  let resolvedBytes;
13716
14222
  try {
13717
- resolvedBytes = await resolveScreenshots(nested, deps.userId, deps.allowLocalScreenshots ?? false);
14223
+ resolvedBytes = await resolveScreenshots(nested, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
13718
14224
  } catch (err) {
13719
14225
  return { error: err.message };
13720
14226
  }
@@ -15164,6 +15670,10 @@ function parseControlPlaneResponse(op, value) {
15164
15670
  case "read_attachment_screens":
15165
15671
  if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
15166
15672
  break;
15673
+ case "read_upload_screens":
15674
+ if (!Array.isArray(body.screens)) throw new Error("control plane response is missing screens");
15675
+ if (body.missing !== void 0 && !Array.isArray(body.missing)) throw new Error("control plane response has malformed missing");
15676
+ break;
15167
15677
  case "refine_project":
15168
15678
  if (typeof body.ok !== "boolean") throw new Error("control plane response is missing refine_project result");
15169
15679
  if (body.ok) {
@@ -15370,8 +15880,25 @@ async function readAccountSnapshot(token2) {
15370
15880
  if (!body) return unavailable2("billing_unavailable");
15371
15881
  return snapshotFromEntitlementBody(body);
15372
15882
  }
15883
+ async function readAccountUserId(token2) {
15884
+ try {
15885
+ const res = await fetch(`${studioOrigin()}/api/billing-entitlement`, {
15886
+ headers: { Authorization: `Bearer ${token2}`, [SHOTOPS_CLIENT_HEADER]: `shotops-mcp/${VERSION}` }
15887
+ });
15888
+ if (!res.ok) return null;
15889
+ const body = await res.json().catch(() => null);
15890
+ return typeof body?.userId === "string" && body.userId.length > 0 ? body.userId : null;
15891
+ } catch {
15892
+ return null;
15893
+ }
15894
+ }
15373
15895
  function connectHostedBridge(token2) {
15896
+ let accountId = null;
15374
15897
  return {
15898
+ async accountUserId() {
15899
+ accountId ??= await readAccountUserId(token2);
15900
+ return accountId;
15901
+ },
15375
15902
  async productionOperation(action, operationId) {
15376
15903
  const response = await callOp(token2, "production_operation", { action, operationId });
15377
15904
  return response.operation;
@@ -15415,6 +15942,18 @@ function connectHostedBridge(token2) {
15415
15942
  const res = await callOp(token2, "read_attachment_screens", { project: projectId, refs: [...refs] });
15416
15943
  return res.screens;
15417
15944
  },
15945
+ async readUploadScreens(refs) {
15946
+ try {
15947
+ const res = await callOp(token2, "read_upload_screens", { refs: [...refs] });
15948
+ return { screens: res.screens, missing: res.missing ?? [] };
15949
+ } catch (err) {
15950
+ if (err instanceof ControlPlaneOperationError && err.code === "foreign_refs") {
15951
+ const named = err.refs.length > 0 ? err.refs : [...refs];
15952
+ throw new Error(`ref does not belong to this account: ${named.join(", ")}`);
15953
+ }
15954
+ throw err;
15955
+ }
15956
+ },
15418
15957
  async refineProject(input) {
15419
15958
  return callOp(token2, "refine_project", input);
15420
15959
  },
@@ -15490,6 +16029,8 @@ var init_hostedBridge = __esm({
15490
16029
  code;
15491
16030
  capability;
15492
16031
  requiredPlan;
16032
+ /** The refs a `read_upload_screens` refusal named (#1014); empty on every other refusal. */
16033
+ refs;
15493
16034
  constructor(op, status, body) {
15494
16035
  const code = typeof body?.error === "string" ? body.error : null;
15495
16036
  super(`${op} failed (${status}${code ? ` ${code}` : ""})`);
@@ -15498,6 +16039,7 @@ var init_hostedBridge = __esm({
15498
16039
  this.code = code;
15499
16040
  this.capability = typeof body?.capability === "string" ? body.capability : null;
15500
16041
  this.requiredPlan = typeof body?.requiredPlan === "string" ? body.requiredPlan : null;
16042
+ this.refs = Array.isArray(body?.refs) ? body.refs.filter((ref) => typeof ref === "string") : [];
15501
16043
  }
15502
16044
  };
15503
16045
  RATE_LIMIT_RETRIES = 3;
@@ -15533,6 +16075,7 @@ function createAgentTurnOp(sources) {
15533
16075
  authorize: ports.authorize,
15534
16076
  emit: ports.emit,
15535
16077
  unresolvedLayout: (rendered) => describeUnresolvedLayout(ports.layoutFindings(rendered)),
16078
+ layoutCollisionFrames: (rendered) => ports.layoutFindings(rendered)?.flatMap((finding) => finding.frames) ?? null,
15536
16079
  commit: async ({ after, assetBindings, expectedVersion }) => {
15537
16080
  const current = ports.readCustody();
15538
16081
  const custody = assetBindings.length > 0 ? bindAgentAssets(
@@ -16882,8 +17425,8 @@ async function postClaim(body) {
16882
17425
  const parsed = await res.json().catch(() => null);
16883
17426
  return { status: res.status, body: parsed ?? {} };
16884
17427
  }
16885
- function claimFailure(step, status, code) {
16886
- const detail = status == null ? step : `${step} (${status}${typeof code === "string" && code ? ` ${code}` : ""})`;
17428
+ function claimFailure(step2, status, code) {
17429
+ const detail = status == null ? step2 : `${step2} (${status}${typeof code === "string" && code ? ` ${code}` : ""})`;
16887
17430
  return new Error(
16888
17431
  `Images delivered, but the editable project was not created: pending-claim ${detail}. The local render/export is still usable; retry save_project to create a fresh private claim link.`
16889
17432
  );
@@ -16978,6 +17521,16 @@ var init_projectClaimClient = __esm({
16978
17521
 
16979
17522
  // src/runtime/localDeps.ts
16980
17523
  import { readFile as readFile4 } from "node:fs/promises";
17524
+ function refsNeedAccount(refs) {
17525
+ throw new Error(
17526
+ `A { "ref" } screenshot needs a signed-in ShotOps account, and this local server has none (${refs.join(", ")}). Pass each screenshot as a local { "path": "\u2026" } PNG, or sign in (npx shotops-mcp login) and try again.`
17527
+ );
17528
+ }
17529
+ async function readUploadsThroughBridge(bridge, refs) {
17530
+ const { screens, missing } = await bridge.readUploadScreens(refs);
17531
+ if (missing.length > 0) throw new Error(missingUploadMessage(missing[0]));
17532
+ return new Map(await Promise.all(screens.map(async ({ ref, url }) => [ref, new Uint8Array(await downloadPng(url, `Screenshot ref "${ref}"`))])));
17533
+ }
16981
17534
  function localToolDeps(token2, options = {}) {
16982
17535
  const base = {
16983
17536
  // #889 — the first render on a machine with no Playwright Chromium downloads it once, here only.
@@ -17036,6 +17589,7 @@ function localToolDeps(token2, options = {}) {
17036
17589
  throw new Error(`Saving an editable project is ${SIGN_IN}`);
17037
17590
  },
17038
17591
  createPendingProjectClaim: (name, record8, ctx) => createAnonymousProjectClaim(name, record8, ctx?.screenshots),
17592
+ readUploadedRefs: async (refs) => refsNeedAccount(refs),
17039
17593
  updateProjectRecord: async () => {
17040
17594
  throw new Error(`Updating a saved project is ${SIGN_IN}`);
17041
17595
  },
@@ -17057,6 +17611,13 @@ function localToolDeps(token2, options = {}) {
17057
17611
  // Read FRESH on every production request and never cached: that is what makes a revoked,
17058
17612
  // expired or downgraded token take effect on the next call rather than at the next restart.
17059
17613
  readAccountSnapshot: () => bridge.readAccountSnapshot(),
17614
+ // #1014 — `userId` is 'local' on this tier, so the account a `{ ref }` must belong to comes
17615
+ // from the token. Without this every ref a save was handed failed the ownership test and was
17616
+ // dropped from the project it reported saving.
17617
+ accountUserId: () => bridge.accountUserId(),
17618
+ // #1014 — the same placeholder `userId` made every render path refuse the caller's own refs as
17619
+ // "not this account's". The control plane checks them against the token's account instead.
17620
+ readUploadedRefs: (refs) => readUploadsThroughBridge(bridge, refs),
17060
17621
  productionOperation: {
17061
17622
  execute: (_userId, action, operationId) => bridge.productionOperation(action, operationId)
17062
17623
  },
@@ -17186,6 +17747,7 @@ var init_localDeps = __esm({
17186
17747
  init_screenManifest();
17187
17748
  init_fingerprint();
17188
17749
  init_persistenceOps();
17750
+ init_uploads();
17189
17751
  SIGN_IN = "not available in local (free) mode \u2014 connect the hosted ShotOps MCP and sign in for saved looks, editable projects, and share links.";
17190
17752
  SHARE_LINK_NOT_BRIDGED = `Creating a share link is ${SIGN_IN} (bridging this needs a hosted rendering step, which local mode deliberately avoids \u2014 connect directly to the hosted MCP for share links.)`;
17191
17753
  }
@@ -28305,6 +28867,10 @@ init_renderCredits();
28305
28867
  init_browserInstall();
28306
28868
  init_appstore();
28307
28869
  init_productionOperationStart();
28870
+ function panelReadFailure(err) {
28871
+ if (err instanceof HostedOnlyPanelError) return errorResult(`failed to read panel: ${err.message}`, "unsupported_input");
28872
+ return errorResult(`failed to read panel: ${err.message}`);
28873
+ }
28308
28874
  function dedupeLocales(list) {
28309
28875
  if (!Array.isArray(list)) return [];
28310
28876
  const seen = /* @__PURE__ */ new Set();
@@ -28345,7 +28911,7 @@ async function directDurableBundlePanels(input) {
28345
28911
  input.slots,
28346
28912
  locale2
28347
28913
  );
28348
- const screenshots2 = await resolveScreenshots(entries, input.deps.userId, false);
28914
+ const screenshots2 = await resolveScreenshots(entries, input.deps.userId, false, input.deps.readUploadedRefs);
28349
28915
  const preset = output2?.panelPresetId ?? input.panelPresetId ?? "r69";
28350
28916
  const dimensions = panelDimensionsFor(preset);
28351
28917
  const slotSizes = screenshots2.map((slot2) => slot2.length);
@@ -28684,7 +29250,7 @@ async function emitBundle(deps, args, scope, attempt, signal) {
28684
29250
  }
28685
29251
  let screenshots2;
28686
29252
  try {
28687
- screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
29253
+ screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
28688
29254
  } catch (err) {
28689
29255
  return { ok: false, error: err.message };
28690
29256
  }
@@ -28899,9 +29465,9 @@ async function emitBundle(deps, args, scope, attempt, signal) {
28899
29465
  readinessReport = resolvedInput.report;
28900
29466
  let bytesList;
28901
29467
  try {
28902
- bytesList = await Promise.all(panelEntries.map((p) => resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false)));
29468
+ bytesList = await Promise.all(panelEntries.map((p) => resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs)));
28903
29469
  } catch (err) {
28904
- return errorResult(`failed to read panel: ${err.message}`);
29470
+ return panelReadFailure(err);
28905
29471
  }
28906
29472
  slots = bytesList.map((bytes, i) => ({ displayPosition: i + 1, bytes: Buffer.from(bytes) }));
28907
29473
  previewPng = bytesList[0] ? Buffer.from(bytesList[0]) : null;
@@ -29041,7 +29607,7 @@ async function emitBundle(deps, args, scope, attempt, signal) {
29041
29607
  if (refused) return errorResult(refused.error);
29042
29608
  let screenshots2;
29043
29609
  try {
29044
- screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
29610
+ screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
29045
29611
  } catch (err) {
29046
29612
  return errorResult(err.message);
29047
29613
  }
@@ -29381,7 +29947,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
29381
29947
  const key = "path" in p ? `path:${p.path}` : `ref:${p.ref}`;
29382
29948
  let pending = bytesCache.get(key);
29383
29949
  if (!pending) {
29384
- pending = resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false);
29950
+ pending = resolvePanelBytes(p, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
29385
29951
  bytesCache.set(key, pending);
29386
29952
  }
29387
29953
  return pending;
@@ -29397,7 +29963,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
29397
29963
  });
29398
29964
  }
29399
29965
  } catch (err) {
29400
- return errorResult(`failed to read panel: ${err.message}`);
29966
+ return panelReadFailure(err);
29401
29967
  }
29402
29968
  const firstPanel = localeSlots[0]?.slots[0];
29403
29969
  previewPng = firstPanel ? Buffer.from(firstPanel.bytes) : null;
@@ -29419,7 +29985,7 @@ async function emitMultiLocaleBundle(deps, args, locales, projectName2, scope, p
29419
29985
  }
29420
29986
  let screenshots2;
29421
29987
  try {
29422
- screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false);
29988
+ screenshots2 = await resolveScreenshots(entries, deps.userId, deps.allowLocalScreenshots ?? false, deps.readUploadedRefs);
29423
29989
  } catch (err) {
29424
29990
  return errorResult(err.message);
29425
29991
  }
@@ -30060,7 +30626,7 @@ async function handleSaveProject(deps, args) {
30060
30626
  "unsigned-new",
30061
30627
  listing2
30062
30628
  );
30063
- if ("error" in p) return errorResult(p.error);
30629
+ if ("error" in p) return errorResult(p.error, p.code, p.details);
30064
30630
  const note = ["screenNote" in p && p.screenNote ? p.screenNote : void 0, baseNote].filter(Boolean).join(" ") || void 0;
30065
30631
  if ("status" in p) {
30066
30632
  return textResult({