shotops-mcp 0.9.1 → 0.9.2
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/README.md +22 -23
- package/dist/local.js +294 -125
- package/harness-dist/assets/index-DdK1IK1N.js +1 -0
- package/harness-dist/index.html +1 -1
- package/package.json +1 -1
- package/harness-dist/assets/index-Wb7M1qGz.js +0 -1
package/dist/local.js
CHANGED
|
@@ -20,9 +20,11 @@ import { URL as NodeUrl, fileURLToPath } from "node:url";
|
|
|
20
20
|
function studioOrigin() {
|
|
21
21
|
return (process.env.STUDIO_ORIGIN || "https://shotops.dev").replace(/\/+$/, "");
|
|
22
22
|
}
|
|
23
|
+
function authorityOrigin() {
|
|
24
|
+
return (process.env.SHOTOPS_AUTH_ORIGIN || "https://mcp.shotops.dev").replace(/\/+$/, "");
|
|
25
|
+
}
|
|
23
26
|
function deployTarget() {
|
|
24
27
|
if (process.env.VERCEL) return "vercel";
|
|
25
|
-
if (process.env.FLY_APP_NAME) return "fly";
|
|
26
28
|
return "local";
|
|
27
29
|
}
|
|
28
30
|
function maxInlineScreenshotBytes() {
|
|
@@ -890,13 +892,19 @@ var init_capabilities = __esm({
|
|
|
890
892
|
});
|
|
891
893
|
|
|
892
894
|
// src/authorization.ts
|
|
895
|
+
function planRequiredMessage(requiredPlan) {
|
|
896
|
+
if (requiredPlan && requiredPlan !== "pro") {
|
|
897
|
+
return `This action needs the ${requiredPlan} plan \u2014 upgrade at https://shotops.dev/app.`;
|
|
898
|
+
}
|
|
899
|
+
return "This action needs an active Pro trial or Pro. Start the free trial or upgrade at https://shotops.dev/app.";
|
|
900
|
+
}
|
|
893
901
|
function authorizationMessage(decision) {
|
|
894
902
|
if (decision.allowed) return null;
|
|
895
903
|
switch (decision.reason) {
|
|
896
904
|
case "authentication_required":
|
|
897
905
|
return "Sign in to use this capability.";
|
|
898
906
|
case "plan_required":
|
|
899
|
-
return
|
|
907
|
+
return planRequiredMessage(decision.requiredPlan);
|
|
900
908
|
case "trial_choice_required":
|
|
901
909
|
return "Your Pro trial has ended. Choose Pro or Free at https://shotops.dev/app before using this action.";
|
|
902
910
|
case "limit_reached":
|
|
@@ -932,7 +940,7 @@ function capabilityDenialFromError(error) {
|
|
|
932
940
|
function capabilityDenialMessage(denial) {
|
|
933
941
|
switch (denial.reason) {
|
|
934
942
|
case "plan_required":
|
|
935
|
-
return
|
|
943
|
+
return planRequiredMessage(denial.requiredPlan);
|
|
936
944
|
case "trial_choice_required":
|
|
937
945
|
return "Your Pro trial has ended. Choose Pro or Free at https://shotops.dev/app before using this action.";
|
|
938
946
|
case "billing_unavailable":
|
|
@@ -1512,18 +1520,37 @@ ${list}${more}` : "";
|
|
|
1512
1520
|
}
|
|
1513
1521
|
return "No such project on this account." + offer;
|
|
1514
1522
|
}
|
|
1523
|
+
function disclosureStatement(disclosure) {
|
|
1524
|
+
return `This call moves ${disclosure.moves}, because ${disclosure.why}. It is kept for ${disclosure.retentionDays} days and then removed. What retains it: ${disclosure.retainedBy}`;
|
|
1525
|
+
}
|
|
1515
1526
|
function buildIdentityInstructions(version2) {
|
|
1516
1527
|
return `This server is ShotOps build \`${version2}\` (package version, plus the deployed commit after the \`+\` when hosted) \u2014 quote it verbatim if asked what version of ShotOps this is, and say that nothing more precise is visible from here. A project\u2019s \`schema\` and a look\u2019s version number (v70, v71 \u2026) describe the USER\u2019S saved work and are never the answer to that question.`;
|
|
1517
1528
|
}
|
|
1518
|
-
var BROWSER_FIRST_ROUTE,
|
|
1529
|
+
var BROWSER_FIRST_ROUTE, OFFER_SAVE_PROJECT_NEXT_STEP, ACCOUNT_UPLOAD_DISCLOSURE, PENDING_CLAIM_DISCLOSURE, SERVER_INSTRUCTIONS, ANONYMOUS_TOOLS;
|
|
1519
1530
|
var init_toolContract = __esm({
|
|
1520
1531
|
"src/toolContract.ts"() {
|
|
1521
1532
|
"use strict";
|
|
1522
1533
|
init_shotLookProjection();
|
|
1534
|
+
init_retentionPolicy();
|
|
1523
1535
|
BROWSER_FIRST_ROUTE = `No file transfer is needed at all: the user can sign in at https://shotops.dev, drop the screenshots into a project, set that project's reach to "Your account", and you then render it from here with render_project({ project }) and NO screenshots \u2014 the project already holds them.`;
|
|
1524
|
-
|
|
1536
|
+
OFFER_SAVE_PROJECT_NEXT_STEP = {
|
|
1537
|
+
action: "offer_save_project",
|
|
1525
1538
|
tool: "save_project",
|
|
1526
|
-
|
|
1539
|
+
projectCreated: false,
|
|
1540
|
+
consent: "required",
|
|
1541
|
+
reason: "No ShotOps project exists for this strip and nothing was written \u2014 these panels are images only, so the design behind them ends with the conversation. That is the correct default for a preview, not a problem to fix. OFFER a save when the user shows they want this work preserved, shared, reused or kept editable; only then call save_project with the same screenshots and NO `project` argument, and hand back the openUrl it returns. Asking for images, full-resolution panels or a bundle is not that intent, and neither is silence: if you cannot put the question to the user, return these panels with the offer standing rather than saving on their behalf. Name an existing project as `project` only when they said the work belongs there."
|
|
1542
|
+
};
|
|
1543
|
+
ACCOUNT_UPLOAD_DISCLOSURE = {
|
|
1544
|
+
moves: "the screenshot bytes you supply, into private storage scoped to this ShotOps account",
|
|
1545
|
+
why: "a render reads a stored ref instead of carrying full-resolution pixels through the conversation, and only an account-scoped ref can be re-rendered later",
|
|
1546
|
+
retentionDays: retentionDays(RETENTION_POLICY.hostedAssetTtlMs),
|
|
1547
|
+
retainedBy: "save_project \u2014 a saved project keeps every ref it references for as long as it references it. Nothing else retains an upload, and nothing is retained automatically."
|
|
1548
|
+
};
|
|
1549
|
+
PENDING_CLAIM_DISCLOSURE = {
|
|
1550
|
+
moves: "the raw PNGs read from this machine, together with the project record, into private pending-claim storage on the ShotOps server",
|
|
1551
|
+
why: "an unsigned local save has no account to file the project under, so the project can only reopen whole in a browser if its sources are staged alongside the record",
|
|
1552
|
+
retentionDays: retentionDays(RETENTION_POLICY.pendingProjectClaimTtlMs),
|
|
1553
|
+
retainedBy: "opening the returned openUrl and signing in within the window, which makes that person the owner and turns the staged copy into their project. Unclaimed, the record and the PNGs are swept."
|
|
1527
1554
|
};
|
|
1528
1555
|
SERVER_INSTRUCTIONS = [
|
|
1529
1556
|
"ShotOps makes the marketing screenshots an app store listing needs. Give it the raw screen",
|
|
@@ -1585,18 +1612,17 @@ var init_toolContract = __esm({
|
|
|
1585
1612
|
' render_strip output:"urls" result as `panels` to package without re-rendering. On the LOCAL',
|
|
1586
1613
|
" stdio tier (no account, so no upload refs) pass panels straight off disk instead:",
|
|
1587
1614
|
' panels: [{ "path": "/abs/panel-01.png" }, \u2026] \u2014 the same { path } local-only door as screenshots.',
|
|
1588
|
-
"4.
|
|
1589
|
-
"
|
|
1590
|
-
|
|
1591
|
-
"
|
|
1592
|
-
"
|
|
1593
|
-
"
|
|
1594
|
-
" explicitly says not to save/create a project. If they explicitly say the work belongs in an",
|
|
1595
|
-
" existing project, pass that project as `project` so save_project updates it, and return that",
|
|
1596
|
-
" `openUrl` instead. Never infer an update target from the most-recent project.",
|
|
1615
|
+
"4. PERSISTENCE IS A SEPARATE ASK. A render creates nothing and updates nothing \u2014 the result says",
|
|
1616
|
+
" so structurally (`contract.effects.projectMutation: false`), and a project-less render also",
|
|
1617
|
+
" carries a `nextStep` whose action is `offer_save_project`: an OFFER to relay, never consent to",
|
|
1618
|
+
" act on. Call save_project only once the user has shown they want this work preserved, shared,",
|
|
1619
|
+
" reused or kept editable. Then omit `project` to create a NEW project and hand back its",
|
|
1620
|
+
" `openUrl`; pass `project` only when they named an existing one. See the lifecycle section below.",
|
|
1597
1621
|
"5. Hosted sources and private outputs expire after the retention window unless a Project or another live",
|
|
1598
|
-
" resource retains them. After delivery, OFFER to clean up
|
|
1599
|
-
" Call delete_assets({ assetIds: [...] }) only when the user explicitly asks;
|
|
1622
|
+
" resource retains them. After delivery, OFFER to clean up UNREFERENCED temporary artifacts by",
|
|
1623
|
+
" opaque id. Call delete_assets({ assetIds: [...] }) only when the user explicitly asks; anything a",
|
|
1624
|
+
" Project, operation, share or preview still depends on is refused \u2014 never propose deleting an",
|
|
1625
|
+
" asset a Project references, and never present cleanup as a step that has to happen.",
|
|
1600
1626
|
"",
|
|
1601
1627
|
"## Preview is open; STORE-READY OUTPUT needs an active Pro trial or Pro",
|
|
1602
1628
|
"Two different things, and the split is the same on the hosted server and on `npx shotops-mcp`:",
|
|
@@ -1605,7 +1631,7 @@ var init_toolContract = __esm({
|
|
|
1605
1631
|
"including a zip composed only from panels that were already rendered \u2014 needs a signed-in account",
|
|
1606
1632
|
"on an active Pro trial or Pro. The refusal arrives BEFORE anything is rendered, written,",
|
|
1607
1633
|
"uploaded or charged, so a denied call costs the user nothing and leaves no half-made files.",
|
|
1608
|
-
"account_status answers this in one free call: no credits, no free-preview allowance, no uploads,",
|
|
1634
|
+
"account_status answers this in one free call: no cloud credits, no free-preview allowance, no uploads,",
|
|
1609
1635
|
"no writes. Call it before starting a long full-resolution job you are not sure will be allowed,",
|
|
1610
1636
|
"and relay its `nextStep` (sign_in, choose_plan, upgrade, retry) rather than retrying the refused",
|
|
1611
1637
|
"call unchanged. Never guess at someone's plan and never claim a limit it did not report.",
|
|
@@ -1723,29 +1749,45 @@ var init_toolContract = __esm({
|
|
|
1723
1749
|
"and heldVersion with a structured `history` denial; explicit historical renders use the latest",
|
|
1724
1750
|
"saved Look instead. A retained Hold stays preserved and becomes active again after upgrading.",
|
|
1725
1751
|
"",
|
|
1726
|
-
//
|
|
1752
|
+
// This section is ALSO carried structurally, as OFFER_SAVE_PROJECT_NEXT_STEP above, which
|
|
1727
1753
|
// render_strip attaches to every project-less render because some clients never deliver these
|
|
1728
1754
|
// instructions. Edit the two together: prose that says one thing while the wire says another is
|
|
1729
1755
|
// worse than either alone.
|
|
1730
|
-
"##
|
|
1731
|
-
"
|
|
1732
|
-
"
|
|
1733
|
-
"
|
|
1734
|
-
"
|
|
1735
|
-
"
|
|
1736
|
-
"
|
|
1737
|
-
|
|
1738
|
-
"
|
|
1739
|
-
"
|
|
1740
|
-
"
|
|
1741
|
-
"
|
|
1742
|
-
"
|
|
1743
|
-
"
|
|
1744
|
-
"
|
|
1745
|
-
"
|
|
1746
|
-
"
|
|
1756
|
+
"## The lifecycle \u2014 preview by default, persistence on request",
|
|
1757
|
+
"Six steps, in order. Only the preview happens on its own; every other step needs something the",
|
|
1758
|
+
"user said.",
|
|
1759
|
+
"1. INSPECT when the input, the persistence or the cost is uncertain. account_status is free and",
|
|
1760
|
+
" says what this connection may do, what a preview costs here, and whether store-ready output is",
|
|
1761
|
+
" available at all. read_project says what a saved project already holds.",
|
|
1762
|
+
"2. PREVIEW, and create nothing. render_strip / render_project with `preview: true` shows the design",
|
|
1763
|
+
" without writing a project, updating a project, or asking anyone for permission beyond the",
|
|
1764
|
+
" request itself. This is the default visual step and the thing to reach for first.",
|
|
1765
|
+
"3. OFFER TO SAVE \u2014 only after the user expressed a PRESERVE, COLLABORATE, REUSE or CONTINUE-EDITING",
|
|
1766
|
+
' intent ("keep this", "I want to tweak it later", "send it to my designer"). save_project with NO',
|
|
1767
|
+
" `project` argument then creates a NEW project and always returns an `openUrl` to hand back; a",
|
|
1768
|
+
" signed-in or token-backed call also returns a projectId, while unsigned local stdio returns",
|
|
1769
|
+
' status:"pending_claim" with NO invented id and the user becomes owner by opening the link and',
|
|
1770
|
+
" signing in inside the claim window. If they said the work belongs in an EXISTING project, pass",
|
|
1771
|
+
" it as `project` and save_project updates that one in place. Never infer an update target from",
|
|
1772
|
+
" the most recently edited project, and never read a render, a bundle request or silence as",
|
|
1773
|
+
" agreement to save. If your client cannot put the question to the user at all, deliver the",
|
|
1774
|
+
" result and the offer \u2014 do not save for them.",
|
|
1775
|
+
"4. DISCLOSE BEFORE BYTES MOVE. Ordinary local rendering and exporting upload nothing. Three calls",
|
|
1776
|
+
" do move bytes into ShotOps storage \u2014 import_screenshot and request_screenshot_upload on the",
|
|
1777
|
+
" hosted server, and an UNSIGNED local save_project, which stages its raw local PNGs in private",
|
|
1778
|
+
" claim storage so the claimed project reopens whole. Before each, say what moves, why, how long",
|
|
1779
|
+
" it is kept and the explicit action that retains it. Each of those results carries the same four",
|
|
1780
|
+
" facts as a `disclosure` block; relay it rather than paraphrasing a retention window.",
|
|
1781
|
+
"5. PRODUCE FINAL only once entitlement and readiness allow it, carrying back the exact waiver",
|
|
1782
|
+
" receipts the server handed you. Refusals arrive before any charge, any pixel and any file.",
|
|
1783
|
+
"6. DELIVER, then OFFER cleanup of unreferenced temporary assets only. Never propose deleting",
|
|
1784
|
+
" something a project still points at, and never make cleanup automatic.",
|
|
1785
|
+
"save_project is FAST and never renders \u2014 structure + look only. render_strip does not write",
|
|
1786
|
+
"projects at all. emit_bundle still accepts the older `createProject` flag for backward",
|
|
1787
|
+
"compatibility: use it ONLY when the user explicitly asked for both the bundle and a saved project,",
|
|
1788
|
+
"and never infer it from a bundle request.",
|
|
1747
1789
|
"",
|
|
1748
|
-
"##
|
|
1790
|
+
"## When you DO save, make the strip re-openable",
|
|
1749
1791
|
"The record itself never stores screenshot BYTES (zero-custody). What it can store is a MANIFEST:",
|
|
1750
1792
|
"when the screenshots you rendered with were account-scoped refs (from request_screenshot_upload",
|
|
1751
1793
|
"or import_screenshot), saving the project records where those uploads live, and the user opens it",
|
|
@@ -4564,7 +4606,7 @@ var init_resultContract = __esm({
|
|
|
4564
4606
|
"What this call DID to the world, independent of what it returned. Every flag is stated on every result, false included: an absent flag would be indistinguishable from an effect nobody thought to declare."
|
|
4565
4607
|
);
|
|
4566
4608
|
costSchema = z3.object({
|
|
4567
|
-
unit: z3.literal("credit").describe("Public credits \u2014 the same unit every ShotOps surface quotes."),
|
|
4609
|
+
unit: z3.literal("credit").describe("Public cloud credits \u2014 the same unit every ShotOps surface quotes."),
|
|
4568
4610
|
model: z3.enum(["metered", "anonymous_allowance", "unmetered"]).describe(
|
|
4569
4611
|
"How this connection pays. `unmetered` is local stdio, which renders on the caller\u2019s own machine; `anonymous_allowance` is the unsigned hosted taste."
|
|
4570
4612
|
),
|
|
@@ -4573,7 +4615,7 @@ var init_resultContract = __esm({
|
|
|
4573
4615
|
reserved: z3.number().optional().describe("Held against the wallet for the duration of the call (#666)."),
|
|
4574
4616
|
released: z3.number().optional().describe("Given back \u2014 an unused reservation or a refund after a post-charge failure (#666)."),
|
|
4575
4617
|
settled: z3.number().optional().describe("Actually taken. Absent when nothing was charged."),
|
|
4576
|
-
balanceAfter: z3.number().optional().describe("The wallet\u2019s public credit balance once this call settled."),
|
|
4618
|
+
balanceAfter: z3.number().optional().describe("The wallet\u2019s public cloud credit balance once this call settled."),
|
|
4577
4619
|
refillAt: z3.string().optional().describe("ISO 8601. When the wallet is next topped up; absent when none is scheduled.")
|
|
4578
4620
|
}).passthrough();
|
|
4579
4621
|
artifactSchema = z3.object({
|
|
@@ -4736,7 +4778,7 @@ var init_resultContract = __esm({
|
|
|
4736
4778
|
|
|
4737
4779
|
// src/outputSchemas.ts
|
|
4738
4780
|
import { z as z4 } from "zod";
|
|
4739
|
-
var projectSummary, storedAssetOutput, panelDeliveryOutput, panelOutput, lookHistoryAccess, noteField, creditsField, layoutFindingOutput, layoutAdvisoryOutput, contractField, operationStateOutputSchema, durableRenderFinalOutputSchema, durableBundleFinalOutputSchema, renderStripOutputSchema, optionalShape, operationStateShape, renderStripToolOutputSchema, emitBundleOutputSchema, emitBundleToolOutputSchema, saveProjectOutputSchema, layoutFieldDiff, layoutReportShape, layoutReportOutput, readLookOutputSchema, storedScreenshots, readProjectOutputSchema, agentOutcomeOutput, refineProjectOutputSchema, renderProjectOutputSchema, renderProjectToolOutputSchema, productionOperationOutputSchema, saveLookOutputSchema, holdLookOutputSchema, releaseLookOutputSchema, describeLookOutputSchema, accountCreditBalanceSchema, accountStatusOutputSchema, requestScreenshotUploadOutputSchema, importedScreenshotOutput, importScreenshotOutputSchema, deleteAssetsOutputSchema;
|
|
4781
|
+
var projectSummary, storedAssetOutput, panelDeliveryOutput, panelOutput, lookHistoryAccess, noteField, creditsField, layoutFindingOutput, layoutAdvisoryOutput, contractField, disclosureField, operationStateOutputSchema, durableRenderFinalOutputSchema, durableBundleFinalOutputSchema, renderStripOutputSchema, optionalShape, operationStateShape, renderStripToolOutputSchema, emitBundleOutputSchema, emitBundleToolOutputSchema, saveProjectOutputSchema, layoutFieldDiff, layoutReportShape, layoutReportOutput, readLookOutputSchema, storedScreenshots, readProjectOutputSchema, agentOutcomeOutput, refineProjectOutputSchema, renderProjectOutputSchema, renderProjectToolOutputSchema, productionOperationOutputSchema, saveLookOutputSchema, holdLookOutputSchema, releaseLookOutputSchema, describeLookOutputSchema, accountCreditBalanceSchema, accountStatusOutputSchema, requestScreenshotUploadOutputSchema, importedScreenshotOutput, importScreenshotOutputSchema, deleteAssetsOutputSchema, advertisedWithFailure, refineProjectToolOutputSchema, saveProjectToolOutputSchema, readLookToolOutputSchema, readProjectToolOutputSchema, saveLookToolOutputSchema, holdLookToolOutputSchema, releaseLookToolOutputSchema, requestScreenshotUploadToolOutputSchema, importScreenshotToolOutputSchema, deleteAssetsToolOutputSchema;
|
|
4740
4782
|
var init_outputSchemas = __esm({
|
|
4741
4783
|
"src/outputSchemas.ts"() {
|
|
4742
4784
|
"use strict";
|
|
@@ -4794,6 +4836,15 @@ var init_outputSchemas = __esm({
|
|
|
4794
4836
|
"What is wrong with this render\u2019s COMPOSITION, measured on the panels it just returned. ABSENT means there is nothing to report \u2014 a clean strip, or one whose captions could not be measured. Advisory: the panels are already rendered and nothing here changed a pixel."
|
|
4795
4837
|
);
|
|
4796
4838
|
contractField = resultEnvelopeSchema.optional();
|
|
4839
|
+
disclosureField = z4.object({
|
|
4840
|
+
moves: z4.string().describe("The bytes that left the caller\u2019s control, in plain words."),
|
|
4841
|
+
why: z4.string().describe("What the move buys \u2014 never a restatement of the mechanism."),
|
|
4842
|
+
retentionDays: z4.number().int().positive().describe("How long ShotOps keeps it without an explicit retaining action. Read from the retention policy, never typed."),
|
|
4843
|
+
retainedBy: z4.string().describe("The explicit action that keeps it past that window."),
|
|
4844
|
+
statement: z4.string().describe("The same four facts as one sentence, safe to relay verbatim.")
|
|
4845
|
+
}).passthrough().optional().describe(
|
|
4846
|
+
"Stated because this call moved caller bytes into ShotOps storage. Relay it before or with the result; do not paraphrase the retention window."
|
|
4847
|
+
);
|
|
4797
4848
|
operationStateOutputSchema = z4.object({
|
|
4798
4849
|
ok: z4.boolean(),
|
|
4799
4850
|
operationId: z4.string(),
|
|
@@ -4841,15 +4892,26 @@ var init_outputSchemas = __esm({
|
|
|
4841
4892
|
renderMs: z4.number(),
|
|
4842
4893
|
output: z4.enum(["inline", "urls"]),
|
|
4843
4894
|
locale: z4.string(),
|
|
4844
|
-
//
|
|
4845
|
-
//
|
|
4846
|
-
//
|
|
4895
|
+
// The OFFER a project-less render carries, in the output because that is the one channel every
|
|
4896
|
+
// client delivers (SERVER_INSTRUCTIONS are truncated or dropped by some). Declared rather than
|
|
4897
|
+
// left to .passthrough() so an agent reading the schema sees it exists.
|
|
4898
|
+
//
|
|
4899
|
+
// `action` and `consent` are what keep it an offer. This field used to be a bare
|
|
4900
|
+
// `{ tool: 'save_project', reason }` whose prose told an agent to save unless the user opted
|
|
4901
|
+
// out — a render request read as consent to write a durable record. It is now typed as the
|
|
4902
|
+
// question it always should have been.
|
|
4903
|
+
//
|
|
4847
4904
|
// OPTIONAL for the #196 reason: a render tied to an existing project omits it, and a declared
|
|
4848
4905
|
// output field that a legitimate response omits must never be required — the SDK validates
|
|
4849
4906
|
// structuredContent, so that would turn a SUCCESSFUL render into a validation error.
|
|
4850
4907
|
// renderProjectOutputSchema extends this and never emits it; optional keeps that honest.
|
|
4851
4908
|
nextStep: z4.object({
|
|
4852
|
-
|
|
4909
|
+
action: z4.literal("offer_save_project").describe("An offer to put to the user. Never a command, and never consent already given."),
|
|
4910
|
+
tool: z4.literal("save_project").describe("The door that would persist this work, if they ask for it."),
|
|
4911
|
+
projectCreated: z4.literal(false).describe("No project was created or updated by this call \u2014 the same fact as `contract.effects.projectMutation`."),
|
|
4912
|
+
consent: z4.literal("required").describe(
|
|
4913
|
+
"Saving needs the user\u2019s own preserve/reuse/keep-editing intent. A client that cannot ask returns this offer unresolved rather than saving."
|
|
4914
|
+
),
|
|
4853
4915
|
reason: z4.string()
|
|
4854
4916
|
}).optional(),
|
|
4855
4917
|
credits: creditsField,
|
|
@@ -4911,7 +4973,14 @@ var init_outputSchemas = __esm({
|
|
|
4911
4973
|
// #224 — save_project used to DROP this entirely: it destructured `{ look, style }` out of
|
|
4912
4974
|
// resolveLook and never forwarded the note, on the one tool where a wrong composition is
|
|
4913
4975
|
// PERSISTED rather than merely rendered.
|
|
4914
|
-
note: noteField
|
|
4976
|
+
note: noteField,
|
|
4977
|
+
// Only on the unsigned local pending-claim path, the one save that stages raw PNGs off this
|
|
4978
|
+
// machine. A signed save moves no bytes and states nothing.
|
|
4979
|
+
disclosure: disclosureField,
|
|
4980
|
+
// #662 — the effects of the one call in this contract whose job IS a project write:
|
|
4981
|
+
// `projectMutation` is true here and false on every render, which is what makes "a render never
|
|
4982
|
+
// triggers a project write" a fact an agent can test rather than a promise in prose.
|
|
4983
|
+
contract: contractField
|
|
4915
4984
|
}).passthrough();
|
|
4916
4985
|
layoutFieldDiff = z4.object({
|
|
4917
4986
|
is: z4.string().describe("The value this strip actually holds. `(none)` means the field is absent."),
|
|
@@ -5137,12 +5206,12 @@ var init_outputSchemas = __esm({
|
|
|
5137
5206
|
model: z4.enum(["metered", "anonymous_allowance", "unmetered"]),
|
|
5138
5207
|
previewPanel: z4.number(),
|
|
5139
5208
|
fullResolutionPanel: z4.number()
|
|
5140
|
-
}).passthrough().describe("How work is paid for here, in public credits. Every figure comes from the monetization policy."),
|
|
5209
|
+
}).passthrough().describe("How work is paid for here, in public cloud credits. Every figure comes from the monetization policy."),
|
|
5141
5210
|
failureCatalog: z4.object({ codes: z4.array(z4.string()), nextActions: z4.array(z4.string()) }).passthrough().describe("The CLOSED set of failure codes and next-actions this server can produce. Identical on both doors."),
|
|
5142
5211
|
account: z4.object({
|
|
5143
5212
|
credits: accountCreditBalanceSchema.optional()
|
|
5144
5213
|
}).passthrough().describe(
|
|
5145
|
-
"The account-wide wallet, named apart from a single call\u2019s debit: this is the BALANCE, while a render\u2019s `contract.cost.settled` is what that one call took. The legacy top-level `credits` key still carries the same balance for the compatibility window."
|
|
5214
|
+
"The account-wide cloud credit wallet, named apart from a single call\u2019s debit: this is the BALANCE, while a render\u2019s `contract.cost.settled` is what that one call took. The legacy top-level `credits` key still carries the same balance for the compatibility window."
|
|
5146
5215
|
)
|
|
5147
5216
|
}).passthrough();
|
|
5148
5217
|
requestScreenshotUploadOutputSchema = z4.object({
|
|
@@ -5157,7 +5226,8 @@ var init_outputSchemas = __esm({
|
|
|
5157
5226
|
)
|
|
5158
5227
|
}).passthrough()
|
|
5159
5228
|
),
|
|
5160
|
-
instructions: z4.string()
|
|
5229
|
+
instructions: z4.string(),
|
|
5230
|
+
disclosure: disclosureField
|
|
5161
5231
|
}).passthrough();
|
|
5162
5232
|
importedScreenshotOutput = z4.object({
|
|
5163
5233
|
ok: z4.boolean(),
|
|
@@ -5173,7 +5243,8 @@ var init_outputSchemas = __esm({
|
|
|
5173
5243
|
ok: z4.boolean(),
|
|
5174
5244
|
count: z4.number().int().nonnegative(),
|
|
5175
5245
|
screenshots: z4.array(importedScreenshotOutput),
|
|
5176
|
-
instructions: z4.string()
|
|
5246
|
+
instructions: z4.string(),
|
|
5247
|
+
disclosure: disclosureField
|
|
5177
5248
|
}).passthrough();
|
|
5178
5249
|
deleteAssetsOutputSchema = z4.object({
|
|
5179
5250
|
ok: z4.boolean(),
|
|
@@ -5181,6 +5252,17 @@ var init_outputSchemas = __esm({
|
|
|
5181
5252
|
deletedRefs: z4.array(z4.string()),
|
|
5182
5253
|
message: z4.string()
|
|
5183
5254
|
}).passthrough();
|
|
5255
|
+
advertisedWithFailure = (strict) => z4.object({ ...optionalShape(strict.shape), ok: z4.boolean().optional() }).passthrough();
|
|
5256
|
+
refineProjectToolOutputSchema = advertisedWithFailure(refineProjectOutputSchema);
|
|
5257
|
+
saveProjectToolOutputSchema = advertisedWithFailure(saveProjectOutputSchema);
|
|
5258
|
+
readLookToolOutputSchema = advertisedWithFailure(readLookOutputSchema);
|
|
5259
|
+
readProjectToolOutputSchema = advertisedWithFailure(readProjectOutputSchema);
|
|
5260
|
+
saveLookToolOutputSchema = advertisedWithFailure(saveLookOutputSchema);
|
|
5261
|
+
holdLookToolOutputSchema = advertisedWithFailure(holdLookOutputSchema);
|
|
5262
|
+
releaseLookToolOutputSchema = advertisedWithFailure(releaseLookOutputSchema);
|
|
5263
|
+
requestScreenshotUploadToolOutputSchema = advertisedWithFailure(requestScreenshotUploadOutputSchema);
|
|
5264
|
+
importScreenshotToolOutputSchema = advertisedWithFailure(importScreenshotOutputSchema);
|
|
5265
|
+
deleteAssetsToolOutputSchema = advertisedWithFailure(deleteAssetsOutputSchema);
|
|
5184
5266
|
}
|
|
5185
5267
|
});
|
|
5186
5268
|
|
|
@@ -5278,7 +5360,7 @@ var init_schemas = __esm({
|
|
|
5278
5360
|
"Render a specific saved look version of the project (implies the saved look). Omit = the project's HELD version if one is held (hold_look), else the latest saved look."
|
|
5279
5361
|
);
|
|
5280
5362
|
createProjectFlag = z5.boolean().optional().describe(
|
|
5281
|
-
"
|
|
5363
|
+
"COMPATIBILITY INPUT: also save this render as an editable ShotOps project (returns projectId + an openUrl). Set it ONLY when the user explicitly asked for both the bundle and a saved project \u2014 never infer it from a delivery request, and prefer save_project, which is the normal persistence door. Pass an existing `project` id to update that project instead of creating a new one. Full-res only \u2014 not allowed with preview:true. The record embeds no image bytes; a token-backed local save may separately upload private source refs so the project reopens with its screenshots."
|
|
5282
5364
|
);
|
|
5283
5365
|
projectName = z5.string().optional().describe('Name for the created project (when createProject makes a new one). Default "ShotOps render".');
|
|
5284
5366
|
sourceDir = z5.string().optional().describe(
|
|
@@ -5801,7 +5883,7 @@ var init_monetizationPolicy = __esm({
|
|
|
5801
5883
|
failedRenewalGraceMs: 7 * 24 * 60 * 60 * 1e3
|
|
5802
5884
|
}
|
|
5803
5885
|
};
|
|
5804
|
-
LEDGER_UNITS_PER_PUBLIC_CREDIT =
|
|
5886
|
+
LEDGER_UNITS_PER_PUBLIC_CREDIT = 1250;
|
|
5805
5887
|
}
|
|
5806
5888
|
});
|
|
5807
5889
|
|
|
@@ -9294,6 +9376,9 @@ function assertOutcome(outcome) {
|
|
|
9294
9376
|
async function runAgentTurn(input, ports) {
|
|
9295
9377
|
const instruction = input.instruction.trim();
|
|
9296
9378
|
if (!instruction) throw new Error("Agent instruction must not be empty");
|
|
9379
|
+
const turnId = input.turnId;
|
|
9380
|
+
if (!turnId) throw new Error("Agent turn id must not be empty");
|
|
9381
|
+
const emit = (event) => ports.emit({ ...event, turnId });
|
|
9297
9382
|
const now = input.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
9298
9383
|
const loaded = await ports.loadProject();
|
|
9299
9384
|
const before = loaded.state;
|
|
@@ -9311,7 +9396,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9311
9396
|
let terminalOutcome = null;
|
|
9312
9397
|
let invalid = false;
|
|
9313
9398
|
let cancelled = false;
|
|
9314
|
-
await
|
|
9399
|
+
await emit({ type: "turn_started" });
|
|
9315
9400
|
try {
|
|
9316
9401
|
rendered = await ports.renderComposed(working, 0);
|
|
9317
9402
|
} catch {
|
|
@@ -9326,7 +9411,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9326
9411
|
break;
|
|
9327
9412
|
}
|
|
9328
9413
|
const phase = phaseFor(cycle);
|
|
9329
|
-
await
|
|
9414
|
+
await emit({ type: "phase_changed", cycle, phase });
|
|
9330
9415
|
let authorization;
|
|
9331
9416
|
try {
|
|
9332
9417
|
authorization = await ports.authorize({
|
|
@@ -9355,7 +9440,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9355
9440
|
createdAt: now()
|
|
9356
9441
|
};
|
|
9357
9442
|
passes.push(pass);
|
|
9358
|
-
await
|
|
9443
|
+
await emit({ type: "pass_completed", cycle, pass });
|
|
9359
9444
|
if (input.cancelled?.()) {
|
|
9360
9445
|
cancelled = true;
|
|
9361
9446
|
break;
|
|
@@ -9399,7 +9484,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9399
9484
|
};
|
|
9400
9485
|
selected = checkpoint;
|
|
9401
9486
|
if (checkpoint.changed.length > 0 && (!best || checkpoint.quality >= best.quality)) best = checkpoint;
|
|
9402
|
-
await
|
|
9487
|
+
await emit({
|
|
9403
9488
|
type: "draft_applied",
|
|
9404
9489
|
cycle,
|
|
9405
9490
|
checkpointId: checkpoint.id,
|
|
@@ -9431,7 +9516,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9431
9516
|
if (terminalOutcome || invalid) best = null;
|
|
9432
9517
|
selected = best;
|
|
9433
9518
|
const selectedState = selected?.state ?? before;
|
|
9434
|
-
await
|
|
9519
|
+
await emit({ type: "draft_selected", checkpointId: selected?.id ?? 0, state: selectedState });
|
|
9435
9520
|
let outcome = terminalOutcome ?? applicationOutcome({
|
|
9436
9521
|
changed: selected?.changed ?? [],
|
|
9437
9522
|
invalid,
|
|
@@ -9440,7 +9525,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9440
9525
|
let committed = false;
|
|
9441
9526
|
let commitData;
|
|
9442
9527
|
if (outcome.status === "applied" || outcome.status === "applied_with_remaining") {
|
|
9443
|
-
await
|
|
9528
|
+
await emit({ type: "commit_started", outcome });
|
|
9444
9529
|
const commit = await ports.commit({
|
|
9445
9530
|
before,
|
|
9446
9531
|
after: selectedState,
|
|
@@ -9451,13 +9536,13 @@ async function runAgentTurn(input, ports) {
|
|
|
9451
9536
|
if (commit.ok) {
|
|
9452
9537
|
committed = true;
|
|
9453
9538
|
commitData = commit.data;
|
|
9454
|
-
await
|
|
9539
|
+
await emit({ type: "commit_succeeded", data: commit.data, outcome });
|
|
9455
9540
|
} else {
|
|
9456
9541
|
outcome = commit.reason === "stale_version" ? applicationOutcome({ changed: [], conflicted: true }) : failureOutcome("service_unavailable", "The Agent could not save this change. Nothing changed.");
|
|
9457
|
-
await
|
|
9542
|
+
await emit({ type: "draft_rolled_back", outcome });
|
|
9458
9543
|
}
|
|
9459
9544
|
} else {
|
|
9460
|
-
await
|
|
9545
|
+
await emit({ type: "draft_rolled_back", outcome });
|
|
9461
9546
|
}
|
|
9462
9547
|
const changed = committed ? selected?.changed ?? [] : [];
|
|
9463
9548
|
const selectedStale = staleCaptionLocaleCount(selectedState.staleCaptionTranslations);
|
|
@@ -9480,6 +9565,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9480
9565
|
usage: aggregateUsage(passes)
|
|
9481
9566
|
};
|
|
9482
9567
|
const result = {
|
|
9568
|
+
turnId,
|
|
9483
9569
|
outcome,
|
|
9484
9570
|
legacy,
|
|
9485
9571
|
before,
|
|
@@ -9489,7 +9575,7 @@ async function runAgentTurn(input, ports) {
|
|
|
9489
9575
|
...commitData === void 0 ? {} : { commitData }
|
|
9490
9576
|
};
|
|
9491
9577
|
assertOutcome(outcome);
|
|
9492
|
-
await
|
|
9578
|
+
await emit({ type: "turn_completed", result });
|
|
9493
9579
|
void input.requestedCycle;
|
|
9494
9580
|
return result;
|
|
9495
9581
|
}
|
|
@@ -9535,6 +9621,9 @@ var init_agentTurnConformance = __esm({
|
|
|
9535
9621
|
};
|
|
9536
9622
|
AGENT_TURN_CONFORMANCE_FIXTURE = {
|
|
9537
9623
|
input: {
|
|
9624
|
+
// A uuid, because every surface's receipt keys on this value and the receipt table's primary
|
|
9625
|
+
// key is one. The fixture is what a surface adapter replays, so it carries a real-shaped id.
|
|
9626
|
+
turnId: "4f4b2f4a-4a7b-4f31-8e21-2c1c4a9b6d10",
|
|
9538
9627
|
instruction: "Give the phone more depth",
|
|
9539
9628
|
focus: { kind: "device", frameId: "p1", deviceId: "s1", label: "Frame 1 \xB7 home.png" }
|
|
9540
9629
|
},
|
|
@@ -10082,6 +10171,12 @@ function backgroundViewport(value, label) {
|
|
|
10082
10171
|
}
|
|
10083
10172
|
return { panelIndex: Number(row.panelIndex), panelCount: Number(row.panelCount) };
|
|
10084
10173
|
}
|
|
10174
|
+
function textList(value, label, allowEmpty = false) {
|
|
10175
|
+
if (!Array.isArray(value) || !allowEmpty && value.length === 0) {
|
|
10176
|
+
throw new ProductionWorkerRequestError(`${label} must be ${allowEmpty ? "an" : "a non-empty"} array`);
|
|
10177
|
+
}
|
|
10178
|
+
return value.map((entry, index) => text(entry, `${label}[${index}]`));
|
|
10179
|
+
}
|
|
10085
10180
|
function uniqueTexts(value, label, allowEmpty = false) {
|
|
10086
10181
|
if (!Array.isArray(value) || !allowEmpty && value.length === 0) {
|
|
10087
10182
|
throw new ProductionWorkerRequestError(`${label} must be ${allowEmpty ? "an" : "a non-empty"} array`);
|
|
@@ -10102,7 +10197,10 @@ function panelRecipe(value, index, allowEmptyScreenshots) {
|
|
|
10102
10197
|
const viewport = backgroundViewport(row.backgroundViewport, `panels[${index}].backgroundViewport`);
|
|
10103
10198
|
return {
|
|
10104
10199
|
key: text(row.key, `panels[${index}].key`),
|
|
10105
|
-
|
|
10200
|
+
// A panel is a COMPOSITION, not a set of images: positions may repeat the same picture, and
|
|
10201
|
+
// `loadAssets` already dedupes ids into a Map before resolving bytes. Uniqueness here rejected
|
|
10202
|
+
// legal input — `uniqueTexts` belongs on `bundle.locales`, where a repeat means something.
|
|
10203
|
+
screenshotAssetIds: textList(
|
|
10106
10204
|
row.screenshotAssetIds,
|
|
10107
10205
|
`panels[${index}].screenshotAssetIds`,
|
|
10108
10206
|
allowEmptyScreenshots
|
|
@@ -10312,7 +10410,7 @@ async function startDurableProduction(deps, input) {
|
|
|
10312
10410
|
return errorResult("That idempotency key already belongs to a different production request.", "idempotency_conflict");
|
|
10313
10411
|
}
|
|
10314
10412
|
if (error instanceof ProductionReservationInsufficientCreditsError) {
|
|
10315
|
-
return errorResult("There are not enough credits to reserve this production render.", "quota_exhausted", {
|
|
10413
|
+
return errorResult("There are not enough cloud credits to reserve this production render.", "quota_exhausted", {
|
|
10316
10414
|
required: publicCreditsForLedgerUnits(error.required),
|
|
10317
10415
|
available: publicCreditsForLedgerUnits(error.availability.available)
|
|
10318
10416
|
});
|
|
@@ -10375,7 +10473,7 @@ var init_package = __esm({
|
|
|
10375
10473
|
"package.json"() {
|
|
10376
10474
|
package_default = {
|
|
10377
10475
|
name: "shotops-mcp",
|
|
10378
|
-
version: "0.9.
|
|
10476
|
+
version: "0.9.2",
|
|
10379
10477
|
private: false,
|
|
10380
10478
|
type: "module",
|
|
10381
10479
|
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.",
|
|
@@ -10867,7 +10965,7 @@ async function renderStrip(deps, args, scope, signal) {
|
|
|
10867
10965
|
locale: locale2,
|
|
10868
10966
|
...screenshotLocales.length > 0 ? { screenshotLocales } : {},
|
|
10869
10967
|
...note ? { note } : {},
|
|
10870
|
-
...args.project ? {} : { nextStep:
|
|
10968
|
+
...args.project ? {} : { nextStep: OFFER_SAVE_PROJECT_NEXT_STEP },
|
|
10871
10969
|
resolvedInput: resolvedInputContract(snapshot),
|
|
10872
10970
|
readiness: readinessContract(preReady.report)
|
|
10873
10971
|
}
|
|
@@ -10943,15 +11041,16 @@ async function renderStrip(deps, args, scope, signal) {
|
|
|
10943
11041
|
// #563 — the composition's own verdict, ADDITIVE: absent on a clean render, so a response with
|
|
10944
11042
|
// nothing to say keeps exactly the key set it had before.
|
|
10945
11043
|
...result.layout ? { layout: result.layout } : {},
|
|
10946
|
-
//
|
|
10947
|
-
//
|
|
11044
|
+
// The OFFER, in the output, because a client may never have received the prose contract
|
|
11045
|
+
// (toolContract.ts owns the wording). It states that no project exists and names the door; it
|
|
11046
|
+
// does not tell an agent to walk through it, and `consent: "required"` says so on the wire.
|
|
10948
11047
|
//
|
|
10949
11048
|
// Only when the render was NOT tied to a project. On render_strip, `project` names a LOOK
|
|
10950
11049
|
// SOURCE, not a save target — so a caller who already has a project in play must not be nudged
|
|
10951
11050
|
// into minting a second one, and telling them to UPDATE it would be wrong too (they asked to
|
|
10952
11051
|
// render with its styling, not to file work under it). That case stays with the prose, which
|
|
10953
11052
|
// handles it correctly: "Never infer an update target from the most-recent project."
|
|
10954
|
-
...args.project ? {} : { nextStep:
|
|
11053
|
+
...args.project ? {} : { nextStep: OFFER_SAVE_PROJECT_NEXT_STEP },
|
|
10955
11054
|
// #662 — the versioned result envelope. Additive: every field above is unchanged, and an old
|
|
10956
11055
|
// client that has never heard of `contract` sees exactly the payload it saw yesterday.
|
|
10957
11056
|
contract: successContract(deps, {
|
|
@@ -13047,12 +13146,14 @@ async function handleImportScreenshot(deps, args) {
|
|
|
13047
13146
|
return errorResult(results.map((r) => r.error).filter(Boolean).join("\n\n"));
|
|
13048
13147
|
}
|
|
13049
13148
|
const locale2 = args.locale?.trim() || void 0;
|
|
13149
|
+
const stored = imported.some((result) => typeof result.ref === "string" && result.ref.length > 0);
|
|
13050
13150
|
return textResult({
|
|
13051
13151
|
ok: imported.length === results.length,
|
|
13052
13152
|
count: imported.length,
|
|
13053
13153
|
screenshots: results,
|
|
13054
13154
|
...locale2 ? { locale: locale2 } : {},
|
|
13055
|
-
|
|
13155
|
+
...stored ? { disclosure: { ...ACCOUNT_UPLOAD_DISCLOSURE, statement: disclosureStatement(ACCOUNT_UPLOAD_DISCLOSURE) } } : {},
|
|
13156
|
+
instructions: instructionsFor(results) + (stored ? ` ${disclosureStatement(ACCOUNT_UPLOAD_DISCLOSURE)}` : "")
|
|
13056
13157
|
});
|
|
13057
13158
|
}
|
|
13058
13159
|
var init_toolsImport = __esm({
|
|
@@ -13084,7 +13185,11 @@ async function handleRequestScreenshotUpload(deps, args) {
|
|
|
13084
13185
|
slots: slots.map(({ assetId: _assetId, ...slot2 }) => ({ ...slot2, variant })),
|
|
13085
13186
|
...locale2 ? { locale: locale2 } : {},
|
|
13086
13187
|
...family ? { family } : {},
|
|
13087
|
-
|
|
13188
|
+
// Minting a slot is the moment to say where the bytes are about to go, not after they are
|
|
13189
|
+
// there. The window is read from the retention policy — typing "7 days" here is how two
|
|
13190
|
+
// surfaces end up promising different weeks.
|
|
13191
|
+
disclosure: { ...ACCOUNT_UPLOAD_DISCLOSURE, statement: disclosureStatement(ACCOUNT_UPLOAD_DISCLOSURE) },
|
|
13192
|
+
instructions: `PUT each screenshot's raw PNG bytes to its uploadUrl, e.g.: curl -T screenshot.png "$UPLOAD_URL" \u2014 then pass { "ref": "THE_MATCHING_REF" } as that screenshot's entry in render_strip/emit_bundle's \`screenshots\` array. If you passed \`names\`, that ref already carries its filename \u2014 no need to also set \`name\` on the screenshot entry. ${disclosureStatement(ACCOUNT_UPLOAD_DISCLOSURE)}` + (locale2 ? ` For render_strip/emit_bundle, place each ref under the matching per-locale key, e.g. { "locales": { "${locale2}": { "ref": "THE_REF" } } }.` : "") + (locale2 || family ? " For render_project, pass each slot's `variant` through with its ref so that exact device-family + locale cell overrides the project instead of its base screenshot." : "")
|
|
13088
13193
|
});
|
|
13089
13194
|
}
|
|
13090
13195
|
async function handleDeleteAssets(deps, args) {
|
|
@@ -13118,7 +13223,7 @@ async function handleDeleteAssets(deps, args) {
|
|
|
13118
13223
|
deletedCount: deletedAssetIds.length,
|
|
13119
13224
|
deletedAssetIds,
|
|
13120
13225
|
notFoundAssetIds: results.filter((result) => result.outcome === "not_found").map((result) => result.requestedAssetId),
|
|
13121
|
-
message: "Deleted every unreferenced owned asset. Missing or already-deleted assets are idempotent; live dependencies are never removed."
|
|
13226
|
+
message: "Deleted every unreferenced owned asset. Missing or already-deleted assets are idempotent; live dependencies are never removed. An asset a Project, operation, share or preview still points at is refused rather than removed, and is never worth proposing for cleanup."
|
|
13122
13227
|
});
|
|
13123
13228
|
} catch (err) {
|
|
13124
13229
|
return errorResult(err.message);
|
|
@@ -13128,6 +13233,7 @@ var init_toolsUploads = __esm({
|
|
|
13128
13233
|
"src/toolsUploads.ts"() {
|
|
13129
13234
|
"use strict";
|
|
13130
13235
|
init_tools();
|
|
13236
|
+
init_toolContract();
|
|
13131
13237
|
init_screenVariants();
|
|
13132
13238
|
}
|
|
13133
13239
|
});
|
|
@@ -13343,7 +13449,16 @@ async function handleSaveProject(deps, args) {
|
|
|
13343
13449
|
locale: locale2,
|
|
13344
13450
|
...outputDevices.length > 0 ? { outputs: outputDevices.map((d) => d.id) } : {},
|
|
13345
13451
|
...note ? { note } : {},
|
|
13346
|
-
|
|
13452
|
+
// The one local path that moves bytes off this machine. Stated with the result, from the
|
|
13453
|
+
// retention policy's own number, rather than left for the user to discover when the claim
|
|
13454
|
+
// window closes over their staged PNGs.
|
|
13455
|
+
disclosure: { ...PENDING_CLAIM_DISCLOSURE, statement: disclosureStatement(PENDING_CLAIM_DISCLOSURE) },
|
|
13456
|
+
contract: successContract(deps, {
|
|
13457
|
+
tool: "save_project",
|
|
13458
|
+
kind: "mutate",
|
|
13459
|
+
effects: { projectMutation: true, upload: true, retainedStorage: true }
|
|
13460
|
+
}),
|
|
13461
|
+
message: `Project staged as a private pending claim \u2014 open ${p.openUrl} and sign in within ${PENDING_CLAIM_DISCLOSURE.retentionDays} days to become its owner. Always give this openUrl to the user; no projectId exists until the claim is completed. ` + disclosureStatement(PENDING_CLAIM_DISCLOSURE)
|
|
13347
13462
|
});
|
|
13348
13463
|
}
|
|
13349
13464
|
return textResult({
|
|
@@ -13357,6 +13472,16 @@ async function handleSaveProject(deps, args) {
|
|
|
13357
13472
|
// byte-identical to before.
|
|
13358
13473
|
...outputDevices.length > 0 ? { outputs: outputDevices.map((d) => d.id) } : {},
|
|
13359
13474
|
...note ? { note } : {},
|
|
13475
|
+
// #662 — this is the ONE tool in the contract whose job is a project write, and the envelope
|
|
13476
|
+
// says so: `projectMutation: true` here, false on every render. That pair is what makes
|
|
13477
|
+
// "a render never creates a project on its own" checkable rather than merely promised.
|
|
13478
|
+
// Nothing is uploaded on this path: the record is structure + look, and the refs it points at
|
|
13479
|
+
// were already in storage before this call.
|
|
13480
|
+
contract: successContract(deps, {
|
|
13481
|
+
tool: "save_project",
|
|
13482
|
+
kind: "mutate",
|
|
13483
|
+
effects: { projectMutation: true, retainedStorage: true }
|
|
13484
|
+
}),
|
|
13360
13485
|
message: `Saved as an editable ShotOps project \u2014 open it at ${p.openUrl} (sign in as the same account). Thread this projectId back on later renders to update it in place.`
|
|
13361
13486
|
});
|
|
13362
13487
|
}
|
|
@@ -13497,17 +13622,15 @@ var init_toolsProject = __esm({
|
|
|
13497
13622
|
});
|
|
13498
13623
|
|
|
13499
13624
|
// src/refineProjectReceipt.ts
|
|
13500
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
13501
|
-
function mintTurnId() {
|
|
13502
|
-
return `turn_${randomUUID3()}`;
|
|
13503
|
-
}
|
|
13504
13625
|
function failure(error, message) {
|
|
13505
13626
|
return { ok: false, error, message };
|
|
13506
13627
|
}
|
|
13507
|
-
function refineProjectReceipt(
|
|
13628
|
+
function refineProjectReceipt(turn) {
|
|
13508
13629
|
return {
|
|
13509
13630
|
ok: true,
|
|
13510
|
-
|
|
13631
|
+
// The id the ADAPTER minted for this turn (#580), never one a caller supplied and never one a
|
|
13632
|
+
// door invented alongside it: the receipt row and this line carry the same identity.
|
|
13633
|
+
turnId: turn.turnKey,
|
|
13511
13634
|
projectId: turn.projectId,
|
|
13512
13635
|
outcome: turn.outcome,
|
|
13513
13636
|
changed: [...turn.outcome.changed],
|
|
@@ -13519,11 +13642,11 @@ function refineProjectReceipt(turnId, turn) {
|
|
|
13519
13642
|
allowance: turn.allowance
|
|
13520
13643
|
};
|
|
13521
13644
|
}
|
|
13522
|
-
function refineProjectResult(
|
|
13645
|
+
function refineProjectResult(turn) {
|
|
13523
13646
|
if ("error" in turn) {
|
|
13524
13647
|
return failure(turn.error, turn.message);
|
|
13525
13648
|
}
|
|
13526
|
-
return refineProjectReceipt(
|
|
13649
|
+
return refineProjectReceipt(turn);
|
|
13527
13650
|
}
|
|
13528
13651
|
function refineProjectUnavailable(message) {
|
|
13529
13652
|
return failure("service_unavailable", message);
|
|
@@ -13693,7 +13816,7 @@ function previewUsage(deps, snapshot) {
|
|
|
13693
13816
|
}
|
|
13694
13817
|
function statusMessage(deps, snapshot, step) {
|
|
13695
13818
|
if (step === "none") {
|
|
13696
|
-
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 credits." : "");
|
|
13819
|
+
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." : "");
|
|
13697
13820
|
}
|
|
13698
13821
|
if (step === "sign_in" && !snapshot.authenticated && deps.anonymous) {
|
|
13699
13822
|
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);
|
|
@@ -13904,7 +14027,7 @@ async function handleReadLook(deps, args) {
|
|
|
13904
14027
|
return textResult({
|
|
13905
14028
|
saved: false,
|
|
13906
14029
|
project: { id: project2.id, name: project2.name },
|
|
13907
|
-
message: `No saved look on project "${project2.name}". Style a strip in ShotOps and use the
|
|
14030
|
+
message: `No saved look on project "${project2.name}". Style a strip in ShotOps and use the Save version button on the stage, or compose one here with describe_look + save_look.`
|
|
13908
14031
|
});
|
|
13909
14032
|
}
|
|
13910
14033
|
const layout = deriveLayoutReport(saved.look);
|
|
@@ -14610,7 +14733,7 @@ function registerTools(server, deps) {
|
|
|
14610
14733
|
"render_strip",
|
|
14611
14734
|
{
|
|
14612
14735
|
title: "Render App Store screenshots",
|
|
14613
|
-
description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the three layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` FROM THE LONGEST HEADLINE IN THE SET, once, before anything else: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The text region is RESERVED, NOT FITTED \u2014 held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw phoneHeight/vOffset numbers: pick a layout, then override one field if you must (the template expands first, your field wins). Past the reservation the caption band grows down INTO the phone \u2014 the failure to avoid. THE RESPONSE ANSWERS BACK: an optional `layout` block names every caption sitting on a device, every caption collision and every device pair overlapping without reading as composed, by frame number and depth. When the repair is deterministic it also carries `layout.correctedLook` \u2014 pass that straight back as `look` to re-render clean. No `layout` key means the composition measured clean. Read it before you show the panels. This tool ONLY renders images: it never creates or updates a project
|
|
14736
|
+
description: 'Make the store screenshots: raw app screen captures in, finished marketing panels out. Each screenshot is composited into a styled 3D device mockup, laid out as an App Store screenshot strip with the headlines you pass, and returned as per-panel PNGs at the App Store panel size `panelPresetId` names. This is the tool for a strip built from screenshots you have in hand; to reproduce one a designer already saved, use render_project. In ChatGPT, call import_screenshot first and pass the returned refs in the preferred flat form: { "screenshots": [{ "ref": "\u2026" }] } \u2014 each flat entry is one panel, and nested slots only when ONE panel should hold several phones. Art-direct it with `style`/`look` or a project\'s saved look, iterate with a low-res `preview` at a quarter the render cost, and take results `inline` or as `urls` for large payloads. Screenshots can vary per store locale via { "locales": \u2026 } entries \u2014 `locale` picks which variant renders (missing variants fall back to en-US). For real, full-resolution screenshots call request_screenshot_upload first \u2014 inline base64 is for small payloads only. CALL describe_look FIRST when nobody has said how the strip should look \u2014 it returns the five palette presets, the three layout templates and every styling field with its default, and guessing a colour scheme out of 30 raw fields is how strips end up looking improvised. PICK `style.layout` FROM THE LONGEST HEADLINE IN THE SET, once, before anything else: "standard" reserves 2 lines (~36 characters) with the whole device under them, "bleed" reserves 4 (~72) with the device running off the bottom edge, "top-bleed" inverts that. The text region is RESERVED, NOT FITTED \u2014 held at full size whether or not the headline fills it, because a fixed reservation is the only thing that makes every device in a swiped SET land on the same line. Do not close a gap by shrinking vOffset on the short panels, and never assemble a composition out of raw phoneHeight/vOffset numbers: pick a layout, then override one field if you must (the template expands first, your field wins). Past the reservation the caption band grows down INTO the phone \u2014 the failure to avoid. THE RESPONSE ANSWERS BACK: an optional `layout` block names every caption sitting on a device, every caption collision and every device pair overlapping without reading as composed, by frame number and depth. When the repair is deterministic it also carries `layout.correctedLook` \u2014 pass that straight back as `look` to re-render clean. No `layout` key means the composition measured clean. Read it before you show the panels. This tool ONLY renders images: it never creates or updates a project, and the result says so \u2014 `contract.effects.projectMutation` is false on every render. A render with no `project` also carries a `nextStep` with action `offer_save_project`, which states that no project exists and names save_project as the door. That is an OFFER to put to the user, never consent already given: save only after they show they want this work preserved, shared, reused or kept editable, and if you cannot ask them, return the panels with the offer standing. No store credential is involved.',
|
|
14614
14737
|
inputSchema: renderStripAdvertisedShape,
|
|
14615
14738
|
outputSchema: renderStripToolOutputSchema,
|
|
14616
14739
|
// ADDITIVE, not destructive: this can mint private generated assets but cannot change a
|
|
@@ -14631,7 +14754,7 @@ function registerTools(server, deps) {
|
|
|
14631
14754
|
"emit_bundle",
|
|
14632
14755
|
{
|
|
14633
14756
|
title: "Export fastlane bundle",
|
|
14634
|
-
description: 'Deliver the finished store screenshots as one file the user can actually upload: a `fastlane deliver`-ready zip holding the screenshots, a pre-filled Deliverfile that is screenshots-only and never submits, and a README. Three ways in, same zip out \u2014 render the strip here from `screenshots`; re-zip pre-rendered `panels` refs from a prior render_strip `output: "urls"` call (no re-render, near-instant); or name a saved `project` alone and it packages the screenshots that project already holds, with nothing attached to this conversation. `locales: [...]` emits ONE localized bundle with a fastlane/screenshots/<locale>/ folder per language, so every language uploads in a single fastlane run \u2014 pair it with per-locale { "locales": \u2026 } screenshot/panel entries, or with a `project`, which supplies each locale\'s own caption words. Returns the zip `inline` or as a `url` for large payloads, and can mint a 14-day share link to a landing page. YOU upload
|
|
14757
|
+
description: 'Deliver the finished store screenshots as one file the user can actually upload: a `fastlane deliver`-ready zip holding the screenshots, a pre-filled Deliverfile that is screenshots-only and never submits, and a README. Three ways in, same zip out \u2014 render the strip here from `screenshots`; re-zip pre-rendered `panels` refs from a prior render_strip `output: "urls"` call (no re-render, near-instant); or name a saved `project` alone and it packages the screenshots that project already holds, with nothing attached to this conversation. `locales: [...]` emits ONE localized bundle with a fastlane/screenshots/<locale>/ folder per language, so every language uploads in a single fastlane run \u2014 pair it with per-locale { "locales": \u2026 } screenshot/panel entries, or with a `project`, which supplies each locale\'s own caption words. Returns the zip `inline` or as a `url` for large payloads, and can mint a 14-day share link to a landing page. Bundling is not saving: `createProject` is a compatibility input kept for older callers, so set it only when the user explicitly asked for BOTH a bundle and a saved project \u2014 never infer it from a delivery request, and use save_project for an ordinary save. YOU upload the zip with your own fastlane: this server never touches an Apple or store credential, never creates an App Store version, and never submits for review.',
|
|
14635
14758
|
inputSchema: emitBundleAdvertisedShape,
|
|
14636
14759
|
outputSchema: emitBundleToolOutputSchema,
|
|
14637
14760
|
annotations: OPEN_WORLD_DESTRUCTIVE_WRITE,
|
|
@@ -14652,9 +14775,9 @@ function registerTools(server, deps) {
|
|
|
14652
14775
|
"save_project",
|
|
14653
14776
|
{
|
|
14654
14777
|
title: "Save as editable project",
|
|
14655
|
-
description: "Let the user keep the strip: save it as an editable ShotOps project they can open in the web app, refine by hand, and have re-rendered later by render_project. Returns an openUrl to hand back and, when already authenticated, a projectId. Unsigned local stdio instead returns an explicit
|
|
14778
|
+
description: "Let the user keep the strip: save it as an editable ShotOps project they can open in the web app, refine by hand, and have re-rendered later by render_project. Returns an openUrl to hand back and, when already authenticated, a projectId. Unsigned local stdio instead returns an explicit pending claim; the user owns it after opening the URL and signing in. No panel PNGs are returned. Fast, and it does NOT render: it builds a byte-free structure + look record, so no panel bytes are returned and it never hits the render timeout. `outputs` names the device sizes the project targets; an existing `project` id updates that project in place, omitting it creates a new one. CALL IT WHEN THE USER ASKS FOR PERSISTENCE, not as the closing step of every job: this is the door for someone who said they want to keep, share, reuse or keep editing the work. A request for images, full-resolution panels or a bundle is not that, and neither is silence \u2014 a render already told them nothing was saved and offered this call. Pass `project` only when they named an existing project as the target; never infer one from the most recently edited. An UNSIGNED local save is the one local path that moves bytes: it stages the raw PNGs in private claim storage so the claimed project reopens whole, and its result carries a `disclosure` block naming what moved, why, how long it is kept and what retains it. Ordinary local render/export never uploads.",
|
|
14656
14779
|
inputSchema: saveProjectAdvertisedShape,
|
|
14657
|
-
outputSchema:
|
|
14780
|
+
outputSchema: saveProjectToolOutputSchema,
|
|
14658
14781
|
annotations: DESTRUCTIVE_WRITE,
|
|
14659
14782
|
_meta: toolMeta(deps, "save_project", "Saving ShotOps project\u2026", "ShotOps project saved")
|
|
14660
14783
|
},
|
|
@@ -14666,7 +14789,7 @@ function registerTools(server, deps) {
|
|
|
14666
14789
|
title: "Read saved look",
|
|
14667
14790
|
description: "Match a design the user already approved instead of inventing a new one: returns the saved ShotOps look on a project \u2014 the device, background and caption styling a human tuned in the web app \u2014 so the next render comes out looking like theirs. Styling only: no screenshots, no caption words, no credentials. Comes with the latest version, the held version (if any) and the full `versions` history. Feed the returned look back into render_strip/emit_bundle as `look`, or just pass `useSavedLook: true`; render an older entry with `version`, or make one the default with hold_look.",
|
|
14668
14791
|
inputSchema: readLookShape,
|
|
14669
|
-
outputSchema:
|
|
14792
|
+
outputSchema: readLookToolOutputSchema,
|
|
14670
14793
|
annotations: READ_ONLY,
|
|
14671
14794
|
_meta: toolMeta(deps, "read_look", "Reading saved look\u2026", "Saved look read")
|
|
14672
14795
|
},
|
|
@@ -14678,7 +14801,7 @@ function registerTools(server, deps) {
|
|
|
14678
14801
|
title: "Read full project",
|
|
14679
14802
|
description: 'Find out what the designer changed in a strip: returns a project\'s FULL current state \u2014 the frame order they settled on (panels), the caption words they typed for each locale (captionText), the locale list, and the styling \u2014 as an opaque ProjectFile, not just the look. To answer a recurring "any updates?", remember the returned `updatedAt` and re-read later; a newer value means the strip was edited since. It ALSO answers whether the project already holds its own screenshots: `screenshots.available` true means render_project can render it with NO `screenshots` argument and nothing attached \u2014 check that before concluding you need the files. Read-only, never returns screenshot bytes, no store credential.',
|
|
14680
14803
|
inputSchema: readProjectShape,
|
|
14681
|
-
outputSchema:
|
|
14804
|
+
outputSchema: readProjectToolOutputSchema,
|
|
14682
14805
|
annotations: READ_ONLY,
|
|
14683
14806
|
_meta: toolMeta(deps, "read_project", "Reading ShotOps project\u2026", "ShotOps project read")
|
|
14684
14807
|
},
|
|
@@ -14713,7 +14836,7 @@ function registerTools(server, deps) {
|
|
|
14713
14836
|
title: "Refine a saved project",
|
|
14714
14837
|
description: "Edit an existing ShotOps project conversationally through one bounded Agent turn. Pass the project id or exact name, the requested change, and optionally the selected frame, device, or caption as focus. A supported request commits exactly once and returns the actual committed diff. Ambiguity, missing input, unsupported intent, a concurrent edit, or a failed pass returns its own typed outcome with no partial project saved. This changes the saved project but returns no panel pixels; call render_project afterwards to see or export the result.",
|
|
14715
14838
|
inputSchema: refineProjectShape,
|
|
14716
|
-
outputSchema:
|
|
14839
|
+
outputSchema: refineProjectToolOutputSchema,
|
|
14717
14840
|
annotations: DESTRUCTIVE_WRITE,
|
|
14718
14841
|
_meta: toolMeta(deps, "refine_project", "Refining ShotOps project\u2026", "ShotOps project refined")
|
|
14719
14842
|
},
|
|
@@ -14725,7 +14848,7 @@ function registerTools(server, deps) {
|
|
|
14725
14848
|
title: "Save look",
|
|
14726
14849
|
description: "Make a design the user liked reusable: persists a composed ShotOps look (styling only) on a project as a new version, so their next strip \u2014 rendered here or opened in the web app \u2014 starts from the design they just approved instead of a default. Offer it once they are happy with how a strip looks. Use describe_look for the field catalog; caption words are never stored, so pass those per render. No store credential is involved.",
|
|
14727
14850
|
inputSchema: saveLookAdvertisedShape,
|
|
14728
|
-
outputSchema:
|
|
14851
|
+
outputSchema: saveLookToolOutputSchema,
|
|
14729
14852
|
annotations: ADDITIVE_WRITE,
|
|
14730
14853
|
_meta: toolMeta(deps, "save_look", "Saving ShotOps look\u2026", "ShotOps look saved")
|
|
14731
14854
|
},
|
|
@@ -14737,7 +14860,7 @@ function registerTools(server, deps) {
|
|
|
14737
14860
|
title: "Hold a look version",
|
|
14738
14861
|
description: "Freeze which design gets rendered: pin one saved look version as this project's DEFAULT, so a designer can keep experimenting and saving newer versions without changing what agents deliver. The version must already exist (see read_look `versions`). render_strip/emit_bundle with `useSavedLook: true` then render the held one. release_look goes back to Follow latest. No credential.",
|
|
14739
14862
|
inputSchema: holdLookShape,
|
|
14740
|
-
outputSchema:
|
|
14863
|
+
outputSchema: holdLookToolOutputSchema,
|
|
14741
14864
|
annotations: IDEMPOTENT_WRITE,
|
|
14742
14865
|
_meta: toolMeta(deps, "hold_look", "Holding look version\u2026", "Look version held")
|
|
14743
14866
|
},
|
|
@@ -14749,7 +14872,7 @@ function registerTools(server, deps) {
|
|
|
14749
14872
|
title: "Release the held look",
|
|
14750
14873
|
description: "Go back to rendering the newest design: clears a project\u2019s held version (undo hold_look), so agents Follow latest and pick up whatever the designer saved most recently. No-op if nothing was held. No store credential.",
|
|
14751
14874
|
inputSchema: releaseLookShape,
|
|
14752
|
-
outputSchema:
|
|
14875
|
+
outputSchema: releaseLookToolOutputSchema,
|
|
14753
14876
|
annotations: IDEMPOTENT_WRITE,
|
|
14754
14877
|
_meta: toolMeta(deps, "release_look", "Releasing look version\u2026", "Look version released")
|
|
14755
14878
|
},
|
|
@@ -14771,9 +14894,9 @@ function registerTools(server, deps) {
|
|
|
14771
14894
|
"request_screenshot_upload",
|
|
14772
14895
|
{
|
|
14773
14896
|
title: "Upload full-size screenshots",
|
|
14774
|
-
description: "Get screenshots that live only on the USER's own machine into a render, at full resolution and without their bytes ever transiting this conversation. It mints signed upload URLs you (or the user's shell) PUT each raw PNG to, and hands back a `ref` per slot to pass into render_strip/emit_bundle. Use it in non-ChatGPT MCP clients and automation, and instead of inline base64 for any real full-resolution screenshot. In ChatGPT, attachments go through import_screenshot instead \u2014 and a user with a ShotOps account can skip file transfer entirely by dropping the screenshots into a project and calling render_project. Uploading one batch per store locale? Tag each batch with `locale` (echoed back for your bookkeeping).",
|
|
14897
|
+
description: "Get screenshots that live only on the USER's own machine into a render, at full resolution and without their bytes ever transiting this conversation. It mints signed upload URLs you (or the user's shell) PUT each raw PNG to, and hands back a `ref` per slot to pass into render_strip/emit_bundle. Use it in non-ChatGPT MCP clients and automation, and instead of inline base64 for any real full-resolution screenshot. In ChatGPT, attachments go through import_screenshot instead \u2014 and a user with a ShotOps account can skip file transfer entirely by dropping the screenshots into a project and calling render_project. Uploading one batch per store locale? Tag each batch with `locale` (echoed back for your bookkeeping). The result carries a `disclosure` block \u2014 what moves into ShotOps storage, why, how long it is kept and the explicit action that retains it. Say that before the bytes move, and relay its numbers rather than your own.",
|
|
14775
14898
|
inputSchema: requestScreenshotUploadShape,
|
|
14776
|
-
outputSchema:
|
|
14899
|
+
outputSchema: requestScreenshotUploadToolOutputSchema,
|
|
14777
14900
|
annotations: ADDITIVE_WRITE,
|
|
14778
14901
|
_meta: toolMeta(deps, "request_screenshot_upload", "Preparing screenshot uploads\u2026", "Screenshot uploads ready")
|
|
14779
14902
|
},
|
|
@@ -14787,9 +14910,9 @@ function registerTools(server, deps) {
|
|
|
14787
14910
|
"import_screenshot",
|
|
14788
14911
|
{
|
|
14789
14912
|
title: "Import screenshots",
|
|
14790
|
-
description: 'How screenshots that exist only in THIS conversation get into ShotOps so they can be rendered: one call takes several \u2014 an attached PNG or JPEG, or a PNG/JPEG URL \u2014 and hands each back as a private, account-scoped `ref` the render tools accept, with the image bytes moving server-to-server so they never enter model context. Every entry of `screenshots` names EXACTLY ONE source: { "url": "https://\u2026" } (this server fetches PNG or JPEG and converts JPEG to PNG \u2014 works on any client), { "file": \u2026 } (a ChatGPT attachment descriptor; in ChatGPT it arrives as the top-level `file` parameter instead), or (local stdio server only) { "path": "/abs/shot.png" }. Results come back one per entry in input order, and a failed entry reports its own error without cancelling the rest \u2014 re-import just that one. Then call render_strip with the preferred flat input { "screenshots": [{ "ref": "THE_REF" }] }; the same refs work in emit_bundle, and with their filenames in render_project. If the screenshots are only on the user\'s machine and this is the hosted server, there is no URL to give \u2014 use request_screenshot_upload instead.',
|
|
14913
|
+
description: 'How screenshots that exist only in THIS conversation get into ShotOps so they can be rendered: one call takes several \u2014 an attached PNG or JPEG, or a PNG/JPEG URL \u2014 and hands each back as a private, account-scoped `ref` the render tools accept, with the image bytes moving server-to-server so they never enter model context. Every entry of `screenshots` names EXACTLY ONE source: { "url": "https://\u2026" } (this server fetches PNG or JPEG and converts JPEG to PNG \u2014 works on any client), { "file": \u2026 } (a ChatGPT attachment descriptor; in ChatGPT it arrives as the top-level `file` parameter instead), or (local stdio server only) { "path": "/abs/shot.png" }. Results come back one per entry in input order, and a failed entry reports its own error without cancelling the rest \u2014 re-import just that one. Then call render_strip with the preferred flat input { "screenshots": [{ "ref": "THE_REF" }] }; the same refs work in emit_bundle, and with their filenames in render_project. If the screenshots are only on the user\'s machine and this is the hosted server, there is no URL to give \u2014 use request_screenshot_upload instead. An import that produced refs puts the bytes in ShotOps storage and says so in a `disclosure` block: what moved, why, how long it is kept and the explicit action that retains it. A local { "path" } import moves nothing and discloses nothing.',
|
|
14791
14914
|
inputSchema: importScreenshotAdvertisedShape,
|
|
14792
|
-
outputSchema:
|
|
14915
|
+
outputSchema: importScreenshotToolOutputSchema,
|
|
14793
14916
|
annotations: ADDITIVE_WRITE,
|
|
14794
14917
|
_meta: toolMeta(deps, "import_screenshot", "Importing attached screenshot\u2026", "Attached screenshot imported", {
|
|
14795
14918
|
"openai/fileParams": ["file"]
|
|
@@ -14801,9 +14924,9 @@ function registerTools(server, deps) {
|
|
|
14801
14924
|
"delete_assets",
|
|
14802
14925
|
{
|
|
14803
14926
|
title: "Delete uploaded assets",
|
|
14804
|
-
description: "Clean up after delivery: permanently removes the private screenshots, rendered panels and generated bundles ShotOps is holding for this account. Offer it once the user has their files, and call it only when they explicitly ask \u2014 this is irreversible. It cannot reach another account\u2019s files
|
|
14927
|
+
description: "Clean up after delivery: permanently removes the private screenshots, rendered panels and generated bundles ShotOps is holding for this account. Offer it once the user has their files, and call it only when they explicitly ask \u2014 this is irreversible. Offer only UNREFERENCED temporary assets: anything a live Project, production operation, share or preview still depends on is refused, so proposing one is proposing a failure. It cannot reach another account\u2019s files. Cleanup is never a step that has to happen \u2014 an asset left alone simply expires.",
|
|
14805
14928
|
inputSchema: deleteAssetsShape,
|
|
14806
|
-
outputSchema:
|
|
14929
|
+
outputSchema: deleteAssetsToolOutputSchema,
|
|
14807
14930
|
annotations: IDEMPOTENT_DESTRUCTIVE_WRITE,
|
|
14808
14931
|
_meta: toolMeta(deps, "delete_assets", "Deleting ShotOps assets\u2026", "ShotOps assets deleted")
|
|
14809
14932
|
},
|
|
@@ -14813,7 +14936,7 @@ function registerTools(server, deps) {
|
|
|
14813
14936
|
"account_status",
|
|
14814
14937
|
{
|
|
14815
14938
|
title: "Check what this connection can do",
|
|
14816
|
-
description: 'Ask whether this connection may produce store-ready screenshots \u2014 full-resolution panels and the fastlane bundle \u2014 before starting a render that might be refused. It reports whether an account is signed in, its plan, trial and remaining ShotOps credits, what a PREVIEW costs here, and \u2014 the field to branch on \u2014 whether production output is available: full-resolution render_strip and render_project, and every emit_bundle form. `production.available: false` comes with a machine-readable `nextStep` (sign_in, choose_plan, upgrade or retry) and a sentence to relay to the user; do not retry the production call until that step is done. CALL THIS FIRST when the user asks for a fastlane bundle, final panels, or "the real files" and you do not already know this connection can produce them \u2014 a refusal after a long render wastes their time, and this call is free: it spends no credits, uses up no free preview, uploads nothing, and writes nothing. Previewing never depends on it, so keep rendering previews regardless.',
|
|
14939
|
+
description: 'Ask whether this connection may produce store-ready screenshots \u2014 full-resolution panels and the fastlane bundle \u2014 before starting a render that might be refused. It reports whether an account is signed in, its plan, trial and remaining ShotOps cloud credits, what a PREVIEW costs here, and \u2014 the field to branch on \u2014 whether production output is available: full-resolution render_strip and render_project, and every emit_bundle form. `production.available: false` comes with a machine-readable `nextStep` (sign_in, choose_plan, upgrade or retry) and a sentence to relay to the user; do not retry the production call until that step is done. CALL THIS FIRST when the user asks for a fastlane bundle, final panels, or "the real files" and you do not already know this connection can produce them \u2014 a refusal after a long render wastes their time, and this call is free: it spends no cloud credits, uses up no free preview, uploads nothing, and writes nothing. Previewing never depends on it, so keep rendering previews regardless.',
|
|
14817
14940
|
inputSchema: accountStatusShape,
|
|
14818
14941
|
outputSchema: accountStatusOutputSchema,
|
|
14819
14942
|
annotations: READ_ONLY,
|
|
@@ -15713,7 +15836,8 @@ function connectHostedBridge(token) {
|
|
|
15713
15836
|
generations: [...settlement.generationIds],
|
|
15714
15837
|
outcome: settlement.outcome,
|
|
15715
15838
|
committed: settlement.committed,
|
|
15716
|
-
capped: settlement.capped
|
|
15839
|
+
capped: settlement.capped,
|
|
15840
|
+
receipt: settlement.receipt
|
|
15717
15841
|
});
|
|
15718
15842
|
return { balanceCredits: res.balanceCredits, refundedCredits: res.refundedCredits };
|
|
15719
15843
|
},
|
|
@@ -15942,7 +16066,7 @@ var init_visionGeneration = __esm({
|
|
|
15942
16066
|
|
|
15943
16067
|
// ../api/_lib/refineProjectContracts.ts
|
|
15944
16068
|
import { z as z9 } from "zod";
|
|
15945
|
-
var localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
|
|
16069
|
+
var localeIds, MAX_DURABLE_MODEL_PROSE, hexSchema, currentColorSchema, shotPatchSchema, aiNumberSchema, captionStylePatchSchema, MAX_WIRE_GRADIENT_STOPS, gradientStopSchema, backgroundPatchSchema, stripBackgroundPatchSchema, framePatchSchema, currentCaptionStyleSchema, currentGradientStopSchema, currentStops, currentBackgroundSchema, currentStripBackgroundSchema, currentShotSchema, currentFramePatchSchema, localeCodeSchema, focusSchema, currentFrameSchema, captionOperationSchema, frameOperationSchema, frameIdSchema, assetIdSchema, structuralOperationSchema, attachmentSchema, visionScreenshotSchema, requestSchema, legacyFinalizeSchema, durableUsageSchema, durablePassSchema, durableStateDiffSchema, agentOutcomeSchema, durableFinalizeSchema, revertSchema, finalizeSchema, createSessionSchema, historySchema, REFINE_FIELD_LABELS;
|
|
15946
16070
|
var init_refineProjectContracts = __esm({
|
|
15947
16071
|
"../api/_lib/refineProjectContracts.ts"() {
|
|
15948
16072
|
"use strict";
|
|
@@ -16260,6 +16384,10 @@ var init_refineProjectContracts = __esm({
|
|
|
16260
16384
|
projectId: z9.string().uuid(),
|
|
16261
16385
|
sessionId: z9.string().uuid(),
|
|
16262
16386
|
turnId: z9.string().uuid(),
|
|
16387
|
+
// #580 — the retry / clarification chain, and the ONLY receipt fact this door takes from the
|
|
16388
|
+
// browser. Every other figure on the receipt is re-read server-side from the ledger or derived
|
|
16389
|
+
// from the validated outcome; a parent id is a correlation the client alone knows.
|
|
16390
|
+
parentTurnId: z9.string().uuid().optional(),
|
|
16263
16391
|
generationIds: z9.array(z9.string().uuid()).min(1).max(3),
|
|
16264
16392
|
outcome: z9.enum(["completed", "cancelled", "failed"]),
|
|
16265
16393
|
committed: z9.boolean(),
|
|
@@ -16304,7 +16432,15 @@ var init_refineProjectContracts = __esm({
|
|
|
16304
16432
|
});
|
|
16305
16433
|
}
|
|
16306
16434
|
});
|
|
16307
|
-
|
|
16435
|
+
revertSchema = z9.object({
|
|
16436
|
+
intent: z9.literal("revert"),
|
|
16437
|
+
projectId: z9.string().uuid(),
|
|
16438
|
+
/** The committed turn being undone. Becomes the new receipt's `parent_turn_id`. */
|
|
16439
|
+
turnId: z9.string().uuid(),
|
|
16440
|
+
/** The revert's own turn id, minted by the browser exactly as a turn id is. */
|
|
16441
|
+
revertTurnId: z9.string().uuid()
|
|
16442
|
+
});
|
|
16443
|
+
finalizeSchema = z9.union([revertSchema, durableFinalizeSchema, legacyFinalizeSchema]);
|
|
16308
16444
|
createSessionSchema = z9.object({
|
|
16309
16445
|
projectId: z9.string().uuid(),
|
|
16310
16446
|
sessionId: z9.string().uuid(),
|
|
@@ -16361,6 +16497,7 @@ var init_refineProjectContracts = __esm({
|
|
|
16361
16497
|
});
|
|
16362
16498
|
|
|
16363
16499
|
// src/agentTurnAdapter.ts
|
|
16500
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
16364
16501
|
function recordEnvelope(existing, state) {
|
|
16365
16502
|
const stored = existing != null && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : { record: 2 };
|
|
16366
16503
|
return { ...stored, project: serializeProject(state) };
|
|
@@ -16599,6 +16736,7 @@ function applyAllowance(allowance, pass) {
|
|
|
16599
16736
|
if (pass.remaining !== null) allowance.remaining = pass.remaining;
|
|
16600
16737
|
}
|
|
16601
16738
|
async function authorizePass(context, request, panelIds, rendered) {
|
|
16739
|
+
context.screenshotCount = request.screenshots.length + (request.composedPreview ? 1 : 0);
|
|
16602
16740
|
const result = await context.runtime.plannerPass(context.userId, request, context.turnKey);
|
|
16603
16741
|
applyAllowance(context.allowance, result.allowance);
|
|
16604
16742
|
if (result.ok) {
|
|
@@ -16626,23 +16764,28 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
16626
16764
|
if (!decision.allowed) {
|
|
16627
16765
|
return decision.reason === "forbidden" ? { ...NOT_FOUND } : { error: "capability_denied", message: denial ?? "This action is not available." };
|
|
16628
16766
|
}
|
|
16767
|
+
const turnId = randomUUID3();
|
|
16768
|
+
const turnKey = `turn_${turnId}`;
|
|
16769
|
+
const startedAt = input.startedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
16629
16770
|
const allowance = { remaining: null, refunded: 0, settled: false };
|
|
16630
16771
|
let storedRecord;
|
|
16631
16772
|
let screenSources = null;
|
|
16632
16773
|
const context = {
|
|
16633
16774
|
userId: deps.userId,
|
|
16634
16775
|
projectId: project2.id,
|
|
16635
|
-
turnKey
|
|
16776
|
+
turnKey,
|
|
16777
|
+
screenshotCount: 0,
|
|
16636
16778
|
instruction: input.instruction,
|
|
16637
16779
|
focus: input.focus,
|
|
16638
16780
|
allowance,
|
|
16639
16781
|
runtime
|
|
16640
16782
|
};
|
|
16641
16783
|
const result = await runAgentTurn({
|
|
16784
|
+
turnId,
|
|
16642
16785
|
instruction: input.instruction,
|
|
16643
16786
|
focus: input.focus,
|
|
16644
16787
|
requestedCycle: input.requestedCycle,
|
|
16645
|
-
startedAt
|
|
16788
|
+
startedAt,
|
|
16646
16789
|
cancelled: input.cancelled
|
|
16647
16790
|
}, {
|
|
16648
16791
|
loadProject: async () => {
|
|
@@ -16694,35 +16837,60 @@ async function runMcpProjectAgentTurn(deps, input, runtime) {
|
|
|
16694
16837
|
},
|
|
16695
16838
|
emit: async (event) => {
|
|
16696
16839
|
if (event.type === "turn_completed") {
|
|
16697
|
-
|
|
16840
|
+
const generationIds = event.result.legacy.passes.map((pass) => pass.generationId);
|
|
16841
|
+
if (generationIds.length > 0) {
|
|
16842
|
+
try {
|
|
16843
|
+
const settlement = await runtime.settleTurn({
|
|
16844
|
+
userId: deps.userId,
|
|
16845
|
+
projectId: project2.id,
|
|
16846
|
+
turnKey,
|
|
16847
|
+
generationIds,
|
|
16848
|
+
outcome: event.result.outcome,
|
|
16849
|
+
committed: event.result.committed,
|
|
16850
|
+
capped: event.result.legacy.status === "capped",
|
|
16851
|
+
receipt: {
|
|
16852
|
+
turnId: event.turnId,
|
|
16853
|
+
startedAt,
|
|
16854
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16855
|
+
screenshotCount: context.screenshotCount,
|
|
16856
|
+
instructionLength: input.instruction.length
|
|
16857
|
+
}
|
|
16858
|
+
});
|
|
16859
|
+
allowance.remaining = settlement.balanceCredits;
|
|
16860
|
+
allowance.refunded = settlement.refundedCredits;
|
|
16861
|
+
allowance.settled = true;
|
|
16862
|
+
} catch (error) {
|
|
16863
|
+
runtime.log("mcp_agent_turn_settlement_failed", {
|
|
16864
|
+
turnKey,
|
|
16865
|
+
...safeFailureDiagnostic(error)
|
|
16866
|
+
});
|
|
16867
|
+
}
|
|
16868
|
+
} else {
|
|
16698
16869
|
allowance.settled = true;
|
|
16699
|
-
await runtime.emit(event);
|
|
16700
|
-
return;
|
|
16701
16870
|
}
|
|
16702
16871
|
try {
|
|
16703
|
-
|
|
16872
|
+
await runtime.recordReceipt?.({
|
|
16873
|
+
turnId: event.turnId,
|
|
16704
16874
|
userId: deps.userId,
|
|
16705
16875
|
projectId: project2.id,
|
|
16706
|
-
|
|
16707
|
-
generationIds: event.result.legacy.passes.map((pass) => pass.generationId),
|
|
16876
|
+
parentTurnId: null,
|
|
16708
16877
|
outcome: event.result.outcome,
|
|
16709
16878
|
committed: event.result.committed,
|
|
16710
|
-
|
|
16879
|
+
generationIds,
|
|
16880
|
+
refundedCredits: allowance.refunded,
|
|
16881
|
+
startedAt,
|
|
16882
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16883
|
+
screenshotCount: context.screenshotCount,
|
|
16884
|
+
instructionLength: input.instruction.length
|
|
16711
16885
|
});
|
|
16712
|
-
allowance.remaining = settlement.balanceCredits;
|
|
16713
|
-
allowance.refunded = settlement.refundedCredits;
|
|
16714
|
-
allowance.settled = true;
|
|
16715
16886
|
} catch (error) {
|
|
16716
|
-
runtime.log("
|
|
16717
|
-
turnKey: input.turnKey,
|
|
16718
|
-
...safeFailureDiagnostic(error)
|
|
16719
|
-
});
|
|
16887
|
+
runtime.log("mcp_agent_turn_receipt_failed", { turnKey, ...safeFailureDiagnostic(error) });
|
|
16720
16888
|
}
|
|
16721
16889
|
}
|
|
16722
16890
|
await runtime.emit(event);
|
|
16723
16891
|
}
|
|
16724
16892
|
});
|
|
16725
|
-
return { ...result, projectId: project2.id, allowance };
|
|
16893
|
+
return { ...result, turnKey, projectId: project2.id, allowance };
|
|
16726
16894
|
}
|
|
16727
16895
|
var VISION_BASE64_BUDGET, NOT_FOUND;
|
|
16728
16896
|
var init_agentTurnAdapter = __esm({
|
|
@@ -16758,7 +16926,10 @@ function writeErrorFrom(error) {
|
|
|
16758
16926
|
function localAgentTurnRuntime(bridge) {
|
|
16759
16927
|
return {
|
|
16760
16928
|
plannerPass: (_userId, request, turnKey) => bridge.refinePass(request.projectId, request, turnKey),
|
|
16761
|
-
|
|
16929
|
+
// The receipt facts travel WITH settlement (#580): the server writes the one receipt for this
|
|
16930
|
+
// turn, keyed by the id the local coordinator ran under, and this machine — which holds no
|
|
16931
|
+
// Supabase credential — writes nothing durable at all.
|
|
16932
|
+
settleTurn: ({ projectId, turnKey, generationIds, outcome, committed, capped, receipt }) => bridge.settleRefineTurn(projectId, { turnKey, generationIds, outcome, committed, capped, receipt }),
|
|
16762
16933
|
writeProjectRecord: async (projectId, expectedVersion, record6) => {
|
|
16763
16934
|
try {
|
|
16764
16935
|
const written = await bridge.updateProjectRecord(projectId, record6, expectedVersion);
|
|
@@ -16772,19 +16943,17 @@ function localAgentTurnRuntime(bridge) {
|
|
|
16772
16943
|
};
|
|
16773
16944
|
}
|
|
16774
16945
|
async function runLocalRefineTurn(deps, bridge, input) {
|
|
16775
|
-
const turnId = mintTurnId();
|
|
16776
16946
|
try {
|
|
16777
16947
|
const turn = await runMcpProjectAgentTurn(
|
|
16778
16948
|
deps,
|
|
16779
16949
|
{
|
|
16780
|
-
turnKey: turnId,
|
|
16781
16950
|
instruction: input.instruction,
|
|
16782
16951
|
focus: input.focus ?? { kind: "project", label: "Whole project" },
|
|
16783
16952
|
...input.project ? { project: input.project } : {}
|
|
16784
16953
|
},
|
|
16785
16954
|
localAgentTurnRuntime(bridge)
|
|
16786
16955
|
);
|
|
16787
|
-
return refineProjectResult(
|
|
16956
|
+
return refineProjectResult(turn);
|
|
16788
16957
|
} catch {
|
|
16789
16958
|
return refineProjectUnavailable("The Agent could not complete this turn. Nothing changed\u2014try again.");
|
|
16790
16959
|
}
|
|
@@ -22435,7 +22604,7 @@ var PLACEHOLDER_PORT = 8765;
|
|
|
22435
22604
|
var CLIENT_NAME = "ShotOps CLI";
|
|
22436
22605
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
22437
22606
|
function authOrigin() {
|
|
22438
|
-
return
|
|
22607
|
+
return authorityOrigin();
|
|
22439
22608
|
}
|
|
22440
22609
|
function base64url2(buf) {
|
|
22441
22610
|
return buf.toString("base64url");
|
|
@@ -22759,7 +22928,7 @@ async function startStdioServer(token) {
|
|
|
22759
22928
|
const localInstructions = token ? `${SERVER_INSTRUCTIONS}
|
|
22760
22929
|
|
|
22761
22930
|
## Local mode (this connection)
|
|
22762
|
-
Rendering runs on THIS machine \u2014 free, no timeout ceiling. A hosted token is configured, so save_project / read_look / save_look also work (they read/write your hosted ShotOps account). refine_project runs the Agent turn HERE, rendering on this machine to inspect its own work, and asks ShotOps' servers only to run each AI pass \u2014 so it costs AI credits on your account, and one call commits at most one edit to the saved project. Never re-implement its edit by hand; ${LOCAL_INTAKE}` : `${SERVER_INSTRUCTIONS}
|
|
22931
|
+
Rendering runs on THIS machine \u2014 free, no timeout ceiling. A hosted token is configured, so save_project / read_look / save_look also work (they read/write your hosted ShotOps account). refine_project runs the Agent turn HERE, rendering on this machine to inspect its own work, and asks ShotOps' servers only to run each AI pass \u2014 so it costs AI cloud credits on your account, and one call commits at most one edit to the saved project. Never re-implement its edit by hand; ${LOCAL_INTAKE}` : `${SERVER_INSTRUCTIONS}
|
|
22763
22932
|
|
|
22764
22933
|
## Local mode (this connection)
|
|
22765
22934
|
Rendering runs on THIS machine \u2014 free, no timeout ceiling. No hosted token is configured. A NEW save_project with local { "path": "..." } PNGs deliberately uploads one temporary private handoff and returns status:"pending_claim" + an openUrl: ALWAYS give that URL to the user. The user owns the project only after opening it and signing in within 7 days; no projectId exists before then. Passing an existing project requires SHOTOPS_TOKEN and is refused here. Ordinary render_strip / emit_bundle calls upload NOTHING, and an explicit request not to save means do not call save_project. read_look / save_look / read_project / render_project / refine_project / share links still require an account connection. ${LOCAL_INTAKE}`;
|