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,113 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fail, ok } from "./utils.js";
|
|
3
|
+
function stripUndefined(value) {
|
|
4
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
5
|
+
}
|
|
6
|
+
export function registerProjectTools(server, client) {
|
|
7
|
+
server.registerTool("list_projects", {
|
|
8
|
+
title: "List Projects",
|
|
9
|
+
description: "List AppLaunchFlow projects for the authenticated user",
|
|
10
|
+
}, async () => {
|
|
11
|
+
try {
|
|
12
|
+
return ok(await client.listProjects(), "Fetched projects");
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return fail(error);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
server.registerTool("get_project", {
|
|
19
|
+
title: "Get Project",
|
|
20
|
+
description: "Get the full hub state for a project",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
projectId: z.string().uuid(),
|
|
23
|
+
},
|
|
24
|
+
}, async ({ projectId }) => {
|
|
25
|
+
try {
|
|
26
|
+
return ok(await client.getProject(projectId), "Fetched project");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return fail(error);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.registerTool("create_project", {
|
|
33
|
+
title: "Create Project",
|
|
34
|
+
description: "Create a new AppLaunchFlow project. Only app name and platform are required. " +
|
|
35
|
+
"Autofill category and description from context when possible — do not ask the user for these unless genuinely ambiguous.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
appName: z
|
|
38
|
+
.string()
|
|
39
|
+
.trim()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(120)
|
|
42
|
+
.describe("The app name."),
|
|
43
|
+
platform: z
|
|
44
|
+
.enum(["ios", "android", "both"])
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Target platform. Defaults to iOS."),
|
|
47
|
+
category: z
|
|
48
|
+
.string()
|
|
49
|
+
.trim()
|
|
50
|
+
.max(120)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("App category. Infer from the app name/context when possible (e.g. 'Travel' for a flight app)."),
|
|
53
|
+
appDescription: z
|
|
54
|
+
.string()
|
|
55
|
+
.trim()
|
|
56
|
+
.max(4000)
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Brief app description. Infer from context when possible."),
|
|
59
|
+
defaultDeviceType: z
|
|
60
|
+
.enum(["phone", "tablet", "desktop", "watch"])
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("Defaults to phone. Only set if the user explicitly asks."),
|
|
63
|
+
logoPath: z
|
|
64
|
+
.string()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe("Optional stored logo path from upload_screenshots when fileType=logo."),
|
|
67
|
+
metadata: z
|
|
68
|
+
.record(z.any())
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Advanced escape hatch for extra metadata fields."),
|
|
71
|
+
},
|
|
72
|
+
}, async (args) => {
|
|
73
|
+
try {
|
|
74
|
+
const platform = args.platform || "ios";
|
|
75
|
+
const metadata = stripUndefined({
|
|
76
|
+
...(args.metadata || {}),
|
|
77
|
+
appName: args.appName,
|
|
78
|
+
platform,
|
|
79
|
+
category: args.category,
|
|
80
|
+
appDescription: args.appDescription,
|
|
81
|
+
defaultDeviceType: args.defaultDeviceType || "phone",
|
|
82
|
+
logoPath: args.logoPath,
|
|
83
|
+
});
|
|
84
|
+
const requestBody = stripUndefined({
|
|
85
|
+
name: args.appName,
|
|
86
|
+
platform,
|
|
87
|
+
metadata,
|
|
88
|
+
});
|
|
89
|
+
const created = await client.createProject(requestBody);
|
|
90
|
+
return ok({
|
|
91
|
+
project: created.project,
|
|
92
|
+
nextRecommendedStep: "upload_screenshots",
|
|
93
|
+
}, "Created project");
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return fail(error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.registerTool("delete_project", {
|
|
100
|
+
title: "Delete Project",
|
|
101
|
+
description: "Delete a project",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
projectId: z.string().uuid(),
|
|
104
|
+
},
|
|
105
|
+
}, async ({ projectId }) => {
|
|
106
|
+
try {
|
|
107
|
+
return ok(await client.deleteProject(projectId), "Deleted project");
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return fail(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createHostedReadReceipt, createReadReceiptStore, fail, hostedMcpEnabled, ok, openUrl, verifyHostedReadReceipt, } from "./utils.js";
|
|
3
|
+
const promoReceiptKey = (generationId, variantId) => ["promo-video", generationId, variantId || "active"].join("::");
|
|
4
|
+
function buildPromoVideoEditorUrl(client, args) {
|
|
5
|
+
const params = new URLSearchParams({ projectId: args.generationId });
|
|
6
|
+
if (args.variantId) {
|
|
7
|
+
params.set("variantId", args.variantId);
|
|
8
|
+
}
|
|
9
|
+
return `${client.credentials.baseUrl}/promovideo?${params.toString()}`;
|
|
10
|
+
}
|
|
11
|
+
export function registerPromoVideoTools(server, client) {
|
|
12
|
+
const promoVideoReadReceipts = createReadReceiptStore();
|
|
13
|
+
server.registerTool("generate_promo_video", {
|
|
14
|
+
title: "Generate Promo Video",
|
|
15
|
+
description: "Run AI generation against the project's screenshots and produce a complete Remotion promo video config. " +
|
|
16
|
+
"Omit variantId so a new variant is always created — never overwrite an existing promo-video variant. " +
|
|
17
|
+
"After generation, the promo video editor opens automatically. " +
|
|
18
|
+
"Use selectedScreenshotIndices to constrain which uploaded screenshots feed the LLM. " +
|
|
19
|
+
"Use the optional message field to pass natural-language regeneration feedback when iterating on an existing variant.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
projectId: z.string().uuid().describe("Project / generation UUID."),
|
|
22
|
+
message: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Optional natural-language feedback for regeneration. Only meaningful when iterating on an existing variant."),
|
|
26
|
+
variantId: z
|
|
27
|
+
.string()
|
|
28
|
+
.uuid()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("DO NOT pass this for fresh takes. Only set when explicitly regenerating an existing variant."),
|
|
31
|
+
selectedScreenshotIndices: z
|
|
32
|
+
.array(z.number().int().min(0))
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Optional indices into the project's screenshots array. If omitted, the generator uses the project's default platform set."),
|
|
35
|
+
},
|
|
36
|
+
}, async (args, extra) => {
|
|
37
|
+
try {
|
|
38
|
+
const result = await client.generatePromoVideo(args);
|
|
39
|
+
const variantId = result?.variantId || args.variantId || "";
|
|
40
|
+
const editorUrl = buildPromoVideoEditorUrl(client, {
|
|
41
|
+
generationId: args.projectId,
|
|
42
|
+
variantId,
|
|
43
|
+
});
|
|
44
|
+
await openUrl(server, editorUrl, "Opening the generated promo video in the editor.", { signal: extra.signal });
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: [
|
|
50
|
+
"Generated promo video successfully.",
|
|
51
|
+
`Editor URL: ${editorUrl}`,
|
|
52
|
+
"IMPORTANT: Paste this exact editor URL in the reply so the user can open it.",
|
|
53
|
+
].join("\n"),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
structuredContent: {
|
|
57
|
+
success: true,
|
|
58
|
+
data: { ...result, editorUrl },
|
|
59
|
+
message: "Generated promo video",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return fail(error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
server.registerTool("get_promo_video", {
|
|
68
|
+
title: "Get Promo Video",
|
|
69
|
+
description: "Fetch the current promo video config (Remotion VideoConfig) for a project. Required before update_promo_video so edits operate on fresh state. " +
|
|
70
|
+
"The returned object follows the schema in the resource applaunchflow://schema/video-config — read that resource to learn which fields and scene types exist and their valid ranges, not just which ones this config happens to use.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
generationId: z.string().uuid(),
|
|
73
|
+
variantId: z.string().uuid().optional(),
|
|
74
|
+
},
|
|
75
|
+
}, async ({ generationId, variantId }) => {
|
|
76
|
+
try {
|
|
77
|
+
const result = await client.getPromoVideo(generationId, variantId);
|
|
78
|
+
const editorUrl = buildPromoVideoEditorUrl(client, {
|
|
79
|
+
generationId,
|
|
80
|
+
variantId,
|
|
81
|
+
});
|
|
82
|
+
promoVideoReadReceipts.record({ generationId, variantId });
|
|
83
|
+
const readReceipt = hostedMcpEnabled()
|
|
84
|
+
? createHostedReadReceipt(promoReceiptKey(generationId, variantId), client.credentials.token)
|
|
85
|
+
: undefined;
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: [
|
|
91
|
+
"Fetched promo video.",
|
|
92
|
+
`Editor URL: ${editorUrl}`,
|
|
93
|
+
"A fresh read receipt was recorded and can be used for one update_promo_video call.",
|
|
94
|
+
].join("\n"),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
structuredContent: {
|
|
98
|
+
success: true,
|
|
99
|
+
data: {
|
|
100
|
+
...result,
|
|
101
|
+
editorUrl,
|
|
102
|
+
readBeforeEditSatisfied: true,
|
|
103
|
+
readReceipt,
|
|
104
|
+
},
|
|
105
|
+
message: "Fetched promo video",
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return fail(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
server.registerTool("update_promo_video", {
|
|
114
|
+
title: "Update Promo Video",
|
|
115
|
+
description: "Persist a promo video config (the same Remotion VideoConfig shape returned by get_promo_video / generate_promo_video). " +
|
|
116
|
+
"This is a full-config replace — fetch the current config with get_promo_video, mutate the parts you want to change, then call this tool with the updated object. " +
|
|
117
|
+
"There is no granular scene-level transform; whole-config replace is the supported edit path at this stage. " +
|
|
118
|
+
"ENFORCED: each call requires a fresh get_promo_video for the same projectId/variantId immediately beforehand. " +
|
|
119
|
+
"SCHEMA REFERENCE: read the resource applaunchflow://schema/video-config for the full field-level reference — the six scene types and their content shapes, theme, TextStyle, ken burns, choreography preset ids, devices, overlays, and audio. " +
|
|
120
|
+
"Values outside the documented ranges fail validation and reject the whole update.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
projectId: z.string().uuid(),
|
|
123
|
+
variantId: z.string().uuid().optional(),
|
|
124
|
+
config: z
|
|
125
|
+
.record(z.any())
|
|
126
|
+
.describe("Full Remotion VideoConfig object — a whole-config replace, not a patch. " +
|
|
127
|
+
"Required: theme (colors + typography) and scenes (at least one; each scene is a discriminated union on `type`: hook | feature | text-only | closeup | multi-phone | cta, with a matching `content` shape). " +
|
|
128
|
+
"Optional: version, duration (seconds), audio, phoneId. " +
|
|
129
|
+
"All coordinates are percentages of the frame (0-100, 50 = centered), never pixels. " +
|
|
130
|
+
"Always start from the object returned by get_promo_video and mutate it — do not hand-build one. " +
|
|
131
|
+
"Full field reference including every scene's content fields and valid ranges: read the resource applaunchflow://schema/video-config."),
|
|
132
|
+
appName: z.string().optional(),
|
|
133
|
+
projectName: z.string().optional(),
|
|
134
|
+
readReceipt: z
|
|
135
|
+
.string()
|
|
136
|
+
.optional()
|
|
137
|
+
.describe("Hosted connector only: pass the readReceipt returned by the immediately preceding get_promo_video call."),
|
|
138
|
+
},
|
|
139
|
+
}, async (args) => {
|
|
140
|
+
try {
|
|
141
|
+
const receiptArgs = {
|
|
142
|
+
generationId: args.projectId,
|
|
143
|
+
variantId: args.variantId,
|
|
144
|
+
};
|
|
145
|
+
const hasReceipt = hostedMcpEnabled()
|
|
146
|
+
? verifyHostedReadReceipt(args.readReceipt, promoReceiptKey(args.projectId, args.variantId), client.credentials.token)
|
|
147
|
+
: promoVideoReadReceipts.has(receiptArgs);
|
|
148
|
+
if (!hasReceipt) {
|
|
149
|
+
return fail(new Error("Call get_promo_video first for this project/variant before update_promo_video. Direct editing is locked until the current state has been read."));
|
|
150
|
+
}
|
|
151
|
+
const { readReceipt: _readReceipt, ...updateArgs } = args;
|
|
152
|
+
const result = await client.updatePromoVideo(updateArgs);
|
|
153
|
+
promoVideoReadReceipts.consume(receiptArgs);
|
|
154
|
+
const editorUrl = buildPromoVideoEditorUrl(client, {
|
|
155
|
+
generationId: args.projectId,
|
|
156
|
+
variantId: args.variantId,
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: [
|
|
163
|
+
"Updated promo video.",
|
|
164
|
+
`Editor URL (already open — do NOT run \`open\` again): ${editorUrl}`,
|
|
165
|
+
"This update consumed the current read receipt. Call get_promo_video again before the next direct edit.",
|
|
166
|
+
].join("\n"),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
structuredContent: {
|
|
170
|
+
success: true,
|
|
171
|
+
data: {
|
|
172
|
+
result,
|
|
173
|
+
editorUrl,
|
|
174
|
+
nextEditRequiresFreshRead: true,
|
|
175
|
+
},
|
|
176
|
+
message: "Updated promo video",
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return fail(error);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
server.registerTool("clear_promo_video", {
|
|
185
|
+
title: "Clear Promo Video",
|
|
186
|
+
description: "Wipe the promo video config for a variant so the user can start over. Does not delete the variant itself.",
|
|
187
|
+
inputSchema: {
|
|
188
|
+
projectId: z.string().uuid(),
|
|
189
|
+
variantId: z.string().uuid().optional(),
|
|
190
|
+
},
|
|
191
|
+
}, async (args) => {
|
|
192
|
+
try {
|
|
193
|
+
return ok(await client.clearPromoVideo(args), "Cleared promo video");
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return fail(error);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { listPublicTemplateIds } from "../catalog.js";
|
|
4
|
+
import { openUrl, fail, ok, startProgressHeartbeat } from "./utils.js";
|
|
5
|
+
export function registerScreenshotTools(server, client) {
|
|
6
|
+
server.registerTool("prepare_screenshot_styles", {
|
|
7
|
+
title: "Prepare Personalized Screenshot Styles",
|
|
8
|
+
description: "Generate or reuse the project's personalized screenshot-style catalog before the user chooses a style. " +
|
|
9
|
+
"This prepares every screenshot template for phone, tablet, and desktop in one AI call and returns a catalogKey. " +
|
|
10
|
+
"Next call browse_templates with the returned templateIds, generationId, and catalogKey, then apply_screenshot_style with the selected templateId and catalogKey. " +
|
|
11
|
+
"Repeating this with the same app context and screenshot paths is a cache hit and does not regenerate AI content.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
generationId: z.string().uuid().describe("Project/generation UUID."),
|
|
14
|
+
selectedScreenshotPaths: z
|
|
15
|
+
.array(z.string().min(1))
|
|
16
|
+
.min(3)
|
|
17
|
+
.max(7)
|
|
18
|
+
.refine((paths) => new Set(paths).size === paths.length, {
|
|
19
|
+
message: "Screenshot paths must be unique",
|
|
20
|
+
})
|
|
21
|
+
.describe("3-7 unique project-relative paths from list_source_screenshots, in the desired story order."),
|
|
22
|
+
deviceType: z
|
|
23
|
+
.enum(["phone", "tablet", "desktop"])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Initial preview device. All three device layouts are prepared regardless. Defaults to phone."),
|
|
26
|
+
},
|
|
27
|
+
}, async ({ generationId, selectedScreenshotPaths, deviceType = "phone" }, extra) => {
|
|
28
|
+
const stopHeartbeat = startProgressHeartbeat(extra, "Preparing personalized screenshot styles for every template…");
|
|
29
|
+
try {
|
|
30
|
+
const result = await client.generateLayouts({
|
|
31
|
+
generationId,
|
|
32
|
+
selectedScreenshotPaths,
|
|
33
|
+
deviceType,
|
|
34
|
+
previewAllTemplates: true,
|
|
35
|
+
});
|
|
36
|
+
const templateIds = listPublicTemplateIds(result.templatePayloads);
|
|
37
|
+
if (!result.catalogKey || templateIds.length === 0) {
|
|
38
|
+
throw new Error("The screenshot catalog response did not include a cache key and templates");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: [
|
|
45
|
+
result.cacheHit
|
|
46
|
+
? "Reused the existing personalized screenshot-style catalog."
|
|
47
|
+
: "Prepared personalized screenshot styles for phone, tablet, and desktop.",
|
|
48
|
+
`Catalog key: ${result.catalogKey}`,
|
|
49
|
+
`Available template ids: ${templateIds.join(", ")}`,
|
|
50
|
+
"Next: call browse_templates with these templateIds, generationId, and this catalog key so the gallery shows the personalized results; then apply_screenshot_style with the selected id.",
|
|
51
|
+
].join("\n"),
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
structuredContent: {
|
|
55
|
+
success: true,
|
|
56
|
+
data: {
|
|
57
|
+
generationId,
|
|
58
|
+
catalogKey: result.catalogKey,
|
|
59
|
+
cacheHit: result.cacheHit === true,
|
|
60
|
+
templateIds,
|
|
61
|
+
devices: ["phone", "tablet", "desktop"],
|
|
62
|
+
selectedScreenshotPaths,
|
|
63
|
+
},
|
|
64
|
+
message: result.cacheHit
|
|
65
|
+
? "Reused personalized screenshot styles"
|
|
66
|
+
: "Prepared personalized screenshot styles",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return fail(error);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
stopHeartbeat();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
server.registerTool("apply_screenshot_style", {
|
|
78
|
+
title: "Apply Personalized Screenshot Style",
|
|
79
|
+
description: "Create a new screenshot variant from a previously prepared personalized style catalog without another AI generation. " +
|
|
80
|
+
"Use only after prepare_screenshot_styles and browse_templates. The new variant includes phone, tablet, and desktop layouts and opens in the editor.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
generationId: z.string().uuid(),
|
|
83
|
+
catalogKey: z.string().min(1).max(128),
|
|
84
|
+
templateId: z.string().min(1),
|
|
85
|
+
deviceType: z
|
|
86
|
+
.enum(["phone", "tablet", "desktop"])
|
|
87
|
+
.optional()
|
|
88
|
+
.describe("Device to show first in the editor. Defaults to phone."),
|
|
89
|
+
paletteMode: z
|
|
90
|
+
.enum(["v1", "v2"])
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Color palette variant. v1 is the original palette; v2 uses stronger color separation. Defaults to v1."),
|
|
93
|
+
},
|
|
94
|
+
}, async ({ generationId, catalogKey, templateId, deviceType = "phone", paletteMode = "v1", }, extra) => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await client.applyScreenshotTemplate({
|
|
97
|
+
generationId,
|
|
98
|
+
catalogKey,
|
|
99
|
+
templateId,
|
|
100
|
+
paletteMode,
|
|
101
|
+
});
|
|
102
|
+
const editorParams = new URLSearchParams({
|
|
103
|
+
projectId: generationId,
|
|
104
|
+
device: deviceType,
|
|
105
|
+
variantId: result.variantId,
|
|
106
|
+
});
|
|
107
|
+
if (result.detectedLanguage) {
|
|
108
|
+
editorParams.set("language", result.detectedLanguage);
|
|
109
|
+
}
|
|
110
|
+
const editorUrl = `${client.credentials.baseUrl}/editor?${editorParams.toString()}`;
|
|
111
|
+
await openUrl(server, editorUrl, "Opening the selected personalized screenshot style in the editor.", { signal: extra.signal });
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: [
|
|
117
|
+
`Applied screenshot style ${templateId} without another AI generation.`,
|
|
118
|
+
`Editor URL: ${editorUrl}`,
|
|
119
|
+
"IMPORTANT: Paste this exact editor URL in the reply so the user can open it.",
|
|
120
|
+
].join("\n"),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
structuredContent: {
|
|
124
|
+
success: true,
|
|
125
|
+
data: {
|
|
126
|
+
generationId,
|
|
127
|
+
variantId: result.variantId,
|
|
128
|
+
templateId,
|
|
129
|
+
editorUrl,
|
|
130
|
+
},
|
|
131
|
+
message: "Applied personalized screenshot style",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return fail(error);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
server.registerTool("generate_layouts", {
|
|
140
|
+
title: "Generate Layouts",
|
|
141
|
+
description: "Legacy direct generation for a single screenshot template. For the normal user-facing style chooser, prefer prepare_screenshot_styles → browse_templates → apply_screenshot_style so all personalized previews are generated once and the chosen style is applied from cache. " +
|
|
142
|
+
"Generate screenshot layouts for a project. " +
|
|
143
|
+
"For existing projects, pass generationId (same as projectId) — the API fetches screenshots from storage automatically. " +
|
|
144
|
+
"Do NOT pass screenshots when using generationId. " +
|
|
145
|
+
"Only pass metadata + screenshots (without generationId) for the ephemeral upload flow. " +
|
|
146
|
+
"Do NOT pass variantId — omit it so a new variant is always created. Never overwrite an existing variant. " +
|
|
147
|
+
"Do not use this tool for small edits to an existing layout; use transform_layout instead.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
generationId: z
|
|
150
|
+
.string()
|
|
151
|
+
.uuid()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("The project/generation UUID. When provided, the API loads screenshots from storage and saves results to DB. This is the primary way to call this tool for existing projects."),
|
|
154
|
+
projectId: z
|
|
155
|
+
.string()
|
|
156
|
+
.uuid()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Only needed for the upload flow (without generationId) to sign screenshot paths."),
|
|
159
|
+
metadata: z
|
|
160
|
+
.record(z.any())
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("App metadata. Required only when generationId is NOT provided (upload flow)."),
|
|
163
|
+
screenshots: z
|
|
164
|
+
.array(z.object({
|
|
165
|
+
path: z.string().optional(),
|
|
166
|
+
url: z.string().optional(),
|
|
167
|
+
filename: z.string().optional(),
|
|
168
|
+
}))
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("Screenshot list. Required only when generationId is NOT provided. Do NOT send when using generationId."),
|
|
171
|
+
templateId: z.string().optional(),
|
|
172
|
+
deviceType: z.enum(["phone", "tablet", "desktop"]).optional(),
|
|
173
|
+
variantId: z
|
|
174
|
+
.string()
|
|
175
|
+
.uuid()
|
|
176
|
+
.optional()
|
|
177
|
+
.describe("DO NOT pass this. Always omit so a new variant is created. Never overwrite existing variants."),
|
|
178
|
+
},
|
|
179
|
+
}, async (args, extra) => {
|
|
180
|
+
try {
|
|
181
|
+
const result = await client.generateLayouts(args);
|
|
182
|
+
const generationId = args.generationId || args.projectId || "";
|
|
183
|
+
const variantId = result.variantId || "";
|
|
184
|
+
const editorParams = new URLSearchParams({
|
|
185
|
+
projectId: generationId,
|
|
186
|
+
device: "phone",
|
|
187
|
+
});
|
|
188
|
+
if (variantId) {
|
|
189
|
+
editorParams.set("variantId", variantId);
|
|
190
|
+
}
|
|
191
|
+
if (result.detectedLanguage) {
|
|
192
|
+
editorParams.set("language", result.detectedLanguage);
|
|
193
|
+
}
|
|
194
|
+
const editorUrl = `${client.credentials.baseUrl}/editor?${editorParams.toString()}`;
|
|
195
|
+
await openUrl(server, editorUrl, "Opening the generated screenshot variant in the editor.", { signal: extra.signal });
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: [
|
|
201
|
+
"Generated layouts successfully.",
|
|
202
|
+
`Editor URL: ${editorUrl}`,
|
|
203
|
+
"IMPORTANT: Paste this exact editor URL in the reply so the user can open it.",
|
|
204
|
+
].join("\n"),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
structuredContent: {
|
|
208
|
+
success: true,
|
|
209
|
+
data: { ...result, editorUrl },
|
|
210
|
+
message: "Generated layouts",
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return fail(error);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
server.registerTool("list_source_screenshots", {
|
|
219
|
+
title: "List Template Source Screenshots",
|
|
220
|
+
description: "List all real source screenshots available to screenshot and social-graphics style generation, including project-relative path, signed preview URL, platform, and phone/tablet/desktop device type. No sample fallback is added. Use the returned paths to choose 3-7 inputs for prepare_screenshot_styles or prepare_social_graphics_styles.",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
projectId: z.string().uuid(),
|
|
223
|
+
},
|
|
224
|
+
}, async ({ projectId }) => {
|
|
225
|
+
try {
|
|
226
|
+
const result = await client.listProjectScreenshots(projectId);
|
|
227
|
+
const screenshots = result.paths.map((path, index) => ({
|
|
228
|
+
path,
|
|
229
|
+
signedUrl: result.screenshotUrls[index],
|
|
230
|
+
platform: result.platforms[index],
|
|
231
|
+
deviceType: result.deviceTypes[index],
|
|
232
|
+
}));
|
|
233
|
+
return ok({
|
|
234
|
+
projectId,
|
|
235
|
+
defaultPlatform: result.defaultPlatform,
|
|
236
|
+
screenshots,
|
|
237
|
+
}, `Listed ${screenshots.length} template source screenshots`);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
return fail(error);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
server.registerTool("list_screenshots", {
|
|
244
|
+
title: "List Screenshots",
|
|
245
|
+
description: "List uploaded screenshot paths for a project",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
projectId: z.string().uuid(),
|
|
248
|
+
deviceType: z.enum(["mobile", "tablet", "desktop"]).optional(),
|
|
249
|
+
platform: z.enum(["ios", "android"]).optional(),
|
|
250
|
+
},
|
|
251
|
+
}, async ({ projectId, deviceType, platform }) => {
|
|
252
|
+
try {
|
|
253
|
+
return ok(await client.listScreenshots({ projectId, deviceType, platform }), "Listed screenshots");
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
return fail(error);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
server.registerTool("view_screenshot", {
|
|
260
|
+
title: "View Screenshot",
|
|
261
|
+
description: "Fetch a screenshot image and return it for visual analysis. " +
|
|
262
|
+
"Use this to inspect screenshots — extract colors, read UI text, understand layout context, or identify visual elements. " +
|
|
263
|
+
"Pass projectId and the relative path from list_screenshots or get_layout (e.g. 'mobile/ios/1234-image.PNG').",
|
|
264
|
+
inputSchema: {
|
|
265
|
+
projectId: z.string().uuid().describe("The project UUID."),
|
|
266
|
+
path: z
|
|
267
|
+
.string()
|
|
268
|
+
.describe("Relative screenshot path (e.g. 'mobile/ios/1234-IMG.PNG') from list_screenshots or the layout's screenshot.path field."),
|
|
269
|
+
},
|
|
270
|
+
}, async ({ projectId, path }) => {
|
|
271
|
+
try {
|
|
272
|
+
const fullPath = `${projectId}/${path}`;
|
|
273
|
+
const previewUrl = `${client.credentials.baseUrl}/api/preview?path=${encodeURIComponent(fullPath)}&w=320`;
|
|
274
|
+
const headers = new Headers();
|
|
275
|
+
headers.set("Authorization", `Bearer ${client.credentials.token}`);
|
|
276
|
+
const response = await fetch(previewUrl, { headers });
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
throw new Error(`Failed to fetch image: ${response.status}`);
|
|
279
|
+
}
|
|
280
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
281
|
+
const mimeType = response.headers.get("content-type") || "image/png";
|
|
282
|
+
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: "image",
|
|
287
|
+
data: base64,
|
|
288
|
+
mimeType,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
structuredContent: {
|
|
292
|
+
success: true,
|
|
293
|
+
data: {
|
|
294
|
+
projectId,
|
|
295
|
+
path,
|
|
296
|
+
mimeType,
|
|
297
|
+
renderedAsImageContent: true,
|
|
298
|
+
},
|
|
299
|
+
message: "Fetched screenshot image",
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return fail(error);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|