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