@pixeldojo/mcp 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -95,7 +95,7 @@ function pollResultToSkillResult(result, successText) {
95
95
  }
96
96
  return {
97
97
  kind: "handoff",
98
- text: `Job ${result.jobId} is still running after the 30s sync window. Use pixeldojo:status with this jobId to check completion, or follow ${result.statusUrl}.`,
98
+ text: `Job ${result.jobId} is still running after the 30s sync window. Use pixeldojo_status with this jobId to check completion, or follow ${result.statusUrl}.`,
99
99
  jobId: result.jobId,
100
100
  statusUrl: result.statusUrl
101
101
  };
@@ -188,7 +188,8 @@ 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
+ if (typeof client?.getModelSchema !== "function") return Promise.resolve(null);
192
+ const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
192
193
  (res) => res.schema?.properties ?? {}
193
194
  ).catch(() => {
194
195
  schemaCache.delete(apiId);
@@ -232,6 +233,46 @@ async function snapAspectRatio(apiId, requested, client, opts = {}) {
232
233
  if (!field.enum || field.enum.length === 0) return requested;
233
234
  return nearestAspectFromEnum(requested, field.enum);
234
235
  }
236
+ async function resolveReferenceField(apiId, client) {
237
+ const props = await getFieldConstraints(apiId, client);
238
+ if (props === null) return void 0;
239
+ for (const name of REFERENCE_FIELD_PRIORITY) {
240
+ const field = props[name];
241
+ if (field) return { name, isArray: field.type === "array" };
242
+ }
243
+ return null;
244
+ }
245
+ function resolutionHeight(value) {
246
+ const v = value.trim().toLowerCase();
247
+ if (v === "4k") return 2160;
248
+ const n = Number.parseInt(v, 10);
249
+ return Number.isFinite(n) ? n : null;
250
+ }
251
+ async function snapResolution(apiId, requested, client, opts = {}) {
252
+ if (!requested) return void 0;
253
+ const props = await getFieldConstraints(apiId, client);
254
+ if (props === null) {
255
+ return opts.onUnknown === "omit" ? void 0 : requested;
256
+ }
257
+ const field = props.resolution;
258
+ if (!field) return void 0;
259
+ if (!field.enum || field.enum.length === 0) return requested;
260
+ if (field.enum.includes(requested)) return requested;
261
+ const target = resolutionHeight(requested);
262
+ if (target === null) return requested;
263
+ let best = requested;
264
+ let bestDiff = Infinity;
265
+ for (const candidate of field.enum) {
266
+ const h = resolutionHeight(candidate);
267
+ if (h === null) continue;
268
+ const diff = Math.abs(h - target);
269
+ if (diff < bestDiff) {
270
+ bestDiff = diff;
271
+ best = candidate;
272
+ }
273
+ }
274
+ return best;
275
+ }
235
276
  async function snapDuration(apiId, requested, client, opts = {}) {
236
277
  if (requested === void 0) return void 0;
237
278
  const props = await getFieldConstraints(apiId, client);
@@ -256,13 +297,49 @@ async function snapDuration(apiId, requested, client, opts = {}) {
256
297
  }
257
298
  return requested;
258
299
  }
259
- var schemaCache;
300
+ async function resolveImageRefs(apiId, urls, client) {
301
+ if (urls.length === 0) return null;
302
+ const props = await getFieldConstraints(apiId, client);
303
+ if (props === null) return null;
304
+ const key = REF_KEYS.find((k) => props[k] !== void 0);
305
+ if (!key) return null;
306
+ const field = props[key];
307
+ if (field.type === "array") {
308
+ const cap = typeof field.maxItems === "number" ? field.maxItems : urls.length;
309
+ const kept = urls.slice(0, Math.max(1, cap));
310
+ return { key, value: kept, dropped: urls.length - kept.length };
311
+ }
312
+ return { key, value: urls[0], dropped: urls.length - 1 };
313
+ }
314
+ var schemaCache, REFERENCE_FIELD_PRIORITY, REF_KEYS;
260
315
  var init_schema_snap = __esm({
261
316
  "src/skills/schema-snap.ts"() {
262
317
  schemaCache = /* @__PURE__ */ new Map();
318
+ REFERENCE_FIELD_PRIORITY = [
319
+ "image_url",
320
+ "start_image_url",
321
+ "input_image",
322
+ "image",
323
+ "reference_images",
324
+ "reference_urls",
325
+ "reference_image_urls",
326
+ "image_urls",
327
+ "image_input",
328
+ "images"
329
+ ];
330
+ REF_KEYS = [
331
+ "image_urls",
332
+ "images",
333
+ "image_input",
334
+ "reference_images",
335
+ "reference_urls",
336
+ "image_url",
337
+ "input_image",
338
+ "image"
339
+ ];
263
340
  }
264
341
  });
265
- function buildPayload(apiId, input) {
342
+ function buildPayload(apiId, input, refField, referenceUrls = []) {
266
343
  const body = { prompt: input.prompt };
267
344
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
268
345
  if (input.preset) body.preset = input.preset;
@@ -280,8 +357,16 @@ function buildPayload(apiId, input) {
280
357
  }
281
358
  break;
282
359
  default:
283
- if (input.image_url) body.image_url = input.image_url;
360
+ if (input.image_url) {
361
+ const refs = referenceUrls.length > 0 ? referenceUrls : [input.image_url];
362
+ if (refField) {
363
+ body[refField.name] = refField.isArray ? refs : refs[0];
364
+ } else {
365
+ body.image_url = refs[0];
366
+ }
367
+ }
284
368
  if (input.duration !== void 0) body.duration = input.duration;
369
+ if (input.resolution) body.resolution = input.resolution;
285
370
  break;
286
371
  }
287
372
  return body;
@@ -289,39 +374,51 @@ function buildPayload(apiId, input) {
289
374
  async function run(input, ctx) {
290
375
  const apiId = input.model ?? DEFAULT_MODEL;
291
376
  let resolved = input;
292
- if (!input.image_url && input.image_path) {
293
- try {
294
- const uploadedUrl = await uploadLocalFile(input.image_path, ctx.client);
295
- resolved = { ...input, image_url: uploadedUrl };
296
- } catch (err) {
297
- if (err instanceof SkillUploadError) {
298
- return { kind: "text", text: `pixeldojo:generate (upload): ${err.userMessage}` };
299
- }
300
- throw err;
377
+ let referenceUrls = [];
378
+ try {
379
+ const singleUrl = input.image_url ?? (input.image_path ? await uploadLocalFile(input.image_path, ctx.client) : void 0);
380
+ const manyUrls = [...input.image_urls ?? []];
381
+ for (const p of input.image_paths ?? []) {
382
+ manyUrls.push(await uploadLocalFile(p, ctx.client));
383
+ }
384
+ referenceUrls = [...singleUrl ? [singleUrl] : [], ...manyUrls].filter(
385
+ (u, i, all) => all.indexOf(u) === i
386
+ );
387
+ if (referenceUrls[0] && referenceUrls[0] !== input.image_url) {
388
+ resolved = { ...input, image_url: referenceUrls[0] };
389
+ }
390
+ } catch (err) {
391
+ if (err instanceof SkillUploadError) {
392
+ return { kind: "text", text: `pixeldojo_generate (upload): ${err.userMessage}` };
301
393
  }
394
+ throw err;
302
395
  }
303
396
  if (apiId === "grok-video-extend") {
304
397
  if (!resolved.image_url) {
305
398
  return {
306
399
  kind: "text",
307
- text: 'pixeldojo:generate with model="grok-video-extend" requires image_url (the source video URL to extend, or image_path for a local file).'
400
+ text: 'pixeldojo_generate with model="grok-video-extend" requires image_url (the source video URL to extend, or image_path for a local file).'
308
401
  };
309
402
  }
310
403
  if (resolved.duration === void 0) {
311
404
  return {
312
405
  kind: "text",
313
- text: 'pixeldojo:generate with model="grok-video-extend" requires duration (extension length in seconds).'
406
+ text: 'pixeldojo_generate with model="grok-video-extend" requires duration (extension length in seconds).'
314
407
  };
315
408
  }
316
409
  }
410
+ let refField;
317
411
  if (!VIDEO_INPUT_MODELS.has(apiId)) {
318
- const [aspect_ratio, duration] = await Promise.all([
412
+ const [aspect_ratio, duration, resolution, ref] = await Promise.all([
319
413
  snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
320
- snapDuration(apiId, resolved.duration, ctx.client)
414
+ snapDuration(apiId, resolved.duration, ctx.client),
415
+ snapResolution(apiId, resolved.resolution, ctx.client),
416
+ resolved.image_url ? resolveReferenceField(apiId, ctx.client) : Promise.resolve(void 0)
321
417
  ]);
322
- resolved = { ...resolved, aspect_ratio, duration };
418
+ resolved = { ...resolved, aspect_ratio, duration, resolution };
419
+ refField = ref;
323
420
  }
324
- const body = buildPayload(apiId, resolved);
421
+ const body = buildPayload(apiId, resolved, refField, referenceUrls);
325
422
  const submitted = await ctx.client.run(apiId, body);
326
423
  const result = await pollJobUntilDoneOrHandoff(
327
424
  submitted.jobId,
@@ -354,9 +451,18 @@ var init_generate = __esm({
354
451
  ),
355
452
  aspect_ratio: zod.z.string().optional().describe('Aspect ratio supported by the chosen model (e.g. "1:1", "16:9", "9:16").'),
356
453
  duration: zod.z.number().int().positive().optional().describe("Video duration in seconds, when applicable."),
454
+ resolution: zod.z.string().optional().describe(
455
+ 'Output resolution when the model supports it (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits. Omit to use the model default.'
456
+ ),
357
457
  image_url: zod.z.string().url().optional().describe("Reference image URL for image-to-image / image-to-video tools."),
358
458
  image_path: zod.z.string().min(1).optional().describe(
359
459
  "Absolute local file path to a reference image/video. The skill uploads it to PixelDojo first (24h temp storage) and then dispatches. Prefer this over image_url when the user has the file on disk."
460
+ ),
461
+ image_urls: zod.z.array(zod.z.string().url()).optional().describe(
462
+ "Multiple reference image URLs, for models that accept a reference set (character + wardrobe + location, for example). Models that take only one reference use the first and the rest are ignored."
463
+ ),
464
+ image_paths: zod.z.array(zod.z.string().min(1)).optional().describe(
465
+ "Multiple absolute local file paths. Each is uploaded to PixelDojo (24h temp storage) and used as a reference, same rules as image_urls."
360
466
  )
361
467
  });
362
468
  DEFAULT_MODEL = "google-nano-banana";
@@ -375,7 +481,21 @@ var init_generate = __esm({
375
481
  },
376
482
  aspect_ratio: { type: "string" },
377
483
  duration: { type: "integer" },
484
+ resolution: {
485
+ type: "string",
486
+ description: 'Output resolution when supported (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits.'
487
+ },
378
488
  image_url: { type: "string", format: "uri" },
489
+ image_urls: {
490
+ type: "array",
491
+ items: { type: "string", format: "uri" },
492
+ description: "Multiple reference image URLs for models that accept a reference set. Single-reference models use the first."
493
+ },
494
+ image_paths: {
495
+ type: "array",
496
+ items: { type: "string" },
497
+ description: "Multiple absolute local file paths, each uploaded to 24h temp storage and used as a reference."
498
+ },
379
499
  image_path: {
380
500
  type: "string",
381
501
  description: "Absolute local file path. The skill uploads it to PixelDojo (24h temp storage) and uses the resulting URL as the reference image. Prefer this over image_url when the user has a local file."
@@ -384,7 +504,7 @@ var init_generate = __esm({
384
504
  required: ["prompt"]
385
505
  };
386
506
  generateSkill = {
387
- name: "pixeldojo:generate",
507
+ name: "pixeldojo_generate",
388
508
  description: "Generate any image or video from a prompt. Auto-routes to a sensible default model; pass `model` to pick a specific apiId from the catalog. For reference-image / image-to-video flows: pass `image_url` if you already have a URL, or `image_path` (absolute local path) and the skill uploads it to 24h temp storage automatically.",
389
509
  inputZod: InputSchema,
390
510
  inputJsonSchema: InputJsonSchema,
@@ -411,12 +531,21 @@ function buildPayload2(apiId, input) {
411
531
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
412
532
  };
413
533
  case "flux-edit":
414
- case "xai-image-edit":
534
+ return {
535
+ prompt: input.prompt,
536
+ input_image_1: urls[0],
537
+ ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
538
+ };
415
539
  case "gpt-image-2-edit":
540
+ return {
541
+ prompt: input.prompt,
542
+ image_urls: urls.slice(0, 8),
543
+ ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
544
+ };
545
+ case "xai-image-edit":
416
546
  return {
417
547
  prompt: input.prompt,
418
548
  image_url: urls[0],
419
- ...urls.length > 1 ? { image_urls: urls } : {},
420
549
  ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
421
550
  };
422
551
  case "google-nano-banana":
@@ -432,6 +561,19 @@ function buildPayload2(apiId, input) {
432
561
  };
433
562
  }
434
563
  }
564
+ async function buildBody(apiId, input, ctx) {
565
+ const urls = input.image_urls && input.image_urls.length > 0 ? input.image_urls : input.image_url ? [input.image_url] : [];
566
+ const refs = await resolveImageRefs(apiId, urls, ctx.client);
567
+ if (!refs) return { body: buildPayload2(apiId, input), dropped: 0 };
568
+ const fallback = buildPayload2(apiId, input);
569
+ const body = { ...fallback };
570
+ for (const key of ["image_url", "image_urls", "images", "image_input", "input_image_1"]) {
571
+ if (key !== refs.key) delete body[key];
572
+ }
573
+ body.prompt = input.prompt;
574
+ body[refs.key] = refs.value;
575
+ return { body, dropped: refs.dropped };
576
+ }
435
577
  async function run2(input, ctx) {
436
578
  let resolved = input;
437
579
  try {
@@ -447,14 +589,14 @@ async function run2(input, ctx) {
447
589
  }
448
590
  } catch (err) {
449
591
  if (err instanceof SkillUploadError) {
450
- return { kind: "text", text: `pixeldojo:edit (upload): ${err.userMessage}` };
592
+ return { kind: "text", text: `pixeldojo_edit (upload): ${err.userMessage}` };
451
593
  }
452
594
  throw err;
453
595
  }
454
596
  if (!resolved.image_url && (!resolved.image_urls || resolved.image_urls.length === 0)) {
455
597
  return {
456
598
  kind: "text",
457
- text: "pixeldojo:edit requires at least one reference image. Pass image_url / image_urls (hosted URL) or image_path / image_paths (local file)."
599
+ text: "pixeldojo_edit requires at least one reference image. Pass image_url / image_urls (hosted URL) or image_path / image_paths (local file)."
458
600
  };
459
601
  }
460
602
  const apiId = resolved.model ?? DEFAULT_MODEL2;
@@ -462,7 +604,8 @@ async function run2(input, ctx) {
462
604
  ...resolved,
463
605
  aspect_ratio: await snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client)
464
606
  };
465
- const body = buildPayload2(apiId, resolved);
607
+ const { body, dropped } = await buildBody(apiId, resolved, ctx);
608
+ const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
466
609
  const submitted = await ctx.client.run(apiId, body);
467
610
  const result = await pollJobUntilDoneOrHandoff(
468
611
  submitted.jobId,
@@ -475,7 +618,7 @@ async function run2(input, ctx) {
475
618
  );
476
619
  return pollResultToSkillResult(
477
620
  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(", ")}`
621
+ (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
622
  );
480
623
  }
481
624
  var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
@@ -547,7 +690,7 @@ var init_edit = __esm({
547
690
  required: ["prompt"]
548
691
  };
549
692
  editSkill = {
550
- name: "pixeldojo:edit",
693
+ name: "pixeldojo_edit",
551
694
  description: "Edit an existing image with a text instruction. Defaults to wan-2.7-image-edit; pass `model` to pick another edit-capable apiId. Supports up to 9 reference images for multi-image composition. For reference images: pass image_url(s) for hosted URLs or image_path(s) for absolute local file paths (skill uploads to 24h temp storage automatically).",
552
695
  inputZod: InputSchema2,
553
696
  inputJsonSchema: InputJsonSchema2,
@@ -562,14 +705,14 @@ async function run3(input, ctx) {
562
705
  if (!ALLOWED.includes(requested)) {
563
706
  return {
564
707
  kind: "text",
565
- text: `Unknown model "${requested}" for pixeldojo:character. Allowed: ${ALLOWED.join(", ")}.`
708
+ text: `Unknown model "${requested}" for pixeldojo_character. Allowed: ${ALLOWED.join(", ")}.`
566
709
  };
567
710
  }
568
711
  const apiId = requested;
569
712
  if (REQUIRES_REFERENCE.has(apiId) && !input.reference_image_url) {
570
713
  return {
571
714
  kind: "text",
572
- text: `pixeldojo:character with model="${apiId}" requires reference_image_url (the source image of the character to edit). For generation from a prompt alone, use model="${DEFAULT_MODEL3}".`
715
+ text: `pixeldojo_character with model="${apiId}" requires reference_image_url (the source image of the character to edit). For generation from a prompt alone, use model="${DEFAULT_MODEL3}".`
573
716
  };
574
717
  }
575
718
  const body = { prompt: input.prompt };
@@ -631,7 +774,7 @@ var init_character = __esm({
631
774
  required: ["prompt"]
632
775
  };
633
776
  characterSkill = {
634
- name: "pixeldojo:character",
777
+ name: "pixeldojo_character",
635
778
  description: "Generate or edit consistent characters across shots. Pass a reference_image_url for identity locking (seedream-5 by default).",
636
779
  inputZod: InputSchema3,
637
780
  inputJsonSchema: InputJsonSchema3,
@@ -646,7 +789,7 @@ async function run4(input, ctx) {
646
789
  if (!ALLOWED2.includes(requested)) {
647
790
  return {
648
791
  kind: "text",
649
- text: `Unknown model "${requested}" for pixeldojo:storyboard. Allowed: ${ALLOWED2.join(", ")}.`
792
+ text: `Unknown model "${requested}" for pixeldojo_storyboard. Allowed: ${ALLOWED2.join(", ")}.`
650
793
  };
651
794
  }
652
795
  const apiId = requested;
@@ -709,7 +852,7 @@ var init_storyboard = __esm({
709
852
  required: ["brief"]
710
853
  };
711
854
  storyboardSkill = {
712
- name: "pixeldojo:storyboard",
855
+ name: "pixeldojo_storyboard",
713
856
  description: "Generate a video storyboard preview from a single brief. v1: single-shot preview; multi-shot orchestration coming.",
714
857
  inputZod: InputSchema4,
715
858
  inputJsonSchema: InputJsonSchema4,
@@ -724,7 +867,7 @@ async function run5(input, ctx) {
724
867
  if (!ALLOWED3.includes(requested)) {
725
868
  return {
726
869
  kind: "text",
727
- text: `Unknown model "${requested}" for pixeldojo:upscale. Allowed: ${ALLOWED3.join(", ")}.`
870
+ text: `Unknown model "${requested}" for pixeldojo_upscale. Allowed: ${ALLOWED3.join(", ")}.`
728
871
  };
729
872
  }
730
873
  const apiId = requested;
@@ -786,7 +929,7 @@ var init_upscale = __esm({
786
929
  required: ["image_url"]
787
930
  };
788
931
  upscaleSkill = {
789
- name: "pixeldojo:upscale",
932
+ name: "pixeldojo_upscale",
790
933
  description: "Enhance an image. Magnific (default) supports configurable scale_factor and creativity; face-enhance for portraits.",
791
934
  inputZod: InputSchema5,
792
935
  inputJsonSchema: InputJsonSchema5,
@@ -864,14 +1007,14 @@ var InputSchema6, InputJsonSchema6, STUCK_HINT_MS, ABANDON_HINT_MS, statusSkill;
864
1007
  var init_status = __esm({
865
1008
  "src/skills/status.ts"() {
866
1009
  InputSchema6 = zod.z.object({
867
- jobId: zod.z.string().min(1).max(200).describe("Job id returned from a prior pixeldojo:* skill handoff.")
1010
+ jobId: zod.z.string().min(1).max(200).describe("Job id returned from a prior pixeldojo_* skill handoff.")
868
1011
  });
869
1012
  InputJsonSchema6 = {
870
1013
  type: "object",
871
1014
  properties: {
872
1015
  jobId: {
873
1016
  type: "string",
874
- description: "Job id returned from a prior pixeldojo:* skill handoff."
1017
+ description: "Job id returned from a prior pixeldojo_* skill handoff."
875
1018
  }
876
1019
  },
877
1020
  required: ["jobId"]
@@ -879,7 +1022,7 @@ var init_status = __esm({
879
1022
  STUCK_HINT_MS = 5 * 60 * 1e3;
880
1023
  ABANDON_HINT_MS = 12 * 60 * 1e3;
881
1024
  statusSkill = {
882
- name: "pixeldojo:status",
1025
+ name: "pixeldojo_status",
883
1026
  description: "Check the status of a long-running job after a 30s sync handoff. Returns assets when ready, failure reason if failed, or current state if still running.",
884
1027
  inputZod: InputSchema6,
885
1028
  inputJsonSchema: InputJsonSchema6,
@@ -924,8 +1067,8 @@ var init_from_url = __esm({
924
1067
  required: ["url"]
925
1068
  };
926
1069
  fromUrlSkill = {
927
- name: "pixeldojo:from_url",
928
- description: "Extract product info (name, description, images) from any product page URL. Use this as the first step before pixeldojo:campaign / :ad / :lookbook so the agent has structured product data instead of a free-text description.",
1070
+ name: "pixeldojo_from_url",
1071
+ description: "Extract product info (name, description, images) from any product page URL. Use this as the first step before pixeldojo_campaign / :ad / :lookbook so the agent has structured product data instead of a free-text description.",
929
1072
  inputZod: InputSchema7,
930
1073
  inputJsonSchema: InputJsonSchema7,
931
1074
  // No model dispatch — this skill calls a server-side scrape, not a generation.
@@ -935,7 +1078,259 @@ var init_from_url = __esm({
935
1078
  };
936
1079
  }
937
1080
  });
1081
+ async function filmApi(ctx, method, path, body) {
1082
+ const res = await fetch(`${ctx.baseUrl}${path}`, {
1083
+ method,
1084
+ headers: {
1085
+ Authorization: `Bearer ${ctx.apiKey}`,
1086
+ "Content-Type": "application/json",
1087
+ "x-pixeldojo-source": `mcp/${ctx.mcpVersion}`,
1088
+ "x-pixeldojo-skill": "film"
1089
+ },
1090
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
1091
+ });
1092
+ const data = await res.json().catch(() => ({}));
1093
+ return { status: res.status, data };
1094
+ }
1095
+ function progressLine(f) {
1096
+ const all = shots(f);
1097
+ const frames = all.filter((sh) => ["frame-ready", "approved", "clip-generating", "clip-ready"].includes(sh.status) || sh.frameUrl).length;
1098
+ const clips = all.filter((sh) => sh.status === "clip-ready").length;
1099
+ const rendering = all.filter((sh) => sh.status === "clip-generating").length;
1100
+ return `frames ${frames}/${all.length}, clips ${clips}/${all.length}${rendering > 0 ? ` (${rendering} rendering)` : ""}`;
1101
+ }
1102
+ async function plan(input, ctx) {
1103
+ if (!input.idea?.trim() && !input.script?.trim()) {
1104
+ return { kind: "text", text: "Provide an `idea` (we write the script, free) or a full `script` to plan a film." };
1105
+ }
1106
+ const created = await filmApi(ctx, "POST", "/api/films", {
1107
+ aspect: input.aspect,
1108
+ look: input.look,
1109
+ lengthTarget: input.length,
1110
+ assetIds: []
1111
+ });
1112
+ if (!created.data.film) return { kind: "text", text: `Could not create the film: ${created.data.error ?? created.status}` };
1113
+ const id = created.data.film.id;
1114
+ let script = input.script?.trim() ?? "";
1115
+ if (!script) {
1116
+ const wrote = await filmApi(ctx, "POST", `/api/films/${id}/script`, { idea: input.idea.trim() });
1117
+ script = wrote.data.film?.script ?? "";
1118
+ if (!script) return { kind: "text", text: `Script writing failed: ${wrote.data.error ?? wrote.status}` };
1119
+ }
1120
+ const planned = await filmApi(ctx, "POST", `/api/films/${id}/plan`, { script });
1121
+ const film = planned.data.film;
1122
+ if (!film) return { kind: "text", text: `Shot planning failed: ${planned.data.error ?? planned.status}` };
1123
+ const total = shots(film).length;
1124
+ const sceneLines = film.scenes.map((s, i) => ` Scene ${i + 1} "${s.title}"${s.location ? ` [${s.location}]` : ""}: ${s.shots.length} shots`);
1125
+ return {
1126
+ kind: "text",
1127
+ text: [
1128
+ `Planned "${film.title}" (film_id: ${film.id}) \u2014 ${film.scenes.length} scenes, ${total} shots, ${input.look}, ${input.aspect}.`,
1129
+ ...sceneLines,
1130
+ "",
1131
+ `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.`,
1132
+ `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.`
1133
+ ].join("\n")
1134
+ };
1135
+ }
1136
+ async function produce(filmId, ctx) {
1137
+ const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}`);
1138
+ if (!data.film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
1139
+ const pending = shots(data.film).filter((sh) => sh.status === "idle" || sh.status === "failed" && !sh.frameUrl);
1140
+ let fired = 0;
1141
+ for (const sh of pending.slice(0, 6)) {
1142
+ const r = await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
1143
+ if (r.status === 200) fired += 1;
1144
+ }
1145
+ return {
1146
+ kind: "text",
1147
+ 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.`
1148
+ };
1149
+ }
1150
+ function buildScorePlanBody(film) {
1151
+ const scenes = film.scenes.filter((s) => s.shots.length > 0);
1152
+ const sections = scenes.flatMap((scene, i) => {
1153
+ const runtime = Math.max(3e3, scene.shots.reduce((n, sh) => n + (sh.durationSec || 0), 0) * 1e3);
1154
+ const styles = i === 0 ? ARC.opening : i === scenes.length - 1 ? ARC.finale : ARC.middle;
1155
+ const parts = Math.max(1, Math.ceil(runtime / 12e4));
1156
+ const partMs = Math.round(runtime / parts);
1157
+ return Array.from({ length: parts }, (_, part) => ({
1158
+ section_name: parts > 1 ? `${scene.title} (part ${part + 1})` : scene.title,
1159
+ positive_local_styles: styles,
1160
+ negative_local_styles: [],
1161
+ duration_ms: part === parts - 1 ? runtime - partMs * (parts - 1) : partMs,
1162
+ lines: []
1163
+ }));
1164
+ });
1165
+ return {
1166
+ composition_plan: {
1167
+ positive_global_styles: (LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]).split(", "),
1168
+ negative_global_styles: ["vocals", "singing", "lyrics"],
1169
+ sections
1170
+ },
1171
+ respect_sections_durations: true,
1172
+ output_format: "mp3_standard"
1173
+ };
1174
+ }
1175
+ function buildFlatMusicBody(film, all) {
1176
+ const runtimeSec = all.reduce((n, sh) => n + (sh.durationSec || 0), 0);
1177
+ const ms = Math.min(6e5, Math.max(5e3, Math.ceil(runtimeSec * 1e3 / 5e3) * 5e3));
1178
+ return {
1179
+ prompt: `${LOOK_MUSIC[film.look] ?? LOOK_MUSIC["live-action"]}. Instrumental only, no vocals. One consistent theme, loopable as a background bed under dialogue.`,
1180
+ music_length_ms: ms,
1181
+ force_instrumental: true,
1182
+ output_format: "mp3_standard"
1183
+ };
1184
+ }
1185
+ async function advance(filmId, soundtrack, musicJobId, ctx) {
1186
+ const { data } = await filmApi(ctx, "GET", `/api/films/${filmId}?refresh=1`);
1187
+ const film = data.film;
1188
+ if (!film) return { kind: "text", text: `Film not found: ${data.error ?? filmId}` };
1189
+ const all = shots(film);
1190
+ if (film.status === "exported" && film.exportUrl) {
1191
+ return {
1192
+ kind: "text",
1193
+ text: `Film finished: "${film.title}"
1194
+ Watch/download: ${film.exportUrl}
1195
+ Workspace: ${ctx.baseUrl}/film-studio/${film.id}
1196
+ It is saved in the Library folder "Film: ${film.title}".`
1197
+ };
1198
+ }
1199
+ if (film.exportError) {
1200
+ return { kind: "text", text: `Export failed: ${film.exportError} Poll again with action "status" to retry.` };
1201
+ }
1202
+ const idleFrames = all.filter((sh) => sh.status === "idle");
1203
+ for (const sh of idleFrames.slice(0, 6)) {
1204
+ await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "frame" });
1205
+ }
1206
+ const framesDone = all.every((sh) => ["frame-ready", "clip-generating", "clip-ready"].includes(sh.status));
1207
+ if (framesDone && all.some((sh) => sh.status === "frame-ready" && !sh.approved)) {
1208
+ await filmApi(ctx, "PATCH", `/api/films/${filmId}`, {
1209
+ scenes: film.scenes.map((s) => ({
1210
+ id: s.id,
1211
+ title: s.title,
1212
+ location: s.location,
1213
+ shots: s.shots.map((sh) => ({
1214
+ id: sh.id,
1215
+ prompt: sh.prompt,
1216
+ characterIds: sh.characterIds ?? [],
1217
+ elementIds: sh.elementIds ?? [],
1218
+ engine: sh.engine,
1219
+ cameraPreset: sh.cameraPreset,
1220
+ approved: true,
1221
+ durationSec: sh.durationSec
1222
+ }))
1223
+ }))
1224
+ });
1225
+ }
1226
+ const inFlight = count(film, (sh) => sh.status === "clip-generating");
1227
+ const clipQueue = all.filter((sh) => (sh.status === "frame-ready" || sh.status === "failed" && Boolean(sh.frameUrl)) && framesDone);
1228
+ for (const sh of clipQueue.slice(0, Math.max(0, 4 - inFlight))) {
1229
+ await filmApi(ctx, "POST", `/api/films/${filmId}/shots/${sh.id}/generate`, { kind: "clip" });
1230
+ }
1231
+ const clipsDone = all.length > 0 && all.every((sh) => sh.status === "clip-ready");
1232
+ if (clipsDone) {
1233
+ const hasMusic = film.audioStems.some((st) => st.kind === "music");
1234
+ if (soundtrack !== "none" && !hasMusic) {
1235
+ if (musicJobId) {
1236
+ const job = await ctx.client.getJob(musicJobId);
1237
+ const out = job.output;
1238
+ const outUrl = out?.audio ?? out?.video ?? out?.images?.[0];
1239
+ if (job.status === "completed" && typeof outUrl === "string") {
1240
+ await filmApi(ctx, "PATCH", `/api/films/${filmId}`, { audioStems: [{ kind: "music", url: outUrl }] });
1241
+ await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
1242
+ return { kind: "text", text: `Score attached \u2014 export started. Poll again in ~60s with {"film_id": "${filmId}", "action": "status"} for the finished film URL.` };
1243
+ }
1244
+ if (job.status === "failed") {
1245
+ 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.` };
1246
+ }
1247
+ return { kind: "text", text: `Score still composing. Poll again in ~20s with {"film_id": "${filmId}", "action": "status", "music_job_id": "${musicJobId}"}.` };
1248
+ }
1249
+ const body = soundtrack === "scene-synced" ? buildScorePlanBody(film) : buildFlatMusicBody(film, all);
1250
+ const dispatch = await ctx.client.run("text-to-music", body);
1251
+ 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}"}.` };
1252
+ }
1253
+ await filmApi(ctx, "POST", `/api/films/${filmId}/export`, {});
1254
+ return { kind: "text", text: `All clips ready \u2014 export started (${progressLine(film)}). Poll again in ~60s with action "status" for the finished film URL.` };
1255
+ }
1256
+ const failed = all.filter((sh) => sh.status === "failed");
1257
+ return {
1258
+ kind: "text",
1259
+ text: [
1260
+ `In progress: ${progressLine(film)}.`,
1261
+ failed.length > 0 ? `${failed.length} shot(s) failed and were re-queued (${failed[0]?.errorMessage?.slice(0, 100) ?? ""}).` : "",
1262
+ `Poll again in ~30s with {"film_id": "${filmId}", "action": "status"}.`
1263
+ ].filter(Boolean).join("\n")
1264
+ };
1265
+ }
938
1266
  async function run8(input, ctx) {
1267
+ const action = input.action === "plan" && input.film_id ? "status" : input.action;
1268
+ if (action === "plan") return plan(input, ctx);
1269
+ if (!input.film_id) return { kind: "text", text: "produce/status need the film_id returned by the plan step." };
1270
+ if (action === "produce") return produce(input.film_id, ctx);
1271
+ return advance(input.film_id, input.soundtrack, input.music_job_id, ctx);
1272
+ }
1273
+ var LOOKS, InputSchema8, InputJsonSchema8, shots, count, LOOK_MUSIC, ARC, filmSkill;
1274
+ var init_film = __esm({
1275
+ "src/skills/film.ts"() {
1276
+ LOOKS = ["live-action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western", "romance", "romantic-comedy", "comedy"];
1277
+ InputSchema8 = zod.z.object({
1278
+ 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."),
1279
+ script: zod.z.string().max(2e4).optional().describe("A full screenplay to plan instead of an idea (sluglines, action lines, dialogue)."),
1280
+ look: zod.z.enum(LOOKS).default("live-action").describe("Visual style carried through frames, clips, and soundtrack."),
1281
+ aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
1282
+ length: zod.z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length; drives the shot count (~6/12/24/60 shots)."),
1283
+ film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status."),
1284
+ 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."),
1285
+ 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."),
1286
+ 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.")
1287
+ });
1288
+ InputJsonSchema8 = {
1289
+ type: "object",
1290
+ properties: {
1291
+ idea: { type: "string", description: "One-to-three sentence film idea. The studio writes the screenplay (free)." },
1292
+ script: { type: "string", description: "A full screenplay to plan instead of an idea." },
1293
+ look: { type: "string", enum: [...LOOKS], description: "Visual style. Default live-action." },
1294
+ aspect: { type: "string", enum: ["16:9", "9:16"], description: "Default 16:9." },
1295
+ length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Target length (~6/12/24/60 shots). Default 60s." },
1296
+ film_id: { type: "string", description: "Existing film id for produce/status." },
1297
+ action: { type: "string", enum: ["plan", "produce", "status"], description: "plan (free) | produce (charged) | status (poll every ~30s)." },
1298
+ soundtrack: { type: "string", enum: ["scene-synced", "flat", "none"], description: "Default scene-synced." },
1299
+ music_job_id: { type: "string", description: "Pass back from a previous status reply while the score is composing." }
1300
+ }
1301
+ };
1302
+ shots = (f) => f.scenes.flatMap((s) => s.shots);
1303
+ count = (f, pred) => shots(f).filter(pred).length;
1304
+ LOOK_MUSIC = {
1305
+ "live-action": "modern cinematic underscore, warm strings and soft felt piano, subtle rhythmic pulse, slow emotional build",
1306
+ anime: "sweeping anime film score, lush strings and piano, taiko drum accents, soaring heartfelt theme",
1307
+ "3d-animation": "playful heartfelt family-animation score, pizzicato strings, celesta and warm woodwinds, gentle sense of wonder",
1308
+ noir: "smoky film-noir jazz underscore, brushed drums, upright bass, distant muted trumpet, slow simmering tension",
1309
+ horror: "sparse horror underscore, low cello drones, dissonant string swells, creeping heartbeat pulse, unsettling quiet",
1310
+ "sci-fi": "atmospheric science-fiction score, analog synth pads, deep sub-bass pulses, shimmering arpeggios, vast and cold",
1311
+ documentary: "understated documentary score, felt piano, soft string beds, organic percussion textures, reflective and true",
1312
+ western: "cinematic western score, lonely twangy guitar, dusty strings, distant horns, wide-open desert tension",
1313
+ romance: "tender romantic score, felt piano and warm strings, soft acoustic guitar, intimate and unhurried, gently swelling",
1314
+ "romantic-comedy": "bright romantic-comedy score, plucky ukulele and handclaps, warm brushed drums, sunny and flirtatious",
1315
+ comedy: "playful comedy score, bouncy pizzicato strings, cheeky woodwinds, light percussion hits punctuating the gags"
1316
+ };
1317
+ ARC = {
1318
+ opening: ["establishing the theme", "restrained", "quiet anticipation"],
1319
+ middle: ["building tension", "rising intensity", "forward motion"],
1320
+ finale: ["climactic", "full arrangement", "emotional peak resolving to stillness"]
1321
+ };
1322
+ filmSkill = {
1323
+ name: "pixeldojo_film",
1324
+ 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).",
1325
+ inputZod: InputSchema8,
1326
+ inputJsonSchema: InputJsonSchema8,
1327
+ allowedModels: [],
1328
+ defaultModel: "",
1329
+ run: run8
1330
+ };
1331
+ }
1332
+ });
1333
+ async function run9(input, ctx) {
939
1334
  const productImages = [
940
1335
  ...input.product_image_urls ?? [],
941
1336
  ...await Promise.all((input.product_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
@@ -978,13 +1373,13 @@ async function run8(input, ctx) {
978
1373
  (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
1374
  );
980
1375
  }
981
- var InputSchema8, InputJsonSchema8, adSkill;
1376
+ var InputSchema9, InputJsonSchema9, adSkill;
982
1377
  var init_ad = __esm({
983
1378
  "src/skills/ad.ts"() {
984
1379
  init_types();
985
1380
  init_poll();
986
1381
  init_upload_helpers();
987
- InputSchema8 = zod.z.object({
1382
+ InputSchema9 = zod.z.object({
988
1383
  style: zod.z.string().max(80).optional().describe(
989
1384
  "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
1385
  ),
@@ -1010,7 +1405,7 @@ var init_ad = __esm({
1010
1405
  path: ["product_url"]
1011
1406
  }
1012
1407
  );
1013
- InputJsonSchema8 = {
1408
+ InputJsonSchema9 = {
1014
1409
  type: "object",
1015
1410
  properties: {
1016
1411
  style: {
@@ -1064,18 +1459,18 @@ var init_ad = __esm({
1064
1459
  // Zod refine; no root-level oneOf (Grok rejects non-object root unions).
1065
1460
  };
1066
1461
  adSkill = {
1067
- name: "pixeldojo:ad",
1068
- 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,
1462
+ name: "pixeldojo_ad",
1463
+ 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.",
1464
+ inputZod: InputSchema9,
1465
+ inputJsonSchema: InputJsonSchema9,
1071
1466
  // Composition skill — the server routes to the ad engine internally.
1072
1467
  allowedModels: [],
1073
1468
  defaultModel: "",
1074
- run: run8
1469
+ run: run9
1075
1470
  };
1076
1471
  }
1077
1472
  });
1078
- async function run9(input, ctx) {
1473
+ async function run10(input, ctx) {
1079
1474
  const client = ctx.client;
1080
1475
  const result = await client.runCampaign(input);
1081
1476
  if (result.kind === "campaign") {
@@ -1097,15 +1492,15 @@ async function run9(input, ctx) {
1097
1492
  }
1098
1493
  return {
1099
1494
  kind: "handoff",
1100
- text: result.message ?? `Campaign ${result.campaignId} queued. Poll via pixeldojo:campaign_status.`,
1495
+ text: result.message ?? `Campaign ${result.campaignId} queued. Poll via pixeldojo_campaign_status.`,
1101
1496
  jobId: result.campaignId,
1102
1497
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
1103
1498
  };
1104
1499
  }
1105
- var InputSchema9, InputJsonSchema9, campaignSkill;
1500
+ var InputSchema10, InputJsonSchema10, campaignSkill;
1106
1501
  var init_campaign = __esm({
1107
1502
  "src/skills/campaign.ts"() {
1108
- InputSchema9 = zod.z.object({
1503
+ InputSchema10 = zod.z.object({
1109
1504
  productUrl: zod.z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
1110
1505
  product: zod.z.object({
1111
1506
  name: zod.z.string().min(1).max(200),
@@ -1121,7 +1516,7 @@ var init_campaign = __esm({
1121
1516
  message: "productUrl or product is required",
1122
1517
  path: ["productUrl"]
1123
1518
  });
1124
- InputJsonSchema9 = {
1519
+ InputJsonSchema10 = {
1125
1520
  type: "object",
1126
1521
  properties: {
1127
1522
  productUrl: {
@@ -1150,19 +1545,19 @@ var init_campaign = __esm({
1150
1545
  oneOf: [{ required: ["productUrl"] }, { required: ["product"] }]
1151
1546
  };
1152
1547
  campaignSkill = {
1153
- name: "pixeldojo:campaign",
1154
- 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,
1548
+ name: "pixeldojo_campaign",
1549
+ 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.",
1550
+ inputZod: InputSchema10,
1551
+ inputJsonSchema: InputJsonSchema10,
1157
1552
  // Composition skill — internal sub-jobs route to per-tool models, no
1158
1553
  // single allowed-models list applies here.
1159
1554
  allowedModels: [],
1160
1555
  defaultModel: "",
1161
- run: run9
1556
+ run: run10
1162
1557
  };
1163
1558
  }
1164
1559
  });
1165
- async function run10(input, ctx) {
1560
+ async function run11(input, ctx) {
1166
1561
  const client = ctx.client;
1167
1562
  const result = await client.getCampaign(input.campaignId);
1168
1563
  if (result.kind === "campaign") {
@@ -1192,40 +1587,40 @@ async function run10(input, ctx) {
1192
1587
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
1193
1588
  };
1194
1589
  }
1195
- var InputSchema10, InputJsonSchema10, campaignStatusSkill;
1590
+ var InputSchema11, InputJsonSchema11, campaignStatusSkill;
1196
1591
  var init_campaign_status = __esm({
1197
1592
  "src/skills/campaign-status.ts"() {
1198
- InputSchema10 = zod.z.object({
1199
- campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
1593
+ InputSchema11 = zod.z.object({
1594
+ campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo_campaign handoff.")
1200
1595
  });
1201
- InputJsonSchema10 = {
1596
+ InputJsonSchema11 = {
1202
1597
  type: "object",
1203
1598
  properties: {
1204
1599
  campaignId: {
1205
1600
  type: "string",
1206
- description: "Campaign id returned from a prior pixeldojo:campaign handoff."
1601
+ description: "Campaign id returned from a prior pixeldojo_campaign handoff."
1207
1602
  }
1208
1603
  },
1209
1604
  required: ["campaignId"]
1210
1605
  };
1211
1606
  campaignStatusSkill = {
1212
- name: "pixeldojo:campaign_status",
1213
- 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,
1607
+ name: "pixeldojo_campaign_status",
1608
+ 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.",
1609
+ inputZod: InputSchema11,
1610
+ inputJsonSchema: InputJsonSchema11,
1216
1611
  allowedModels: [],
1217
1612
  defaultModel: "",
1218
- run: run10
1613
+ run: run11
1219
1614
  };
1220
1615
  }
1221
1616
  });
1222
- async function run11(input, ctx) {
1617
+ async function run12(input, ctx) {
1223
1618
  let url;
1224
1619
  try {
1225
1620
  url = await uploadLocalFile(input.path, ctx.client);
1226
1621
  } catch (err) {
1227
1622
  if (err instanceof SkillUploadError) {
1228
- return { kind: "text", text: `pixeldojo:upload: ${err.userMessage}` };
1623
+ return { kind: "text", text: `pixeldojo_upload: ${err.userMessage}` };
1229
1624
  }
1230
1625
  throw err;
1231
1626
  }
@@ -1235,16 +1630,16 @@ async function run11(input, ctx) {
1235
1630
  (temp storage; auto-deletes in ~24h \u2014 pass this URL as image_url to :generate or :edit)`
1236
1631
  };
1237
1632
  }
1238
- var InputSchema11, InputJsonSchema11, uploadSkill;
1633
+ var InputSchema12, InputJsonSchema12, uploadSkill;
1239
1634
  var init_upload = __esm({
1240
1635
  "src/skills/upload.ts"() {
1241
1636
  init_upload_helpers();
1242
- InputSchema11 = zod.z.object({
1637
+ InputSchema12 = zod.z.object({
1243
1638
  path: zod.z.string().min(1).max(4096).describe(
1244
1639
  "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
1640
  )
1246
1641
  });
1247
- InputJsonSchema11 = {
1642
+ InputJsonSchema12 = {
1248
1643
  type: "object",
1249
1644
  properties: {
1250
1645
  path: {
@@ -1255,22 +1650,22 @@ var init_upload = __esm({
1255
1650
  required: ["path"]
1256
1651
  };
1257
1652
  uploadSkill = {
1258
- name: "pixeldojo:upload",
1653
+ name: "pixeldojo_upload",
1259
1654
  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,
1655
+ inputZod: InputSchema12,
1656
+ inputJsonSchema: InputJsonSchema12,
1262
1657
  allowedModels: [],
1263
1658
  defaultModel: "",
1264
- run: run11
1659
+ run: run12
1265
1660
  };
1266
1661
  }
1267
1662
  });
1268
- async function run12(input, ctx) {
1663
+ async function run13(input, ctx) {
1269
1664
  const requested = input.model ?? DEFAULT_MODEL6;
1270
1665
  if (!ALLOWED4.includes(requested)) {
1271
1666
  return {
1272
1667
  kind: "text",
1273
- text: `Unknown model "${requested}" for pixeldojo:audio. Allowed: ${ALLOWED4.join(", ")}.`
1668
+ text: `Unknown model "${requested}" for pixeldojo_audio. Allowed: ${ALLOWED4.join(", ")}.`
1274
1669
  };
1275
1670
  }
1276
1671
  const apiId = requested;
@@ -1281,7 +1676,7 @@ async function run12(input, ctx) {
1281
1676
  audioUrls = [...audioUrls, ...uploaded].slice(0, 3);
1282
1677
  } catch (err) {
1283
1678
  if (err instanceof SkillUploadError) {
1284
- return { kind: "text", text: `pixeldojo:audio (upload): ${err.userMessage}` };
1679
+ return { kind: "text", text: `pixeldojo_audio (upload): ${err.userMessage}` };
1285
1680
  }
1286
1681
  throw err;
1287
1682
  }
@@ -1314,7 +1709,7 @@ async function run12(input, ctx) {
1314
1709
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
1315
1710
  );
1316
1711
  }
1317
- var ALLOWED4, DEFAULT_MODEL6, InputSchema12, InputJsonSchema12, audioSkill;
1712
+ var ALLOWED4, DEFAULT_MODEL6, InputSchema13, InputJsonSchema13, audioSkill;
1318
1713
  var init_audio = __esm({
1319
1714
  "src/skills/audio.ts"() {
1320
1715
  init_poll();
@@ -1322,7 +1717,7 @@ var init_audio = __esm({
1322
1717
  init_types();
1323
1718
  ALLOWED4 = ["seed-audio", "text-to-speech"];
1324
1719
  DEFAULT_MODEL6 = "seed-audio";
1325
- InputSchema12 = zod.z.object({
1720
+ InputSchema13 = zod.z.object({
1326
1721
  prompt: zod.z.string().min(1).describe(
1327
1722
  "A script for speech, or a scene description for cinematic audio (dialogue + effects + music). Reference uploaded clips as @Audio1..@Audio3."
1328
1723
  ),
@@ -1333,7 +1728,7 @@ var init_audio = __esm({
1333
1728
  output_format: zod.z.string().optional().describe("seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3, wav, flac, pcm."),
1334
1729
  speed: zod.z.number().min(0.5).max(2).optional().describe("Speech speed multiplier (0.5\u20132).")
1335
1730
  });
1336
- InputJsonSchema12 = {
1731
+ InputJsonSchema13 = {
1337
1732
  type: "object",
1338
1733
  properties: {
1339
1734
  prompt: { type: "string" },
@@ -1347,17 +1742,17 @@ var init_audio = __esm({
1347
1742
  required: ["prompt"]
1348
1743
  };
1349
1744
  audioSkill = {
1350
- name: "pixeldojo:audio",
1745
+ name: "pixeldojo_audio",
1351
1746
  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,
1747
+ inputZod: InputSchema13,
1748
+ inputJsonSchema: InputJsonSchema13,
1354
1749
  allowedModels: [...ALLOWED4],
1355
1750
  defaultModel: DEFAULT_MODEL6,
1356
- run: run12
1751
+ run: run13
1357
1752
  };
1358
1753
  }
1359
1754
  });
1360
- async function run13(input, ctx) {
1755
+ async function run14(input, ctx) {
1361
1756
  const res = await ctx.client.listLibrary({
1362
1757
  q: input.q,
1363
1758
  model: input.model,
@@ -1367,7 +1762,7 @@ async function run13(input, ctx) {
1367
1762
  if (res.items.length === 0) {
1368
1763
  return {
1369
1764
  kind: "text",
1370
- text: input.q ? `No library items match "${input.q}". Try a broader query, or omit q to list recent generations.` : "Your library is empty. Generate something with pixeldojo:generate first."
1765
+ text: input.q ? `No library items match "${input.q}". Try a broader query, or omit q to list recent generations.` : "Your library is empty. Generate something with pixeldojo_generate first."
1371
1766
  };
1372
1767
  }
1373
1768
  const lines = res.items.map((it) => {
@@ -1388,16 +1783,16 @@ ${lines.join("\n")}
1388
1783
  Pass any URL back into :generate, :edit, :character, or :upscale as a reference input.`
1389
1784
  };
1390
1785
  }
1391
- var InputSchema13, InputJsonSchema13, librarySkill;
1786
+ var InputSchema14, InputJsonSchema14, librarySkill;
1392
1787
  var init_library = __esm({
1393
1788
  "src/skills/library.ts"() {
1394
- InputSchema13 = zod.z.object({
1789
+ InputSchema14 = zod.z.object({
1395
1790
  q: zod.z.string().optional().describe("Substring match against the prompt that produced the asset."),
1396
1791
  model: zod.z.string().optional().describe("Restrict to a single model (apiId)."),
1397
1792
  modality: zod.z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
1398
1793
  limit: zod.z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
1399
1794
  });
1400
- InputJsonSchema13 = {
1795
+ InputJsonSchema14 = {
1401
1796
  type: "object",
1402
1797
  properties: {
1403
1798
  q: { type: "string" },
@@ -1408,17 +1803,17 @@ var init_library = __esm({
1408
1803
  required: []
1409
1804
  };
1410
1805
  librarySkill = {
1411
- name: "pixeldojo:library",
1412
- 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,
1806
+ name: "pixeldojo_library",
1807
+ 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.",
1808
+ inputZod: InputSchema14,
1809
+ inputJsonSchema: InputJsonSchema14,
1415
1810
  allowedModels: [],
1416
1811
  defaultModel: "",
1417
- run: run13
1812
+ run: run14
1418
1813
  };
1419
1814
  }
1420
1815
  });
1421
- async function run14(input, ctx) {
1816
+ async function run15(input, ctx) {
1422
1817
  const res = await ctx.client.saveMedia({
1423
1818
  url: input.url,
1424
1819
  prompt: input.prompt,
@@ -1429,20 +1824,20 @@ async function run14(input, ctx) {
1429
1824
  return {
1430
1825
  kind: "text",
1431
1826
  text: `Saved to My Media (permanent): ${res.url}
1432
- It will appear at https://pixeldojo.ai/my-images and in pixeldojo:library searches.`
1827
+ It will appear at https://pixeldojo.ai/my-images and in pixeldojo_library searches.`
1433
1828
  };
1434
1829
  }
1435
- var InputSchema14, InputJsonSchema14, saveSkill;
1830
+ var InputSchema15, InputJsonSchema15, saveSkill;
1436
1831
  var init_save = __esm({
1437
1832
  "src/skills/save.ts"() {
1438
- InputSchema14 = zod.z.object({
1833
+ InputSchema15 = zod.z.object({
1439
1834
  url: zod.z.string().url().describe("The asset URL to save (image or video)."),
1440
1835
  prompt: zod.z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
1441
1836
  tool_name: zod.z.string().optional().describe("Tool/model slug that produced it."),
1442
1837
  modality: zod.z.enum(["image", "video", "audio"]).optional(),
1443
1838
  job_id: zod.z.string().optional().describe("The jobId that produced this asset, when known.")
1444
1839
  });
1445
- InputJsonSchema14 = {
1840
+ InputJsonSchema15 = {
1446
1841
  type: "object",
1447
1842
  properties: {
1448
1843
  url: { type: "string", format: "uri" },
@@ -1454,13 +1849,13 @@ var init_save = __esm({
1454
1849
  required: ["url"]
1455
1850
  };
1456
1851
  saveSkill = {
1457
- name: "pixeldojo:save",
1852
+ name: "pixeldojo_save",
1458
1853
  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,
1854
+ inputZod: InputSchema15,
1855
+ inputJsonSchema: InputJsonSchema15,
1461
1856
  allowedModels: [],
1462
1857
  defaultModel: "",
1463
- run: run14
1858
+ run: run15
1464
1859
  };
1465
1860
  }
1466
1861
  });
@@ -1502,7 +1897,7 @@ async function runSave(input, ctx) {
1502
1897
  });
1503
1898
  return {
1504
1899
  kind: "text",
1505
- text: res.created ? `Saved workflow "${res.name}" (${res.steps} step${res.steps === 1 ? "" : "s"}). Run it with pixeldojo:run_workflow { name: "${res.name}" }.` : `Overwrote workflow "${res.name}" (${res.steps} step${res.steps === 1 ? "" : "s"}).`
1900
+ text: res.created ? `Saved workflow "${res.name}" (${res.steps} step${res.steps === 1 ? "" : "s"}). Run it with pixeldojo_run_workflow { name: "${res.name}" }.` : `Overwrote workflow "${res.name}" (${res.steps} step${res.steps === 1 ? "" : "s"}).`
1506
1901
  };
1507
1902
  }
1508
1903
  async function runList(_input, ctx) {
@@ -1510,7 +1905,7 @@ async function runList(_input, ctx) {
1510
1905
  if (res.count === 0) {
1511
1906
  return {
1512
1907
  kind: "text",
1513
- text: "No saved workflows yet. Save one with pixeldojo:save_workflow { name, steps: [{ skill, args }] }."
1908
+ text: "No saved workflows yet. Save one with pixeldojo_save_workflow { name, steps: [{ skill, args }] }."
1514
1909
  };
1515
1910
  }
1516
1911
  const lines = res.workflows.map(
@@ -1579,7 +1974,7 @@ async function runWorkflow(input, ctx) {
1579
1974
  } else if (result.kind === "handoff") {
1580
1975
  return {
1581
1976
  kind: "text",
1582
- text: `Workflow "${workflow.name}" is still on step ${i + 1} (${step.skill}) after an extended wait. Poll with pixeldojo:status { jobId: "${result.jobId}" }, then re-run the workflow once it completes.`
1977
+ text: `Workflow "${workflow.name}" is still on step ${i + 1} (${step.skill}) after an extended wait. Poll with pixeldojo_status { jobId: "${result.jobId}" }, then re-run the workflow once it completes.`
1583
1978
  };
1584
1979
  } else {
1585
1980
  outputs.push(`step ${i + 1} (${step.skill}): done (no asset URL)`);
@@ -1598,7 +1993,7 @@ var init_workflows = __esm({
1598
1993
  init_types();
1599
1994
  WORKFLOW_STEP_EXTRA_BUDGET_MS = 6e4;
1600
1995
  StepSchema = zod.z.object({
1601
- skill: zod.z.string().regex(/^pixeldojo:(generate|edit|character|storyboard|upscale|audio|upload|from_url)$/, {
1996
+ skill: zod.z.string().regex(/^pixeldojo[:_](generate|edit|character|storyboard|upscale|audio|upload|from_url)$/, {
1602
1997
  message: "Runnable skills only: generate, edit, character, storyboard, upscale, audio, upload, from_url."
1603
1998
  }),
1604
1999
  args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
@@ -1624,7 +2019,7 @@ var init_workflows = __esm({
1624
2019
  required: ["skill"]
1625
2020
  };
1626
2021
  saveWorkflowSkill = {
1627
- name: "pixeldojo:save_workflow",
2022
+ name: "pixeldojo_save_workflow",
1628
2023
  description: "Save a named multi-step skill chain (generate \u2192 edit \u2192 upscale \u2026). String args may contain {{input.<key>}} and {{prev}} placeholders. Saving to an existing name overwrites it.",
1629
2024
  inputZod: SaveInputSchema,
1630
2025
  inputJsonSchema: {
@@ -1641,7 +2036,7 @@ var init_workflows = __esm({
1641
2036
  run: runSave
1642
2037
  };
1643
2038
  runWorkflowSkill = {
1644
- name: "pixeldojo:run_workflow",
2039
+ name: "pixeldojo_run_workflow",
1645
2040
  description: "Run a saved workflow by name. Pass inputs for its {{input.<key>}} placeholders; each step feeds the next via {{prev}}.",
1646
2041
  inputZod: RunInputSchema,
1647
2042
  inputJsonSchema: {
@@ -1657,7 +2052,7 @@ var init_workflows = __esm({
1657
2052
  run: runWorkflow
1658
2053
  };
1659
2054
  listWorkflowsSkill = {
1660
- name: "pixeldojo:list_workflows",
2055
+ name: "pixeldojo_list_workflows",
1661
2056
  description: "List your saved workflows with step counts and run totals.",
1662
2057
  inputZod: ListInputSchema,
1663
2058
  inputJsonSchema: { type: "object", properties: {}, required: [] },
@@ -1678,6 +2073,7 @@ __export(skills_exports, {
1678
2073
  campaignStatusSkill: () => campaignStatusSkill,
1679
2074
  characterSkill: () => characterSkill,
1680
2075
  editSkill: () => editSkill,
2076
+ filmSkill: () => filmSkill,
1681
2077
  findSkill: () => findSkill,
1682
2078
  fromUrlSkill: () => fromUrlSkill,
1683
2079
  generateSkill: () => generateSkill,
@@ -1692,7 +2088,8 @@ __export(skills_exports, {
1692
2088
  upscaleSkill: () => upscaleSkill
1693
2089
  });
1694
2090
  function findSkill(name) {
1695
- return exports.ALL_SKILLS.find((s) => s.name === name);
2091
+ const wanted = name.replace(/^pixeldojo:/, "pixeldojo_");
2092
+ return exports.ALL_SKILLS.find((s) => s.name === wanted);
1696
2093
  }
1697
2094
  exports.ALL_SKILLS = void 0;
1698
2095
  var init_skills = __esm({
@@ -1704,6 +2101,7 @@ var init_skills = __esm({
1704
2101
  init_upscale();
1705
2102
  init_status();
1706
2103
  init_from_url();
2104
+ init_film();
1707
2105
  init_ad();
1708
2106
  init_campaign();
1709
2107
  init_campaign_status();
@@ -1729,7 +2127,8 @@ var init_skills = __esm({
1729
2127
  uploadSkill,
1730
2128
  adSkill,
1731
2129
  campaignSkill,
1732
- campaignStatusSkill
2130
+ campaignStatusSkill,
2131
+ filmSkill
1733
2132
  ];
1734
2133
  }
1735
2134
  });
@@ -1784,7 +2183,7 @@ function makeRedactedLogger(target = console) {
1784
2183
  }
1785
2184
 
1786
2185
  // src/server.ts
1787
- var PACKAGE_VERSION = "0.8.0" ;
2186
+ var PACKAGE_VERSION = "0.9.1" ;
1788
2187
  function buildServer(options) {
1789
2188
  if (!options.apiKey) {
1790
2189
  throw new Error(
@@ -1851,6 +2250,8 @@ function buildServer(options) {
1851
2250
  });
1852
2251
  const ctx = {
1853
2252
  client,
2253
+ apiKey: options.apiKey,
2254
+ baseUrl,
1854
2255
  mcpVersion: version,
1855
2256
  logger,
1856
2257
  buildStatusUrl: (jobId) => `${baseUrl}/api/v1/jobs/${encodeURIComponent(jobId)}`