@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.cjs CHANGED
@@ -188,7 +188,7 @@ var init_upload_helpers = __esm({
188
188
  function getFieldConstraints(apiId, client) {
189
189
  const cached = schemaCache.get(apiId);
190
190
  if (cached) return cached;
191
- const pending = client.getModelSchema(apiId).then(
191
+ const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
192
192
  (res) => res.schema?.properties ?? {}
193
193
  ).catch(() => {
194
194
  schemaCache.delete(apiId);
@@ -256,10 +256,34 @@ async function snapDuration(apiId, requested, client, opts = {}) {
256
256
  }
257
257
  return requested;
258
258
  }
259
- var schemaCache;
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;
260
274
  var init_schema_snap = __esm({
261
275
  "src/skills/schema-snap.ts"() {
262
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
+ ];
263
287
  }
264
288
  });
265
289
  function buildPayload(apiId, input) {
@@ -411,12 +435,21 @@ function buildPayload2(apiId, input) {
411
435
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
412
436
  };
413
437
  case "flux-edit":
414
- 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
+ };
415
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":
416
450
  return {
417
451
  prompt: input.prompt,
418
452
  image_url: urls[0],
419
- ...urls.length > 1 ? { image_urls: urls } : {},
420
453
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
421
454
  };
422
455
  case "google-nano-banana":
@@ -432,6 +465,19 @@ function buildPayload2(apiId, input) {
432
465
  };
433
466
  }
434
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
+ }
435
481
  async function run2(input, ctx) {
436
482
  let resolved = input;
437
483
  try {
@@ -462,7 +508,8 @@ async function run2(input, ctx) {
462
508
  ...resolved,
463
509
  aspect_ratio: await snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client)
464
510
  };
465
- const body = buildPayload2(apiId, resolved);
511
+ const { body, dropped } = await buildBody(apiId, resolved, ctx);
512
+ const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
466
513
  const submitted = await ctx.client.run(apiId, body);
467
514
  const result = await pollJobUntilDoneOrHandoff(
468
515
  submitted.jobId,
@@ -475,7 +522,7 @@ async function run2(input, ctx) {
475
522
  );
476
523
  return pollResultToSkillResult(
477
524
  result,
478
- (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(", ")}`
479
526
  );
480
527
  }
481
528
  var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
@@ -935,7 +982,256 @@ var init_from_url = __esm({
935
982
  };
936
983
  }
937
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
+ }
938
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) {
939
1235
  const productImages = [
940
1236
  ...input.product_image_urls ?? [],
941
1237
  ...await Promise.all((input.product_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
@@ -978,13 +1274,13 @@ async function run8(input, ctx) {
978
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]}`
979
1275
  );
980
1276
  }
981
- var InputSchema8, InputJsonSchema8, adSkill;
1277
+ var InputSchema9, InputJsonSchema9, adSkill;
982
1278
  var init_ad = __esm({
983
1279
  "src/skills/ad.ts"() {
984
1280
  init_types();
985
1281
  init_poll();
986
1282
  init_upload_helpers();
987
- InputSchema8 = zod.z.object({
1283
+ InputSchema9 = zod.z.object({
988
1284
  style: zod.z.string().max(80).optional().describe(
989
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."
990
1286
  ),
@@ -1010,7 +1306,7 @@ var init_ad = __esm({
1010
1306
  path: ["product_url"]
1011
1307
  }
1012
1308
  );
1013
- InputJsonSchema8 = {
1309
+ InputJsonSchema9 = {
1014
1310
  type: "object",
1015
1311
  properties: {
1016
1312
  style: {
@@ -1066,16 +1362,16 @@ var init_ad = __esm({
1066
1362
  adSkill = {
1067
1363
  name: "pixeldojo:ad",
1068
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.",
1069
- inputZod: InputSchema8,
1070
- inputJsonSchema: InputJsonSchema8,
1365
+ inputZod: InputSchema9,
1366
+ inputJsonSchema: InputJsonSchema9,
1071
1367
  // Composition skill — the server routes to the ad engine internally.
1072
1368
  allowedModels: [],
1073
1369
  defaultModel: "",
1074
- run: run8
1370
+ run: run9
1075
1371
  };
1076
1372
  }
1077
1373
  });
1078
- async function run9(input, ctx) {
1374
+ async function run10(input, ctx) {
1079
1375
  const client = ctx.client;
1080
1376
  const result = await client.runCampaign(input);
1081
1377
  if (result.kind === "campaign") {
@@ -1102,10 +1398,10 @@ async function run9(input, ctx) {
1102
1398
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
1103
1399
  };
1104
1400
  }
1105
- var InputSchema9, InputJsonSchema9, campaignSkill;
1401
+ var InputSchema10, InputJsonSchema10, campaignSkill;
1106
1402
  var init_campaign = __esm({
1107
1403
  "src/skills/campaign.ts"() {
1108
- InputSchema9 = zod.z.object({
1404
+ InputSchema10 = zod.z.object({
1109
1405
  productUrl: zod.z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
1110
1406
  product: zod.z.object({
1111
1407
  name: zod.z.string().min(1).max(200),
@@ -1121,7 +1417,7 @@ var init_campaign = __esm({
1121
1417
  message: "productUrl or product is required",
1122
1418
  path: ["productUrl"]
1123
1419
  });
1124
- InputJsonSchema9 = {
1420
+ InputJsonSchema10 = {
1125
1421
  type: "object",
1126
1422
  properties: {
1127
1423
  productUrl: {
@@ -1152,17 +1448,17 @@ var init_campaign = __esm({
1152
1448
  campaignSkill = {
1153
1449
  name: "pixeldojo:campaign",
1154
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.",
1155
- inputZod: InputSchema9,
1156
- inputJsonSchema: InputJsonSchema9,
1451
+ inputZod: InputSchema10,
1452
+ inputJsonSchema: InputJsonSchema10,
1157
1453
  // Composition skill — internal sub-jobs route to per-tool models, no
1158
1454
  // single allowed-models list applies here.
1159
1455
  allowedModels: [],
1160
1456
  defaultModel: "",
1161
- run: run9
1457
+ run: run10
1162
1458
  };
1163
1459
  }
1164
1460
  });
1165
- async function run10(input, ctx) {
1461
+ async function run11(input, ctx) {
1166
1462
  const client = ctx.client;
1167
1463
  const result = await client.getCampaign(input.campaignId);
1168
1464
  if (result.kind === "campaign") {
@@ -1192,13 +1488,13 @@ async function run10(input, ctx) {
1192
1488
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
1193
1489
  };
1194
1490
  }
1195
- var InputSchema10, InputJsonSchema10, campaignStatusSkill;
1491
+ var InputSchema11, InputJsonSchema11, campaignStatusSkill;
1196
1492
  var init_campaign_status = __esm({
1197
1493
  "src/skills/campaign-status.ts"() {
1198
- InputSchema10 = zod.z.object({
1494
+ InputSchema11 = zod.z.object({
1199
1495
  campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
1200
1496
  });
1201
- InputJsonSchema10 = {
1497
+ InputJsonSchema11 = {
1202
1498
  type: "object",
1203
1499
  properties: {
1204
1500
  campaignId: {
@@ -1211,15 +1507,15 @@ var init_campaign_status = __esm({
1211
1507
  campaignStatusSkill = {
1212
1508
  name: "pixeldojo:campaign_status",
1213
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.",
1214
- inputZod: InputSchema10,
1215
- inputJsonSchema: InputJsonSchema10,
1510
+ inputZod: InputSchema11,
1511
+ inputJsonSchema: InputJsonSchema11,
1216
1512
  allowedModels: [],
1217
1513
  defaultModel: "",
1218
- run: run10
1514
+ run: run11
1219
1515
  };
1220
1516
  }
1221
1517
  });
1222
- async function run11(input, ctx) {
1518
+ async function run12(input, ctx) {
1223
1519
  let url;
1224
1520
  try {
1225
1521
  url = await uploadLocalFile(input.path, ctx.client);
@@ -1235,16 +1531,16 @@ async function run11(input, ctx) {
1235
1531
  (temp storage; auto-deletes in ~24h \u2014 pass this URL as image_url to :generate or :edit)`
1236
1532
  };
1237
1533
  }
1238
- var InputSchema11, InputJsonSchema11, uploadSkill;
1534
+ var InputSchema12, InputJsonSchema12, uploadSkill;
1239
1535
  var init_upload = __esm({
1240
1536
  "src/skills/upload.ts"() {
1241
1537
  init_upload_helpers();
1242
- InputSchema11 = zod.z.object({
1538
+ InputSchema12 = zod.z.object({
1243
1539
  path: zod.z.string().min(1).max(4096).describe(
1244
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."
1245
1541
  )
1246
1542
  });
1247
- InputJsonSchema11 = {
1543
+ InputJsonSchema12 = {
1248
1544
  type: "object",
1249
1545
  properties: {
1250
1546
  path: {
@@ -1257,15 +1553,15 @@ var init_upload = __esm({
1257
1553
  uploadSkill = {
1258
1554
  name: "pixeldojo:upload",
1259
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.",
1260
- inputZod: InputSchema11,
1261
- inputJsonSchema: InputJsonSchema11,
1556
+ inputZod: InputSchema12,
1557
+ inputJsonSchema: InputJsonSchema12,
1262
1558
  allowedModels: [],
1263
1559
  defaultModel: "",
1264
- run: run11
1560
+ run: run12
1265
1561
  };
1266
1562
  }
1267
1563
  });
1268
- async function run12(input, ctx) {
1564
+ async function run13(input, ctx) {
1269
1565
  const requested = input.model ?? DEFAULT_MODEL6;
1270
1566
  if (!ALLOWED4.includes(requested)) {
1271
1567
  return {
@@ -1314,7 +1610,7 @@ async function run12(input, ctx) {
1314
1610
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
1315
1611
  );
1316
1612
  }
1317
- var ALLOWED4, DEFAULT_MODEL6, InputSchema12, InputJsonSchema12, audioSkill;
1613
+ var ALLOWED4, DEFAULT_MODEL6, InputSchema13, InputJsonSchema13, audioSkill;
1318
1614
  var init_audio = __esm({
1319
1615
  "src/skills/audio.ts"() {
1320
1616
  init_poll();
@@ -1322,7 +1618,7 @@ var init_audio = __esm({
1322
1618
  init_types();
1323
1619
  ALLOWED4 = ["seed-audio", "text-to-speech"];
1324
1620
  DEFAULT_MODEL6 = "seed-audio";
1325
- InputSchema12 = zod.z.object({
1621
+ InputSchema13 = zod.z.object({
1326
1622
  prompt: zod.z.string().min(1).describe(
1327
1623
  "A script for speech, or a scene description for cinematic audio (dialogue + effects + music). Reference uploaded clips as @Audio1..@Audio3."
1328
1624
  ),
@@ -1333,7 +1629,7 @@ var init_audio = __esm({
1333
1629
  output_format: zod.z.string().optional().describe("seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3, wav, flac, pcm."),
1334
1630
  speed: zod.z.number().min(0.5).max(2).optional().describe("Speech speed multiplier (0.5\u20132).")
1335
1631
  });
1336
- InputJsonSchema12 = {
1632
+ InputJsonSchema13 = {
1337
1633
  type: "object",
1338
1634
  properties: {
1339
1635
  prompt: { type: "string" },
@@ -1349,15 +1645,15 @@ var init_audio = __esm({
1349
1645
  audioSkill = {
1350
1646
  name: "pixeldojo:audio",
1351
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).",
1352
- inputZod: InputSchema12,
1353
- inputJsonSchema: InputJsonSchema12,
1648
+ inputZod: InputSchema13,
1649
+ inputJsonSchema: InputJsonSchema13,
1354
1650
  allowedModels: [...ALLOWED4],
1355
1651
  defaultModel: DEFAULT_MODEL6,
1356
- run: run12
1652
+ run: run13
1357
1653
  };
1358
1654
  }
1359
1655
  });
1360
- async function run13(input, ctx) {
1656
+ async function run14(input, ctx) {
1361
1657
  const res = await ctx.client.listLibrary({
1362
1658
  q: input.q,
1363
1659
  model: input.model,
@@ -1388,16 +1684,16 @@ ${lines.join("\n")}
1388
1684
  Pass any URL back into :generate, :edit, :character, or :upscale as a reference input.`
1389
1685
  };
1390
1686
  }
1391
- var InputSchema13, InputJsonSchema13, librarySkill;
1687
+ var InputSchema14, InputJsonSchema14, librarySkill;
1392
1688
  var init_library = __esm({
1393
1689
  "src/skills/library.ts"() {
1394
- InputSchema13 = zod.z.object({
1690
+ InputSchema14 = zod.z.object({
1395
1691
  q: zod.z.string().optional().describe("Substring match against the prompt that produced the asset."),
1396
1692
  model: zod.z.string().optional().describe("Restrict to a single model (apiId)."),
1397
1693
  modality: zod.z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
1398
1694
  limit: zod.z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
1399
1695
  });
1400
- InputJsonSchema13 = {
1696
+ InputJsonSchema14 = {
1401
1697
  type: "object",
1402
1698
  properties: {
1403
1699
  q: { type: "string" },
@@ -1410,15 +1706,15 @@ var init_library = __esm({
1410
1706
  librarySkill = {
1411
1707
  name: "pixeldojo:library",
1412
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.",
1413
- inputZod: InputSchema13,
1414
- inputJsonSchema: InputJsonSchema13,
1709
+ inputZod: InputSchema14,
1710
+ inputJsonSchema: InputJsonSchema14,
1415
1711
  allowedModels: [],
1416
1712
  defaultModel: "",
1417
- run: run13
1713
+ run: run14
1418
1714
  };
1419
1715
  }
1420
1716
  });
1421
- async function run14(input, ctx) {
1717
+ async function run15(input, ctx) {
1422
1718
  const res = await ctx.client.saveMedia({
1423
1719
  url: input.url,
1424
1720
  prompt: input.prompt,
@@ -1432,17 +1728,17 @@ async function run14(input, ctx) {
1432
1728
  It will appear at https://pixeldojo.ai/my-images and in pixeldojo:library searches.`
1433
1729
  };
1434
1730
  }
1435
- var InputSchema14, InputJsonSchema14, saveSkill;
1731
+ var InputSchema15, InputJsonSchema15, saveSkill;
1436
1732
  var init_save = __esm({
1437
1733
  "src/skills/save.ts"() {
1438
- InputSchema14 = zod.z.object({
1734
+ InputSchema15 = zod.z.object({
1439
1735
  url: zod.z.string().url().describe("The asset URL to save (image or video)."),
1440
1736
  prompt: zod.z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
1441
1737
  tool_name: zod.z.string().optional().describe("Tool/model slug that produced it."),
1442
1738
  modality: zod.z.enum(["image", "video", "audio"]).optional(),
1443
1739
  job_id: zod.z.string().optional().describe("The jobId that produced this asset, when known.")
1444
1740
  });
1445
- InputJsonSchema14 = {
1741
+ InputJsonSchema15 = {
1446
1742
  type: "object",
1447
1743
  properties: {
1448
1744
  url: { type: "string", format: "uri" },
@@ -1456,11 +1752,11 @@ var init_save = __esm({
1456
1752
  saveSkill = {
1457
1753
  name: "pixeldojo:save",
1458
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.",
1459
- inputZod: InputSchema14,
1460
- inputJsonSchema: InputJsonSchema14,
1755
+ inputZod: InputSchema15,
1756
+ inputJsonSchema: InputJsonSchema15,
1461
1757
  allowedModels: [],
1462
1758
  defaultModel: "",
1463
- run: run14
1759
+ run: run15
1464
1760
  };
1465
1761
  }
1466
1762
  });
@@ -1678,6 +1974,7 @@ __export(skills_exports, {
1678
1974
  campaignStatusSkill: () => campaignStatusSkill,
1679
1975
  characterSkill: () => characterSkill,
1680
1976
  editSkill: () => editSkill,
1977
+ filmSkill: () => filmSkill,
1681
1978
  findSkill: () => findSkill,
1682
1979
  fromUrlSkill: () => fromUrlSkill,
1683
1980
  generateSkill: () => generateSkill,
@@ -1704,6 +2001,7 @@ var init_skills = __esm({
1704
2001
  init_upscale();
1705
2002
  init_status();
1706
2003
  init_from_url();
2004
+ init_film();
1707
2005
  init_ad();
1708
2006
  init_campaign();
1709
2007
  init_campaign_status();
@@ -1729,7 +2027,8 @@ var init_skills = __esm({
1729
2027
  uploadSkill,
1730
2028
  adSkill,
1731
2029
  campaignSkill,
1732
- campaignStatusSkill
2030
+ campaignStatusSkill,
2031
+ filmSkill
1733
2032
  ];
1734
2033
  }
1735
2034
  });
@@ -1784,7 +2083,7 @@ function makeRedactedLogger(target = console) {
1784
2083
  }
1785
2084
 
1786
2085
  // src/server.ts
1787
- var PACKAGE_VERSION = "0.8.0" ;
2086
+ var PACKAGE_VERSION = "0.9.0" ;
1788
2087
  function buildServer(options) {
1789
2088
  if (!options.apiKey) {
1790
2089
  throw new Error(
@@ -1851,6 +2150,8 @@ function buildServer(options) {
1851
2150
  });
1852
2151
  const ctx = {
1853
2152
  client,
2153
+ apiKey: options.apiKey,
2154
+ baseUrl,
1854
2155
  mcpVersion: version,
1855
2156
  logger,
1856
2157
  buildStatusUrl: (jobId) => `${baseUrl}/api/v1/jobs/${encodeURIComponent(jobId)}`