@pixeldojo/mcp 0.6.0 → 0.9.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/dist/cli.cjs CHANGED
@@ -183,6 +183,109 @@ var init_upload_helpers = __esm({
183
183
  };
184
184
  }
185
185
  });
186
+
187
+ // src/skills/schema-snap.ts
188
+ function getFieldConstraints(apiId, client) {
189
+ const cached = schemaCache.get(apiId);
190
+ if (cached) return cached;
191
+ const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
192
+ (res) => res.schema?.properties ?? {}
193
+ ).catch(() => {
194
+ schemaCache.delete(apiId);
195
+ return null;
196
+ });
197
+ schemaCache.set(apiId, pending);
198
+ return pending;
199
+ }
200
+ function parseAspectRatio(ar) {
201
+ const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(ar);
202
+ if (!m) return null;
203
+ const w = Number(m[1]);
204
+ const h = Number(m[2]);
205
+ return h > 0 ? w / h : null;
206
+ }
207
+ function nearestAspectFromEnum(requested, allowed) {
208
+ if (allowed.includes(requested)) return requested;
209
+ const target = parseAspectRatio(requested);
210
+ if (target === null) return requested;
211
+ let best = requested;
212
+ let bestDiff = Infinity;
213
+ for (const candidate of allowed) {
214
+ const r = parseAspectRatio(candidate);
215
+ if (r === null) continue;
216
+ const diff = Math.abs(r - target);
217
+ if (diff < bestDiff) {
218
+ bestDiff = diff;
219
+ best = candidate;
220
+ }
221
+ }
222
+ return best;
223
+ }
224
+ async function snapAspectRatio(apiId, requested, client, opts = {}) {
225
+ if (!requested) return void 0;
226
+ const props = await getFieldConstraints(apiId, client);
227
+ if (props === null) {
228
+ return opts.onUnknown === "omit" ? void 0 : requested;
229
+ }
230
+ const field = props.aspect_ratio;
231
+ if (!field) return void 0;
232
+ if (!field.enum || field.enum.length === 0) return requested;
233
+ return nearestAspectFromEnum(requested, field.enum);
234
+ }
235
+ async function snapDuration(apiId, requested, client, opts = {}) {
236
+ if (requested === void 0) return void 0;
237
+ const props = await getFieldConstraints(apiId, client);
238
+ if (props === null) {
239
+ return opts.onUnknown === "omit" ? void 0 : requested;
240
+ }
241
+ const field = props.duration;
242
+ if (!field) return void 0;
243
+ if (field.enum && field.enum.length > 0) {
244
+ const allowed = field.enum.map(Number).filter((n) => Number.isFinite(n));
245
+ if (allowed.length === 0) return requested;
246
+ if (allowed.includes(requested)) return requested;
247
+ return allowed.reduce(
248
+ (best, candidate) => Math.abs(candidate - requested) < Math.abs(best - requested) ? candidate : best
249
+ );
250
+ }
251
+ if (typeof field.minimum === "number" || typeof field.maximum === "number") {
252
+ let v = requested;
253
+ if (typeof field.minimum === "number") v = Math.max(field.minimum, v);
254
+ if (typeof field.maximum === "number") v = Math.min(field.maximum, v);
255
+ return v;
256
+ }
257
+ return requested;
258
+ }
259
+ async function resolveImageRefs(apiId, urls, client) {
260
+ if (urls.length === 0) return null;
261
+ const props = await getFieldConstraints(apiId, client);
262
+ if (props === null) return null;
263
+ const key = REF_KEYS.find((k) => props[k] !== void 0);
264
+ if (!key) return null;
265
+ const field = props[key];
266
+ if (field.type === "array") {
267
+ const cap = typeof field.maxItems === "number" ? field.maxItems : urls.length;
268
+ const kept = urls.slice(0, Math.max(1, cap));
269
+ return { key, value: kept, dropped: urls.length - kept.length };
270
+ }
271
+ return { key, value: urls[0], dropped: urls.length - 1 };
272
+ }
273
+ var schemaCache, REF_KEYS;
274
+ var init_schema_snap = __esm({
275
+ "src/skills/schema-snap.ts"() {
276
+ schemaCache = /* @__PURE__ */ new Map();
277
+ REF_KEYS = [
278
+ "image_urls",
279
+ "images",
280
+ "image_input",
281
+ "reference_images",
282
+ "reference_urls",
283
+ "image_url",
284
+ "input_image",
285
+ "image"
286
+ ];
287
+ }
288
+ });
186
289
  function buildPayload(apiId, input) {
187
290
  const body = { prompt: input.prompt };
188
291
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
@@ -235,6 +338,13 @@ async function run(input, ctx) {
235
338
  };
236
339
  }
237
340
  }
341
+ if (!VIDEO_INPUT_MODELS.has(apiId)) {
342
+ const [aspect_ratio, duration] = await Promise.all([
343
+ snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
344
+ snapDuration(apiId, resolved.duration, ctx.client)
345
+ ]);
346
+ resolved = { ...resolved, aspect_ratio, duration };
347
+ }
238
348
  const body = buildPayload(apiId, resolved);
239
349
  const submitted = await ctx.client.run(apiId, body);
240
350
  const result = await pollJobUntilDoneOrHandoff(
@@ -251,12 +361,13 @@ async function run(input, ctx) {
251
361
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Generated. Asset: ${urls[0]}` : `Generated ${urls.length} assets. URLs: ${urls.join(", ")}`
252
362
  );
253
363
  }
254
- var InputSchema, DEFAULT_MODEL, InputJsonSchema, generateSkill;
364
+ var InputSchema, DEFAULT_MODEL, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
255
365
  var init_generate = __esm({
256
366
  "src/skills/generate.ts"() {
257
367
  init_poll();
258
368
  init_types();
259
369
  init_upload_helpers();
370
+ init_schema_snap();
260
371
  InputSchema = zod.z.object({
261
372
  prompt: zod.z.string().min(1).max(8e3).describe("Prompt describing what to generate."),
262
373
  model: zod.z.string().optional().describe(
@@ -273,6 +384,7 @@ var init_generate = __esm({
273
384
  )
274
385
  });
275
386
  DEFAULT_MODEL = "google-nano-banana";
387
+ VIDEO_INPUT_MODELS = /* @__PURE__ */ new Set(["grok-video-extend", "wan-video-edit", "seedance-video-edit"]);
276
388
  InputJsonSchema = {
277
389
  type: "object",
278
390
  properties: {
@@ -323,12 +435,21 @@ function buildPayload2(apiId, input) {
323
435
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
324
436
  };
325
437
  case "flux-edit":
326
- case "xai-image-edit":
438
+ return {
439
+ prompt: input.prompt,
440
+ input_image_1: urls[0],
441
+ ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
442
+ };
327
443
  case "gpt-image-2-edit":
444
+ return {
445
+ prompt: input.prompt,
446
+ image_urls: urls.slice(0, 8),
447
+ ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
448
+ };
449
+ case "xai-image-edit":
328
450
  return {
329
451
  prompt: input.prompt,
330
452
  image_url: urls[0],
331
- ...urls.length > 1 ? { image_urls: urls } : {},
332
453
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
333
454
  };
334
455
  case "google-nano-banana":
@@ -344,6 +465,19 @@ function buildPayload2(apiId, input) {
344
465
  };
345
466
  }
346
467
  }
468
+ async function buildBody(apiId, input, ctx) {
469
+ const urls = input.image_urls && input.image_urls.length > 0 ? input.image_urls : input.image_url ? [input.image_url] : [];
470
+ const refs = await resolveImageRefs(apiId, urls, ctx.client);
471
+ if (!refs) return { body: buildPayload2(apiId, input), dropped: 0 };
472
+ const fallback = buildPayload2(apiId, input);
473
+ const body = { ...fallback };
474
+ for (const key of ["image_url", "image_urls", "images", "image_input", "input_image_1"]) {
475
+ if (key !== refs.key) delete body[key];
476
+ }
477
+ body.prompt = input.prompt;
478
+ body[refs.key] = refs.value;
479
+ return { body, dropped: refs.dropped };
480
+ }
347
481
  async function run2(input, ctx) {
348
482
  let resolved = input;
349
483
  try {
@@ -370,7 +504,12 @@ async function run2(input, ctx) {
370
504
  };
371
505
  }
372
506
  const apiId = resolved.model ?? DEFAULT_MODEL2;
373
- const body = buildPayload2(apiId, resolved);
507
+ resolved = {
508
+ ...resolved,
509
+ aspect_ratio: await snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client)
510
+ };
511
+ const { body, dropped } = await buildBody(apiId, resolved, ctx);
512
+ const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
374
513
  const submitted = await ctx.client.run(apiId, body);
375
514
  const result = await pollJobUntilDoneOrHandoff(
376
515
  submitted.jobId,
@@ -383,7 +522,7 @@ async function run2(input, ctx) {
383
522
  );
384
523
  return pollResultToSkillResult(
385
524
  result,
386
- (urls) => urls.length === 0 ? `Edit job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Edit complete. Asset: ${urls[0]}` : `Edit produced ${urls.length} assets. URLs: ${urls.join(", ")}`
525
+ (urls) => urls.length === 0 ? `Edit job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Edit complete${droppedNote}. Asset: ${urls[0]}` : `Edit produced ${urls.length} assets${droppedNote}. URLs: ${urls.join(", ")}`
387
526
  );
388
527
  }
389
528
  var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
@@ -392,6 +531,7 @@ var init_edit = __esm({
392
531
  init_poll();
393
532
  init_types();
394
533
  init_upload_helpers();
534
+ init_schema_snap();
395
535
  DEFAULT_MODEL2 = "wan-2.7-image-edit";
396
536
  ALLOWED_MODELS = [
397
537
  "wan-2.7-image-edit",
@@ -487,9 +627,10 @@ async function run3(input, ctx) {
487
627
  body.image_urls = [input.reference_image_url];
488
628
  }
489
629
  }
490
- if (input.aspect_ratio && apiId === "flux-edit" && FLUX_EDIT_ASPECT_RATIOS.has(input.aspect_ratio)) {
491
- body.aspect_ratio = input.aspect_ratio;
492
- }
630
+ const snappedAspect = await snapAspectRatio(apiId, input.aspect_ratio, ctx.client, {
631
+ onUnknown: "omit"
632
+ });
633
+ if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
493
634
  const submitted = await ctx.client.run(apiId, body);
494
635
  const result = await pollJobUntilDoneOrHandoff(
495
636
  submitted.jobId,
@@ -505,28 +646,15 @@ async function run3(input, ctx) {
505
646
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
506
647
  );
507
648
  }
508
- var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE, FLUX_EDIT_ASPECT_RATIOS, InputSchema3, InputJsonSchema3, characterSkill;
649
+ var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE, InputSchema3, InputJsonSchema3, characterSkill;
509
650
  var init_character = __esm({
510
651
  "src/skills/character.ts"() {
511
652
  init_poll();
512
653
  init_types();
654
+ init_schema_snap();
513
655
  ALLOWED = ["seedream-5", "seedream-4", "flux-edit", "gpt-image-2-edit"];
514
656
  DEFAULT_MODEL3 = "seedream-5";
515
657
  REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["flux-edit", "gpt-image-2-edit"]);
516
- FLUX_EDIT_ASPECT_RATIOS = /* @__PURE__ */ new Set([
517
- "1:1",
518
- "16:9",
519
- "9:16",
520
- "21:9",
521
- "9:21",
522
- "3:2",
523
- "2:3",
524
- "4:5",
525
- "5:4",
526
- "3:4",
527
- "4:3",
528
- "match_input_image"
529
- ]);
530
658
  InputSchema3 = zod.z.object({
531
659
  prompt: zod.z.string().min(1).max(8e3),
532
660
  model: zod.z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
@@ -569,9 +697,13 @@ async function run4(input, ctx) {
569
697
  };
570
698
  }
571
699
  const apiId = requested;
700
+ const [snappedAspect, snappedDuration] = await Promise.all([
701
+ snapAspectRatio(apiId, input.aspect_ratio, ctx.client),
702
+ snapDuration(apiId, input.duration, ctx.client)
703
+ ]);
572
704
  const body = { prompt: input.brief };
573
- if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
574
- if (input.duration !== void 0) body.duration = input.duration;
705
+ if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
706
+ if (snappedDuration !== void 0) body.duration = snappedDuration;
575
707
  const submitted = await ctx.client.run(apiId, body);
576
708
  const result = await pollJobUntilDoneOrHandoff(
577
709
  submitted.jobId,
@@ -592,6 +724,7 @@ var init_storyboard = __esm({
592
724
  "src/skills/storyboard.ts"() {
593
725
  init_poll();
594
726
  init_types();
727
+ init_schema_snap();
595
728
  ALLOWED2 = [
596
729
  "veo-3.1-fast",
597
730
  "veo-3.1-standard",
@@ -849,7 +982,396 @@ var init_from_url = __esm({
849
982
  };
850
983
  }
851
984
  });
985
+ async function filmApi(ctx, method, path, body) {
986
+ const res = await fetch(`${ctx.baseUrl}${path}`, {
987
+ method,
988
+ headers: {
989
+ Authorization: `Bearer ${ctx.apiKey}`,
990
+ "Content-Type": "application/json",
991
+ "x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
992
+ "x-pixeldojo-skill": "film"
993
+ },
994
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
995
+ });
996
+ const data = await res.json().catch(() => ({}));
997
+ return { status: res.status, data };
998
+ }
999
+ function progressLine(f) {
1000
+ const all = shots(f);
1001
+ const frames = all.filter((sh) => ["frame-ready", "approved", "clip-generating", "clip-ready"].includes(sh.status) || sh.frameUrl).length;
1002
+ const clips = all.filter((sh) => sh.status === "clip-ready").length;
1003
+ const rendering = all.filter((sh) => sh.status === "clip-generating").length;
1004
+ return `frames ${frames}/${all.length}, clips ${clips}/${all.length}${rendering > 0 ? ` (${rendering} rendering)` : ""}`;
1005
+ }
1006
+ async function plan(input, ctx) {
1007
+ if (!input.idea?.trim() && !input.script?.trim()) {
1008
+ return { kind: "text", text: "Provide an `idea` (we write the script, free) or a full `script` to plan a film." };
1009
+ }
1010
+ const created = await filmApi(ctx, "POST", "/api/films", {
1011
+ aspect: input.aspect,
1012
+ look: input.look,
1013
+ lengthTarget: input.length,
1014
+ assetIds: []
1015
+ });
1016
+ if (!created.data.film) return { kind: "text", text: `Could not create the film: ${created.data.error ?? created.status}` };
1017
+ const id = created.data.film.id;
1018
+ let script = input.script?.trim() ?? "";
1019
+ if (!script) {
1020
+ const wrote = await filmApi(ctx, "POST", `/api/films/${id}/script`, { idea: input.idea.trim() });
1021
+ script = wrote.data.film?.script ?? "";
1022
+ if (!script) return { kind: "text", text: `Script writing failed: ${wrote.data.error ?? wrote.status}` };
1023
+ }
1024
+ const planned = await filmApi(ctx, "POST", `/api/films/${id}/plan`, { script });
1025
+ const film = planned.data.film;
1026
+ if (!film) return { kind: "text", text: `Shot planning failed: ${planned.data.error ?? planned.status}` };
1027
+ const total = shots(film).length;
1028
+ const sceneLines = film.scenes.map((s, i) => ` Scene ${i + 1} "${s.title}"${s.location ? ` [${s.location}]` : ""}: ${s.shots.length} shots`);
1029
+ return {
1030
+ kind: "text",
1031
+ text: [
1032
+ `Planned "${film.title}" (film_id: ${film.id}) \u2014 ${film.scenes.length} scenes, ${total} shots, ${input.look}, ${input.aspect}.`,
1033
+ ...sceneLines,
1034
+ "",
1035
+ `Nothing has been charged yet. Producing renders every shot (roughly 20-25 credits per shot at default engines, charged as each renders) plus a few credits for the soundtrack.`,
1036
+ `Review or edit at ${ctx.baseUrl}/film-studio/${film.id} \u2014 or call pixeldojo:film with {"film_id": "${film.id}", "action": "produce"} to start production.`
1037
+ ].join("\n")
1038
+ };
1039
+ }
1040
+ async function produce(filmId, ctx) {
1041
+ const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
1042
+ if (!data.film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
1043
+ const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
1044
+ let fired = 0;
1045
+ for (const sh of pending.slice(0, 6)) {
1046
+ const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
1047
+ if (r.status === 200) fired += 1;
1048
+ }
1049
+ return {
1050
+ kind: "text",
1051
+ text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one). Poll pixeldojo:film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`
1052
+ };
1053
+ }
1054
+ function buildScorePlanBody(film) {
1055
+ const scenes = film.scenes.filter((s) => s.shots.length > 0);
1056
+ const sections = scenes.flatMap((scene, i) => {
1057
+ const runtime = Math.max(3e3, scene.shots.reduce((n, sh) => n + (sh.durationSec || 0), 0) * 1e3);
1058
+ const styles = i === 0 ? ARC.opening : i === scenes.length - 1 ? ARC.finale : ARC.middle;
1059
+ const parts = Math.max(1, Math.ceil(runtime / 12e4));
1060
+ const partMs = Math.round(runtime / parts);
1061
+ return Array.from({ length: parts }, (_, part) => ({
1062
+ section_name: parts > 1 ? `${scene.title} (part ${part + 1})` : scene.title,
1063
+ positive_local_styles: styles,
1064
+ negative_local_styles: [],
1065
+ duration_ms: part === parts - 1 ? runtime - partMs * (parts - 1) : partMs,
1066
+ lines: []
1067
+ }));
1068
+ });
1069
+ return {
1070
+ composition_plan: {
1071
+ positive_global_styles: (LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]).split(", "),
1072
+ negative_global_styles: ["vocals", "singing", "lyrics"],
1073
+ sections
1074
+ },
1075
+ respect_sections_durations: true,
1076
+ output_format: "mp3_standard"
1077
+ };
1078
+ }
1079
+ function buildFlatMusicBody(film, all) {
1080
+ const runtimeSec = all.reduce((n, sh) => n + (sh.durationSec || 0), 0);
1081
+ const ms = Math.min(6e5, Math.max(5e3, Math.ceil(runtimeSec * 1e3 / 5e3) * 5e3));
1082
+ return {
1083
+ prompt: `${LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]}. Instrumental only, no vocals. One consistent theme, loopable as a background bed under dialogue.`,
1084
+ music_length_ms: ms,
1085
+ force_instrumental: true,
1086
+ output_format: "mp3_standard"
1087
+ };
1088
+ }
1089
+ async function advance(filmId, soundtrack, musicJobId, ctx) {
1090
+ const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}?refresh=1`);
1091
+ const film = data.film;
1092
+ if (!film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
1093
+ const all = shots(film);
1094
+ if (film.status === "exported" && film.exportUrl) {
1095
+ return {
1096
+ kind: "text",
1097
+ text: `Film finished: "${film.title}"
1098
+ Watch/download: ${film.exportUrl}
1099
+ Workspace: ${ctx.baseUrl}/film-studio/${film.id}
1100
+ It is saved in the Library folder "Film: ${film.title}".`
1101
+ };
1102
+ }
1103
+ if (film.exportError) {
1104
+ return { kind: "text", text: `Export failed: ${film.exportError} Poll again with action "status" to retry.` };
1105
+ }
1106
+ const idleFrames = all.filter((sh) => sh.status === "idle");
1107
+ for (const sh of idleFrames.slice(0, 6)) {
1108
+ await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
1109
+ }
1110
+ const framesDone = all.every((sh) => ["frame-ready", "clip-generating", "clip-ready"].includes(sh.status));
1111
+ if (framesDone && all.some((sh) => sh.status === "frame-ready" && !sh.approved)) {
1112
+ await filmApi(ctx, "PATCH", `/api/films/${filmId}`, {
1113
+ scenes: film.scenes.map((s) => ({
1114
+ id: s.id,
1115
+ title: s.title,
1116
+ location: s.location,
1117
+ shots: s.shots.map((sh) => ({
1118
+ id: sh.id,
1119
+ prompt: sh.prompt,
1120
+ characterIds: sh.characterIds ?? [],
1121
+ elementIds: sh.elementIds ?? [],
1122
+ engine: sh.engine,
1123
+ cameraPreset: sh.cameraPreset,
1124
+ approved: true,
1125
+ durationSec: sh.durationSec
1126
+ }))
1127
+ }))
1128
+ });
1129
+ }
1130
+ const inFlight = count(film, (sh) => sh.status === "clip-generating");
1131
+ const clipQueue = all.filter((sh) => (sh.status === "frame-ready" || sh.status === "failed" && Boolean(sh.frameUrl)) && framesDone);
1132
+ for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) {
1133
+ await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "clip" });
1134
+ }
1135
+ const clipsDone = all.length > 0 && all.every((sh) => sh.status === "clip-ready");
1136
+ if (clipsDone) {
1137
+ const hasMusic = film.audioStems.some((st) => st.kind === "music");
1138
+ if (soundtrack !== "none" && !hasMusic) {
1139
+ if (musicJobId) {
1140
+ const job = await ctx.client.getJob(musicJobId);
1141
+ const out = job.output;
1142
+ const outUrl = out?.audio ?? out?.video ?? out?.images?.[0];
1143
+ if (job.status === "completed" && typeof outUrl === "string") {
1144
+ await filmApi(ctx, "PATCH", `/api/films/${filmId}`, { audioStems: [{ kind: "music", url: outUrl }] });
1145
+ await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
1146
+ return { kind: "text", text: `Score attached \u2014 export started. Poll again in ~60s with {"film_id": "${filmId}", "action": "status"} for the finished film URL.` };
1147
+ }
1148
+ if (job.status === "failed") {
1149
+ return { kind: "text", text: `The score failed (${job.error ?? "unknown"}) and was refunded. Poll again to retry, or pass soundtrack "none" to export without music.` };
1150
+ }
1151
+ return { kind: "text", text: `Score still composing. Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${musicJobId}"}.` };
1152
+ }
1153
+ const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, all);
1154
+ const dispatch = await ctx.client.run("text-to-music", body);
1155
+ return { kind: "text", text: `All ${all.length} clips ready \u2014 the ${soundtrack} score is composing (charged like any music generation). Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${dispatch.jobId}"}.` };
1156
+ }
1157
+ await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
1158
+ return { kind: "text", text: `All clips ready \u2014 export started (${progressLine(film)}). Poll again in ~60s with action "status" for the finished film URL.` };
1159
+ }
1160
+ const failed = all.filter((sh) => sh.status === "failed");
1161
+ return {
1162
+ kind: "text",
1163
+ text: [
1164
+ `In progress: ${progressLine(film)}.`,
1165
+ failed.length > 0 ? `${failed.length} shot(s) failed and were re-queued (${failed[0]?.errorMessage?.slice(0, 100) ?? ""}).` : "",
1166
+ `Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.`
1167
+ ].filter(Boolean).join("\n")
1168
+ };
1169
+ }
852
1170
  async function run8(input, ctx) {
1171
+ const action = input.action === "plan" && input.film_id ? "status" : input.action;
1172
+ if (action === "plan") return plan(input, ctx);
1173
+ if (!input.film_id) return { kind: "text", text: "produce/status need the film_id returned by the plan step." };
1174
+ if (action === "produce") return produce(input.film_id, ctx);
1175
+ return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
1176
+ }
1177
+ var LOOKS, InputSchema8, InputJsonSchema8, shots, count, LOOK_MUSIC, ARC, filmSkill;
1178
+ var init_film = __esm({
1179
+ "src/skills/film.ts"() {
1180
+ LOOKS = ["live-action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western"];
1181
+ InputSchema8 = zod.z.object({
1182
+ idea: zod.z.string().max(2e3).optional().describe("One-to-three sentence film idea. The studio writes the screenplay (free). Provide idea OR script for planning."),
1183
+ script: zod.z.string().max(2e4).optional().describe("A full screenplay to plan instead of an idea (sluglines, action lines, dialogue)."),
1184
+ look: zod.z.enum(LOOKS).default("live-action").describe("Visual style carried through frames, clips, and soundtrack."),
1185
+ aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
1186
+ length: zod.z.enum(["30s", "60s", "2-3min"]).default("60s").describe("Target length; drives the shot count (~6/12/24 shots)."),
1187
+ film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status."),
1188
+ action: zod.z.enum(["plan", "produce", "status"]).default("plan").describe("'plan' creates + storyboards (free). 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film."),
1189
+ soundtrack: zod.z.enum(["scene-synced", "flat", "none"]).default("scene-synced").describe("scene-synced composes one musical section per scene, pinned to the scene lengths (default). flat is a single cohesive bed. none skips music."),
1190
+ music_job_id: zod.z.string().max(80).optional().describe("Internal: pass back the music job id from a previous status reply so the composing score is attached instead of re-charged.")
1191
+ });
1192
+ InputJsonSchema8 = {
1193
+ type: "object",
1194
+ properties: {
1195
+ idea: { type: "string", description: "One-to-three sentence film idea. The studio writes the screenplay (free)." },
1196
+ script: { type: "string", description: "A full screenplay to plan instead of an idea." },
1197
+ look: { type: "string", enum: [...LOOKS], description: "Visual style. Default live-action." },
1198
+ aspect: { type: "string", enum: ["16:9", "9:16"], description: "Default 16:9." },
1199
+ length: { type: "string", enum: ["30s", "60s", "2-3min"], description: "Target length (~6/12/24 shots). Default 60s." },
1200
+ film_id: { type: "string", description: "Existing film id for produce/status." },
1201
+ action: { type: "string", enum: ["plan", "produce", "status"], description: "plan (free) | produce (charged) | status (poll every ~30s)." },
1202
+ soundtrack: { type: "string", enum: ["scene-synced", "flat", "none"], description: "Default scene-synced." },
1203
+ music_job_id: { type: "string", description: "Pass back from a previous status reply while the score is composing." }
1204
+ }
1205
+ };
1206
+ shots = (f) => f.scenes.flatMap((s) => s.shots);
1207
+ count = (f, pred) => shots(f).filter(pred).length;
1208
+ LOOK_MUSIC = {
1209
+ "live-action": "modern cinematic underscore, warm strings and soft felt piano, subtle rhythmic pulse, slow emotional build",
1210
+ anime: "sweeping anime film score, lush strings and piano, taiko drum accents, soaring heartfelt theme",
1211
+ "3d-animation": "playful heartfelt family-animation score, pizzicato strings, celesta and warm woodwinds, gentle sense of wonder",
1212
+ noir: "smoky film-noir jazz underscore, brushed drums, upright bass, distant muted trumpet, slow simmering tension",
1213
+ horror: "sparse horror underscore, low cello drones, dissonant string swells, creeping heartbeat pulse, unsettling quiet",
1214
+ "sci-fi": "atmospheric science-fiction score, analog synth pads, deep sub-bass pulses, shimmering arpeggios, vast and cold",
1215
+ documentary: "understated documentary score, felt piano, soft string beds, organic percussion textures, reflective and true",
1216
+ western: "cinematic western score, lonely twangy guitar, dusty strings, distant horns, wide-open desert tension"
1217
+ };
1218
+ ARC = {
1219
+ opening: ["establishing the theme", "restrained", "quiet anticipation"],
1220
+ middle: ["building tension", "rising intensity", "forward motion"],
1221
+ finale: ["climactic", "full arrangement", "emotional peak resolving to stillness"]
1222
+ };
1223
+ filmSkill = {
1224
+ name: "pixeldojo:film",
1225
+ description: "Make a finished short film from one idea: screenplay, storyboard frames (AI-reviewed), video clips with spoken dialogue, scene-synced soundtrack, stitched export. Phased: plan is free and returns a cost estimate; produce starts charged generation; status advances production (poll every ~30s).",
1226
+ inputZod: InputSchema8,
1227
+ inputJsonSchema: InputJsonSchema8,
1228
+ allowedModels: [],
1229
+ defaultModel: "",
1230
+ run: run8
1231
+ };
1232
+ }
1233
+ });
1234
+ async function run9(input, ctx) {
1235
+ const productImages = [
1236
+ ...input.product_image_urls ?? [],
1237
+ ...await Promise.all((input.product_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
1238
+ ];
1239
+ const characterImages = [
1240
+ ...input.character_image_urls ?? [],
1241
+ ...await Promise.all((input.character_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
1242
+ ];
1243
+ const body = {
1244
+ style: input.style,
1245
+ product_url: input.product_url,
1246
+ product: !input.product_url && input.product_name ? {
1247
+ name: input.product_name,
1248
+ description: input.product_description,
1249
+ images: productImages
1250
+ } : void 0,
1251
+ character_images: characterImages.length > 0 ? characterImages : void 0,
1252
+ location: input.location_description || input.location_image_url ? { description: input.location_description, image_url: input.location_image_url } : void 0,
1253
+ hook: input.hook,
1254
+ script: input.script,
1255
+ quality: input.quality,
1256
+ resolution: input.resolution,
1257
+ aspect_ratio: input.aspect_ratio,
1258
+ duration: input.duration
1259
+ };
1260
+ const client = ctx.client;
1261
+ const submitted = await client.runAd(body);
1262
+ const result = await pollJobUntilDoneOrHandoff(
1263
+ submitted.jobId,
1264
+ (id) => ctx.client.getJob(id),
1265
+ ctx.buildStatusUrl,
1266
+ {
1267
+ now: ctx.pollOverrides?.now,
1268
+ sleep: ctx.pollOverrides?.sleep
1269
+ }
1270
+ );
1271
+ const styleLabel = submitted.ad?.style ?? input.style ?? "ugc-testimonial";
1272
+ return pollResultToSkillResult(
1273
+ result,
1274
+ (urls) => urls.length === 0 ? `Ad job ${submitted.jobId} completed but returned no assets.` : `Ad rendered (style: ${styleLabel}${submitted.ad?.product?.name ? `, product: ${submitted.ad.product.name}` : ""}). Video: ${urls[0]}`
1275
+ );
1276
+ }
1277
+ var InputSchema9, InputJsonSchema9, adSkill;
1278
+ var init_ad = __esm({
1279
+ "src/skills/ad.ts"() {
1280
+ init_types();
1281
+ init_poll();
1282
+ init_upload_helpers();
1283
+ InputSchema9 = zod.z.object({
1284
+ style: zod.z.string().max(80).optional().describe(
1285
+ "Ad style slug (default ugc-testimonial). Catalog: GET /api/v1/marketing/presets \u2014 e.g. ugc-testimonial, unboxing-reveal, unboxing-asmr, virtual-try-on, street-style, product-showcase, macro-details, crush-test, hyper-motion, before-after, day-in-the-life, the-gift, tv-spot."
1286
+ ),
1287
+ product_url: zod.z.string().url().optional().describe("Product page URL. Name, description, and images are extracted automatically."),
1288
+ product_name: zod.z.string().max(200).optional(),
1289
+ product_description: zod.z.string().max(2e3).optional(),
1290
+ product_image_urls: zod.z.array(zod.z.string().url()).max(5).optional(),
1291
+ product_image_paths: zod.z.array(zod.z.string()).max(5).optional().describe("Absolute local paths; uploaded to 24h temp storage automatically."),
1292
+ character_image_urls: zod.z.array(zod.z.string().url()).max(4).optional().describe("Presenter reference images for a consistent on-camera character."),
1293
+ character_image_paths: zod.z.array(zod.z.string()).max(4).optional(),
1294
+ location_description: zod.z.string().max(1500).optional().describe('Where the ad takes place ("a sunlit Parisian cafe"). Text is enough; no image needed.'),
1295
+ location_image_url: zod.z.string().url().optional(),
1296
+ hook: zod.z.string().max(1e3).optional().describe("The opening visual beat (first 1-2 seconds). Omit to use the style default."),
1297
+ script: zod.z.string().max(1e3).optional().describe("The exact spoken line, voiced word for word. Omit and the presenter improvises."),
1298
+ quality: zod.z.enum(["mini", "fast", "high"]).optional(),
1299
+ resolution: zod.z.enum(["480p", "720p", "1080p", "4k"]).optional().describe('1080p/4k require quality "high".'),
1300
+ aspect_ratio: zod.z.enum(["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"]).optional(),
1301
+ duration: zod.z.number().int().min(4).max(15).optional()
1302
+ }).refine(
1303
+ (v) => v.product_url || (v.product_image_urls?.length || v.product_image_paths?.length) && v.product_name,
1304
+ {
1305
+ message: "Pass product_url, OR product_name plus product_image_urls/product_image_paths.",
1306
+ path: ["product_url"]
1307
+ }
1308
+ );
1309
+ InputJsonSchema9 = {
1310
+ type: "object",
1311
+ properties: {
1312
+ style: {
1313
+ type: "string",
1314
+ description: "Ad style slug (default ugc-testimonial). Catalog: GET /api/v1/marketing/presets \u2014 e.g. ugc-testimonial, unboxing-reveal, unboxing-asmr, virtual-try-on, street-style, mirror-fit-check, pro-try-on, product-showcase, macro-details, secret-hack-reveal, crush-test, hyper-motion, before-after, day-in-the-life, the-gift, tv-spot, gadget-saved-me, selfie-testimonial, direct-to-camera, pack-an-order, mystery-box."
1315
+ },
1316
+ product_url: {
1317
+ type: "string",
1318
+ format: "uri",
1319
+ description: "Product page URL. Name, description, and images are extracted automatically."
1320
+ },
1321
+ product_name: { type: "string" },
1322
+ product_description: { type: "string" },
1323
+ product_image_urls: { type: "array", items: { type: "string", format: "uri" }, maxItems: 5 },
1324
+ product_image_paths: {
1325
+ type: "array",
1326
+ items: { type: "string" },
1327
+ maxItems: 5,
1328
+ description: "Absolute local file paths; uploaded to 24h temp storage automatically."
1329
+ },
1330
+ character_image_urls: {
1331
+ type: "array",
1332
+ items: { type: "string", format: "uri" },
1333
+ maxItems: 4,
1334
+ description: "Presenter reference images for a consistent on-camera character."
1335
+ },
1336
+ character_image_paths: { type: "array", items: { type: "string" }, maxItems: 4 },
1337
+ location_description: {
1338
+ type: "string",
1339
+ description: 'Where the ad takes place ("a sunlit Parisian cafe"). Text is enough.'
1340
+ },
1341
+ location_image_url: { type: "string", format: "uri" },
1342
+ hook: {
1343
+ type: "string",
1344
+ description: "The opening visual beat (first 1-2 seconds). Omit to use the style default."
1345
+ },
1346
+ script: {
1347
+ type: "string",
1348
+ description: "The exact spoken line, voiced word for word. Omit and the presenter improvises."
1349
+ },
1350
+ quality: { type: "string", enum: ["mini", "fast", "high"] },
1351
+ resolution: {
1352
+ type: "string",
1353
+ enum: ["480p", "720p", "1080p", "4k"],
1354
+ description: '1080p/4k require quality "high".'
1355
+ },
1356
+ aspect_ratio: { type: "string", enum: ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"] },
1357
+ duration: { type: "integer", minimum: 4, maximum: 15 }
1358
+ }
1359
+ // "product_url OR product_name+images" is enforced by the server and the
1360
+ // Zod refine; no root-level oneOf (Grok rejects non-object root unions).
1361
+ };
1362
+ adSkill = {
1363
+ name: "pixeldojo:ad",
1364
+ description: "Marketing Studio: turn any product into a video ad. Pass a product URL (auto-extracted) or product images, pick an ad style (UGC testimonial, unboxing, try-on, showcase, TV spot, and more via GET /api/v1/marketing/presets), optionally add presenter reference images, a location, a hook, and the exact spoken script. Renders with voiceover; returns the video or a statusUrl handoff to poll via pixeldojo:status. Ideal for automated generate-and-post workflows.",
1365
+ inputZod: InputSchema9,
1366
+ inputJsonSchema: InputJsonSchema9,
1367
+ // Composition skill — the server routes to the ad engine internally.
1368
+ allowedModels: [],
1369
+ defaultModel: "",
1370
+ run: run9
1371
+ };
1372
+ }
1373
+ });
1374
+ async function run10(input, ctx) {
853
1375
  const client = ctx.client;
854
1376
  const result = await client.runCampaign(input);
855
1377
  if (result.kind === "campaign") {
@@ -876,10 +1398,10 @@ async function run8(input, ctx) {
876
1398
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
877
1399
  };
878
1400
  }
879
- var InputSchema8, InputJsonSchema8, campaignSkill;
1401
+ var InputSchema10, InputJsonSchema10, campaignSkill;
880
1402
  var init_campaign = __esm({
881
1403
  "src/skills/campaign.ts"() {
882
- InputSchema8 = zod.z.object({
1404
+ InputSchema10 = zod.z.object({
883
1405
  productUrl: zod.z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
884
1406
  product: zod.z.object({
885
1407
  name: zod.z.string().min(1).max(200),
@@ -895,7 +1417,7 @@ var init_campaign = __esm({
895
1417
  message: "productUrl or product is required",
896
1418
  path: ["productUrl"]
897
1419
  });
898
- InputJsonSchema8 = {
1420
+ InputJsonSchema10 = {
899
1421
  type: "object",
900
1422
  properties: {
901
1423
  productUrl: {
@@ -926,17 +1448,17 @@ var init_campaign = __esm({
926
1448
  campaignSkill = {
927
1449
  name: "pixeldojo:campaign",
928
1450
  description: "Generate a coherent product launch package (hero image + N lifestyle variants + optional vertical video) from one URL or product profile. Composes hero / lifestyle / video models server-side; returns assets in one call or a handoff with campaignId to poll via pixeldojo:campaign_status.",
929
- inputZod: InputSchema8,
930
- inputJsonSchema: InputJsonSchema8,
1451
+ inputZod: InputSchema10,
1452
+ inputJsonSchema: InputJsonSchema10,
931
1453
  // Composition skill — internal sub-jobs route to per-tool models, no
932
1454
  // single allowed-models list applies here.
933
1455
  allowedModels: [],
934
1456
  defaultModel: "",
935
- run: run8
1457
+ run: run10
936
1458
  };
937
1459
  }
938
1460
  });
939
- async function run9(input, ctx) {
1461
+ async function run11(input, ctx) {
940
1462
  const client = ctx.client;
941
1463
  const result = await client.getCampaign(input.campaignId);
942
1464
  if (result.kind === "campaign") {
@@ -966,13 +1488,13 @@ async function run9(input, ctx) {
966
1488
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
967
1489
  };
968
1490
  }
969
- var InputSchema9, InputJsonSchema9, campaignStatusSkill;
1491
+ var InputSchema11, InputJsonSchema11, campaignStatusSkill;
970
1492
  var init_campaign_status = __esm({
971
1493
  "src/skills/campaign-status.ts"() {
972
- InputSchema9 = zod.z.object({
1494
+ InputSchema11 = zod.z.object({
973
1495
  campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
974
1496
  });
975
- InputJsonSchema9 = {
1497
+ InputJsonSchema11 = {
976
1498
  type: "object",
977
1499
  properties: {
978
1500
  campaignId: {
@@ -985,15 +1507,15 @@ var init_campaign_status = __esm({
985
1507
  campaignStatusSkill = {
986
1508
  name: "pixeldojo:campaign_status",
987
1509
  description: "Poll a campaign by campaignId returned from pixeldojo:campaign. Returns assets when every sub-job is done, or a handoff describing what is still in flight.",
988
- inputZod: InputSchema9,
989
- inputJsonSchema: InputJsonSchema9,
1510
+ inputZod: InputSchema11,
1511
+ inputJsonSchema: InputJsonSchema11,
990
1512
  allowedModels: [],
991
1513
  defaultModel: "",
992
- run: run9
1514
+ run: run11
993
1515
  };
994
1516
  }
995
1517
  });
996
- async function run10(input, ctx) {
1518
+ async function run12(input, ctx) {
997
1519
  let url;
998
1520
  try {
999
1521
  url = await uploadLocalFile(input.path, ctx.client);
@@ -1009,16 +1531,16 @@ async function run10(input, ctx) {
1009
1531
  (temp storage; auto-deletes in ~24h \u2014 pass this URL as image_url to :generate or :edit)`
1010
1532
  };
1011
1533
  }
1012
- var InputSchema10, InputJsonSchema10, uploadSkill;
1534
+ var InputSchema12, InputJsonSchema12, uploadSkill;
1013
1535
  var init_upload = __esm({
1014
1536
  "src/skills/upload.ts"() {
1015
1537
  init_upload_helpers();
1016
- InputSchema10 = zod.z.object({
1538
+ InputSchema12 = zod.z.object({
1017
1539
  path: zod.z.string().min(1).max(4096).describe(
1018
1540
  "Absolute local file path to upload. JPEG, PNG, WebP, GIF (\u226450MB) or MP4, WebM, MOV (\u2264200MB). Returns a public URL on the temp CDN; storage auto-expires after 24 hours."
1019
1541
  )
1020
1542
  });
1021
- InputJsonSchema10 = {
1543
+ InputJsonSchema12 = {
1022
1544
  type: "object",
1023
1545
  properties: {
1024
1546
  path: {
@@ -1031,15 +1553,15 @@ var init_upload = __esm({
1031
1553
  uploadSkill = {
1032
1554
  name: "pixeldojo:upload",
1033
1555
  description: "Upload a local image or video file and get back a public URL you can pass to :generate or :edit as image_url. Use when the user has a reference image on disk (drag-dropped into Claude Code, screenshot, etc.) and you need a URL. Storage auto-expires after 24 hours.",
1034
- inputZod: InputSchema10,
1035
- inputJsonSchema: InputJsonSchema10,
1556
+ inputZod: InputSchema12,
1557
+ inputJsonSchema: InputJsonSchema12,
1036
1558
  allowedModels: [],
1037
1559
  defaultModel: "",
1038
- run: run10
1560
+ run: run12
1039
1561
  };
1040
1562
  }
1041
1563
  });
1042
- async function run11(input, ctx) {
1564
+ async function run13(input, ctx) {
1043
1565
  const requested = input.model ?? DEFAULT_MODEL6;
1044
1566
  if (!ALLOWED4.includes(requested)) {
1045
1567
  return {
@@ -1088,7 +1610,7 @@ async function run11(input, ctx) {
1088
1610
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
1089
1611
  );
1090
1612
  }
1091
- var ALLOWED4, DEFAULT_MODEL6, InputSchema11, InputJsonSchema11, audioSkill;
1613
+ var ALLOWED4, DEFAULT_MODEL6, InputSchema13, InputJsonSchema13, audioSkill;
1092
1614
  var init_audio = __esm({
1093
1615
  "src/skills/audio.ts"() {
1094
1616
  init_poll();
@@ -1096,7 +1618,7 @@ var init_audio = __esm({
1096
1618
  init_types();
1097
1619
  ALLOWED4 = ["seed-audio", "text-to-speech"];
1098
1620
  DEFAULT_MODEL6 = "seed-audio";
1099
- InputSchema11 = zod.z.object({
1621
+ InputSchema13 = zod.z.object({
1100
1622
  prompt: zod.z.string().min(1).describe(
1101
1623
  "A script for speech, or a scene description for cinematic audio (dialogue + effects + music). Reference uploaded clips as @Audio1..@Audio3."
1102
1624
  ),
@@ -1107,7 +1629,7 @@ var init_audio = __esm({
1107
1629
  output_format: zod.z.string().optional().describe("seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3, wav, flac, pcm."),
1108
1630
  speed: zod.z.number().min(0.5).max(2).optional().describe("Speech speed multiplier (0.5\u20132).")
1109
1631
  });
1110
- InputJsonSchema11 = {
1632
+ InputJsonSchema13 = {
1111
1633
  type: "object",
1112
1634
  properties: {
1113
1635
  prompt: { type: "string" },
@@ -1123,15 +1645,15 @@ var init_audio = __esm({
1123
1645
  audioSkill = {
1124
1646
  name: "pixeldojo:audio",
1125
1647
  description: "Generate audio: speech, multi-character dialogue, sound effects, and background music from one prompt (Seed Audio 1.0 by default; plain TTS available).",
1126
- inputZod: InputSchema11,
1127
- inputJsonSchema: InputJsonSchema11,
1648
+ inputZod: InputSchema13,
1649
+ inputJsonSchema: InputJsonSchema13,
1128
1650
  allowedModels: [...ALLOWED4],
1129
1651
  defaultModel: DEFAULT_MODEL6,
1130
- run: run11
1652
+ run: run13
1131
1653
  };
1132
1654
  }
1133
1655
  });
1134
- async function run12(input, ctx) {
1656
+ async function run14(input, ctx) {
1135
1657
  const res = await ctx.client.listLibrary({
1136
1658
  q: input.q,
1137
1659
  model: input.model,
@@ -1162,16 +1684,16 @@ ${lines.join("\n")}
1162
1684
  Pass any URL back into :generate, :edit, :character, or :upscale as a reference input.`
1163
1685
  };
1164
1686
  }
1165
- var InputSchema12, InputJsonSchema12, librarySkill;
1687
+ var InputSchema14, InputJsonSchema14, librarySkill;
1166
1688
  var init_library = __esm({
1167
1689
  "src/skills/library.ts"() {
1168
- InputSchema12 = zod.z.object({
1690
+ InputSchema14 = zod.z.object({
1169
1691
  q: zod.z.string().optional().describe("Substring match against the prompt that produced the asset."),
1170
1692
  model: zod.z.string().optional().describe("Restrict to a single model (apiId)."),
1171
1693
  modality: zod.z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
1172
1694
  limit: zod.z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
1173
1695
  });
1174
- InputJsonSchema12 = {
1696
+ InputJsonSchema14 = {
1175
1697
  type: "object",
1176
1698
  properties: {
1177
1699
  q: { type: "string" },
@@ -1184,15 +1706,15 @@ var init_library = __esm({
1184
1706
  librarySkill = {
1185
1707
  name: "pixeldojo:library",
1186
1708
  description: "Search your media library: saved My Media items (permanent) and recent API/MCP generations (expire after 24h), newest first. Returns asset URLs to reuse as reference inputs. Keep a result forever with pixeldojo:save.",
1187
- inputZod: InputSchema12,
1188
- inputJsonSchema: InputJsonSchema12,
1709
+ inputZod: InputSchema14,
1710
+ inputJsonSchema: InputJsonSchema14,
1189
1711
  allowedModels: [],
1190
1712
  defaultModel: "",
1191
- run: run12
1713
+ run: run14
1192
1714
  };
1193
1715
  }
1194
1716
  });
1195
- async function run13(input, ctx) {
1717
+ async function run15(input, ctx) {
1196
1718
  const res = await ctx.client.saveMedia({
1197
1719
  url: input.url,
1198
1720
  prompt: input.prompt,
@@ -1206,17 +1728,17 @@ async function run13(input, ctx) {
1206
1728
  It will appear at https://pixeldojo.ai/my-images and in pixeldojo:library searches.`
1207
1729
  };
1208
1730
  }
1209
- var InputSchema13, InputJsonSchema13, saveSkill;
1731
+ var InputSchema15, InputJsonSchema15, saveSkill;
1210
1732
  var init_save = __esm({
1211
1733
  "src/skills/save.ts"() {
1212
- InputSchema13 = zod.z.object({
1734
+ InputSchema15 = zod.z.object({
1213
1735
  url: zod.z.string().url().describe("The asset URL to save (image or video)."),
1214
1736
  prompt: zod.z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
1215
1737
  tool_name: zod.z.string().optional().describe("Tool/model slug that produced it."),
1216
1738
  modality: zod.z.enum(["image", "video", "audio"]).optional(),
1217
1739
  job_id: zod.z.string().optional().describe("The jobId that produced this asset, when known.")
1218
1740
  });
1219
- InputJsonSchema13 = {
1741
+ InputJsonSchema15 = {
1220
1742
  type: "object",
1221
1743
  properties: {
1222
1744
  url: { type: "string", format: "uri" },
@@ -1230,11 +1752,11 @@ var init_save = __esm({
1230
1752
  saveSkill = {
1231
1753
  name: "pixeldojo:save",
1232
1754
  description: "Save a generated image or video into the user's My Media library permanently. Generation outputs otherwise expire after 24 hours \u2014 call this when the user wants to keep a result.",
1233
- inputZod: InputSchema13,
1234
- inputJsonSchema: InputJsonSchema13,
1755
+ inputZod: InputSchema15,
1756
+ inputJsonSchema: InputJsonSchema15,
1235
1757
  allowedModels: [],
1236
1758
  defaultModel: "",
1237
- run: run13
1759
+ run: run15
1238
1760
  };
1239
1761
  }
1240
1762
  });
@@ -1446,11 +1968,13 @@ var init_workflows = __esm({
1446
1968
  var skills_exports = {};
1447
1969
  __export(skills_exports, {
1448
1970
  ALL_SKILLS: () => ALL_SKILLS,
1971
+ adSkill: () => adSkill,
1449
1972
  audioSkill: () => audioSkill,
1450
1973
  campaignSkill: () => campaignSkill,
1451
1974
  campaignStatusSkill: () => campaignStatusSkill,
1452
1975
  characterSkill: () => characterSkill,
1453
1976
  editSkill: () => editSkill,
1977
+ filmSkill: () => filmSkill,
1454
1978
  findSkill: () => findSkill,
1455
1979
  fromUrlSkill: () => fromUrlSkill,
1456
1980
  generateSkill: () => generateSkill,
@@ -1477,6 +2001,8 @@ var init_skills = __esm({
1477
2001
  init_upscale();
1478
2002
  init_status();
1479
2003
  init_from_url();
2004
+ init_film();
2005
+ init_ad();
1480
2006
  init_campaign();
1481
2007
  init_campaign_status();
1482
2008
  init_upload();
@@ -1499,8 +2025,10 @@ var init_skills = __esm({
1499
2025
  statusSkill,
1500
2026
  fromUrlSkill,
1501
2027
  uploadSkill,
2028
+ adSkill,
1502
2029
  campaignSkill,
1503
- campaignStatusSkill
2030
+ campaignStatusSkill,
2031
+ filmSkill
1504
2032
  ];
1505
2033
  }
1506
2034
  });
@@ -1555,7 +2083,7 @@ function makeRedactedLogger(target = console) {
1555
2083
  }
1556
2084
 
1557
2085
  // src/server.ts
1558
- var PACKAGE_VERSION = "0.6.0" ;
2086
+ var PACKAGE_VERSION = "0.9.0" ;
1559
2087
  function buildServer(options) {
1560
2088
  if (!options.apiKey) {
1561
2089
  throw new Error(
@@ -1622,6 +2150,8 @@ function buildServer(options) {
1622
2150
  });
1623
2151
  const ctx = {
1624
2152
  client,
2153
+ apiKey: options.apiKey,
2154
+ baseUrl,
1625
2155
  mcpVersion: version,
1626
2156
  logger,
1627
2157
  buildStatusUrl: (jobId) => `${baseUrl}/api/v1/jobs/${encodeURIComponent(jobId)}`