@hubfluencer/mcp 0.11.0 → 0.13.0

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/dist/index.js CHANGED
@@ -29508,6 +29508,8 @@ function normalizeStatus(kind, slug, data) {
29508
29508
  status.failure_source = d.failure_source;
29509
29509
  if (typeof d.failure_attempt === "number")
29510
29510
  status.failure_attempt = d.failure_attempt;
29511
+ if (typeof d.generation_attempt === "number")
29512
+ status.generation_attempt = d.generation_attempt;
29511
29513
  if (typeof d.latest_render_free === "boolean") {
29512
29514
  status.latest_render_free = d.latest_render_free;
29513
29515
  }
@@ -29555,6 +29557,7 @@ function normalizeStatus(kind, slug, data) {
29555
29557
  const failedSegments = segments.filter((segment) => segment.status === "failed");
29556
29558
  const batchStatus = d.batch_generation_status ?? null;
29557
29559
  const batchActive = batchStatus === "running" || batchStatus === "preparing" || batchStatus === "awaiting_previews";
29560
+ const autoRenderHandoffActive = batchStatus === "completed" && d.auto_render_after_batch === true;
29558
29561
  const batchRetryRequired = batchStatus === "paused_for_retry";
29559
29562
  const batchInsufficientCredits = batchStatus === "paused_insufficient_credits";
29560
29563
  const batchFailed = batchStatus === "failed";
@@ -29599,7 +29602,7 @@ function normalizeStatus(kind, slug, data) {
29599
29602
  currentMusic.error_message
29600
29603
  ].find((value) => typeof value === "string" && value.trim().length > 0);
29601
29604
  const audioActive = currentAudio.status === "pending" || currentAudio.status === "processing" || currentMusic.status === "pending" || currentMusic.status === "processing" || newerAudioVersions.some(versionActive) || newerMusicVersions.some(versionActive);
29602
- const childWorkActive = batchActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
29605
+ const childWorkActive = batchActive || autoRenderHandoffActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
29603
29606
  const autopilotStartedAt = typeof d.autopilot_started_at === "string" ? Date.parse(d.autopilot_started_at) : Number.NaN;
29604
29607
  const now = Date.now();
29605
29608
  const autopilotResumeGraceActive = autopilotActive && Number.isFinite(autopilotStartedAt) && autopilotStartedAt <= now && now - autopilotStartedAt <= 120000;
@@ -29642,6 +29645,8 @@ function normalizeStatus(kind, slug, data) {
29642
29645
  stage = "segment_processing";
29643
29646
  else if (batchActive)
29644
29647
  stage = "segment_generation";
29648
+ else if (autoRenderHandoffActive)
29649
+ stage = "rendering";
29645
29650
  else if (renderActive)
29646
29651
  stage = "rendering";
29647
29652
  else if (scenarioActive)
@@ -29661,7 +29666,7 @@ function normalizeStatus(kind, slug, data) {
29661
29666
  terminal,
29662
29667
  ready,
29663
29668
  video_url: videoUrl,
29664
- error: ready ? null : batchRetryRequired ? "Batch generation is paused after a failed scene. Call retry_editor_batch or cancel_editor_batch before continuing." : (parentFailureActionable ? d.autopilot_error_message : null) ?? actionableChildError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume." : null),
29669
+ error: ready ? null : batchRetryRequired ? "Batch generation is paused after a failed scene. Call retry_editor_batch or cancel_editor_batch before continuing." : (parentFailureActionable ? d.autopilot_error_message : null) ?? actionableChildError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then invoke the generation/render path again; retry_editor_batch only applies to paused_for_retry." : null),
29665
29670
  stale,
29666
29671
  active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
29667
29672
  stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
@@ -29694,10 +29699,10 @@ function makeVideoAutopilotKey(slug, a, startState) {
29694
29699
  return idemKey("autopilot", slug, a.prompt, a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", startState);
29695
29700
  }
29696
29701
  function editorAdCreateKey(a) {
29697
- return idemKey("create-editor", a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_image_identity ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.closing_image_identity ?? "", a.logo_path ?? "", a.logo_identity ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.cast_mode ?? "");
29702
+ return idemKey("create-editor", a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_image_identity ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.closing_image_identity ?? "", a.logo_path ?? "", a.logo_identity ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds));
29698
29703
  }
29699
29704
  function editorAdAutopilotKey(slug, a) {
29700
- return idemKey("autopilot", slug, a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.logo_path ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.cast_mode ?? "", a.max_credits === undefined ? "" : String(a.max_credits));
29705
+ return idemKey("autopilot", slug, a.product_prompt ?? "", a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", a.voice_id ?? "", a.export_aspect_ratio ?? "", a.product_image_path ?? "", a.product_description ?? "", a.closing_image_path ?? "", a.logo_path ?? "", a.logo_treatment ?? "", a.logo_position ?? "", a.logo_duration_seconds === undefined ? "" : String(a.logo_duration_seconds), a.max_credits === undefined ? "" : String(a.max_credits));
29701
29706
  }
29702
29707
  function sliderCreateKey(a) {
29703
29708
  return idemKey("create-slider", a.prompt, a.mode ?? "", a.template ?? "", a.slide_count === undefined ? "" : String(a.slide_count), a.aspect_ratio ?? "", a.accent_color ?? "", a.text_position ?? "");
@@ -30254,7 +30259,7 @@ function readStoredCredentials() {
30254
30259
  // package.json
30255
30260
  var package_default = {
30256
30261
  name: "@hubfluencer/mcp",
30257
- version: "0.11.0",
30262
+ version: "0.13.0",
30258
30263
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30259
30264
  license: "MIT",
30260
30265
  author: "Monocursive <contact@monocursive.com>",
@@ -30572,6 +30577,7 @@ var getStatusOutput = exports_external.object({
30572
30577
  failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30573
30578
  failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30574
30579
  failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
30580
+ generation_attempt: exports_external.number().optional().describe("Short only: current paid attempt; echo when cancelling."),
30575
30581
  latest_render_free: exports_external.boolean().optional(),
30576
30582
  last_free_rerender_failed: exports_external.boolean().optional()
30577
30583
  }).passthrough();
@@ -30591,6 +30597,7 @@ var waitForCompletionOutput = exports_external.object({
30591
30597
  failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30592
30598
  failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30593
30599
  failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
30600
+ generation_attempt: exports_external.number().optional().describe("Short only: current paid attempt; echo when cancelling."),
30594
30601
  latest_render_free: exports_external.boolean().optional(),
30595
30602
  last_free_rerender_failed: exports_external.boolean().optional(),
30596
30603
  saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
@@ -31566,9 +31573,9 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
31566
31573
  const params = new URLSearchParams;
31567
31574
  if (overrides.language)
31568
31575
  params.set("language", overrides.language);
31569
- if (overrides.product_subject)
31576
+ if (overrides.product_subject !== undefined)
31570
31577
  params.set("product_subject", overrides.product_subject);
31571
- if (overrides.product_prompt)
31578
+ if (overrides.product_prompt !== undefined)
31572
31579
  params.set("product_prompt", overrides.product_prompt);
31573
31580
  if (overrides.restart)
31574
31581
  params.set("restart", "true");
@@ -31795,6 +31802,23 @@ var VISUAL_LANGUAGES = [
31795
31802
  "startup_explainer",
31796
31803
  "luxury_minimal"
31797
31804
  ];
31805
+ var SCENARIO_THEMES = [
31806
+ "none",
31807
+ "realistic",
31808
+ "cinematic",
31809
+ "anime",
31810
+ "sci_fi",
31811
+ "fantasy",
31812
+ "noir",
31813
+ "superhero",
31814
+ "horror",
31815
+ "mockumentary",
31816
+ "sports",
31817
+ "gaming",
31818
+ "retro_80s",
31819
+ "minimalist",
31820
+ "cyberpunk"
31821
+ ];
31798
31822
  var LANGUAGES = [
31799
31823
  "en",
31800
31824
  "fr",
@@ -31921,7 +31945,7 @@ registerTool("make_video", {
31921
31945
  music_instruments: exports_external.array(exports_external.string()).optional().describe('SHORTS only: instrument hints woven into the music (≤10, each ≤40 chars), e.g. ["piano", "strings"]'),
31922
31946
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("SHORTS + EDITOR: narrative arc / segment structure. Omit for Auto/generic."),
31923
31947
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("SHORTS + EDITOR: visual style direction and render look. Default app choice is kinetic_creator."),
31924
- theme: exports_external.string().optional().describe("Visual theme: editor genre overlay, and legacy shorts fallback only when visual_language is unset. " + "For editor, when visual_language is set it drives the look and 'none' is ignored. " + "none (literal — no imposed style), realistic (default), cinematic, " + "anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " + "minimalist, cyberpunk"),
31948
+ theme: exports_external.enum(SCENARIO_THEMES).optional().describe("Visual theme: editor genre overlay, and legacy shorts fallback only when visual_language is unset. " + "For editor, visual_language drives the look when present; make_video defaults theme to none " + "(literal — no imposed genre style). Options: none, realistic, cinematic, " + "anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " + "minimalist, cyberpunk"),
31925
31949
  save_path: exports_external.string().optional().describe("Optional .mp4 path to download to (confined to HUBFLUENCER_OUTPUT_DIR or cwd)"),
31926
31950
  max_wait_seconds: exports_external.number().optional().describe("Block budget seconds (default 240, capped 10–280)"),
31927
31951
  dry_run: exports_external.boolean().optional().describe("Preview only: create a free draft, price it, and STOP before spending credits. " + "Returns {estimated_credits, available_credits, slug}. Resume with generate_short / start_autopilot."),
@@ -32000,8 +32024,8 @@ registerTool("make_video", {
32000
32024
  voice_id: args.voice_id,
32001
32025
  creative_format: args.creative_format,
32002
32026
  visual_language: args.visual_language,
32003
- theme: args.theme
32004
- }, makeVideoEditorCreateKey(args));
32027
+ theme: args.theme ?? "none"
32028
+ }, makeVideoEditorCreateKey({ ...args, theme: args.theme ?? "none" }));
32005
32029
  slug = created.data.slug;
32006
32030
  }
32007
32031
  const costPath = kind === "short" ? `/shorts/${slug}/cost` : `/editor/${slug}/autopilot/cost`;
@@ -32152,7 +32176,7 @@ registerTool("create_short", {
32152
32176
  subheadline: exports_external.string().max(200).optional().describe("The SECONDARY title / supporting line under the headline. ≤200 chars."),
32153
32177
  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."),
32154
32178
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling. Good default: kinetic_creator."),
32155
- theme: exports_external.string().optional().describe("Deprecated for shorts: legacy visual theme used only when visual_language is unset."),
32179
+ theme: exports_external.enum(SCENARIO_THEMES).optional().describe("Deprecated for shorts: legacy visual theme used only when visual_language is unset."),
32156
32180
  music_vibe: exports_external.string().optional().describe("Background-music mood. Recognized: Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz."),
32157
32181
  music_instruments: exports_external.array(exports_external.string().max(40)).max(10).optional().describe('Optional instrument hints (≤10, each ≤40 chars), e.g. ["piano", "strings"]'),
32158
32182
  text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("Where the title overlay sits (default bottom)"),
@@ -32250,7 +32274,7 @@ registerTool("update_short", {
32250
32274
  subheadline: exports_external.string().max(200).optional().describe("Secondary title / supporting line. ≤200 chars."),
32251
32275
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal."),
32252
32276
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling."),
32253
- theme: exports_external.string().optional().describe("Deprecated for shorts: legacy visual theme used only when visual_language is unset."),
32277
+ theme: exports_external.enum(SCENARIO_THEMES).optional().describe("Deprecated for shorts: legacy visual theme used only when visual_language is unset."),
32254
32278
  music_vibe: exports_external.string().optional().describe("Background-music mood: Upbeat, Cinematic, Minimal, Luxury, Playful, Jazz."),
32255
32279
  music_instruments: exports_external.array(exports_external.string().max(40)).max(10).optional().describe('Instrument hints (≤10, each ≤40 chars), e.g. ["piano", "strings"]'),
32256
32280
  text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("Where the title overlay sits."),
@@ -32513,6 +32537,31 @@ registerTool("generate_short", {
32513
32537
  throw e;
32514
32538
  }
32515
32539
  }));
32540
+ registerTool("cancel_short_generation", {
32541
+ title: "Cancel a running short generation",
32542
+ description: "Stops the caller-observed paid Short attempt, fences late provider callbacks, and refunds the 15-credit " + "generation exactly once. Pass generation_attempt from get_status so a delayed command cannot cancel a " + "newer run on the same draft. The draft is preserved for editing and regeneration. Safe to repeat with " + "the same attempt after a successful cancellation. Scope: video:generate.",
32543
+ inputSchema: {
32544
+ slug: exports_external.string().describe("Slug of the processing short"),
32545
+ generation_attempt: exports_external.number().int().min(0).describe("generation_attempt from the latest get_status response")
32546
+ },
32547
+ annotations: {
32548
+ title: "Cancel short generation",
32549
+ readOnlyHint: false,
32550
+ destructiveHint: true,
32551
+ openWorldHint: true,
32552
+ idempotentHint: true
32553
+ }
32554
+ }, tool(async (args, client) => {
32555
+ const res = await client.post(`/shorts/${args.slug}/cancel`, { generation_attempt: args.generation_attempt }, undefined);
32556
+ const data = asRecord(asRecord(res).data ?? res);
32557
+ return ok({
32558
+ ...data,
32559
+ slug: args.slug,
32560
+ charged: false,
32561
+ refund: "refunded" in data ? data.refunded : "settled (idempotent — no new credits minted on repeat)",
32562
+ next: "Edit the preserved draft, then call generate_short when ready."
32563
+ });
32564
+ }));
32516
32565
  registerTool("rerender_short", {
32517
32566
  title: "Re-render a short (FREE — 0 credits)",
32518
32567
  description: "FREE re-render: applies the short's current text/style/CTA/end-card/logo state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks, or after a logo change " + "(set_short_logo, or short_logo_position/short_logo_treatment via update_short). 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; when the pinned music bed is shorter than the " + "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " + "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).",
@@ -32756,7 +32805,7 @@ registerTool("edit_slider_slide", {
32756
32805
  }));
32757
32806
  registerTool("create_editor_ad", {
32758
32807
  title: "Create, configure, and price a multi-scene editor ad",
32759
- description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "Supplying assets here is safer than adding them after autopilot because the scenario and scenes are grounded " + "in the real product from the start. (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 a mascot or hero product that must stay visually identical with no human presenter, set " + "cast_mode:'product_only' together with product_image_path. Presenter-led casting still belongs on " + "create_editor_draft because it requires a separately configured presenter.",
32808
+ description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "A product image is analyzed into semantic project context and sent as a product-identity reference to every " + "generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " + "was authored; generated fine label text can still vary. Scene prompts are the complete content contract: describe any people, " + "products, visual continuity, absences, and composition directly in the brief. (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.)",
32760
32809
  inputSchema: {
32761
32810
  product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
32762
32811
  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."),
@@ -32764,17 +32813,16 @@ registerTool("create_editor_ad", {
32764
32813
  language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag (use "es", not "es-MX"). ' + "Drives the WHOLE video: scenario, narration script, narration voice, and captions. " + "Ask the user / infer from their locale."),
32765
32814
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
32766
32815
  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."),
32767
- 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.'),
32816
+ theme: exports_external.enum(SCENARIO_THEMES).optional().describe('Optional visual theme / genre overlay (default "none", so scene prompts drive the look). One of: none, realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
32768
32817
  voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
32769
32818
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
32770
- product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB) to attach before autopilot."),
32771
- product_description: exports_external.string().max(500).optional().describe("Exact product/character description used with product_image_path."),
32819
+ product_image_path: exports_external.string().optional().describe("Local product image (.jpg/.jpeg/.png, ≤8 MiB) used as the product-identity reference for every generated Editor scene and linked as the exact closing image unless one was already authored."),
32820
+ product_description: exports_external.string().max(500).optional().describe("Product description used to ground the scenario and narration when product_image_path is supplied."),
32772
32821
  closing_image_path: exports_external.string().optional().describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
32773
32822
  logo_path: exports_external.string().optional().describe("Local logo (.jpg/.jpeg/.png, ≤5 MiB) to overlay."),
32774
32823
  logo_treatment: exports_external.enum(["intro", "outro", "both", "none"]).optional().describe('When the logo bumper plays: "intro", "outro", "both" (default), or "none". Requires logo_path.'),
32775
32824
  logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe('Logo corner: "top-left", "top-right" (default), "bottom-left", or "bottom-right". Requires logo_path.'),
32776
32825
  logo_duration_seconds: exports_external.number().min(0.5).max(3).optional().describe("Logo bumper duration in seconds, 0.5–3 (default 1.5). Requires logo_path."),
32777
- cast_mode: exports_external.literal("product_only").optional().describe("Lock every AI scene to the supplied product/mascot reference with no people. Requires product_image_path."),
32778
32826
  max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to create/configure/price only; no credits are charged.")
32779
32827
  },
32780
32828
  outputSchema: createEditorOutput,
@@ -32791,9 +32839,6 @@ registerTool("create_editor_ad", {
32791
32839
  const promptErr = validateProductPrompt(args.product_prompt);
32792
32840
  if (promptErr)
32793
32841
  return fail(promptErr);
32794
- if (args.cast_mode === "product_only" && !args.product_image_path) {
32795
- return fail("cast_mode product_only requires product_image_path so identity can be locked before generation.");
32796
- }
32797
32842
  if ((args.logo_treatment !== undefined || args.logo_position !== undefined || args.logo_duration_seconds !== undefined) && !args.logo_path) {
32798
32843
  return fail("logo_treatment/logo_position/logo_duration_seconds require logo_path (the logo image they style). Provide logo_path or drop these settings.");
32799
32844
  }
@@ -32819,12 +32864,12 @@ registerTool("create_editor_ad", {
32819
32864
  project_intent: args.project_intent,
32820
32865
  creative_format: args.creative_format,
32821
32866
  visual_language: args.visual_language,
32822
- theme: args.theme,
32867
+ theme: args.theme ?? "none",
32823
32868
  voice_id: args.voice_id,
32824
- export_aspect_ratio: args.export_aspect_ratio,
32825
- cast_mode: args.cast_mode
32869
+ export_aspect_ratio: args.export_aspect_ratio
32826
32870
  }, editorAdCreateKey({
32827
32871
  ...args,
32872
+ theme: args.theme ?? "none",
32828
32873
  product_image_identity: productImage ? preparedUploadIdentity(productImage) : undefined,
32829
32874
  closing_image_identity: closingImage ? preparedUploadIdentity(closingImage) : undefined,
32830
32875
  logo_identity: logo ? preparedUploadIdentity(logo) : undefined
@@ -32897,8 +32942,8 @@ registerTool("start_autopilot", {
32897
32942
  inputSchema: {
32898
32943
  slug: exports_external.string().describe("Editor project slug"),
32899
32944
  language: exports_external.enum(LANGUAGES).optional().describe('Optional language override applied before launch. Bare ISO-639-1 code only, e.g. "fr" — no ' + "region/script subtag. Drives scenario, narration, voice, and captions."),
32900
- product_subject: exports_external.string().max(300).optional().describe("Optional locked product / main subject applied before launch so the AI cannot invent an " + "unrelated product."),
32901
- product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch."),
32945
+ product_subject: exports_external.string().max(300).optional().describe("Optional locked product / main subject applied before launch so the AI cannot invent an " + "unrelated product. Omit to keep the stored subject; send blank to clear it."),
32946
+ product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it."),
32902
32947
  max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
32903
32948
  restart: exports_external.boolean().optional().describe("Set true only on a completed project to quote/build a fresh creative run. The delivered timeline stays available until replacement apply.")
32904
32949
  },
@@ -32948,7 +32993,7 @@ registerTool("start_autopilot", {
32948
32993
  }));
32949
32994
  registerTool("retry_editor_batch", {
32950
32995
  title: "Retry a paused editor batch",
32951
- description: "Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " + "scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " + "paid. It does not authorize or restart a failed parent Autopilot run; after retrying, wait for the batch " + "to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " + "relaunch only with an approved cap. A batch parked on " + "insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
32996
+ description: "Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " + "scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " + "paid. Before retrying you may REPAIR the one failed scene: while paused_for_retry, set_segment_prompt can " + "edit that scene's prompt (and use_previous_frame) — but only for the exact failed position the server " + "names in batch_generation_failed_position (from get_editor); the rest of the paused batch stays " + "mutation-locked. Explicit paused retries are capped — once the failure code is free_retries_exhausted a " + "further retry is rejected (start a fresh run instead). It does not authorize or restart a failed parent " + "Autopilot run; after retrying, wait for the batch " + "to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " + "relaunch only with an approved cap. A batch parked on " + "insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
32952
32997
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
32953
32998
  annotations: {
32954
32999
  title: "Retry editor batch",
@@ -32956,7 +33001,10 @@ registerTool("retry_editor_batch", {
32956
33001
  idempotentHint: false
32957
33002
  }
32958
33003
  }, tool(async (args, client) => {
32959
- const res = await client.post(`/video-factories/${args.slug}/retry-batch-segment`);
33004
+ const command = await observedEditorBatchCommand(client, args.slug, [
33005
+ "paused_for_retry"
33006
+ ]);
33007
+ const res = await client.post(`/video-factories/${args.slug}/retry-batch-segment`, command);
32960
33008
  return ok({
32961
33009
  ...asRecord(asRecord(res).data ?? res),
32962
33010
  slug: args.slug,
@@ -32967,7 +33015,7 @@ registerTool("retry_editor_batch", {
32967
33015
  }));
32968
33016
  registerTool("cancel_editor_batch", {
32969
33017
  title: "Cancel an editor batch",
32970
- description: "Cancels an editor batch in awaiting_previews, running, or paused_for_retry so the project can be edited or " + "Autopilot can be started again. Cancellation is destructive: dispatched scene work is stopped and its prepaid " + "credits are not refunded; only an awaiting_previews batch (before dispatch) is refunded by the server. Spends " + "0 additional credits. Scope: video:generate.",
33018
+ description: "Cancels an editor batch only while it is preparing or paused_for_retry so the project can be edited or " + "Autopilot can be started again. Preparing cancellation releases the uncharged preflight. Cancelling a paused " + "batch is destructive: dispatched scene work is stopped and its prepaid credits are not refunded. Spends " + "0 additional credits. Scope: video:generate.",
32971
33019
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
32972
33020
  annotations: {
32973
33021
  title: "Cancel editor batch",
@@ -32977,7 +33025,11 @@ registerTool("cancel_editor_batch", {
32977
33025
  idempotentHint: false
32978
33026
  }
32979
33027
  }, tool(async (args, client) => {
32980
- const res = await client.post(`/video-factories/${args.slug}/cancel-batch`);
33028
+ const command = await observedEditorBatchCommand(client, args.slug, [
33029
+ "preparing",
33030
+ "paused_for_retry"
33031
+ ]);
33032
+ const res = await client.post(`/video-factories/${args.slug}/cancel-batch`, command);
32981
33033
  return ok({
32982
33034
  ...asRecord(asRecord(res).data ?? res),
32983
33035
  slug: args.slug,
@@ -32985,6 +33037,22 @@ registerTool("cancel_editor_batch", {
32985
33037
  next: "get_status"
32986
33038
  });
32987
33039
  }));
33040
+ async function observedEditorBatchCommand(client, slug, allowedStatuses) {
33041
+ const response = await client.get(`/editor/${slug}`);
33042
+ const state = asRecord(asRecord(response).data);
33043
+ const status = state.batch_generation_status;
33044
+ if (typeof status !== "string" || !allowedStatuses.includes(status)) {
33045
+ throw new Error(`Editor batch is not in an allowed phase (${allowedStatuses.join(" or ")}); refresh status before retrying.`);
33046
+ }
33047
+ const runId = state.batch_generation_run_id;
33048
+ if (runId !== null && (typeof runId !== "string" || runId.trim().length === 0)) {
33049
+ throw new Error("Editor state did not include a valid batch_generation_run_id; refresh status before retrying.");
33050
+ }
33051
+ return {
33052
+ expected_batch_run_id: runId,
33053
+ expected_batch_status: status
33054
+ };
33055
+ }
32988
33056
  registerTool("get_ai_assists", {
32989
33057
  title: "Get AI assist quota",
32990
33058
  description: "Returns the free daily AI-assist quota {used, limit, bonus, remaining, resets_at, unlock_cost, " + "unlock_batch_size}. AI assists power helper calls (generate_scenario, generate_narration, " + "enhance_prompt, suggest_next_scene, suggest_music_prompt). Default limit is 20/day. When remaining " + "hits 0 those helpers return 429 — either unlock_ai_assists (1 credit → +10) or write the content " + "yourself with set_scenario / set_narration_script / set_segment_prompt (those are free, no quota).",
@@ -33010,8 +33078,7 @@ registerTool("create_editor_draft", {
33010
33078
  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". Drives scenario, narration, voice, and captions.'),
33011
33079
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
33012
33080
  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."),
33013
- 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.'),
33014
- 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."),
33081
+ theme: exports_external.enum(SCENARIO_THEMES).optional().describe('Optional visual theme / genre overlay (default "none", so scene prompts drive the look). One of: none, realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
33015
33082
  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"),
33016
33083
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
33017
33084
  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).")
@@ -33029,15 +33096,14 @@ registerTool("create_editor_draft", {
33029
33096
  product_subject: args.product_subject,
33030
33097
  creative_format: args.creative_format,
33031
33098
  visual_language: args.visual_language,
33032
- theme: args.theme,
33033
- cast_mode: args.cast_mode,
33099
+ theme: args.theme ?? "none",
33034
33100
  voice_id: args.voice_id,
33035
33101
  export_aspect_ratio: args.export_aspect_ratio,
33036
33102
  project_intent: args.project_intent
33037
33103
  };
33038
33104
  if (prompt)
33039
33105
  body.product_prompt = prompt;
33040
- 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 ?? ""));
33106
+ 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 ?? "none", args.voice_id ?? "", args.export_aspect_ratio ?? ""));
33041
33107
  return ok({
33042
33108
  slug: created.data.slug,
33043
33109
  kind: "editor",
@@ -33060,6 +33126,9 @@ function conciseEditorState(value) {
33060
33126
  "scenario_apply_status",
33061
33127
  "scenario_prompt",
33062
33128
  "batch_generation_status",
33129
+ "batch_generation_run_id",
33130
+ "batch_generation_failed_position",
33131
+ "auto_render_after_batch",
33063
33132
  "narration_status",
33064
33133
  "narration_script",
33065
33134
  "narration_stale",
@@ -33083,9 +33152,8 @@ function conciseEditorState(value) {
33083
33152
  prompt: segment.prompt,
33084
33153
  status: segment.status,
33085
33154
  source_type: segment.source_type,
33155
+ generation_claim_token: segment.generation_claim_token,
33086
33156
  use_previous_frame: segment.use_previous_frame,
33087
- protagonist_presence: segment.protagonist_presence,
33088
- cast_mode_snapshot: segment.cast_mode_snapshot,
33089
33157
  video_stale: segment.video_stale,
33090
33158
  error_code: segment.error_code,
33091
33159
  error_message: segment.error_message,
@@ -33095,7 +33163,7 @@ function conciseEditorState(value) {
33095
33163
  }
33096
33164
  registerTool("get_editor", {
33097
33165
  title: "Get editor state (review step)",
33098
- description: "Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status, narration, audio, render, spend, and quota.",
33166
+ description: "Returns the full editor state by default for backward compatibility. Pass response_format:'concise' for a smaller review projection containing orchestration, scenario, scene prompts/status, narration_status, current_audio, current_music, latest_render, batch_generation_status, spend, and quota.",
33099
33167
  inputSchema: {
33100
33168
  slug: exports_external.string().describe("Editor project slug"),
33101
33169
  response_format: exports_external.enum(["concise", "full"]).default("full")
@@ -33124,9 +33192,9 @@ registerTool("generate_scenario", {
33124
33192
  inputSchema: {
33125
33193
  slug: exports_external.string().describe("Editor project slug"),
33126
33194
  segments_count: exports_external.number().int().min(3).max(10).optional().describe("How many scenes (3–10, server default 5)"),
33127
- creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear."),
33128
- visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Persist the visual style / render look before generating. When set it drives the look and a theme of 'none' is ignored."),
33129
- theme: exports_external.string().optional().describe("Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none)."),
33195
+ creative_format: exports_external.union([exports_external.enum(CREATIVE_FORMATS), exports_external.literal("")]).optional().describe("Persist the narrative arc / segment structure before generating. Omit to leave it unchanged; pass an empty string to clear."),
33196
+ visual_language: exports_external.union([exports_external.enum(VISUAL_LANGUAGES), exports_external.literal("")]).optional().describe("Persist the visual style / render look before generating. Omit to leave it unchanged; pass an empty string to clear. When set it drives the look and a theme of 'none' is ignored."),
33197
+ theme: exports_external.enum(SCENARIO_THEMES).optional().describe("Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none)."),
33130
33198
  auto_unlock: exports_external.boolean().optional().describe("On 429, spend 1 credit to unlock +10 assists and retry once (default false)")
33131
33199
  },
33132
33200
  annotations: {
@@ -33281,6 +33349,50 @@ registerTool("generate_segment", {
33281
33349
  const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/generate`, undefined, undefined);
33282
33350
  return ok(asRecord(res).data ?? res);
33283
33351
  }));
33352
+ registerTool("cancel_segment_generation", {
33353
+ title: "Cancel one editor scene generation",
33354
+ description: "Stops one caller-observed, individually processing Editor scene; its generation_claim_token fence " + "prevents a delayed command from cancelling a newer attempt. The token is returned by get_editor. " + "Refunds that scene's charge idempotently (a free retry creates no refund). Use cancel_editor_batch or " + "cancel_autopilot for workflow-owned scenes. After cancellation, edit the prompt if needed and call " + "generate_segment again. Scope: video:generate.",
33355
+ inputSchema: {
33356
+ slug: exports_external.string().describe("Editor project slug"),
33357
+ segment_id: exports_external.union([exports_external.number(), exports_external.string()]).describe("Processing segment id (from get_editor)"),
33358
+ generation_claim_token: exports_external.string().nullable().describe("generation_claim_token from that processing segment")
33359
+ },
33360
+ annotations: {
33361
+ title: "Cancel segment generation",
33362
+ readOnlyHint: false,
33363
+ destructiveHint: true,
33364
+ openWorldHint: true,
33365
+ idempotentHint: true
33366
+ }
33367
+ }, tool(async (args, client) => {
33368
+ const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/cancel`, { expected_generation_claim_token: args.generation_claim_token }, undefined);
33369
+ return ok({
33370
+ ...asRecord(asRecord(res).data ?? res),
33371
+ slug: args.slug,
33372
+ charged: false,
33373
+ next: "set_segment_prompt or generate_segment"
33374
+ });
33375
+ }));
33376
+ registerTool("cancel_autopilot", {
33377
+ title: "Cancel a running Autopilot run",
33378
+ description: "Stops an in-flight Autopilot run on an editor project: sets autopilot_status to cancelled so the " + "orchestration chain (scenario → scenes → narration → voice → music → render) advances no further. Use " + "this when Autopilot is still running and you want to halt it. Any scenes already charged stay charged and " + "all completed work (generated scenes, voice, music, a finished render) is kept — a step already enqueued " + "may still finish, but nothing new is started. Idempotent: a no-op that returns the project unchanged when " + "Autopilot is not running (already completed, failed, cancelled, or never launched), so it is safe to " + "repeat. It does NOT itself refund or re-run anything. For a non-Autopilot prepaid batch " + "(batch_generation_status preparing / paused_for_retry) use cancel_editor_batch; to stop one " + "individually-generated scene use cancel_segment_generation. Scope: video:generate.",
33379
+ inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33380
+ annotations: {
33381
+ title: "Cancel autopilot",
33382
+ readOnlyHint: false,
33383
+ destructiveHint: true,
33384
+ openWorldHint: true,
33385
+ idempotentHint: true
33386
+ }
33387
+ }, tool(async (args, client) => {
33388
+ const res = await client.post(`/editor/${args.slug}/autopilot/cancel`, undefined, undefined);
33389
+ return ok({
33390
+ ...asRecord(asRecord(res).data ?? res),
33391
+ slug: args.slug,
33392
+ charged: false,
33393
+ next: "get_status"
33394
+ });
33395
+ }));
33284
33396
  registerTool("regenerate_segment", {
33285
33397
  title: "Regenerate a completed segment (new version, 4 credits)",
33286
33398
  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.",
@@ -33303,7 +33415,7 @@ registerTool("regenerate_segment", {
33303
33415
  }));
33304
33416
  registerTool("generate_all_segments", {
33305
33417
  title: "Generate all pending segments (sequential)",
33306
- 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.",
33418
+ 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.",
33307
33419
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33308
33420
  annotations: {
33309
33421
  title: "Generate all segments",
@@ -33429,10 +33541,10 @@ registerTool("generate_all_segments", {
33429
33541
  }));
33430
33542
  registerTool("set_narration_script", {
33431
33543
  title: "Set the narration script",
33432
- description: "Writes your own narration script (1–12000 chars) instead of generating one. Free — no credits, no AI " + "assist. Use this in place of generate_narration when you want to control the voiceover text. Changing the narration after generating voice/music marks them stale — regenerate (generate_voice / generate_music) before render.",
33544
+ description: "Writes your own narration script (1–10000 chars) instead of generating one. Free — no credits, no AI " + "assist. Use this in place of generate_narration when you want to control the voiceover text. Changing the narration after generating voice/music marks them stale — regenerate (generate_voice / generate_music) before render.",
33433
33545
  inputSchema: {
33434
33546
  slug: exports_external.string().describe("Editor project slug"),
33435
- script: exports_external.string().min(1).max(12000).describe("Narration / voiceover text (1–12000 chars)")
33547
+ script: exports_external.string().min(1).max(1e4).describe("Narration / voiceover text (1–10000 chars)")
33436
33548
  },
33437
33549
  annotations: { title: "Set narration", ...WRITE, idempotentHint: true }
33438
33550
  }, tool(async (args, client) => {
@@ -33443,7 +33555,7 @@ registerTool("set_narration_script", {
33443
33555
  }));
33444
33556
  registerTool("generate_narration", {
33445
33557
  title: "Generate the narration (AI assist)",
33446
- description: "Generates the narration script with AI from the timeline. CONSUMES 1 AI ASSIST (free daily quota) — " + "it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " + "retried once) or write the script yourself with set_narration_script.",
33558
+ description: "Generates one coherent narration script with AI from the complete timeline. Every current scene must be completed with a known positive duration, and at least one scene must not keep original upload audio. CONSUMES 1 AI ASSIST (free daily quota) only when a fresh job is queued; an exact retry is deduplicated without consuming another assist — " + "it is free of CREDITS but NOT free of quota. On 429 set auto_unlock:true (1 credit → +10 assists, " + "retried once) or write the script yourself with set_narration_script.",
33447
33559
  inputSchema: {
33448
33560
  slug: exports_external.string().describe("Editor project slug"),
33449
33561
  auto_unlock: exports_external.boolean().optional().describe("On 429, spend 1 credit to unlock +10 assists and retry once (default false)")
@@ -33459,7 +33571,7 @@ registerTool("generate_narration", {
33459
33571
  }));
33460
33572
  registerTool("generate_voice", {
33461
33573
  title: "Generate the voiceover (3 credits)",
33462
- description: "Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." + "voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
33574
+ description: "Synthesizes the voiceover from the narration script. Costs 3 credits. Safe to retry; re-call after changing the narration or voice to regenerate." + " All current scenes must be completed with known durations first. voice_id is REQUIRED — pick one from list_voices (≤64 chars, [A-Za-z0-9_-] only). 402 fails cleanly. Editing the scenario/narration/timeline after this marks the voice stale, and render will 422 (editor_voice_stale) until you re-run generate_voice.",
33463
33575
  inputSchema: {
33464
33576
  slug: exports_external.string().describe("Editor project slug"),
33465
33577
  voice_id: exports_external.string().regex(/^[A-Za-z0-9_-]+$/).max(64).describe("Narration voice id from list_voices (required)")
@@ -33504,7 +33616,7 @@ registerTool("generate_music", {
33504
33616
  }));
33505
33617
  registerTool("render", {
33506
33618
  title: "Render the final editor video (0 credits)",
33507
- description: "Renders the final MP4 from the generated segments + voice + music. Costs 0 credits (any still-ungenerated " + "segments are auto-charged at generation cost first). Render NEVER generates or charges for voice/music; a new " + "music charge comes only from generate_music or an already-running autopilot child job. Re-rendering reflects the current segments/voice/music (it does not replay a prior render). Returns the latest_render; " + 'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' + "STALENESS: editing the scenario or narration AFTER generating voice/music marks those audio tracks stale, and render then 422s with editor_narration_stale / editor_voice_stale / editor_music_stale — regenerate the named track (generate_narration → generate_voice, and/or generate_music) before rendering.",
33619
+ description: "Renders the final MP4 from the generated segments + voice + music. Costs 0 credits. Pending or previously " + "failed AI scenes are generated first and charged as a new batch. Render NEVER generates or charges for voice/music; a new " + "music charge comes only from generate_music or an already-running autopilot child job. Re-rendering reflects the current segments/voice/music (it does not replay a prior render). Returns the latest_render; " + 'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' + "STALENESS: editing the scenario or narration AFTER generating voice/music marks those audio tracks stale, and render then 422s with editor_narration_stale / editor_voice_stale / editor_music_stale — regenerate the named track (generate_narration → generate_voice, and/or generate_music) before rendering.",
33508
33620
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33509
33621
  annotations: { title: "Render", ...WRITE, idempotentHint: false }
33510
33622
  }, tool(async (args, client) => {
@@ -33571,7 +33683,7 @@ registerTool("suggest_music_prompt", {
33571
33683
  }));
33572
33684
  registerTool("list_renders", {
33573
33685
  title: "List renders for an editor project",
33574
- description: "Lists every render version for an editor project: {id, version, status, video_url, duration_seconds, " + "error_message, inserted_at}. Use it to find a FAILED render to retry, or to recover a finished video_url " + "(presigned, ~24h). Read-only.",
33686
+ description: "Lists every render version for an editor project: {id, version, status, video_url, thumbnail_url, " + "duration_seconds, error_message, retryable, combination_hash, is_stale, inserted_at}. Use it to find a " + "FAILED render to retry, or to recover a finished video_url (presigned, ~24h). status is pending | " + "processing | completed | failed; is_stale null means unknown, not current. Read-only.",
33575
33687
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33576
33688
  annotations: { title: "List renders", ...RO }
33577
33689
  }, tool(async (args, client) => {
@@ -33744,7 +33856,7 @@ registerTool("use_asset_for_segment", {
33744
33856
  }));
33745
33857
  registerTool("set_product", {
33746
33858
  title: "Attach a product image (from a local file)",
33747
- description: "Uploads a local product image and attaches it to the project as the product. Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. The FIRST product added " + "defaults every pending AI scene to feature it 'throughout' (change with set_product_placement). Optional " + "description (≤500 chars) guides how it's woven into scenes.",
33859
+ description: "Uploads a local product image and attaches it to the project as the product. Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Its pixels guide product identity " + "in every generated Editor scene and its confirmed description grounds scenario and narration. The exact photo " + "is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary. " + "WARNING: replacing the product photo on a project that already has generated AI scenes invalidates their product " + "identity — those scenes were built from the old photo and must be regenerated to match the new one.",
33748
33860
  inputSchema: {
33749
33861
  slug: exports_external.string().describe("Editor project slug"),
33750
33862
  file_path: exports_external.string().describe("Local product image path (.jpg/.jpeg/.png)"),
@@ -33756,22 +33868,6 @@ registerTool("set_product", {
33756
33868
  const res = await client.post(`/editor/${args.slug}/product/confirm`, { s3_key, product_description: args.description });
33757
33869
  return ok(asRecord(res).data ?? res);
33758
33870
  }));
33759
- registerTool("set_product_placement", {
33760
- title: "Set default product placement on pending scenes (0 credits)",
33761
- description: "Sets where the product appears across pending (never-generated) AI scenes. mode is 'throughout' (every scene) " + "or 'end' (closing scene only; requires a first+last-frame capable model). 0 credits. Add a product first with " + "set_product. (There is no 'none' — to remove the product, that's a separate action.)",
33762
- inputSchema: {
33763
- slug: exports_external.string().describe("Editor project slug"),
33764
- mode: exports_external.enum(["throughout", "end"]).describe("'throughout' or 'end'")
33765
- },
33766
- annotations: {
33767
- title: "Set product placement",
33768
- ...WRITE,
33769
- idempotentHint: true
33770
- }
33771
- }, tool(async (args, client) => {
33772
- const res = await client.post(`/editor/${args.slug}/product/apply-default-placement`, { placement: args.mode });
33773
- return ok(asRecord(res).data ?? res);
33774
- }));
33775
33871
  registerTool("set_closing_image", {
33776
33872
  title: "Set the closing-card image (0 credits)",
33777
33873
  description: "Sets the optional ~2s end-card image. Either upload a local file (file_path; .jpg/.jpeg/.png ≤20 MiB, confined " + "to HUBFLUENCER_INPUT_DIR/cwd) OR reuse the project's product image (from_product:true — a 0-credit server-side " + "copy, no upload). If a closing image already exists, pass overwrite:true to replace it (else " + "editor_closing_image_exists).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
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>",