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