@pixeldojo/mcp 0.30.0 → 0.34.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/dist/cli.js CHANGED
@@ -246,6 +246,14 @@ async function snapAspectRatio(apiId, requested, client, opts = {}) {
246
246
  if (!field.enum || field.enum.length === 0) return requested;
247
247
  return nearestAspectFromEnum(requested, field.enum);
248
248
  }
249
+ async function snapImageSizePreset(apiId, requested, client) {
250
+ if (!requested) return void 0;
251
+ const props2 = await getFieldConstraints(apiId, client);
252
+ const values = props2?.image_size?.enum;
253
+ if (!values || props2?.aspect_ratio) return void 0;
254
+ const key = requested.trim().replace(":", "_");
255
+ return values.find((v) => String(v).endsWith(`_${key}`));
256
+ }
249
257
  async function resolveMediaSlot(apiId, kind, client) {
250
258
  const info = await getModelInfo(apiId, client);
251
259
  if (info === null) return void 0;
@@ -504,20 +512,24 @@ async function run(input, ctx) {
504
512
  }
505
513
  }
506
514
  let refField;
515
+ let imageSizePreset;
507
516
  let audioSlot;
508
517
  if (!VIDEO_INPUT_MODELS.has(apiId)) {
509
- const [aspect_ratio, duration, resolution, ref, audio] = await Promise.all([
518
+ const [aspect_ratio, imageSize, duration, resolution, ref, audio] = await Promise.all([
510
519
  snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
520
+ snapImageSizePreset(apiId, resolved.aspect_ratio, ctx.client),
511
521
  snapDuration(apiId, resolved.duration, ctx.client),
512
522
  snapResolution(apiId, resolved.resolution, ctx.client),
513
523
  src.url ? resolveMediaSlot(apiId, src.isClip ? "video" : "image", ctx.client) : Promise.resolve(void 0),
514
524
  src.audioUrl ? resolveMediaSlot(apiId, "audio", ctx.client) : Promise.resolve(void 0)
515
525
  ]);
516
526
  resolved = { ...resolved, aspect_ratio, duration, resolution };
527
+ imageSizePreset = imageSize;
517
528
  refField = ref === void 0 && src.url ? fallbackSlot(apiId, src.isClip ? "video" : "image") : ref;
518
529
  audioSlot = audio === void 0 && src.audioUrl ? fallbackSlot(apiId, "audio") : audio;
519
530
  }
520
531
  const body = buildPayload(apiId, resolved, src, refField, audioSlot);
532
+ if (imageSizePreset) body.image_size = imageSizePreset;
521
533
  const paramsNote = applyParams(body, input.params, input.model, apiId);
522
534
  const note = [routeNote, paramsNote].filter(Boolean).join(" ");
523
535
  const submitted = await ctx.client.run(apiId, body);
@@ -549,7 +561,7 @@ var init_generate = __esm({
549
561
  InputSchema = z.object({
550
562
  prompt: z.string().min(1).max(8e3).describe('Prompt describing what to generate. Write @Name to use a character or element the user saved in Character Studio ("@Mara on a beach"): its photos are attached automatically on models that take reference images. pixeldojo_library source=characters lists them.'),
551
563
  model: z.string().optional().describe(
552
- `Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
564
+ `Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "kling-4-0" for a whole scene up to 30 s written as a shot list (dialogue in quotes, exact on-screen text), "minimax-h3-turbo" for the cheapest video from a prompt alone, a photo or up to 9 reference images, "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
553
565
  ),
554
566
  preset: z.string().optional().describe(
555
567
  "Preset slug that seeds the running model's settings; explicit fields and params override it. Each preset belongs to one model (browse them by model at https://pixeldojo.ai/presets), so set `model` to that preset's model: a slug the running model has no preset for is rejected with a 400 before any charge."
@@ -588,7 +600,7 @@ var init_generate = __esm({
588
600
  prompt: { type: "string", description: 'Prompt describing what to generate. Write @Name to use a character or element the user saved in Character Studio ("@Mara on a beach"): its photos are attached automatically on models that take reference images. pixeldojo_library source=characters lists them.' },
589
601
  model: {
590
602
  type: "string",
591
- description: `Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
603
+ description: `Model apiId. Recommended (the models the PixelDojo studios use): "seedream-5" for images (the default), "seedance-2-5" for video (prompt only or a starting image), "kling-4-0" for a whole scene up to 30 s written as a shot list (dialogue in quotes, exact on-screen text), "minimax-h3-turbo" for the cheapest video from a prompt alone, a photo or up to 9 reference images, "wan-3-0-video" for video the others refuse. Use another model only when the user names one. Full catalog: https://pixeldojo.ai/api/v1/models; one model's fields: https://pixeldojo.ai/llm/{apiId}.txt. Omit for seedream-5, which makes still images only; name a video model to get a video.`
592
604
  },
593
605
  preset: {
594
606
  type: "string",
@@ -636,8 +648,8 @@ var init_generate = __esm({
636
648
  };
637
649
  generateSkill = {
638
650
  name: "pixeldojo_generate",
639
- description: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills, `video_url` for a source clip, `image_path`/`image_paths` for absolute local files (uploaded to 24h temp storage first). A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
640
- hostedDescription: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills and `video_url` for a source clip, all public http(s) URLs. A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
651
+ description: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image; kling-4-0 for a multi-shot scene up to 30 s with dialogue; minimax-h3-turbo for the lowest price, from a prompt alone, a photo or reference images. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills, `video_url` for a source clip, `image_path`/`image_paths` for absolute local files (uploaded to 24h temp storage first). A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
652
+ hostedDescription: "Generate an image or video from a prompt with any model in the PixelDojo catalog. With no `model`, a prompt runs on seedream-5, which makes still images only. For a video, name seedance-2-5, which works from a prompt alone or a starting image; kling-4-0 for a multi-shot scene up to 30 s with dialogue; minimax-h3-turbo for the lowest price, from a prompt alone, a photo or reference images. If a provider refuses the content, retry once on wan-3-0-video (video) or seedream-5 (images), the least-restricted models. For a multi-shot film or a music video synced to a song use pixeldojo_film; to change, move or extend an existing clip use pixeldojo_video. These are the models the PixelDojo studios use; name another only when the user asks for it. References: `image_url`/`image_urls` for stills and `video_url` for a source clip, all public http(s) URLs. A clip or an audio track URL with no `model` runs on seedance-2-reference; a named model that cannot read a clip is swapped for it and the result says so, while a named model with no audio input drops the track. Model-specific settings go in `params` alongside an explicit `model`. Returns asset URLs, or a jobId for pixeldojo_status when the job outlasts the 30s wait.",
641
653
  inputZod: InputSchema,
642
654
  inputJsonSchema: InputJsonSchema,
643
655
  allowedModels: null,
@@ -923,7 +935,8 @@ var init_character = __esm({
923
935
  "google-nano-banana",
924
936
  "wan-2.7-image-edit",
925
937
  "qwen-3-pro",
926
- "qwen-image-2.0-pro"
938
+ "qwen-image-2.0-pro",
939
+ "p-h3-image"
927
940
  ];
928
941
  DEFAULT_MODEL3 = "seedream-5";
929
942
  MAX_REFERENCES = 9;
@@ -935,7 +948,8 @@ var init_character = __esm({
935
948
  "google-nano-banana": "image_input",
936
949
  "wan-2.7-image-edit": "image_urls",
937
950
  "qwen-3-pro": "image",
938
- "qwen-image-2.0-pro": "reference_images"
951
+ "qwen-image-2.0-pro": "reference_images",
952
+ "p-h3-image": "reference_images"
939
953
  };
940
954
  InputSchema3 = z.object({
941
955
  prompt: z.string().min(1).max(8e3),
@@ -1051,6 +1065,8 @@ var init_storyboard = __esm({
1051
1065
  ALLOWED2 = [
1052
1066
  "veo-3.1-fast",
1053
1067
  "veo-3.1-standard",
1068
+ // Kling 4.0 (2026-10-24): a brief written as a shot list renders as one clip, up to 30 s.
1069
+ "kling-4-0",
1054
1070
  "kling-video-v3-standard-text",
1055
1071
  "seedance-2-high",
1056
1072
  VIDEO_REFERENCE_MODEL
@@ -1677,7 +1693,7 @@ async function filmApi(ctx, method, path, body) {
1677
1693
  Authorization: `Bearer ${ctx.apiKey}`,
1678
1694
  "Content-Type": "application/json",
1679
1695
  "x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
1680
- "x-pixeldojo-skill": "film"
1696
+ "x-pixeldojo-skill": "pixeldojo_film"
1681
1697
  },
1682
1698
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
1683
1699
  });
@@ -2006,7 +2022,7 @@ It is saved in the Library folder "Film: ${film.title}".`,
2006
2022
  }
2007
2023
  const startExport = async () => {
2008
2024
  const r = await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
2009
- return r.status === 200 || r.status === 409 ? null : r.data.error ?? `HTTP ${r.status}`;
2025
+ return r.status === 200 || r.status === 409 && r.data.code === "export-running" ? null : r.data.error ?? `HTTP ${r.status}`;
2010
2026
  };
2011
2027
  if (film.exportError && all.length > 0 && all.every((sh) => sh.status === "clip-ready")) {
2012
2028
  const again = await startExport();
@@ -2436,17 +2452,47 @@ function buildVideoPayload(verb, apiId, i) {
2436
2452
  ...vd !== void 0 ? { source_duration_seconds: vd } : {}
2437
2453
  }
2438
2454
  };
2439
- case "flux-video-edit":
2440
- return { body: { prompt, video: i.videoUrl, video_duration: Math.ceil(vd) }, note: droppedImages };
2441
2455
  case "xai-video-edit":
2442
2456
  return { body: { prompt, video_url: i.videoUrl, video_duration: vd }, note: droppedImages };
2443
2457
  case "happyhorse-1.0-video-edit":
2444
2458
  return { body: { prompt, video_url: i.videoUrl, video_duration: vd, reference_urls: i.images } };
2459
+ case "kling-4-0":
2460
+ return {
2461
+ body: {
2462
+ prompt: /@video\s*1/i.test(prompt) ? prompt : `Edit @Video1: ${prompt}`,
2463
+ video: i.videoUrl,
2464
+ reference_images: i.images,
2465
+ duration: clamp(Math.ceil(vd), 3, 30),
2466
+ resolution: "1080p"
2467
+ }
2468
+ };
2469
+ case "minimax-h3-turbo":
2470
+ return {
2471
+ body: {
2472
+ prompt: `Recreate <Video 1> exactly: the same people, the same motion and timing, the same camera move and framing, with one change: ${prompt}`,
2473
+ reference_videos: [i.videoUrl],
2474
+ reference_video_audio: true,
2475
+ resolution: "720p",
2476
+ duration: clamp(Math.ceil(vd ?? 5), 5, 15)
2477
+ },
2478
+ note: droppedImages
2479
+ };
2445
2480
  default:
2446
2481
  return { body: { prompt, video_url: i.videoUrl, video_duration: vd, image_urls: i.images } };
2447
2482
  }
2448
2483
  }
2449
2484
  if (verb === "move") {
2485
+ if (apiId === "p-video-2-pro-replace") {
2486
+ return {
2487
+ body: {
2488
+ prompt: "",
2489
+ video: i.videoUrl,
2490
+ images: i.images.slice(0, 2),
2491
+ video_duration: vd,
2492
+ ...prompt ? { instruction_prompt: prompt } : {}
2493
+ }
2494
+ };
2495
+ }
2450
2496
  if (apiId === "wan-video-character-swap") {
2451
2497
  return { body: { prompt: "", image_url: i.images[0], video_url: i.videoUrl, video_duration: vd } };
2452
2498
  }
@@ -2568,30 +2614,31 @@ var init_video = __esm({
2568
2614
  VIDEO_STUDIO_ENGINES = {
2569
2615
  change: [
2570
2616
  "seedance-2-5-video-edit",
2571
- "flux-video-edit",
2572
- "kling-video-edit",
2617
+ "kling-4-0",
2618
+ "minimax-h3-turbo",
2573
2619
  "wan-video-edit",
2574
2620
  "seedance-video-edit",
2575
2621
  "happyhorse-1.0-video-edit",
2576
2622
  "xai-video-edit"
2577
2623
  ],
2578
- move: ["kling-motion-control", "wan-video-character-swap"],
2624
+ move: ["p-video-2-pro-replace", "kling-motion-control", "wan-video-character-swap"],
2579
2625
  extend: ["seedance-2-5-video-edit", "wan-2.7-i2v", "grok-video-extend"]
2580
2626
  };
2581
2627
  VERBS = ["change", "move", "extend"];
2582
2628
  BACKWARD_ENGINES = /* @__PURE__ */ new Set(["seedance-2-5-video-edit"]);
2583
2629
  clamp = (n, min, max) => Math.min(max, Math.max(min, Math.round(n)));
2584
2630
  NEEDS_VIDEO_DURATION = /* @__PURE__ */ new Set([
2585
- "flux-video-edit",
2586
- "kling-video-edit",
2587
2631
  "wan-video-edit",
2588
2632
  "seedance-video-edit",
2589
2633
  "happyhorse-1.0-video-edit",
2590
2634
  "xai-video-edit",
2591
2635
  "kling-motion-control",
2592
- "wan-video-character-swap"
2636
+ "wan-video-character-swap",
2637
+ "kling-4-0",
2638
+ // Its schema defaults the length, but the price is per second: a missing length would under-charge.
2639
+ "p-video-2-pro-replace"
2593
2640
  ]);
2594
- NO_IMAGE_ENGINES = /* @__PURE__ */ new Set(["flux-video-edit", "xai-video-edit"]);
2641
+ NO_IMAGE_ENGINES = /* @__PURE__ */ new Set(["xai-video-edit", "minimax-h3-turbo"]);
2595
2642
  InputSchema11 = z.object({
2596
2643
  verb: z.enum(VERBS),
2597
2644
  prompt: z.string().max(8e3).optional(),
@@ -2617,7 +2664,7 @@ var init_video = __esm({
2617
2664
  },
2618
2665
  prompt: {
2619
2666
  type: "string",
2620
- description: "What to change, how the character should move, or what happens next. Required for change and extend. Optional for move (wan-video-character-swap ignores it). @Name uses a character or element the user saved in Character Studio (photos attached automatically)."
2667
+ description: "What to change, how the character should move, or what happens next. Required for change and extend. Optional for move (p-video-2-pro-replace reads it as who becomes whom; wan-video-character-swap ignores it). @Name uses a character or element the user saved in Character Studio (photos attached automatically)."
2621
2668
  },
2622
2669
  video_url: {
2623
2670
  type: "string",
@@ -2683,7 +2730,7 @@ async function episodeApi(ctx, method, path, body) {
2683
2730
  Authorization: `Bearer ${ctx.apiKey}`,
2684
2731
  "Content-Type": "application/json",
2685
2732
  "x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
2686
- "x-pixeldojo-skill": "episode"
2733
+ "x-pixeldojo-skill": "pixeldojo_episode"
2687
2734
  },
2688
2735
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
2689
2736
  });
@@ -2924,7 +2971,7 @@ async function dramaApi(ctx, method, path, body) {
2924
2971
  Authorization: `Bearer ${ctx.apiKey}`,
2925
2972
  "Content-Type": "application/json",
2926
2973
  "x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
2927
- "x-pixeldojo-skill": "drama"
2974
+ "x-pixeldojo-skill": "pixeldojo_drama"
2928
2975
  },
2929
2976
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
2930
2977
  });
@@ -2938,14 +2985,23 @@ function describeDrama(v, o = DRAMA_OPTS) {
2938
2985
  const next2 = (action) => `${o.tool} with {"${o.idField}": "${id}", "action": "${action}"}`;
2939
2986
  const music = o.unit === "section";
2940
2987
  const what = music ? "music video" : "episode";
2988
+ if (v.status === "failed" && v.error?.startsWith("Prohibited content")) return { kind: "text", text: `${v.error} Tell the user this idea is prohibited content; do not rewrite it to get around the refusal.` };
2941
2989
  if (v.status === "failed") return { kind: "text", text: `The ${what} stopped: ${v.error ?? "unknown error"}. Each render that failed was refunded.${v.beats.some((b) => b.url) ? ` Re-film ${o.unit}s with action "redo".` : ""}` };
2942
2990
  if (v.status === "quote") {
2943
- const cost = music ? `Filming costs about ${v.estimate_credits} credits (cast portraits and ${v.sections?.length ?? "several"} sections lip-synced to the song).` : `Filming costs about ${v.estimate_credits} credits (cast portraits, 12-second scenes with their dialogue and sound${v.score === false ? ", no music" : ", and one score over the finished edit"}); every scene is checked, and a scene that drops a line or shows a clear fault waits for the user's OK before it is filmed again, at the scene's own price (a render that fails is tried again free).`;
2991
+ const cost = music ? `Filming costs about ${v.estimate_credits} credits (cast portraits and ${v.sections?.length ?? "several"} sections lip-synced to the song).` : `Filming costs about ${v.estimate_credits} credits (cast portraits, scenes of up to 30 seconds with their dialogue and sound${v.score === false ? ", no music" : ", and one score over the finished edit"}); every scene is checked, and a scene that drops a line or shows a clear fault waits for the user's OK before it is filmed again, at the scene's own price (a render that fails is tried again free).`;
2944
2992
  return {
2945
2993
  kind: "text",
2946
2994
  text: [`Planned (${o.idField}: ${id}), ${v.format === "9:16" ? "vertical 9:16" : "widescreen 16:9"} (to switch, create it again in the other format before producing):`, v.plan ?? "", "", `Nothing has been charged yet. ${cost} Show this to the user and, once they agree, call ${next2("produce")}. (In Sensei: put it in pixeldojo_plan as ONE step with skill "${music ? "music_video" : "drama"}" and params {"${o.idField}": "${id}"}, which prices it from this quote.)`].join("\n")
2947
2995
  };
2948
2996
  }
2997
+ const gist = (note) => {
2998
+ const first = note.split(/(?<=\.) /)[0];
2999
+ return first.length > 160 ? `${first.slice(0, 157).trimEnd()}...` : first;
3000
+ };
3001
+ const rewrites = (b) => {
3002
+ const shots2 = Array.from(new Set(Array.from((b.redo_films ?? "").matchAll(/Shot (\d+) \(/g), (m) => m[1])));
3003
+ return shots2.length ? ` (the redo rewrites shot${shots2.length > 1 ? "s" : ""} ${shots2.join(", ")})` : "";
3004
+ };
2949
3005
  if (v.status === "notes") {
2950
3006
  const flagged = v.beats.filter((b) => b.note && (b.needs_redo ?? true));
2951
3007
  const other = v.beats.filter((b) => b.note && b.needs_redo === false);
@@ -2953,17 +3009,17 @@ function describeDrama(v, o = DRAMA_OPTS) {
2953
3009
  kind: "text",
2954
3010
  text: [
2955
3011
  `Every ${o.unit} is filmed. Before the cut, the review flagged ${o.unit}s ${flagged.map((b) => b.beat).join(", ")}:`,
2956
- ...flagged.map((b) => `- ${o.unit} ${b.beat}: ${b.note}${b.redo_films ? ` The redo films: ${b.redo_films}` : ""}`),
2957
- other.length ? `Also noted, no redo needed: ${other.map((b) => `${o.unit} ${b.beat}: ${b.note}`).join(" ")}` : "",
3012
+ ...flagged.map((b) => `- ${o.unit} ${b.beat}: ${gist(b.note)}${rewrites(b)}`),
3013
+ other.length ? `Also noted, no redo needed: ${other.map((b) => `${o.unit} ${b.beat}: ${gist(b.note)}`).join(" ")}` : "",
2958
3014
  simplify,
2959
- `Show these to the user and ask which they want: redo those ${o.unit}s with these notes (${o.tool} with {"${o.idField}": "${id}", "action": "redo", "beats": [${flagged.map((b) => b.beat).join(", ")}], "notes": [the notes, same order]}, each at its own price), or cut it as it is (action "cut", free). Nothing more is filmed until they choose.`
3015
+ `Show these to the user and ask which they want: redo those ${o.unit}s with the review's fixes (${o.tool} with {"${o.idField}": "${id}", "action": "redo", "beats": [${flagged.map((b) => b.beat).join(", ")}]}, no notes needed, each at its own price; add notes only for the user's own changes), or cut it as it is (action "cut", free). Nothing more is filmed until they choose.`
2960
3016
  ].filter(Boolean).join("\n")
2961
3017
  };
2962
3018
  }
2963
3019
  if (v.status === "done" && v.final_url) {
2964
3020
  const redo = v.beats.filter((b) => (b.note || b.missing_lines.length) && (b.needs_redo ?? true));
2965
- const notes = redo.map((b) => `${o.unit} ${b.beat}: ${b.note ?? `did not say ${b.missing_lines.map((l) => `"${l}"`).join(", ")}`}${b.redo_films ? ` The redo films: ${b.redo_films}` : ""}`);
2966
- const other = v.beats.filter((b) => b.note && b.needs_redo === false).map((b) => `${o.unit} ${b.beat}: ${b.note}`);
3021
+ const notes = redo.map((b) => `${o.unit} ${b.beat}: ${b.note ? gist(b.note) : `did not say ${b.missing_lines.map((l) => `"${l}"`).join(", ")}`}`);
3022
+ const other = v.beats.filter((b) => b.note && b.needs_redo === false).map((b) => `${o.unit} ${b.beat}: ${gist(b.note)}`);
2967
3023
  return {
2968
3024
  kind: "assets",
2969
3025
  text: [
@@ -2979,7 +3035,7 @@ function describeDrama(v, o = DRAMA_OPTS) {
2979
3035
  const doing = music ? { scripting: "listening to the song and writing the plan", shots: "filming the sections", cutting: "cutting it to the song" }[v.status] ?? STEP[v.status] ?? v.status : STEP[v.status] ?? v.status;
2980
3036
  const ready = v.beats.filter((b) => b.url).length;
2981
3037
  const total = v.beats.length || (music ? v.sections?.length ?? 0 : 5);
2982
- const again = v.rendering.map((n) => v.beats.find((b) => b.beat === n)).filter((b) => Boolean(b?.url)).map((b) => `${o.unit} ${b.beat} is being filmed again${b.missing_lines.length ? ` (it dropped the line "${b.missing_lines[0]}")` : b.note ? ` (the review asked: ${b.note.split(/(?<=\.) /)[0]})` : ""}`);
3038
+ const again = v.rendering.map((n) => v.beats.find((b) => b.beat === n)).filter((b) => Boolean(b?.url)).map((b) => `${o.unit} ${b.beat} is being filmed again${b.filming_note ? ` (your note: ${gist(b.filming_note)})` : b.missing_lines.length ? ` (it dropped the line "${b.missing_lines[0]}")` : b.note ? ` (the review asked: ${gist(b.note)})` : ""}`);
2983
3039
  return {
2984
3040
  kind: "text",
2985
3041
  text: `Still ${doing}${v.status === "shots" ? ` (${ready} of ${total} ${o.unit}s ready${v.rendering.length ? `, filming ${o.unit} ${v.rendering.join(", ")}` : ""})` : ""}.${again.length ? ` ${again.join("; ")}.` : ""} It takes about 10 to 15 minutes in all. Call ${next2("status")} in about a minute.`,
@@ -3015,9 +3071,11 @@ var init_drama = __esm({
3015
3071
  end_text: z.string().max(60).optional(),
3016
3072
  brand_color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
3017
3073
  logo_url: z.string().url().optional(),
3018
- cast: z.array(z.object({ name: z.string().min(1).max(40), image_url: z.string().url() })).max(5).optional(),
3074
+ cast: z.array(z.object({ name: z.string().min(1).max(40), image_url: z.string().url(), outfit_image_url: z.string().url().optional() })).max(5).optional(),
3075
+ length_sec: z.number().int().min(24).max(180).multipleOf(6).optional(),
3019
3076
  scenes: z.number().int().min(1).max(15).optional(),
3020
3077
  score: z.boolean().optional(),
3078
+ engine: z.enum(["seedance-2-5", "wan-3-0-video", "kling-4-0"]).optional(),
3021
3079
  drama_id: z.string().max(60).optional(),
3022
3080
  beats: z.array(z.number().int().min(1).max(15)).min(1).max(15).optional(),
3023
3081
  notes: z.array(z.string().max(300)).max(15).optional()
@@ -3028,7 +3086,7 @@ var init_drama = __esm({
3028
3086
  action: {
3029
3087
  type: "string",
3030
3088
  enum: ["create", "produce", "status", "redo", "cut"],
3031
- description: "create (default, free): writes the episode plan (5 scenes unless scenes says, cast, lines) and returns drama_id and the price. Show the plan and price to the user and wait for their OK. produce (charged): films it. status (free): progress; call every ~60s until it returns the episode. redo (charged): re-film chosen beats of a finished episode or one waiting at the review, then it cuts again. cut (free): when the review flagged beats and the user wants it as it is, cut it now."
3089
+ description: "create (default, free): writes the plan (30 seconds unless length_sec says, cast, lines) and returns drama_id and the price. Show the plan and price to the user and wait for their OK. produce (charged): films it. status (free): progress; call every ~60s until it returns the episode. redo (charged): re-film chosen beats of a finished episode or one waiting at the review, then it cuts again. cut (free): when the review flagged beats and the user wants it as it is, cut it now."
3032
3090
  },
3033
3091
  idea: { type: "string", description: "Create: the story in a line or two (who, the conflict, the twist)." },
3034
3092
  format: { type: "string", enum: ["9:16", "16:9"], description: "Create: vertical 9:16 (default) or widescreen 16:9." },
@@ -3036,7 +3094,7 @@ var init_drama = __esm({
3036
3094
  kind: {
3037
3095
  type: "string",
3038
3096
  enum: [...KINDS],
3039
- description: "Create: micro-drama (default: episode one with a hook and a cliffhanger), short-film (a complete 60 s story) or trailer (sells a film that does not exist, never shows the ending)."
3097
+ description: "Create: micro-drama (default: episode one with a hook and a cliffhanger), short-film (a complete story) or trailer (sells a film that does not exist, never shows the ending)."
3040
3098
  },
3041
3099
  look: { type: "string", enum: [...LOOKS2], description: "Create (optional): the visual style and genre. Default live-action (a micro-drama story type brings its own)." },
3042
3100
  story_type: {
@@ -3056,13 +3114,16 @@ var init_drama = __esm({
3056
3114
  type: "object",
3057
3115
  properties: {
3058
3116
  name: { type: "string", description: "The character's name as the story uses it." },
3059
- image_url: { type: "string", description: "A clear front-facing portrait on the PixelDojo CDN." }
3117
+ image_url: { type: "string", description: `A clear front-facing portrait on the PixelDojo CDN (a saved character's first "for video" picture when it has one).` },
3118
+ outfit_image_url: { type: "string", description: `Optional: the saved character's outfit picture (the second "for video" picture from pixeldojo_library). The scenes then keep their clothes closer.` }
3060
3119
  },
3061
3120
  required: ["name", "image_url"]
3062
3121
  },
3063
- description: "Create (optional): up to 5 characters to star (people, animals or creatures), each with a portrait on the PixelDojo CDN (a saved character from pixeldojo_library). Others get a portrait made for them."
3122
+ description: 'Create (optional): up to 5 characters to star (people, animals or creatures), each with a portrait on the PixelDojo CDN (a saved character from pixeldojo_library; pass its "for video" face and outfit pictures when it lists them). Others get a portrait made for them.'
3064
3123
  },
3065
- scenes: { type: "integer", minimum: 1, maximum: 15, description: "Create (optional): how many 12-second scenes, default 5. Pass it when the user names a count or a length (about 12 s a scene)." },
3124
+ length_sec: { type: "integer", minimum: 24, maximum: 180, multipleOf: 6, description: "Create (optional): the film's length in seconds, 24 to 180 in steps of 6. Default 30. If the user has not said how long, ask (30 seconds, 1, 2 or 3 minutes; the price grows with the length) or leave it out for 30 seconds. The writer splits the length into scenes of up to 30 s, one per place and cast." },
3125
+ scenes: { type: "integer", minimum: 1, maximum: 15, description: "Create (optional, older): the length in 12-second units (5 = 60 s). Prefer length_sec." },
3126
+ engine: { type: "string", enum: ["seedance-2-5", "wan-3-0-video", "kling-4-0"], description: "Create (optional): the video model every scene films on, from the cast pictures. seedance-2-5 (default), wan-3-0-video (about a third of the price), kling-4-0. Pass it only when the user names one. Adult work always runs on wan-3-0-video." },
3066
3127
  score: { type: "boolean", description: "Create (optional): one score laid over the finished edit, on by default and included in the price. Pass false only when the user wants no music (dialogue and ambience only)." },
3067
3128
  drama_id: { type: "string", description: "From create. Required for produce, status and redo." },
3068
3129
  beats: { type: "array", items: { type: "integer", minimum: 1, maximum: 15 }, description: "Redo: the beat numbers to film again." },
@@ -3079,7 +3140,7 @@ var init_drama = __esm({
3079
3140
  DRAMA_OPTS = { tool: "pixeldojo_drama", idField: "drama_id", unit: "beat" };
3080
3141
  dramaSkill = {
3081
3142
  name: "pixeldojo_drama",
3082
- description: "Make a 60-second micro-drama episode (hook, twist, cliffhanger; optionally a classic genre such as billionaire, werewolf or mafia), short film (a complete story) or movie trailer from an idea, vertical 9:16 or 16:9, in any of Film Studio's looks (live-action, anime, 3D animation, noir, horror, sci-fi, western, romance, comedy and more). Five 12-second scenes, each filmed in one pass with several cuts, the cast's faces kept from portraits and every line spoken and lip-synced, cut together with their own sound and one score over the finished edit, with free motion graphics (title card, name tags, end screen; each can be turned off), and saved to the Library. Not for music videos (pixeldojo_music_video) create is free and returns the plan and the exact price (about 380 credits for a regular episode, about 160 for mature work); produce films it; then call status until it is finished (10-15 minutes).",
3143
+ description: "Make a micro-drama episode (30 seconds by default, up to 3 minutes with length_sec; hook, twist, cliffhanger; optionally a classic genre such as billionaire, werewolf or mafia), short film (a complete story) or movie trailer from an idea, vertical 9:16 or 16:9, in any of Film Studio's looks (live-action, anime, 3D animation, noir, horror, sci-fi, western, romance, comedy and more). Scenes of up to 30 seconds (one per place and cast), each filmed in one pass with several cuts, on Seedance 2.5, WAN 3 or Kling 4, the cast's faces kept from portraits and every line spoken and lip-synced, cut together with their own sound and one score over the finished edit, with free motion graphics (title card, name tags, end screen; each can be turned off), and saved to the Library. Not for music videos (pixeldojo_music_video) create is free and returns the plan and the exact price (about 190 credits for a regular 30 second film, about 380 for a minute; less on the wan-3-0-video engine and for mature work); produce films it; then call status until it is finished (10-15 minutes).",
3083
3144
  inputZod: InputSchema13,
3084
3145
  inputJsonSchema: InputJsonSchema13,
3085
3146
  allowedModels: ["seedance-2-5", "seedream-4"],
@@ -3094,7 +3155,9 @@ var init_drama = __esm({
3094
3155
  ...input.mature ? { mature: true } : {},
3095
3156
  ...input.look ? { look: input.look } : {},
3096
3157
  ...input.story_type ? { story_type: input.story_type } : {},
3158
+ ...input.length_sec ? { length_sec: input.length_sec } : {},
3097
3159
  ...input.scenes ? { scenes: input.scenes } : {},
3160
+ ...input.engine ? { engine: input.engine } : {},
3098
3161
  ...input.score === false ? { score: false } : {},
3099
3162
  ...Object.fromEntries(
3100
3163
  ["title_card", "name_tags", "end_screen", "end_text", "brand_color", "logo_url"].filter((k) => input[k] !== void 0).map((k) => [k, input[k]])
@@ -3123,7 +3186,7 @@ async function api(ctx, method, path, body) {
3123
3186
  Authorization: `Bearer ${ctx.apiKey}`,
3124
3187
  "Content-Type": "application/json",
3125
3188
  "x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
3126
- "x-pixeldojo-skill": "music_video"
3189
+ "x-pixeldojo-skill": "pixeldojo_music_video"
3127
3190
  },
3128
3191
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
3129
3192
  });
@@ -3252,7 +3315,7 @@ async function kataApi(ctx, method, path, body) {
3252
3315
  Authorization: `Bearer ${ctx.apiKey}`,
3253
3316
  "Content-Type": "application/json",
3254
3317
  "x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
3255
- "x-pixeldojo-skill": "kata"
3318
+ "x-pixeldojo-skill": "pixeldojo_kata"
3256
3319
  },
3257
3320
  ...body !== void 0 ? { body: JSON.stringify(body) } : {}
3258
3321
  });
@@ -3331,7 +3394,7 @@ var init_kata = __esm({
3331
3394
  },
3332
3395
  photos: {
3333
3396
  type: "array",
3334
- items: { type: "object", properties: { url: { type: "string" }, note: { type: "string", description: 'What it is (e.g. "the product").' } }, required: ["url"] },
3397
+ items: { type: "object", properties: { url: { type: "string", description: "Public URL of the photo." }, note: { type: "string", description: 'What it is (e.g. "the product").' } }, required: ["url"] },
3335
3398
  description: "Create: up to 4 products or photos to put in the edit, on the PixelDojo CDN."
3336
3399
  },
3337
3400
  instructions: { type: "string", description: "Create (optional): what to change, e.g. the setting, the words on screen, where the product goes." },
@@ -3416,7 +3479,7 @@ async function run13(input, ctx) {
3416
3479
  aspect_ratio: input.aspect_ratio,
3417
3480
  duration: input.duration,
3418
3481
  reference_video_url: input.reference_video_url,
3419
- brand_kit: input.brand_kit
3482
+ brand_kit: input.brand_kit === false ? false : input.brand_kit_id ?? input.brand_kit
3420
3483
  };
3421
3484
  const client = ctx.client;
3422
3485
  const submitted = await client.runAd(body);
@@ -3461,7 +3524,8 @@ var init_ad = __esm({
3461
3524
  aspect_ratio: z.enum(["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"]).optional(),
3462
3525
  duration: z.number().int().min(4).max(15).optional(),
3463
3526
  reference_video_url: z.string().url().optional(),
3464
- brand_kit: z.boolean().optional()
3527
+ brand_kit: z.boolean().optional(),
3528
+ brand_kit_id: z.string().max(80).optional()
3465
3529
  }).refine(
3466
3530
  (v) => v.product_url || (v.product_image_urls?.length || v.product_image_paths?.length) && v.product_name,
3467
3531
  {
@@ -3555,7 +3619,11 @@ var init_ad = __esm({
3555
3619
  },
3556
3620
  brand_kit: {
3557
3621
  type: "boolean",
3558
- description: "The account's brand kit (palette, voice, selling points; see pixeldojo_brand_kit) rides on every ad. false skips it."
3622
+ description: "The account's default brand kit (palette, voice, selling points; see pixeldojo_brand_kit) rides on every ad. false skips it."
3623
+ },
3624
+ brand_kit_id: {
3625
+ type: "string",
3626
+ description: "Use this saved brand kit instead of the default (ids listed by pixeldojo_brand_kit)."
3559
3627
  }
3560
3628
  }
3561
3629
  // "product_url OR product_name+images" is enforced by the server and the
@@ -3896,7 +3964,8 @@ async function run18(input, ctx) {
3896
3964
  }
3897
3965
  const lines2 = characters.map(
3898
3966
  (c) => `- ${c.mention} (${c.kind})${c.description ? `: ${c.description.slice(0, 100)}` : ""}
3899
- images: ${c.image_urls.slice(0, 4).join(" ")}`
3967
+ images: ${c.image_urls.slice(0, 4).join(" ")}${c.video_pair && c.video_image_urls?.length === 2 ? `
3968
+ for video (face, outfit; pass both to pixeldojo_drama cast as image_url + outfit_image_url): ${c.video_image_urls.join(" ")}` : ""}`
3900
3969
  );
3901
3970
  return {
3902
3971
  kind: "text",
@@ -4119,7 +4188,8 @@ async function effectRun(input, ctx) {
4119
4188
  });
4120
4189
  const data = await res.json().catch(() => ({}));
4121
4190
  if (!res.ok || !data.jobId) {
4122
- return { kind: "text", text: `Effect not started (nothing charged): ${data.error ?? `HTTP ${res.status}`}` };
4191
+ const err = typeof data.error === "string" ? data.error : data.error?.message;
4192
+ return { kind: "text", text: `Effect not started (nothing charged): ${err ?? `HTTP ${res.status}`}` };
4123
4193
  }
4124
4194
  const result = await pollJobUntilDoneOrHandoff(data.jobId, (id) => ctx.client.getJob(id), ctx.buildStatusUrl, {
4125
4195
  now: ctx.pollOverrides?.now,
@@ -4251,7 +4321,15 @@ ${describeKit(r2.data.brand_kit)}` };
4251
4321
  }
4252
4322
  const r = await call2(ctx, "pixeldojo_brand_kit", path);
4253
4323
  if (!r.ok) return { kind: "text", text: `Could not read the brand kit: ${r.error}` };
4254
- return { kind: "text", text: describeKit(r.data.brand_kit ?? null) };
4324
+ const all = r.data.brand_kits ?? [];
4325
+ const others = all.filter((k) => !k.isDefault);
4326
+ return {
4327
+ kind: "text",
4328
+ text: [
4329
+ describeKit(r.data.brand_kit ?? null),
4330
+ ...others.length ? [`Other brand kits (pass brand_kit_id on a run to use one): ${others.map((k) => `${k.name || "(no name)"} (id ${k.id})`).join("; ")}`] : []
4331
+ ].join("\n")
4332
+ };
4255
4333
  }
4256
4334
  async function templatesRun(input, ctx) {
4257
4335
  const q = new URLSearchParams();
@@ -4297,7 +4375,7 @@ async function imageRun(input, ctx) {
4297
4375
  engine: input.engine,
4298
4376
  count: input.count,
4299
4377
  reference_ad_url: input.reference_ad_url,
4300
- brand_kit: input.brand_kit
4378
+ brand_kit: input.brand_kit === false ? false : input.brand_kit_id ?? input.brand_kit
4301
4379
  }
4302
4380
  });
4303
4381
  if (!r.ok) return { kind: "text", text: `Images not started: ${r.error}` };
@@ -4367,7 +4445,7 @@ async function motionRun(input, ctx) {
4367
4445
  video_url: input.video_url,
4368
4446
  brand_name: input.brand_name,
4369
4447
  colors: input.colors?.map((c) => c.trim().toLowerCase()),
4370
- brand_kit: input.brand_kit
4448
+ brand_kit: input.brand_kit === false ? false : input.brand_kit_id ?? input.brand_kit
4371
4449
  }
4372
4450
  });
4373
4451
  if (!r.ok) return { kind: "text", text: `Not started: ${r.error}` };
@@ -4422,11 +4500,11 @@ var init_marketing = __esm({
4422
4500
  action: { type: "string", enum: ["get", "import", "update"], description: "Default get." },
4423
4501
  url: { type: "string", description: "import: the brand website, e.g. yourbrand.com." },
4424
4502
  save: { type: "boolean", description: "import: false returns the kit without saving it. Default true." },
4425
- name: { type: "string" },
4426
- tagline: { type: "string" },
4503
+ name: { type: "string", description: "Brand name." },
4504
+ tagline: { type: "string", description: "Short brand tagline." },
4427
4505
  voice: { type: "string", description: "How the brand talks, one or two sentences." },
4428
4506
  colors: { type: "array", items: { type: "string" }, maxItems: 5, description: "#RRGGBB, lead color first." },
4429
- fonts: { type: "array", items: { type: "string" }, maxItems: 2 },
4507
+ fonts: { type: "array", items: { type: "string" }, maxItems: 2, description: "Up to two brand font names." },
4430
4508
  selling_points: { type: "array", items: { type: "string" }, maxItems: 5, description: "Short product claims, 2-7 words each." },
4431
4509
  logo_url: { type: "string", format: "uri", description: "A logo on PixelDojo (upload it first); PNG with transparency is best." }
4432
4510
  }
@@ -4464,7 +4542,8 @@ var init_marketing = __esm({
4464
4542
  motion_id: z.string().max(80).optional(),
4465
4543
  brand_name: z.string().max(60).optional(),
4466
4544
  colors: z.array(z.string()).max(5).optional(),
4467
- brand_kit: z.boolean().optional()
4545
+ brand_kit: z.boolean().optional(),
4546
+ brand_kit_id: z.string().max(80).optional()
4468
4547
  });
4469
4548
  props = {
4470
4549
  action: {
@@ -4479,8 +4558,8 @@ var init_marketing = __esm({
4479
4558
  description: `image: an image template id from templates (e.g. stacked-shout, pure-white, ice-float). motion: ${MOTION.join(", ")}.`
4480
4559
  },
4481
4560
  product_url: { type: "string", format: "uri", description: "image: product page; name, description and photos are read from it." },
4482
- product_name: { type: "string" },
4483
- product_description: { type: "string" },
4561
+ product_name: { type: "string", description: "Product name when there is no product_url (with product_image_urls)." },
4562
+ product_description: { type: "string", description: "Short product description when there is no product_url." },
4484
4563
  product_image_urls: { type: "array", items: { type: "string", format: "uri" }, maxItems: 5, description: "Product photos. motion uses the first, cut out automatically." },
4485
4564
  product_image_paths: { type: "array", items: { type: "string" }, maxItems: 5, description: "Local product photos instead of URLs; uploaded first." },
4486
4565
  character_image_urls: { type: "array", items: { type: "string", format: "uri" }, maxItems: 2, description: "image: your own character (e.g. from pixeldojo_library source characters) for templates with a person." },
@@ -4500,7 +4579,8 @@ var init_marketing = __esm({
4500
4579
  motion_id: { type: "string", description: "status: the id a motion or end_card call returned." },
4501
4580
  brand_name: { type: "string", description: "motion: the brand shown when there is no logo. Default: the brand kit." },
4502
4581
  colors: { type: "array", items: { type: "string" }, maxItems: 5, description: "motion: #RRGGBB, lead color first. Default: the brand kit." },
4503
- brand_kit: { type: "boolean", description: "false skips the brand kit for this call." }
4582
+ brand_kit: { type: "boolean", description: "false skips the brand kit for this call." },
4583
+ brand_kit_id: { type: "string", description: "Use this saved brand kit instead of the default (ids listed by pixeldojo_brand_kit)." }
4504
4584
  };
4505
4585
  marketingSkill = {
4506
4586
  name: "pixeldojo_marketing",
@@ -5028,7 +5108,7 @@ var init_skills = __esm({
5028
5108
  }
5029
5109
  };
5030
5110
  });
5031
- render = (title) => ({ title, readOnlyHint: false, destructiveHint: false, openWorldHint: false });
5111
+ render = (title) => ({ title, readOnlyHint: false, destructiveHint: false, openWorldHint: true });
5032
5112
  read = (title) => ({ title, readOnlyHint: true, destructiveHint: false, openWorldHint: false });
5033
5113
  TOOL_ANNOTATIONS = {
5034
5114
  pixeldojo_generate: render("Generate image or video"),
@@ -5043,22 +5123,22 @@ var init_skills = __esm({
5043
5123
  pixeldojo_effect: render("Run one-click effect"),
5044
5124
  pixeldojo_video: render("Change, animate or extend video"),
5045
5125
  pixeldojo_ad: render("Make video ad"),
5046
- pixeldojo_brand_kit: render("Brand kit from website"),
5126
+ pixeldojo_brand_kit: { ...render("Brand kit from website"), destructiveHint: true },
5047
5127
  pixeldojo_marketing: render("Image ads, motion graphics and templates"),
5048
5128
  pixeldojo_campaign: render("Make product launch campaign"),
5049
- pixeldojo_film: render("Make short film"),
5129
+ pixeldojo_film: { ...render("Make short film"), destructiveHint: true },
5050
5130
  pixeldojo_episode: render("Make faceless video"),
5051
5131
  pixeldojo_drama: render("Make micro-drama, short film or trailer"),
5052
5132
  pixeldojo_music_video: render("Make music video for a song"),
5053
5133
  pixeldojo_kata: render("Recreate an edit with your cast"),
5054
5134
  pixeldojo_run_workflow: render("Run saved workflow"),
5055
5135
  pixeldojo_upload: render("Upload local file"),
5056
- pixeldojo_save_workflow: { ...render("Save workflow"), destructiveHint: true },
5136
+ pixeldojo_save_workflow: { ...render("Save workflow"), destructiveHint: true, openWorldHint: false },
5057
5137
  pixeldojo_status: read("Check job status"),
5058
5138
  pixeldojo_campaign_status: read("Check campaign status"),
5059
5139
  pixeldojo_library: read("Search media library"),
5060
- pixeldojo_save: render("Save to My Media"),
5061
- pixeldojo_save_character: render("Save character or element"),
5140
+ pixeldojo_save: { ...render("Save to My Media"), openWorldHint: false },
5141
+ pixeldojo_save_character: { ...render("Save character or element"), openWorldHint: false },
5062
5142
  pixeldojo_build_character: render("Build AI character from traits"),
5063
5143
  pixeldojo_perform: render("Make character dance or do a trend"),
5064
5144
  pixeldojo_list_workflows: read("List saved workflows"),
@@ -5117,7 +5197,7 @@ function makeRedactedLogger(target = console) {
5117
5197
  }
5118
5198
 
5119
5199
  // src/server.ts
5120
- var PACKAGE_VERSION = "0.30.0" ;
5200
+ var PACKAGE_VERSION = "0.34.1" ;
5121
5201
  var SERVER_INSTRUCTIONS = "Pixel Dojo generates images, video, audio and short films from one catalog of AI models, billed in credits from the connected Pixel Dojo account. A job that runs past the 30-second wait returns a jobId to check later; check it rather than submitting again, since every new submission is charged. Generated asset URLs are deleted one hour after they are created: pixeldojo_save keeps one in the user's My Media permanently (after a safety check), and finished films stay in the account library. In any prompt, @Name uses a character, product, outfit, location, prop or style the user saved in Character Studio: its photos are attached automatically. pixeldojo_library source=characters lists them, and pixeldojo_save_character saves a new one from 3 to 5 photos.";
5122
5202
  function buildServer(options) {
5123
5203
  if (!options.apiKey) {
@@ -5235,7 +5315,10 @@ function errorToActionableMessage(err) {
5235
5315
  return "Authentication failed. Check PIXELDOJO_API_KEY. Get a key at https://pixeldojo.ai/api-platform/api-keys.";
5236
5316
  }
5237
5317
  if (e.status === 402 || e.code === "insufficient_credits") {
5238
- return "Insufficient credits. Top up at https://pixeldojo.ai/api-platform/buy-credits.";
5318
+ return (
5319
+ // ponytail: no checkout link, ChatGPT's plugin review bans linking to a purchase page.
5320
+ "Not enough credits for this. Add credits in your Pixel Dojo account, then try again."
5321
+ );
5239
5322
  }
5240
5323
  if (e.status === 429 || e.code === "rate_limit") {
5241
5324
  return "Rate limit hit. Try again in a few seconds.";