@hubfluencer/mcp 0.7.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/dist/index.js +197 -37
- package/package.json +1 -1
- package/src/core.ts +22 -1
- package/src/index.ts +494 -49
- package/src/output-schemas.ts +8 -0
package/dist/index.js
CHANGED
|
@@ -29489,7 +29489,7 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29489
29489
|
const videoUrl = latest.video_url ?? null;
|
|
29490
29490
|
if (kind === "short") {
|
|
29491
29491
|
const stage = d.stage ?? "unknown";
|
|
29492
|
-
|
|
29492
|
+
const status = {
|
|
29493
29493
|
kind,
|
|
29494
29494
|
slug,
|
|
29495
29495
|
stage,
|
|
@@ -29498,6 +29498,13 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29498
29498
|
video_url: videoUrl,
|
|
29499
29499
|
error: d.error_message ?? null
|
|
29500
29500
|
};
|
|
29501
|
+
if (typeof d.latest_render_free === "boolean") {
|
|
29502
|
+
status.latest_render_free = d.latest_render_free;
|
|
29503
|
+
}
|
|
29504
|
+
if (typeof d.last_free_rerender_failed === "boolean") {
|
|
29505
|
+
status.last_free_rerender_failed = d.last_free_rerender_failed;
|
|
29506
|
+
}
|
|
29507
|
+
return status;
|
|
29501
29508
|
}
|
|
29502
29509
|
if (kind === "tracking") {
|
|
29503
29510
|
const result = asRecord(d.result);
|
|
@@ -30120,7 +30127,7 @@ function readStoredCredentials() {
|
|
|
30120
30127
|
// package.json
|
|
30121
30128
|
var package_default = {
|
|
30122
30129
|
name: "@hubfluencer/mcp",
|
|
30123
|
-
version: "0.
|
|
30130
|
+
version: "0.8.2",
|
|
30124
30131
|
description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
30125
30132
|
license: "MIT",
|
|
30126
30133
|
author: "Monocursive <contact@monocursive.com>",
|
|
@@ -30429,7 +30436,9 @@ var getStatusOutput = exports_external.object({
|
|
|
30429
30436
|
terminal: exports_external.boolean().optional(),
|
|
30430
30437
|
ready: exports_external.boolean().optional(),
|
|
30431
30438
|
video_url: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30432
|
-
error: exports_external.union([exports_external.string(), exports_external.null()]).optional()
|
|
30439
|
+
error: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30440
|
+
latest_render_free: exports_external.boolean().optional(),
|
|
30441
|
+
last_free_rerender_failed: exports_external.boolean().optional()
|
|
30433
30442
|
}).passthrough();
|
|
30434
30443
|
var waitForCompletionOutput = exports_external.object({
|
|
30435
30444
|
kind: exports_external.string().optional(),
|
|
@@ -30439,6 +30448,8 @@ var waitForCompletionOutput = exports_external.object({
|
|
|
30439
30448
|
ready: exports_external.boolean().optional(),
|
|
30440
30449
|
video_url: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30441
30450
|
error: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30451
|
+
latest_render_free: exports_external.boolean().optional(),
|
|
30452
|
+
last_free_rerender_failed: exports_external.boolean().optional(),
|
|
30442
30453
|
saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30443
30454
|
timed_out: exports_external.boolean().optional()
|
|
30444
30455
|
}).passthrough();
|
|
@@ -30482,7 +30493,9 @@ var projectRow = exports_external.object({
|
|
|
30482
30493
|
terminal: exports_external.boolean().optional(),
|
|
30483
30494
|
ready: exports_external.boolean().optional(),
|
|
30484
30495
|
video_url: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30485
|
-
error: exports_external.union([exports_external.string(), exports_external.null()]).optional()
|
|
30496
|
+
error: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
30497
|
+
latest_render_free: exports_external.boolean().optional(),
|
|
30498
|
+
last_free_rerender_failed: exports_external.boolean().optional()
|
|
30486
30499
|
}).passthrough();
|
|
30487
30500
|
var listProjectsOutput = exports_external.object({
|
|
30488
30501
|
shorts: exports_external.array(projectRow).optional(),
|
|
@@ -31454,7 +31467,7 @@ var SERVER_INSTRUCTIONS = `Hubfluencer turns a prompt — or a whole product pag
|
|
|
31454
31467
|
|
|
31455
31468
|
MAKE ONE ASSET (simplest)
|
|
31456
31469
|
- make_video({ prompt }) creates, generates, and returns a finished MP4 — it picks a single-clip short or a multi-scene editor ad from the prompt (override with kind:"short"|"editor"). create_short / create_editor_ad are the per-product one-shots; create_slider makes an image carousel; create_tracking_video burns a CV overlay onto a local clip.
|
|
31457
|
-
- Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario →
|
|
31470
|
+
- Granular (full control): create a draft, then drive the pipeline step by step (create_editor_draft → generate_scenario → apply_scenario → review/edit prompts (set_segment_prompt) → generate_segment(s) → generate_voice + generate_music → render; regenerate_segment re-rolls a completed scene as a new version). Poll with get_status / wait_for_completion; fetch with download_result (a slider has no MP4 — read its slides with get_slider).
|
|
31458
31471
|
|
|
31459
31472
|
DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
31460
31473
|
1. ONBOARD a product: extract_product_page(url) pulls facts + imagery (SSRF-hardened, 0cr); import_product_image saves its picture/logo. Persist identity once with create_product_profile + create_brand_profile so every later draft inherits it instead of re-describing the brand.
|
|
@@ -31467,7 +31480,7 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
|
|
|
31467
31480
|
8. RECURRING: create_series + plan_series_episodes → materialize_episode drafts each episode; get_series_dashboard shows upcoming/drafted/published lanes; mark_episode_posted closes a recurring show's loop.
|
|
31468
31481
|
|
|
31469
31482
|
CREDITS (spent server-side; each tool prices before charging — see its description)
|
|
31470
|
-
- Free (0 credits): every draft create/edit, reorder, all slider re-composites, and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
|
|
31483
|
+
- Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
|
|
31471
31484
|
- Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video/create_* report the estimate first — pass dry_run to preview, max_credits to cap.
|
|
31472
31485
|
- AI ASSISTS (all generate_*/enhance/suggest/plan/hook tools) draw a FREE daily quota (20/day), NOT credits. On 429, set auto_unlock:true to spend 1 credit for +10 assists (retried once), or write the copy yourself and use the set_* tools.
|
|
31473
31486
|
|
|
@@ -31520,7 +31533,7 @@ async function pollSegmentToTerminal(client, slug, segmentId, extra, budgetMs, i
|
|
|
31520
31533
|
}
|
|
31521
31534
|
registerTool("make_video", {
|
|
31522
31535
|
title: "Make a video from a prompt (one shot)",
|
|
31523
|
-
description: "The simplest path: give a prompt, get a finished MP4. Creates the project (free), PRICES it against " + "your live credit balance, then — only if it's affordable and within max_credits — starts generation, " + "polls to completion (emitting progress), and (if save_path is given) downloads the result. " + "Spends credits (15 for a short; a multi-scene editor ad ~28). Pass dry_run:true to preview the cost " + "WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " + "spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " + "for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " + "the render is still running — call wait_for_completion with the returned slug to finish. " + "BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " + "music_vibe so a one-shot short isn't bare. creative_format + visual_language apply to BOTH kinds (editor and " + "shorts); headline/subheadline/music_vibe/text_* and the conversion graphics are SHORTS-only and ignored for " + "editor; theme applies to editor (genre overlay) and is legacy-only for shorts (used when visual_language is unset). " + "For richer branding — a product image, brand logo, or " + "closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " + "create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short.",
|
|
31536
|
+
description: "The simplest path: give a prompt, get a finished MP4. Creates the project (free), PRICES it against " + "your live credit balance, then — only if it's affordable and within max_credits — starts generation, " + "polls to completion (emitting progress), and (if save_path is given) downloads the result. " + "Spends credits (15 for a short; a multi-scene editor ad ~28). Pass dry_run:true to preview the cost " + "WITHOUT charging (returns {estimated_credits, available_credits, slug}); pass max_credits to cap the " + "spend. kind defaults to 'auto' — a multi-scene editor ad for ad/promo/story briefs, a single-clip short " + "for simple/short ones; the chosen kind is reported back as kind_inferred. If it returns terminal=false " + "the render is still running — call wait_for_completion with the returned slug to finish. " + "BE PROACTIVE WITH BRANDING: pass headline (the on-screen TITLE) and subheadline (secondary title) plus " + "music_vibe so a one-shot short isn't bare. Always set cta_text — a short without a CTA doesn't convert " + "(plus offer_text only if there's a real deal). Note: blank cta_text → the server renders a neutral " + "localized ask (e.g. 'Learn more'); a truly CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language " + "explicitly (kinetic_creator is a strong default). If headline/subheadline/text_beats are left blank the " + "server auto-writes the headline + caption beats at generate time; pass skip_auto_text:true for a " + "deliberately bare clip. creative_format + visual_language apply to BOTH kinds (editor and " + "shorts); headline/subheadline/music_vibe/text_* and the conversion graphics are SHORTS-only and ignored for " + "editor; theme applies to editor (genre overlay) and is legacy-only for shorts (used when visual_language is unset). " + "For richer branding — a product image, brand logo, or " + "closing card — drive the granular path instead: create_short + set_short_product/set_short_poster, or " + "create_editor_draft + set_product/set_logo/set_closing_image, then start_autopilot/generate_short. " + "For a SHORT, iterating after the first render is FREE: update_short (copy/style/CTA/end-card) → " + "rerender_short re-renders over the already-paid footage for 0 credits.",
|
|
31524
31537
|
inputSchema: {
|
|
31525
31538
|
prompt: exports_external.string().describe("What the ad/video should be about (min 10 chars)"),
|
|
31526
31539
|
kind: exports_external.string().optional().describe("'short' (fast, 1 clip), 'editor' (multi-scene), or 'auto' (default — inferred)"),
|
|
@@ -31529,16 +31542,17 @@ registerTool("make_video", {
|
|
|
31529
31542
|
language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag ' + '(use "pt", never "pt-BR"/"en-US"). Drives scenario, narration, voice, and captions.'),
|
|
31530
31543
|
aspect: exports_external.string().optional().describe("Aspect ratio for editor: 9:16 (default), 16:9, or 1:1"),
|
|
31531
31544
|
voice_id: exports_external.string().optional().describe("Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover"),
|
|
31532
|
-
headline: exports_external.string().optional().describe("SHORTS only: the on-screen TITLE overlay
|
|
31545
|
+
headline: exports_external.string().max(80).optional().describe("SHORTS only: the on-screen TITLE overlay. ≈4 words; ≤40 chars hits hardest; hard cap 80 — " + "longer hooks wrap past the safe area. Set this so the short isn't bare."),
|
|
31533
31546
|
subheadline: exports_external.string().optional().describe("SHORTS only: the secondary title / supporting line (≤200)"),
|
|
31534
31547
|
text_beats: exports_external.array(exports_external.string()).optional().describe("SHORTS only: caption beats shown sequentially instead of a static subheadline (≤8, each ≤120 chars)."),
|
|
31535
31548
|
headline_color: exports_external.string().optional().describe("SHORTS only: headline hex color, e.g. #ffffff"),
|
|
31536
31549
|
subheadline_color: exports_external.string().optional().describe("SHORTS only: subheadline hex color, e.g. #ffffff"),
|
|
31537
31550
|
accent_color: exports_external.string().optional().describe("SHORTS only: 6-digit brand accent hex color, e.g. #09EFBE"),
|
|
31538
31551
|
offer_text: exports_external.string().optional().describe("SHORTS only: optional offer chip, e.g. -40% (≤16 chars)"),
|
|
31539
|
-
cta_text: exports_external.string().optional().describe("SHORTS only:
|
|
31540
|
-
badge_text: exports_external.string().optional().describe("SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars)"),
|
|
31541
|
-
star_rating: exports_external.number().int().min(0).max(5).optional().describe("SHORTS only: optional 0..5 star rating under the subheadline"),
|
|
31552
|
+
cta_text: exports_external.string().optional().describe("SHORTS only: CTA pill, e.g. Shop now (≤24 chars). Always set it — a short without a CTA doesn't convert."),
|
|
31553
|
+
badge_text: exports_external.string().optional().describe("SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars). Only badges you can " + "substantiate — fabricated social proof is deceptive advertising."),
|
|
31554
|
+
star_rating: exports_external.number().int().min(0).max(5).optional().describe("SHORTS only: optional 0..5 star rating under the subheadline. Only ratings you can " + "substantiate — fabricated social proof is deceptive advertising."),
|
|
31555
|
+
skip_auto_text: exports_external.boolean().optional().describe("SHORTS only: when headline/subheadline/text_beats are blank the server auto-writes the copy at " + "generate time; pass true to skip that and render a deliberately bare clip (default false)."),
|
|
31542
31556
|
text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("SHORTS only: title overlay position (default bottom)"),
|
|
31543
31557
|
text_animation: exports_external.enum([
|
|
31544
31558
|
"auto",
|
|
@@ -31622,7 +31636,7 @@ registerTool("make_video", {
|
|
|
31622
31636
|
creative_format: args.creative_format,
|
|
31623
31637
|
visual_language: args.visual_language,
|
|
31624
31638
|
theme: args.theme
|
|
31625
|
-
}, idemKey("make-short", args.prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", JSON.stringify(args.text_beats ?? []), args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.creative_format ?? "", args.visual_language ?? "", args.theme ?? ""));
|
|
31639
|
+
}, idemKey("make-short", args.prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", JSON.stringify(args.text_beats ?? []), args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.skip_auto_text ? "bare" : "auto-copy"));
|
|
31626
31640
|
slug = created.data.slug;
|
|
31627
31641
|
} else {
|
|
31628
31642
|
const created = await client.post("/editor", {
|
|
@@ -31646,7 +31660,7 @@ registerTool("make_video", {
|
|
|
31646
31660
|
estimated_credits = typeof cost.total === "number" ? cost.total : null;
|
|
31647
31661
|
available_credits = typeof cost.available_credits === "number" ? cost.available_credits : null;
|
|
31648
31662
|
} catch {}
|
|
31649
|
-
const resume = kind === "short" ? `generate_short({ slug: "${slug}" })` : `start_autopilot({ slug: "${slug}" })`;
|
|
31663
|
+
const resume = kind === "short" ? `generate_short({ slug: "${slug}"${args.skip_auto_text ? ", skip_auto_text: true" : ""} })` : `start_autopilot({ slug: "${slug}" })`;
|
|
31650
31664
|
const affordable = available_credits === null || estimated_credits === null || available_credits >= estimated_credits;
|
|
31651
31665
|
const overCap = args.max_credits != null && estimated_credits != null && estimated_credits > args.max_credits;
|
|
31652
31666
|
if (args.dry_run || overCap || !affordable) {
|
|
@@ -31668,7 +31682,7 @@ registerTool("make_video", {
|
|
|
31668
31682
|
startState = startStateDiscriminator(kind, asRecord(cur).data);
|
|
31669
31683
|
} catch {}
|
|
31670
31684
|
if (kind === "short") {
|
|
31671
|
-
await client.post(`/shorts/${slug}/generate`, undefined, `gen-short:${slug}:${startState}`);
|
|
31685
|
+
await client.post(`/shorts/${slug}/generate`, args.skip_auto_text ? { skip_auto_text: true } : undefined, `gen-short:${slug}:${startState}${args.skip_auto_text ? ":bare" : ""}`);
|
|
31672
31686
|
} else {
|
|
31673
31687
|
await client.post(`/editor/${slug}/autopilot`, undefined, makeVideoAutopilotKey(slug, args, startState));
|
|
31674
31688
|
}
|
|
@@ -31754,11 +31768,11 @@ registerTool("list_projects", {
|
|
|
31754
31768
|
}));
|
|
31755
31769
|
registerTool("create_short", {
|
|
31756
31770
|
title: "Create a short (draft)",
|
|
31757
|
-
description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when
|
|
31771
|
+
description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " + "overlay — that's an editor-only feature; use create_editor_ad for logo branding.)",
|
|
31758
31772
|
inputSchema: {
|
|
31759
31773
|
product_prompt: exports_external.string().min(10).describe("What the short should be about"),
|
|
31760
31774
|
language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en" (default). Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
|
|
31761
|
-
headline: exports_external.string().max(
|
|
31775
|
+
headline: exports_external.string().max(80).optional().describe("The on-screen TITLE composited over the short (poster text overlay). ≈4 words; ≤40 chars hits " + "hardest; hard cap 80 — longer hooks wrap past the safe area. Set this."),
|
|
31762
31776
|
subheadline: exports_external.string().max(200).optional().describe("The SECONDARY title / supporting line under the headline. ≤200 chars."),
|
|
31763
31777
|
creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Optional structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal. Omit for Auto."),
|
|
31764
31778
|
visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling. Good default: kinetic_creator."),
|
|
@@ -31781,10 +31795,13 @@ registerTool("create_short", {
|
|
|
31781
31795
|
subheadline_color: exports_external.string().optional().describe("Subheadline hex color, e.g. #ffffff"),
|
|
31782
31796
|
accent_color: exports_external.string().optional().describe("6-digit brand accent hex color, e.g. #09EFBE"),
|
|
31783
31797
|
offer_text: exports_external.string().max(16).optional().describe('Optional offer chip, e.g. "-40%" or "2 FOR 1"'),
|
|
31784
|
-
cta_text: exports_external.string().max(24).optional().describe(
|
|
31785
|
-
badge_text: exports_external.string().max(24).optional().describe('Optional badge stamp, e.g. "BEST SELLER"'),
|
|
31786
|
-
star_rating: exports_external.number().int().min(0).max(5).optional().describe("Optional 0..5 star rating under the subheadline"),
|
|
31798
|
+
cta_text: exports_external.string().max(24).optional().describe(`CTA pill, e.g. "Shop now". Always set it — a short without a CTA doesn't convert.`),
|
|
31799
|
+
badge_text: exports_external.string().max(24).optional().describe('Optional badge stamp, e.g. "BEST SELLER". Only badges you can substantiate — fabricated social ' + "proof is deceptive advertising."),
|
|
31800
|
+
star_rating: exports_external.number().int().min(0).max(5).optional().describe("Optional 0..5 star rating under the subheadline. Only ratings you can substantiate — fabricated " + "social proof is deceptive advertising."),
|
|
31787
31801
|
text_beats: exports_external.array(exports_external.string().max(120)).max(8).optional().describe("Optional caption beats shown sequentially instead of the static subheadline."),
|
|
31802
|
+
closing_claim: exports_external.string().max(80).optional().describe("End-card claim shown on the closing slate (≤80 chars) — a rephrase of the promise, not a new " + "claim. Falls back to the headline when unset."),
|
|
31803
|
+
brand_name: exports_external.string().max(40).optional().describe("Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " + "poster is set. Server falls back to your brand profile's name when unset."),
|
|
31804
|
+
end_card: exports_external.enum(["auto", "none"]).optional().describe('"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' + 'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' + "loop-style shorts)."),
|
|
31788
31805
|
brand_profile_id: exports_external.number().int().optional().describe("Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " + "list_brand_profiles). Its creative format, visual language, font, accent, and default CTA fill " + "any blank short fields and a snapshot is frozen onto the short. Must be one of your own profiles.")
|
|
31789
31806
|
},
|
|
31790
31807
|
outputSchema: createDraftOutput,
|
|
@@ -31831,23 +31848,29 @@ registerTool("create_short", {
|
|
|
31831
31848
|
body.star_rating = args.star_rating;
|
|
31832
31849
|
if (args.text_beats !== undefined)
|
|
31833
31850
|
body.text_beats = args.text_beats;
|
|
31851
|
+
if (args.closing_claim !== undefined)
|
|
31852
|
+
body.closing_claim = args.closing_claim;
|
|
31853
|
+
if (args.brand_name !== undefined)
|
|
31854
|
+
body.brand_name = args.brand_name;
|
|
31855
|
+
if (args.end_card !== undefined)
|
|
31856
|
+
body.end_card = args.end_card;
|
|
31834
31857
|
if (args.brand_profile_id !== undefined)
|
|
31835
31858
|
body.brand_profile_id = args.brand_profile_id;
|
|
31836
|
-
const res = await client.post("/shorts", body, idemKey("create-short", args.product_prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), JSON.stringify(args.text_beats ?? []), String(args.brand_profile_id ?? "")));
|
|
31859
|
+
const res = await client.post("/shorts", body, idemKey("create-short", args.product_prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), JSON.stringify(args.text_beats ?? []), args.closing_claim ?? "", args.brand_name ?? "", args.end_card ?? "", String(args.brand_profile_id ?? "")));
|
|
31837
31860
|
return ok({
|
|
31838
31861
|
slug: res.data.slug,
|
|
31839
31862
|
kind: "short",
|
|
31840
|
-
next: "set_short_product / set_short_poster (optional branding)
|
|
31863
|
+
next: "set_short_product / set_short_poster (optional branding); generate_hook_variations to test hook angles before you spend; then generate_short. After that first render, hook/CTA/style iteration is free: update_short → rerender_short (0 credits)."
|
|
31841
31864
|
});
|
|
31842
31865
|
}));
|
|
31843
31866
|
registerTool("update_short", {
|
|
31844
31867
|
title: "Update a short draft (0 credits)",
|
|
31845
|
-
description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render (
|
|
31868
|
+
description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
|
|
31846
31869
|
inputSchema: {
|
|
31847
31870
|
slug: exports_external.string().describe("Short slug (from create_short)"),
|
|
31848
31871
|
product_prompt: exports_external.string().min(10).optional().describe("Rewrite the brief (what the short is about, ≥10 chars); drives the next generate_short."),
|
|
31849
31872
|
language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en". Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
|
|
31850
|
-
headline: exports_external.string().max(
|
|
31873
|
+
headline: exports_external.string().max(80).optional().describe("On-screen TITLE overlay. ≈4 words; ≤40 chars hits hardest; hard cap 80 — longer hooks wrap " + "past the safe area."),
|
|
31851
31874
|
subheadline: exports_external.string().max(200).optional().describe("Secondary title / supporting line. ≤200 chars."),
|
|
31852
31875
|
creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal."),
|
|
31853
31876
|
visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling."),
|
|
@@ -31870,10 +31893,14 @@ registerTool("update_short", {
|
|
|
31870
31893
|
subheadline_color: exports_external.string().optional().describe("Subheadline hex color, e.g. #ffffff"),
|
|
31871
31894
|
accent_color: exports_external.string().optional().describe("6-digit brand accent hex color, e.g. #09EFBE"),
|
|
31872
31895
|
offer_text: exports_external.string().max(16).optional().describe('Offer chip (opt-in), e.g. "-40%". Pass "" to remove it.'),
|
|
31873
|
-
cta_text: exports_external.string().max(24).optional().describe(
|
|
31874
|
-
badge_text: exports_external.string().max(24).optional().describe('Badge stamp (opt-in), e.g. "BEST SELLER". Pass "" to remove it.'),
|
|
31875
|
-
star_rating: exports_external.number().int().min(0).max(5).optional().describe("0..5 star rating under the subheadline (opt-in). Pass 0 to remove it."),
|
|
31896
|
+
cta_text: exports_external.string().max(24).optional().describe(`CTA pill, e.g. "Shop now". Always set it — a short without a CTA doesn't convert. Pass "" to remove it.`),
|
|
31897
|
+
badge_text: exports_external.string().max(24).optional().describe('Badge stamp (opt-in), e.g. "BEST SELLER". Only badges you can substantiate — fabricated social ' + 'proof is deceptive advertising. Pass "" to remove it.'),
|
|
31898
|
+
star_rating: exports_external.number().int().min(0).max(5).optional().describe("0..5 star rating under the subheadline (opt-in). Only ratings you can substantiate — fabricated " + "social proof is deceptive advertising. Pass 0 to remove it."),
|
|
31876
31899
|
text_beats: exports_external.array(exports_external.string().max(120)).max(8).optional().describe("Caption beats shown sequentially instead of the static subheadline. Pass [] to clear."),
|
|
31900
|
+
closing_claim: exports_external.string().max(80).optional().describe("End-card claim shown on the closing slate (≤80 chars) — a rephrase of the promise, not a new " + 'claim. Falls back to the headline when unset. Pass "" to remove it.'),
|
|
31901
|
+
brand_name: exports_external.string().max(40).optional().describe("Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " + `poster is set. Server falls back to your brand profile's name when unset. Pass "" to remove it.`),
|
|
31902
|
+
end_card: exports_external.enum(["auto", "none"]).optional().describe('"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' + 'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' + "loop-style shorts)."),
|
|
31903
|
+
poster_includes_lockup: exports_external.boolean().optional().describe("Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " + "claim/CTA so they don't collide (default false)."),
|
|
31877
31904
|
brand_profile_id: exports_external.number().int().optional().describe("Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " + "list_brand_profiles). Best set at create_short time (that path also freezes a brand snapshot); " + "on update it sets the linked profile. Must be one of your own profiles.")
|
|
31878
31905
|
},
|
|
31879
31906
|
annotations: {
|
|
@@ -31923,10 +31950,22 @@ registerTool("update_short", {
|
|
|
31923
31950
|
body.star_rating = args.star_rating;
|
|
31924
31951
|
if (args.text_beats !== undefined)
|
|
31925
31952
|
body.text_beats = args.text_beats;
|
|
31953
|
+
if (args.closing_claim !== undefined)
|
|
31954
|
+
body.closing_claim = args.closing_claim;
|
|
31955
|
+
if (args.brand_name !== undefined)
|
|
31956
|
+
body.brand_name = args.brand_name;
|
|
31957
|
+
if (args.end_card !== undefined)
|
|
31958
|
+
body.end_card = args.end_card;
|
|
31959
|
+
if (args.poster_includes_lockup !== undefined)
|
|
31960
|
+
body.poster_includes_lockup = args.poster_includes_lockup;
|
|
31926
31961
|
if (args.brand_profile_id !== undefined)
|
|
31927
31962
|
body.brand_profile_id = args.brand_profile_id;
|
|
31928
31963
|
const res = await client.patch(`/shorts/${args.slug}`, body);
|
|
31929
|
-
|
|
31964
|
+
const data = asRecord(res).data ?? res;
|
|
31965
|
+
return ok({
|
|
31966
|
+
...asRecord(data),
|
|
31967
|
+
next: "Saved. Only text/style/CTA/end-card changed on an already-generated short? " + `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` + "(prompt, product image, format, visual_language, theme, music_*, language) need " + "generate_short (15 credits)."
|
|
31968
|
+
});
|
|
31930
31969
|
}));
|
|
31931
31970
|
async function getTracking(client, slug, deadline) {
|
|
31932
31971
|
const res = await client.get(`/tracking/${slug}`, undefined, deadline);
|
|
@@ -32066,12 +32105,15 @@ registerTool("create_tracking_video", {
|
|
|
32066
32105
|
}));
|
|
32067
32106
|
registerTool("generate_short", {
|
|
32068
32107
|
title: "Generate (render) a short",
|
|
32069
|
-
description: "Deducts 15 credits and starts the render pipeline for an existing short. Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. Then poll with get_status or wait_for_completion (kind=short).",
|
|
32070
|
-
inputSchema: {
|
|
32108
|
+
description: "Deducts 15 credits and starts the render pipeline for an existing short — this RE-ROLLS the footage " + "and music (a fresh AI generation). For text/style/CTA/end-card tweaks on an already-generated short, " + "use rerender_short instead: it re-applies the overlay over the existing footage for 0 credits. " + "Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " + "auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " + "bare clip). Then poll with get_status or wait_for_completion (kind=short).",
|
|
32109
|
+
inputSchema: {
|
|
32110
|
+
slug: exports_external.string().describe("Short slug from create_short"),
|
|
32111
|
+
skip_auto_text: exports_external.boolean().optional().describe("When the draft has no headline/subheadline/text_beats the server auto-writes the copy; pass " + "true to skip that and render a deliberately bare clip (default false).")
|
|
32112
|
+
},
|
|
32071
32113
|
annotations: { title: "Generate short", ...WRITE }
|
|
32072
32114
|
}, tool(async (args, client) => {
|
|
32073
32115
|
try {
|
|
32074
|
-
const res = await client.post(`/shorts/${args.slug}/generate
|
|
32116
|
+
const res = await client.post(`/shorts/${args.slug}/generate`, args.skip_auto_text ? { skip_auto_text: true } : undefined);
|
|
32075
32117
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
32076
32118
|
return ok(status);
|
|
32077
32119
|
} catch (e) {
|
|
@@ -32089,6 +32131,44 @@ registerTool("generate_short", {
|
|
|
32089
32131
|
throw e;
|
|
32090
32132
|
}
|
|
32091
32133
|
}));
|
|
32134
|
+
registerTool("rerender_short", {
|
|
32135
|
+
title: "Re-render a short (FREE — 0 credits)",
|
|
32136
|
+
description: "FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; the pinned music bed is faded to fit rather than " + "regenerated — for perfectly synced music after such a change, use generate_short. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
|
|
32137
|
+
inputSchema: {
|
|
32138
|
+
slug: exports_external.string().describe("Short slug (from create_short)")
|
|
32139
|
+
},
|
|
32140
|
+
annotations: { title: "Re-render short", ...WRITE, idempotentHint: false }
|
|
32141
|
+
}, tool(async (args, client) => {
|
|
32142
|
+
try {
|
|
32143
|
+
const res = await client.post(`/shorts/${args.slug}/rerender`, undefined, idemKey("rerender-short", args.slug, randomUUID()));
|
|
32144
|
+
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
32145
|
+
return ok({
|
|
32146
|
+
...status,
|
|
32147
|
+
next: `wait_for_completion({ slug: "${args.slug}", kind: "short" }) — or poll get_status. A re-render is fast (no AI generation).`
|
|
32148
|
+
});
|
|
32149
|
+
} catch (e) {
|
|
32150
|
+
const he = e;
|
|
32151
|
+
if (he.status === 409) {
|
|
32152
|
+
return ok({
|
|
32153
|
+
kind: "short",
|
|
32154
|
+
slug: args.slug,
|
|
32155
|
+
stage: "processing",
|
|
32156
|
+
terminal: false,
|
|
32157
|
+
ready: false,
|
|
32158
|
+
video_url: null,
|
|
32159
|
+
error: null,
|
|
32160
|
+
next: `A render is already running — wait_for_completion({ slug: "${args.slug}", kind: "short" }).`
|
|
32161
|
+
});
|
|
32162
|
+
}
|
|
32163
|
+
if (he.status === 422 && he.code === "short_not_ready") {
|
|
32164
|
+
return fail("This short has no completed footage + music to re-render over (short_not_ready). " + `Run generate_short({ slug: "${args.slug}" }) once (15 credits) — after that first render, every ` + "text/style/CTA/end-card tweak re-renders free with rerender_short.");
|
|
32165
|
+
}
|
|
32166
|
+
if (he.status === 422 && he.code === "short_paid_regeneration_required") {
|
|
32167
|
+
return fail("A footage or music input changed after the last paid render " + "(short_paid_regeneration_required). The pinned assets no longer match the draft. " + `Run generate_short({ slug: "${args.slug}" }) (15 credits) before using free re-renders again.`);
|
|
32168
|
+
}
|
|
32169
|
+
throw e;
|
|
32170
|
+
}
|
|
32171
|
+
}));
|
|
32092
32172
|
registerTool("generate_short_text", {
|
|
32093
32173
|
title: "Generate short overlay text (AI assist)",
|
|
32094
32174
|
description: "Generates editable headline, subheadline, and caption beats for an existing short draft. " + "CONSUMES 1 AI ASSIST (free daily quota) and spends no video credits. The server reads the saved " + "short fields, so update/create the draft with the current product prompt, creative_format, visual_language, " + "language, and seed copy first. On 429 set auto_unlock:true (1 credit -> +10 assists, retried once) or write the text yourself.",
|
|
@@ -32294,7 +32374,7 @@ registerTool("edit_slider_slide", {
|
|
|
32294
32374
|
}));
|
|
32295
32375
|
registerTool("create_editor_ad", {
|
|
32296
32376
|
title: "Create a multi-scene editor ad (autopilot)",
|
|
32297
|
-
description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path. " + "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " + "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " + "start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " + "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.)",
|
|
32377
|
+
description: "Creates an editor project from a product prompt and starts autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Returns the slug. Costs credits. Then poll with get_status / wait_for_completion (kind=editor). Prefer " + "make_video for the one-shot path. " + "BRANDING: to attach the user's own product image, brand logo, or closing card, create the draft with " + "create_editor_draft first, call set_product / set_logo / set_closing_image (all 0 credits), then " + "start_autopilot — autopilot weaves them in. (Editor ads carry their copy in-scene/narration, not as a " + "title overlay; for an on-screen title/subtitle use a short with headline/subheadline instead.) " + "For product_only or presenter_led cast modes, use create_editor_draft and configure the required " + "product/presenter before start_autopilot; this one-shot tool intentionally uses automatic casting.",
|
|
32298
32378
|
inputSchema: {
|
|
32299
32379
|
product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
|
|
32300
32380
|
product_subject: exports_external.string().max(300).optional().describe('The concrete product or main subject the video must be about (e.g. "Bordeaux red wine, dark green ' + 'bottle with a gold label"). Distinct from the freeform brief: hard-grounds the AI scenario + narration ' + "so it cannot invent an unrelated product. Strongly recommended for ads."),
|
|
@@ -32391,6 +32471,7 @@ registerTool("create_editor_draft", {
|
|
|
32391
32471
|
creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
|
|
32392
32472
|
visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored."),
|
|
32393
32473
|
theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
|
|
32474
|
+
cast_mode: exports_external.enum(["product_only", "presenter_led"]).optional().describe("Who/what anchors every AI scene. product_only = a product-led ad with NO people; requires a " + "product image (set_product) before generation and generates every scene from persistent product " + "reference images — the best product fidelity. presenter_led = one locked presenter appears in " + "every scene; requires a presenter to be configured first (in the app / REST presenter endpoints " + "— no MCP tool yet), else generation fails with presenter_required. Omit for the default auto " + "behavior."),
|
|
32394
32475
|
voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).optional().describe("Preferred narration voice id (see list_voices); ≤64 chars, [A-Za-z0-9_-] only"),
|
|
32395
32476
|
export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
|
|
32396
32477
|
project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("social_ad for promotional/ad content (product focus + CTA), creative_story for brand " + "narratives (default).")
|
|
@@ -32409,17 +32490,18 @@ registerTool("create_editor_draft", {
|
|
|
32409
32490
|
creative_format: args.creative_format,
|
|
32410
32491
|
visual_language: args.visual_language,
|
|
32411
32492
|
theme: args.theme,
|
|
32493
|
+
cast_mode: args.cast_mode,
|
|
32412
32494
|
voice_id: args.voice_id,
|
|
32413
32495
|
export_aspect_ratio: args.export_aspect_ratio,
|
|
32414
32496
|
project_intent: args.project_intent
|
|
32415
32497
|
};
|
|
32416
32498
|
if (prompt)
|
|
32417
32499
|
body.product_prompt = prompt;
|
|
32418
|
-
const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? ""));
|
|
32500
|
+
const created = await client.post("/editor", body, idemKey("create-editor-draft", prompt ?? "", args.product_subject ?? "", args.project_intent ?? "", args.language ?? "en", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.voice_id ?? "", args.export_aspect_ratio ?? "", args.cast_mode ?? ""));
|
|
32419
32501
|
return ok({
|
|
32420
32502
|
slug: created.data.slug,
|
|
32421
32503
|
kind: "editor",
|
|
32422
|
-
next: "generate_scenario | set_scenario, then get_editor to review"
|
|
32504
|
+
next: "generate_scenario | set_scenario, then get_editor to review, then apply_scenario"
|
|
32423
32505
|
});
|
|
32424
32506
|
}));
|
|
32425
32507
|
registerTool("get_editor", {
|
|
@@ -32445,7 +32527,7 @@ registerTool("set_scenario", {
|
|
|
32445
32527
|
}));
|
|
32446
32528
|
registerTool("generate_scenario", {
|
|
32447
32529
|
title: "Generate the editor scenario (AI assist)",
|
|
32448
|
-
description: "Generates a scenario with AI. CONSUMES 1 AI ASSIST (free daily quota; check get_ai_assists). On 429 " + "(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " + "write the scenario yourself with set_scenario. Server default segments_count is 5. " + "Optionally pass creative_format / visual_language / theme to persist the project’s creative style before " + "generating (this is the granular path’s way to set/change style; PATCH /scenario does not).",
|
|
32530
|
+
description: "Generates a scenario with AI. CONSUMES 1 AI ASSIST (free daily quota; check get_ai_assists). On 429 " + "(quota exhausted) either set auto_unlock:true to spend 1 credit for +10 assists and retry once, or " + "write the scenario yourself with set_scenario. Server default segments_count is 5. " + "Optionally pass creative_format / visual_language / theme to persist the project’s creative style before " + "generating (this is the granular path’s way to set/change style; PATCH /scenario does not). " + "Generation is ASYNC: get_editor shows scenario_status generating → ready (a few seconds). NEXT STEP: " + "once ready, review the scenario with get_editor, then call apply_scenario to turn it into per-scene " + "segment prompts on the timeline.",
|
|
32449
32531
|
inputSchema: {
|
|
32450
32532
|
slug: exports_external.string().describe("Editor project slug"),
|
|
32451
32533
|
segments_count: exports_external.number().int().min(3).max(10).optional().describe("How many scenes (3–10, server default 5)"),
|
|
@@ -32472,6 +32554,63 @@ registerTool("generate_scenario", {
|
|
|
32472
32554
|
const res = await withAssist(client, args.auto_unlock ?? false, () => client.post(`/editor/${args.slug}/generate-scenario`, body));
|
|
32473
32555
|
return ok(asRecord(res).data ?? res);
|
|
32474
32556
|
}));
|
|
32557
|
+
registerTool("apply_scenario", {
|
|
32558
|
+
title: "Apply the scenario to the timeline (per-scene prompts)",
|
|
32559
|
+
description: "Turns the READY scenario into per-scene segment prompts on the timeline — the missing step between " + "generate_scenario and generate_segment in the granular flow. Existing AI segments are REPLACED with " + "freshly generated scene prompts (uploaded clips are preserved); segments_count 0 removes the AI scenes " + "and keeps the scenario only. Free — no credits, no AI assist (rate limit 15/min). Requires " + 'scenario_status "ready" (422 scenario_not_ready otherwise): after generate_scenario wait until ' + "get_editor shows ready (a few seconds); set_scenario is ready immediately. The apply itself runs async " + "server-side — this tool polls get_editor (bounded ~2 min) until scenario_apply_status leaves " + '"applying" and returns the materialized prompts; if it reports timed_out, keep polling get_editor ' + "yourself. Then review/edit with set_segment_prompt and generate with generate_segment / " + "generate_all_segments (or just render).",
|
|
32560
|
+
inputSchema: {
|
|
32561
|
+
slug: exports_external.string().describe("Editor project slug"),
|
|
32562
|
+
segments_count: exports_external.union([exports_external.literal(0), exports_external.number().int().min(3).max(10)]).optional().describe("How many AI scenes to create on the timeline — 0 or any integer from 3 to 10. Omit to use " + "the scene count saved by generate_scenario (or 5 when none is stored). " + "0 keeps the scenario only and preserves manual timeline control.")
|
|
32563
|
+
},
|
|
32564
|
+
annotations: { title: "Apply scenario", ...WRITE, idempotentHint: false }
|
|
32565
|
+
}, tool(async (args, client, extra) => {
|
|
32566
|
+
const body = {};
|
|
32567
|
+
if (args.segments_count !== undefined)
|
|
32568
|
+
body.segments_count = args.segments_count;
|
|
32569
|
+
const res = await client.post(`/editor/${args.slug}/apply-scenario`, body);
|
|
32570
|
+
const accepted = asRecord(asRecord(res).data);
|
|
32571
|
+
const APPLY_BUDGET_MS = 120000;
|
|
32572
|
+
const APPLY_POLL_MS = 2500;
|
|
32573
|
+
const deadline = Date.now() + APPLY_BUDGET_MS;
|
|
32574
|
+
const readEditor = async () => {
|
|
32575
|
+
const r = await client.get(`/editor/${args.slug}`, undefined, deadline);
|
|
32576
|
+
return asRecord(asRecord(r).data);
|
|
32577
|
+
};
|
|
32578
|
+
let n = 0;
|
|
32579
|
+
let editor = await readEditor();
|
|
32580
|
+
while (editor.scenario_apply_status === "applying" && Date.now() + APPLY_POLL_MS <= deadline) {
|
|
32581
|
+
await reportProgress(extra, ++n, "applying scenario to timeline");
|
|
32582
|
+
await sleep4(APPLY_POLL_MS);
|
|
32583
|
+
editor = await readEditor();
|
|
32584
|
+
}
|
|
32585
|
+
if (editor.scenario_apply_status === "applying") {
|
|
32586
|
+
return ok({
|
|
32587
|
+
slug: args.slug,
|
|
32588
|
+
applied: false,
|
|
32589
|
+
timed_out: true,
|
|
32590
|
+
note: 'The apply is still running server-side. Poll get_editor until scenario_apply_status is no longer "applying", then review the scene prompts.'
|
|
32591
|
+
});
|
|
32592
|
+
}
|
|
32593
|
+
if (editor.scenario_apply_status === "failed") {
|
|
32594
|
+
return fail("Scenario apply failed server-side. Re-run apply_scenario to retry, or build the timeline yourself with set_scene_count + set_segment_prompt.");
|
|
32595
|
+
}
|
|
32596
|
+
const segments = Array.isArray(editor.segments) ? editor.segments : [];
|
|
32597
|
+
return ok({
|
|
32598
|
+
slug: args.slug,
|
|
32599
|
+
applied: true,
|
|
32600
|
+
ai_segments_replaced: accepted.ai_segments_replaced,
|
|
32601
|
+
uploads_preserved: accepted.uploads_preserved,
|
|
32602
|
+
target_ai_segments: accepted.target_ai_segments,
|
|
32603
|
+
scene_count: segments.length,
|
|
32604
|
+
segments: segments.map((s) => ({
|
|
32605
|
+
id: s.id,
|
|
32606
|
+
position: s.position,
|
|
32607
|
+
source_type: s.source_type,
|
|
32608
|
+
status: s.status,
|
|
32609
|
+
prompt: s.prompt
|
|
32610
|
+
})),
|
|
32611
|
+
next: "Review/edit prompts (set_segment_prompt), then generate_segment / generate_all_segments — or just render (it auto-charges ungenerated scenes at the batch rate)"
|
|
32612
|
+
});
|
|
32613
|
+
}));
|
|
32475
32614
|
registerTool("set_scene_count", {
|
|
32476
32615
|
title: "Set the number of scenes",
|
|
32477
32616
|
description: "Adjusts the editor to exactly `count` scenes (1–20) by adding pending segments to grow, or deleting " + "only TRAILING un-generated (pending or failed) segments to shrink. Never deletes completed/processing segments — " + "if a shrink would require that, it stops and reports. Free (no credits, no assist). Each AI-generated " + "scene is a fixed 8 seconds, so `count` scenes ≈ count×8s of AI footage (uploaded clips keep their own length).",
|
|
@@ -32539,7 +32678,7 @@ registerTool("set_segment_prompt", {
|
|
|
32539
32678
|
}));
|
|
32540
32679
|
registerTool("generate_segment", {
|
|
32541
32680
|
title: "Generate one segment (5 credits)",
|
|
32542
|
-
description: "Renders the video for one scene. Each AI-generated scene is a fixed 8 seconds long. Costs 5 credits.
|
|
32681
|
+
description: "Renders the video for one PENDING or FAILED scene. Each AI-generated scene is a fixed 8 seconds long. " + "Costs 5 credits. Re-call to retry a failed scene or after a prompt edit — the server blocks one that's " + "still processing (editor_segment_busy), so no double-charge; a COMPLETED scene returns " + "editor_segment_completed — re-roll those with regenerate_segment instead. Visual " + "continuity is handled server-side (this scene uses the previous scene's last frame). 402 " + "credits_insufficient fails cleanly. Generate scenes in position order for correct continuity.",
|
|
32543
32682
|
inputSchema: {
|
|
32544
32683
|
slug: exports_external.string().describe("Editor project slug"),
|
|
32545
32684
|
segment_id: exports_external.union([exports_external.number(), exports_external.string()]).describe("Segment id (from get_editor)")
|
|
@@ -32549,6 +32688,23 @@ registerTool("generate_segment", {
|
|
|
32549
32688
|
const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/generate`, undefined, undefined);
|
|
32550
32689
|
return ok(asRecord(res).data ?? res);
|
|
32551
32690
|
}));
|
|
32691
|
+
registerTool("regenerate_segment", {
|
|
32692
|
+
title: "Regenerate a completed segment (new version, 4 credits)",
|
|
32693
|
+
description: "Re-rolls a COMPLETED scene as a NEW VERSION of that segment — the previous version is kept (switch back " + "in the app). Costs 4 credits. Use generate_segment for pending/failed scenes; use THIS for a completed " + "scene you want different — optionally with a new prompt (omit it to re-roll the current prompt). The new " + "version starts rendering immediately; watch it with get_editor (status processing → completed|failed). " + "KNOWN LIMITATION: scenes chain visual continuity off the previous scene's last frame, so regenerating a " + "MID-TIMELINE scene can degrade the transition into the FOLLOWING scene (it was seeded from the old " + "version's last frame). A scene still processing returns editor_segment_busy (no double charge); 402 " + "credits_insufficient fails cleanly.",
|
|
32694
|
+
inputSchema: {
|
|
32695
|
+
slug: exports_external.string().describe("Editor project slug"),
|
|
32696
|
+
segment_id: exports_external.union([exports_external.number(), exports_external.string()]).describe("Segment id (from get_editor)"),
|
|
32697
|
+
prompt: exports_external.string().min(1).max(2000).optional().describe("Optional new scene prompt for the regenerated version (1–2000 chars); omit to reuse the current prompt")
|
|
32698
|
+
},
|
|
32699
|
+
annotations: {
|
|
32700
|
+
title: "Regenerate segment",
|
|
32701
|
+
...WRITE,
|
|
32702
|
+
idempotentHint: false
|
|
32703
|
+
}
|
|
32704
|
+
}, tool(async (args, client) => {
|
|
32705
|
+
const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/regenerate`, args.prompt !== undefined ? { prompt: args.prompt } : undefined, undefined);
|
|
32706
|
+
return ok(asRecord(res).data ?? res);
|
|
32707
|
+
}));
|
|
32552
32708
|
registerTool("generate_all_segments", {
|
|
32553
32709
|
title: "Generate all pending segments (sequential)",
|
|
32554
32710
|
description: "Generates every not-yet-completed scene (pending or previously failed) in position order, waiting for each to finish before starting" + "the next (preserves visual continuity). Each AI-generated scene is a fixed 8 seconds, so N scenes ≈ N×8s of footage. Costs 5 credits per segment generated. Stops and reports what " + "completed if a scene errors on submit (e.g. 402 credits_insufficient) or fails while rendering; if a " + "scene is still rendering past the budget it returns timed_out so you can re-run to continue (completed " + "scenes are skipped). The whole call is capped at ~280s: on a project with many scenes it processes as " + "many as fit, returns timed_out, and you re-run to continue (it picks up the remaining pending scenes). " + "Tip: to generate everything at lowest cost, skip this and just call render — it auto-generates any ungenerated scenes at the batch rate (4 credits/scene for ≥3); use this tool when you want to review or stop per scene. May take several minutes.",
|
|
@@ -33011,12 +33167,16 @@ registerTool("set_short_poster", {
|
|
|
33011
33167
|
description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MB), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
|
|
33012
33168
|
inputSchema: {
|
|
33013
33169
|
slug: exports_external.string().describe("Short slug (from create_short)"),
|
|
33014
|
-
file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)")
|
|
33170
|
+
file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)"),
|
|
33171
|
+
poster_includes_lockup: exports_external.boolean().optional().describe("Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " + "claim/CTA so they don't collide (default false).")
|
|
33015
33172
|
},
|
|
33016
33173
|
annotations: { title: "Set short poster", ...WRITE, idempotentHint: false }
|
|
33017
33174
|
}, tool(async (args, client) => {
|
|
33018
33175
|
const { s3_key } = await uploadShortPoster(client, args.slug, args.file_path);
|
|
33019
|
-
const res = await client.post(`/shorts/${args.slug}/poster/confirm`, {
|
|
33176
|
+
const res = await client.post(`/shorts/${args.slug}/poster/confirm`, {
|
|
33177
|
+
s3_key,
|
|
33178
|
+
poster_includes_lockup: args.poster_includes_lockup ?? false
|
|
33179
|
+
});
|
|
33020
33180
|
return ok(asRecord(res).data ?? res);
|
|
33021
33181
|
}));
|
|
33022
33182
|
registerTool("get_status", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubfluencer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Monocursive <contact@monocursive.com>",
|
package/src/core.ts
CHANGED
|
@@ -265,6 +265,14 @@ export interface NormalizedStatus {
|
|
|
265
265
|
ready: boolean;
|
|
266
266
|
video_url: string | null;
|
|
267
267
|
error: string | null;
|
|
268
|
+
/** Short only (additive, absent on older APIs): the newest render row is a free re-render. */
|
|
269
|
+
latest_render_free?: boolean;
|
|
270
|
+
/**
|
|
271
|
+
* Short only (additive): the newest render row is a FAILED free re-render.
|
|
272
|
+
* The delivered video stays intact (stage/latest_render fall back to the
|
|
273
|
+
* latest completed render), so the agent can just retry rerender_short — free.
|
|
274
|
+
*/
|
|
275
|
+
last_free_rerender_failed?: boolean;
|
|
268
276
|
}
|
|
269
277
|
|
|
270
278
|
/** The structured fields of an API error envelope errMessage formats from. */
|
|
@@ -318,7 +326,7 @@ export function normalizeStatus(
|
|
|
318
326
|
|
|
319
327
|
if (kind === "short") {
|
|
320
328
|
const stage = (d.stage as string) ?? "unknown";
|
|
321
|
-
|
|
329
|
+
const status: NormalizedStatus = {
|
|
322
330
|
kind,
|
|
323
331
|
slug,
|
|
324
332
|
stage,
|
|
@@ -327,6 +335,19 @@ export function normalizeStatus(
|
|
|
327
335
|
video_url: videoUrl,
|
|
328
336
|
error: (d.error_message as string) ?? null,
|
|
329
337
|
};
|
|
338
|
+
// Additive free-re-render bookkeeping (2026-07 API): surfaced only when
|
|
339
|
+
// the server actually sends the booleans, so older payloads normalize
|
|
340
|
+
// byte-identically. A failed free re-render does NOT flip stage to
|
|
341
|
+
// "failed" server-side — the delivered video stays visible — so these
|
|
342
|
+
// flags are the only signal that the newest render row was a free
|
|
343
|
+
// re-render (or a failed one worth retrying, still free).
|
|
344
|
+
if (typeof d.latest_render_free === "boolean") {
|
|
345
|
+
status.latest_render_free = d.latest_render_free;
|
|
346
|
+
}
|
|
347
|
+
if (typeof d.last_free_rerender_failed === "boolean") {
|
|
348
|
+
status.last_free_rerender_failed = d.last_free_rerender_failed;
|
|
349
|
+
}
|
|
350
|
+
return status;
|
|
330
351
|
}
|
|
331
352
|
|
|
332
353
|
if (kind === "tracking") {
|