applaunchflow 0.3.3 → 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/build/index.js
CHANGED
|
@@ -59,7 +59,7 @@ Screenshot workflows:
|
|
|
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.
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
});
|
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)
|
|
@@ -179,6 +200,9 @@ export function registerGraphicsTools(server, client) {
|
|
|
179
200
|
},
|
|
180
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,8 +228,11 @@ export function registerGraphicsTools(server, client) {
|
|
|
204
228
|
title,
|
|
205
229
|
generationId,
|
|
206
230
|
catalogKey,
|
|
231
|
+
applySelection: Boolean(generationId && catalogKey),
|
|
207
232
|
});
|
|
208
|
-
const opened = await openUrl(server, galleryUrl,
|
|
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 });
|
|
209
236
|
return {
|
|
210
237
|
content: [
|
|
211
238
|
{
|
|
@@ -215,7 +242,9 @@ export function registerGraphicsTools(server, client) {
|
|
|
215
242
|
? "Opened the social template gallery in the browser."
|
|
216
243
|
: "The client could not open the browser automatically; paste this exact gallery URL into the user-visible reply.",
|
|
217
244
|
`Social template gallery URL: ${galleryUrl}`,
|
|
218
|
-
|
|
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.",
|
|
219
248
|
].join("\n"),
|
|
220
249
|
},
|
|
221
250
|
{
|
|
@@ -234,6 +263,7 @@ export function registerGraphicsTools(server, client) {
|
|
|
234
263
|
format,
|
|
235
264
|
templateIds: filteredTemplateIds,
|
|
236
265
|
browserOpened: opened,
|
|
266
|
+
personalized: Boolean(generationId && catalogKey),
|
|
237
267
|
},
|
|
238
268
|
message: "Prepared social template gallery",
|
|
239
269
|
},
|
|
@@ -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
|
}
|