@hubfluencer/mcp 0.12.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/README.md CHANGED
@@ -68,7 +68,7 @@ claude mcp add hubfluencer --env HUBFLUENCER_API_TOKEN=YOUR_TOKEN -- npx -y @hub
68
68
 
69
69
  ## Tools
70
70
 
71
- 95 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
71
+ 97 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
72
72
  below are the ground truth. **"$0" means it spends no video credits**; planning/AI-helper tools
73
73
  instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_assists`: 1 credit
74
74
  → +10). Reads need the `video:read` scope; writes need `video:generate`. Nothing here needs
@@ -82,12 +82,14 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
82
82
  | `create_short` | Create a short draft (12s vertical: two 6s AI segments + title overlay + music; 14s when it ends on a poster or brand-lockup end card). Set `headline` (≈4 words, ≤40 chars), `subheadline`, `music_vibe`, and an explicit `visual_language` (`kinetic_creator` is a strong default) — and **always `cta_text`** (a short without a CTA doesn't convert; blank `cta_text` → the server renders a neutral localized ask, e.g. "Learn more" — a truly CTA-less short is not currently supported). End-card fields: `closing_claim` (≤80), `brand_name` (≤40), `end_card` (`"auto"`\|`"none"`). Other opt-in conversion graphics: `offer_text`/`badge_text`/`star_rating` — only what you can substantiate. | **0** |
83
83
  | `update_short` | Patch an existing short draft — brief (`product_prompt`), copy, styling, end-card fields (`closing_claim`/`brand_name`/`poster_includes_lockup`/`end_card`), logo overlay placement (`short_logo_position`/`short_logo_treatment`), or the opt-in conversion graphics; only the fields you pass change. Clear one with `""` (`[]` for `text_beats`, `0` for `star_rating`). Apply text/style/CTA/end-card/logo edits to a generated short free with `rerender_short`; footage/music edits need `generate_short`. | **0** |
84
84
  | `generate_short` | Render an existing short — re-rolls the footage + music (fresh AI generation). If headline/subheadline/beats are all blank the server auto-writes the copy first (`skip_auto_text:true` for a deliberately bare clip). Safe to re-call (in-flight = reported, no double charge; a failed short can re-generate). | **15** |
85
+ | `cancel_short_generation` | Stop the caller-observed paid short attempt, fence late workers from publishing, preserve the draft, and restore its 15 credits idempotently. Requires `generation_attempt` from `get_status`; stale commands cannot cancel a newer run. | **full refund** |
85
86
  | `rerender_short` | **FREE re-render** — applies the short's current text/style/CTA/end-card/logo state over the already-paid footage + music. Use after `update_short` for copy/style/CTA tweaks, or after a logo change (`set_short_logo`, or `short_logo_position`/`short_logo_treatment`); footage or music changes need `generate_short` instead. Needs one completed generation first (422 `short_not_ready` otherwise); changed footage/music inputs return 422 `short_paid_regeneration_required`. A failed free re-render leaves the delivered video intact — retry `rerender_short`, it stays free. | **0** |
86
87
  | `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
87
88
  | `create_editor_ad` | Create an editor project, attach optional local product/logo/closing assets, and return the live autopilot quote. Omit `max_credits` to stop free; provide an approved cap to launch. Write every required person, product, object, and continuity decision directly in the prompt. | **0** until capped launch |
88
89
  | `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; launch only with an explicit `max_credits` that covers the estimate and an adequate balance. On a completed project, pass `restart:true` to quote and build a fresh creative run; omit it to resume pending edits. | **0** quote; credits on capped launch |
89
90
  | `retry_editor_batch` | Recover `batch_retry_required` by atomically re-queueing the failed scene and resuming its prepaid batch. If the parent Autopilot run is terminal, quote/relaunch it separately afterward. | **0 additional** |
90
91
  | `cancel_editor_batch` | Cancel an editor batch only while it is `preparing` or `paused_for_retry` (awaiting-previews/running batches are worker-owned and not cancellable). Preparing cancellation releases the uncharged preflight; paused cancellation is destructive — prepaid credits are not refunded. | **0 additional** |
92
+ | `cancel_autopilot` | Stop a **running** Autopilot run: flips `autopilot_status` to `cancelled` so the chain (scenario → scenes → narration → voice → music → render) advances no further. Charged scenes stay charged and all completed work is kept; a step already enqueued may still finish. Idempotent — a no-op when Autopilot isn't running. (Use `cancel_editor_batch` for a non-Autopilot prepaid batch, `cancel_segment_generation` for one scene.) | **0 additional** |
91
93
  | `create_slider` | Create an image-carousel draft: one prompt → N still slides + a ready-to-post caption + hashtags. `mode` `creative` (story) or `ad_driven` (facts). | **0** |
92
94
  | `generate_slider` | Render a carousel (copy → AI backgrounds → composite). Poll `get_slider` until `completed`. | **1 / slide** (3–10) |
93
95
  | `get_slider` | Read a carousel's status and, when completed, the per-slide image URLs + caption + hashtags. | **0** |
@@ -106,12 +108,13 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
106
108
  | `set_scene_count` | Grow/shrink to N scenes (1–20); deletes only trailing un-generated scenes. Each AI scene is a fixed 8s. | **0** |
107
109
  | `set_segment_prompt` | Write one scene's prompt (1–2000 chars), identified by segment id. | **0** |
108
110
  | `generate_segment` | Render one **pending/failed** scene (re-call to retry a failed one). | **5** |
111
+ | `cancel_segment_generation` | Stop the caller-observed individually generated scene, fence its provider job from publishing, and restore any charged credits idempotently. Requires that scene's `generation_claim_token` from `get_editor`; stale commands cannot cancel a newer attempt. Batch/Autopilot scenes use their workflow-level controls. | **full refund** |
109
112
  | `regenerate_segment` | Re-roll a **completed** scene as a new version (optional new `prompt`). Mid-timeline: the next scene's transition can degrade because it was seeded from the old version's last frame. | **4** |
110
113
  | `generate_all_segments` | Render every not-yet-completed scene in order (continuity preserved); capped ~280s → re-run to continue. | **5 / segment** |
111
114
  | `set_narration_script` / `generate_narration` | Write your own voiceover script (free) **or** AI-draft one coherent script after every scene has completed with a known positive duration. AI drafting requires at least one scene without retained original audio and consumes an assist only when a fresh job is queued (exact retries dedupe for free). | free / 1 assist |
112
115
  | `generate_voice` | Synthesize the voiceover. `voice_id` required (see `list_voices`). | **3** |
113
116
  | `generate_music` | Generate background music (each call is a fresh take). | **5** |
114
- | `render` | Final MP4. Auto-charges any still-ungenerated scenes first (4/scene in a batch of ≥3). | **0** |
117
+ | `render` | Final MP4. Auto-charges and generates any pending or failed scenes first (4/scene in a batch of ≥3). | **0** |
115
118
  | `list_renders` | List 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`) — find a failed render or recover a finished URL. | **0** |
116
119
  | `retry_render` | Re-run a **failed** render from its exact saved snapshot (recovery without re-driving the pipeline). | **0** |
117
120
  | `enhance_prompt` / `suggest_next_scene` / `suggest_music_prompt` | AI helpers along the way. | 1 assist each |
@@ -159,9 +162,15 @@ completed | failed`). Batch state is `idle | preparing | awaiting_previews | run
159
162
  paused_for_retry | paused_insufficient_credits | completed | cancelled | failed`.
160
163
  Only `paused_for_retry` is resumed by `retry_editor_batch`; `cancel_editor_batch` works
161
164
  only while the batch is `preparing` or `paused_for_retry` — awaiting-previews/running
162
- batches are worker-owned and cannot be cancelled. Preparing cancellation releases the
163
- uncharged preflight (nothing was charged yet); paused cancellation is destructive and
164
- non-refundable. Both tools re-read the editor state and echo `expected_batch_run_id` +
165
+ batches are worker-owned and cannot be cancelled. While `paused_for_retry` you may first
166
+ REPAIR the one failed scene before retrying: `set_segment_prompt` edits its prompt (and
167
+ `use_previous_frame`), but only for the exact failed position the server names in
168
+ `batch_generation_failed_position` — the rest of the paused batch stays mutation-locked, and
169
+ explicit paused retries are capped (`free_retries_exhausted`). Preparing cancellation releases
170
+ the uncharged preflight (nothing was charged yet); paused cancellation is destructive and
171
+ non-refundable. To stop a **running** parent Autopilot run (not a paused batch), use
172
+ `cancel_autopilot` — it flips `autopilot_status` to `cancelled`, keeps completed work and
173
+ charged scenes, and is a no-op once the run has finished. Both tools re-read the editor state and echo `expected_batch_run_id` +
165
174
  `expected_batch_status` with the REST command, so a phase change between poll and command
166
175
  is a safe 409 `batch_command_stale` (re-poll and retry) instead of acting on newer work;
167
176
  the REST endpoints reject editor commands that omit these fields with 409
@@ -281,7 +290,7 @@ If it returns `terminal:false`, the render is still going — call `wait_for_com
281
290
  `create_short` → optional `generate_hook_variations` (test hook angles free) / `generate_short_text` → `generate_short` → `wait_for_completion {kind:"short"}` → `download_result`, or
282
291
  `create_editor_ad` (free configure + quote) → obtain spend approval → `start_autopilot {slug, max_credits}` → `wait_for_completion {kind:"editor"}` → `download_result`.
283
292
 
284
- If waiting returns `stage:"batch_retry_required"`, call `retry_editor_batch` to continue the prepaid failed scene, or `cancel_editor_batch` to abandon the batch. Resolve that child batch before calling `start_autopilot` again; a retry does not itself authorize a new parent Autopilot run.
293
+ If waiting returns `stage:"batch_retry_required"`, call `retry_editor_batch` to continue the prepaid failed scene (optionally repairing its prompt first with `set_segment_prompt` — see the batch-state notes above), or `cancel_editor_batch` to abandon the batch. Resolve that child batch before calling `start_autopilot` again; a retry does not itself authorize a new parent Autopilot run. To halt a **running** Autopilot run outright, call `cancel_autopilot`.
285
294
 
286
295
  **Iterate a short for free (never re-roll):** the first `generate_short` (15 credits) buys the footage +
287
296
  music; after that, every text/style/CTA/end-card/logo tweak re-renders over them for **0 credits** —
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
  }
@@ -30257,7 +30259,7 @@ function readStoredCredentials() {
30257
30259
  // package.json
30258
30260
  var package_default = {
30259
30261
  name: "@hubfluencer/mcp",
30260
- version: "0.12.0",
30262
+ version: "0.13.0",
30261
30263
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30262
30264
  license: "MIT",
30263
30265
  author: "Monocursive <contact@monocursive.com>",
@@ -30575,6 +30577,7 @@ var getStatusOutput = exports_external.object({
30575
30577
  failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30576
30578
  failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30577
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."),
30578
30581
  latest_render_free: exports_external.boolean().optional(),
30579
30582
  last_free_rerender_failed: exports_external.boolean().optional()
30580
30583
  }).passthrough();
@@ -30594,6 +30597,7 @@ var waitForCompletionOutput = exports_external.object({
30594
30597
  failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30595
30598
  failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30596
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."),
30597
30601
  latest_render_free: exports_external.boolean().optional(),
30598
30602
  last_free_rerender_failed: exports_external.boolean().optional(),
30599
30603
  saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
@@ -31798,6 +31802,23 @@ var VISUAL_LANGUAGES = [
31798
31802
  "startup_explainer",
31799
31803
  "luxury_minimal"
31800
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
+ ];
31801
31822
  var LANGUAGES = [
31802
31823
  "en",
31803
31824
  "fr",
@@ -31924,7 +31945,7 @@ registerTool("make_video", {
31924
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"]'),
31925
31946
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("SHORTS + EDITOR: narrative arc / segment structure. Omit for Auto/generic."),
31926
31947
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("SHORTS + EDITOR: visual style direction and render look. Default app choice is kinetic_creator."),
31927
- 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"),
31928
31949
  save_path: exports_external.string().optional().describe("Optional .mp4 path to download to (confined to HUBFLUENCER_OUTPUT_DIR or cwd)"),
31929
31950
  max_wait_seconds: exports_external.number().optional().describe("Block budget seconds (default 240, capped 10–280)"),
31930
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."),
@@ -32003,8 +32024,8 @@ registerTool("make_video", {
32003
32024
  voice_id: args.voice_id,
32004
32025
  creative_format: args.creative_format,
32005
32026
  visual_language: args.visual_language,
32006
- theme: args.theme
32007
- }, makeVideoEditorCreateKey(args));
32027
+ theme: args.theme ?? "none"
32028
+ }, makeVideoEditorCreateKey({ ...args, theme: args.theme ?? "none" }));
32008
32029
  slug = created.data.slug;
32009
32030
  }
32010
32031
  const costPath = kind === "short" ? `/shorts/${slug}/cost` : `/editor/${slug}/autopilot/cost`;
@@ -32155,7 +32176,7 @@ registerTool("create_short", {
32155
32176
  subheadline: exports_external.string().max(200).optional().describe("The SECONDARY title / supporting line under the headline. ≤200 chars."),
32156
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."),
32157
32178
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling. Good default: kinetic_creator."),
32158
- 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."),
32159
32180
  music_vibe: exports_external.string().optional().describe("Background-music mood. Recognized: Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz."),
32160
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"]'),
32161
32182
  text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("Where the title overlay sits (default bottom)"),
@@ -32253,7 +32274,7 @@ registerTool("update_short", {
32253
32274
  subheadline: exports_external.string().max(200).optional().describe("Secondary title / supporting line. ≤200 chars."),
32254
32275
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal."),
32255
32276
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling."),
32256
- 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."),
32257
32278
  music_vibe: exports_external.string().optional().describe("Background-music mood: Upbeat, Cinematic, Minimal, Luxury, Playful, Jazz."),
32258
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"]'),
32259
32280
  text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("Where the title overlay sits."),
@@ -32516,6 +32537,31 @@ registerTool("generate_short", {
32516
32537
  throw e;
32517
32538
  }
32518
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
+ }));
32519
32565
  registerTool("rerender_short", {
32520
32566
  title: "Re-render a short (FREE — 0 credits)",
32521
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).",
@@ -32767,7 +32813,7 @@ registerTool("create_editor_ad", {
32767
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."),
32768
32814
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
32769
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."),
32770
- 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.'),
32771
32817
  voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
32772
32818
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
32773
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."),
@@ -32818,11 +32864,12 @@ registerTool("create_editor_ad", {
32818
32864
  project_intent: args.project_intent,
32819
32865
  creative_format: args.creative_format,
32820
32866
  visual_language: args.visual_language,
32821
- theme: args.theme,
32867
+ theme: args.theme ?? "none",
32822
32868
  voice_id: args.voice_id,
32823
32869
  export_aspect_ratio: args.export_aspect_ratio
32824
32870
  }, editorAdCreateKey({
32825
32871
  ...args,
32872
+ theme: args.theme ?? "none",
32826
32873
  product_image_identity: productImage ? preparedUploadIdentity(productImage) : undefined,
32827
32874
  closing_image_identity: closingImage ? preparedUploadIdentity(closingImage) : undefined,
32828
32875
  logo_identity: logo ? preparedUploadIdentity(logo) : undefined
@@ -32946,7 +32993,7 @@ registerTool("start_autopilot", {
32946
32993
  }));
32947
32994
  registerTool("retry_editor_batch", {
32948
32995
  title: "Retry a paused editor batch",
32949
- 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.",
32950
32997
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
32951
32998
  annotations: {
32952
32999
  title: "Retry editor batch",
@@ -33031,7 +33078,7 @@ registerTool("create_editor_draft", {
33031
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.'),
33032
33079
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
33033
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."),
33034
- 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.'),
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.'),
33035
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"),
33036
33083
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
33037
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).")
@@ -33049,14 +33096,14 @@ registerTool("create_editor_draft", {
33049
33096
  product_subject: args.product_subject,
33050
33097
  creative_format: args.creative_format,
33051
33098
  visual_language: args.visual_language,
33052
- theme: args.theme,
33099
+ theme: args.theme ?? "none",
33053
33100
  voice_id: args.voice_id,
33054
33101
  export_aspect_ratio: args.export_aspect_ratio,
33055
33102
  project_intent: args.project_intent
33056
33103
  };
33057
33104
  if (prompt)
33058
33105
  body.product_prompt = prompt;
33059
- 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 ?? ""));
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 ?? ""));
33060
33107
  return ok({
33061
33108
  slug: created.data.slug,
33062
33109
  kind: "editor",
@@ -33080,6 +33127,7 @@ function conciseEditorState(value) {
33080
33127
  "scenario_prompt",
33081
33128
  "batch_generation_status",
33082
33129
  "batch_generation_run_id",
33130
+ "batch_generation_failed_position",
33083
33131
  "auto_render_after_batch",
33084
33132
  "narration_status",
33085
33133
  "narration_script",
@@ -33104,6 +33152,7 @@ function conciseEditorState(value) {
33104
33152
  prompt: segment.prompt,
33105
33153
  status: segment.status,
33106
33154
  source_type: segment.source_type,
33155
+ generation_claim_token: segment.generation_claim_token,
33107
33156
  use_previous_frame: segment.use_previous_frame,
33108
33157
  video_stale: segment.video_stale,
33109
33158
  error_code: segment.error_code,
@@ -33145,7 +33194,7 @@ registerTool("generate_scenario", {
33145
33194
  segments_count: exports_external.number().int().min(3).max(10).optional().describe("How many scenes (3–10, server default 5)"),
33146
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."),
33147
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."),
33148
- theme: exports_external.string().optional().describe("Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none)."),
33197
+ theme: exports_external.enum(SCENARIO_THEMES).optional().describe("Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none)."),
33149
33198
  auto_unlock: exports_external.boolean().optional().describe("On 429, spend 1 credit to unlock +10 assists and retry once (default false)")
33150
33199
  },
33151
33200
  annotations: {
@@ -33300,6 +33349,50 @@ registerTool("generate_segment", {
33300
33349
  const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/generate`, undefined, undefined);
33301
33350
  return ok(asRecord(res).data ?? res);
33302
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
+ }));
33303
33396
  registerTool("regenerate_segment", {
33304
33397
  title: "Regenerate a completed segment (new version, 4 credits)",
33305
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.",
@@ -33322,7 +33415,7 @@ registerTool("regenerate_segment", {
33322
33415
  }));
33323
33416
  registerTool("generate_all_segments", {
33324
33417
  title: "Generate all pending segments (sequential)",
33325
- 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.",
33326
33419
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33327
33420
  annotations: {
33328
33421
  title: "Generate all segments",
@@ -33523,7 +33616,7 @@ registerTool("generate_music", {
33523
33616
  }));
33524
33617
  registerTool("render", {
33525
33618
  title: "Render the final editor video (0 credits)",
33526
- 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.",
33527
33620
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33528
33621
  annotations: { title: "Render", ...WRITE, idempotentHint: false }
33529
33622
  }, tool(async (args, client) => {
@@ -33763,7 +33856,7 @@ registerTool("use_asset_for_segment", {
33763
33856
  }));
33764
33857
  registerTool("set_product", {
33765
33858
  title: "Attach a product image (from a local file)",
33766
- 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.",
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.",
33767
33860
  inputSchema: {
33768
33861
  slug: exports_external.string().describe("Editor project slug"),
33769
33862
  file_path: exports_external.string().describe("Local product image path (.jpg/.jpeg/.png)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.12.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>",
package/src/core.ts CHANGED
@@ -275,6 +275,8 @@ export interface NormalizedStatus {
275
275
  failure_source?: string;
276
276
  /** Short only: paid generation attempt number that failed. */
277
277
  failure_attempt?: number;
278
+ /** Short only: current paid attempt; echo when cancelling. */
279
+ generation_attempt?: number;
278
280
  /** Editor only: the newest delivered render no longer matches the live project. */
279
281
  stale?: boolean;
280
282
  /** Editor only: current scene ids that are still generating. */
@@ -361,6 +363,8 @@ export function normalizeStatus(
361
363
  status.failure_source = d.failure_source;
362
364
  if (typeof d.failure_attempt === "number")
363
365
  status.failure_attempt = d.failure_attempt;
366
+ if (typeof d.generation_attempt === "number")
367
+ status.generation_attempt = d.generation_attempt;
364
368
  // Additive free-re-render bookkeeping (2026-07 API): surfaced only when
365
369
  // the server actually sends the booleans, so older payloads normalize
366
370
  // byte-identically. A failed free re-render does NOT flip stage to
package/src/index.ts CHANGED
@@ -565,6 +565,25 @@ const VISUAL_LANGUAGES = [
565
565
  "startup_explainer",
566
566
  "luxury_minimal",
567
567
  ] as const;
568
+ // Closed API enum from VideoFactories.ScenarioThemes. Keeping this explicit in
569
+ // MCP prevents an agent typo from spending a round trip on an avoidable 422.
570
+ const SCENARIO_THEMES = [
571
+ "none",
572
+ "realistic",
573
+ "cinematic",
574
+ "anime",
575
+ "sci_fi",
576
+ "fantasy",
577
+ "noir",
578
+ "superhero",
579
+ "horror",
580
+ "mockumentary",
581
+ "sports",
582
+ "gaming",
583
+ "retro_80s",
584
+ "minimalist",
585
+ "cyberpunk",
586
+ ] as const;
568
587
  // The languages the API's VideoFactory.language accepts (video_factory.ex
569
588
  // @supported_languages, validate_inclusion at create + autopilot). It's a CLOSED
570
589
  // set of bare ISO-639-1 codes — the server rejects region/script subtags ("en",
@@ -914,12 +933,12 @@ registerTool(
914
933
  "SHORTS + EDITOR: visual style direction and render look. Default app choice is kinetic_creator.",
915
934
  ),
916
935
  theme: z
917
- .string()
936
+ .enum(SCENARIO_THEMES)
918
937
  .optional()
919
938
  .describe(
920
939
  "Visual theme: editor genre overlay, and legacy shorts fallback only when visual_language is unset. " +
921
- "For editor, when visual_language is set it drives the look and 'none' is ignored. " +
922
- "none (literal — no imposed style), realistic (default), cinematic, " +
940
+ "For editor, visual_language drives the look when present; make_video defaults theme to none " +
941
+ "(literal — no imposed genre style). Options: none, realistic, cinematic, " +
923
942
  "anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " +
924
943
  "minimalist, cyberpunk",
925
944
  ),
@@ -1115,12 +1134,14 @@ registerTool(
1115
1134
  voice_id: args.voice_id,
1116
1135
  creative_format: args.creative_format,
1117
1136
  visual_language: args.visual_language,
1118
- theme: args.theme,
1137
+ // Prompt-first default: do not silently inject the API's legacy
1138
+ // realistic/neutral art direction when the caller authored a look.
1139
+ theme: args.theme ?? "none",
1119
1140
  },
1120
1141
  // Fold every create-affecting param into the key so two calls that
1121
1142
  // differ only by style/theme/voice/aspect get distinct drafts (shared
1122
1143
  // with the golden-pin test via core.ts).
1123
- makeVideoEditorCreateKey(args),
1144
+ makeVideoEditorCreateKey({ ...args, theme: args.theme ?? "none" }),
1124
1145
  );
1125
1146
  slug = created.data.slug;
1126
1147
  }
@@ -1530,7 +1551,7 @@ registerTool(
1530
1551
  "Visual language for Veo direction and render styling. Good default: kinetic_creator.",
1531
1552
  ),
1532
1553
  theme: z
1533
- .string()
1554
+ .enum(SCENARIO_THEMES)
1534
1555
  .optional()
1535
1556
  .describe(
1536
1557
  "Deprecated for shorts: legacy visual theme used only when visual_language is unset.",
@@ -1837,7 +1858,7 @@ registerTool(
1837
1858
  .optional()
1838
1859
  .describe("Visual language for Veo direction and render styling."),
1839
1860
  theme: z
1840
- .string()
1861
+ .enum(SCENARIO_THEMES)
1841
1862
  .optional()
1842
1863
  .describe(
1843
1864
  "Deprecated for shorts: legacy visual theme used only when visual_language is unset.",
@@ -2529,6 +2550,55 @@ registerTool(
2529
2550
  }),
2530
2551
  );
2531
2552
 
2553
+ registerTool(
2554
+ "cancel_short_generation",
2555
+ {
2556
+ title: "Cancel a running short generation",
2557
+ description:
2558
+ "Stops the caller-observed paid Short attempt, fences late provider callbacks, and refunds the 15-credit " +
2559
+ "generation exactly once. Pass generation_attempt from get_status so a delayed command cannot cancel a " +
2560
+ "newer run on the same draft. The draft is preserved for editing and regeneration. Safe to repeat with " +
2561
+ "the same attempt after a successful cancellation. Scope: video:generate.",
2562
+ inputSchema: {
2563
+ slug: z.string().describe("Slug of the processing short"),
2564
+ generation_attempt: z
2565
+ .number()
2566
+ .int()
2567
+ .min(0)
2568
+ .describe("generation_attempt from the latest get_status response"),
2569
+ },
2570
+ annotations: {
2571
+ title: "Cancel short generation",
2572
+ readOnlyHint: false,
2573
+ destructiveHint: true,
2574
+ openWorldHint: true,
2575
+ idempotentHint: true,
2576
+ },
2577
+ },
2578
+ tool(async (args: { slug: string; generation_attempt: number }, client) => {
2579
+ const res = await client.post<{ data: unknown }>(
2580
+ `/shorts/${args.slug}/cancel`,
2581
+ { generation_attempt: args.generation_attempt },
2582
+ undefined,
2583
+ );
2584
+ const data = asRecord(asRecord(res).data ?? res);
2585
+ return ok({
2586
+ ...data,
2587
+ slug: args.slug,
2588
+ charged: false,
2589
+ // The cancel endpoint returns the short's STATE, not a refund receipt,
2590
+ // so don't assert refunded:true on every call — a repeat cancel is a
2591
+ // no-op that mints no new credits. Echo the server's own flag if it ever
2592
+ // sends one; otherwise describe the honest idempotent settlement.
2593
+ refund:
2594
+ "refunded" in data
2595
+ ? data.refunded
2596
+ : "settled (idempotent — no new credits minted on repeat)",
2597
+ next: "Edit the preserved draft, then call generate_short when ready.",
2598
+ });
2599
+ }),
2600
+ );
2601
+
2532
2602
  registerTool(
2533
2603
  "rerender_short",
2534
2604
  {
@@ -3066,10 +3136,10 @@ registerTool(
3066
3136
  "Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored.",
3067
3137
  ),
3068
3138
  theme: z
3069
- .string()
3139
+ .enum(SCENARIO_THEMES)
3070
3140
  .optional()
3071
3141
  .describe(
3072
- '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.',
3142
+ '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.',
3073
3143
  ),
3074
3144
  voice_id: z
3075
3145
  .string()
@@ -3226,7 +3296,7 @@ registerTool(
3226
3296
  project_intent: args.project_intent,
3227
3297
  creative_format: args.creative_format,
3228
3298
  visual_language: args.visual_language,
3229
- theme: args.theme,
3299
+ theme: args.theme ?? "none",
3230
3300
  voice_id: args.voice_id,
3231
3301
  export_aspect_ratio: args.export_aspect_ratio,
3232
3302
  },
@@ -3235,6 +3305,7 @@ registerTool(
3235
3305
  // Composition lives in core.ts, shared with the golden-pin test.
3236
3306
  editorAdCreateKey({
3237
3307
  ...args,
3308
+ theme: args.theme ?? "none",
3238
3309
  product_image_identity: productImage
3239
3310
  ? preparedUploadIdentity(productImage)
3240
3311
  : undefined,
@@ -3507,7 +3578,12 @@ registerTool(
3507
3578
  description:
3508
3579
  "Recovers an editor whose normalized status is batch_retry_required by atomically re-queueing the failed " +
3509
3580
  "scene and resuming its prepaid batch. This spends 0 additional credits: the batch generation was already " +
3510
- "paid. It does not authorize or restart a failed parent Autopilot run; after retrying, wait for the batch " +
3581
+ "paid. Before retrying you may REPAIR the one failed scene: while paused_for_retry, set_segment_prompt can " +
3582
+ "edit that scene's prompt (and use_previous_frame) — but only for the exact failed position the server " +
3583
+ "names in batch_generation_failed_position (from get_editor); the rest of the paused batch stays " +
3584
+ "mutation-locked. Explicit paused retries are capped — once the failure code is free_retries_exhausted a " +
3585
+ "further retry is rejected (start a fresh run instead). It does not authorize or restart a failed parent " +
3586
+ "Autopilot run; after retrying, wait for the batch " +
3511
3587
  "to settle. If the parent is then terminal, call start_autopilot without max_credits for a fresh quote and " +
3512
3588
  "relaunch only with an approved cap. A batch parked on " +
3513
3589
  "insufficient_credits needs a top-up/scope change instead. Scope: video:generate.",
@@ -3713,10 +3789,10 @@ registerTool(
3713
3789
  "Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored.",
3714
3790
  ),
3715
3791
  theme: z
3716
- .string()
3792
+ .enum(SCENARIO_THEMES)
3717
3793
  .optional()
3718
3794
  .describe(
3719
- '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.',
3795
+ '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.',
3720
3796
  ),
3721
3797
  voice_id: z
3722
3798
  .string()
@@ -3766,7 +3842,7 @@ registerTool(
3766
3842
  product_subject: args.product_subject,
3767
3843
  creative_format: args.creative_format,
3768
3844
  visual_language: args.visual_language,
3769
- theme: args.theme,
3845
+ theme: args.theme ?? "none",
3770
3846
  voice_id: args.voice_id,
3771
3847
  export_aspect_ratio: args.export_aspect_ratio,
3772
3848
  project_intent: args.project_intent,
@@ -3786,7 +3862,7 @@ registerTool(
3786
3862
  args.language ?? "en",
3787
3863
  args.creative_format ?? "",
3788
3864
  args.visual_language ?? "",
3789
- args.theme ?? "",
3865
+ args.theme ?? "none",
3790
3866
  args.voice_id ?? "",
3791
3867
  args.export_aspect_ratio ?? "",
3792
3868
  ),
@@ -3817,6 +3893,7 @@ function conciseEditorState(value: unknown): Record<string, unknown> {
3817
3893
  "scenario_prompt",
3818
3894
  "batch_generation_status",
3819
3895
  "batch_generation_run_id",
3896
+ "batch_generation_failed_position",
3820
3897
  "auto_render_after_batch",
3821
3898
  "narration_status",
3822
3899
  "narration_script",
@@ -3841,6 +3918,7 @@ function conciseEditorState(value: unknown): Record<string, unknown> {
3841
3918
  prompt: segment.prompt,
3842
3919
  status: segment.status,
3843
3920
  source_type: segment.source_type,
3921
+ generation_claim_token: segment.generation_claim_token,
3844
3922
  use_previous_frame: segment.use_previous_frame,
3845
3923
  video_stale: segment.video_stale,
3846
3924
  error_code: segment.error_code,
@@ -3938,7 +4016,7 @@ registerTool(
3938
4016
  "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.",
3939
4017
  ),
3940
4018
  theme: z
3941
- .string()
4019
+ .enum(SCENARIO_THEMES)
3942
4020
  .optional()
3943
4021
  .describe(
3944
4022
  "Persist the genre overlay / theme before generating (e.g. realistic, cinematic, anime, none).",
@@ -4247,6 +4325,96 @@ registerTool(
4247
4325
  }),
4248
4326
  );
4249
4327
 
4328
+ registerTool(
4329
+ "cancel_segment_generation",
4330
+ {
4331
+ title: "Cancel one editor scene generation",
4332
+ description:
4333
+ "Stops one caller-observed, individually processing Editor scene; its generation_claim_token fence " +
4334
+ "prevents a delayed command from cancelling a newer attempt. The token is returned by get_editor. " +
4335
+ "Refunds that scene's charge idempotently (a free retry creates no refund). Use cancel_editor_batch or " +
4336
+ "cancel_autopilot for workflow-owned scenes. After cancellation, edit the prompt if needed and call " +
4337
+ "generate_segment again. Scope: video:generate.",
4338
+ inputSchema: {
4339
+ slug: z.string().describe("Editor project slug"),
4340
+ segment_id: z
4341
+ .union([z.number(), z.string()])
4342
+ .describe("Processing segment id (from get_editor)"),
4343
+ generation_claim_token: z
4344
+ .string()
4345
+ .nullable()
4346
+ .describe("generation_claim_token from that processing segment"),
4347
+ },
4348
+ annotations: {
4349
+ title: "Cancel segment generation",
4350
+ readOnlyHint: false,
4351
+ destructiveHint: true,
4352
+ openWorldHint: true,
4353
+ idempotentHint: true,
4354
+ },
4355
+ },
4356
+ tool(
4357
+ async (
4358
+ args: {
4359
+ slug: string;
4360
+ segment_id: number | string;
4361
+ generation_claim_token: string | null;
4362
+ },
4363
+ client,
4364
+ ) => {
4365
+ const res = await client.post<{ data: unknown }>(
4366
+ `/editor/${args.slug}/segments/${String(args.segment_id)}/cancel`,
4367
+ { expected_generation_claim_token: args.generation_claim_token },
4368
+ undefined,
4369
+ );
4370
+ return ok({
4371
+ ...asRecord(asRecord(res).data ?? res),
4372
+ slug: args.slug,
4373
+ charged: false,
4374
+ next: "set_segment_prompt or generate_segment",
4375
+ });
4376
+ },
4377
+ ),
4378
+ );
4379
+
4380
+ registerTool(
4381
+ "cancel_autopilot",
4382
+ {
4383
+ title: "Cancel a running Autopilot run",
4384
+ description:
4385
+ "Stops an in-flight Autopilot run on an editor project: sets autopilot_status to cancelled so the " +
4386
+ "orchestration chain (scenario → scenes → narration → voice → music → render) advances no further. Use " +
4387
+ "this when Autopilot is still running and you want to halt it. Any scenes already charged stay charged and " +
4388
+ "all completed work (generated scenes, voice, music, a finished render) is kept — a step already enqueued " +
4389
+ "may still finish, but nothing new is started. Idempotent: a no-op that returns the project unchanged when " +
4390
+ "Autopilot is not running (already completed, failed, cancelled, or never launched), so it is safe to " +
4391
+ "repeat. It does NOT itself refund or re-run anything. For a non-Autopilot prepaid batch " +
4392
+ "(batch_generation_status preparing / paused_for_retry) use cancel_editor_batch; to stop one " +
4393
+ "individually-generated scene use cancel_segment_generation. Scope: video:generate.",
4394
+ inputSchema: { slug: z.string().describe("Editor project slug") },
4395
+ annotations: {
4396
+ title: "Cancel autopilot",
4397
+ readOnlyHint: false,
4398
+ destructiveHint: true,
4399
+ openWorldHint: true,
4400
+ idempotentHint: true,
4401
+ },
4402
+ },
4403
+ tool(async (args: { slug: string }, client) => {
4404
+ const res = await client.post<{ data: unknown }>(
4405
+ `/editor/${args.slug}/autopilot/cancel`,
4406
+ undefined,
4407
+ undefined,
4408
+ );
4409
+ return ok({
4410
+ ...asRecord(asRecord(res).data ?? res),
4411
+ slug: args.slug,
4412
+ charged: false,
4413
+ next: "get_status",
4414
+ });
4415
+ }),
4416
+ );
4417
+
4250
4418
  registerTool(
4251
4419
  "regenerate_segment",
4252
4420
  {
@@ -4308,7 +4476,7 @@ registerTool(
4308
4476
  {
4309
4477
  title: "Generate all pending segments (sequential)",
4310
4478
  description:
4311
- "Generates every not-yet-completed scene (pending or previously failed) in position order, waiting for each to finish before starting" +
4479
+ "Generates every not-yet-completed scene (pending or previously failed) in position order, waiting for each to finish before starting " +
4312
4480
  "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 " +
4313
4481
  "completed if a scene errors on submit (e.g. 402 credits_insufficient) or fails while rendering; if a " +
4314
4482
  "scene is still rendering past the budget it returns timed_out so you can re-run to continue (completed " +
@@ -4655,8 +4823,8 @@ registerTool(
4655
4823
  {
4656
4824
  title: "Render the final editor video (0 credits)",
4657
4825
  description:
4658
- "Renders the final MP4 from the generated segments + voice + music. Costs 0 credits (any still-ungenerated " +
4659
- "segments are auto-charged at generation cost first). Render NEVER generates or charges for voice/music; a new " +
4826
+ "Renders the final MP4 from the generated segments + voice + music. Costs 0 credits. Pending or previously " +
4827
+ "failed AI scenes are generated first and charged as a new batch. Render NEVER generates or charges for voice/music; a new " +
4660
4828
  "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; " +
4661
4829
  'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' +
4662
4830
  "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.",
@@ -5160,7 +5328,9 @@ registerTool(
5160
5328
  IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
5161
5329
  " (≤8 MiB), local path confined to HUBFLUENCER_INPUT_DIR (or cwd). 0 credits. Its pixels guide product identity " +
5162
5330
  "in every generated Editor scene and its confirmed description grounds scenario and narration. The exact photo " +
5163
- "is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary.",
5331
+ "is linked as the closing image unless a separate closing image was authored; generated fine label text can still vary. " +
5332
+ "WARNING: replacing the product photo on a project that already has generated AI scenes invalidates their product " +
5333
+ "identity — those scenes were built from the old photo and must be regenerated to match the new one.",
5164
5334
  inputSchema: {
5165
5335
  slug: z.string().describe("Editor project slug"),
5166
5336
  file_path: z
@@ -91,6 +91,10 @@ export const getStatusOutput = z
91
91
  .number()
92
92
  .optional()
93
93
  .describe("Short only: paid generation attempt number that failed."),
94
+ generation_attempt: z
95
+ .number()
96
+ .optional()
97
+ .describe("Short only: current paid attempt; echo when cancelling."),
94
98
  latest_render_free: z.boolean().optional(),
95
99
  last_free_rerender_failed: z.boolean().optional(),
96
100
  })
@@ -134,6 +138,10 @@ export const waitForCompletionOutput = z
134
138
  .number()
135
139
  .optional()
136
140
  .describe("Short only: paid generation attempt number that failed."),
141
+ generation_attempt: z
142
+ .number()
143
+ .optional()
144
+ .describe("Short only: current paid attempt; echo when cancelling."),
137
145
  latest_render_free: z.boolean().optional(),
138
146
  last_free_rerender_failed: z.boolean().optional(),
139
147
  saved_to: z.union([z.string(), z.null()]).optional(),