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