@hubfluencer/mcp 0.9.0 → 0.9.1

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,7 +85,7 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
85
85
  | `rerender_short` | **FREE re-render** — applies the short's current text/style/CTA/end-card state over the already-paid footage + music. Use after `update_short` for copy/style/CTA tweaks; 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
86
  | `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
87
87
  | `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. For a mascot/hero product that must remain identical with no humans, pass `cast_mode:"product_only"` plus `product_image_path`. | **0** until capped launch |
88
- | `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. | **0** quote; credits on capped launch |
88
+ | `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
89
  | `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** |
90
90
  | `generate_slider` | Render a carousel (copy → AI backgrounds → composite). Poll `get_slider` until `completed`. | **1 / slide** (3–10) |
91
91
  | `get_slider` | Read a carousel's status and, when completed, the per-slide image URLs + caption + hashtags. | **0** |
package/dist/index.js CHANGED
@@ -29558,7 +29558,7 @@ function normalizeStatus(kind, slug, data) {
29558
29558
  const renderFresh = latest.is_stale === false;
29559
29559
  const stale = renderStale || staleSegments.length > 0 || audioStale;
29560
29560
  const renderActive = renderStatus === "pending" || renderStatus === "rendering" || renderStatus === "processing";
29561
- const scenarioActive = d.scenario_status === "generating" || d.scenario_apply_status === "applying";
29561
+ const scenarioActive = d.scenario_status === "generating" || d.scenario_apply_status === "pending" || d.scenario_apply_status === "applying";
29562
29562
  const narrationActive = d.narration_status === "generating";
29563
29563
  const scenarioFailed = d.scenario_status === "failed";
29564
29564
  const scenarioApplyFailed = d.scenario_apply_status === "failed";
@@ -29589,13 +29589,16 @@ function normalizeStatus(kind, slug, data) {
29589
29589
  ].find((value) => typeof value === "string" && value.trim().length > 0);
29590
29590
  const audioActive = currentAudio.status === "pending" || currentAudio.status === "processing" || currentMusic.status === "pending" || currentMusic.status === "processing" || newerAudioVersions.some(versionActive) || newerMusicVersions.some(versionActive);
29591
29591
  const workActive = autopilotActive || batchActive || renderActive || scenarioActive || narrationActive || audioActive || activeSegments.length > 0;
29592
- const ready = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed && !scenarioFailed && !scenarioApplyFailed && !narrationFailed && !batchFailed;
29592
+ const deliveredArtifactSupersedesOrchestrationFailure = renderStatus === "completed" && Boolean(videoUrl) && renderFresh && !stale && !workActive && failedSegments.length === 0 && !audioFailed;
29593
+ const ready = deliveredArtifactSupersedesOrchestrationFailure;
29593
29594
  const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
29594
29595
  const renderFailed = renderStatus === "failed";
29595
- const failed = autopilotFailed || renderFailed || scenarioApplyFailed || scenarioFailed || narrationFailed || batchFailed || failedSegments.length > 0 || audioFailed;
29596
+ const failed = autopilotFailed && !deliveredArtifactSupersedesOrchestrationFailure || renderFailed || scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure || scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure || narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure || batchFailed && !deliveredArtifactSupersedesOrchestrationFailure || failedSegments.length > 0 || audioFailed;
29596
29597
  const terminal = ready || failed || batchInsufficientCredits || !workActive;
29597
29598
  let stage = autopilot;
29598
- if (autopilot === "failed")
29599
+ if (ready)
29600
+ stage = "completed";
29601
+ else if (autopilot === "failed")
29599
29602
  stage = "failed";
29600
29603
  else if (autopilot === "cancelled")
29601
29604
  stage = "cancelled";
@@ -29629,8 +29632,6 @@ function normalizeStatus(kind, slug, data) {
29629
29632
  stage = "audio_processing";
29630
29633
  else if (!workActive && stale)
29631
29634
  stage = "editing_required";
29632
- else if (ready)
29633
- stage = "completed";
29634
29635
  const segmentError = failedSegments.map((segment) => segment.error_message).find((value) => typeof value === "string" && value.length > 0);
29635
29636
  return {
29636
29637
  kind,
@@ -29639,7 +29640,7 @@ function normalizeStatus(kind, slug, data) {
29639
29640
  terminal,
29640
29641
  ready,
29641
29642
  video_url: videoUrl,
29642
- error: d.autopilot_error_message ?? latest.error_message ?? d.scenario_apply_error_message ?? d.scenario_error_message ?? d.narration_error_message ?? d.batch_generation_error_message ?? segmentError ?? audioError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume." : null),
29643
+ error: ready ? null : d.autopilot_error_message ?? latest.error_message ?? d.scenario_apply_error_message ?? d.scenario_error_message ?? d.narration_error_message ?? d.batch_generation_error_message ?? segmentError ?? audioError ?? (batchInsufficientCredits ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume." : null),
29643
29644
  stale,
29644
29645
  active_segment_ids: activeSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string"),
29645
29646
  stale_segment_ids: staleSegments.map((segment) => segment.id).filter((id) => typeof id === "number" || typeof id === "string")
@@ -30232,7 +30233,7 @@ function readStoredCredentials() {
30232
30233
  // package.json
30233
30234
  var package_default = {
30234
30235
  name: "@hubfluencer/mcp",
30235
- version: "0.9.0",
30236
+ version: "0.9.1",
30236
30237
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30237
30238
  license: "MIT",
30238
30239
  author: "Monocursive <contact@monocursive.com>",
@@ -31531,6 +31532,8 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
31531
31532
  params.set("product_subject", overrides.product_subject);
31532
31533
  if (overrides.product_prompt)
31533
31534
  params.set("product_prompt", overrides.product_prompt);
31535
+ if (overrides.restart)
31536
+ params.set("restart", "true");
31534
31537
  const query = params.toString();
31535
31538
  const response = await client.get(`/editor/${slug}/autopilot/cost${query ? `?${query}` : ""}`);
31536
31539
  const cost = asRecord(asRecord(response).data);
@@ -31553,6 +31556,26 @@ async function editorAutopilotQuote(client, slug, overrides = {}) {
31553
31556
  };
31554
31557
  }
31555
31558
  }
31559
+ async function editorChargeReceipt(client, slug, action, listPriceCredits) {
31560
+ try {
31561
+ const response = await client.get(`/editor/${slug}`);
31562
+ const editor = asRecord(asRecord(response).data);
31563
+ return {
31564
+ action,
31565
+ list_price_credits: listPriceCredits,
31566
+ project_spend: editor.project_spend ?? null,
31567
+ approved_cap: editor.autopilot_max_credits ?? null,
31568
+ approved_run_spend: editor.autopilot_credits_spent ?? null
31569
+ };
31570
+ } catch {
31571
+ return {
31572
+ action,
31573
+ list_price_credits: listPriceCredits,
31574
+ project_spend: null,
31575
+ enrichment_unavailable: true
31576
+ };
31577
+ }
31578
+ }
31556
31579
  function isRecompositeInProgressConflict(e) {
31557
31580
  const err = e;
31558
31581
  if (err?.status !== 409)
@@ -32805,13 +32828,14 @@ registerTool("create_editor_ad", {
32805
32828
  }));
32806
32829
  registerTool("start_autopilot", {
32807
32830
  title: "Start autopilot on an existing editor draft",
32808
- description: "Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " + "EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " + "explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " + "the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " + "if a run is already in progress for this slug the server returns already_running — it never double-starts " + "or double-charges. Once finished, failed, or cancelled, calling again relaunches it (resume after a top-up, raising your spend cap, or a mid-pipeline failure). Then poll with " + 'wait_for_completion({ slug, kind: "editor" }).',
32831
+ description: "Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " + "EXISTING editor project/draft. Without max_credits it returns the live quote and charges nothing. Supply an " + "explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " + "the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " + "if a run is already in progress for this slug the server returns already_running — it never double-starts " + "or double-charges. Failed or cancelled runs resume normally. For a completed project, set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " + 'wait_for_completion({ slug, kind: "editor" }).',
32809
32832
  inputSchema: {
32810
32833
  slug: exports_external.string().describe("Editor project slug"),
32811
32834
  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."),
32812
32835
  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."),
32813
32836
  product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch."),
32814
- max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged.")
32837
+ max_credits: exports_external.number().int().nonnegative().optional().describe("Explicit spend cap. Omit to quote only; no credits are charged."),
32838
+ 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.")
32815
32839
  },
32816
32840
  outputSchema: createEditorOutput,
32817
32841
  annotations: { title: "Start autopilot", ...WRITE, idempotentHint: false }
@@ -32830,6 +32854,8 @@ registerTool("start_autopilot", {
32830
32854
  overrides.product_subject = args.product_subject;
32831
32855
  if (args.product_prompt !== undefined)
32832
32856
  overrides.product_prompt = args.product_prompt;
32857
+ if (args.restart)
32858
+ overrides.restart = true;
32833
32859
  const quote = await editorAutopilotQuote(client, slug, overrides);
32834
32860
  const overCap = args.max_credits !== undefined && quote.estimated_credits !== null && quote.estimated_credits > args.max_credits;
32835
32861
  if (args.max_credits === undefined || quote.estimated_credits === null || overCap || !quote.affordable) {
@@ -33110,7 +33136,10 @@ registerTool("regenerate_segment", {
33110
33136
  }
33111
33137
  }, tool(async (args, client) => {
33112
33138
  const res = await client.post(`/editor/${args.slug}/segments/${String(args.segment_id)}/regenerate`, args.prompt !== undefined ? { prompt: args.prompt } : undefined, undefined);
33113
- return ok(asRecord(res).data ?? res);
33139
+ return ok({
33140
+ ...asRecord(asRecord(res).data ?? res),
33141
+ charge_receipt: await editorChargeReceipt(client, args.slug, "scene_regeneration", 4)
33142
+ });
33114
33143
  }));
33115
33144
  registerTool("generate_all_segments", {
33116
33145
  title: "Generate all pending segments (sequential)",
@@ -33227,7 +33256,10 @@ registerTool("generate_voice", {
33227
33256
  annotations: { title: "Generate voice", ...WRITE, idempotentHint: true }
33228
33257
  }, tool(async (args, client) => {
33229
33258
  const res = await client.post(`/editor/${args.slug}/generate-voice`, { voice_id: args.voice_id }, await voiceKey(client, args.slug, args.voice_id));
33230
- return ok(asRecord(res).data ?? res);
33259
+ return ok({
33260
+ ...asRecord(asRecord(res).data ?? res),
33261
+ charge_receipt: await editorChargeReceipt(client, args.slug, "voice_generation", 3)
33262
+ });
33231
33263
  }));
33232
33264
  registerTool("generate_music", {
33233
33265
  title: "Generate the background music (5 credits)",
@@ -33254,11 +33286,14 @@ registerTool("generate_music", {
33254
33286
  if (args.instruments !== undefined)
33255
33287
  body.instruments = args.instruments;
33256
33288
  const res = await client.post(`/editor/${args.slug}/generate-music`, body, undefined);
33257
- return ok(asRecord(res).data ?? res);
33289
+ return ok({
33290
+ ...asRecord(asRecord(res).data ?? res),
33291
+ charge_receipt: await editorChargeReceipt(client, args.slug, "music_generation", 5)
33292
+ });
33258
33293
  }));
33259
33294
  registerTool("render", {
33260
33295
  title: "Render the final editor video (0 credits)",
33261
- 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). 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.",
33296
+ 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.",
33262
33297
  inputSchema: { slug: exports_external.string().describe("Editor project slug") },
33263
33298
  annotations: { title: "Render", ...WRITE, idempotentHint: false }
33264
33299
  }, tool(async (args, client) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
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
@@ -477,10 +477,11 @@ export function normalizeStatus(
477
477
  renderStatus === "processing";
478
478
  // Scenario text generation / scenario apply run as async workers too
479
479
  // (scenario_status: none|generating|ready|failed; scenario_apply_status:
480
- // idle|applying|failed) — count them as active so a wait right after
480
+ // idle|pending|applying|failed) — count them as active so a wait right after
481
481
  // generate_scenario/apply_scenario doesn't bail out as an "idle" draft.
482
482
  const scenarioActive =
483
483
  d.scenario_status === "generating" ||
484
+ d.scenario_apply_status === "pending" ||
484
485
  d.scenario_apply_status === "applying";
485
486
  // Narration generation is a distinct async worker that runs before an
486
487
  // AudioVersion exists. Without this gate, a project with an older completed
@@ -553,27 +554,27 @@ export function normalizeStatus(
553
554
  narrationActive ||
554
555
  audioActive ||
555
556
  activeSegments.length > 0;
556
- const ready =
557
+ // Orchestration failures are historical once a repaired timeline has a fresh,
558
+ // completed MP4. Keeping them terminal would permanently block download_result
559
+ // after a successful granular recovery.
560
+ const deliveredArtifactSupersedesOrchestrationFailure =
557
561
  renderStatus === "completed" &&
558
562
  Boolean(videoUrl) &&
559
563
  renderFresh &&
560
564
  !stale &&
561
565
  !workActive &&
562
566
  failedSegments.length === 0 &&
563
- !audioFailed &&
564
- !scenarioFailed &&
565
- !scenarioApplyFailed &&
566
- !narrationFailed &&
567
- !batchFailed;
567
+ !audioFailed;
568
+ const ready = deliveredArtifactSupersedesOrchestrationFailure;
568
569
  const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
569
570
  const renderFailed = renderStatus === "failed";
570
571
  const failed =
571
- autopilotFailed ||
572
+ (autopilotFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
572
573
  renderFailed ||
573
- scenarioApplyFailed ||
574
- scenarioFailed ||
575
- narrationFailed ||
576
- batchFailed ||
574
+ (scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
575
+ (scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
576
+ (narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
577
+ (batchFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
577
578
  failedSegments.length > 0 ||
578
579
  audioFailed;
579
580
  // Terminal = control should return to the agent because nothing will progress
@@ -589,7 +590,8 @@ export function normalizeStatus(
589
590
  // Failure always wins over an unrelated active flag. Workers can leave an
590
591
  // outer autopilot/batch state active after a child stage has failed; reporting
591
592
  // the active stage would make agents poll forever instead of taking action.
592
- if (autopilot === "failed") stage = "failed";
593
+ if (ready) stage = "completed";
594
+ else if (autopilot === "failed") stage = "failed";
593
595
  else if (autopilot === "cancelled") stage = "cancelled";
594
596
  else if (renderFailed) stage = "render_failed";
595
597
  else if (scenarioApplyFailed) stage = "scenario_apply_failed";
@@ -606,7 +608,6 @@ export function normalizeStatus(
606
608
  else if (narrationActive) stage = "narration_processing";
607
609
  else if (audioActive) stage = "audio_processing";
608
610
  else if (!workActive && stale) stage = "editing_required";
609
- else if (ready) stage = "completed";
610
611
  const segmentError = failedSegments
611
612
  .map((segment) => segment.error_message)
612
613
  .find(
@@ -619,18 +620,19 @@ export function normalizeStatus(
619
620
  terminal,
620
621
  ready,
621
622
  video_url: videoUrl,
622
- error:
623
- (d.autopilot_error_message as string) ??
624
- (latest.error_message as string) ??
625
- (d.scenario_apply_error_message as string) ??
626
- (d.scenario_error_message as string) ??
627
- (d.narration_error_message as string) ??
628
- (d.batch_generation_error_message as string) ??
629
- segmentError ??
630
- audioError ??
631
- (batchInsufficientCredits
632
- ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume."
633
- : null),
623
+ error: ready
624
+ ? null
625
+ : ((d.autopilot_error_message as string) ??
626
+ (latest.error_message as string) ??
627
+ (d.scenario_apply_error_message as string) ??
628
+ (d.scenario_error_message as string) ??
629
+ (d.narration_error_message as string) ??
630
+ (d.batch_generation_error_message as string) ??
631
+ segmentError ??
632
+ audioError ??
633
+ (batchInsufficientCredits
634
+ ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume."
635
+ : null)),
634
636
  stale,
635
637
  active_segment_ids: activeSegments
636
638
  .map((segment) => segment.id)
package/src/index.ts CHANGED
@@ -160,6 +160,7 @@ async function editorAutopilotQuote(
160
160
  language?: string;
161
161
  product_subject?: string;
162
162
  product_prompt?: string;
163
+ restart?: boolean;
163
164
  } = {},
164
165
  ): Promise<{
165
166
  estimated_credits: number | null;
@@ -173,6 +174,7 @@ async function editorAutopilotQuote(
173
174
  params.set("product_subject", overrides.product_subject);
174
175
  if (overrides.product_prompt)
175
176
  params.set("product_prompt", overrides.product_prompt);
177
+ if (overrides.restart) params.set("restart", "true");
176
178
  const query = params.toString();
177
179
  const response = await client.get<{ data: unknown }>(
178
180
  `/editor/${slug}/autopilot/cost${query ? `?${query}` : ""}`,
@@ -215,6 +217,34 @@ async function editorAutopilotQuote(
215
217
  }
216
218
  }
217
219
 
220
+ async function editorChargeReceipt(
221
+ client: HubfluencerClient,
222
+ slug: string,
223
+ action: "scene_regeneration" | "voice_generation" | "music_generation",
224
+ listPriceCredits: number,
225
+ ): Promise<Record<string, unknown>> {
226
+ try {
227
+ const response = await client.get<{ data: unknown }>(`/editor/${slug}`);
228
+ const editor = asRecord(asRecord(response).data);
229
+ return {
230
+ action,
231
+ list_price_credits: listPriceCredits,
232
+ project_spend: editor.project_spend ?? null,
233
+ approved_cap: editor.autopilot_max_credits ?? null,
234
+ approved_run_spend: editor.autopilot_credits_spent ?? null,
235
+ };
236
+ } catch {
237
+ // The paid mutation already succeeded. Never make a receipt-enrichment GET
238
+ // look like a failed charge that an agent should retry.
239
+ return {
240
+ action,
241
+ list_price_credits: listPriceCredits,
242
+ project_spend: null,
243
+ enrichment_unavailable: true,
244
+ };
245
+ }
246
+ }
247
+
218
248
  function isRecompositeInProgressConflict(e: unknown): boolean {
219
249
  const err = e as Partial<HubfluencerError>;
220
250
  if (err?.status !== 409) return false;
@@ -3278,7 +3308,7 @@ registerTool(
3278
3308
  "explicit max_credits cap to authorize launch; the tool refuses if pricing is unavailable, the estimate exceeds " +
3279
3309
  "the cap, or the balance is insufficient. Each authorized call is a genuine start attempt (like tapping Launch in the app): " +
3280
3310
  "if a run is already in progress for this slug the server returns already_running — it never double-starts " +
3281
- "or double-charges. Once finished, failed, or cancelled, calling again relaunches it (resume after a top-up, raising your spend cap, or a mid-pipeline failure). Then poll with " +
3311
+ "or double-charges. Failed or cancelled runs resume normally. For a completed project, set restart:true to quote and build a fresh creative run; omitting it only resumes pending edits. Then poll with " +
3282
3312
  'wait_for_completion({ slug, kind: "editor" }).',
3283
3313
  inputSchema: {
3284
3314
  slug: z.string().describe("Editor project slug"),
@@ -3309,6 +3339,12 @@ registerTool(
3309
3339
  .describe(
3310
3340
  "Explicit spend cap. Omit to quote only; no credits are charged.",
3311
3341
  ),
3342
+ restart: z
3343
+ .boolean()
3344
+ .optional()
3345
+ .describe(
3346
+ "Set true only on a completed project to quote/build a fresh creative run. The delivered timeline stays available until replacement apply.",
3347
+ ),
3312
3348
  },
3313
3349
  outputSchema: createEditorOutput,
3314
3350
  annotations: { title: "Start autopilot", ...WRITE, idempotentHint: false },
@@ -3321,6 +3357,7 @@ registerTool(
3321
3357
  product_subject?: string;
3322
3358
  product_prompt?: string;
3323
3359
  max_credits?: number;
3360
+ restart?: boolean;
3324
3361
  },
3325
3362
  client,
3326
3363
  ) => {
@@ -3334,12 +3371,14 @@ registerTool(
3334
3371
  language?: string;
3335
3372
  product_subject?: string;
3336
3373
  product_prompt?: string;
3374
+ restart?: boolean;
3337
3375
  } = {};
3338
3376
  if (args.language !== undefined) overrides.language = args.language;
3339
3377
  if (args.product_subject !== undefined)
3340
3378
  overrides.product_subject = args.product_subject;
3341
3379
  if (args.product_prompt !== undefined)
3342
3380
  overrides.product_prompt = args.product_prompt;
3381
+ if (args.restart) overrides.restart = true;
3343
3382
 
3344
3383
  const quote = await editorAutopilotQuote(client, slug, overrides);
3345
3384
  const overCap =
@@ -4021,7 +4060,15 @@ registerTool(
4021
4060
  args.prompt !== undefined ? { prompt: args.prompt } : undefined,
4022
4061
  undefined,
4023
4062
  );
4024
- return ok(asRecord(res).data ?? res);
4063
+ return ok({
4064
+ ...asRecord(asRecord(res).data ?? res),
4065
+ charge_receipt: await editorChargeReceipt(
4066
+ client,
4067
+ args.slug,
4068
+ "scene_regeneration",
4069
+ 4,
4070
+ ),
4071
+ });
4025
4072
  },
4026
4073
  ),
4027
4074
  );
@@ -4230,7 +4277,15 @@ registerTool(
4230
4277
  { voice_id: args.voice_id },
4231
4278
  await voiceKey(client, args.slug, args.voice_id),
4232
4279
  );
4233
- return ok(asRecord(res).data ?? res);
4280
+ return ok({
4281
+ ...asRecord(asRecord(res).data ?? res),
4282
+ charge_receipt: await editorChargeReceipt(
4283
+ client,
4284
+ args.slug,
4285
+ "voice_generation",
4286
+ 3,
4287
+ ),
4288
+ });
4234
4289
  }),
4235
4290
  );
4236
4291
 
@@ -4278,7 +4333,15 @@ registerTool(
4278
4333
  body,
4279
4334
  undefined,
4280
4335
  );
4281
- return ok(asRecord(res).data ?? res);
4336
+ return ok({
4337
+ ...asRecord(asRecord(res).data ?? res),
4338
+ charge_receipt: await editorChargeReceipt(
4339
+ client,
4340
+ args.slug,
4341
+ "music_generation",
4342
+ 5,
4343
+ ),
4344
+ });
4282
4345
  },
4283
4346
  ),
4284
4347
  );
@@ -4289,7 +4352,8 @@ registerTool(
4289
4352
  title: "Render the final editor video (0 credits)",
4290
4353
  description:
4291
4354
  "Renders the final MP4 from the generated segments + voice + music. Costs 0 credits (any still-ungenerated " +
4292
- "segments are auto-charged at generation cost first). Re-rendering reflects the current segments/voice/music (it does not replay a prior render). Returns the latest_render; " +
4355
+ "segments are auto-charged at generation cost first). Render NEVER generates or charges for voice/music; a new " +
4356
+ "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; " +
4293
4357
  'pair with wait_for_completion({ slug, kind: "editor" }) then download_result. ' +
4294
4358
  "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.",
4295
4359
  inputSchema: { slug: z.string().describe("Editor project slug") },