@hubfluencer/mcp 0.10.0 → 0.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.10.0",
3
+ "version": "0.12.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
@@ -457,15 +457,21 @@ export function normalizeStatus(
457
457
  );
458
458
  const batchStatus = (d.batch_generation_status as string) ?? null;
459
459
  // The batch enum's ACTIVE states (video_factory.ex): preparing (spinning up),
460
- // awaiting_previews (previews generating), running, and paused_for_retry
461
- // (auto-resuming). "queued" is not a real value and was dropped; "preparing"
462
- // and "awaiting_previews" were missing, so an in-flight batch briefly looked
463
- // idle and could be reported terminal mid-run.
460
+ // awaiting_previews (previews generating), and running. "queued" is not a real
461
+ // value and was dropped. paused_for_retry is deliberately NOT active: no worker
462
+ // owns it until the user explicitly retries or cancels the paused batch.
464
463
  const batchActive =
465
464
  batchStatus === "running" ||
466
465
  batchStatus === "preparing" ||
467
- batchStatus === "awaiting_previews" ||
468
- batchStatus === "paused_for_retry";
466
+ batchStatus === "awaiting_previews";
467
+ // The scene batch commits `completed` before the durable auto-render handoff
468
+ // creates its VideoResult row. While the persisted intent remains armed, the
469
+ // server still owns the project and a reconciler may create the render at any
470
+ // moment. Treat that narrow gap as active work so wait_for_completion cannot
471
+ // return an idle/terminal result just before the render appears.
472
+ const autoRenderHandoffActive =
473
+ batchStatus === "completed" && d.auto_render_after_batch === true;
474
+ const batchRetryRequired = batchStatus === "paused_for_retry";
469
475
  // A parked batch that ran out of credits: it cannot progress without the user
470
476
  // topping up or reducing scope, so it is a needs-user-action TERMINAL state
471
477
  // (not active work to wait on).
@@ -566,14 +572,40 @@ export function normalizeStatus(
566
572
  currentMusic.status === "processing" ||
567
573
  newerAudioVersions.some(versionActive) ||
568
574
  newerMusicVersions.some(versionActive);
569
- const workActive =
570
- autopilotActive ||
575
+ const childWorkActive =
571
576
  batchActive ||
577
+ autoRenderHandoffActive ||
572
578
  renderActive ||
573
579
  scenarioActive ||
574
580
  narrationActive ||
575
581
  audioActive ||
576
582
  activeSegments.length > 0;
583
+ // A resumed Autopilot briefly exposes the previous attempt's failed child
584
+ // states before its new worker reaches and clears them. Suppress that history
585
+ // only while real child work is active or for a short, timestamped hand-off
586
+ // window. A parent stuck "running" with no worker must eventually surface the
587
+ // child failure instead of making wait_for_completion poll forever.
588
+ const autopilotStartedAt =
589
+ typeof d.autopilot_started_at === "string"
590
+ ? Date.parse(d.autopilot_started_at)
591
+ : Number.NaN;
592
+ const now = Date.now();
593
+ const autopilotResumeGraceActive =
594
+ autopilotActive &&
595
+ Number.isFinite(autopilotStartedAt) &&
596
+ autopilotStartedAt <= now &&
597
+ now - autopilotStartedAt <= 120_000;
598
+ const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
599
+ // Retrying a paused batch deliberately leaves the failed parent Autopilot
600
+ // marker in place while the already-paid child batch finishes. In that one
601
+ // state combination the parent/old scene failures are history, not a reason
602
+ // to return terminal immediately. Once the batch settles, the parent failure
603
+ // becomes actionable again and the agent can quote/relaunch Autopilot.
604
+ const batchRecoveryActive = autopilotFailed && batchActive;
605
+ const childFailureActionable =
606
+ !batchRecoveryActive &&
607
+ (!autopilotActive || (!childWorkActive && !autopilotResumeGraceActive));
608
+ const workActive = autopilotActive || childWorkActive;
577
609
  // Orchestration failures are historical once a repaired timeline has a fresh,
578
610
  // completed MP4. Keeping them terminal would permanently block download_result
579
611
  // after a successful granular recovery.
@@ -586,17 +618,26 @@ export function normalizeStatus(
586
618
  failedSegments.length === 0 &&
587
619
  !audioFailed;
588
620
  const ready = deliveredArtifactSupersedesOrchestrationFailure;
589
- const autopilotFailed = autopilot === "failed" || autopilot === "cancelled";
590
- const renderFailed = renderStatus === "failed";
621
+ const parentFailureActionable = autopilotFailed && !batchRecoveryActive;
622
+ const renderFailed = renderStatus === "failed" && childFailureActionable;
591
623
  const failed =
592
- (autopilotFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
624
+ (parentFailureActionable &&
625
+ !deliveredArtifactSupersedesOrchestrationFailure) ||
593
626
  renderFailed ||
594
- (scenarioApplyFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
595
- (scenarioFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
596
- (narrationFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
597
- (batchFailed && !deliveredArtifactSupersedesOrchestrationFailure) ||
598
- failedSegments.length > 0 ||
599
- audioFailed;
627
+ (childFailureActionable &&
628
+ scenarioApplyFailed &&
629
+ !deliveredArtifactSupersedesOrchestrationFailure) ||
630
+ (childFailureActionable &&
631
+ scenarioFailed &&
632
+ !deliveredArtifactSupersedesOrchestrationFailure) ||
633
+ (childFailureActionable &&
634
+ narrationFailed &&
635
+ !deliveredArtifactSupersedesOrchestrationFailure) ||
636
+ (childFailureActionable &&
637
+ batchFailed &&
638
+ !deliveredArtifactSupersedesOrchestrationFailure) ||
639
+ (childFailureActionable && failedSegments.length > 0) ||
640
+ (childFailureActionable && audioFailed);
600
641
  // Terminal = control should return to the agent because nothing will progress
601
642
  // without another tool call. With every async job type folded into workActive
602
643
  // above (autopilot, batch, segments, scenario, narration, audio, render), NO
@@ -605,24 +646,39 @@ export function normalizeStatus(
605
646
  // a new tool call can move it, so wait_for_completion must return instead of
606
647
  // polling its full budget. (ready is subsumed by !workActive but kept for
607
648
  // clarity; explicit failure states are terminal even while cleanup work spins.)
608
- const terminal = ready || failed || batchInsufficientCredits || !workActive;
649
+ const terminal =
650
+ ready ||
651
+ failed ||
652
+ batchRetryRequired ||
653
+ batchInsufficientCredits ||
654
+ !workActive;
609
655
  let stage = autopilot;
610
656
  // Failure always wins over an unrelated active flag. Workers can leave an
611
657
  // outer autopilot/batch state active after a child stage has failed; reporting
612
658
  // the active stage would make agents poll forever instead of taking action.
613
659
  if (ready) stage = "completed";
614
- else if (autopilot === "failed") stage = "failed";
615
- else if (autopilot === "cancelled") stage = "cancelled";
660
+ // A paused child batch is the immediate recovery gate even when its parent
661
+ // Autopilot run has already failed/cancelled. Reporting only the generic
662
+ // parent terminal state hides the one action that must happen before a new
663
+ // Autopilot run is allowed to start.
664
+ else if (batchRetryRequired) stage = "batch_retry_required";
665
+ else if (parentFailureActionable && autopilot === "failed") stage = "failed";
666
+ else if (parentFailureActionable && autopilot === "cancelled")
667
+ stage = "cancelled";
616
668
  else if (renderFailed) stage = "render_failed";
617
- else if (scenarioApplyFailed) stage = "scenario_apply_failed";
618
- else if (scenarioFailed) stage = "scenario_failed";
619
- else if (narrationFailed) stage = "narration_failed";
620
- else if (batchFailed) stage = "batch_failed";
621
- else if (failedSegments.length > 0) stage = "segment_failed";
622
- else if (audioFailed) stage = "audio_failed";
669
+ else if (childFailureActionable && scenarioApplyFailed)
670
+ stage = "scenario_apply_failed";
671
+ else if (childFailureActionable && scenarioFailed) stage = "scenario_failed";
672
+ else if (childFailureActionable && narrationFailed)
673
+ stage = "narration_failed";
674
+ else if (childFailureActionable && batchFailed) stage = "batch_failed";
675
+ else if (childFailureActionable && failedSegments.length > 0)
676
+ stage = "segment_failed";
677
+ else if (childFailureActionable && audioFailed) stage = "audio_failed";
623
678
  else if (batchInsufficientCredits) stage = "insufficient_credits";
624
679
  else if (activeSegments.length > 0) stage = "segment_processing";
625
680
  else if (batchActive) stage = "segment_generation";
681
+ else if (autoRenderHandoffActive) stage = "rendering";
626
682
  else if (renderActive) stage = "rendering";
627
683
  else if (scenarioActive) stage = "scenario_processing";
628
684
  else if (narrationActive) stage = "narration_processing";
@@ -633,6 +689,15 @@ export function normalizeStatus(
633
689
  .find(
634
690
  (value): value is string => typeof value === "string" && value.length > 0,
635
691
  );
692
+ const actionableChildError = childFailureActionable
693
+ ? ((latest.error_message as string) ??
694
+ (d.scenario_apply_error_message as string) ??
695
+ (d.scenario_error_message as string) ??
696
+ (d.narration_error_message as string) ??
697
+ (d.batch_generation_error_message as string) ??
698
+ segmentError ??
699
+ audioError)
700
+ : null;
636
701
  return {
637
702
  kind,
638
703
  slug,
@@ -642,17 +707,15 @@ export function normalizeStatus(
642
707
  video_url: videoUrl,
643
708
  error: ready
644
709
  ? null
645
- : ((d.autopilot_error_message as string) ??
646
- (latest.error_message as string) ??
647
- (d.scenario_apply_error_message as string) ??
648
- (d.scenario_error_message as string) ??
649
- (d.narration_error_message as string) ??
650
- (d.batch_generation_error_message as string) ??
651
- segmentError ??
652
- audioError ??
653
- (batchInsufficientCredits
654
- ? "Batch generation paused: not enough credits to finish. Top up or reduce scope, then resume."
655
- : null)),
710
+ : batchRetryRequired
711
+ ? "Batch generation is paused after a failed scene. Call retry_editor_batch or cancel_editor_batch before continuing."
712
+ : ((parentFailureActionable
713
+ ? (d.autopilot_error_message as string)
714
+ : null) ??
715
+ actionableChildError ??
716
+ (batchInsufficientCredits
717
+ ? "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."
718
+ : null)),
656
719
  stale,
657
720
  active_segment_ids: activeSegments
658
721
  .map((segment) => segment.id)
@@ -802,7 +865,6 @@ export interface EditorAdKeyArgs {
802
865
  logo_treatment?: string;
803
866
  logo_position?: string;
804
867
  logo_duration_seconds?: number;
805
- cast_mode?: string;
806
868
  /** Identities of the retained, pre-create asset byte snapshots. */
807
869
  product_image_identity?: string;
808
870
  closing_image_identity?: string;
@@ -842,7 +904,6 @@ export function editorAdCreateKey(a: EditorAdKeyArgs): string {
842
904
  a.logo_duration_seconds === undefined
843
905
  ? ""
844
906
  : String(a.logo_duration_seconds),
845
- a.cast_mode ?? "",
846
907
  );
847
908
  }
848
909
 
@@ -877,7 +938,6 @@ export function editorAdAutopilotKey(slug: string, a: EditorAdKeyArgs): string {
877
938
  a.logo_duration_seconds === undefined
878
939
  ? ""
879
940
  : String(a.logo_duration_seconds),
880
- a.cast_mode ?? "",
881
941
  a.max_credits === undefined ? "" : String(a.max_credits),
882
942
  );
883
943
  }
@@ -1022,10 +1082,9 @@ export function validateLanguage(language: string | undefined): string | null {
1022
1082
  export function validateProductPrompt(
1023
1083
  prompt: string | undefined,
1024
1084
  ): string | null {
1025
- // A trimmed-empty string is a no-op on every path the server exposes:
1026
- // pre-launch override handling drops blank values (leave the stored value
1027
- // untouched) and the editor prompt is valid when blank, so treat "" the same
1028
- // as undefined. Still reject 1–9 non-blank chars (too short to be a brief).
1085
+ // Empty is valid but its meaning is endpoint-specific: draft creation omits
1086
+ // it, while Autopilot treats an explicitly present blank as an atomic clear.
1087
+ // Still reject 1–9 non-blank chars (too short to be a brief).
1029
1088
  if (prompt === undefined) return null;
1030
1089
  const len = prompt.trim().length;
1031
1090
  if (len === 0) return null;