applaunchflow 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -0
- package/build/catalog.js +5 -0
- package/build/catalog.test.js +13 -0
- package/build/cli-core.js +20 -0
- package/build/cli-core.test.js +24 -0
- package/build/cli.js +71 -0
- package/build/client/api.js +341 -0
- package/build/http.js +178 -0
- package/build/http.test.js +111 -0
- package/build/index.js +127 -0
- package/build/prompts/register.js +66 -0
- package/build/read-receipt.test.js +36 -0
- package/build/resources/data.js +723 -0
- package/build/resources/register.js +115 -0
- package/build/social-template-previews.js +84 -0
- package/build/template-previews.js +83 -0
- package/build/tool-metadata.js +112 -0
- package/build/tool-metadata.test.js +32 -0
- package/build/tools/assets.js +311 -0
- package/build/tools/graphics.js +475 -0
- package/build/tools/keywords.js +132 -0
- package/build/tools/layouts.js +283 -0
- package/build/tools/localization.js +59 -0
- package/build/tools/mockups.js +324 -0
- package/build/tools/projects.js +113 -0
- package/build/tools/promovideo.js +199 -0
- package/build/tools/screenshots.js +307 -0
- package/build/tools/templates.js +195 -0
- package/build/tools/utils.js +212 -0
- package/build/tools/variants.js +71 -0
- package/package.json +33 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createHostedReadReceipt, fail, hostedMcpEnabled, ok, verifyHostedReadReceipt, } from "./utils.js";
|
|
3
|
+
const transformOperationSchema = z.object({
|
|
4
|
+
type: z.enum([
|
|
5
|
+
"update_node",
|
|
6
|
+
"delete_node",
|
|
7
|
+
"add_node",
|
|
8
|
+
"reorder",
|
|
9
|
+
"replace_color",
|
|
10
|
+
]).describe("Operation type. replace_color is a find-and-replace for colors across the layout — use it for bulk color changes instead of updating each text node individually. " +
|
|
11
|
+
"Example: {type:'replace_color', target:{nodeType:'screen'}, changes:{find:'#F6EFE9', replace:'#7C3AED'}} replaces that color everywhere (text marks, icon colors, backgrounds). " +
|
|
12
|
+
"Use screens:'all' to replace across all screens, or screens:[6] for a single screen."),
|
|
13
|
+
target: z.object({
|
|
14
|
+
nodeType: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe("REQUIRED. The node type to target: 'screen', 'text', 'screenshot', 'illustration', 'pill', 'badge', 'blob', 'rating', 'logo', 'emoji', 'header', 'panoramaBackground', 'backgroundImage'."),
|
|
17
|
+
nodeId: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Optional. Target a specific node by id. If omitted, the operation applies to ALL nodes of nodeType in the target screens."),
|
|
21
|
+
selector: z
|
|
22
|
+
.string()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Optional. Target screens by id: 'screenId:<id>'. Do NOT use '#' prefix."),
|
|
25
|
+
screens: z
|
|
26
|
+
.union([z.literal("all"), z.array(z.number())])
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Optional. Target specific screens by index array (e.g. [0, 1, 2]) or 'all' for every screen. If omitted, targets all screens."),
|
|
29
|
+
}),
|
|
30
|
+
changes: z.record(z.any()),
|
|
31
|
+
}).superRefine((operation, ctx) => {
|
|
32
|
+
// Require nodeType for all operations except replace_color
|
|
33
|
+
if (!operation.target.nodeType && operation.type !== "replace_color") {
|
|
34
|
+
ctx.addIssue({
|
|
35
|
+
code: z.ZodIssueCode.custom,
|
|
36
|
+
path: ["target", "nodeType"],
|
|
37
|
+
message: "nodeType is required in target",
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const payload = operation.changes?.node &&
|
|
42
|
+
typeof operation.changes.node === "object" &&
|
|
43
|
+
!Array.isArray(operation.changes.node)
|
|
44
|
+
? operation.changes.node
|
|
45
|
+
: operation.changes;
|
|
46
|
+
if (operation.type === "add_node") {
|
|
47
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
48
|
+
ctx.addIssue({
|
|
49
|
+
code: z.ZodIssueCode.custom,
|
|
50
|
+
path: ["changes"],
|
|
51
|
+
message: "add_node requires a node object in changes or changes.node",
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typeof payload.id !== "string" || payload.id.trim().length === 0) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: z.ZodIssueCode.custom,
|
|
58
|
+
path: ["changes", "id"],
|
|
59
|
+
message: "add_node requires a non-empty id field",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (operation.target.nodeType === "screen" &&
|
|
63
|
+
("text" in payload || "screenshotPath" in payload)) {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: z.ZodIssueCode.custom,
|
|
66
|
+
path: ["changes"],
|
|
67
|
+
message: "When adding a screen, only provide the screen container fields. Add screenshot and text nodes with separate add_node operations targeting that screen index.",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
export function registerLayoutTools(server, client) {
|
|
73
|
+
const layoutReadReceipts = new Map();
|
|
74
|
+
function buildReadReceiptKey(args) {
|
|
75
|
+
return [args.generationId, args.language, args.variantId || "default"].join("::");
|
|
76
|
+
}
|
|
77
|
+
function buildEditorUrl(args) {
|
|
78
|
+
const params = new URLSearchParams({
|
|
79
|
+
projectId: args.generationId,
|
|
80
|
+
device: "phone",
|
|
81
|
+
});
|
|
82
|
+
if (args.variantId) {
|
|
83
|
+
params.set("variantId", args.variantId);
|
|
84
|
+
}
|
|
85
|
+
if (args.language) {
|
|
86
|
+
params.set("language", args.language);
|
|
87
|
+
}
|
|
88
|
+
return `${client.credentials.baseUrl}/editor?${params.toString()}`;
|
|
89
|
+
}
|
|
90
|
+
function buildVariantPreviewUrl(args) {
|
|
91
|
+
if (!args.variantId) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const params = new URLSearchParams({
|
|
95
|
+
device: "phone",
|
|
96
|
+
});
|
|
97
|
+
if (args.language) {
|
|
98
|
+
params.set("language", args.language);
|
|
99
|
+
}
|
|
100
|
+
return `${client.credentials.baseUrl}/api/variants/${args.variantId}/preview?${params.toString()}`;
|
|
101
|
+
}
|
|
102
|
+
server.registerTool("get_layout", {
|
|
103
|
+
title: "Get Layout",
|
|
104
|
+
description: "Get layout JSON for the current translation before editing or reviewing a variant. " +
|
|
105
|
+
"This is mandatory before every direct transform_layout call. " +
|
|
106
|
+
"Returns the editor URL as a reference link — do NOT auto-open it. The user already has the editor open from the initial generation. " +
|
|
107
|
+
"The returned JSON follows the layout schema documented in the resource applaunchflow://schema/layout — read it to learn which fields exist and their valid ranges, not just which ones happen to be set here.",
|
|
108
|
+
inputSchema: {
|
|
109
|
+
generationId: z.string().uuid(),
|
|
110
|
+
language: z.string().optional(),
|
|
111
|
+
variantId: z.string().uuid().optional(),
|
|
112
|
+
sign: z.boolean().optional(),
|
|
113
|
+
},
|
|
114
|
+
}, async ({ generationId, language, variantId, sign }, extra) => {
|
|
115
|
+
try {
|
|
116
|
+
const layout = await client.getLayout({
|
|
117
|
+
generationId,
|
|
118
|
+
language,
|
|
119
|
+
variantId,
|
|
120
|
+
sign,
|
|
121
|
+
});
|
|
122
|
+
const hasEditReceipt = Boolean(language);
|
|
123
|
+
let readReceipt;
|
|
124
|
+
if (language) {
|
|
125
|
+
const receiptKey = buildReadReceiptKey({
|
|
126
|
+
generationId,
|
|
127
|
+
language,
|
|
128
|
+
variantId,
|
|
129
|
+
});
|
|
130
|
+
layoutReadReceipts.set(receiptKey, Date.now());
|
|
131
|
+
if (hostedMcpEnabled()) {
|
|
132
|
+
readReceipt = createHostedReadReceipt(receiptKey, client.credentials.token);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const editorUrl = buildEditorUrl({ generationId, language, variantId });
|
|
136
|
+
const previewUrl = buildVariantPreviewUrl({ language, variantId });
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: [
|
|
142
|
+
"Fetched layout data.",
|
|
143
|
+
`Editor URL: ${editorUrl}`,
|
|
144
|
+
previewUrl ? `Preview URL: ${previewUrl}` : null,
|
|
145
|
+
hasEditReceipt
|
|
146
|
+
? "A fresh get_layout read is now recorded for this generation/language/variant and can be used for one transform_layout call."
|
|
147
|
+
: "No edit receipt was recorded because language was omitted. Provide language when reading a layout you intend to transform.",
|
|
148
|
+
]
|
|
149
|
+
.filter(Boolean)
|
|
150
|
+
.join("\n"),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
structuredContent: {
|
|
154
|
+
success: true,
|
|
155
|
+
data: {
|
|
156
|
+
layout,
|
|
157
|
+
editorUrl,
|
|
158
|
+
previewUrl,
|
|
159
|
+
readBeforeEditSatisfied: hasEditReceipt,
|
|
160
|
+
readReceipt,
|
|
161
|
+
},
|
|
162
|
+
message: "Fetched layout data",
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return fail(error);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
server.registerTool("save_layout", {
|
|
171
|
+
title: "Save Layout",
|
|
172
|
+
description: "Persist a full translation layout payload. Whole-layout replace per device size — prefer transform_layout for targeted edits. " +
|
|
173
|
+
"Each layout must be a complete, valid Layout object; see the resource applaunchflow://schema/layout for every field and valid value range.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
generationId: z.string().uuid(),
|
|
176
|
+
language: z.string(),
|
|
177
|
+
variantId: z.string().uuid().optional(),
|
|
178
|
+
mobileLayout: z
|
|
179
|
+
.record(z.any())
|
|
180
|
+
.describe("Complete Layout object for the phone canvas. Shape documented in applaunchflow://schema/layout."),
|
|
181
|
+
tabletLayout: z
|
|
182
|
+
.record(z.any())
|
|
183
|
+
.describe("Complete Layout object for the tablet canvas. Same shape as mobileLayout, different canvasWidth/canvasHeight."),
|
|
184
|
+
desktopLayout: z
|
|
185
|
+
.record(z.any())
|
|
186
|
+
.nullable()
|
|
187
|
+
.optional()
|
|
188
|
+
.describe("Optional complete Layout object for the desktop canvas. Same shape as mobileLayout."),
|
|
189
|
+
},
|
|
190
|
+
}, async (args, extra) => {
|
|
191
|
+
try {
|
|
192
|
+
return ok(await client.saveLayout(args), "Saved layout");
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return fail(error);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
server.registerTool("transform_layout", {
|
|
199
|
+
title: "Transform Layout",
|
|
200
|
+
description: "Apply transform operations to an existing layout. Primary editing tool for text, screenshots, colors, and structure changes. " +
|
|
201
|
+
"IMPORTANT: Always call get_layout FIRST to inspect the current layout state before using this tool. Never transform blindly. " +
|
|
202
|
+
"ENFORCED RULE: each transform_layout call requires a fresh get_layout call for the same generationId, language, and variantId immediately beforehand. " +
|
|
203
|
+
"RULES: " +
|
|
204
|
+
"1. nodeType is REQUIRED in every operation target. " +
|
|
205
|
+
"2. Omit nodeId to update ALL nodes of that type in the target screens. " +
|
|
206
|
+
"3. Use screens:'all' to target every screen at once. Do NOT send one operation per screen when the same change applies to all. " +
|
|
207
|
+
"4. Dot-notation is supported for deep updates without replacing the whole object. Example: {'richContent.attrs.defaultFontSize': 96} updates only the font size inside richContent.attrs, preserving all other fields. Always use dot-notation for nested property changes. " +
|
|
208
|
+
"5. For add_node, changes MUST include an 'id' field. " +
|
|
209
|
+
"6. To add new screens, first add empty screen containers, then populate them in a SECOND call using selector 'screenId:<id>'. " +
|
|
210
|
+
"7. Default to layouts:['mobile']. Only include tablet/desktop if the user asks. " +
|
|
211
|
+
"8. FONT SIZE: The rendered font size is controlled ONLY by 'richContent.attrs.defaultFontSize' (pixel value). To change font size, use dot-notation: {'richContent.attrs.defaultFontSize': 80}. Do NOT use 'fontSizeScale' — that property is for promo videos only and has NO effect on screenshot rendering. " +
|
|
212
|
+
"SCHEMA REFERENCE: read the resource applaunchflow://schema/transforms for the full operation and selector reference, and applaunchflow://schema/layout for every node type's fields and valid value ranges. Read them before any non-trivial edit rather than guessing field names.",
|
|
213
|
+
inputSchema: {
|
|
214
|
+
generationId: z.string().uuid(),
|
|
215
|
+
language: z.string(),
|
|
216
|
+
variantId: z.string().uuid().optional(),
|
|
217
|
+
atomic: z.boolean().optional(),
|
|
218
|
+
layouts: z
|
|
219
|
+
.array(z.enum(["mobile", "tablet", "desktop"]))
|
|
220
|
+
.optional()
|
|
221
|
+
.describe("Which layout sizes to transform. Default to ['mobile'] unless the user explicitly asks for tablet or desktop."),
|
|
222
|
+
operations: z.array(transformOperationSchema).min(1),
|
|
223
|
+
readReceipt: z
|
|
224
|
+
.string()
|
|
225
|
+
.optional()
|
|
226
|
+
.describe("Hosted connector only: pass the readReceipt returned by the immediately preceding get_layout call."),
|
|
227
|
+
},
|
|
228
|
+
}, async (args, extra) => {
|
|
229
|
+
try {
|
|
230
|
+
const receiptKey = buildReadReceiptKey({
|
|
231
|
+
generationId: args.generationId,
|
|
232
|
+
language: args.language,
|
|
233
|
+
variantId: args.variantId,
|
|
234
|
+
});
|
|
235
|
+
const hasReceipt = hostedMcpEnabled()
|
|
236
|
+
? verifyHostedReadReceipt(args.readReceipt, receiptKey, client.credentials.token)
|
|
237
|
+
: layoutReadReceipts.has(receiptKey);
|
|
238
|
+
if (!hasReceipt) {
|
|
239
|
+
return fail(new Error("Call get_layout first for this generation/language/variant before transform_layout. Direct layout editing is locked until the current layout has been read."));
|
|
240
|
+
}
|
|
241
|
+
const { readReceipt: _readReceipt, ...transformArgs } = args;
|
|
242
|
+
const transformed = await client.transformLayout(transformArgs);
|
|
243
|
+
layoutReadReceipts.delete(receiptKey);
|
|
244
|
+
const editorUrl = buildEditorUrl({
|
|
245
|
+
generationId: args.generationId,
|
|
246
|
+
language: args.language,
|
|
247
|
+
variantId: args.variantId,
|
|
248
|
+
});
|
|
249
|
+
const previewUrl = buildVariantPreviewUrl({
|
|
250
|
+
language: args.language,
|
|
251
|
+
variantId: args.variantId,
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: [
|
|
258
|
+
"Applied layout transform.",
|
|
259
|
+
`Editor URL (already open — do NOT run \`open\` again): ${editorUrl}`,
|
|
260
|
+
previewUrl ? `Preview URL: ${previewUrl}` : null,
|
|
261
|
+
"This transform consumed the current read receipt. Call get_layout again before the next direct edit.",
|
|
262
|
+
]
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.join("\n"),
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
structuredContent: {
|
|
268
|
+
success: true,
|
|
269
|
+
data: {
|
|
270
|
+
result: transformed,
|
|
271
|
+
editorUrl,
|
|
272
|
+
previewUrl,
|
|
273
|
+
nextEditRequiresFreshRead: true,
|
|
274
|
+
},
|
|
275
|
+
message: "Applied layout transform",
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
return fail(error);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fail, ok } from "./utils.js";
|
|
3
|
+
const SUPPORTED_LANGUAGE_CODES = [
|
|
4
|
+
"en", "es", "fr", "de", "it", "pt", "pt-BR", "ja", "ko",
|
|
5
|
+
"zh-CN", "zh-TW", "nl", "ru", "ar", "tr", "pl", "sv",
|
|
6
|
+
"no", "da", "fi", "cs", "hi", "hu", "ro", "uk",
|
|
7
|
+
];
|
|
8
|
+
export function registerLocalizationTools(server, client) {
|
|
9
|
+
server.registerTool("translate_layouts", {
|
|
10
|
+
title: "Translate Layouts",
|
|
11
|
+
description: "Translate screenshot layouts into one or more target languages using AI. " +
|
|
12
|
+
"This is the PREFERRED way to localize screenshots — do NOT manually edit text nodes for translation. " +
|
|
13
|
+
"The backend translates all text in the layout while preserving positioning, styling, and screenshots. " +
|
|
14
|
+
"Requires a source screenshot layout to already exist — created via apply_screenshot_style (the normal flow) or generate_layouts. " +
|
|
15
|
+
"Pass the variantId returned by apply_screenshot_style/generate_layouts to translate that exact variant; if omitted, the active variant is used.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
generationId: z.string().uuid().describe("The project/generation UUID."),
|
|
18
|
+
variantId: z
|
|
19
|
+
.string()
|
|
20
|
+
.uuid()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Variant to translate. If omitted, uses the active variant."),
|
|
23
|
+
targetLanguages: z
|
|
24
|
+
.array(z.enum(SUPPORTED_LANGUAGE_CODES))
|
|
25
|
+
.min(1)
|
|
26
|
+
.describe("Array of target language codes (e.g. ['en', 'ja', 'de']). The source language is auto-detected and excluded."),
|
|
27
|
+
layouts: z
|
|
28
|
+
.array(z.enum(["mobile", "tablet", "desktop"]))
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Which layout sizes to translate. Defaults to ['mobile', 'tablet']. Include 'desktop' only if the project has a desktop layout."),
|
|
31
|
+
},
|
|
32
|
+
}, async (args) => {
|
|
33
|
+
try {
|
|
34
|
+
const body = {
|
|
35
|
+
...args,
|
|
36
|
+
layouts: args.layouts || ["mobile", "tablet"],
|
|
37
|
+
};
|
|
38
|
+
return ok(await client.translateLayouts(body), "Translated layouts");
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return fail(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
server.registerTool("list_translations", {
|
|
45
|
+
title: "List Translations",
|
|
46
|
+
description: "List available translations for a project variant. Returns which languages have been translated.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
generationId: z.string().uuid(),
|
|
49
|
+
variantId: z.string().uuid().optional(),
|
|
50
|
+
},
|
|
51
|
+
}, async ({ generationId, variantId }) => {
|
|
52
|
+
try {
|
|
53
|
+
return ok(await client.getLayout({ generationId, variantId }), "Fetched translations");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return fail(error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createHostedReadReceipt, createReadReceiptStore, fail, hostedMcpEnabled, ok, openUrl, verifyHostedReadReceipt, } from "./utils.js";
|
|
3
|
+
const mockupReceiptKey = (generationId, variantId) => ["mockup", generationId, variantId || "active"].join("::");
|
|
4
|
+
const SCENE_PRESET_IDS = [
|
|
5
|
+
"hero-launch",
|
|
6
|
+
"pivot-showcase",
|
|
7
|
+
"slow-rotate",
|
|
8
|
+
"drifting-tilt",
|
|
9
|
+
];
|
|
10
|
+
const OUTPUT_RATIOS = ["1:1", "4:3", "16:9", "9:16"];
|
|
11
|
+
const MOCKUP_PRESETS_DATA = {
|
|
12
|
+
motions: [
|
|
13
|
+
{ id: "hero-reveal", label: "Hero reveal", durationSeconds: 6 },
|
|
14
|
+
{ id: "feature-sweep", label: "Feature sweep", durationSeconds: 7 },
|
|
15
|
+
{ id: "showcase-orbit", label: "Showcase orbit", durationSeconds: 10 },
|
|
16
|
+
{ id: "parallax-tilt", label: "Parallax tilt", durationSeconds: 8 },
|
|
17
|
+
],
|
|
18
|
+
finishes: [
|
|
19
|
+
{ id: "silver", label: "Silver" },
|
|
20
|
+
{ id: "cosmic-orange", label: "Cosmic Orange" },
|
|
21
|
+
{ id: "deep-blue", label: "Deep Blue" },
|
|
22
|
+
],
|
|
23
|
+
backgroundPresets: [
|
|
24
|
+
{ id: "soft", label: "Soft" },
|
|
25
|
+
{ id: "paper", label: "Paper" },
|
|
26
|
+
{ id: "midnight", label: "Midnight" },
|
|
27
|
+
{ id: "transparent", label: "Transparent" },
|
|
28
|
+
],
|
|
29
|
+
backgroundModes: ["color", "gradient", "image"],
|
|
30
|
+
outputRatios: [
|
|
31
|
+
{ id: "4:3", label: "Classic", detail: "1440×1080 (default)" },
|
|
32
|
+
{ id: "1:1", label: "Square", detail: "1080×1080" },
|
|
33
|
+
{ id: "16:9", label: "Landscape", detail: "1920×1080" },
|
|
34
|
+
{ id: "9:16", label: "Portrait", detail: "1080×1920" },
|
|
35
|
+
],
|
|
36
|
+
easings: ["linear", "ease-in", "ease-out", "ease-in-out"],
|
|
37
|
+
scenePresets: [
|
|
38
|
+
{
|
|
39
|
+
id: "hero-launch",
|
|
40
|
+
label: "Cinematic swing",
|
|
41
|
+
motion: "hero-reveal",
|
|
42
|
+
keyframeCount: 4,
|
|
43
|
+
motionDurationSeconds: 6,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "pivot-showcase",
|
|
47
|
+
label: "Spin reveal",
|
|
48
|
+
motion: "feature-sweep",
|
|
49
|
+
keyframeCount: 4,
|
|
50
|
+
motionDurationSeconds: 7,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "slow-rotate",
|
|
54
|
+
label: "Drop & zoom",
|
|
55
|
+
motion: "showcase-orbit",
|
|
56
|
+
keyframeCount: 5,
|
|
57
|
+
motionDurationSeconds: 10,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "drifting-tilt",
|
|
61
|
+
label: "Punch zoom",
|
|
62
|
+
motion: "parallax-tilt",
|
|
63
|
+
keyframeCount: 4,
|
|
64
|
+
motionDurationSeconds: 8,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
stateBounds: {
|
|
68
|
+
speed: { min: 0.7, max: 1.4 },
|
|
69
|
+
motionDuration: { min: 1, max: 60 },
|
|
70
|
+
deviceScale: { min: 0.7, max: 1.3 },
|
|
71
|
+
primaryKeyframes: { minCount: 2, maxCount: 8 },
|
|
72
|
+
},
|
|
73
|
+
keyframeBounds: {
|
|
74
|
+
time: { min: 0, max: 1, note: "Normalized position along the animation." },
|
|
75
|
+
x: { min: -3, max: 3 },
|
|
76
|
+
y: { min: -3, max: 3 },
|
|
77
|
+
rotationX: { min: -6.283185307179586, max: 6.283185307179586 },
|
|
78
|
+
rotationY: { min: -6.283185307179586, max: 6.283185307179586 },
|
|
79
|
+
rotationZ: { min: -6.283185307179586, max: 6.283185307179586 },
|
|
80
|
+
scale: { min: 0.2, max: 3 },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
function buildMockupEditorUrl(client, args) {
|
|
84
|
+
const params = new URLSearchParams({ projectId: args.generationId });
|
|
85
|
+
if (args.variantId) {
|
|
86
|
+
params.set("variantId", args.variantId);
|
|
87
|
+
}
|
|
88
|
+
return `${client.credentials.baseUrl}/mockups?${params.toString()}`;
|
|
89
|
+
}
|
|
90
|
+
export function registerMockupTools(server, client) {
|
|
91
|
+
const mockupReadReceipts = createReadReceiptStore();
|
|
92
|
+
server.registerTool("create_mockup_animation", {
|
|
93
|
+
title: "Create Mockup Animation",
|
|
94
|
+
description: "Create a new mockup-animation variant seeded from a SCENE_PRESETS preset and a specific screenshot/recording path. " +
|
|
95
|
+
"Always omit variantId — this tool always creates a new variant. Never overwrites an existing mockup variant. " +
|
|
96
|
+
"Call list_mockup_media first to pick a valid screenshotPath and list_mockup_presets to pick a presetId. " +
|
|
97
|
+
"The editor opens automatically after creation.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
projectId: z.string().uuid().describe("Project / generation UUID."),
|
|
100
|
+
screenshotPath: z
|
|
101
|
+
.string()
|
|
102
|
+
.min(1)
|
|
103
|
+
.describe('Storage-relative path for the device screen content, e.g. "mockups/1715191234567-clip.mp4" or "mobile/ios/1715191234567-home.png". Returned by list_mockup_media.'),
|
|
104
|
+
presetId: z
|
|
105
|
+
.enum(SCENE_PRESET_IDS)
|
|
106
|
+
.describe("Scene preset id: 'hero-launch' (cinematic swing), 'pivot-showcase' (spin reveal), 'slow-rotate' (drop & zoom), or 'drifting-tilt' (punch zoom)."),
|
|
107
|
+
outputRatio: z
|
|
108
|
+
.enum(OUTPUT_RATIOS)
|
|
109
|
+
.optional()
|
|
110
|
+
.describe("Optional output aspect ratio. Defaults to 4:3 (classic)."),
|
|
111
|
+
motionDurationSeconds: z
|
|
112
|
+
.number()
|
|
113
|
+
.min(1)
|
|
114
|
+
.max(60)
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("Optional override for the animation loop length (seconds). For video screenshotPath, pass the recording duration so the loop matches one cycle."),
|
|
117
|
+
label: z
|
|
118
|
+
.string()
|
|
119
|
+
.min(1)
|
|
120
|
+
.max(120)
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Optional variant label shown in the studio's variant dropdown."),
|
|
123
|
+
},
|
|
124
|
+
}, async (args, extra) => {
|
|
125
|
+
try {
|
|
126
|
+
const result = await client.createMockupAnimation({
|
|
127
|
+
generationId: args.projectId,
|
|
128
|
+
screenshotPath: args.screenshotPath,
|
|
129
|
+
presetId: args.presetId,
|
|
130
|
+
outputRatio: args.outputRatio,
|
|
131
|
+
motionDurationSeconds: args.motionDurationSeconds,
|
|
132
|
+
label: args.label,
|
|
133
|
+
});
|
|
134
|
+
const variantId = result?.variantId || "";
|
|
135
|
+
const editorUrl = buildMockupEditorUrl(client, {
|
|
136
|
+
generationId: args.projectId,
|
|
137
|
+
variantId,
|
|
138
|
+
});
|
|
139
|
+
await openUrl(server, editorUrl, "Opening the new mockup animation in the editor.", { signal: extra.signal });
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: [
|
|
145
|
+
"Created mockup animation variant.",
|
|
146
|
+
`Editor URL: ${editorUrl}`,
|
|
147
|
+
"IMPORTANT: Paste this exact editor URL in the reply so the user can open it.",
|
|
148
|
+
"To fine-tune the animation, call get_mockup_animation for this variant before update_mockup_animation.",
|
|
149
|
+
].join("\n"),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
structuredContent: {
|
|
153
|
+
success: true,
|
|
154
|
+
data: { ...result, editorUrl },
|
|
155
|
+
message: "Created mockup animation",
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return fail(error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
server.registerTool("get_mockup_animation", {
|
|
164
|
+
title: "Get Mockup Animation",
|
|
165
|
+
description: "Fetch the current mockup animation state (MockupProjectState shape) for a project. " +
|
|
166
|
+
"Required before update_mockup_animation so edits operate on fresh state.",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
generationId: z.string().uuid(),
|
|
169
|
+
variantId: z.string().uuid().optional(),
|
|
170
|
+
},
|
|
171
|
+
}, async ({ generationId, variantId }) => {
|
|
172
|
+
try {
|
|
173
|
+
const result = await client.getMockupAnimation(generationId, variantId);
|
|
174
|
+
const editorUrl = buildMockupEditorUrl(client, {
|
|
175
|
+
generationId,
|
|
176
|
+
variantId,
|
|
177
|
+
});
|
|
178
|
+
mockupReadReceipts.record({ generationId, variantId });
|
|
179
|
+
const readReceipt = hostedMcpEnabled()
|
|
180
|
+
? createHostedReadReceipt(mockupReceiptKey(generationId, variantId), client.credentials.token)
|
|
181
|
+
: undefined;
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: [
|
|
187
|
+
"Fetched mockup animation.",
|
|
188
|
+
`Editor URL: ${editorUrl}`,
|
|
189
|
+
"A fresh read receipt was recorded and can be used for one update_mockup_animation call.",
|
|
190
|
+
].join("\n"),
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
structuredContent: {
|
|
194
|
+
success: true,
|
|
195
|
+
data: {
|
|
196
|
+
...result,
|
|
197
|
+
editorUrl,
|
|
198
|
+
readBeforeEditSatisfied: true,
|
|
199
|
+
readReceipt,
|
|
200
|
+
},
|
|
201
|
+
message: "Fetched mockup animation",
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return fail(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
server.registerTool("update_mockup_animation", {
|
|
210
|
+
title: "Update Mockup Animation",
|
|
211
|
+
description: "Persist a mockup animation state (the same MockupProjectState shape returned by get_mockup_animation). " +
|
|
212
|
+
"This is a full-state replace — fetch the current state with get_mockup_animation, mutate the parts you want to change, then call this tool with the updated object. " +
|
|
213
|
+
"There is no granular per-keyframe transform; whole-state replace is the supported edit path. " +
|
|
214
|
+
"ENFORCED: each call requires a fresh get_mockup_animation for the same projectId/variantId immediately beforehand. " +
|
|
215
|
+
"Validation bounds (see list_mockup_presets for the full reference): primaryKeyframes count 2–8, time 0–1, x/y -3..3, rotations -2π..2π, scale 0.2..3, speed 0.7..1.4, motionDuration 1..60, deviceScale 0.7..1.3.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
projectId: z.string().uuid(),
|
|
218
|
+
variantId: z.string().uuid().optional(),
|
|
219
|
+
state: z
|
|
220
|
+
.record(z.any())
|
|
221
|
+
.describe("Full MockupProjectState object (selectedMediaPath, motion, finish, speed, background, backgroundMode, backgroundColor, backgroundGradient, backgroundImage, showDynamicIsland, outputRatio, motionDuration, deviceScale, primaryKeyframes, isPlaying). Use the object returned by get_mockup_animation as a starting point."),
|
|
222
|
+
readReceipt: z
|
|
223
|
+
.string()
|
|
224
|
+
.optional()
|
|
225
|
+
.describe("Hosted connector only: pass the readReceipt returned by the immediately preceding get_mockup_animation call."),
|
|
226
|
+
},
|
|
227
|
+
}, async (args) => {
|
|
228
|
+
try {
|
|
229
|
+
const receiptArgs = {
|
|
230
|
+
generationId: args.projectId,
|
|
231
|
+
variantId: args.variantId,
|
|
232
|
+
};
|
|
233
|
+
const hasReceipt = hostedMcpEnabled()
|
|
234
|
+
? verifyHostedReadReceipt(args.readReceipt, mockupReceiptKey(args.projectId, args.variantId), client.credentials.token)
|
|
235
|
+
: mockupReadReceipts.has(receiptArgs);
|
|
236
|
+
if (!hasReceipt) {
|
|
237
|
+
return fail(new Error("Call get_mockup_animation first for this project/variant before update_mockup_animation. Direct editing is locked until the current state has been read."));
|
|
238
|
+
}
|
|
239
|
+
const result = await client.updateMockupAnimation({
|
|
240
|
+
generationId: args.projectId,
|
|
241
|
+
variantId: args.variantId,
|
|
242
|
+
state: args.state,
|
|
243
|
+
});
|
|
244
|
+
mockupReadReceipts.consume(receiptArgs);
|
|
245
|
+
const editorUrl = buildMockupEditorUrl(client, {
|
|
246
|
+
generationId: args.projectId,
|
|
247
|
+
variantId: args.variantId,
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: "text",
|
|
253
|
+
text: [
|
|
254
|
+
"Updated mockup animation.",
|
|
255
|
+
`Editor URL (already open — do NOT run \`open\` again): ${editorUrl}`,
|
|
256
|
+
"This update consumed the current read receipt. Call get_mockup_animation again before the next direct edit.",
|
|
257
|
+
].join("\n"),
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
structuredContent: {
|
|
261
|
+
success: true,
|
|
262
|
+
data: {
|
|
263
|
+
result,
|
|
264
|
+
editorUrl,
|
|
265
|
+
nextEditRequiresFreshRead: true,
|
|
266
|
+
},
|
|
267
|
+
message: "Updated mockup animation",
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
return fail(error);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
server.registerTool("list_mockup_media", {
|
|
276
|
+
title: "List Mockup Media",
|
|
277
|
+
description: "List the screenshots and screen recordings uploaded under the project's mockups/ storage folder. " +
|
|
278
|
+
"Call this before create_mockup_animation to discover valid screenshotPath values. " +
|
|
279
|
+
"Returns media items with { path, signedUrl, kind: 'image' | 'video' }.",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
projectId: z.string().uuid(),
|
|
282
|
+
},
|
|
283
|
+
}, async ({ projectId }) => {
|
|
284
|
+
try {
|
|
285
|
+
const result = await client.listMockupMedia(projectId);
|
|
286
|
+
return ok(result, "Listed mockup media");
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return fail(error);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
server.registerTool("list_mockup_presets", {
|
|
293
|
+
title: "List Mockup Presets",
|
|
294
|
+
description: "Return the structured set of valid mockup configuration values: motion presets, device finishes, background presets, output ratios, scene presets, and validation bounds. " +
|
|
295
|
+
"Use before constructing an update_mockup_animation payload to pick valid enum values without round-tripping through the server validator. " +
|
|
296
|
+
"If projectId is supplied, also returns the active screenshots variant's themeColors so the LLM can pick on-brand swatches for backgroundColor / gradient.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
projectId: z
|
|
299
|
+
.string()
|
|
300
|
+
.uuid()
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("Optional generation UUID to also fetch theme colors."),
|
|
303
|
+
},
|
|
304
|
+
}, async ({ projectId }) => {
|
|
305
|
+
try {
|
|
306
|
+
let themeColors = null;
|
|
307
|
+
if (projectId) {
|
|
308
|
+
try {
|
|
309
|
+
const themeResult = await client.getMockupThemeColors(projectId);
|
|
310
|
+
themeColors = themeResult?.themeColors ?? null;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Theme colors are optional — a missing screenshots variant is not
|
|
314
|
+
// a failure for the presets lookup.
|
|
315
|
+
themeColors = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return ok({ ...MOCKUP_PRESETS_DATA, themeColors }, "Mockup preset reference");
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
return fail(error);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|