@pixeldojo/mcp 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -115,6 +115,9 @@ var InputSchema = zod.z.object({
115
115
  model: zod.z.string().optional().describe(
116
116
  'Override the model. Any apiId from /api/v1/models (e.g. "flux", "google-nano-banana", "veo-3.1", "kling-video"). Omit to use the default.'
117
117
  ),
118
+ preset: zod.z.string().optional().describe(
119
+ 'Preset slug to seed the params (e.g. "cinematic-portrait", "product-floating-clean", "magazine-cover-text"). Browse the catalog at https://pixeldojo.ai/presets. Explicit fields override preset values.'
120
+ ),
118
121
  aspect_ratio: zod.z.string().optional().describe('Aspect ratio supported by the chosen model (e.g. "1:1", "16:9", "9:16").'),
119
122
  duration: zod.z.number().int().positive().optional().describe("Video duration in seconds, when applicable."),
120
123
  image_url: zod.z.string().url().optional().describe("Reference image URL for image-to-image / image-to-video tools.")
@@ -128,6 +131,10 @@ var InputJsonSchema = {
128
131
  type: "string",
129
132
  description: 'Override the model. Any apiId from /api/v1/models (e.g. "flux", "google-nano-banana", "veo-3.1", "kling-video"). Omit to use the default.'
130
133
  },
134
+ preset: {
135
+ type: "string",
136
+ description: 'Preset slug to seed the params (e.g. "cinematic-portrait", "product-floating-clean"). Explicit fields override preset values.'
137
+ },
131
138
  aspect_ratio: { type: "string" },
132
139
  duration: { type: "integer" },
133
140
  image_url: { type: "string", format: "uri" }
@@ -140,6 +147,7 @@ async function run(input, ctx) {
140
147
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
141
148
  if (input.duration !== void 0) body.duration = input.duration;
142
149
  if (input.image_url) body.image_url = input.image_url;
150
+ if (input.preset) body.preset = input.preset;
143
151
  const submitted = await ctx.client.run(apiId, body);
144
152
  const result = await pollJobUntilDoneOrHandoff(
145
153
  submitted.jobId,
@@ -360,6 +368,8 @@ var InputJsonSchema5 = {
360
368
  },
361
369
  required: ["jobId"]
362
370
  };
371
+ var STUCK_HINT_MS = 5 * 60 * 1e3;
372
+ var ABANDON_HINT_MS = 12 * 60 * 1e3;
363
373
  async function run5(input, ctx) {
364
374
  const job = await ctx.client.getJob(input.jobId);
365
375
  if (job.status === "completed") {
@@ -377,13 +387,44 @@ async function run5(input, ctx) {
377
387
  text: `Job ${input.jobId} failed: ${errMsg}`
378
388
  };
379
389
  }
390
+ const apiId = job.apiId ?? "unknown";
391
+ const ageMs = jobAgeMs(job);
392
+ const ageLabel = formatAge(ageMs);
393
+ let hint;
394
+ if (ageMs >= ABANDON_HINT_MS) {
395
+ hint = `That's well past the typical generation window for any model on PixelDojo. The job is almost certainly stuck. Submit a fresh request, or open ${ctx.buildStatusUrl(input.jobId)} for the raw record.`;
396
+ } else if (ageMs >= STUCK_HINT_MS) {
397
+ hint = `That's longer than most generations should take. Poll once more, or submit a fresh request if the next check still says ${job.status}.`;
398
+ } else {
399
+ hint = `Try again in a few seconds, or follow ${ctx.buildStatusUrl(input.jobId)}.`;
400
+ }
380
401
  return {
381
402
  kind: "handoff",
382
- text: `Job ${input.jobId} is still ${job.status}. Try again in a few seconds, or follow ${ctx.buildStatusUrl(input.jobId)}.`,
403
+ text: `Job ${input.jobId} (${apiId}) is ${job.status} after ${ageLabel}. ${hint}`,
383
404
  jobId: input.jobId,
384
405
  statusUrl: ctx.buildStatusUrl(input.jobId)
385
406
  };
386
407
  }
408
+ function jobAgeMs(job) {
409
+ const createdAt = job.createdAt;
410
+ if (!createdAt) return 0;
411
+ const t = Date.parse(createdAt);
412
+ if (Number.isNaN(t)) return 0;
413
+ const age = Date.now() - t;
414
+ return age >= 0 ? age : 0;
415
+ }
416
+ function formatAge(ms) {
417
+ if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))}s`;
418
+ const totalSeconds = Math.round(ms / 1e3);
419
+ const minutes = Math.floor(totalSeconds / 60);
420
+ const seconds = totalSeconds % 60;
421
+ if (minutes < 60) {
422
+ return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
423
+ }
424
+ const hours = Math.floor(minutes / 60);
425
+ const remMinutes = minutes % 60;
426
+ return remMinutes === 0 ? `${hours}h` : `${hours}h ${remMinutes}m`;
427
+ }
387
428
  function extractAssetUrls2(job) {
388
429
  if (job.assets && job.assets.length > 0) return job.assets.map((a) => a.url);
389
430
  const output = job.output;
@@ -403,6 +444,180 @@ var statusSkill = {
403
444
  defaultModel: "",
404
445
  run: run5
405
446
  };
447
+ var InputSchema6 = zod.z.object({
448
+ url: zod.z.string().min(1).max(2048).describe("Product page URL. Shopify, BigCommerce, WooCommerce, or any page with JSON-LD / OpenGraph product metadata.")
449
+ });
450
+ var InputJsonSchema6 = {
451
+ type: "object",
452
+ properties: {
453
+ url: {
454
+ type: "string",
455
+ description: "Product page URL. Shopify, BigCommerce, WooCommerce, or any page with JSON-LD / OpenGraph product metadata."
456
+ }
457
+ },
458
+ required: ["url"]
459
+ };
460
+ async function run6(input, ctx) {
461
+ const product = await ctx.client.extractProduct(input.url);
462
+ const summary = [
463
+ `Product: ${product.name}`,
464
+ product.description ? `Description: ${truncate(product.description, 280)}` : null,
465
+ `Images (${product.images.length}): ${product.images.slice(0, 3).join(", ")}${product.images.length > 3 ? `, +${product.images.length - 3} more` : ""}`,
466
+ `Source strategy: ${product.source}`
467
+ ].filter(Boolean).join("\n");
468
+ return {
469
+ kind: "text",
470
+ text: summary
471
+ };
472
+ }
473
+ function truncate(input, max) {
474
+ if (input.length <= max) return input;
475
+ return input.slice(0, max - 3) + "...";
476
+ }
477
+ var fromUrlSkill = {
478
+ name: "pixeldojo:from_url",
479
+ 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.",
480
+ inputZod: InputSchema6,
481
+ inputJsonSchema: InputJsonSchema6,
482
+ // No model dispatch — this skill calls a server-side scrape, not a generation.
483
+ allowedModels: [],
484
+ defaultModel: "",
485
+ run: run6
486
+ };
487
+ var InputSchema7 = zod.z.object({
488
+ productUrl: zod.z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
489
+ product: zod.z.object({
490
+ name: zod.z.string().min(1).max(200),
491
+ description: zod.z.string().max(2e3).optional(),
492
+ images: zod.z.array(zod.z.string().url()).max(5).optional()
493
+ }).optional().describe("Manual product fields, used when productUrl is omitted."),
494
+ tone: zod.z.string().max(120).optional().describe('Optional creative direction ("premium minimalist", "playful Y2K"). Threaded through every prompt.'),
495
+ variants: zod.z.number().int().min(1).max(8).optional().describe("Lifestyle shots in addition to the hero (default 4, max 8)."),
496
+ includeVideo: zod.z.boolean().optional().describe("Whether to also generate a vertical launch video (default true)."),
497
+ videoAspectRatio: zod.z.enum(["9:16", "16:9", "1:1"]).optional().describe("Aspect ratio of the launch video (default 9:16 for Reels/TikTok)."),
498
+ videoMode: zod.z.enum(["lifestyle", "cgi", "ugc", "skip"]).optional().describe("Video styling \u2014 lifestyle (default), cgi, ugc, or skip to omit video.")
499
+ }).refine((v) => v.productUrl || v.product, {
500
+ message: "productUrl or product is required",
501
+ path: ["productUrl"]
502
+ });
503
+ var InputJsonSchema7 = {
504
+ type: "object",
505
+ properties: {
506
+ productUrl: {
507
+ type: "string",
508
+ format: "uri",
509
+ description: "Product page URL. We extract name + description + images automatically."
510
+ },
511
+ product: {
512
+ type: "object",
513
+ properties: {
514
+ name: { type: "string" },
515
+ description: { type: "string" },
516
+ images: { type: "array", items: { type: "string", format: "uri" } }
517
+ },
518
+ required: ["name"]
519
+ },
520
+ tone: {
521
+ type: "string",
522
+ description: "Creative direction threaded through every prompt."
523
+ },
524
+ variants: { type: "integer", minimum: 1, maximum: 8 },
525
+ includeVideo: { type: "boolean" },
526
+ videoAspectRatio: { type: "string", enum: ["9:16", "16:9", "1:1"] },
527
+ videoMode: { type: "string", enum: ["lifestyle", "cgi", "ugc", "skip"] }
528
+ },
529
+ oneOf: [{ required: ["productUrl"] }, { required: ["product"] }]
530
+ };
531
+ async function run7(input, ctx) {
532
+ const client = ctx.client;
533
+ const result = await client.runCampaign(input);
534
+ if (result.kind === "campaign") {
535
+ const assets = result.assets ?? [];
536
+ const lines = [
537
+ `Campaign ${result.campaignId} for "${result.product?.name ?? "product"}":`,
538
+ ...assets.map(
539
+ (a) => ` - ${a.role}${a.variantIndex !== void 0 ? ` #${a.variantIndex}` : ""} (${a.kind}, ${a.apiId}): ${a.url}`
540
+ ),
541
+ "",
542
+ `Total credits: ${result.totalCreditCost ?? 0}.`,
543
+ ...result.warnings && result.warnings.length > 0 ? [`Warnings: ${result.warnings.join("; ")}`] : []
544
+ ];
545
+ return {
546
+ kind: "assets",
547
+ text: lines.join("\n"),
548
+ assetUrls: assets.map((a) => a.url)
549
+ };
550
+ }
551
+ return {
552
+ kind: "handoff",
553
+ text: result.message ?? `Campaign ${result.campaignId} queued. Poll via pixeldojo:campaign_status.`,
554
+ jobId: result.campaignId,
555
+ statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
556
+ };
557
+ }
558
+ var campaignSkill = {
559
+ name: "pixeldojo:campaign",
560
+ 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.",
561
+ inputZod: InputSchema7,
562
+ inputJsonSchema: InputJsonSchema7,
563
+ // Composition skill — internal sub-jobs route to per-tool models, no
564
+ // single allowed-models list applies here.
565
+ allowedModels: [],
566
+ defaultModel: "",
567
+ run: run7
568
+ };
569
+ var InputSchema8 = zod.z.object({
570
+ campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
571
+ });
572
+ var InputJsonSchema8 = {
573
+ type: "object",
574
+ properties: {
575
+ campaignId: {
576
+ type: "string",
577
+ description: "Campaign id returned from a prior pixeldojo:campaign handoff."
578
+ }
579
+ },
580
+ required: ["campaignId"]
581
+ };
582
+ async function run8(input, ctx) {
583
+ const client = ctx.client;
584
+ const result = await client.getCampaign(input.campaignId);
585
+ if (result.kind === "campaign") {
586
+ const assets = result.assets ?? [];
587
+ const lines = [
588
+ `Campaign ${result.campaignId} complete (${assets.length} assets):`,
589
+ ...assets.map(
590
+ (a) => ` - ${a.role}${a.variantIndex !== void 0 ? ` #${a.variantIndex}` : ""} (${a.kind}, ${a.apiId}): ${a.url}`
591
+ ),
592
+ "",
593
+ `Total credits: ${result.totalCreditCost ?? 0}.`,
594
+ ...result.warnings && result.warnings.length > 0 ? [`Warnings: ${result.warnings.join("; ")}`] : []
595
+ ];
596
+ return {
597
+ kind: "assets",
598
+ text: lines.join("\n"),
599
+ assetUrls: assets.map((a) => a.url)
600
+ };
601
+ }
602
+ const stillRunning = (result.subJobs ?? []).filter(
603
+ (s) => s.status !== "completed" && s.status !== "failed"
604
+ );
605
+ return {
606
+ kind: "handoff",
607
+ text: `Campaign ${result.campaignId}: ` + (stillRunning.length > 0 ? `${stillRunning.length} of ${result.subJobs?.length ?? 0} sub-jobs still ${stillRunning[0].status}. Wait a few seconds, then poll again.` : `aggregate state pending. Poll again shortly.`),
608
+ jobId: result.campaignId,
609
+ statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
610
+ };
611
+ }
612
+ var campaignStatusSkill = {
613
+ name: "pixeldojo:campaign_status",
614
+ 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.",
615
+ inputZod: InputSchema8,
616
+ inputJsonSchema: InputJsonSchema8,
617
+ allowedModels: [],
618
+ defaultModel: "",
619
+ run: run8
620
+ };
406
621
 
407
622
  // src/skills/index.ts
408
623
  var ALL_SKILLS = [
@@ -410,7 +625,10 @@ var ALL_SKILLS = [
410
625
  characterSkill,
411
626
  storyboardSkill,
412
627
  upscaleSkill,
413
- statusSkill
628
+ statusSkill,
629
+ fromUrlSkill,
630
+ campaignSkill,
631
+ campaignStatusSkill
414
632
  ];
415
633
  function findSkill(name) {
416
634
  return ALL_SKILLS.find((s) => s.name === name);