@pixeldojo/mcp 0.5.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -181,6 +181,85 @@ var init_upload_helpers = __esm({
181
181
  };
182
182
  }
183
183
  });
184
+
185
+ // src/skills/schema-snap.ts
186
+ function getFieldConstraints(apiId, client) {
187
+ const cached = schemaCache.get(apiId);
188
+ if (cached) return cached;
189
+ const pending = client.getModelSchema(apiId).then(
190
+ (res) => res.schema?.properties ?? {}
191
+ ).catch(() => {
192
+ schemaCache.delete(apiId);
193
+ return null;
194
+ });
195
+ schemaCache.set(apiId, pending);
196
+ return pending;
197
+ }
198
+ function parseAspectRatio(ar) {
199
+ const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(ar);
200
+ if (!m) return null;
201
+ const w = Number(m[1]);
202
+ const h = Number(m[2]);
203
+ return h > 0 ? w / h : null;
204
+ }
205
+ function nearestAspectFromEnum(requested, allowed) {
206
+ if (allowed.includes(requested)) return requested;
207
+ const target = parseAspectRatio(requested);
208
+ if (target === null) return requested;
209
+ let best = requested;
210
+ let bestDiff = Infinity;
211
+ for (const candidate of allowed) {
212
+ const r = parseAspectRatio(candidate);
213
+ if (r === null) continue;
214
+ const diff = Math.abs(r - target);
215
+ if (diff < bestDiff) {
216
+ bestDiff = diff;
217
+ best = candidate;
218
+ }
219
+ }
220
+ return best;
221
+ }
222
+ async function snapAspectRatio(apiId, requested, client, opts = {}) {
223
+ if (!requested) return void 0;
224
+ const props = await getFieldConstraints(apiId, client);
225
+ if (props === null) {
226
+ return opts.onUnknown === "omit" ? void 0 : requested;
227
+ }
228
+ const field = props.aspect_ratio;
229
+ if (!field) return void 0;
230
+ if (!field.enum || field.enum.length === 0) return requested;
231
+ return nearestAspectFromEnum(requested, field.enum);
232
+ }
233
+ async function snapDuration(apiId, requested, client, opts = {}) {
234
+ if (requested === void 0) return void 0;
235
+ const props = await getFieldConstraints(apiId, client);
236
+ if (props === null) {
237
+ return opts.onUnknown === "omit" ? void 0 : requested;
238
+ }
239
+ const field = props.duration;
240
+ if (!field) return void 0;
241
+ if (field.enum && field.enum.length > 0) {
242
+ const allowed = field.enum.map(Number).filter((n) => Number.isFinite(n));
243
+ if (allowed.length === 0) return requested;
244
+ if (allowed.includes(requested)) return requested;
245
+ return allowed.reduce(
246
+ (best, candidate) => Math.abs(candidate - requested) < Math.abs(best - requested) ? candidate : best
247
+ );
248
+ }
249
+ if (typeof field.minimum === "number" || typeof field.maximum === "number") {
250
+ let v = requested;
251
+ if (typeof field.minimum === "number") v = Math.max(field.minimum, v);
252
+ if (typeof field.maximum === "number") v = Math.min(field.maximum, v);
253
+ return v;
254
+ }
255
+ return requested;
256
+ }
257
+ var schemaCache;
258
+ var init_schema_snap = __esm({
259
+ "src/skills/schema-snap.ts"() {
260
+ schemaCache = /* @__PURE__ */ new Map();
261
+ }
262
+ });
184
263
  function buildPayload(apiId, input) {
185
264
  const body = { prompt: input.prompt };
186
265
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
@@ -233,6 +312,13 @@ async function run(input, ctx) {
233
312
  };
234
313
  }
235
314
  }
315
+ if (!VIDEO_INPUT_MODELS.has(apiId)) {
316
+ const [aspect_ratio, duration] = await Promise.all([
317
+ snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
318
+ snapDuration(apiId, resolved.duration, ctx.client)
319
+ ]);
320
+ resolved = { ...resolved, aspect_ratio, duration };
321
+ }
236
322
  const body = buildPayload(apiId, resolved);
237
323
  const submitted = await ctx.client.run(apiId, body);
238
324
  const result = await pollJobUntilDoneOrHandoff(
@@ -249,16 +335,17 @@ async function run(input, ctx) {
249
335
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Generated. Asset: ${urls[0]}` : `Generated ${urls.length} assets. URLs: ${urls.join(", ")}`
250
336
  );
251
337
  }
252
- var InputSchema, DEFAULT_MODEL, InputJsonSchema, generateSkill;
338
+ var InputSchema, DEFAULT_MODEL, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
253
339
  var init_generate = __esm({
254
340
  "src/skills/generate.ts"() {
255
341
  init_poll();
256
342
  init_types();
257
343
  init_upload_helpers();
344
+ init_schema_snap();
258
345
  InputSchema = z.object({
259
346
  prompt: z.string().min(1).max(8e3).describe("Prompt describing what to generate."),
260
347
  model: z.string().optional().describe(
261
- 'Override the model. Any apiId from /api/v1/models (e.g. "flux", "google-nano-banana", "veo-3.1", "kling-video"). Omit to use the default.'
348
+ 'Override the model. Any apiId from /api/v1/models (e.g. "flux", "google-nano-banana", "veo-3.1-fast", "kling-video-v3-standard-text"). Omit to use the default.'
262
349
  ),
263
350
  preset: z.string().optional().describe(
264
351
  'Preset slug to seed the params (e.g. "cinematic-portrait", "product-floating-clean", "magazine-cover-text"). Browse the catalog at https://pixeldojo.ai/presets. Explicit fields override preset values.'
@@ -271,13 +358,14 @@ var init_generate = __esm({
271
358
  )
272
359
  });
273
360
  DEFAULT_MODEL = "google-nano-banana";
361
+ VIDEO_INPUT_MODELS = /* @__PURE__ */ new Set(["grok-video-extend", "wan-video-edit", "seedance-video-edit"]);
274
362
  InputJsonSchema = {
275
363
  type: "object",
276
364
  properties: {
277
365
  prompt: { type: "string", description: "Prompt describing what to generate." },
278
366
  model: {
279
367
  type: "string",
280
- description: 'Override the model. Any apiId from /api/v1/models (e.g. "flux", "google-nano-banana", "veo-3.1", "kling-video"). Omit to use the default.'
368
+ description: 'Override the model. Any apiId from /api/v1/models (e.g. "flux", "google-nano-banana", "veo-3.1-fast", "kling-video-v3-standard-text"). Omit to use the default.'
281
369
  },
282
370
  preset: {
283
371
  type: "string",
@@ -368,6 +456,10 @@ async function run2(input, ctx) {
368
456
  };
369
457
  }
370
458
  const apiId = resolved.model ?? DEFAULT_MODEL2;
459
+ resolved = {
460
+ ...resolved,
461
+ aspect_ratio: await snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client)
462
+ };
371
463
  const body = buildPayload2(apiId, resolved);
372
464
  const submitted = await ctx.client.run(apiId, body);
373
465
  const result = await pollJobUntilDoneOrHandoff(
@@ -390,6 +482,7 @@ var init_edit = __esm({
390
482
  init_poll();
391
483
  init_types();
392
484
  init_upload_helpers();
485
+ init_schema_snap();
393
486
  DEFAULT_MODEL2 = "wan-2.7-image-edit";
394
487
  ALLOWED_MODELS = [
395
488
  "wan-2.7-image-edit",
@@ -471,18 +564,24 @@ async function run3(input, ctx) {
471
564
  };
472
565
  }
473
566
  const apiId = requested;
474
- if ((apiId === "flux-edit" || apiId === "gpt-image-2-edit") && !input.reference_image_url) {
567
+ if (REQUIRES_REFERENCE.has(apiId) && !input.reference_image_url) {
475
568
  return {
476
569
  kind: "text",
477
- 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="ideogram-character".`
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}".`
478
571
  };
479
572
  }
480
573
  const body = { prompt: input.prompt };
481
- if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
482
574
  if (input.reference_image_url) {
483
- body.image_url = input.reference_image_url;
484
- body.reference_image_url = input.reference_image_url;
575
+ if (apiId === "flux-edit") {
576
+ body.input_image = input.reference_image_url;
577
+ } else {
578
+ body.image_urls = [input.reference_image_url];
579
+ }
485
580
  }
581
+ const snappedAspect = await snapAspectRatio(apiId, input.aspect_ratio, ctx.client, {
582
+ onUnknown: "omit"
583
+ });
584
+ if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
486
585
  const submitted = await ctx.client.run(apiId, body);
487
586
  const result = await pollJobUntilDoneOrHandoff(
488
587
  submitted.jobId,
@@ -498,20 +597,22 @@ async function run3(input, ctx) {
498
597
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
499
598
  );
500
599
  }
501
- var ALLOWED, DEFAULT_MODEL3, InputSchema3, InputJsonSchema3, characterSkill;
600
+ var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE, InputSchema3, InputJsonSchema3, characterSkill;
502
601
  var init_character = __esm({
503
602
  "src/skills/character.ts"() {
504
603
  init_poll();
505
604
  init_types();
506
- ALLOWED = ["ideogram-character", "flux-edit", "gpt-image-2-edit"];
507
- DEFAULT_MODEL3 = "ideogram-character";
605
+ init_schema_snap();
606
+ ALLOWED = ["seedream-5", "seedream-4", "flux-edit", "gpt-image-2-edit"];
607
+ DEFAULT_MODEL3 = "seedream-5";
608
+ REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["flux-edit", "gpt-image-2-edit"]);
508
609
  InputSchema3 = z.object({
509
610
  prompt: z.string().min(1).max(8e3),
510
611
  model: z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
511
612
  reference_image_url: z.string().url().optional().describe(
512
- "Reference image of the character. Required by flux-edit and gpt-image-2-edit; optional for ideogram-character."
613
+ "Reference image of the character. Required by flux-edit and gpt-image-2-edit; optional for seedream-5/seedream-4."
513
614
  ),
514
- aspect_ratio: z.string().optional()
615
+ aspect_ratio: z.string().optional().describe('Only honored for model="flux-edit".')
515
616
  });
516
617
  InputJsonSchema3 = {
517
618
  type: "object",
@@ -529,7 +630,7 @@ var init_character = __esm({
529
630
  };
530
631
  characterSkill = {
531
632
  name: "pixeldojo:character",
532
- description: "Generate or edit consistent characters across shots. Pass a reference_image_url for edit-style models.",
633
+ description: "Generate or edit consistent characters across shots. Pass a reference_image_url for identity locking (seedream-5 by default).",
533
634
  inputZod: InputSchema3,
534
635
  inputJsonSchema: InputJsonSchema3,
535
636
  allowedModels: [...ALLOWED],
@@ -547,9 +648,13 @@ async function run4(input, ctx) {
547
648
  };
548
649
  }
549
650
  const apiId = requested;
651
+ const [snappedAspect, snappedDuration] = await Promise.all([
652
+ snapAspectRatio(apiId, input.aspect_ratio, ctx.client),
653
+ snapDuration(apiId, input.duration, ctx.client)
654
+ ]);
550
655
  const body = { prompt: input.brief };
551
- if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
552
- if (input.duration !== void 0) body.duration = input.duration;
656
+ if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
657
+ if (snappedDuration !== void 0) body.duration = snappedDuration;
553
658
  const submitted = await ctx.client.run(apiId, body);
554
659
  const result = await pollJobUntilDoneOrHandoff(
555
660
  submitted.jobId,
@@ -570,8 +675,14 @@ var init_storyboard = __esm({
570
675
  "src/skills/storyboard.ts"() {
571
676
  init_poll();
572
677
  init_types();
573
- ALLOWED2 = ["veo-3.1", "kling-video", "seedance-2"];
574
- DEFAULT_MODEL4 = "veo-3.1";
678
+ init_schema_snap();
679
+ ALLOWED2 = [
680
+ "veo-3.1-fast",
681
+ "veo-3.1-standard",
682
+ "kling-video-v3-standard-text",
683
+ "seedance-2-high"
684
+ ];
685
+ DEFAULT_MODEL4 = "veo-3.1-fast";
575
686
  InputSchema4 = z.object({
576
687
  brief: z.string().min(1).max(8e3).describe("Storyboard brief: scene, shots, mood, action."),
577
688
  model: z.string().optional().describe(`One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`),
@@ -617,11 +728,20 @@ async function run5(input, ctx) {
617
728
  const apiId = requested;
618
729
  const body = {
619
730
  prompt: "",
620
- image_url: input.image_url
731
+ image: input.image_url
621
732
  };
622
733
  if (apiId === "magnific-upscaler") {
623
- body.scale_factor = input.scale_factor ?? 2;
734
+ const allowed = [2, 4, 8, 16];
735
+ const requestedScale = input.scale_factor ?? 2;
736
+ const nearest = allowed.reduce(
737
+ (a, b) => Math.abs(b - requestedScale) < Math.abs(a - requestedScale) ? b : a
738
+ );
739
+ body.scale_factor = `${nearest}x`;
624
740
  if (input.creativity !== void 0) body.creativity = input.creativity;
741
+ } else if (apiId === "creative-upscale") {
742
+ body.scale_factor = Math.min(10, Math.max(1, input.scale_factor ?? 2));
743
+ } else if (apiId === "face-enhance") {
744
+ body.scale_factor = Math.min(8, Math.max(1, input.scale_factor ?? 4));
625
745
  }
626
746
  const submitted = await ctx.client.run(apiId, body);
627
747
  const result = await pollJobUntilDoneOrHandoff(
@@ -648,7 +768,9 @@ var init_upscale = __esm({
648
768
  InputSchema5 = z.object({
649
769
  image_url: z.string().url().describe("URL of the source image to upscale."),
650
770
  model: z.string().optional().describe(`One of: ${ALLOWED3.join(", ")}. Defaults to ${DEFAULT_MODEL5}.`),
651
- scale_factor: z.number().int().min(1).max(4).optional().describe("Magnific only: 1, 2, or 4. Defaults to 2."),
771
+ scale_factor: z.number().min(1).max(16).optional().describe(
772
+ "Upscale multiplier. Magnific snaps to the nearest of 2/4/8/16 (default 2); creative-upscale accepts 1-10 (default 2); face-enhance accepts 1-8 (default 4)."
773
+ ),
652
774
  creativity: z.number().min(0).max(1).optional().describe("Magnific only: 0 (preserve) \u2192 1 (creative). Defaults to 0.")
653
775
  });
654
776
  InputJsonSchema5 = {
@@ -656,7 +778,7 @@ var init_upscale = __esm({
656
778
  properties: {
657
779
  image_url: { type: "string", format: "uri" },
658
780
  model: { type: "string", enum: ALLOWED3 },
659
- scale_factor: { type: "integer", minimum: 1, maximum: 4 },
781
+ scale_factor: { type: "number", minimum: 1, maximum: 16 },
660
782
  creativity: { type: "number", minimum: 0, maximum: 1 }
661
783
  },
662
784
  required: ["image_url"]
@@ -812,6 +934,146 @@ var init_from_url = __esm({
812
934
  }
813
935
  });
814
936
  async function run8(input, ctx) {
937
+ const productImages = [
938
+ ...input.product_image_urls ?? [],
939
+ ...await Promise.all((input.product_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
940
+ ];
941
+ const characterImages = [
942
+ ...input.character_image_urls ?? [],
943
+ ...await Promise.all((input.character_image_paths ?? []).map((p) => uploadLocalFile(p, ctx.client)))
944
+ ];
945
+ const body = {
946
+ style: input.style,
947
+ product_url: input.product_url,
948
+ product: !input.product_url && input.product_name ? {
949
+ name: input.product_name,
950
+ description: input.product_description,
951
+ images: productImages
952
+ } : void 0,
953
+ character_images: characterImages.length > 0 ? characterImages : void 0,
954
+ location: input.location_description || input.location_image_url ? { description: input.location_description, image_url: input.location_image_url } : void 0,
955
+ hook: input.hook,
956
+ script: input.script,
957
+ quality: input.quality,
958
+ resolution: input.resolution,
959
+ aspect_ratio: input.aspect_ratio,
960
+ duration: input.duration
961
+ };
962
+ const client = ctx.client;
963
+ const submitted = await client.runAd(body);
964
+ const result = await pollJobUntilDoneOrHandoff(
965
+ submitted.jobId,
966
+ (id) => ctx.client.getJob(id),
967
+ ctx.buildStatusUrl,
968
+ {
969
+ now: ctx.pollOverrides?.now,
970
+ sleep: ctx.pollOverrides?.sleep
971
+ }
972
+ );
973
+ const styleLabel = submitted.ad?.style ?? input.style ?? "ugc-testimonial";
974
+ return pollResultToSkillResult(
975
+ result,
976
+ (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
+ );
978
+ }
979
+ var InputSchema8, InputJsonSchema8, adSkill;
980
+ var init_ad = __esm({
981
+ "src/skills/ad.ts"() {
982
+ init_types();
983
+ init_poll();
984
+ init_upload_helpers();
985
+ InputSchema8 = z.object({
986
+ style: z.string().max(80).optional().describe(
987
+ "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
+ ),
989
+ product_url: z.string().url().optional().describe("Product page URL. Name, description, and images are extracted automatically."),
990
+ product_name: z.string().max(200).optional(),
991
+ product_description: z.string().max(2e3).optional(),
992
+ product_image_urls: z.array(z.string().url()).max(5).optional(),
993
+ product_image_paths: z.array(z.string()).max(5).optional().describe("Absolute local paths; uploaded to 24h temp storage automatically."),
994
+ character_image_urls: z.array(z.string().url()).max(4).optional().describe("Presenter reference images for a consistent on-camera character."),
995
+ character_image_paths: z.array(z.string()).max(4).optional(),
996
+ location_description: z.string().max(1500).optional().describe('Where the ad takes place ("a sunlit Parisian cafe"). Text is enough; no image needed.'),
997
+ location_image_url: z.string().url().optional(),
998
+ hook: z.string().max(1e3).optional().describe("The opening visual beat (first 1-2 seconds). Omit to use the style default."),
999
+ script: z.string().max(1e3).optional().describe("The exact spoken line, voiced word for word. Omit and the presenter improvises."),
1000
+ quality: z.enum(["mini", "fast", "high"]).optional(),
1001
+ resolution: z.enum(["480p", "720p", "1080p", "4k"]).optional().describe('1080p/4k require quality "high".'),
1002
+ aspect_ratio: z.enum(["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"]).optional(),
1003
+ duration: z.number().int().min(4).max(15).optional()
1004
+ }).refine(
1005
+ (v) => v.product_url || (v.product_image_urls?.length || v.product_image_paths?.length) && v.product_name,
1006
+ {
1007
+ message: "Pass product_url, OR product_name plus product_image_urls/product_image_paths.",
1008
+ path: ["product_url"]
1009
+ }
1010
+ );
1011
+ InputJsonSchema8 = {
1012
+ type: "object",
1013
+ properties: {
1014
+ style: {
1015
+ type: "string",
1016
+ description: "Ad style slug (default ugc-testimonial). Catalog: GET /api/v1/marketing/presets \u2014 e.g. ugc-testimonial, unboxing-reveal, unboxing-asmr, virtual-try-on, street-style, mirror-fit-check, pro-try-on, product-showcase, macro-details, secret-hack-reveal, crush-test, hyper-motion, before-after, day-in-the-life, the-gift, tv-spot, gadget-saved-me, selfie-testimonial, direct-to-camera, pack-an-order, mystery-box."
1017
+ },
1018
+ product_url: {
1019
+ type: "string",
1020
+ format: "uri",
1021
+ description: "Product page URL. Name, description, and images are extracted automatically."
1022
+ },
1023
+ product_name: { type: "string" },
1024
+ product_description: { type: "string" },
1025
+ product_image_urls: { type: "array", items: { type: "string", format: "uri" }, maxItems: 5 },
1026
+ product_image_paths: {
1027
+ type: "array",
1028
+ items: { type: "string" },
1029
+ maxItems: 5,
1030
+ description: "Absolute local file paths; uploaded to 24h temp storage automatically."
1031
+ },
1032
+ character_image_urls: {
1033
+ type: "array",
1034
+ items: { type: "string", format: "uri" },
1035
+ maxItems: 4,
1036
+ description: "Presenter reference images for a consistent on-camera character."
1037
+ },
1038
+ character_image_paths: { type: "array", items: { type: "string" }, maxItems: 4 },
1039
+ location_description: {
1040
+ type: "string",
1041
+ description: 'Where the ad takes place ("a sunlit Parisian cafe"). Text is enough.'
1042
+ },
1043
+ location_image_url: { type: "string", format: "uri" },
1044
+ hook: {
1045
+ type: "string",
1046
+ description: "The opening visual beat (first 1-2 seconds). Omit to use the style default."
1047
+ },
1048
+ script: {
1049
+ type: "string",
1050
+ description: "The exact spoken line, voiced word for word. Omit and the presenter improvises."
1051
+ },
1052
+ quality: { type: "string", enum: ["mini", "fast", "high"] },
1053
+ resolution: {
1054
+ type: "string",
1055
+ enum: ["480p", "720p", "1080p", "4k"],
1056
+ description: '1080p/4k require quality "high".'
1057
+ },
1058
+ aspect_ratio: { type: "string", enum: ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"] },
1059
+ duration: { type: "integer", minimum: 4, maximum: 15 }
1060
+ }
1061
+ // "product_url OR product_name+images" is enforced by the server and the
1062
+ // Zod refine; no root-level oneOf (Grok rejects non-object root unions).
1063
+ };
1064
+ 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,
1069
+ // Composition skill — the server routes to the ad engine internally.
1070
+ allowedModels: [],
1071
+ defaultModel: "",
1072
+ run: run8
1073
+ };
1074
+ }
1075
+ });
1076
+ async function run9(input, ctx) {
815
1077
  const client = ctx.client;
816
1078
  const result = await client.runCampaign(input);
817
1079
  if (result.kind === "campaign") {
@@ -838,10 +1100,10 @@ async function run8(input, ctx) {
838
1100
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
839
1101
  };
840
1102
  }
841
- var InputSchema8, InputJsonSchema8, campaignSkill;
1103
+ var InputSchema9, InputJsonSchema9, campaignSkill;
842
1104
  var init_campaign = __esm({
843
1105
  "src/skills/campaign.ts"() {
844
- InputSchema8 = z.object({
1106
+ InputSchema9 = z.object({
845
1107
  productUrl: z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
846
1108
  product: z.object({
847
1109
  name: z.string().min(1).max(200),
@@ -857,7 +1119,7 @@ var init_campaign = __esm({
857
1119
  message: "productUrl or product is required",
858
1120
  path: ["productUrl"]
859
1121
  });
860
- InputJsonSchema8 = {
1122
+ InputJsonSchema9 = {
861
1123
  type: "object",
862
1124
  properties: {
863
1125
  productUrl: {
@@ -888,17 +1150,17 @@ var init_campaign = __esm({
888
1150
  campaignSkill = {
889
1151
  name: "pixeldojo:campaign",
890
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.",
891
- inputZod: InputSchema8,
892
- inputJsonSchema: InputJsonSchema8,
1153
+ inputZod: InputSchema9,
1154
+ inputJsonSchema: InputJsonSchema9,
893
1155
  // Composition skill — internal sub-jobs route to per-tool models, no
894
1156
  // single allowed-models list applies here.
895
1157
  allowedModels: [],
896
1158
  defaultModel: "",
897
- run: run8
1159
+ run: run9
898
1160
  };
899
1161
  }
900
1162
  });
901
- async function run9(input, ctx) {
1163
+ async function run10(input, ctx) {
902
1164
  const client = ctx.client;
903
1165
  const result = await client.getCampaign(input.campaignId);
904
1166
  if (result.kind === "campaign") {
@@ -928,13 +1190,13 @@ async function run9(input, ctx) {
928
1190
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
929
1191
  };
930
1192
  }
931
- var InputSchema9, InputJsonSchema9, campaignStatusSkill;
1193
+ var InputSchema10, InputJsonSchema10, campaignStatusSkill;
932
1194
  var init_campaign_status = __esm({
933
1195
  "src/skills/campaign-status.ts"() {
934
- InputSchema9 = z.object({
1196
+ InputSchema10 = z.object({
935
1197
  campaignId: z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
936
1198
  });
937
- InputJsonSchema9 = {
1199
+ InputJsonSchema10 = {
938
1200
  type: "object",
939
1201
  properties: {
940
1202
  campaignId: {
@@ -947,15 +1209,15 @@ var init_campaign_status = __esm({
947
1209
  campaignStatusSkill = {
948
1210
  name: "pixeldojo:campaign_status",
949
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.",
950
- inputZod: InputSchema9,
951
- inputJsonSchema: InputJsonSchema9,
1212
+ inputZod: InputSchema10,
1213
+ inputJsonSchema: InputJsonSchema10,
952
1214
  allowedModels: [],
953
1215
  defaultModel: "",
954
- run: run9
1216
+ run: run10
955
1217
  };
956
1218
  }
957
1219
  });
958
- async function run10(input, ctx) {
1220
+ async function run11(input, ctx) {
959
1221
  let url;
960
1222
  try {
961
1223
  url = await uploadLocalFile(input.path, ctx.client);
@@ -971,16 +1233,16 @@ async function run10(input, ctx) {
971
1233
  (temp storage; auto-deletes in ~24h \u2014 pass this URL as image_url to :generate or :edit)`
972
1234
  };
973
1235
  }
974
- var InputSchema10, InputJsonSchema10, uploadSkill;
1236
+ var InputSchema11, InputJsonSchema11, uploadSkill;
975
1237
  var init_upload = __esm({
976
1238
  "src/skills/upload.ts"() {
977
1239
  init_upload_helpers();
978
- InputSchema10 = z.object({
1240
+ InputSchema11 = z.object({
979
1241
  path: z.string().min(1).max(4096).describe(
980
1242
  "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."
981
1243
  )
982
1244
  });
983
- InputJsonSchema10 = {
1245
+ InputJsonSchema11 = {
984
1246
  type: "object",
985
1247
  properties: {
986
1248
  path: {
@@ -993,15 +1255,15 @@ var init_upload = __esm({
993
1255
  uploadSkill = {
994
1256
  name: "pixeldojo:upload",
995
1257
  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.",
996
- inputZod: InputSchema10,
997
- inputJsonSchema: InputJsonSchema10,
1258
+ inputZod: InputSchema11,
1259
+ inputJsonSchema: InputJsonSchema11,
998
1260
  allowedModels: [],
999
1261
  defaultModel: "",
1000
- run: run10
1262
+ run: run11
1001
1263
  };
1002
1264
  }
1003
1265
  });
1004
- async function run11(input, ctx) {
1266
+ async function run12(input, ctx) {
1005
1267
  const requested = input.model ?? DEFAULT_MODEL6;
1006
1268
  if (!ALLOWED4.includes(requested)) {
1007
1269
  return {
@@ -1050,7 +1312,7 @@ async function run11(input, ctx) {
1050
1312
  (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Audio ready. Asset: ${urls[0]}`
1051
1313
  );
1052
1314
  }
1053
- var ALLOWED4, DEFAULT_MODEL6, InputSchema11, InputJsonSchema11, audioSkill;
1315
+ var ALLOWED4, DEFAULT_MODEL6, InputSchema12, InputJsonSchema12, audioSkill;
1054
1316
  var init_audio = __esm({
1055
1317
  "src/skills/audio.ts"() {
1056
1318
  init_poll();
@@ -1058,7 +1320,7 @@ var init_audio = __esm({
1058
1320
  init_types();
1059
1321
  ALLOWED4 = ["seed-audio", "text-to-speech"];
1060
1322
  DEFAULT_MODEL6 = "seed-audio";
1061
- InputSchema11 = z.object({
1323
+ InputSchema12 = z.object({
1062
1324
  prompt: z.string().min(1).describe(
1063
1325
  "A script for speech, or a scene description for cinematic audio (dialogue + effects + music). Reference uploaded clips as @Audio1..@Audio3."
1064
1326
  ),
@@ -1069,7 +1331,7 @@ var init_audio = __esm({
1069
1331
  output_format: z.string().optional().describe("seed-audio: mp3 (default), wav, pcm, ogg_opus. text-to-speech: mp3, wav, flac, pcm."),
1070
1332
  speed: z.number().min(0.5).max(2).optional().describe("Speech speed multiplier (0.5\u20132).")
1071
1333
  });
1072
- InputJsonSchema11 = {
1334
+ InputJsonSchema12 = {
1073
1335
  type: "object",
1074
1336
  properties: {
1075
1337
  prompt: { type: "string" },
@@ -1085,15 +1347,15 @@ var init_audio = __esm({
1085
1347
  audioSkill = {
1086
1348
  name: "pixeldojo:audio",
1087
1349
  description: "Generate audio: speech, multi-character dialogue, sound effects, and background music from one prompt (Seed Audio 1.0 by default; plain TTS available).",
1088
- inputZod: InputSchema11,
1089
- inputJsonSchema: InputJsonSchema11,
1350
+ inputZod: InputSchema12,
1351
+ inputJsonSchema: InputJsonSchema12,
1090
1352
  allowedModels: [...ALLOWED4],
1091
1353
  defaultModel: DEFAULT_MODEL6,
1092
- run: run11
1354
+ run: run12
1093
1355
  };
1094
1356
  }
1095
1357
  });
1096
- async function run12(input, ctx) {
1358
+ async function run13(input, ctx) {
1097
1359
  const res = await ctx.client.listLibrary({
1098
1360
  q: input.q,
1099
1361
  model: input.model,
@@ -1124,16 +1386,16 @@ ${lines.join("\n")}
1124
1386
  Pass any URL back into :generate, :edit, :character, or :upscale as a reference input.`
1125
1387
  };
1126
1388
  }
1127
- var InputSchema12, InputJsonSchema12, librarySkill;
1389
+ var InputSchema13, InputJsonSchema13, librarySkill;
1128
1390
  var init_library = __esm({
1129
1391
  "src/skills/library.ts"() {
1130
- InputSchema12 = z.object({
1392
+ InputSchema13 = z.object({
1131
1393
  q: z.string().optional().describe("Substring match against the prompt that produced the asset."),
1132
1394
  model: z.string().optional().describe("Restrict to a single model (apiId)."),
1133
1395
  modality: z.enum(["image", "video", "audio"]).optional().describe("Restrict to one output type."),
1134
1396
  limit: z.number().int().min(1).max(50).optional().describe("Max results (default 10).")
1135
1397
  });
1136
- InputJsonSchema12 = {
1398
+ InputJsonSchema13 = {
1137
1399
  type: "object",
1138
1400
  properties: {
1139
1401
  q: { type: "string" },
@@ -1146,15 +1408,15 @@ var init_library = __esm({
1146
1408
  librarySkill = {
1147
1409
  name: "pixeldojo:library",
1148
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.",
1149
- inputZod: InputSchema12,
1150
- inputJsonSchema: InputJsonSchema12,
1411
+ inputZod: InputSchema13,
1412
+ inputJsonSchema: InputJsonSchema13,
1151
1413
  allowedModels: [],
1152
1414
  defaultModel: "",
1153
- run: run12
1415
+ run: run13
1154
1416
  };
1155
1417
  }
1156
1418
  });
1157
- async function run13(input, ctx) {
1419
+ async function run14(input, ctx) {
1158
1420
  const res = await ctx.client.saveMedia({
1159
1421
  url: input.url,
1160
1422
  prompt: input.prompt,
@@ -1168,17 +1430,17 @@ async function run13(input, ctx) {
1168
1430
  It will appear at https://pixeldojo.ai/my-images and in pixeldojo:library searches.`
1169
1431
  };
1170
1432
  }
1171
- var InputSchema13, InputJsonSchema13, saveSkill;
1433
+ var InputSchema14, InputJsonSchema14, saveSkill;
1172
1434
  var init_save = __esm({
1173
1435
  "src/skills/save.ts"() {
1174
- InputSchema13 = z.object({
1436
+ InputSchema14 = z.object({
1175
1437
  url: z.string().url().describe("The asset URL to save (image or video)."),
1176
1438
  prompt: z.string().optional().describe("The prompt that produced it \u2014 makes it findable in the library later."),
1177
1439
  tool_name: z.string().optional().describe("Tool/model slug that produced it."),
1178
1440
  modality: z.enum(["image", "video", "audio"]).optional(),
1179
1441
  job_id: z.string().optional().describe("The jobId that produced this asset, when known.")
1180
1442
  });
1181
- InputJsonSchema13 = {
1443
+ InputJsonSchema14 = {
1182
1444
  type: "object",
1183
1445
  properties: {
1184
1446
  url: { type: "string", format: "uri" },
@@ -1192,11 +1454,11 @@ var init_save = __esm({
1192
1454
  saveSkill = {
1193
1455
  name: "pixeldojo:save",
1194
1456
  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.",
1195
- inputZod: InputSchema13,
1196
- inputJsonSchema: InputJsonSchema13,
1457
+ inputZod: InputSchema14,
1458
+ inputJsonSchema: InputJsonSchema14,
1197
1459
  allowedModels: [],
1198
1460
  defaultModel: "",
1199
- run: run13
1461
+ run: run14
1200
1462
  };
1201
1463
  }
1202
1464
  });
@@ -1285,7 +1547,24 @@ async function runWorkflow(input, ctx) {
1285
1547
  text: `Step ${i + 1} (${step.skill}) has invalid args: ${issue?.path.join(".") || "input"} ${issue?.message || "invalid"}.`
1286
1548
  };
1287
1549
  }
1288
- const result = await skill.run(parsed.data, ctx);
1550
+ let result = await skill.run(parsed.data, ctx);
1551
+ if (result.kind === "handoff") {
1552
+ const handoffJobId = result.jobId;
1553
+ const polled = await pollJobUntilDoneOrHandoff(
1554
+ handoffJobId,
1555
+ (id) => ctx.client.getJob(id),
1556
+ ctx.buildStatusUrl,
1557
+ {
1558
+ syncBudgetMs: WORKFLOW_STEP_EXTRA_BUDGET_MS,
1559
+ now: ctx.pollOverrides?.now,
1560
+ sleep: ctx.pollOverrides?.sleep
1561
+ }
1562
+ );
1563
+ result = pollResultToSkillResult(
1564
+ polled,
1565
+ (urls) => urls.length === 0 ? `Job ${handoffJobId} completed but returned no assets.` : `Done. Asset: ${urls[0]}`
1566
+ );
1567
+ }
1289
1568
  const url = firstUrl(result);
1290
1569
  if (url) {
1291
1570
  prevUrl = url;
@@ -1295,6 +1574,11 @@ async function runWorkflow(input, ctx) {
1295
1574
  kind: "text",
1296
1575
  text: `Workflow "${workflow.name}" stopped at step ${i + 1} (${step.skill}): ${result.text}`
1297
1576
  };
1577
+ } else if (result.kind === "handoff") {
1578
+ return {
1579
+ 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.`
1581
+ };
1298
1582
  } else {
1299
1583
  outputs.push(`step ${i + 1} (${step.skill}): done (no asset URL)`);
1300
1584
  }
@@ -1305,9 +1589,12 @@ async function runWorkflow(input, ctx) {
1305
1589
  ${outputs.join("\n")}`
1306
1590
  };
1307
1591
  }
1308
- var StepSchema, SaveInputSchema, RunInputSchema, ListInputSchema, stepJson, saveWorkflowSkill, runWorkflowSkill, listWorkflowsSkill;
1592
+ var WORKFLOW_STEP_EXTRA_BUDGET_MS, StepSchema, SaveInputSchema, RunInputSchema, ListInputSchema, stepJson, saveWorkflowSkill, runWorkflowSkill, listWorkflowsSkill;
1309
1593
  var init_workflows = __esm({
1310
1594
  "src/skills/workflows.ts"() {
1595
+ init_poll();
1596
+ init_types();
1597
+ WORKFLOW_STEP_EXTRA_BUDGET_MS = 6e4;
1311
1598
  StepSchema = z.object({
1312
1599
  skill: z.string().regex(/^pixeldojo:(generate|edit|character|storyboard|upscale|audio|upload|from_url)$/, {
1313
1600
  message: "Runnable skills only: generate, edit, character, storyboard, upscale, audio, upload, from_url."
@@ -1383,6 +1670,7 @@ var init_workflows = __esm({
1383
1670
  var skills_exports = {};
1384
1671
  __export(skills_exports, {
1385
1672
  ALL_SKILLS: () => ALL_SKILLS,
1673
+ adSkill: () => adSkill,
1386
1674
  audioSkill: () => audioSkill,
1387
1675
  campaignSkill: () => campaignSkill,
1388
1676
  campaignStatusSkill: () => campaignStatusSkill,
@@ -1414,6 +1702,7 @@ var init_skills = __esm({
1414
1702
  init_upscale();
1415
1703
  init_status();
1416
1704
  init_from_url();
1705
+ init_ad();
1417
1706
  init_campaign();
1418
1707
  init_campaign_status();
1419
1708
  init_upload();
@@ -1436,6 +1725,7 @@ var init_skills = __esm({
1436
1725
  statusSkill,
1437
1726
  fromUrlSkill,
1438
1727
  uploadSkill,
1728
+ adSkill,
1439
1729
  campaignSkill,
1440
1730
  campaignStatusSkill
1441
1731
  ];
@@ -1492,7 +1782,7 @@ function makeRedactedLogger(target = console) {
1492
1782
  }
1493
1783
 
1494
1784
  // src/server.ts
1495
- var PACKAGE_VERSION = "0.5.1" ;
1785
+ var PACKAGE_VERSION = "0.8.0" ;
1496
1786
  function buildServer(options) {
1497
1787
  if (!options.apiKey) {
1498
1788
  throw new Error(