@pixeldojo/mcp 0.9.0 → 0.10.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/index.cjs CHANGED
@@ -95,7 +95,7 @@ function pollResultToSkillResult(result, successText) {
95
95
  }
96
96
  return {
97
97
  kind: "handoff",
98
- text: `Job ${result.jobId} is still running after the 30s sync window. Use pixeldojo:status with this jobId to check completion, or follow ${result.statusUrl}.`,
98
+ text: `Job ${result.jobId} is still running after the 30s sync window. Use pixeldojo_status with this jobId to check completion, or follow ${result.statusUrl}.`,
99
99
  jobId: result.jobId,
100
100
  statusUrl: result.statusUrl
101
101
  };
@@ -185,18 +185,23 @@ var init_upload_helpers = __esm({
185
185
  });
186
186
 
187
187
  // src/skills/schema-snap.ts
188
- function getFieldConstraints(apiId, client) {
188
+ function getModelInfo(apiId, client) {
189
189
  const cached = schemaCache.get(apiId);
190
190
  if (cached) return cached;
191
- const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then(
192
- (res) => res.schema?.properties ?? {}
193
- ).catch(() => {
191
+ if (typeof client?.getModelSchema !== "function") return Promise.resolve(null);
192
+ const pending = Promise.resolve().then(() => client.getModelSchema(apiId)).then((res) => {
193
+ const body = res;
194
+ return { props: body.schema?.properties ?? {}, media: body.media };
195
+ }).catch(() => {
194
196
  schemaCache.delete(apiId);
195
197
  return null;
196
198
  });
197
199
  schemaCache.set(apiId, pending);
198
200
  return pending;
199
201
  }
202
+ async function getFieldConstraints(apiId, client) {
203
+ return (await getModelInfo(apiId, client))?.props ?? null;
204
+ }
200
205
  function parseAspectRatio(ar) {
201
206
  const m = /^\s*(\d+)\s*:\s*(\d+)\s*$/.exec(ar);
202
207
  if (!m) return null;
@@ -232,6 +237,55 @@ async function snapAspectRatio(apiId, requested, client, opts = {}) {
232
237
  if (!field.enum || field.enum.length === 0) return requested;
233
238
  return nearestAspectFromEnum(requested, field.enum);
234
239
  }
240
+ async function resolveMediaSlot(apiId, kind, client) {
241
+ const info = await getModelInfo(apiId, client);
242
+ if (info === null) return void 0;
243
+ const declared = info.media?.[kind];
244
+ if (declared) {
245
+ return {
246
+ name: declared.key,
247
+ isArray: info.props[declared.key]?.type === "array",
248
+ also: declared.also
249
+ };
250
+ }
251
+ if (info.media && kind !== "audio") return null;
252
+ for (const name of FALLBACK_KEYS[kind]) {
253
+ const field = info.props[name];
254
+ if (field) return { name, isArray: field.type === "array" };
255
+ }
256
+ return null;
257
+ }
258
+ function resolutionHeight(value) {
259
+ const v = value.trim().toLowerCase();
260
+ if (v === "4k") return 2160;
261
+ const n = Number.parseInt(v, 10);
262
+ return Number.isFinite(n) ? n : null;
263
+ }
264
+ async function snapResolution(apiId, requested, client, opts = {}) {
265
+ if (!requested) return void 0;
266
+ const props = await getFieldConstraints(apiId, client);
267
+ if (props === null) {
268
+ return opts.onUnknown === "omit" ? void 0 : requested;
269
+ }
270
+ const field = props.resolution;
271
+ if (!field) return void 0;
272
+ if (!field.enum || field.enum.length === 0) return requested;
273
+ if (field.enum.includes(requested)) return requested;
274
+ const target = resolutionHeight(requested);
275
+ if (target === null) return requested;
276
+ let best = requested;
277
+ let bestDiff = Infinity;
278
+ for (const candidate of field.enum) {
279
+ const h = resolutionHeight(candidate);
280
+ if (h === null) continue;
281
+ const diff = Math.abs(h - target);
282
+ if (diff < bestDiff) {
283
+ bestDiff = diff;
284
+ best = candidate;
285
+ }
286
+ }
287
+ return best;
288
+ }
235
289
  async function snapDuration(apiId, requested, client, opts = {}) {
236
290
  if (requested === void 0) return void 0;
237
291
  const props = await getFieldConstraints(apiId, client);
@@ -256,24 +310,49 @@ async function snapDuration(apiId, requested, client, opts = {}) {
256
310
  }
257
311
  return requested;
258
312
  }
313
+ function isVideoUrl(url) {
314
+ return /\.(mp4|mov|webm|m4v)(?:[?#]|$)/i.test(url);
315
+ }
316
+ function isAudioUrl(url) {
317
+ return /\.(mp3|wav|m4a|aac|ogg|flac)(?:[?#]|$)/i.test(url);
318
+ }
259
319
  async function resolveImageRefs(apiId, urls, client) {
260
320
  if (urls.length === 0) return null;
261
- const props = await getFieldConstraints(apiId, client);
262
- if (props === null) return null;
263
- const key = REF_KEYS.find((k) => props[k] !== void 0);
321
+ const info = await getModelInfo(apiId, client);
322
+ if (info === null) return null;
323
+ const props = info.props;
324
+ const key = info.media ? info.media.image?.key : REF_KEYS.find((k) => props[k] !== void 0);
264
325
  if (!key) return null;
265
- const field = props[key];
326
+ const also = info.media?.image?.also;
327
+ const field = props[key] ?? {};
266
328
  if (field.type === "array") {
267
329
  const cap = typeof field.maxItems === "number" ? field.maxItems : urls.length;
268
330
  const kept = urls.slice(0, Math.max(1, cap));
269
- return { key, value: kept, dropped: urls.length - kept.length };
331
+ return { key, value: kept, dropped: urls.length - kept.length, also };
270
332
  }
271
- return { key, value: urls[0], dropped: urls.length - 1 };
333
+ return { key, value: urls[0], dropped: urls.length - 1, also };
272
334
  }
273
- var schemaCache, REF_KEYS;
335
+ var schemaCache, REFERENCE_FIELD_PRIORITY, FALLBACK_KEYS, REF_KEYS;
274
336
  var init_schema_snap = __esm({
275
337
  "src/skills/schema-snap.ts"() {
276
338
  schemaCache = /* @__PURE__ */ new Map();
339
+ REFERENCE_FIELD_PRIORITY = [
340
+ "image_url",
341
+ "start_image_url",
342
+ "input_image",
343
+ "image",
344
+ "reference_images",
345
+ "reference_urls",
346
+ "reference_image_urls",
347
+ "image_urls",
348
+ "image_input",
349
+ "images"
350
+ ];
351
+ FALLBACK_KEYS = {
352
+ image: REFERENCE_FIELD_PRIORITY,
353
+ video: ["reference_videos"],
354
+ audio: ["reference_audios", "audio_url"]
355
+ };
277
356
  REF_KEYS = [
278
357
  "image_urls",
279
358
  "images",
@@ -286,7 +365,51 @@ var init_schema_snap = __esm({
286
365
  ];
287
366
  }
288
367
  });
289
- function buildPayload(apiId, input) {
368
+ function paramsWithoutModel(params, model) {
369
+ if (!params || Object.keys(params).length === 0) return null;
370
+ if (model) return null;
371
+ return "Those settings are model-specific. Tell me which model to use and I will apply them.";
372
+ }
373
+ function applyParams(body, params, paramsModel, apiId) {
374
+ if (!params || Object.keys(params).length === 0) return void 0;
375
+ if (paramsModel !== apiId) {
376
+ return `This ran on ${apiId}, so I left those settings off: they are ${paramsModel}'s fields. Ask again naming ${apiId} if you want them applied.`;
377
+ }
378
+ const rest = { ...params };
379
+ delete rest.prompt;
380
+ Object.assign(body, rest);
381
+ return "prompt" in params ? "I kept the prompt from the skill call and ignored the one in params." : void 0;
382
+ }
383
+ function prefixNote(result, note) {
384
+ if (!note) return result;
385
+ return { ...result, text: `${note} ${result.text}`.trim() };
386
+ }
387
+ var ParamsInput, PARAMS_JSON_SCHEMA;
388
+ var init_params = __esm({
389
+ "src/skills/params.ts"() {
390
+ ParamsInput = zod.z.record(zod.z.unknown()).optional();
391
+ PARAMS_JSON_SCHEMA = {
392
+ type: "object",
393
+ additionalProperties: true,
394
+ description: 'Advanced settings for the model that runs this, named exactly as that model documents them (e.g. {"resolution": "1080p", "seed": 42}). Set `model` in the same call: field names differ per model, so params without one are refused. Applied last, so these override anything derived from the other inputs. An invalid field comes back named with its allowed values and nothing is charged, so fix it and call again.'
395
+ };
396
+ }
397
+ });
398
+ function fallbackSlot(apiId, kind) {
399
+ return apiId === VIDEO_REFERENCE_MODEL ? VIDEO_REFERENCE_SLOTS[kind] : void 0;
400
+ }
401
+ function sourceMedia(videoUrl, referenceUrls) {
402
+ const clip = videoUrl && !isAudioUrl(videoUrl) ? videoUrl : void 0;
403
+ const stills = referenceUrls.filter((u) => !isAudioUrl(u));
404
+ const url = clip ?? stills[0];
405
+ return {
406
+ url,
407
+ isClip: Boolean(clip) || !!url && isVideoUrl(url),
408
+ audioUrl: [videoUrl, ...referenceUrls].find((u) => u && isAudioUrl(u)),
409
+ stills
410
+ };
411
+ }
412
+ function buildPayload(apiId, input, src, slot, audioSlot) {
290
413
  const body = { prompt: input.prompt };
291
414
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
292
415
  if (input.preset) body.preset = input.preset;
@@ -294,9 +417,9 @@ function buildPayload(apiId, input) {
294
417
  case "grok-video-extend":
295
418
  case "wan-video-edit":
296
419
  case "seedance-video-edit":
297
- if (input.image_url) {
298
- body.video_url = input.image_url;
299
- body.image_url = input.image_url;
420
+ if (src.url) {
421
+ body.video_url = src.url;
422
+ body.image_url = src.url;
300
423
  }
301
424
  if (input.duration !== void 0) {
302
425
  body.video_duration = input.duration;
@@ -304,48 +427,90 @@ function buildPayload(apiId, input) {
304
427
  }
305
428
  break;
306
429
  default:
307
- if (input.image_url) body.image_url = input.image_url;
430
+ if (src.url) {
431
+ const refs = src.isClip ? [src.url] : src.stills.length > 0 ? src.stills : [src.url];
432
+ if (slot) {
433
+ body[slot.name] = slot.isArray ? refs : refs[0];
434
+ if (slot.also) Object.assign(body, slot.also);
435
+ } else {
436
+ body.image_url = refs[0];
437
+ }
438
+ }
439
+ if (src.audioUrl && audioSlot) {
440
+ body[audioSlot.name] = audioSlot.isArray ? [src.audioUrl] : src.audioUrl;
441
+ if (audioSlot.also) Object.assign(body, audioSlot.also);
442
+ }
308
443
  if (input.duration !== void 0) body.duration = input.duration;
444
+ if (input.resolution) body.resolution = input.resolution;
309
445
  break;
310
446
  }
311
447
  return body;
312
448
  }
313
449
  async function run(input, ctx) {
314
- const apiId = input.model ?? DEFAULT_MODEL;
450
+ const paramsGate = paramsWithoutModel(input.params, input.model);
451
+ if (paramsGate) return { kind: "text", text: paramsGate };
315
452
  let resolved = input;
316
- if (!input.image_url && input.image_path) {
317
- try {
318
- const uploadedUrl = await uploadLocalFile(input.image_path, ctx.client);
319
- resolved = { ...input, image_url: uploadedUrl };
320
- } catch (err) {
321
- if (err instanceof SkillUploadError) {
322
- return { kind: "text", text: `pixeldojo:generate (upload): ${err.userMessage}` };
323
- }
324
- throw err;
453
+ let referenceUrls = [];
454
+ try {
455
+ const singleUrl = input.image_url ?? (input.image_path ? await uploadLocalFile(input.image_path, ctx.client) : void 0);
456
+ const manyUrls = [...input.image_urls ?? []];
457
+ for (const p of input.image_paths ?? []) {
458
+ manyUrls.push(await uploadLocalFile(p, ctx.client));
459
+ }
460
+ referenceUrls = [...singleUrl ? [singleUrl] : [], ...manyUrls].filter(
461
+ (u, i, all) => all.indexOf(u) === i
462
+ );
463
+ if (referenceUrls[0] && referenceUrls[0] !== input.image_url) {
464
+ resolved = { ...input, image_url: referenceUrls[0] };
465
+ }
466
+ } catch (err) {
467
+ if (err instanceof SkillUploadError) {
468
+ return { kind: "text", text: `pixeldojo_generate (upload): ${err.userMessage}` };
469
+ }
470
+ throw err;
471
+ }
472
+ const src = sourceMedia(input.video_url, referenceUrls);
473
+ let apiId = input.model ?? DEFAULT_MODEL;
474
+ let routeNote;
475
+ if ((src.isClip || src.audioUrl) && !VIDEO_INPUT_MODELS.has(apiId)) {
476
+ if (!input.model) {
477
+ apiId = VIDEO_REFERENCE_MODEL;
478
+ } else if (src.isClip && await resolveMediaSlot(input.model, "video", ctx.client) === null) {
479
+ apiId = VIDEO_REFERENCE_MODEL;
480
+ routeNote = `${input.model} can't work from a video clip, so I used ${VIDEO_REFERENCE_MODEL} instead.`;
325
481
  }
326
482
  }
327
483
  if (apiId === "grok-video-extend") {
328
- if (!resolved.image_url) {
484
+ if (!src.url) {
329
485
  return {
330
486
  kind: "text",
331
- text: 'pixeldojo:generate with model="grok-video-extend" requires image_url (the source video URL to extend, or image_path for a local file).'
487
+ text: 'pixeldojo_generate with model="grok-video-extend" requires video_url (the source video URL to extend, or image_path for a local file).'
332
488
  };
333
489
  }
334
490
  if (resolved.duration === void 0) {
335
491
  return {
336
492
  kind: "text",
337
- text: 'pixeldojo:generate with model="grok-video-extend" requires duration (extension length in seconds).'
493
+ text: 'pixeldojo_generate with model="grok-video-extend" requires duration (extension length in seconds).'
338
494
  };
339
495
  }
340
496
  }
497
+ let refField;
498
+ let audioSlot;
341
499
  if (!VIDEO_INPUT_MODELS.has(apiId)) {
342
- const [aspect_ratio, duration] = await Promise.all([
500
+ const [aspect_ratio, duration, resolution, ref, audio] = await Promise.all([
343
501
  snapAspectRatio(apiId, resolved.aspect_ratio, ctx.client),
344
- snapDuration(apiId, resolved.duration, ctx.client)
502
+ snapDuration(apiId, resolved.duration, ctx.client),
503
+ snapResolution(apiId, resolved.resolution, ctx.client),
504
+ src.url ? resolveMediaSlot(apiId, src.isClip ? "video" : "image", ctx.client) : Promise.resolve(void 0),
505
+ src.audioUrl ? resolveMediaSlot(apiId, "audio", ctx.client) : Promise.resolve(void 0)
345
506
  ]);
346
- resolved = { ...resolved, aspect_ratio, duration };
507
+ resolved = { ...resolved, aspect_ratio, duration, resolution };
508
+ refField = ref === void 0 && src.url ? fallbackSlot(apiId, src.isClip ? "video" : "image") : ref;
509
+ audioSlot = audio === void 0 && src.audioUrl ? fallbackSlot(apiId, "audio") : audio;
347
510
  }
348
- const body = buildPayload(apiId, resolved);
511
+ const body = buildPayload(apiId, resolved, src, refField, audioSlot);
512
+ const paramsNote = applyParams(body, input.params, input.model, apiId);
513
+ const note = [routeNote, paramsNote].filter(Boolean).join(" ");
349
514
  const submitted = await ctx.client.run(apiId, body);
350
515
  const result = await pollJobUntilDoneOrHandoff(
351
516
  submitted.jobId,
@@ -356,18 +521,22 @@ async function run(input, ctx) {
356
521
  sleep: ctx.pollOverrides?.sleep
357
522
  }
358
523
  );
359
- return pollResultToSkillResult(
360
- result,
361
- (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(", ")}`
524
+ return prefixNote(
525
+ pollResultToSkillResult(
526
+ result,
527
+ (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(", ")}`
528
+ ),
529
+ note
362
530
  );
363
531
  }
364
- var InputSchema, DEFAULT_MODEL, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
532
+ var InputSchema, DEFAULT_MODEL, VIDEO_REFERENCE_MODEL, VIDEO_REFERENCE_SLOTS, VIDEO_INPUT_MODELS, InputJsonSchema, generateSkill;
365
533
  var init_generate = __esm({
366
534
  "src/skills/generate.ts"() {
367
535
  init_poll();
368
536
  init_types();
369
537
  init_upload_helpers();
370
538
  init_schema_snap();
539
+ init_params();
371
540
  InputSchema = zod.z.object({
372
541
  prompt: zod.z.string().min(1).max(8e3).describe("Prompt describing what to generate."),
373
542
  model: zod.z.string().optional().describe(
@@ -378,12 +547,31 @@ var init_generate = __esm({
378
547
  ),
379
548
  aspect_ratio: zod.z.string().optional().describe('Aspect ratio supported by the chosen model (e.g. "1:1", "16:9", "9:16").'),
380
549
  duration: zod.z.number().int().positive().optional().describe("Video duration in seconds, when applicable."),
550
+ resolution: zod.z.string().optional().describe(
551
+ 'Output resolution when the model supports it (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits. Omit to use the model default.'
552
+ ),
381
553
  image_url: zod.z.string().url().optional().describe("Reference image URL for image-to-image / image-to-video tools."),
554
+ video_url: zod.z.string().url().optional().describe(
555
+ "Source video clip to animate / restyle / extend. Use this for videos; image_url is for still frames. A clip routes to a model that can actually read one."
556
+ ),
557
+ params: ParamsInput,
382
558
  image_path: zod.z.string().min(1).optional().describe(
383
559
  "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."
560
+ ),
561
+ image_urls: zod.z.array(zod.z.string().url()).optional().describe(
562
+ "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."
563
+ ),
564
+ image_paths: zod.z.array(zod.z.string().min(1)).optional().describe(
565
+ "Multiple absolute local file paths. Each is uploaded to PixelDojo (24h temp storage) and used as a reference, same rules as image_urls."
384
566
  )
385
567
  });
386
568
  DEFAULT_MODEL = "google-nano-banana";
569
+ VIDEO_REFERENCE_MODEL = "seedance-2-reference";
570
+ VIDEO_REFERENCE_SLOTS = {
571
+ image: { name: "reference_images", isArray: true },
572
+ video: { name: "reference_videos", isArray: true },
573
+ audio: { name: "reference_audios", isArray: true }
574
+ };
387
575
  VIDEO_INPUT_MODELS = /* @__PURE__ */ new Set(["grok-video-extend", "wan-video-edit", "seedance-video-edit"]);
388
576
  InputJsonSchema = {
389
577
  type: "object",
@@ -399,7 +587,27 @@ var init_generate = __esm({
399
587
  },
400
588
  aspect_ratio: { type: "string" },
401
589
  duration: { type: "integer" },
590
+ resolution: {
591
+ type: "string",
592
+ description: 'Output resolution when supported (e.g. "480p", "720p", "1080p", "4k"). Lower tiers cost fewer credits.'
593
+ },
402
594
  image_url: { type: "string", format: "uri" },
595
+ video_url: {
596
+ type: "string",
597
+ format: "uri",
598
+ description: "Source video clip to animate / restyle / extend. Use this for videos; image_url is for still frames."
599
+ },
600
+ params: PARAMS_JSON_SCHEMA,
601
+ image_urls: {
602
+ type: "array",
603
+ items: { type: "string", format: "uri" },
604
+ description: "Multiple reference image URLs for models that accept a reference set. Single-reference models use the first."
605
+ },
606
+ image_paths: {
607
+ type: "array",
608
+ items: { type: "string" },
609
+ description: "Multiple absolute local file paths, each uploaded to 24h temp storage and used as a reference."
610
+ },
403
611
  image_path: {
404
612
  type: "string",
405
613
  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."
@@ -408,8 +616,8 @@ var init_generate = __esm({
408
616
  required: ["prompt"]
409
617
  };
410
618
  generateSkill = {
411
- name: "pixeldojo:generate",
412
- 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.",
619
+ name: "pixeldojo_generate",
620
+ 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 flows: pass `image_url` for a still, `video_url` for a source clip, or `image_path` (absolute local path) and the skill uploads it to 24h temp storage automatically. A clip or an audio reference routes to an engine that can read one. Model-specific settings go in `params` alongside an explicit `model`.",
413
621
  inputZod: InputSchema,
414
622
  inputJsonSchema: InputJsonSchema,
415
623
  allowedModels: null,
@@ -476,9 +684,12 @@ async function buildBody(apiId, input, ctx) {
476
684
  }
477
685
  body.prompt = input.prompt;
478
686
  body[refs.key] = refs.value;
687
+ if (refs.also) Object.assign(body, refs.also);
479
688
  return { body, dropped: refs.dropped };
480
689
  }
481
690
  async function run2(input, ctx) {
691
+ const paramsGate = paramsWithoutModel(input.params, input.model);
692
+ if (paramsGate) return { kind: "text", text: paramsGate };
482
693
  let resolved = input;
483
694
  try {
484
695
  if (input.image_paths && input.image_paths.length > 0) {
@@ -493,14 +704,14 @@ async function run2(input, ctx) {
493
704
  }
494
705
  } catch (err) {
495
706
  if (err instanceof SkillUploadError) {
496
- return { kind: "text", text: `pixeldojo:edit (upload): ${err.userMessage}` };
707
+ return { kind: "text", text: `pixeldojo_edit (upload): ${err.userMessage}` };
497
708
  }
498
709
  throw err;
499
710
  }
500
711
  if (!resolved.image_url && (!resolved.image_urls || resolved.image_urls.length === 0)) {
501
712
  return {
502
713
  kind: "text",
503
- text: "pixeldojo:edit requires at least one reference image. Pass image_url / image_urls (hosted URL) or image_path / image_paths (local file)."
714
+ text: "pixeldojo_edit requires at least one reference image. Pass image_url / image_urls (hosted URL) or image_path / image_paths (local file)."
504
715
  };
505
716
  }
506
717
  const apiId = resolved.model ?? DEFAULT_MODEL2;
@@ -510,6 +721,7 @@ async function run2(input, ctx) {
510
721
  };
511
722
  const { body, dropped } = await buildBody(apiId, resolved, ctx);
512
723
  const droppedNote = dropped > 0 ? ` (${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer)` : "";
724
+ const paramsNote = applyParams(body, input.params, input.model, apiId);
513
725
  const submitted = await ctx.client.run(apiId, body);
514
726
  const result = await pollJobUntilDoneOrHandoff(
515
727
  submitted.jobId,
@@ -520,9 +732,12 @@ async function run2(input, ctx) {
520
732
  sleep: ctx.pollOverrides?.sleep
521
733
  }
522
734
  );
523
- return pollResultToSkillResult(
524
- result,
525
- (urls) => urls.length === 0 ? `Edit job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Edit complete${droppedNote}. Asset: ${urls[0]}` : `Edit produced ${urls.length} assets${droppedNote}. URLs: ${urls.join(", ")}`
735
+ return prefixNote(
736
+ pollResultToSkillResult(
737
+ result,
738
+ (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(", ")}`
739
+ ),
740
+ paramsNote
526
741
  );
527
742
  }
528
743
  var DEFAULT_MODEL2, ALLOWED_MODELS, InputSchema2, InputJsonSchema2, editSkill;
@@ -532,6 +747,7 @@ var init_edit = __esm({
532
747
  init_types();
533
748
  init_upload_helpers();
534
749
  init_schema_snap();
750
+ init_params();
535
751
  DEFAULT_MODEL2 = "wan-2.7-image-edit";
536
752
  ALLOWED_MODELS = [
537
753
  "wan-2.7-image-edit",
@@ -555,7 +771,8 @@ var init_edit = __esm({
555
771
  model: zod.z.enum(ALLOWED_MODELS).optional().describe(
556
772
  "Override the default edit model. Defaults to wan-2.7-image-edit (most popular)."
557
773
  ),
558
- aspect_ratio: zod.z.string().optional().describe('Aspect ratio when supported (e.g. "1:1", "16:9", "match_input_image").')
774
+ aspect_ratio: zod.z.string().optional().describe('Aspect ratio when supported (e.g. "1:1", "16:9", "match_input_image").'),
775
+ params: ParamsInput
559
776
  });
560
777
  InputJsonSchema2 = {
561
778
  type: "object",
@@ -589,12 +806,13 @@ var init_edit = __esm({
589
806
  enum: [...ALLOWED_MODELS],
590
807
  description: "Override edit model. Default: wan-2.7-image-edit."
591
808
  },
592
- aspect_ratio: { type: "string" }
809
+ aspect_ratio: { type: "string" },
810
+ params: PARAMS_JSON_SCHEMA
593
811
  },
594
812
  required: ["prompt"]
595
813
  };
596
814
  editSkill = {
597
- name: "pixeldojo:edit",
815
+ name: "pixeldojo_edit",
598
816
  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).",
599
817
  inputZod: InputSchema2,
600
818
  inputJsonSchema: InputJsonSchema2,
@@ -605,32 +823,50 @@ var init_edit = __esm({
605
823
  }
606
824
  });
607
825
  async function run3(input, ctx) {
826
+ const paramsGate = paramsWithoutModel(input.params, input.model);
827
+ if (paramsGate) return { kind: "text", text: paramsGate };
608
828
  const requested = input.model ?? DEFAULT_MODEL3;
609
829
  if (!ALLOWED.includes(requested)) {
610
830
  return {
611
831
  kind: "text",
612
- text: `Unknown model "${requested}" for pixeldojo:character. Allowed: ${ALLOWED.join(", ")}.`
832
+ text: `Unknown model "${requested}" for pixeldojo_character. Allowed: ${ALLOWED.join(", ")}.`
613
833
  };
614
834
  }
615
835
  const apiId = requested;
616
- if (REQUIRES_REFERENCE.has(apiId) && !input.reference_image_url) {
836
+ const refUrls = (input.reference_image_urls && input.reference_image_urls.length > 0 ? input.reference_image_urls : input.reference_image_url ? [input.reference_image_url] : []).filter((u, i, all) => all.indexOf(u) === i);
837
+ const wrongKind = refUrls.find((u) => isVideoUrl(u) || isAudioUrl(u));
838
+ if (wrongKind) {
839
+ return {
840
+ kind: "text",
841
+ text: `pixeldojo_character works on still images, and ${wrongKind} is not one. Use pixeldojo_generate or pixeldojo_storyboard with video_url to work from a clip (or image_url for an audio track \u2014 they route to the reference engine). For this treatment, give me a still frame.`
842
+ };
843
+ }
844
+ if (REQUIRES_REFERENCE.has(apiId) && refUrls.length === 0) {
617
845
  return {
618
846
  kind: "text",
619
- 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}".`
847
+ text: `pixeldojo_character with model="${apiId}" requires a reference image (reference_image_url / reference_image_urls \u2014 the source image of the character to edit). For generation from a prompt alone, use model="${DEFAULT_MODEL3}".`
620
848
  };
621
849
  }
622
850
  const body = { prompt: input.prompt };
623
- if (input.reference_image_url) {
624
- if (apiId === "flux-edit") {
625
- body.input_image = input.reference_image_url;
851
+ let dropped = 0;
852
+ if (refUrls.length > 0) {
853
+ const refs = await resolveImageRefs(apiId, refUrls, ctx.client);
854
+ if (refs) {
855
+ body[refs.key] = refs.value;
856
+ if (refs.also) Object.assign(body, refs.also);
857
+ dropped = refs.dropped;
626
858
  } else {
627
- body.image_urls = [input.reference_image_url];
859
+ body[FALLBACK_REF_KEY[apiId]] = refUrls;
628
860
  }
629
861
  }
630
862
  const snappedAspect = await snapAspectRatio(apiId, input.aspect_ratio, ctx.client, {
631
863
  onUnknown: "omit"
632
864
  });
633
865
  if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
866
+ const note = [
867
+ dropped > 0 ? `${dropped} reference${dropped === 1 ? "" : "s"} dropped \u2014 ${apiId} accepts fewer.` : void 0,
868
+ applyParams(body, input.params, input.model, apiId)
869
+ ].filter(Boolean).join(" ");
634
870
  const submitted = await ctx.client.run(apiId, body);
635
871
  const result = await pollJobUntilDoneOrHandoff(
636
872
  submitted.jobId,
@@ -641,27 +877,49 @@ async function run3(input, ctx) {
641
877
  sleep: ctx.pollOverrides?.sleep
642
878
  }
643
879
  );
644
- return pollResultToSkillResult(
645
- result,
646
- (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
880
+ return prefixNote(
881
+ pollResultToSkillResult(
882
+ result,
883
+ (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Character shot ready. Asset: ${urls[0]}`
884
+ ),
885
+ note
647
886
  );
648
887
  }
649
- var ALLOWED, DEFAULT_MODEL3, REQUIRES_REFERENCE, InputSchema3, InputJsonSchema3, characterSkill;
888
+ var ALLOWED, DEFAULT_MODEL3, MAX_REFERENCES, REQUIRES_REFERENCE, FALLBACK_REF_KEY, InputSchema3, InputJsonSchema3, characterSkill;
650
889
  var init_character = __esm({
651
890
  "src/skills/character.ts"() {
652
891
  init_poll();
653
892
  init_types();
654
893
  init_schema_snap();
655
- ALLOWED = ["seedream-5", "seedream-4", "flux-edit", "gpt-image-2-edit"];
894
+ init_params();
895
+ ALLOWED = [
896
+ "seedream-5",
897
+ "seedream-4",
898
+ "google-nano-banana",
899
+ "wan-2.7-image-edit",
900
+ "qwen-image-2.0-pro"
901
+ ];
656
902
  DEFAULT_MODEL3 = "seedream-5";
657
- REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["flux-edit", "gpt-image-2-edit"]);
903
+ MAX_REFERENCES = 9;
904
+ REQUIRES_REFERENCE = /* @__PURE__ */ new Set(["wan-2.7-image-edit"]);
905
+ FALLBACK_REF_KEY = {
906
+ "seedream-5": "image_urls",
907
+ "seedream-4": "image_urls",
908
+ "google-nano-banana": "image_input",
909
+ "wan-2.7-image-edit": "image_urls",
910
+ "qwen-image-2.0-pro": "reference_images"
911
+ };
658
912
  InputSchema3 = zod.z.object({
659
913
  prompt: zod.z.string().min(1).max(8e3),
660
914
  model: zod.z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
661
915
  reference_image_url: zod.z.string().url().optional().describe(
662
- "Reference image of the character. Required by flux-edit and gpt-image-2-edit; optional for seedream-5/seedream-4."
916
+ "Single reference image of the character. Prefer reference_image_urls \u2014 more angles lock the face better."
663
917
  ),
664
- aspect_ratio: zod.z.string().optional().describe('Only honored for model="flux-edit".')
918
+ reference_image_urls: zod.z.array(zod.z.string().url()).min(1).max(MAX_REFERENCES).optional().describe(
919
+ `All references for this shot (1-${MAX_REFERENCES}), most important first. Multiple angles of the same person lock the face far better than one photo. Models that take fewer are clamped to their own cap.`
920
+ ),
921
+ aspect_ratio: zod.z.string().optional().describe("Honored where the model declares the field."),
922
+ params: ParamsInput
665
923
  });
666
924
  InputJsonSchema3 = {
667
925
  type: "object",
@@ -672,14 +930,26 @@ var init_character = __esm({
672
930
  enum: ALLOWED,
673
931
  description: `One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`
674
932
  },
675
- reference_image_url: { type: "string", format: "uri" },
676
- aspect_ratio: { type: "string" }
933
+ reference_image_url: {
934
+ type: "string",
935
+ format: "uri",
936
+ description: "Single reference. Prefer reference_image_urls."
937
+ },
938
+ reference_image_urls: {
939
+ type: "array",
940
+ items: { type: "string", format: "uri" },
941
+ minItems: 1,
942
+ maxItems: MAX_REFERENCES,
943
+ description: "All references for this shot, most important first. Multiple angles of the same person lock the face far better than one photo."
944
+ },
945
+ aspect_ratio: { type: "string" },
946
+ params: PARAMS_JSON_SCHEMA
677
947
  },
678
948
  required: ["prompt"]
679
949
  };
680
950
  characterSkill = {
681
- name: "pixeldojo:character",
682
- description: "Generate or edit consistent characters across shots. Pass a reference_image_url for identity locking (seedream-5 by default).",
951
+ name: "pixeldojo_character",
952
+ description: "Generate or edit consistent characters across shots. Pass reference_image_urls (up to 9) to lock identity \u2014 send every photo of the person you have, since more angles hold the face far better than one. Defaults to seedream-5.",
683
953
  inputZod: InputSchema3,
684
954
  inputJsonSchema: InputJsonSchema3,
685
955
  allowedModels: [...ALLOWED],
@@ -689,13 +959,29 @@ var init_character = __esm({
689
959
  }
690
960
  });
691
961
  async function run4(input, ctx) {
962
+ const paramsGate = paramsWithoutModel(input.params, input.model);
963
+ if (paramsGate) return { kind: "text", text: paramsGate };
692
964
  const requested = input.model ?? DEFAULT_MODEL4;
693
965
  if (!ALLOWED2.includes(requested)) {
694
966
  return {
695
967
  kind: "text",
696
- text: `Unknown model "${requested}" for pixeldojo:storyboard. Allowed: ${ALLOWED2.join(", ")}.`
968
+ text: `Unknown model "${requested}" for pixeldojo_storyboard. Allowed: ${ALLOWED2.join(", ")}.`
697
969
  };
698
970
  }
971
+ if (input.image_url || input.video_url) {
972
+ return generateSkill.run(
973
+ {
974
+ prompt: input.brief,
975
+ model: input.model ?? VIDEO_REFERENCE_MODEL,
976
+ image_url: input.image_url,
977
+ video_url: input.video_url,
978
+ aspect_ratio: input.aspect_ratio,
979
+ duration: input.duration,
980
+ params: input.params
981
+ },
982
+ ctx
983
+ );
984
+ }
699
985
  const apiId = requested;
700
986
  const [snappedAspect, snappedDuration] = await Promise.all([
701
987
  snapAspectRatio(apiId, input.aspect_ratio, ctx.client),
@@ -704,6 +990,7 @@ async function run4(input, ctx) {
704
990
  const body = { prompt: input.brief };
705
991
  if (snappedAspect !== void 0) body.aspect_ratio = snappedAspect;
706
992
  if (snappedDuration !== void 0) body.duration = snappedDuration;
993
+ const note = applyParams(body, input.params, input.model, apiId);
707
994
  const submitted = await ctx.client.run(apiId, body);
708
995
  const result = await pollJobUntilDoneOrHandoff(
709
996
  submitted.jobId,
@@ -714,9 +1001,12 @@ async function run4(input, ctx) {
714
1001
  sleep: ctx.pollOverrides?.sleep
715
1002
  }
716
1003
  );
717
- return pollResultToSkillResult(
718
- result,
719
- (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Storyboard preview ready. Asset: ${urls[0]}`
1004
+ return prefixNote(
1005
+ pollResultToSkillResult(
1006
+ result,
1007
+ (urls) => urls.length === 0 ? `Job ${submitted.jobId} completed but returned no assets.` : `Storyboard preview ready. Asset: ${urls[0]}`
1008
+ ),
1009
+ note
720
1010
  );
721
1011
  }
722
1012
  var ALLOWED2, DEFAULT_MODEL4, InputSchema4, InputJsonSchema4, storyboardSkill;
@@ -725,18 +1015,24 @@ var init_storyboard = __esm({
725
1015
  init_poll();
726
1016
  init_types();
727
1017
  init_schema_snap();
1018
+ init_generate();
1019
+ init_params();
728
1020
  ALLOWED2 = [
729
1021
  "veo-3.1-fast",
730
1022
  "veo-3.1-standard",
731
1023
  "kling-video-v3-standard-text",
732
- "seedance-2-high"
1024
+ "seedance-2-high",
1025
+ VIDEO_REFERENCE_MODEL
733
1026
  ];
734
1027
  DEFAULT_MODEL4 = "veo-3.1-fast";
735
1028
  InputSchema4 = zod.z.object({
736
1029
  brief: zod.z.string().min(1).max(8e3).describe("Storyboard brief: scene, shots, mood, action."),
737
1030
  model: zod.z.string().optional().describe(`One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`),
738
1031
  aspect_ratio: zod.z.string().optional(),
739
- duration: zod.z.number().int().positive().optional()
1032
+ duration: zod.z.number().int().positive().optional(),
1033
+ image_url: zod.z.string().url().optional().describe("Reference still to build the shot from (character, product, location)."),
1034
+ video_url: zod.z.string().url().optional().describe("Source clip to work from. Use this for videos; image_url is for still frames."),
1035
+ params: ParamsInput
740
1036
  });
741
1037
  InputJsonSchema4 = {
742
1038
  type: "object",
@@ -751,13 +1047,24 @@ var init_storyboard = __esm({
751
1047
  description: `One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`
752
1048
  },
753
1049
  aspect_ratio: { type: "string" },
754
- duration: { type: "integer" }
1050
+ duration: { type: "integer" },
1051
+ image_url: {
1052
+ type: "string",
1053
+ format: "uri",
1054
+ description: "Reference still to build the shot from."
1055
+ },
1056
+ video_url: {
1057
+ type: "string",
1058
+ format: "uri",
1059
+ description: "Source clip to work from. Use this for videos; image_url is for stills."
1060
+ },
1061
+ params: PARAMS_JSON_SCHEMA
755
1062
  },
756
1063
  required: ["brief"]
757
1064
  };
758
1065
  storyboardSkill = {
759
- name: "pixeldojo:storyboard",
760
- description: "Generate a video storyboard preview from a single brief. v1: single-shot preview; multi-shot orchestration coming.",
1066
+ name: "pixeldojo_storyboard",
1067
+ description: "Generate a video storyboard preview from a single brief. Pass image_url for a reference still or video_url for a source clip. v1: single-shot preview; multi-shot orchestration coming.",
761
1068
  inputZod: InputSchema4,
762
1069
  inputJsonSchema: InputJsonSchema4,
763
1070
  allowedModels: [...ALLOWED2],
@@ -771,7 +1078,7 @@ async function run5(input, ctx) {
771
1078
  if (!ALLOWED3.includes(requested)) {
772
1079
  return {
773
1080
  kind: "text",
774
- text: `Unknown model "${requested}" for pixeldojo:upscale. Allowed: ${ALLOWED3.join(", ")}.`
1081
+ text: `Unknown model "${requested}" for pixeldojo_upscale. Allowed: ${ALLOWED3.join(", ")}.`
775
1082
  };
776
1083
  }
777
1084
  const apiId = requested;
@@ -833,7 +1140,7 @@ var init_upscale = __esm({
833
1140
  required: ["image_url"]
834
1141
  };
835
1142
  upscaleSkill = {
836
- name: "pixeldojo:upscale",
1143
+ name: "pixeldojo_upscale",
837
1144
  description: "Enhance an image. Magnific (default) supports configurable scale_factor and creativity; face-enhance for portraits.",
838
1145
  inputZod: InputSchema5,
839
1146
  inputJsonSchema: InputJsonSchema5,
@@ -911,14 +1218,14 @@ var InputSchema6, InputJsonSchema6, STUCK_HINT_MS, ABANDON_HINT_MS, statusSkill;
911
1218
  var init_status = __esm({
912
1219
  "src/skills/status.ts"() {
913
1220
  InputSchema6 = zod.z.object({
914
- jobId: zod.z.string().min(1).max(200).describe("Job id returned from a prior pixeldojo:* skill handoff.")
1221
+ jobId: zod.z.string().min(1).max(200).describe("Job id returned from a prior pixeldojo_* skill handoff.")
915
1222
  });
916
1223
  InputJsonSchema6 = {
917
1224
  type: "object",
918
1225
  properties: {
919
1226
  jobId: {
920
1227
  type: "string",
921
- description: "Job id returned from a prior pixeldojo:* skill handoff."
1228
+ description: "Job id returned from a prior pixeldojo_* skill handoff."
922
1229
  }
923
1230
  },
924
1231
  required: ["jobId"]
@@ -926,7 +1233,7 @@ var init_status = __esm({
926
1233
  STUCK_HINT_MS = 5 * 60 * 1e3;
927
1234
  ABANDON_HINT_MS = 12 * 60 * 1e3;
928
1235
  statusSkill = {
929
- name: "pixeldojo:status",
1236
+ name: "pixeldojo_status",
930
1237
  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.",
931
1238
  inputZod: InputSchema6,
932
1239
  inputJsonSchema: InputJsonSchema6,
@@ -971,8 +1278,8 @@ var init_from_url = __esm({
971
1278
  required: ["url"]
972
1279
  };
973
1280
  fromUrlSkill = {
974
- name: "pixeldojo:from_url",
975
- 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.",
1281
+ name: "pixeldojo_from_url",
1282
+ 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.",
976
1283
  inputZod: InputSchema7,
977
1284
  inputJsonSchema: InputJsonSchema7,
978
1285
  // No model dispatch — this skill calls a server-side scrape, not a generation.
@@ -1033,7 +1340,7 @@ async function plan(input, ctx) {
1033
1340
  ...sceneLines,
1034
1341
  "",
1035
1342
  `Nothing has been charged yet. Producing renders every shot (roughly 20-25 credits per shot at default engines, charged as each renders) plus a few credits for the soundtrack.`,
1036
- `Review or edit at ${ctx.baseUrl}/film-studio/${film.id} \u2014 or call pixeldojo:film with {"film_id": "${film.id}", "action": "produce"} to start production.`
1343
+ `Review or edit at ${ctx.baseUrl}/film-studio/${film.id} \u2014 or call pixeldojo_film with {"film_id": "${film.id}", "action": "produce"} to start production.`
1037
1344
  ].join("\n")
1038
1345
  };
1039
1346
  }
@@ -1048,7 +1355,7 @@ async function produce(filmId, ctx) {
1048
1355
  }
1049
1356
  return {
1050
1357
  kind: "text",
1051
- text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one). Poll pixeldojo:film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`
1358
+ text: `Production started on "${data.film.title}": ${fired} frame(s) dispatched (each is a normal frame charge; the server's Frame check reviews every one). Poll pixeldojo_film with {"film_id": "${filmId}", "action": "status"} every ~30 seconds \u2014 each poll advances the pipeline.`
1052
1359
  };
1053
1360
  }
1054
1361
  function buildScorePlanBody(film) {
@@ -1177,13 +1484,13 @@ async function run8(input, ctx) {
1177
1484
  var LOOKS, InputSchema8, InputJsonSchema8, shots, count, LOOK_MUSIC, ARC, filmSkill;
1178
1485
  var init_film = __esm({
1179
1486
  "src/skills/film.ts"() {
1180
- LOOKS = ["live-action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western"];
1487
+ LOOKS = ["live-action", "anime", "3d-animation", "noir", "horror", "sci-fi", "documentary", "western", "romance", "romantic-comedy", "comedy"];
1181
1488
  InputSchema8 = zod.z.object({
1182
1489
  idea: zod.z.string().max(2e3).optional().describe("One-to-three sentence film idea. The studio writes the screenplay (free). Provide idea OR script for planning."),
1183
1490
  script: zod.z.string().max(2e4).optional().describe("A full screenplay to plan instead of an idea (sluglines, action lines, dialogue)."),
1184
1491
  look: zod.z.enum(LOOKS).default("live-action").describe("Visual style carried through frames, clips, and soundtrack."),
1185
1492
  aspect: zod.z.enum(["16:9", "9:16"]).default("16:9"),
1186
- length: zod.z.enum(["30s", "60s", "2-3min"]).default("60s").describe("Target length; drives the shot count (~6/12/24 shots)."),
1493
+ length: zod.z.enum(["30s", "60s", "2-3min", "5min"]).default("60s").describe("Target length; drives the shot count (~6/12/24/60 shots)."),
1187
1494
  film_id: zod.z.string().max(40).optional().describe("Existing film id (from a previous call) for produce/status."),
1188
1495
  action: zod.z.enum(["plan", "produce", "status"]).default("plan").describe("'plan' creates + storyboards (free). 'produce' starts charged generation. 'status' advances production and reports progress; poll it every ~30s until it returns the finished film."),
1189
1496
  soundtrack: zod.z.enum(["scene-synced", "flat", "none"]).default("scene-synced").describe("scene-synced composes one musical section per scene, pinned to the scene lengths (default). flat is a single cohesive bed. none skips music."),
@@ -1196,7 +1503,7 @@ var init_film = __esm({
1196
1503
  script: { type: "string", description: "A full screenplay to plan instead of an idea." },
1197
1504
  look: { type: "string", enum: [...LOOKS], description: "Visual style. Default live-action." },
1198
1505
  aspect: { type: "string", enum: ["16:9", "9:16"], description: "Default 16:9." },
1199
- length: { type: "string", enum: ["30s", "60s", "2-3min"], description: "Target length (~6/12/24 shots). Default 60s." },
1506
+ length: { type: "string", enum: ["30s", "60s", "2-3min", "5min"], description: "Target length (~6/12/24/60 shots). Default 60s." },
1200
1507
  film_id: { type: "string", description: "Existing film id for produce/status." },
1201
1508
  action: { type: "string", enum: ["plan", "produce", "status"], description: "plan (free) | produce (charged) | status (poll every ~30s)." },
1202
1509
  soundtrack: { type: "string", enum: ["scene-synced", "flat", "none"], description: "Default scene-synced." },
@@ -1213,7 +1520,10 @@ var init_film = __esm({
1213
1520
  horror: "sparse horror underscore, low cello drones, dissonant string swells, creeping heartbeat pulse, unsettling quiet",
1214
1521
  "sci-fi": "atmospheric science-fiction score, analog synth pads, deep sub-bass pulses, shimmering arpeggios, vast and cold",
1215
1522
  documentary: "understated documentary score, felt piano, soft string beds, organic percussion textures, reflective and true",
1216
- western: "cinematic western score, lonely twangy guitar, dusty strings, distant horns, wide-open desert tension"
1523
+ western: "cinematic western score, lonely twangy guitar, dusty strings, distant horns, wide-open desert tension",
1524
+ romance: "tender romantic score, felt piano and warm strings, soft acoustic guitar, intimate and unhurried, gently swelling",
1525
+ "romantic-comedy": "bright romantic-comedy score, plucky ukulele and handclaps, warm brushed drums, sunny and flirtatious",
1526
+ comedy: "playful comedy score, bouncy pizzicato strings, cheeky woodwinds, light percussion hits punctuating the gags"
1217
1527
  };
1218
1528
  ARC = {
1219
1529
  opening: ["establishing the theme", "restrained", "quiet anticipation"],
@@ -1221,7 +1531,7 @@ var init_film = __esm({
1221
1531
  finale: ["climactic", "full arrangement", "emotional peak resolving to stillness"]
1222
1532
  };
1223
1533
  filmSkill = {
1224
- name: "pixeldojo:film",
1534
+ name: "pixeldojo_film",
1225
1535
  description: "Make a finished short film from one idea: screenplay, storyboard frames (AI-reviewed), video clips with spoken dialogue, scene-synced soundtrack, stitched export. Phased: plan is free and returns a cost estimate; produce starts charged generation; status advances production (poll every ~30s).",
1226
1536
  inputZod: InputSchema8,
1227
1537
  inputJsonSchema: InputJsonSchema8,
@@ -1360,8 +1670,8 @@ var init_ad = __esm({
1360
1670
  // Zod refine; no root-level oneOf (Grok rejects non-object root unions).
1361
1671
  };
1362
1672
  adSkill = {
1363
- name: "pixeldojo:ad",
1364
- description: "Marketing Studio: turn any product into a video ad. Pass a product URL (auto-extracted) or product images, pick an ad style (UGC testimonial, unboxing, try-on, showcase, TV spot, and more via GET /api/v1/marketing/presets), optionally add presenter reference images, a location, a hook, and the exact spoken script. Renders with voiceover; returns the video or a statusUrl handoff to poll via pixeldojo:status. Ideal for automated generate-and-post workflows.",
1673
+ name: "pixeldojo_ad",
1674
+ 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.",
1365
1675
  inputZod: InputSchema9,
1366
1676
  inputJsonSchema: InputJsonSchema9,
1367
1677
  // Composition skill — the server routes to the ad engine internally.
@@ -1393,7 +1703,7 @@ async function run10(input, ctx) {
1393
1703
  }
1394
1704
  return {
1395
1705
  kind: "handoff",
1396
- text: result.message ?? `Campaign ${result.campaignId} queued. Poll via pixeldojo:campaign_status.`,
1706
+ text: result.message ?? `Campaign ${result.campaignId} queued. Poll via pixeldojo_campaign_status.`,
1397
1707
  jobId: result.campaignId,
1398
1708
  statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
1399
1709
  };
@@ -1446,8 +1756,8 @@ var init_campaign = __esm({
1446
1756
  oneOf: [{ required: ["productUrl"] }, { required: ["product"] }]
1447
1757
  };
1448
1758
  campaignSkill = {
1449
- name: "pixeldojo:campaign",
1450
- description: "Generate a coherent product launch package (hero image + N lifestyle variants + optional vertical video) from one URL or product profile. Composes hero / lifestyle / video models server-side; returns assets in one call or a handoff with campaignId to poll via pixeldojo:campaign_status.",
1759
+ name: "pixeldojo_campaign",
1760
+ 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.",
1451
1761
  inputZod: InputSchema10,
1452
1762
  inputJsonSchema: InputJsonSchema10,
1453
1763
  // Composition skill — internal sub-jobs route to per-tool models, no
@@ -1492,21 +1802,21 @@ var InputSchema11, InputJsonSchema11, campaignStatusSkill;
1492
1802
  var init_campaign_status = __esm({
1493
1803
  "src/skills/campaign-status.ts"() {
1494
1804
  InputSchema11 = zod.z.object({
1495
- campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
1805
+ campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo_campaign handoff.")
1496
1806
  });
1497
1807
  InputJsonSchema11 = {
1498
1808
  type: "object",
1499
1809
  properties: {
1500
1810
  campaignId: {
1501
1811
  type: "string",
1502
- description: "Campaign id returned from a prior pixeldojo:campaign handoff."
1812
+ description: "Campaign id returned from a prior pixeldojo_campaign handoff."
1503
1813
  }
1504
1814
  },
1505
1815
  required: ["campaignId"]
1506
1816
  };
1507
1817
  campaignStatusSkill = {
1508
- name: "pixeldojo:campaign_status",
1509
- description: "Poll a campaign by campaignId returned from pixeldojo:campaign. Returns assets when every sub-job is done, or a handoff describing what is still in flight.",
1818
+ name: "pixeldojo_campaign_status",
1819
+ 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.",
1510
1820
  inputZod: InputSchema11,
1511
1821
  inputJsonSchema: InputJsonSchema11,
1512
1822
  allowedModels: [],
@@ -1521,7 +1831,7 @@ async function run12(input, ctx) {
1521
1831
  url = await uploadLocalFile(input.path, ctx.client);
1522
1832
  } catch (err) {
1523
1833
  if (err instanceof SkillUploadError) {
1524
- return { kind: "text", text: `pixeldojo:upload: ${err.userMessage}` };
1834
+ return { kind: "text", text: `pixeldojo_upload: ${err.userMessage}` };
1525
1835
  }
1526
1836
  throw err;
1527
1837
  }
@@ -1551,7 +1861,7 @@ var init_upload = __esm({
1551
1861
  required: ["path"]
1552
1862
  };
1553
1863
  uploadSkill = {
1554
- name: "pixeldojo:upload",
1864
+ name: "pixeldojo_upload",
1555
1865
  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.",
1556
1866
  inputZod: InputSchema12,
1557
1867
  inputJsonSchema: InputJsonSchema12,
@@ -1566,7 +1876,7 @@ async function run13(input, ctx) {
1566
1876
  if (!ALLOWED4.includes(requested)) {
1567
1877
  return {
1568
1878
  kind: "text",
1569
- text: `Unknown model "${requested}" for pixeldojo:audio. Allowed: ${ALLOWED4.join(", ")}.`
1879
+ text: `Unknown model "${requested}" for pixeldojo_audio. Allowed: ${ALLOWED4.join(", ")}.`
1570
1880
  };
1571
1881
  }
1572
1882
  const apiId = requested;
@@ -1577,7 +1887,7 @@ async function run13(input, ctx) {
1577
1887
  audioUrls = [...audioUrls, ...uploaded].slice(0, 3);
1578
1888
  } catch (err) {
1579
1889
  if (err instanceof SkillUploadError) {
1580
- return { kind: "text", text: `pixeldojo:audio (upload): ${err.userMessage}` };
1890
+ return { kind: "text", text: `pixeldojo_audio (upload): ${err.userMessage}` };
1581
1891
  }
1582
1892
  throw err;
1583
1893
  }
@@ -1643,7 +1953,7 @@ var init_audio = __esm({
1643
1953
  required: ["prompt"]
1644
1954
  };
1645
1955
  audioSkill = {
1646
- name: "pixeldojo:audio",
1956
+ name: "pixeldojo_audio",
1647
1957
  description: "Generate audio: speech, multi-character dialogue, sound effects, and background music from one prompt (Seed Audio 1.0 by default; plain TTS available).",
1648
1958
  inputZod: InputSchema13,
1649
1959
  inputJsonSchema: InputJsonSchema13,
@@ -1663,7 +1973,7 @@ async function run14(input, ctx) {
1663
1973
  if (res.items.length === 0) {
1664
1974
  return {
1665
1975
  kind: "text",
1666
- 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."
1976
+ 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."
1667
1977
  };
1668
1978
  }
1669
1979
  const lines = res.items.map((it) => {
@@ -1704,8 +2014,8 @@ var init_library = __esm({
1704
2014
  required: []
1705
2015
  };
1706
2016
  librarySkill = {
1707
- name: "pixeldojo:library",
1708
- description: "Search your media library: saved My Media items (permanent) and recent API/MCP generations (expire after 24h), newest first. Returns asset URLs to reuse as reference inputs. Keep a result forever with pixeldojo:save.",
2017
+ name: "pixeldojo_library",
2018
+ 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.",
1709
2019
  inputZod: InputSchema14,
1710
2020
  inputJsonSchema: InputJsonSchema14,
1711
2021
  allowedModels: [],
@@ -1725,7 +2035,7 @@ async function run15(input, ctx) {
1725
2035
  return {
1726
2036
  kind: "text",
1727
2037
  text: `Saved to My Media (permanent): ${res.url}
1728
- It will appear at https://pixeldojo.ai/my-images and in pixeldojo:library searches.`
2038
+ It will appear at https://pixeldojo.ai/my-images and in pixeldojo_library searches.`
1729
2039
  };
1730
2040
  }
1731
2041
  var InputSchema15, InputJsonSchema15, saveSkill;
@@ -1750,7 +2060,7 @@ var init_save = __esm({
1750
2060
  required: ["url"]
1751
2061
  };
1752
2062
  saveSkill = {
1753
- name: "pixeldojo:save",
2063
+ name: "pixeldojo_save",
1754
2064
  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.",
1755
2065
  inputZod: InputSchema15,
1756
2066
  inputJsonSchema: InputJsonSchema15,
@@ -1798,7 +2108,7 @@ async function runSave(input, ctx) {
1798
2108
  });
1799
2109
  return {
1800
2110
  kind: "text",
1801
- 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"}).`
2111
+ 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"}).`
1802
2112
  };
1803
2113
  }
1804
2114
  async function runList(_input, ctx) {
@@ -1806,7 +2116,7 @@ async function runList(_input, ctx) {
1806
2116
  if (res.count === 0) {
1807
2117
  return {
1808
2118
  kind: "text",
1809
- text: "No saved workflows yet. Save one with pixeldojo:save_workflow { name, steps: [{ skill, args }] }."
2119
+ text: "No saved workflows yet. Save one with pixeldojo_save_workflow { name, steps: [{ skill, args }] }."
1810
2120
  };
1811
2121
  }
1812
2122
  const lines = res.workflows.map(
@@ -1875,7 +2185,7 @@ async function runWorkflow(input, ctx) {
1875
2185
  } else if (result.kind === "handoff") {
1876
2186
  return {
1877
2187
  kind: "text",
1878
- 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.`
2188
+ 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.`
1879
2189
  };
1880
2190
  } else {
1881
2191
  outputs.push(`step ${i + 1} (${step.skill}): done (no asset URL)`);
@@ -1894,7 +2204,7 @@ var init_workflows = __esm({
1894
2204
  init_types();
1895
2205
  WORKFLOW_STEP_EXTRA_BUDGET_MS = 6e4;
1896
2206
  StepSchema = zod.z.object({
1897
- skill: zod.z.string().regex(/^pixeldojo:(generate|edit|character|storyboard|upscale|audio|upload|from_url)$/, {
2207
+ skill: zod.z.string().regex(/^pixeldojo[:_](generate|edit|character|storyboard|upscale|audio|upload|from_url)$/, {
1898
2208
  message: "Runnable skills only: generate, edit, character, storyboard, upscale, audio, upload, from_url."
1899
2209
  }),
1900
2210
  args: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
@@ -1920,7 +2230,7 @@ var init_workflows = __esm({
1920
2230
  required: ["skill"]
1921
2231
  };
1922
2232
  saveWorkflowSkill = {
1923
- name: "pixeldojo:save_workflow",
2233
+ name: "pixeldojo_save_workflow",
1924
2234
  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.",
1925
2235
  inputZod: SaveInputSchema,
1926
2236
  inputJsonSchema: {
@@ -1937,7 +2247,7 @@ var init_workflows = __esm({
1937
2247
  run: runSave
1938
2248
  };
1939
2249
  runWorkflowSkill = {
1940
- name: "pixeldojo:run_workflow",
2250
+ name: "pixeldojo_run_workflow",
1941
2251
  description: "Run a saved workflow by name. Pass inputs for its {{input.<key>}} placeholders; each step feeds the next via {{prev}}.",
1942
2252
  inputZod: RunInputSchema,
1943
2253
  inputJsonSchema: {
@@ -1953,7 +2263,7 @@ var init_workflows = __esm({
1953
2263
  run: runWorkflow
1954
2264
  };
1955
2265
  listWorkflowsSkill = {
1956
- name: "pixeldojo:list_workflows",
2266
+ name: "pixeldojo_list_workflows",
1957
2267
  description: "List your saved workflows with step counts and run totals.",
1958
2268
  inputZod: ListInputSchema,
1959
2269
  inputJsonSchema: { type: "object", properties: {}, required: [] },
@@ -1989,7 +2299,8 @@ __export(skills_exports, {
1989
2299
  upscaleSkill: () => upscaleSkill
1990
2300
  });
1991
2301
  function findSkill(name) {
1992
- return exports.ALL_SKILLS.find((s) => s.name === name);
2302
+ const wanted = name.replace(/^pixeldojo:/, "pixeldojo_");
2303
+ return exports.ALL_SKILLS.find((s) => s.name === wanted);
1993
2304
  }
1994
2305
  exports.ALL_SKILLS = void 0;
1995
2306
  var init_skills = __esm({
@@ -2083,7 +2394,7 @@ function makeRedactedLogger(target = console) {
2083
2394
  }
2084
2395
 
2085
2396
  // src/server.ts
2086
- var PACKAGE_VERSION = "0.9.0" ;
2397
+ var PACKAGE_VERSION = "0.10.0" ;
2087
2398
  function buildServer(options) {
2088
2399
  if (!options.apiKey) {
2089
2400
  throw new Error(