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