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) !== "
|
|
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((
|
|
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
|
|
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,
|
|
5469
|
-
if (
|
|
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 (
|
|
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
|
|
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:
|
|
5650
|
-
message: statusMessage(deps, snapshot,
|
|
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
|
|
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
|
-
|
|
9981
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
11720
|
-
if (!
|
|
11721
|
-
const
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
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
|
|
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 = (
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
16886
|
-
const detail = status == null ?
|
|
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
|
|
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
|
|
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({
|