@pixeldojo/mcp 0.8.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.js CHANGED
@@ -186,7 +186,7 @@ var init_upload_helpers = __esm({
186
186
  function getFieldConstraints(apiId, client) {
187
187
  const cached = schemaCache.get(apiId);
188
188
  if (cached) return cached;
189
- const pending = client.getModelSchema(apiId).then(
189
+ const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
190
190
  (res) => res.schema?.properties ?? {}
191
191
  ).catch(() => {
192
192
  schemaCache.delete(apiId);
@@ -254,10 +254,34 @@ async function snapDuration(apiId, requested, client, opts = {}) {
254
254
  }
255
255
  return requested;
256
256
  }
257
- var schemaCache;
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;
258
272
  var init_schema_snap = __esm({
259
273
  "src/skills/schema-snap.ts"() {
260
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
+ ];
261
285
  }
262
286
  });
263
287
  function buildPayload(apiId, input) {
@@ -409,12 +433,21 @@ function buildPayload2(apiId, input) {
409
433
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
410
434
  };
411
435
  case "flux-edit":
412
- 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
+ };
413
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":
414
448
  return {
415
449
  prompt: input.prompt,
416
450
  image_url: urls[0],
417
- ...urls.length > 1 ? { image_urls: urls } : {},
418
451
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
419
452
  };
420
453
  case "google-nano-banana":
@@ -430,6 +463,19 @@ function buildPayload2(apiId, input) {
430
463
  };
431
464
  }
432
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
+ }
433
479
  async function run2(input, ctx) {
434
480
  let resolved = input;
435
481
  try {
@@ -460,7 +506,8 @@ async function run2(input, ctx) {
460
506
  ...resolved,
461
507
  aspect_ratio: await snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client)
462
508
  };
463
- const body = buildPayload2(apiId, resolved);
509
+ const { body, dropped } = await buildBody(apiId, resolved, ctx);
510
+ const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
464
511
  const submitted = await ctx.client.run(apiId, body);
465
512
  const result = await pollJobUntilDoneOrHandoff(
466
513
  submitted.jobId,
@@ -473,7 +520,7 @@ async function run2(input, ctx) {
473
520
  );
474
521
  return pollResultToSkillResult(
475
522
  result,
476
- (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(", ")}`
477
524
  );
478
525
  }
479
526
  var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
@@ -933,7 +980,256 @@ var init_from_url = __esm({
933
980
  };
934
981
  }
935
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
+ }
936
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) {
937
1233
  const productImages = [
938
1234
  ...input.product_image_urls ?? [],
939
1235
  ...await Promise.all((input.product_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
@@ -976,13 +1272,13 @@ async function run8(input, ctx) {
976
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]}`
977
1273
  );
978
1274
  }
979
- var InputSchema8, InputJsonSchema8, adSkill;
1275
+ var InputSchema9, InputJsonSchema9, adSkill;
980
1276
  var init_ad = __esm({
981
1277
  "src/skills/ad.ts"() {
982
1278
  init_types();
983
1279
  init_poll();
984
1280
  init_upload_helpers();
985
- InputSchema8 = z.object({
1281
+ InputSchema9 = z.object({
986
1282
  style: z.string().max(80).optional().describe(
987
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."
988
1284
  ),
@@ -1008,7 +1304,7 @@ var init_ad = __esm({
1008
1304
  path: ["product_url"]
1009
1305
  }
1010
1306
  );
1011
- InputJsonSchema8 = {
1307
+ InputJsonSchema9 = {
1012
1308
  type: "object",
1013
1309
  properties: {
1014
1310
  style: {
@@ -1064,16 +1360,16 @@ var init_ad = __esm({
1064
1360
  adSkill = {
1065
1361
  name: "pixeldojo:ad",
1066
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.",
1067
- inputZod: InputSchema8,
1068
- inputJsonSchema: InputJsonSchema8,
1363
+ inputZod: InputSchema9,
1364
+ inputJsonSchema: InputJsonSchema9,
1069
1365
  // Composition skill — the server routes to the ad engine internally.
1070
1366
  allowedModels: [],
1071
1367
  defaultModel: "",
1072
- run: run8
1368
+ run: run9
1073
1369
  };
1074
1370
  }
1075
1371
  });
1076
- async function run9(input, ctx) {
1372
+ async function run10(input, ctx) {
1077
1373
  const client = ctx.client;
1078
1374
  const result = await client.runCampaign(input);
1079
1375
  if (result.kind === "campaign") {
@@ -1100,10 +1396,10 @@ async function run9(input, ctx) {
1100
1396
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
1101
1397
  };
1102
1398
  }
1103
- var InputSchema9, InputJsonSchema9, campaignSkill;
1399
+ var InputSchema10, InputJsonSchema10, campaignSkill;
1104
1400
  var init_campaign = __esm({
1105
1401
  "src/skills/campaign.ts"() {
1106
- InputSchema9 = z.object({
1402
+ InputSchema10 = z.object({
1107
1403
  productUrl: z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
1108
1404
  product: z.object({
1109
1405
  name: z.string().min(1).max(200),
@@ -1119,7 +1415,7 @@ var init_campaign = __esm({
1119
1415
  message: "productUrl or product is required",
1120
1416
  path: ["productUrl"]
1121
1417
  });
1122
- InputJsonSchema9 = {
1418
+ InputJsonSchema10 = {
1123
1419
  type: "object",
1124
1420
  properties: {
1125
1421
  productUrl: {
@@ -1150,17 +1446,17 @@ var init_campaign = __esm({
1150
1446
  campaignSkill = {
1151
1447
  name: "pixeldojo:campaign",
1152
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.",
1153
- inputZod: InputSchema9,
1154
- inputJsonSchema: InputJsonSchema9,
1449
+ inputZod: InputSchema10,
1450
+ inputJsonSchema: InputJsonSchema10,
1155
1451
  // Composition skill — internal sub-jobs route to per-tool models, no
1156
1452
  // single allowed-models list applies here.
1157
1453
  allowedModels: [],
1158
1454
  defaultModel: "",
1159
- run: run9
1455
+ run: run10
1160
1456
  };
1161
1457
  }
1162
1458
  });
1163
- async function run10(input, ctx) {
1459
+ async function run11(input, ctx) {
1164
1460
  const client = ctx.client;
1165
1461
  const result = await client.getCampaign(input.campaignId);
1166
1462
  if (result.kind === "campaign") {
@@ -1190,13 +1486,13 @@ async function run10(input, ctx) {
1190
1486
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
1191
1487
  };
1192
1488
  }
1193
- var InputSchema10, InputJsonSchema10, campaignStatusSkill;
1489
+ var InputSchema11, InputJsonSchema11, campaignStatusSkill;
1194
1490
  var init_campaign_status = __esm({
1195
1491
  "src/skills/campaign-status.ts"() {
1196
- InputSchema10 = z.object({
1492
+ InputSchema11 = z.object({
1197
1493
  campaignId: z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
1198
1494
  });
1199
- InputJsonSchema10 = {
1495
+ InputJsonSchema11 = {
1200
1496
  type: "object",
1201
1497
  properties: {
1202
1498
  campaignId: {
@@ -1209,15 +1505,15 @@ var init_campaign_status = __esm({
1209
1505
  campaignStatusSkill = {
1210
1506
  name: "pixeldojo:campaign_status",
1211
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.",
1212
- inputZod: InputSchema10,
1213
- inputJsonSchema: InputJsonSchema10,
1508
+ inputZod: InputSchema11,
1509
+ inputJsonSchema: InputJsonSchema11,
1214
1510
  allowedModels: [],
1215
1511
  defaultModel: "",
1216
- run: run10
1512
+ run: run11
1217
1513
  };
1218
1514
  }
1219
1515
  });
1220
- async function run11(input, ctx) {
1516
+ async function run12(input, ctx) {
1221
1517
  let url;
1222
1518
  try {
1223
1519
  url = await uploadLocalFile(input.path, ctx.client);
@@ -1233,16 +1529,16 @@ async function run11(input, ctx) {
1233
1529
  (temp storage; auto-deletes in ~24h \u2014 pass this URL as image_url to :generate or :edit)`
1234
1530
  };
1235
1531
  }
1236
- var InputSchema11, InputJsonSchema11, uploadSkill;
1532
+ var InputSchema12, InputJsonSchema12, uploadSkill;
1237
1533
  var init_upload = __esm({
1238
1534
  "src/skills/upload.ts"() {
1239
1535
  init_upload_helpers();
1240
- InputSchema11 = z.object({
1536
+ InputSchema12 = z.object({
1241
1537
  path: z.string().min(1).max(4096).describe(
1242
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."
1243
1539
  )
1244
1540
  });
1245
- InputJsonSchema11 = {
1541
+ InputJsonSchema12 = {
1246
1542
  type: "object",
1247
1543
  properties: {
1248
1544
  path: {
@@ -1255,15 +1551,15 @@ var init_upload = __esm({
1255
1551
  uploadSkill = {
1256
1552
  name: "pixeldojo:upload",
1257
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.",
1258
- inputZod: InputSchema11,
1259
- inputJsonSchema: InputJsonSchema11,
1554
+ inputZod: InputSchema12,
1555
+ inputJsonSchema: InputJsonSchema12,
1260
1556
  allowedModels: [],
1261
1557
  defaultModel: "",
1262
- run: run11
1558
+ run: run12
1263
1559
  };
1264
1560
  }
1265
1561
  });
1266
- async function run12(input, ctx) {
1562
+ async function run13(input, ctx) {
1267
1563
  const requested = input.model ?? DEFAULT_MODEL6;
1268
1564
  if (!ALLOWED4.includes(requested)) {
1269
1565
  return {
@@ -1312,7 +1608,7 @@ async function run12(input, ctx) {
1312
1608
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
1313
1609
  );
1314
1610
  }
1315
- var ALLOWED4, DEFAULT_MODEL6, InputSchema12, InputJsonSchema12, audioSkill;
1611
+ var ALLOWED4, DEFAULT_MODEL6, InputSchema13, InputJsonSchema13, audioSkill;
1316
1612
  var init_audio = __esm({
1317
1613
  "src/skills/audio.ts"() {
1318
1614
  init_poll();
@@ -1320,7 +1616,7 @@ var init_audio = __esm({
1320
1616
  init_types();
1321
1617
  ALLOWED4 = ["seed-audio", "text-to-speech"];
1322
1618
  DEFAULT_MODEL6 = "seed-audio";
1323
- InputSchema12 = z.object({
1619
+ InputSchema13 = z.object({
1324
1620
  prompt: z.string().min(1).describe(
1325
1621
  "A script for speech, or a scene description for cinematic audio (dialogue + effects + music). Reference uploaded clips as @Audio1..@Audio3."
1326
1622
  ),
@@ -1331,7 +1627,7 @@ var init_audio = __esm({
1331
1627
  output_format: z.string().optional().describe("seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3, wav, flac, pcm."),
1332
1628
  speed: z.number().min(0.5).max(2).optional().describe("Speech speed multiplier (0.5\u20132).")
1333
1629
  });
1334
- InputJsonSchema12 = {
1630
+ InputJsonSchema13 = {
1335
1631
  type: "object",
1336
1632
  properties: {
1337
1633
  prompt: { type: "string" },
@@ -1347,15 +1643,15 @@ var init_audio = __esm({
1347
1643
  audioSkill = {
1348
1644
  name: "pixeldojo:audio",
1349
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).",
1350
- inputZod: InputSchema12,
1351
- inputJsonSchema: InputJsonSchema12,
1646
+ inputZod: InputSchema13,
1647
+ inputJsonSchema: InputJsonSchema13,
1352
1648
  allowedModels: [...ALLOWED4],
1353
1649
  defaultModel: DEFAULT_MODEL6,
1354
- run: run12
1650
+ run: run13
1355
1651
  };
1356
1652
  }
1357
1653
  });
1358
- async function run13(input, ctx) {
1654
+ async function run14(input, ctx) {
1359
1655
  const res = await ctx.client.listLibrary({
1360
1656
  q: input.q,
1361
1657
  model: input.model,
@@ -1386,16 +1682,16 @@ ${lines.join("\n")}
1386
1682
  Pass any URL back into :generate, :edit, :character, or :upscale as a reference input.`
1387
1683
  };
1388
1684
  }
1389
- var InputSchema13, InputJsonSchema13, librarySkill;
1685
+ var InputSchema14, InputJsonSchema14, librarySkill;
1390
1686
  var init_library = __esm({
1391
1687
  "src/skills/library.ts"() {
1392
- InputSchema13 = z.object({
1688
+ InputSchema14 = z.object({
1393
1689
  q: z.string().optional().describe("Substring match against the prompt that produced the asset."),
1394
1690
  model: z.string().optional().describe("Restrict to a single model (apiId)."),
1395
1691
  modality: z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
1396
1692
  limit: z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
1397
1693
  });
1398
- InputJsonSchema13 = {
1694
+ InputJsonSchema14 = {
1399
1695
  type: "object",
1400
1696
  properties: {
1401
1697
  q: { type: "string" },
@@ -1408,15 +1704,15 @@ var init_library = __esm({
1408
1704
  librarySkill = {
1409
1705
  name: "pixeldojo:library",
1410
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.",
1411
- inputZod: InputSchema13,
1412
- inputJsonSchema: InputJsonSchema13,
1707
+ inputZod: InputSchema14,
1708
+ inputJsonSchema: InputJsonSchema14,
1413
1709
  allowedModels: [],
1414
1710
  defaultModel: "",
1415
- run: run13
1711
+ run: run14
1416
1712
  };
1417
1713
  }
1418
1714
  });
1419
- async function run14(input, ctx) {
1715
+ async function run15(input, ctx) {
1420
1716
  const res = await ctx.client.saveMedia({
1421
1717
  url: input.url,
1422
1718
  prompt: input.prompt,
@@ -1430,17 +1726,17 @@ async function run14(input, ctx) {
1430
1726
  It will appear at https://pixeldojo.ai/my-images and in pixeldojo:library searches.`
1431
1727
  };
1432
1728
  }
1433
- var InputSchema14, InputJsonSchema14, saveSkill;
1729
+ var InputSchema15, InputJsonSchema15, saveSkill;
1434
1730
  var init_save = __esm({
1435
1731
  "src/skills/save.ts"() {
1436
- InputSchema14 = z.object({
1732
+ InputSchema15 = z.object({
1437
1733
  url: z.string().url().describe("The asset URL to save (image or video)."),
1438
1734
  prompt: z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
1439
1735
  tool_name: z.string().optional().describe("Tool/model slug that produced it."),
1440
1736
  modality: z.enum(["image", "video", "audio"]).optional(),
1441
1737
  job_id: z.string().optional().describe("The jobId that produced this asset, when known.")
1442
1738
  });
1443
- InputJsonSchema14 = {
1739
+ InputJsonSchema15 = {
1444
1740
  type: "object",
1445
1741
  properties: {
1446
1742
  url: { type: "string", format: "uri" },
@@ -1454,11 +1750,11 @@ var init_save = __esm({
1454
1750
  saveSkill = {
1455
1751
  name: "pixeldojo:save",
1456
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.",
1457
- inputZod: InputSchema14,
1458
- inputJsonSchema: InputJsonSchema14,
1753
+ inputZod: InputSchema15,
1754
+ inputJsonSchema: InputJsonSchema15,
1459
1755
  allowedModels: [],
1460
1756
  defaultModel: "",
1461
- run: run14
1757
+ run: run15
1462
1758
  };
1463
1759
  }
1464
1760
  });
@@ -1676,6 +1972,7 @@ __export(skills_exports, {
1676
1972
  campaignStatusSkill: () => campaignStatusSkill,
1677
1973
  characterSkill: () => characterSkill,
1678
1974
  editSkill: () => editSkill,
1975
+ filmSkill: () => filmSkill,
1679
1976
  findSkill: () => findSkill,
1680
1977
  fromUrlSkill: () => fromUrlSkill,
1681
1978
  generateSkill: () => generateSkill,
@@ -1702,6 +1999,7 @@ var init_skills = __esm({
1702
1999
  init_upscale();
1703
2000
  init_status();
1704
2001
  init_from_url();
2002
+ init_film();
1705
2003
  init_ad();
1706
2004
  init_campaign();
1707
2005
  init_campaign_status();
@@ -1727,7 +2025,8 @@ var init_skills = __esm({
1727
2025
  uploadSkill,
1728
2026
  adSkill,
1729
2027
  campaignSkill,
1730
- campaignStatusSkill
2028
+ campaignStatusSkill,
2029
+ filmSkill
1731
2030
  ];
1732
2031
  }
1733
2032
  });
@@ -1782,7 +2081,7 @@ function makeRedactedLogger(target = console) {
1782
2081
  }
1783
2082
 
1784
2083
  // src/server.ts
1785
- var PACKAGE_VERSION = "0.8.0" ;
2084
+ var PACKAGE_VERSION = "0.9.0" ;
1786
2085
  function buildServer(options) {
1787
2086
  if (!options.apiKey) {
1788
2087
  throw new Error(
@@ -1849,6 +2148,8 @@ function buildServer(options) {
1849
2148
  });
1850
2149
  const ctx = {
1851
2150
  client,
2151
+ apiKey: options.apiKey,
2152
+ baseUrl,
1852
2153
  mcpVersion: version,
1853
2154
  logger,
1854
2155
  buildStatusUrl: (jobId) => `${baseUrl}/api/v1/jobs/${encodeURIComponent(jobId)}`