@hubfluencer/mcp 0.15.0 → 0.16.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
@@ -85,8 +85,8 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
85
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** |
86
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** |
87
87
  | `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
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 |
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 |
88
+ | `create_editor_ad` | Create an editor project, attach optional local product/logo/closing assets, and return the live autopilot quote. Pass `segments_count` (3–10) as the exact structural target. 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 |
89
+ | `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; `segments_count` (3–10) overrides an older brief and is reflected in the matching quote. 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 |
90
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** |
91
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
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** |
@@ -111,7 +111,7 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
111
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** |
112
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** |
113
113
  | `generate_all_segments` | Render every not-yet-completed scene in order (continuity preserved); capped ~280s → re-run to continue. | **5 / segment** |
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 |
114
+ | `set_narration_script` / `generate_narration` | Write the exact words to read aloud (free)—never pass a production prompt, camera directions, or a creative brief as `script`—**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 |
115
115
  | `generate_voice` | Synthesize the voiceover. `voice_id` required (see `list_voices`). | **3** |
116
116
  | `generate_music` | Generate background music (each call is a fresh take). Optional `instruments` is a string array (≤10 non-empty hints, ≤80 chars each), not a comma-separated string. | **5** |
117
117
  | `render` | Final MP4. Auto-charges and generates any pending or failed scenes first (4/scene in a batch of ≥3). | **0** |
@@ -137,7 +137,7 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
137
137
  | `set_closing_image` | Set the ~2s end-card image (JPEG/PNG, ≤20 MiB; upload a file, or reuse the product image with `from_product:true`). |
138
138
  | `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤5 MiB; editor only; optional treatment/position/duration). |
139
139
  | `set_short_product` | Attach a product image (JPEG/PNG, ≤20 MiB) to a **short**. |
140
- | `set_short_logo` | Attach an exact deterministic logo overlay (JPEG/PNG, ≤5 MiB) to a **short**; choose its preferred corner and `throughout`/`end_card`/`none` (hide) treatment. Forge moves it away from text/CTA/chips. The logo is never sent to the video model. |
140
+ | `set_short_logo` | Attach an exact deterministic logo overlay (JPEG/PNG, ≤5 MiB) to a **short**. The corner is preferred and Forge moves it away from text/CTA/chips. Omit treatment to auto-select: transparent marks use `throughout`, opaque square lockups use `end_card`; explicit `throughout`/`end_card`/`none` still wins. The logo is never sent to the video model. |
141
141
  | `set_short_poster` | Set a short's end-card poster (JPEG/PNG, ≤20 MiB; extends the render 12s → 14s). Phone-shaped portrait art is cover-cropped to 9:16; square, classic portrait, and landscape art is contained over a blurred backdrop. Pass `poster_includes_lockup:true` if the poster already carries brand name/CTA/badges — suppresses the overlaid claim/CTA and an end-card-only logo so they don't collide. |
142
142
 
143
143
  ### Status & delivery
@@ -152,9 +152,9 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
152
152
 
153
153
  ### Creative export contract
154
154
 
155
- - Production prompts (`product_prompt`, `scenario_prompt`, and scene `prompt`) are visual directions. Narration scripts are read aloud verbatim, so `set_narration_script.script` must contain only the exact spoken words—never camera notes, prompt syntax, labels, or scene directions. Prefer `generate_narration` or Autopilot when exact wording is unnecessary.
155
+ - Production prompts (`product_prompt`, `scenario_prompt`, and scene `prompt`) are visual directions. Narration scripts are read aloud verbatim, so `set_narration_script.script` must contain only the exact spoken words—never camera notes, prompt syntax, labels, or scene directions. Never copy a creative brief into `script`; prefer `generate_narration` or Autopilot unless the user supplied final voice-over copy.
156
156
  - Do not rely on the video model to spell essential brand names, UI, signs, prices, offers, or CTAs. Put exact copy in Hubfluencer overlays, narration, or supplied ending artwork.
157
- - `short_logo_position` is a preferred corner. Forge moves the logo away from headline, captions, CTA, offer, and badge areas. Use a tight transparent wordmark; use square/full-bleed art as the poster. `end_card` logos are suppressed when `poster_includes_lockup:true` to avoid double branding.
157
+ - `short_logo_position` is a preferred corner. Forge moves the logo away from headline, captions, CTA, offer, and badge areas. Omit treatment so transparent marks default to `throughout` and opaque square lockups default to `end_card`. Use a tight transparent wordmark; use square/full-bleed art as the poster. `end_card` logos are suppressed when `poster_includes_lockup:true` to avoid double branding.
158
158
  - Short posters fill the ending frame: phone-shaped portrait art is cover-cropped, while square, classic portrait, and landscape art is contained over a blurred backdrop. Editor closing images are always cover-cropped. Keep important content in the central 9:16 safe area. An Editor closing image gets no generic CTA, so put the CTA in the artwork or state it in the preceding narration/scene.
159
159
  - Each Editor AI scene is 8 seconds. Request 3 scenes (about 24 seconds) for a concise social ad unless the brief needs a longer story.
160
160
 
package/dist/index.js CHANGED
@@ -29578,6 +29578,7 @@ function normalizeStatus(kind, slug, data) {
29578
29578
  const renderStale = latest.is_stale === true;
29579
29579
  const renderFresh = latest.is_stale === false;
29580
29580
  const stale = renderStale || staleSegments.length > 0 || audioStale;
29581
+ const autopilotContractIncomplete = autopilot === "completed" && d.autopilot_contract_satisfied === false && !stale;
29581
29582
  const renderActive = renderStatus === "pending" || renderStatus === "rendering" || renderStatus === "processing";
29582
29583
  const scenarioActive = d.scenario_status === "generating" || d.scenario_apply_status === "pending" || d.scenario_apply_status === "applying";
29583
29584
  const narrationActive = d.narration_status === "generating";
@@ -29617,7 +29618,7 @@ function normalizeStatus(kind, slug, data) {
29617
29618
  const batchRecoveryActive = autopilotFailed && batchActive;
29618
29619
  const childFailureActionable = !batchRecoveryActive && (!autopilotActive || !childWorkActive && !autopilotResumeGraceActive);
29619
29620
  const workActive = autopilotActive || childWorkActive;
29620
- const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed;
29621
+ const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed && !autopilotContractIncomplete;
29621
29622
  const ready = deliveredArtifactSupersedesOrchestrationFailure;
29622
29623
  const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
29623
29624
  const renderFailed = renderStatus === "failed" && childFailureActionable;
@@ -29648,6 +29649,8 @@ function normalizeStatus(kind, slug, data) {
29648
29649
  stage = "audio_failed";
29649
29650
  else if (batchInsufficientCredits)
29650
29651
  stage = "insufficient_credits";
29652
+ else if (autopilotContractIncomplete)
29653
+ stage = "contract_incomplete";
29651
29654
  else if (activeSegments.length > 0)
29652
29655
  stage = "segment_processing";
29653
29656
  else if (batchActive)
@@ -29673,10 +29676,13 @@ function normalizeStatus(kind, slug, data) {
29673
29676
  terminal,
29674
29677
  ready,
29675
29678
  video_url: videoUrl,
29676
- 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),
29679
+ 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) ?? (autopilotContractIncomplete ? "Autopilot completed without every quoted deliverable. Resume Autopilot to repair the missing component." : null),
29677
29680
  stale,
29678
29681
  active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
29679
- stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
29682
+ stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
29683
+ ...typeof d.autopilot_contract_satisfied === "boolean" ? {
29684
+ autopilot_contract_satisfied: d.autopilot_contract_satisfied
29685
+ } : {}
29680
29686
  };
29681
29687
  }
29682
29688
  function startStateDiscriminator(kind, data) {
@@ -29700,16 +29706,16 @@ function idemKey(...parts) {
29700
29706
  return createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 32);
29701
29707
  }
29702
29708
  function makeVideoEditorCreateKey(a) {
29703
- return idemKey("make-editor", a.prompt, a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "");
29709
+ return idemKey("make-editor", a.prompt, a.segments_count === undefined ? "" : String(a.segments_count), a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "");
29704
29710
  }
29705
29711
  function makeVideoAutopilotKey(slug, a, startState) {
29706
- 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);
29712
+ return idemKey("autopilot", slug, a.prompt, a.segments_count === undefined ? "" : String(a.segments_count), a.product_subject ?? "", a.project_intent ?? "", a.language ?? "en", a.aspect ?? "", a.voice_id ?? "", a.creative_format ?? "", a.visual_language ?? "", a.theme ?? "", startState);
29707
29713
  }
29708
29714
  function editorAdCreateKey(a) {
29709
- 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));
29715
+ return idemKey("create-editor", a.product_prompt ?? "", a.segments_count === undefined ? "" : String(a.segments_count), 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));
29710
29716
  }
29711
29717
  function editorAdAutopilotKey(slug, a) {
29712
- 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));
29718
+ return idemKey("autopilot", slug, a.product_prompt ?? "", a.segments_count === undefined ? "" : String(a.segments_count), 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));
29713
29719
  }
29714
29720
  function sliderCreateKey(a) {
29715
29721
  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 ?? "");
@@ -30266,7 +30272,7 @@ function readStoredCredentials() {
30266
30272
  // package.json
30267
30273
  var package_default = {
30268
30274
  name: "@hubfluencer/mcp",
30269
- version: "0.15.0",
30275
+ version: "0.16.0",
30270
30276
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30271
30277
  license: "MIT",
30272
30278
  author: "Monocursive <contact@monocursive.com>",
@@ -30992,6 +30998,38 @@ function assetSubscriptionErrorText(status, code, message) {
30992
30998
  return `${message} The reusable asset catalog requires an ACTIVE SUBSCRIPTION; ` + "this is a billing gate, not a scope or credit issue, so retrying won't help — " + "subscribe in the app first. (Generating videos/carousels does not require this.)";
30993
30999
  }
30994
31000
 
31001
+ // src/polling.ts
31002
+ async function pollWithFinalReconciliation({
31003
+ read,
31004
+ isTerminal: isTerminal2,
31005
+ budgetMs,
31006
+ intervalMs,
31007
+ onPoll,
31008
+ now = Date.now,
31009
+ sleep: sleep3 = (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))
31010
+ }) {
31011
+ const deadline = now() + budgetMs;
31012
+ let value = await read(deadline);
31013
+ let pollNumber = 0;
31014
+ while (!isTerminal2(value)) {
31015
+ const remainingMs = deadline - now();
31016
+ if (remainingMs <= 0)
31017
+ break;
31018
+ await onPoll?.(value, ++pollNumber);
31019
+ await sleep3(Math.min(intervalMs, remainingMs));
31020
+ if (now() >= deadline)
31021
+ break;
31022
+ value = await read(deadline);
31023
+ }
31024
+ if (!isTerminal2(value)) {
31025
+ const finalReadDeadline = now() + Math.min(5000, Math.max(1000, intervalMs));
31026
+ try {
31027
+ value = await read(finalReadDeadline);
31028
+ } catch {}
31029
+ }
31030
+ return value;
31031
+ }
31032
+
30995
31033
  // src/series-calendar.ts
30996
31034
  var SERIES_FIELDS = [
30997
31035
  "brand_profile_id",
@@ -31590,6 +31628,8 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
31590
31628
  params.set("product_subject", overrides.product_subject);
31591
31629
  if (overrides.product_prompt !== undefined)
31592
31630
  params.set("product_prompt", overrides.product_prompt);
31631
+ if (overrides.segments_count !== undefined)
31632
+ params.set("segments_count", String(overrides.segments_count));
31593
31633
  if (overrides.restart)
31594
31634
  params.set("restart", "true");
31595
31635
  const query = params.toString();
@@ -31597,9 +31637,13 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
31597
31637
  const cost = asRecord(asRecord(response).data);
31598
31638
  const estimated_credits = typeof cost.total === "number" ? cost.total : null;
31599
31639
  const available_credits = typeof cost.available_credits === "number" ? cost.available_credits : null;
31640
+ const scene_count = typeof cost.scene_count === "number" ? cost.scene_count : null;
31641
+ const target_duration_seconds = typeof cost.target_duration_seconds === "number" ? cost.target_duration_seconds : null;
31600
31642
  return {
31601
31643
  estimated_credits,
31602
31644
  available_credits,
31645
+ scene_count,
31646
+ target_duration_seconds,
31603
31647
  affordable: estimated_credits !== null && available_credits !== null && available_credits >= estimated_credits
31604
31648
  };
31605
31649
  } catch (error2) {
@@ -31610,6 +31654,8 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
31610
31654
  return {
31611
31655
  estimated_credits: null,
31612
31656
  available_credits: null,
31657
+ scene_count: null,
31658
+ target_duration_seconds: null,
31613
31659
  affordable: false
31614
31660
  };
31615
31661
  }
@@ -31885,15 +31931,13 @@ AUTH: set HUBFLUENCER_API_TOKEN (Settings → Access tokens) or run \`npx -y @hu
31885
31931
  var server = new McpServer({ name: "hubfluencer", version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
31886
31932
  var registerTool = server.registerTool.bind(server);
31887
31933
  async function pollToTerminal(client, kind, slug, extra, budgetMs, intervalMs) {
31888
- const deadline = Date.now() + budgetMs;
31889
- let status = await fetchStatus(client, kind, slug, deadline);
31890
- let n = 0;
31891
- while (!status.terminal && Date.now() + intervalMs <= deadline) {
31892
- await reportProgress(extra, ++n, `stage: ${status.stage}`);
31893
- await sleep4(intervalMs);
31894
- status = await fetchStatus(client, kind, slug, deadline);
31895
- }
31896
- return status;
31934
+ return pollWithFinalReconciliation({
31935
+ read: (deadline) => fetchStatus(client, kind, slug, deadline),
31936
+ isTerminal: (status) => status.terminal,
31937
+ budgetMs,
31938
+ intervalMs,
31939
+ onPoll: (status, pollNumber) => reportProgress(extra, pollNumber, `stage: ${status.stage}`)
31940
+ });
31897
31941
  }
31898
31942
  async function pollSegmentToTerminal(client, slug, segmentId, extra, deadline, intervalMs) {
31899
31943
  const sid = String(segmentId);
@@ -31928,6 +31972,7 @@ registerTool("make_video", {
31928
31972
  kind: exports_external.string().optional().describe("'short' (fast, 1 clip), 'editor' (multi-scene), or 'auto' (default — inferred)"),
31929
31973
  product_subject: exports_external.string().max(300).optional().describe('EDITOR only: the concrete product / main subject the video must be about (e.g. "Bordeaux red ' + 'wine, dark green bottle"). Hard-grounds the AI so it cannot invent an unrelated product. ' + "Recommended for ads."),
31930
31974
  project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("EDITOR only: social_ad for promotional content (product focus + CTA) or creative_story (default)."),
31975
+ segments_count: exports_external.number().int().min(3).max(10).optional().describe("EDITOR only: exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad."),
31931
31976
  language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag ' + '(use "pt", never "pt-BR"/"en-US"). Drives scenario, narration, voice, and captions.'),
31932
31977
  aspect: exports_external.string().optional().describe("Aspect ratio for editor: 9:16 (default), 16:9, or 1:1"),
31933
31978
  voice_id: exports_external.string().optional().describe("Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover"),
@@ -31979,6 +32024,9 @@ registerTool("make_video", {
31979
32024
  return fail(kindErr);
31980
32025
  const requestedKind = args.kind === "short" || args.kind === "editor" ? args.kind : undefined;
31981
32026
  const kind = requestedKind ?? inferKind(args.prompt);
32027
+ if (kind !== "editor" && args.segments_count !== undefined) {
32028
+ return fail("segments_count is only supported for editor videos.");
32029
+ }
31982
32030
  const waitSeconds = Math.min(280, Math.max(10, args.max_wait_seconds ?? 240));
31983
32031
  const budgetMs = waitSeconds * 1000;
31984
32032
  if (args.star_rating !== undefined && (args.star_rating < 0 || args.star_rating > 5)) {
@@ -32031,6 +32079,7 @@ registerTool("make_video", {
32031
32079
  const created = await client.post("/editor", {
32032
32080
  language: args.language ?? "en",
32033
32081
  product_prompt: args.prompt,
32082
+ segments_count: args.segments_count,
32034
32083
  product_subject: args.product_subject,
32035
32084
  project_intent: args.project_intent,
32036
32085
  export_aspect_ratio: args.aspect,
@@ -32889,6 +32938,7 @@ registerTool("create_editor_ad", {
32889
32938
  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.'),
32890
32939
  voice_id: exports_external.string().optional().describe("Preferred narration voice id (see list_voices); omit for the default voice"),
32891
32940
  export_aspect_ratio: exports_external.enum(["9:16", "16:9", "1:1"]).optional().describe('Aspect ratio (default "9:16")'),
32941
+ segments_count: exports_external.number().int().min(3).max(10).optional().describe("Exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad; omit to retain the server default."),
32892
32942
  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."),
32893
32943
  product_description: exports_external.string().max(500).optional().describe("Product description used to ground the scenario and narration when product_image_path is supplied."),
32894
32944
  closing_image_path: exports_external.string().optional().describe("Local closing-card image (.jpg/.jpeg/.png, ≤20 MiB)."),
@@ -32933,6 +32983,7 @@ registerTool("create_editor_ad", {
32933
32983
  const created = await client.post("/editor", {
32934
32984
  language: args.language ?? "en",
32935
32985
  product_prompt: args.product_prompt,
32986
+ segments_count: args.segments_count,
32936
32987
  product_subject: args.product_subject,
32937
32988
  project_intent: args.project_intent,
32938
32989
  creative_format: args.creative_format,
@@ -32979,7 +33030,9 @@ registerTool("create_editor_ad", {
32979
33030
  logoSettings.logo_duration_seconds = args.logo_duration_seconds;
32980
33031
  if (args.logo_path && Object.keys(logoSettings).length > 0)
32981
33032
  await client.patch(`/editor/${slug}/logo`, logoSettings);
32982
- const quote = await editorAutopilotQuote(client, slug);
33033
+ const quote = await editorAutopilotQuote(client, slug, {
33034
+ segments_count: args.segments_count
33035
+ });
32983
33036
  const overCap = args.max_credits !== undefined && quote.estimated_credits !== null && quote.estimated_credits > args.max_credits;
32984
33037
  if (args.max_credits === undefined || quote.estimated_credits === null || overCap || !quote.affordable) {
32985
33038
  const note = args.max_credits === undefined ? `Free draft configured. Estimated ${quote.estimated_credits ?? "?"} credits (have ${quote.available_credits ?? "?"}); nothing charged. Relaunch with start_autopilot({ slug: "${slug}", max_credits: ${quote.estimated_credits ?? "<approved cap>"} }).` : quote.estimated_credits === null ? "Pricing is temporarily unavailable, so the paid pipeline was not started. Retry the quote later." : overCap ? `Estimated ${quote.estimated_credits} credits exceeds max_credits ${args.max_credits}; nothing charged.` : `Not enough credits (needs ${quote.estimated_credits}, have ${quote.available_credits ?? "?"}); nothing charged.`;
@@ -32992,7 +33045,10 @@ registerTool("create_editor_ad", {
32992
33045
  next: "start_autopilot"
32993
33046
  });
32994
33047
  }
32995
- const started = await client.post(`/editor/${slug}/autopilot`, { max_credits: args.max_credits }, editorAdAutopilotKey(slug, args));
33048
+ const started = await client.post(`/editor/${slug}/autopilot`, {
33049
+ max_credits: args.max_credits,
33050
+ segments_count: args.segments_count
33051
+ }, editorAdAutopilotKey(slug, args));
32996
33052
  const status = normalizeStatus("editor", slug, asRecord(started).data);
32997
33053
  return ok({
32998
33054
  slug,
@@ -33017,6 +33073,7 @@ registerTool("start_autopilot", {
33017
33073
  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."),
33018
33074
  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."),
33019
33075
  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."),
33076
+ segments_count: exports_external.number().int().min(3).max(10).optional().describe("Optional exact number of 8-second AI scenes (3–10). Omit to keep the stored structural target."),
33020
33077
  max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
33021
33078
  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.")
33022
33079
  },
@@ -33037,6 +33094,8 @@ registerTool("start_autopilot", {
33037
33094
  overrides.product_subject = args.product_subject;
33038
33095
  if (args.product_prompt !== undefined)
33039
33096
  overrides.product_prompt = args.product_prompt;
33097
+ if (args.segments_count !== undefined)
33098
+ overrides.segments_count = args.segments_count;
33040
33099
  if (args.restart)
33041
33100
  overrides.restart = true;
33042
33101
  const quote = await editorAutopilotQuote(client, slug, overrides);
@@ -34020,7 +34079,7 @@ registerTool("set_short_logo", {
34020
34079
  slug: exports_external.string().describe("Short slug"),
34021
34080
  file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
34022
34081
  position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Preferred logo corner (default top-right; auto-relocated to avoid text)"),
34023
- treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears (default throughout)")
34082
+ treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears. Omit for automatic treatment: transparent wordmarks run throughout; opaque square artwork moves to the end card.")
34024
34083
  },
34025
34084
  annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
34026
34085
  }, tool(async (args, client) => {
@@ -34028,7 +34087,7 @@ registerTool("set_short_logo", {
34028
34087
  const res = await client.post(`/shorts/${args.slug}/logo/confirm`, {
34029
34088
  s3_key,
34030
34089
  position: args.position ?? "top-right",
34031
- treatment: args.treatment ?? "throughout"
34090
+ ...args.treatment ? { treatment: args.treatment } : {}
34032
34091
  });
34033
34092
  return ok(asRecord(res).data ?? res);
34034
34093
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.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
@@ -289,6 +289,8 @@ export interface NormalizedStatus {
289
289
  active_segment_ids?: Array<number | string>;
290
290
  /** Editor only: completed current scene ids invalidated by continuity edits. */
291
291
  stale_segment_ids?: Array<number | string>;
292
+ /** Editor Autopilot only: server-verified quoted delivery contract. */
293
+ autopilot_contract_satisfied?: boolean;
292
294
  /** Short only (additive, absent on older APIs): the newest render row is a free re-render. */
293
295
  latest_render_free?: boolean;
294
296
  /**
@@ -515,6 +517,21 @@ export function normalizeStatus(
515
517
  // Legacy/null/omitted freshness is unknown and must never be promoted to ready.
516
518
  const renderFresh = latest.is_stale === false;
517
519
  const stale = renderStale || staleSegments.length > 0 || audioStale;
520
+ // A completed Autopilot run the API reports as NOT satisfying its delivery
521
+ // contract — gated on a FRESH timeline. The server recomputes
522
+ // autopilot_contract_satisfied LIVE for completed runs, folding in staleness,
523
+ // so any ordinary post-delivery edit (e.g. adding narration makes music stale)
524
+ // flips it false even though the run genuinely delivered. Gating on !stale
525
+ // keeps this signal to the real missing-component case — canonically a legacy
526
+ // "music-only" run where nothing is stale but voice/narration were never
527
+ // produced — and lets the stale/editing_required semantics win everywhere once
528
+ // an edit lands. Declared after `stale` (which it depends on); the raw
529
+ // autopilot_contract_satisfied wire field is still passed through verbatim
530
+ // below, only this derived stage/error signal is freshness-gated.
531
+ const autopilotContractIncomplete =
532
+ autopilot === "completed" &&
533
+ d.autopilot_contract_satisfied === false &&
534
+ !stale;
518
535
  // A render in flight is active work too — fold it into workActive so the
519
536
  // terminal computation below can never fire while a render is queued/running.
520
537
  // "pending" here is NOT an idle default: a video_results row only exists once
@@ -638,7 +655,8 @@ export function normalizeStatus(
638
655
  !stale &&
639
656
  !workActive &&
640
657
  failedSegments.length === 0 &&
641
- !audioFailed;
658
+ !audioFailed &&
659
+ !autopilotContractIncomplete;
642
660
  const ready = deliveredArtifactSupersedesOrchestrationFailure;
643
661
  const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
644
662
  const renderFailed = renderStatus === "failed" && childFailureActionable;
@@ -698,6 +716,7 @@ export function normalizeStatus(
698
716
  stage = "segment_failed";
699
717
  else if (childFailureActionable && audioFailed) stage = "audio_failed";
700
718
  else if (batchInsufficientCredits) stage = "insufficient_credits";
719
+ else if (autopilotContractIncomplete) stage = "contract_incomplete";
701
720
  else if (activeSegments.length > 0) stage = "segment_processing";
702
721
  else if (batchActive) stage = "segment_generation";
703
722
  else if (autoRenderHandoffActive) stage = "rendering";
@@ -727,6 +746,13 @@ export function normalizeStatus(
727
746
  terminal,
728
747
  ready,
729
748
  video_url: videoUrl,
749
+ // Mirror the stage chain's precedence EXACTLY so the error text always
750
+ // matches the stage the caller sees. batch-retry, then the actionable
751
+ // parent/child failures, then insufficient-credits, and only last the
752
+ // contract-incomplete message — because a completed-autopilot editor with a
753
+ // later paused manual batch reports stage "batch_retry_required", so it must
754
+ // read the retry guidance, not "Resume Autopilot" (the one action
755
+ // start_autopilot refuses while a batch is paused).
730
756
  error: ready
731
757
  ? null
732
758
  : batchRetryRequired
@@ -737,6 +763,9 @@ export function normalizeStatus(
737
763
  actionableChildError ??
738
764
  (batchInsufficientCredits
739
765
  ? "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."
766
+ : null) ??
767
+ (autopilotContractIncomplete
768
+ ? "Autopilot completed without every quoted deliverable. Resume Autopilot to repair the missing component."
740
769
  : null)),
741
770
  stale,
742
771
  active_segment_ids: activeSegments
@@ -751,6 +780,12 @@ export function normalizeStatus(
751
780
  (id): id is number | string =>
752
781
  typeof id === "number" || typeof id === "string",
753
782
  ),
783
+ ...(typeof d.autopilot_contract_satisfied === "boolean"
784
+ ? {
785
+ autopilot_contract_satisfied:
786
+ d.autopilot_contract_satisfied as boolean,
787
+ }
788
+ : {}),
754
789
  };
755
790
  }
756
791
 
@@ -817,6 +852,7 @@ export function idemKey(...parts: string[]): string {
817
852
  /** make_video's editor draft-create key (`POST /editor`). */
818
853
  export interface MakeVideoEditorCreateArgs {
819
854
  prompt: string;
855
+ segments_count?: number;
820
856
  product_subject?: string;
821
857
  project_intent?: string;
822
858
  language?: string;
@@ -831,6 +867,7 @@ export function makeVideoEditorCreateKey(a: MakeVideoEditorCreateArgs): string {
831
867
  return idemKey(
832
868
  "make-editor",
833
869
  a.prompt,
870
+ a.segments_count === undefined ? "" : String(a.segments_count),
834
871
  a.product_subject ?? "",
835
872
  a.project_intent ?? "",
836
873
  a.language ?? "en",
@@ -857,6 +894,7 @@ export function makeVideoAutopilotKey(
857
894
  "autopilot",
858
895
  slug,
859
896
  a.prompt,
897
+ a.segments_count === undefined ? "" : String(a.segments_count),
860
898
  a.product_subject ?? "",
861
899
  a.project_intent ?? "",
862
900
  a.language ?? "en",
@@ -872,6 +910,7 @@ export function makeVideoAutopilotKey(
872
910
  /** create_editor_ad's draft-create and autopilot-start key argument set. */
873
911
  export interface EditorAdKeyArgs {
874
912
  product_prompt?: string;
913
+ segments_count?: number;
875
914
  product_subject?: string;
876
915
  project_intent?: string;
877
916
  language?: string;
@@ -906,6 +945,7 @@ export function editorAdCreateKey(a: EditorAdKeyArgs): string {
906
945
  return idemKey(
907
946
  "create-editor",
908
947
  a.product_prompt ?? "",
948
+ a.segments_count === undefined ? "" : String(a.segments_count),
909
949
  a.product_subject ?? "",
910
950
  a.project_intent ?? "",
911
951
  a.language ?? "en",
@@ -943,6 +983,7 @@ export function editorAdAutopilotKey(slug: string, a: EditorAdKeyArgs): string {
943
983
  "autopilot",
944
984
  slug,
945
985
  a.product_prompt ?? "",
986
+ a.segments_count === undefined ? "" : String(a.segments_count),
946
987
  a.product_subject ?? "",
947
988
  a.project_intent ?? "",
948
989
  a.language ?? "en",
package/src/index.ts CHANGED
@@ -101,6 +101,7 @@ import {
101
101
  validatePerformanceMetrics,
102
102
  validateReviewOutput,
103
103
  } from "./perf-review-assets.js";
104
+ import { pollWithFinalReconciliation } from "./polling.js";
104
105
  import {
105
106
  buildScheduledPostBody,
106
107
  calendarScopeErrorText,
@@ -161,11 +162,14 @@ async function editorAutopilotQuote(
161
162
  language?: string;
162
163
  product_subject?: string;
163
164
  product_prompt?: string;
165
+ segments_count?: number;
164
166
  restart?: boolean;
165
167
  } = {},
166
168
  ): Promise<{
167
169
  estimated_credits: number | null;
168
170
  available_credits: number | null;
171
+ scene_count: number | null;
172
+ target_duration_seconds: number | null;
169
173
  affordable: boolean;
170
174
  }> {
171
175
  try {
@@ -175,6 +179,8 @@ async function editorAutopilotQuote(
175
179
  params.set("product_subject", overrides.product_subject);
176
180
  if (overrides.product_prompt !== undefined)
177
181
  params.set("product_prompt", overrides.product_prompt);
182
+ if (overrides.segments_count !== undefined)
183
+ params.set("segments_count", String(overrides.segments_count));
178
184
  if (overrides.restart) params.set("restart", "true");
179
185
  const query = params.toString();
180
186
  const response = await client.get<{ data: unknown }>(
@@ -187,9 +193,17 @@ async function editorAutopilotQuote(
187
193
  typeof cost.available_credits === "number"
188
194
  ? cost.available_credits
189
195
  : null;
196
+ const scene_count =
197
+ typeof cost.scene_count === "number" ? cost.scene_count : null;
198
+ const target_duration_seconds =
199
+ typeof cost.target_duration_seconds === "number"
200
+ ? cost.target_duration_seconds
201
+ : null;
190
202
  return {
191
203
  estimated_credits,
192
204
  available_credits,
205
+ scene_count,
206
+ target_duration_seconds,
193
207
  affordable:
194
208
  estimated_credits !== null &&
195
209
  available_credits !== null &&
@@ -213,6 +227,8 @@ async function editorAutopilotQuote(
213
227
  return {
214
228
  estimated_credits: null,
215
229
  available_credits: null,
230
+ scene_count: null,
231
+ target_duration_seconds: null,
216
232
  affordable: false,
217
233
  };
218
234
  }
@@ -674,15 +690,14 @@ async function pollToTerminal(
674
690
  budgetMs: number,
675
691
  intervalMs: number,
676
692
  ): Promise<NormalizedStatus> {
677
- const deadline = Date.now() + budgetMs;
678
- let status = await fetchStatus(client, kind, slug, deadline);
679
- let n = 0;
680
- while (!status.terminal && Date.now() + intervalMs <= deadline) {
681
- await reportProgress(extra, ++n, `stage: ${status.stage}`);
682
- await sleep(intervalMs);
683
- status = await fetchStatus(client, kind, slug, deadline);
684
- }
685
- return status;
693
+ return pollWithFinalReconciliation({
694
+ read: (deadline) => fetchStatus(client, kind, slug, deadline),
695
+ isTerminal: (status) => status.terminal,
696
+ budgetMs,
697
+ intervalMs,
698
+ onPoll: (status, pollNumber) =>
699
+ reportProgress(extra, pollNumber, `stage: ${status.stage}`),
700
+ });
686
701
  }
687
702
 
688
703
  /**
@@ -799,6 +814,15 @@ registerTool(
799
814
  .describe(
800
815
  "EDITOR only: social_ad for promotional content (product focus + CTA) or creative_story (default).",
801
816
  ),
817
+ segments_count: z
818
+ .number()
819
+ .int()
820
+ .min(3)
821
+ .max(10)
822
+ .optional()
823
+ .describe(
824
+ "EDITOR only: exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad.",
825
+ ),
802
826
  language: z
803
827
  .enum(LANGUAGES)
804
828
  .optional()
@@ -978,6 +1002,7 @@ registerTool(
978
1002
  kind?: string;
979
1003
  product_subject?: string;
980
1004
  project_intent?: string;
1005
+ segments_count?: number;
981
1006
  language?: string;
982
1007
  aspect?: string;
983
1008
  voice_id?: string;
@@ -1022,6 +1047,9 @@ registerTool(
1022
1047
  const requestedKind =
1023
1048
  args.kind === "short" || args.kind === "editor" ? args.kind : undefined;
1024
1049
  const kind: Kind = requestedKind ?? inferKind(args.prompt);
1050
+ if (kind !== "editor" && args.segments_count !== undefined) {
1051
+ return fail("segments_count is only supported for editor videos.");
1052
+ }
1025
1053
  const waitSeconds = Math.min(
1026
1054
  280,
1027
1055
  Math.max(10, args.max_wait_seconds ?? 240),
@@ -1128,6 +1156,7 @@ registerTool(
1128
1156
  {
1129
1157
  language: args.language ?? "en",
1130
1158
  product_prompt: args.prompt,
1159
+ segments_count: args.segments_count,
1131
1160
  product_subject: args.product_subject,
1132
1161
  project_intent: args.project_intent,
1133
1162
  export_aspect_ratio: args.aspect,
@@ -3245,6 +3274,15 @@ registerTool(
3245
3274
  .enum(["9:16", "16:9", "1:1"])
3246
3275
  .optional()
3247
3276
  .describe('Aspect ratio (default "9:16")'),
3277
+ segments_count: z
3278
+ .number()
3279
+ .int()
3280
+ .min(3)
3281
+ .max(10)
3282
+ .optional()
3283
+ .describe(
3284
+ "Exact number of 8-second AI scenes (3–10). Use 3 for a concise ~24s ad; omit to retain the server default.",
3285
+ ),
3248
3286
  product_image_path: z
3249
3287
  .string()
3250
3288
  .optional()
@@ -3310,6 +3348,7 @@ registerTool(
3310
3348
  theme?: string;
3311
3349
  voice_id?: string;
3312
3350
  export_aspect_ratio?: string;
3351
+ segments_count?: number;
3313
3352
  product_image_path?: string;
3314
3353
  product_description?: string;
3315
3354
  closing_image_path?: string;
@@ -3386,6 +3425,7 @@ registerTool(
3386
3425
  {
3387
3426
  language: args.language ?? "en",
3388
3427
  product_prompt: args.product_prompt,
3428
+ segments_count: args.segments_count,
3389
3429
  product_subject: args.product_subject,
3390
3430
  project_intent: args.project_intent,
3391
3431
  creative_format: args.creative_format,
@@ -3464,7 +3504,9 @@ registerTool(
3464
3504
  if (args.logo_path && Object.keys(logoSettings).length > 0)
3465
3505
  await client.patch(`/editor/${slug}/logo`, logoSettings);
3466
3506
 
3467
- const quote = await editorAutopilotQuote(client, slug);
3507
+ const quote = await editorAutopilotQuote(client, slug, {
3508
+ segments_count: args.segments_count,
3509
+ });
3468
3510
  const overCap =
3469
3511
  args.max_credits !== undefined &&
3470
3512
  quote.estimated_credits !== null &&
@@ -3495,7 +3537,10 @@ registerTool(
3495
3537
 
3496
3538
  const started = await client.post<{ data: unknown }>(
3497
3539
  `/editor/${slug}/autopilot`,
3498
- { max_credits: args.max_credits },
3540
+ {
3541
+ max_credits: args.max_credits,
3542
+ segments_count: args.segments_count,
3543
+ },
3499
3544
  // Fold the create-affecting params into the start key so it tracks
3500
3545
  // inputs, not just the slug. Self-consistent per-tool only: this key
3501
3546
  // is compared against retries of *this* tool on *this* slug. The
@@ -3561,6 +3606,15 @@ registerTool(
3561
3606
  .describe(
3562
3607
  "Optional freeform brief to set/replace before launch. Omit to keep the stored brief; send blank to clear it.",
3563
3608
  ),
3609
+ segments_count: z
3610
+ .number()
3611
+ .int()
3612
+ .min(3)
3613
+ .max(10)
3614
+ .optional()
3615
+ .describe(
3616
+ "Optional exact number of 8-second AI scenes (3–10). Omit to keep the stored structural target.",
3617
+ ),
3564
3618
  max_credits: z
3565
3619
  .number()
3566
3620
  .int()
@@ -3586,6 +3640,7 @@ registerTool(
3586
3640
  language?: string;
3587
3641
  product_subject?: string;
3588
3642
  product_prompt?: string;
3643
+ segments_count?: number;
3589
3644
  max_credits?: number;
3590
3645
  restart?: boolean;
3591
3646
  },
@@ -3601,6 +3656,7 @@ registerTool(
3601
3656
  language?: string;
3602
3657
  product_subject?: string;
3603
3658
  product_prompt?: string;
3659
+ segments_count?: number;
3604
3660
  restart?: boolean;
3605
3661
  } = {};
3606
3662
  if (args.language !== undefined) overrides.language = args.language;
@@ -3608,6 +3664,8 @@ registerTool(
3608
3664
  overrides.product_subject = args.product_subject;
3609
3665
  if (args.product_prompt !== undefined)
3610
3666
  overrides.product_prompt = args.product_prompt;
3667
+ if (args.segments_count !== undefined)
3668
+ overrides.segments_count = args.segments_count;
3611
3669
  if (args.restart) overrides.restart = true;
3612
3670
 
3613
3671
  const quote = await editorAutopilotQuote(client, slug, overrides);
@@ -5665,7 +5723,9 @@ registerTool(
5665
5723
  treatment: z
5666
5724
  .enum(["throughout", "end_card", "none"])
5667
5725
  .optional()
5668
- .describe("When the logo appears (default throughout)"),
5726
+ .describe(
5727
+ "When the logo appears. Omit for automatic treatment: transparent wordmarks run throughout; opaque square artwork moves to the end card.",
5728
+ ),
5669
5729
  },
5670
5730
  annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false },
5671
5731
  },
@@ -5690,7 +5750,7 @@ registerTool(
5690
5750
  {
5691
5751
  s3_key,
5692
5752
  position: args.position ?? "top-right",
5693
- treatment: args.treatment ?? "throughout",
5753
+ ...(args.treatment ? { treatment: args.treatment } : {}),
5694
5754
  },
5695
5755
  );
5696
5756
  return ok(asRecord(res).data ?? res);
package/src/polling.ts ADDED
@@ -0,0 +1,51 @@
1
+ export interface PollOptions<T> {
2
+ read: (deadline: number) => Promise<T>;
3
+ isTerminal: (value: T) => boolean;
4
+ budgetMs: number;
5
+ intervalMs: number;
6
+ onPoll?: (value: T, pollNumber: number) => Promise<void>;
7
+ now?: () => number;
8
+ sleep?: (milliseconds: number) => Promise<void>;
9
+ }
10
+
11
+ /**
12
+ * Poll within a fixed budget, then perform one separately bounded
13
+ * reconciliation read. The final read closes the common race where work
14
+ * finishes during the last partial interval and the caller otherwise returns
15
+ * the stale pre-completion snapshot.
16
+ */
17
+ export async function pollWithFinalReconciliation<T>({
18
+ read,
19
+ isTerminal,
20
+ budgetMs,
21
+ intervalMs,
22
+ onPoll,
23
+ now = Date.now,
24
+ sleep = (milliseconds) =>
25
+ new Promise<void>((resolve) => setTimeout(resolve, milliseconds)),
26
+ }: PollOptions<T>): Promise<T> {
27
+ const deadline = now() + budgetMs;
28
+ let value = await read(deadline);
29
+ let pollNumber = 0;
30
+
31
+ while (!isTerminal(value)) {
32
+ const remainingMs = deadline - now();
33
+ if (remainingMs <= 0) break;
34
+ await onPoll?.(value, ++pollNumber);
35
+ await sleep(Math.min(intervalMs, remainingMs));
36
+ if (now() >= deadline) break;
37
+ value = await read(deadline);
38
+ }
39
+
40
+ if (!isTerminal(value)) {
41
+ const finalReadDeadline =
42
+ now() + Math.min(5_000, Math.max(1_000, intervalMs));
43
+ try {
44
+ value = await read(finalReadDeadline);
45
+ } catch {
46
+ // The normal timeout/resume contract uses the last successful state.
47
+ }
48
+ }
49
+
50
+ return value;
51
+ }