applaunchflow 0.3.2 → 0.3.4
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 +4 -2
- package/build/index.js +7 -6
- package/build/promovideo-picker.test.js +18 -0
- package/build/resources/data.js +4 -6
- package/build/social-template-previews.js +3 -0
- package/build/social-template-previews.test.js +17 -0
- package/build/template-previews.js +3 -0
- package/build/template-previews.test.js +18 -0
- package/build/tools/graphics.js +40 -6
- package/build/tools/localization.js +2 -2
- package/build/tools/promovideo.js +35 -18
- package/build/tools/screenshots.js +25 -4
- package/build/tools/templates.js +18 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,8 +86,10 @@ For App Store screenshots:
|
|
|
86
86
|
|
|
87
87
|
1. `list_source_screenshots`
|
|
88
88
|
2. `prepare_screenshot_styles` with 3-7 ordered paths
|
|
89
|
-
3.
|
|
90
|
-
|
|
89
|
+
3. The personalized gallery opens automatically. Compare each template's V1/V2 render and confirm the choice; the gallery creates the new variant from cache and opens the editor.
|
|
90
|
+
|
|
91
|
+
`browse_templates` can reopen a prepared catalog, and `apply_screenshot_style`
|
|
92
|
+
remains available when an API client supplies a template id directly.
|
|
91
93
|
|
|
92
94
|
For social graphics:
|
|
93
95
|
|
package/build/index.js
CHANGED
|
@@ -42,7 +42,7 @@ Schema references (MCP resources — read them, they are not loaded automaticall
|
|
|
42
42
|
Screenshot workflows:
|
|
43
43
|
- Entry point without a known project: ask whether the user wants to create a new app or edit an existing project. If they want existing, list/select projects. If they want new, create the project first.
|
|
44
44
|
- For the normal style-choice flow, call list_source_screenshots, choose 3-7 real screenshots in story order, then call prepare_screenshot_styles. This generates or reuses one personalized catalog containing every template for phone, tablet, and desktop.
|
|
45
|
-
-
|
|
45
|
+
- prepare_screenshot_styles opens the personalized gallery automatically. The user compares the real v1/v2 renders there; confirming a style creates a new variant from cache and opens the editor. Do not ask the user to paste a template id back into chat. Use browse_templates only to reopen the gallery, and apply_screenshot_style only when a template id was supplied directly in chat or by an API client. Never overwrite an existing variant.
|
|
46
46
|
- Use generate_layouts only for an explicitly requested legacy/direct single-template generation. Do not use it for the normal visual style chooser.
|
|
47
47
|
- For small, precise edits to existing known nodes, transform_layout can be used directly.
|
|
48
48
|
- For any composition-sensitive edit, inspect the current layout first with get_layout. This includes adding screens, reusing screenshots, changing screenshot placement, moving text, changing spacing, or anything that should match the existing visual system.
|
|
@@ -53,13 +53,13 @@ Screenshot workflows:
|
|
|
53
53
|
- When adding or editing elements, ensure text and screenshots do not overlap. Verify that positions place elements in distinct, non-conflicting areas of the canvas.
|
|
54
54
|
- After composition-sensitive edits, inspect the returned translation or re-fetch the layout before reporting success. If elements overlap or are poorly positioned, fix them before telling the user the edit is done.
|
|
55
55
|
- get_layout is mandatory before every direct transform_layout call. Do not edit a layout without a fresh read of the current state first.
|
|
56
|
-
-
|
|
56
|
+
- Never offer screenshot templates via text bullet points. prepare_screenshot_styles opens the personalized picker automatically. If browser opening is unsupported, show its exact fallback gallery URL. Do not wait for a selected template id because the picker applies the choice directly.
|
|
57
57
|
- When you need visual context about a screenshot (e.g. to extract colors, understand the app UI, or make context-specific edits), use view_screenshot to look at the actual image.
|
|
58
58
|
- After generating a new variant, include the editor URL in the reply.
|
|
59
59
|
|
|
60
60
|
Social graphics workflows (mirror the screenshot flow):
|
|
61
61
|
- Call list_source_screenshots, select 3-7 real screenshots in story order, then call prepare_social_graphics_styles. This generates or reuses every social template across all six formats in one catalog.
|
|
62
|
-
-
|
|
62
|
+
- prepare_social_graphics_styles opens the personalized gallery automatically. Confirming a style creates a fresh variant containing all six formats and opens the graphics editor. Do not ask the user to paste a template id back into chat. Use browse_social_templates only to reopen the gallery, and apply_social_graphics_style only when a template id was supplied directly in chat or by an API client.
|
|
63
63
|
- Use generate_graphics only for an explicitly requested legacy/direct single-template generation. Never overwrite an existing graphics variant.
|
|
64
64
|
- For edits to existing social graphics, ALWAYS call get_graphics_format first, then save_graphics_format. Mutate JSON for exactly one format in memory and save only that one format. The same get-before-edit receipt rule applies as for screenshots.
|
|
65
65
|
- Default to the variant's primary format unless the user explicitly asks to edit another format.
|
|
@@ -68,7 +68,8 @@ Social graphics workflows (mirror the screenshot flow):
|
|
|
68
68
|
- Use create_variant with contentType:"socialGraphics" or duplicate_variant to create a copy / fresh take without overwriting the current graphics variant.
|
|
69
69
|
|
|
70
70
|
Promo video workflows:
|
|
71
|
-
- generate_promo_video
|
|
71
|
+
- generate_promo_video prepares three transient, personalized candidates and opens the same dashboard picker used by the web app. No saved variant is created until the user chooses one. Do not summarize the three options in chat or ask for a candidate id.
|
|
72
|
+
- To replace an existing promo-video variant, pass replaceVariantId only after explicit user approval. The chosen candidate is created first and the old variant is retired only after the new one saves successfully, so replacement works safely at the plan limit.
|
|
72
73
|
- For edits to an existing promo video, ALWAYS call get_promo_video first to fetch the current config, mutate the config object in memory, then call update_promo_video with the full updated config. There is no granular transform tool for promo videos at this stage — full-config replace is the supported edit path. The same get-before-edit receipt rule as graphics applies: update_promo_video is locked until a fresh get_promo_video has been called for the same project/variant.
|
|
73
74
|
- Use clear_promo_video to wipe a variant's video config when the user wants to start over.
|
|
74
75
|
- Use create_variant with contentType:"promoVideo" or duplicate_variant for copies / A-B tests. duplicate_variant clones the source promo-video variant, including its config.
|
|
@@ -97,7 +98,7 @@ HOSTED CONNECTOR SAFETY RULES:
|
|
|
97
98
|
- Never reveal, repeat, log, or place OAuth access tokens, refresh tokens, authorization codes, PKCE verifiers, or read receipts in user-facing text.
|
|
98
99
|
- A readReceipt returned inside structured tool data is an opaque safety input. Pass it only to the matching edit tool, for the exact same project, variant, language, and format.
|
|
99
100
|
- Before deleting, clearing, overwriting, or replacing user content, require clear user intent for that exact action. Do not infer destructive intent from a broad request.
|
|
100
|
-
- Hosted gallery tools
|
|
101
|
+
- Hosted gallery tools request the MCP client to open the browser and always return the exact URL as a fallback. Personalized screenshot galleries apply the user's v1/v2 choice directly and open the new variant in the editor. Do not ask the user to relay the template id.
|
|
101
102
|
|
|
102
103
|
${SERVER_INSTRUCTIONS}
|
|
103
104
|
`.trim();
|
|
@@ -105,7 +106,7 @@ export function createAppLaunchFlowServer(credentials) {
|
|
|
105
106
|
const client = new AppLaunchFlowClient(credentials);
|
|
106
107
|
const server = new McpServer({
|
|
107
108
|
name: "applaunchflow-mcp",
|
|
108
|
-
version: "0.3.
|
|
109
|
+
version: "0.3.3",
|
|
109
110
|
}, {
|
|
110
111
|
instructions: HOSTED_SERVER_INSTRUCTIONS,
|
|
111
112
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildPromoVideoCandidateRequest, buildPromoVideoEditorUrl, } from "./tools/promovideo.js";
|
|
4
|
+
test("generate_promo_video requests three transient candidates", () => {
|
|
5
|
+
assert.deepEqual(buildPromoVideoCandidateRequest({ projectId: "project", message: "fresh" }), { projectId: "project", message: "fresh", candidateCount: 3 });
|
|
6
|
+
});
|
|
7
|
+
test("the MCP opens the dashboard candidate picker instead of a saved variant", () => {
|
|
8
|
+
const url = new URL(buildPromoVideoEditorUrl({
|
|
9
|
+
credentials: { baseUrl: "https://dashboard.applaunchflow.com" },
|
|
10
|
+
}, {
|
|
11
|
+
generationId: "11111111-1111-4111-8111-111111111111",
|
|
12
|
+
candidateKey: "candidate-key",
|
|
13
|
+
replaceVariantId: "22222222-2222-4222-8222-222222222222",
|
|
14
|
+
}));
|
|
15
|
+
assert.equal(url.pathname, "/promovideo");
|
|
16
|
+
assert.equal(url.searchParams.get("candidateKey"), "candidate-key");
|
|
17
|
+
assert.equal(url.searchParams.get("replaceVariantId"), "22222222-2222-4222-8222-222222222222");
|
|
18
|
+
});
|
package/build/resources/data.js
CHANGED
|
@@ -661,7 +661,7 @@ export const WORKFLOW_GUIDE_RESOURCE = {
|
|
|
661
661
|
preferredSteps: [
|
|
662
662
|
"browse_templates",
|
|
663
663
|
],
|
|
664
|
-
notes: "Use browse_templates
|
|
664
|
+
notes: "Use browse_templates for static discovery or to reopen a prepared catalog. It asks the MCP client to open the hosted visual gallery automatically. Keep the full template catalog available unless the user asks for a shortlist.",
|
|
665
665
|
},
|
|
666
666
|
{
|
|
667
667
|
name: "Start screenshot work",
|
|
@@ -677,10 +677,8 @@ export const WORKFLOW_GUIDE_RESOURCE = {
|
|
|
677
677
|
preferredSteps: [
|
|
678
678
|
"list_source_screenshots",
|
|
679
679
|
"prepare_screenshot_styles",
|
|
680
|
-
"browse_templates",
|
|
681
|
-
"apply_screenshot_style",
|
|
682
680
|
],
|
|
683
|
-
notes: "Select 3-7 real screenshots in story order and prepare the personalized catalog once.
|
|
681
|
+
notes: "Select 3-7 real screenshots in story order and prepare the personalized catalog once. Preparation opens the personalized v1/v2 gallery automatically; confirming there creates the cached selection as a new phone, tablet, and desktop variant without a second AI call.",
|
|
684
682
|
},
|
|
685
683
|
{
|
|
686
684
|
name: "Prepare and apply personalized social graphics",
|
|
@@ -714,9 +712,9 @@ export const WORKFLOW_GUIDE_RESOURCE = {
|
|
|
714
712
|
"Do not force menu-based interaction when the user already gave a concrete natural-language edit request.",
|
|
715
713
|
"Do not reduce the template catalog to a tiny recommendation list unless the user explicitly asks for that.",
|
|
716
714
|
"Do not describe templates without showing preview resources when template previews are available.",
|
|
717
|
-
"Do not read template previews one by one when browse_templates can
|
|
715
|
+
"Do not read template previews one by one when prepare_screenshot_styles or browse_templates can open the full gallery.",
|
|
718
716
|
"Do not begin a screenshot session with template browsing before the user has chosen create new app or edit existing project.",
|
|
719
|
-
"Do not use legacy generate_layouts or generate_graphics for the normal style chooser. Prepare a personalized catalog and apply the selected cached style
|
|
717
|
+
"Do not use legacy generate_layouts or generate_graphics for the normal style chooser. Prepare a personalized catalog and let its gallery apply the selected cached screenshot style.",
|
|
720
718
|
"Do not invent fresh x/y positions, widths, screenshot scale values, or headline styling for new screens when the user wants to preserve the current design language.",
|
|
721
719
|
"Do not report success for new screens or composition edits without checking that the new nodes match the surrounding screens and do not overlap key content.",
|
|
722
720
|
],
|
|
@@ -40,6 +40,9 @@ export function buildSocialTemplateGalleryUrl(baseUrl, options) {
|
|
|
40
40
|
url.searchParams.set("generationId", options.generationId);
|
|
41
41
|
url.searchParams.set("catalogKey", options.catalogKey);
|
|
42
42
|
}
|
|
43
|
+
if (options?.applySelection) {
|
|
44
|
+
url.searchParams.set("action", "apply");
|
|
45
|
+
}
|
|
43
46
|
return url.toString();
|
|
44
47
|
}
|
|
45
48
|
function decorateSocialTemplate(template, baseUrl) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildSocialTemplateGalleryUrl } from "./social-template-previews.js";
|
|
4
|
+
test("personalized social gallery URLs use the direct-apply flow", () => {
|
|
5
|
+
const url = new URL(buildSocialTemplateGalleryUrl("https://dashboard.applaunchflow.com", {
|
|
6
|
+
format: "og",
|
|
7
|
+
templateIds: ["social-orbit", "social-dark"],
|
|
8
|
+
generationId: "11111111-1111-4111-8111-111111111111",
|
|
9
|
+
catalogKey: "catalog-key",
|
|
10
|
+
applySelection: true,
|
|
11
|
+
}));
|
|
12
|
+
assert.equal(url.pathname, "/template-gallery");
|
|
13
|
+
assert.equal(url.searchParams.get("kind"), "social");
|
|
14
|
+
assert.equal(url.searchParams.get("format"), "og");
|
|
15
|
+
assert.equal(url.searchParams.get("ids"), "social-orbit,social-dark");
|
|
16
|
+
assert.equal(url.searchParams.get("action"), "apply");
|
|
17
|
+
});
|
|
@@ -39,6 +39,9 @@ export function buildTemplateGalleryUrl(baseUrl, options) {
|
|
|
39
39
|
url.searchParams.set("generationId", options.generationId);
|
|
40
40
|
url.searchParams.set("catalogKey", options.catalogKey);
|
|
41
41
|
}
|
|
42
|
+
if (options?.applySelection) {
|
|
43
|
+
url.searchParams.set("action", "apply");
|
|
44
|
+
}
|
|
42
45
|
return url.toString();
|
|
43
46
|
}
|
|
44
47
|
function decorateTemplate(template, baseUrl) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildTemplateGalleryUrl } from "./template-previews.js";
|
|
4
|
+
test("personalized gallery URLs carry the generated catalog and direct-apply action", () => {
|
|
5
|
+
const url = new URL(buildTemplateGalleryUrl("https://dashboard.applaunchflow.com", {
|
|
6
|
+
deviceType: "phone",
|
|
7
|
+
templateIds: ["bold-frame", "minimal-clean"],
|
|
8
|
+
generationId: "11111111-1111-4111-8111-111111111111",
|
|
9
|
+
catalogKey: "catalog-key",
|
|
10
|
+
applySelection: true,
|
|
11
|
+
}));
|
|
12
|
+
assert.equal(url.pathname, "/template-gallery");
|
|
13
|
+
assert.equal(url.searchParams.get("device"), "phone");
|
|
14
|
+
assert.equal(url.searchParams.get("ids"), "bold-frame,minimal-clean");
|
|
15
|
+
assert.equal(url.searchParams.get("generationId"), "11111111-1111-4111-8111-111111111111");
|
|
16
|
+
assert.equal(url.searchParams.get("catalogKey"), "catalog-key");
|
|
17
|
+
assert.equal(url.searchParams.get("action"), "apply");
|
|
18
|
+
});
|
package/build/tools/graphics.js
CHANGED
|
@@ -17,7 +17,8 @@ export function registerGraphicsTools(server, client) {
|
|
|
17
17
|
title: "Prepare Personalized Social Graphics Styles",
|
|
18
18
|
description: "Generate or reuse the project's personalized social-graphics style catalog before the user chooses a style. " +
|
|
19
19
|
"This prepares every social template across OG image, social post, Instagram story, Play Store feature graphic, X banner, and LinkedIn banner in one AI call and returns a catalogKey. " +
|
|
20
|
-
"
|
|
20
|
+
"The personalized gallery opens automatically and creates the chosen variant directly in the graphics editor. " +
|
|
21
|
+
"Repeating the same app context and screenshot paths reuses the cache.",
|
|
21
22
|
inputSchema: {
|
|
22
23
|
generationId: z.string().uuid(),
|
|
23
24
|
selectedScreenshotPaths: z
|
|
@@ -46,6 +47,14 @@ export function registerGraphicsTools(server, client) {
|
|
|
46
47
|
if (!result.catalogKey || templateIds.length === 0) {
|
|
47
48
|
throw new Error("The social graphics catalog response did not include a cache key and templates");
|
|
48
49
|
}
|
|
50
|
+
const galleryUrl = buildSocialTemplateGalleryUrl(client.credentials.baseUrl, {
|
|
51
|
+
format: primaryFormat,
|
|
52
|
+
templateIds,
|
|
53
|
+
generationId,
|
|
54
|
+
catalogKey: result.catalogKey,
|
|
55
|
+
applySelection: true,
|
|
56
|
+
});
|
|
57
|
+
const opened = await openUrl(server, galleryUrl, "Choose a personalized social-graphics style, then create the new variant in the editor.", { signal: extra.signal });
|
|
49
58
|
return {
|
|
50
59
|
content: [
|
|
51
60
|
{
|
|
@@ -56,9 +65,19 @@ export function registerGraphicsTools(server, client) {
|
|
|
56
65
|
: "Prepared personalized social graphics across all six formats.",
|
|
57
66
|
`Catalog key: ${result.catalogKey}`,
|
|
58
67
|
`Available template ids: ${templateIds.join(", ")}`,
|
|
59
|
-
|
|
68
|
+
opened
|
|
69
|
+
? "Opened the personalized social-graphics gallery in the browser."
|
|
70
|
+
: `Open the personalized social-graphics gallery: ${galleryUrl}`,
|
|
71
|
+
"Confirming a style creates a new variant containing all six formats and opens it in the graphics editor; no template id needs to be pasted back into chat.",
|
|
60
72
|
].join("\n"),
|
|
61
73
|
},
|
|
74
|
+
{
|
|
75
|
+
type: "resource_link",
|
|
76
|
+
uri: galleryUrl,
|
|
77
|
+
name: "Open Personalized Social Graphics Gallery",
|
|
78
|
+
mimeType: "text/html",
|
|
79
|
+
description: "Personalized social-graphics previews with direct variant creation.",
|
|
80
|
+
},
|
|
62
81
|
],
|
|
63
82
|
structuredContent: {
|
|
64
83
|
success: true,
|
|
@@ -70,6 +89,8 @@ export function registerGraphicsTools(server, client) {
|
|
|
70
89
|
formats: [...SOCIAL_FORMATS],
|
|
71
90
|
primaryFormat,
|
|
72
91
|
selectedScreenshotPaths,
|
|
92
|
+
galleryUrl,
|
|
93
|
+
browserOpened: opened,
|
|
73
94
|
},
|
|
74
95
|
message: result.cacheHit
|
|
75
96
|
? "Reused personalized social graphics styles"
|
|
@@ -147,7 +168,7 @@ export function registerGraphicsTools(server, client) {
|
|
|
147
168
|
});
|
|
148
169
|
server.registerTool("browse_social_templates", {
|
|
149
170
|
title: "Browse & Select Social Template",
|
|
150
|
-
description: "
|
|
171
|
+
description: "Open the visual social style gallery. With generationId and catalogKey from prepare_social_graphics_styles it renders personalized previews and creates the selected variant directly; without them it provides static style discovery. Never offer social templates via text or AskUserQuestion.",
|
|
151
172
|
inputSchema: {
|
|
152
173
|
format: z
|
|
153
174
|
.enum(SOCIAL_FORMATS)
|
|
@@ -177,8 +198,11 @@ export function registerGraphicsTools(server, client) {
|
|
|
177
198
|
.optional()
|
|
178
199
|
.describe("Catalog key from prepare_social_graphics_styles. Pass together with generationId."),
|
|
179
200
|
},
|
|
180
|
-
}, async ({ format = "og", templateIds, selectedTemplateId, title, generationId, catalogKey, }) => {
|
|
201
|
+
}, async ({ format = "og", templateIds, selectedTemplateId, title, generationId, catalogKey, }, extra) => {
|
|
181
202
|
try {
|
|
203
|
+
if (Boolean(generationId) !== Boolean(catalogKey)) {
|
|
204
|
+
return fail(new Error("generationId and catalogKey must be passed together to open personalized previews."));
|
|
205
|
+
}
|
|
182
206
|
const payload = decorateSocialTemplatePayload(await client.listSocialTemplates(), client.credentials.baseUrl);
|
|
183
207
|
const availableIds = new Set(payload.templates.map((t) => t.id));
|
|
184
208
|
const filteredTemplateIds = templateIds?.filter((id) => availableIds.has(id)) || [];
|
|
@@ -204,15 +228,23 @@ export function registerGraphicsTools(server, client) {
|
|
|
204
228
|
title,
|
|
205
229
|
generationId,
|
|
206
230
|
catalogKey,
|
|
231
|
+
applySelection: Boolean(generationId && catalogKey),
|
|
207
232
|
});
|
|
233
|
+
const opened = await openUrl(server, galleryUrl, generationId && catalogKey
|
|
234
|
+
? "Choose a personalized social-graphics style, then create the new variant in the editor."
|
|
235
|
+
: "Browse social-graphics styles in AppLaunchFlow.", { signal: extra.signal });
|
|
208
236
|
return {
|
|
209
237
|
content: [
|
|
210
238
|
{
|
|
211
239
|
type: "text",
|
|
212
240
|
text: [
|
|
213
|
-
|
|
241
|
+
opened
|
|
242
|
+
? "Opened the social template gallery in the browser."
|
|
243
|
+
: "The client could not open the browser automatically; paste this exact gallery URL into the user-visible reply.",
|
|
214
244
|
`Social template gallery URL: ${galleryUrl}`,
|
|
215
|
-
|
|
245
|
+
generationId && catalogKey
|
|
246
|
+
? "The gallery shows the personalized renders. Confirming a style creates the variant across all six formats and opens the graphics editor directly."
|
|
247
|
+
: "Static discovery mode copies the chosen template id.",
|
|
216
248
|
].join("\n"),
|
|
217
249
|
},
|
|
218
250
|
{
|
|
@@ -230,6 +262,8 @@ export function registerGraphicsTools(server, client) {
|
|
|
230
262
|
userFacingUrl: galleryUrl,
|
|
231
263
|
format,
|
|
232
264
|
templateIds: filteredTemplateIds,
|
|
265
|
+
browserOpened: opened,
|
|
266
|
+
personalized: Boolean(generationId && catalogKey),
|
|
233
267
|
},
|
|
234
268
|
message: "Prepared social template gallery",
|
|
235
269
|
},
|
|
@@ -11,8 +11,8 @@ export function registerLocalizationTools(server, client) {
|
|
|
11
11
|
description: "Translate screenshot layouts into one or more target languages using AI. " +
|
|
12
12
|
"This is the PREFERRED way to localize screenshots — do NOT manually edit text nodes for translation. " +
|
|
13
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
|
|
15
|
-
"Pass the variantId
|
|
14
|
+
"Requires a source screenshot layout to already exist — created by the personalized gallery, apply_screenshot_style, or generate_layouts. " +
|
|
15
|
+
"Pass the source variantId when available to translate that exact variant; if omitted, the active variant is used.",
|
|
16
16
|
inputSchema: {
|
|
17
17
|
generationId: z.string().uuid().describe("The project/generation UUID."),
|
|
18
18
|
variantId: z
|
|
@@ -1,62 +1,79 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createHostedReadReceipt, createReadReceiptStore, fail, hostedMcpEnabled, ok, openUrl, verifyHostedReadReceipt, } from "./utils.js";
|
|
3
3
|
const promoReceiptKey = (generationId, variantId) => ["promo-video", generationId, variantId || "active"].join("::");
|
|
4
|
-
function buildPromoVideoEditorUrl(client, args) {
|
|
4
|
+
export function buildPromoVideoEditorUrl(client, args) {
|
|
5
5
|
const params = new URLSearchParams({ projectId: args.generationId });
|
|
6
6
|
if (args.variantId) {
|
|
7
7
|
params.set("variantId", args.variantId);
|
|
8
8
|
}
|
|
9
|
+
if (args.candidateKey) {
|
|
10
|
+
params.set("candidateKey", args.candidateKey);
|
|
11
|
+
}
|
|
12
|
+
if (args.replaceVariantId) {
|
|
13
|
+
params.set("replaceVariantId", args.replaceVariantId);
|
|
14
|
+
}
|
|
9
15
|
return `${client.credentials.baseUrl}/promovideo?${params.toString()}`;
|
|
10
16
|
}
|
|
17
|
+
export function buildPromoVideoCandidateRequest(args) {
|
|
18
|
+
return { ...args, candidateCount: 3 };
|
|
19
|
+
}
|
|
11
20
|
export function registerPromoVideoTools(server, client) {
|
|
12
21
|
const promoVideoReadReceipts = createReadReceiptStore();
|
|
13
22
|
server.registerTool("generate_promo_video", {
|
|
14
23
|
title: "Generate Promo Video",
|
|
15
|
-
description: "
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"Use
|
|
19
|
-
"Use the optional message field to pass natural-language regeneration feedback when iterating on an existing variant.",
|
|
24
|
+
description: "Generate or reuse three transient promo-video candidates from the project's screenshots. " +
|
|
25
|
+
"The dashboard picker opens automatically so the user can preview all three; only the chosen candidate becomes a saved variant. " +
|
|
26
|
+
"Use replaceVariantId only when the user explicitly chose to replace that existing variant, which also allows a safe swap at the plan limit. " +
|
|
27
|
+
"Use selectedScreenshotPaths to constrain which uploaded screenshots feed the candidates.",
|
|
20
28
|
inputSchema: {
|
|
21
29
|
projectId: z.string().uuid().describe("Project / generation UUID."),
|
|
22
30
|
message: z
|
|
23
31
|
.string()
|
|
24
32
|
.optional()
|
|
25
|
-
.describe("Optional
|
|
26
|
-
|
|
33
|
+
.describe("Optional creative direction applied to all three candidates."),
|
|
34
|
+
replaceVariantId: z
|
|
27
35
|
.string()
|
|
28
36
|
.uuid()
|
|
29
37
|
.optional()
|
|
30
|
-
.describe("
|
|
38
|
+
.describe("Existing promo-video variant to replace after the user chooses a candidate. Only pass with explicit user approval."),
|
|
39
|
+
selectedScreenshotPaths: z
|
|
40
|
+
.array(z.string().min(1))
|
|
41
|
+
.min(3)
|
|
42
|
+
.max(7)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Optional project-relative phone screenshot paths from list_source_screenshots, in story order."),
|
|
31
45
|
selectedScreenshotIndices: z
|
|
32
46
|
.array(z.number().int().min(0))
|
|
33
47
|
.optional()
|
|
34
|
-
.describe("
|
|
48
|
+
.describe("Legacy screenshot positions. Prefer selectedScreenshotPaths."),
|
|
35
49
|
},
|
|
36
50
|
}, async (args, extra) => {
|
|
37
51
|
try {
|
|
38
|
-
const result = await client.generatePromoVideo(args);
|
|
39
|
-
|
|
52
|
+
const result = await client.generatePromoVideo(buildPromoVideoCandidateRequest(args));
|
|
53
|
+
if (!result?.candidateKey || result?.candidates?.length !== 3) {
|
|
54
|
+
throw new Error("Promo video generation did not return a reusable three-option picker");
|
|
55
|
+
}
|
|
40
56
|
const editorUrl = buildPromoVideoEditorUrl(client, {
|
|
41
57
|
generationId: args.projectId,
|
|
42
|
-
|
|
58
|
+
candidateKey: result.candidateKey,
|
|
59
|
+
replaceVariantId: args.replaceVariantId,
|
|
43
60
|
});
|
|
44
|
-
await openUrl(server, editorUrl, "
|
|
61
|
+
await openUrl(server, editorUrl, "Compare three personalized promo-video candidates and choose which one to create.", { signal: extra.signal });
|
|
45
62
|
return {
|
|
46
63
|
content: [
|
|
47
64
|
{
|
|
48
65
|
type: "text",
|
|
49
66
|
text: [
|
|
50
|
-
"
|
|
51
|
-
`
|
|
52
|
-
"
|
|
67
|
+
"Prepared three personalized promo-video candidates without creating a saved variant yet.",
|
|
68
|
+
`Candidate picker URL: ${editorUrl}`,
|
|
69
|
+
"The dashboard picker previews all three options. Choosing one creates that variant and opens it in the promo-video editor.",
|
|
53
70
|
].join("\n"),
|
|
54
71
|
},
|
|
55
72
|
],
|
|
56
73
|
structuredContent: {
|
|
57
74
|
success: true,
|
|
58
75
|
data: { ...result, editorUrl },
|
|
59
|
-
message: "
|
|
76
|
+
message: "Prepared promo video candidates",
|
|
60
77
|
},
|
|
61
78
|
};
|
|
62
79
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { listPublicTemplateIds } from "../catalog.js";
|
|
4
|
+
import { buildTemplateGalleryUrl } from "../template-previews.js";
|
|
4
5
|
import { openUrl, fail, ok, startProgressHeartbeat } from "./utils.js";
|
|
5
6
|
export function registerScreenshotTools(server, client) {
|
|
6
7
|
server.registerTool("prepare_screenshot_styles", {
|
|
7
8
|
title: "Prepare Personalized Screenshot Styles",
|
|
8
9
|
description: "Generate or reuse the project's personalized screenshot-style catalog before the user chooses a style. " +
|
|
9
10
|
"This prepares every screenshot template for phone, tablet, and desktop in one AI call and returns a catalogKey. " +
|
|
10
|
-
"
|
|
11
|
+
"The personalized gallery opens automatically, shows the real v1/v2 renders, and creates the chosen variant directly in the browser. " +
|
|
11
12
|
"Repeating this with the same app context and screenshot paths is a cache hit and does not regenerate AI content.",
|
|
12
13
|
inputSchema: {
|
|
13
14
|
generationId: z.string().uuid().describe("Project/generation UUID."),
|
|
@@ -37,6 +38,14 @@ export function registerScreenshotTools(server, client) {
|
|
|
37
38
|
if (!result.catalogKey || templateIds.length === 0) {
|
|
38
39
|
throw new Error("The screenshot catalog response did not include a cache key and templates");
|
|
39
40
|
}
|
|
41
|
+
const galleryUrl = buildTemplateGalleryUrl(client.credentials.baseUrl, {
|
|
42
|
+
deviceType,
|
|
43
|
+
templateIds,
|
|
44
|
+
generationId,
|
|
45
|
+
catalogKey: result.catalogKey,
|
|
46
|
+
applySelection: true,
|
|
47
|
+
});
|
|
48
|
+
const opened = await openUrl(server, galleryUrl, "Choose a personalized screenshot style. Compare v1 and v2, then create the new variant in the editor.", { signal: extra.signal });
|
|
40
49
|
return {
|
|
41
50
|
content: [
|
|
42
51
|
{
|
|
@@ -47,9 +56,19 @@ export function registerScreenshotTools(server, client) {
|
|
|
47
56
|
: "Prepared personalized screenshot styles for phone, tablet, and desktop.",
|
|
48
57
|
`Catalog key: ${result.catalogKey}`,
|
|
49
58
|
`Available template ids: ${templateIds.join(", ")}`,
|
|
50
|
-
|
|
59
|
+
opened
|
|
60
|
+
? "Opened the personalized style gallery in the browser."
|
|
61
|
+
: `Open the personalized style gallery: ${galleryUrl}`,
|
|
62
|
+
"The gallery renders v1 and v2 from this catalog. Confirming a style creates a new variant and opens it in the editor; no template id needs to be pasted back into chat.",
|
|
51
63
|
].join("\n"),
|
|
52
64
|
},
|
|
65
|
+
{
|
|
66
|
+
type: "resource_link",
|
|
67
|
+
uri: galleryUrl,
|
|
68
|
+
name: "Open Personalized Style Gallery",
|
|
69
|
+
mimeType: "text/html",
|
|
70
|
+
description: "Personalized screenshot previews with v1/v2 palette choices and direct variant creation.",
|
|
71
|
+
},
|
|
53
72
|
],
|
|
54
73
|
structuredContent: {
|
|
55
74
|
success: true,
|
|
@@ -60,6 +79,8 @@ export function registerScreenshotTools(server, client) {
|
|
|
60
79
|
templateIds,
|
|
61
80
|
devices: ["phone", "tablet", "desktop"],
|
|
62
81
|
selectedScreenshotPaths,
|
|
82
|
+
galleryUrl,
|
|
83
|
+
browserOpened: opened,
|
|
63
84
|
},
|
|
64
85
|
message: result.cacheHit
|
|
65
86
|
? "Reused personalized screenshot styles"
|
|
@@ -77,7 +98,7 @@ export function registerScreenshotTools(server, client) {
|
|
|
77
98
|
server.registerTool("apply_screenshot_style", {
|
|
78
99
|
title: "Apply Personalized Screenshot Style",
|
|
79
100
|
description: "Create a new screenshot variant from a previously prepared personalized style catalog without another AI generation. " +
|
|
80
|
-
"
|
|
101
|
+
"Fallback for applying a template id supplied directly in chat or by an API client. The normal prepare_screenshot_styles flow now lets the user compare v1/v2 and create the variant directly in the browser. The new variant includes phone, tablet, and desktop layouts and opens in the editor.",
|
|
81
102
|
inputSchema: {
|
|
82
103
|
generationId: z.string().uuid(),
|
|
83
104
|
catalogKey: z.string().min(1).max(128),
|
|
@@ -138,7 +159,7 @@ export function registerScreenshotTools(server, client) {
|
|
|
138
159
|
});
|
|
139
160
|
server.registerTool("generate_layouts", {
|
|
140
161
|
title: "Generate Layouts",
|
|
141
|
-
description: "Legacy direct generation for a single screenshot template. For the normal user-facing style chooser, prefer prepare_screenshot_styles
|
|
162
|
+
description: "Legacy direct generation for a single screenshot template. For the normal user-facing style chooser, prefer prepare_screenshot_styles so all personalized v1/v2 previews are generated once and the browser applies the chosen style from cache. " +
|
|
142
163
|
"Generate screenshot layouts for a project. " +
|
|
143
164
|
"For existing projects, pass generationId (same as projectId) — the API fetches screenshots from storage automatically. " +
|
|
144
165
|
"Do NOT pass screenshots when using generationId. " +
|
package/build/tools/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { buildTemplateGalleryUrl, TEMPLATE_PREVIEW_DEVICE_TYPES, decorateTemplatePayload, } from "../template-previews.js";
|
|
3
|
-
import { fail } from "./utils.js";
|
|
3
|
+
import { fail, openUrl } from "./utils.js";
|
|
4
4
|
function formatTemplateLine(template, deviceType) {
|
|
5
5
|
const parts = [
|
|
6
6
|
`${template.name} (${template.id})`,
|
|
@@ -98,7 +98,7 @@ function buildTemplateDetailsResult(payload) {
|
|
|
98
98
|
export function registerTemplateTools(server, client) {
|
|
99
99
|
server.registerTool("browse_templates", {
|
|
100
100
|
title: "Browse & Select Template",
|
|
101
|
-
description: "
|
|
101
|
+
description: "Open the visual style gallery. With generationId and catalogKey from prepare_screenshot_styles it renders personalized v1/v2 previews and creates the selected variant directly; without them it provides static style discovery. Never offer templates via text or AskUserQuestion.",
|
|
102
102
|
inputSchema: {
|
|
103
103
|
deviceType: z
|
|
104
104
|
.enum(TEMPLATE_PREVIEW_DEVICE_TYPES)
|
|
@@ -128,8 +128,11 @@ export function registerTemplateTools(server, client) {
|
|
|
128
128
|
.optional()
|
|
129
129
|
.describe("Catalog key from prepare_screenshot_styles. Pass together with generationId."),
|
|
130
130
|
},
|
|
131
|
-
}, async ({ deviceType = "phone", templateIds, selectedTemplateId, title, generationId, catalogKey, }) => {
|
|
131
|
+
}, async ({ deviceType = "phone", templateIds, selectedTemplateId, title, generationId, catalogKey, }, extra) => {
|
|
132
132
|
try {
|
|
133
|
+
if (Boolean(generationId) !== Boolean(catalogKey)) {
|
|
134
|
+
return fail(new Error("generationId and catalogKey must be passed together to open personalized previews."));
|
|
135
|
+
}
|
|
133
136
|
const payload = decorateTemplatePayload(await client.listTemplates(), client.credentials.baseUrl);
|
|
134
137
|
const availableIds = new Set(payload.templates.map((template) => template.id));
|
|
135
138
|
const filteredTemplateIds = templateIds?.filter((templateId) => availableIds.has(templateId)) || [];
|
|
@@ -156,16 +159,23 @@ export function registerTemplateTools(server, client) {
|
|
|
156
159
|
title,
|
|
157
160
|
generationId,
|
|
158
161
|
catalogKey,
|
|
162
|
+
applySelection: Boolean(generationId && catalogKey),
|
|
159
163
|
});
|
|
164
|
+
const opened = await openUrl(server, galleryUrl, generationId && catalogKey
|
|
165
|
+
? "Choose a personalized screenshot style. Compare v1 and v2, then create the new variant in the editor."
|
|
166
|
+
: "Browse screenshot styles in AppLaunchFlow.", { signal: extra.signal });
|
|
160
167
|
return {
|
|
161
168
|
content: [
|
|
162
169
|
{
|
|
163
170
|
type: "text",
|
|
164
171
|
text: [
|
|
165
|
-
|
|
172
|
+
opened
|
|
173
|
+
? "Opened the template gallery in the browser."
|
|
174
|
+
: "The client could not open the browser automatically; paste this exact gallery URL into the user-visible reply.",
|
|
166
175
|
`Template gallery URL: ${galleryUrl}`,
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
generationId && catalogKey
|
|
177
|
+
? "The gallery shows the personalized v1/v2 renders. Confirming a style creates the variant and opens the editor directly."
|
|
178
|
+
: "Static discovery mode copies the chosen template id.",
|
|
169
179
|
].join("\n"),
|
|
170
180
|
},
|
|
171
181
|
{
|
|
@@ -183,6 +193,8 @@ export function registerTemplateTools(server, client) {
|
|
|
183
193
|
userFacingUrl: galleryUrl,
|
|
184
194
|
deviceType,
|
|
185
195
|
templateIds: filteredTemplateIds,
|
|
196
|
+
browserOpened: opened,
|
|
197
|
+
personalized: Boolean(generationId && catalogKey),
|
|
186
198
|
},
|
|
187
199
|
message: "Prepared template gallery",
|
|
188
200
|
},
|