@pixeldojo/mcp 0.1.1 → 0.3.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.cjs CHANGED
@@ -5,6 +5,8 @@ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
5
5
  var types_js = require('@modelcontextprotocol/sdk/types.js');
6
6
  var sdk = require('@pixeldojo/sdk');
7
7
  var zod = require('zod');
8
+ var promises = require('fs/promises');
9
+ var path = require('path');
8
10
 
9
11
  // src/server.ts
10
12
  function makeAttributedClient(options) {
@@ -108,6 +110,71 @@ function extractAssetUrls(job) {
108
110
  if (output?.video) return [output.video];
109
111
  return [];
110
112
  }
113
+ var MAX_IMAGE_BYTES = 50 * 1024 * 1024;
114
+ var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
115
+ var EXT_TO_MIME = {
116
+ ".jpg": "image/jpeg",
117
+ ".jpeg": "image/jpeg",
118
+ ".png": "image/png",
119
+ ".webp": "image/webp",
120
+ ".gif": "image/gif",
121
+ ".mp4": "video/mp4",
122
+ ".webm": "video/webm",
123
+ ".mov": "video/quicktime"
124
+ };
125
+ var SkillUploadError = class extends Error {
126
+ constructor(userMessage) {
127
+ super(userMessage);
128
+ this.userMessage = userMessage;
129
+ this.name = "SkillUploadError";
130
+ }
131
+ userMessage;
132
+ };
133
+ async function uploadLocalFile(path$1, client) {
134
+ const ext = path.extname(path$1).toLowerCase();
135
+ const mime = EXT_TO_MIME[ext];
136
+ if (!mime) {
137
+ throw new SkillUploadError(
138
+ `Unsupported file extension "${ext || "(none)"}" for "${path$1}". Allowed: ${Object.keys(EXT_TO_MIME).join(", ")}.`
139
+ );
140
+ }
141
+ let size;
142
+ try {
143
+ const info = await promises.stat(path$1);
144
+ if (!info.isFile()) {
145
+ throw new SkillUploadError(`"${path$1}" is not a file.`);
146
+ }
147
+ size = info.size;
148
+ } catch (err) {
149
+ if (err instanceof SkillUploadError) throw err;
150
+ const code = err?.code;
151
+ if (code === "ENOENT") {
152
+ throw new SkillUploadError(
153
+ `File not found at "${path$1}". Pass an absolute path.`
154
+ );
155
+ }
156
+ throw new SkillUploadError(
157
+ `Cannot read "${path$1}": ${err instanceof Error ? err.message : String(err)}`
158
+ );
159
+ }
160
+ const isImage = mime.startsWith("image/");
161
+ const limit = isImage ? MAX_IMAGE_BYTES : MAX_VIDEO_BYTES;
162
+ if (size > limit) {
163
+ const gotMB = (size / 1024 / 1024).toFixed(1);
164
+ const limitMB = (limit / 1024 / 1024).toFixed(0);
165
+ throw new SkillUploadError(
166
+ `"${path.basename(path$1)}" is ${gotMB}MB; limit for ${isImage ? "images" : "videos"} is ${limitMB}MB. Downscale or re-encode and try again.`
167
+ );
168
+ }
169
+ const buffer = await promises.readFile(path$1);
170
+ const blob = new Blob([new Uint8Array(buffer)], { type: mime });
171
+ const filename = path.basename(path$1);
172
+ const response = await client.uploadImage(blob, filename);
173
+ return response.url;
174
+ }
175
+ async function uploadLocalFiles(paths, client) {
176
+ return Promise.all(paths.map((p) => uploadLocalFile(p, client)));
177
+ }
111
178
 
112
179
  // src/skills/generate.ts
113
180
  var InputSchema = zod.z.object({
@@ -115,9 +182,15 @@ var InputSchema = zod.z.object({
115
182
  model: zod.z.string().optional().describe(
116
183
  '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
184
  ),
185
+ preset: zod.z.string().optional().describe(
186
+ '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.'
187
+ ),
118
188
  aspect_ratio: zod.z.string().optional().describe('Aspect ratio supported by the chosen model (e.g. "1:1", "16:9", "9:16").'),
119
189
  duration: zod.z.number().int().positive().optional().describe("Video duration in seconds, when applicable."),
120
- image_url: zod.z.string().url().optional().describe("Reference image URL for image-to-image / image-to-video tools.")
190
+ image_url: zod.z.string().url().optional().describe("Reference image URL for image-to-image / image-to-video tools."),
191
+ image_path: zod.z.string().min(1).optional().describe(
192
+ "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."
193
+ )
121
194
  });
122
195
  var DEFAULT_MODEL = "google-nano-banana";
123
196
  var InputJsonSchema = {
@@ -128,18 +201,73 @@ var InputJsonSchema = {
128
201
  type: "string",
129
202
  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
203
  },
204
+ preset: {
205
+ type: "string",
206
+ description: 'Preset slug to seed the params (e.g. "cinematic-portrait", "product-floating-clean"). Explicit fields override preset values.'
207
+ },
131
208
  aspect_ratio: { type: "string" },
132
209
  duration: { type: "integer" },
133
- image_url: { type: "string", format: "uri" }
210
+ image_url: { type: "string", format: "uri" },
211
+ image_path: {
212
+ type: "string",
213
+ 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."
214
+ }
134
215
  },
135
216
  required: ["prompt"]
136
217
  };
137
- async function run(input, ctx) {
138
- const apiId = input.model ?? DEFAULT_MODEL;
218
+ function buildPayload(apiId, input) {
139
219
  const body = { prompt: input.prompt };
140
220
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
141
- if (input.duration !== void 0) body.duration = input.duration;
142
- if (input.image_url) body.image_url = input.image_url;
221
+ if (input.preset) body.preset = input.preset;
222
+ switch (apiId) {
223
+ case "grok-video-extend":
224
+ case "wan-video-edit":
225
+ case "seedance-video-edit":
226
+ if (input.image_url) {
227
+ body.video_url = input.image_url;
228
+ body.image_url = input.image_url;
229
+ }
230
+ if (input.duration !== void 0) {
231
+ body.video_duration = input.duration;
232
+ body.duration = input.duration;
233
+ }
234
+ break;
235
+ default:
236
+ if (input.image_url) body.image_url = input.image_url;
237
+ if (input.duration !== void 0) body.duration = input.duration;
238
+ break;
239
+ }
240
+ return body;
241
+ }
242
+ async function run(input, ctx) {
243
+ const apiId = input.model ?? DEFAULT_MODEL;
244
+ let resolved = input;
245
+ if (!input.image_url && input.image_path) {
246
+ try {
247
+ const uploadedUrl = await uploadLocalFile(input.image_path, ctx.client);
248
+ resolved = { ...input, image_url: uploadedUrl };
249
+ } catch (err) {
250
+ if (err instanceof SkillUploadError) {
251
+ return { kind: "text", text: `pixeldojo:generate (upload): ${err.userMessage}` };
252
+ }
253
+ throw err;
254
+ }
255
+ }
256
+ if (apiId === "grok-video-extend") {
257
+ if (!resolved.image_url) {
258
+ return {
259
+ kind: "text",
260
+ text: 'pixeldojo:generate with model="grok-video-extend" requires image_url (the source video URL to extend, or image_path for a local file).'
261
+ };
262
+ }
263
+ if (resolved.duration === void 0) {
264
+ return {
265
+ kind: "text",
266
+ text: 'pixeldojo:generate with model="grok-video-extend" requires duration (extension length in seconds).'
267
+ };
268
+ }
269
+ }
270
+ const body = buildPayload(apiId, resolved);
143
271
  const submitted = await ctx.client.run(apiId, body);
144
272
  const result = await pollJobUntilDoneOrHandoff(
145
273
  submitted.jobId,
@@ -157,39 +285,191 @@ async function run(input, ctx) {
157
285
  }
158
286
  var generateSkill = {
159
287
  name: "pixeldojo:generate",
160
- 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.",
288
+ 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.",
161
289
  inputZod: InputSchema,
162
290
  inputJsonSchema: InputJsonSchema,
163
291
  allowedModels: null,
164
292
  defaultModel: DEFAULT_MODEL,
165
293
  run
166
294
  };
167
- var ALLOWED = ["ideogram-character", "flux-edit", "gpt-image-2-edit"];
168
- var DEFAULT_MODEL2 = "ideogram-character";
295
+ var DEFAULT_MODEL2 = "wan-2.7-image-edit";
296
+ var ALLOWED_MODELS = [
297
+ "wan-2.7-image-edit",
298
+ "wan-2.6-image-edit",
299
+ "flux-edit",
300
+ "xai-image-edit",
301
+ "p-image-edit",
302
+ "gpt-image-2-edit",
303
+ "hidream-edit",
304
+ "google-nano-banana"
305
+ ];
169
306
  var InputSchema2 = zod.z.object({
307
+ prompt: zod.z.string().min(1).max(5e3).describe("Edit instruction."),
308
+ image_url: zod.z.string().url().optional().describe("Single reference image to edit. Pass image_urls for multi-image edits."),
309
+ image_urls: zod.z.array(zod.z.string().url()).min(1).max(9).optional().describe("Multiple reference images (1-9). Used for composition/multi-ref edits."),
310
+ image_path: zod.z.string().min(1).optional().describe(
311
+ "Absolute local file path. Skill uploads it (24h temp storage) and edits it. Prefer over image_url when the user has the file on disk."
312
+ ),
313
+ image_paths: zod.z.array(zod.z.string().min(1)).min(1).max(9).optional().describe(
314
+ "Multiple absolute local file paths (1-9) for multi-image edits. Each uploaded to 24h temp storage before dispatch."
315
+ ),
316
+ model: zod.z.enum(ALLOWED_MODELS).optional().describe(
317
+ "Override the default edit model. Defaults to wan-2.7-image-edit (most popular)."
318
+ ),
319
+ aspect_ratio: zod.z.string().optional().describe('Aspect ratio when supported (e.g. "1:1", "16:9", "match_input_image").')
320
+ });
321
+ var InputJsonSchema2 = {
322
+ type: "object",
323
+ properties: {
324
+ prompt: { type: "string", description: "Edit instruction." },
325
+ image_url: {
326
+ type: "string",
327
+ format: "uri",
328
+ description: "Single reference image. Pass image_urls for multi-image edits."
329
+ },
330
+ image_urls: {
331
+ type: "array",
332
+ items: { type: "string", format: "uri" },
333
+ minItems: 1,
334
+ maxItems: 9,
335
+ description: "Multiple reference images (1-9)."
336
+ },
337
+ image_path: {
338
+ type: "string",
339
+ description: "Absolute local file path. Skill uploads it (24h temp storage) and edits the resulting URL. Prefer over image_url when the user has a local file."
340
+ },
341
+ image_paths: {
342
+ type: "array",
343
+ items: { type: "string" },
344
+ minItems: 1,
345
+ maxItems: 9,
346
+ description: "Multiple absolute local file paths (1-9). Each uploaded before dispatch."
347
+ },
348
+ model: {
349
+ type: "string",
350
+ enum: [...ALLOWED_MODELS],
351
+ description: "Override edit model. Default: wan-2.7-image-edit."
352
+ },
353
+ aspect_ratio: { type: "string" }
354
+ },
355
+ required: ["prompt"]
356
+ };
357
+ function buildPayload2(apiId, input) {
358
+ const urls = input.image_urls && input.image_urls.length > 0 ? input.image_urls : input.image_url ? [input.image_url] : [];
359
+ switch (apiId) {
360
+ case "wan-2.7-image-edit":
361
+ case "wan-2.6-image-edit":
362
+ return {
363
+ prompt: input.prompt,
364
+ image_urls: urls
365
+ // wan-edit uses `size` not aspect_ratio. Default 2K, let model defaults handle it otherwise.
366
+ };
367
+ case "p-image-edit":
368
+ return {
369
+ prompt: input.prompt,
370
+ images: urls.slice(0, 5),
371
+ ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
372
+ };
373
+ case "flux-edit":
374
+ case "xai-image-edit":
375
+ case "gpt-image-2-edit":
376
+ case "hidream-edit":
377
+ return {
378
+ prompt: input.prompt,
379
+ image_url: urls[0],
380
+ ...urls.length > 1 ? { image_urls: urls } : {},
381
+ ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
382
+ };
383
+ case "google-nano-banana":
384
+ return {
385
+ prompt: input.prompt,
386
+ image_input: urls,
387
+ ...input.aspect_ratio ? { aspect_ratio: input.aspect_ratio } : {}
388
+ };
389
+ default:
390
+ return {
391
+ prompt: input.prompt,
392
+ image_url: urls[0]
393
+ };
394
+ }
395
+ }
396
+ async function run2(input, ctx) {
397
+ let resolved = input;
398
+ try {
399
+ if (input.image_paths && input.image_paths.length > 0) {
400
+ const urls = await uploadLocalFiles(input.image_paths, ctx.client);
401
+ resolved = {
402
+ ...input,
403
+ image_urls: [...input.image_urls ?? [], ...urls]
404
+ };
405
+ } else if (input.image_path && !input.image_url) {
406
+ const url = await uploadLocalFile(input.image_path, ctx.client);
407
+ resolved = { ...input, image_url: url };
408
+ }
409
+ } catch (err) {
410
+ if (err instanceof SkillUploadError) {
411
+ return { kind: "text", text: `pixeldojo:edit (upload): ${err.userMessage}` };
412
+ }
413
+ throw err;
414
+ }
415
+ if (!resolved.image_url && (!resolved.image_urls || resolved.image_urls.length === 0)) {
416
+ return {
417
+ kind: "text",
418
+ text: "pixeldojo:edit requires at least one reference image. Pass image_url / image_urls (hosted URL) or image_path / image_paths (local file)."
419
+ };
420
+ }
421
+ const apiId = resolved.model ?? DEFAULT_MODEL2;
422
+ const body = buildPayload2(apiId, resolved);
423
+ const submitted = await ctx.client.run(apiId, body);
424
+ const result = await pollJobUntilDoneOrHandoff(
425
+ submitted.jobId,
426
+ (id) => ctx.client.getJob(id),
427
+ ctx.buildStatusUrl,
428
+ {
429
+ now: ctx.pollOverrides?.now,
430
+ sleep: ctx.pollOverrides?.sleep
431
+ }
432
+ );
433
+ return pollResultToSkillResult(
434
+ result,
435
+ (urls) => urls.length === 0 ? `Edit job ${submitted.jobId} completed but returned no assets.` : urls.length === 1 ? `Edit complete. Asset: ${urls[0]}` : `Edit produced ${urls.length} assets. URLs: ${urls.join(", ")}`
436
+ );
437
+ }
438
+ var editSkill = {
439
+ name: "pixeldojo:edit",
440
+ 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).",
441
+ inputZod: InputSchema2,
442
+ inputJsonSchema: InputJsonSchema2,
443
+ allowedModels: [...ALLOWED_MODELS],
444
+ defaultModel: DEFAULT_MODEL2,
445
+ run: run2
446
+ };
447
+ var ALLOWED = ["ideogram-character", "flux-edit", "gpt-image-2-edit"];
448
+ var DEFAULT_MODEL3 = "ideogram-character";
449
+ var InputSchema3 = zod.z.object({
170
450
  prompt: zod.z.string().min(1).max(8e3),
171
- model: zod.z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL2}.`),
451
+ model: zod.z.string().optional().describe(`One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
172
452
  reference_image_url: zod.z.string().url().optional().describe(
173
453
  "Reference image of the character. Required by flux-edit and gpt-image-2-edit; optional for ideogram-character."
174
454
  ),
175
455
  aspect_ratio: zod.z.string().optional()
176
456
  });
177
- var InputJsonSchema2 = {
457
+ var InputJsonSchema3 = {
178
458
  type: "object",
179
459
  properties: {
180
460
  prompt: { type: "string" },
181
461
  model: {
182
462
  type: "string",
183
463
  enum: ALLOWED,
184
- description: `One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL2}.`
464
+ description: `One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`
185
465
  },
186
466
  reference_image_url: { type: "string", format: "uri" },
187
467
  aspect_ratio: { type: "string" }
188
468
  },
189
469
  required: ["prompt"]
190
470
  };
191
- async function run2(input, ctx) {
192
- const requested = input.model ?? DEFAULT_MODEL2;
471
+ async function run3(input, ctx) {
472
+ const requested = input.model ?? DEFAULT_MODEL3;
193
473
  if (!ALLOWED.includes(requested)) {
194
474
  return {
195
475
  kind: "text",
@@ -197,6 +477,12 @@ async function run2(input, ctx) {
197
477
  };
198
478
  }
199
479
  const apiId = requested;
480
+ if ((apiId === "flux-edit" || apiId === "gpt-image-2-edit") && !input.reference_image_url) {
481
+ return {
482
+ kind: "text",
483
+ text: `pixeldojo:character with model="${apiId}" requires reference_image_url (the source image of the character to edit). For generation from a prompt alone, use model="ideogram-character".`
484
+ };
485
+ }
200
486
  const body = { prompt: input.prompt };
201
487
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
202
488
  if (input.reference_image_url) {
@@ -221,21 +507,21 @@ async function run2(input, ctx) {
221
507
  var characterSkill = {
222
508
  name: "pixeldojo:character",
223
509
  description: "Generate or edit consistent characters across shots. Pass a reference_image_url for edit-style models.",
224
- inputZod: InputSchema2,
225
- inputJsonSchema: InputJsonSchema2,
510
+ inputZod: InputSchema3,
511
+ inputJsonSchema: InputJsonSchema3,
226
512
  allowedModels: [...ALLOWED],
227
- defaultModel: DEFAULT_MODEL2,
228
- run: run2
513
+ defaultModel: DEFAULT_MODEL3,
514
+ run: run3
229
515
  };
230
516
  var ALLOWED2 = ["veo-3.1", "kling-video", "seedance-2"];
231
- var DEFAULT_MODEL3 = "veo-3.1";
232
- var InputSchema3 = zod.z.object({
517
+ var DEFAULT_MODEL4 = "veo-3.1";
518
+ var InputSchema4 = zod.z.object({
233
519
  brief: zod.z.string().min(1).max(8e3).describe("Storyboard brief: scene, shots, mood, action."),
234
- model: zod.z.string().optional().describe(`One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`),
520
+ model: zod.z.string().optional().describe(`One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`),
235
521
  aspect_ratio: zod.z.string().optional(),
236
522
  duration: zod.z.number().int().positive().optional()
237
523
  });
238
- var InputJsonSchema3 = {
524
+ var InputJsonSchema4 = {
239
525
  type: "object",
240
526
  properties: {
241
527
  brief: {
@@ -245,15 +531,15 @@ var InputJsonSchema3 = {
245
531
  model: {
246
532
  type: "string",
247
533
  enum: ALLOWED2,
248
- description: `One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`
534
+ description: `One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`
249
535
  },
250
536
  aspect_ratio: { type: "string" },
251
537
  duration: { type: "integer" }
252
538
  },
253
539
  required: ["brief"]
254
540
  };
255
- async function run3(input, ctx) {
256
- const requested = input.model ?? DEFAULT_MODEL3;
541
+ async function run4(input, ctx) {
542
+ const requested = input.model ?? DEFAULT_MODEL4;
257
543
  if (!ALLOWED2.includes(requested)) {
258
544
  return {
259
545
  kind: "text",
@@ -282,21 +568,21 @@ async function run3(input, ctx) {
282
568
  var storyboardSkill = {
283
569
  name: "pixeldojo:storyboard",
284
570
  description: "Generate a video storyboard preview from a single brief. v1: single-shot preview; multi-shot orchestration coming.",
285
- inputZod: InputSchema3,
286
- inputJsonSchema: InputJsonSchema3,
571
+ inputZod: InputSchema4,
572
+ inputJsonSchema: InputJsonSchema4,
287
573
  allowedModels: [...ALLOWED2],
288
- defaultModel: DEFAULT_MODEL3,
289
- run: run3
574
+ defaultModel: DEFAULT_MODEL4,
575
+ run: run4
290
576
  };
291
577
  var ALLOWED3 = ["magnific-upscaler", "creative-upscale", "face-enhance"];
292
- var DEFAULT_MODEL4 = "magnific-upscaler";
293
- var InputSchema4 = zod.z.object({
578
+ var DEFAULT_MODEL5 = "magnific-upscaler";
579
+ var InputSchema5 = zod.z.object({
294
580
  image_url: zod.z.string().url().describe("URL of the source image to upscale."),
295
- model: zod.z.string().optional().describe(`One of: ${ALLOWED3.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`),
581
+ model: zod.z.string().optional().describe(`One of: ${ALLOWED3.join(", ")}. Defaults to ${DEFAULT_MODEL5}.`),
296
582
  scale_factor: zod.z.number().int().min(1).max(4).optional().describe("Magnific only: 1, 2, or 4. Defaults to 2."),
297
583
  creativity: zod.z.number().min(0).max(1).optional().describe("Magnific only: 0 (preserve) \u2192 1 (creative). Defaults to 0.")
298
584
  });
299
- var InputJsonSchema4 = {
585
+ var InputJsonSchema5 = {
300
586
  type: "object",
301
587
  properties: {
302
588
  image_url: { type: "string", format: "uri" },
@@ -306,8 +592,8 @@ var InputJsonSchema4 = {
306
592
  },
307
593
  required: ["image_url"]
308
594
  };
309
- async function run4(input, ctx) {
310
- const requested = input.model ?? DEFAULT_MODEL4;
595
+ async function run5(input, ctx) {
596
+ const requested = input.model ?? DEFAULT_MODEL5;
311
597
  if (!ALLOWED3.includes(requested)) {
312
598
  return {
313
599
  kind: "text",
@@ -341,16 +627,16 @@ async function run4(input, ctx) {
341
627
  var upscaleSkill = {
342
628
  name: "pixeldojo:upscale",
343
629
  description: "Enhance an image. Magnific (default) supports configurable scale_factor and creativity; face-enhance for portraits.",
344
- inputZod: InputSchema4,
345
- inputJsonSchema: InputJsonSchema4,
630
+ inputZod: InputSchema5,
631
+ inputJsonSchema: InputJsonSchema5,
346
632
  allowedModels: [...ALLOWED3],
347
- defaultModel: DEFAULT_MODEL4,
348
- run: run4
633
+ defaultModel: DEFAULT_MODEL5,
634
+ run: run5
349
635
  };
350
- var InputSchema5 = zod.z.object({
636
+ var InputSchema6 = zod.z.object({
351
637
  jobId: zod.z.string().min(1).max(200).describe("Job id returned from a prior pixeldojo:* skill handoff.")
352
638
  });
353
- var InputJsonSchema5 = {
639
+ var InputJsonSchema6 = {
354
640
  type: "object",
355
641
  properties: {
356
642
  jobId: {
@@ -362,7 +648,7 @@ var InputJsonSchema5 = {
362
648
  };
363
649
  var STUCK_HINT_MS = 5 * 60 * 1e3;
364
650
  var ABANDON_HINT_MS = 12 * 60 * 1e3;
365
- async function run5(input, ctx) {
651
+ async function run6(input, ctx) {
366
652
  const job = await ctx.client.getJob(input.jobId);
367
653
  if (job.status === "completed") {
368
654
  const urls = extractAssetUrls2(job);
@@ -429,21 +715,240 @@ function extractAssetUrls2(job) {
429
715
  var statusSkill = {
430
716
  name: "pixeldojo:status",
431
717
  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.",
432
- inputZod: InputSchema5,
433
- inputJsonSchema: InputJsonSchema5,
718
+ inputZod: InputSchema6,
719
+ inputJsonSchema: InputJsonSchema6,
434
720
  // status is special — it doesn't dispatch to a model
435
721
  allowedModels: [],
436
722
  defaultModel: "",
437
- run: run5
723
+ run: run6
724
+ };
725
+ var InputSchema7 = zod.z.object({
726
+ url: zod.z.string().min(1).max(2048).describe("Product page URL. Shopify, BigCommerce, WooCommerce, or any page with JSON-LD / OpenGraph product metadata.")
727
+ });
728
+ var InputJsonSchema7 = {
729
+ type: "object",
730
+ properties: {
731
+ url: {
732
+ type: "string",
733
+ description: "Product page URL. Shopify, BigCommerce, WooCommerce, or any page with JSON-LD / OpenGraph product metadata."
734
+ }
735
+ },
736
+ required: ["url"]
737
+ };
738
+ async function run7(input, ctx) {
739
+ const product = await ctx.client.extractProduct(input.url);
740
+ const summary = [
741
+ `Product: ${product.name}`,
742
+ product.description ? `Description: ${truncate(product.description, 280)}` : null,
743
+ `Images (${product.images.length}): ${product.images.slice(0, 3).join(", ")}${product.images.length > 3 ? `, +${product.images.length - 3} more` : ""}`,
744
+ `Source strategy: ${product.source}`
745
+ ].filter(Boolean).join("\n");
746
+ return {
747
+ kind: "text",
748
+ text: summary
749
+ };
750
+ }
751
+ function truncate(input, max) {
752
+ if (input.length <= max) return input;
753
+ return input.slice(0, max - 3) + "...";
754
+ }
755
+ var fromUrlSkill = {
756
+ name: "pixeldojo:from_url",
757
+ 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.",
758
+ inputZod: InputSchema7,
759
+ inputJsonSchema: InputJsonSchema7,
760
+ // No model dispatch — this skill calls a server-side scrape, not a generation.
761
+ allowedModels: [],
762
+ defaultModel: "",
763
+ run: run7
764
+ };
765
+ var InputSchema8 = zod.z.object({
766
+ productUrl: zod.z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
767
+ product: zod.z.object({
768
+ name: zod.z.string().min(1).max(200),
769
+ description: zod.z.string().max(2e3).optional(),
770
+ images: zod.z.array(zod.z.string().url()).max(5).optional()
771
+ }).optional().describe("Manual product fields, used when productUrl is omitted."),
772
+ tone: zod.z.string().max(120).optional().describe('Optional creative direction ("premium minimalist", "playful Y2K"). Threaded through every prompt.'),
773
+ variants: zod.z.number().int().min(1).max(8).optional().describe("Lifestyle shots in addition to the hero (default 4, max 8)."),
774
+ includeVideo: zod.z.boolean().optional().describe("Whether to also generate a vertical launch video (default true)."),
775
+ videoAspectRatio: zod.z.enum(["9:16", "16:9", "1:1"]).optional().describe("Aspect ratio of the launch video (default 9:16 for Reels/TikTok)."),
776
+ videoMode: zod.z.enum(["lifestyle", "cgi", "ugc", "skip"]).optional().describe("Video styling \u2014 lifestyle (default), cgi, ugc, or skip to omit video.")
777
+ }).refine((v) => v.productUrl || v.product, {
778
+ message: "productUrl or product is required",
779
+ path: ["productUrl"]
780
+ });
781
+ var InputJsonSchema8 = {
782
+ type: "object",
783
+ properties: {
784
+ productUrl: {
785
+ type: "string",
786
+ format: "uri",
787
+ description: "Product page URL. We extract name + description + images automatically."
788
+ },
789
+ product: {
790
+ type: "object",
791
+ properties: {
792
+ name: { type: "string" },
793
+ description: { type: "string" },
794
+ images: { type: "array", items: { type: "string", format: "uri" } }
795
+ },
796
+ required: ["name"]
797
+ },
798
+ tone: {
799
+ type: "string",
800
+ description: "Creative direction threaded through every prompt."
801
+ },
802
+ variants: { type: "integer", minimum: 1, maximum: 8 },
803
+ includeVideo: { type: "boolean" },
804
+ videoAspectRatio: { type: "string", enum: ["9:16", "16:9", "1:1"] },
805
+ videoMode: { type: "string", enum: ["lifestyle", "cgi", "ugc", "skip"] }
806
+ },
807
+ oneOf: [{ required: ["productUrl"] }, { required: ["product"] }]
808
+ };
809
+ async function run8(input, ctx) {
810
+ const client = ctx.client;
811
+ const result = await client.runCampaign(input);
812
+ if (result.kind === "campaign") {
813
+ const assets = result.assets ?? [];
814
+ const lines = [
815
+ `Campaign ${result.campaignId} for "${result.product?.name ?? "product"}":`,
816
+ ...assets.map(
817
+ (a) => ` - ${a.role}${a.variantIndex !== void 0 ? ` #${a.variantIndex}` : ""} (${a.kind}, ${a.apiId}): ${a.url}`
818
+ ),
819
+ "",
820
+ `Total credits: ${result.totalCreditCost ?? 0}.`,
821
+ ...result.warnings && result.warnings.length > 0 ? [`Warnings: ${result.warnings.join("; ")}`] : []
822
+ ];
823
+ return {
824
+ kind: "assets",
825
+ text: lines.join("\n"),
826
+ assetUrls: assets.map((a) => a.url)
827
+ };
828
+ }
829
+ return {
830
+ kind: "handoff",
831
+ text: result.message ?? `Campaign ${result.campaignId} queued. Poll via pixeldojo:campaign_status.`,
832
+ jobId: result.campaignId,
833
+ statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
834
+ };
835
+ }
836
+ var campaignSkill = {
837
+ name: "pixeldojo:campaign",
838
+ 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.",
839
+ inputZod: InputSchema8,
840
+ inputJsonSchema: InputJsonSchema8,
841
+ // Composition skill — internal sub-jobs route to per-tool models, no
842
+ // single allowed-models list applies here.
843
+ allowedModels: [],
844
+ defaultModel: "",
845
+ run: run8
846
+ };
847
+ var InputSchema9 = zod.z.object({
848
+ campaignId: zod.z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
849
+ });
850
+ var InputJsonSchema9 = {
851
+ type: "object",
852
+ properties: {
853
+ campaignId: {
854
+ type: "string",
855
+ description: "Campaign id returned from a prior pixeldojo:campaign handoff."
856
+ }
857
+ },
858
+ required: ["campaignId"]
859
+ };
860
+ async function run9(input, ctx) {
861
+ const client = ctx.client;
862
+ const result = await client.getCampaign(input.campaignId);
863
+ if (result.kind === "campaign") {
864
+ const assets = result.assets ?? [];
865
+ const lines = [
866
+ `Campaign ${result.campaignId} complete (${assets.length} assets):`,
867
+ ...assets.map(
868
+ (a) => ` - ${a.role}${a.variantIndex !== void 0 ? ` #${a.variantIndex}` : ""} (${a.kind}, ${a.apiId}): ${a.url}`
869
+ ),
870
+ "",
871
+ `Total credits: ${result.totalCreditCost ?? 0}.`,
872
+ ...result.warnings && result.warnings.length > 0 ? [`Warnings: ${result.warnings.join("; ")}`] : []
873
+ ];
874
+ return {
875
+ kind: "assets",
876
+ text: lines.join("\n"),
877
+ assetUrls: assets.map((a) => a.url)
878
+ };
879
+ }
880
+ const stillRunning = (result.subJobs ?? []).filter(
881
+ (s) => s.status !== "completed" && s.status !== "failed"
882
+ );
883
+ return {
884
+ kind: "handoff",
885
+ 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.`),
886
+ jobId: result.campaignId,
887
+ statusUrl: result.statusUrl ?? ctx.buildStatusUrl(result.campaignId)
888
+ };
889
+ }
890
+ var campaignStatusSkill = {
891
+ name: "pixeldojo:campaign_status",
892
+ 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.",
893
+ inputZod: InputSchema9,
894
+ inputJsonSchema: InputJsonSchema9,
895
+ allowedModels: [],
896
+ defaultModel: "",
897
+ run: run9
898
+ };
899
+ var InputSchema10 = zod.z.object({
900
+ path: zod.z.string().min(1).max(4096).describe(
901
+ "Absolute local file path to upload. JPEG, PNG, WebP, GIF (\u226450MB) or MP4, WebM, MOV (\u2264200MB). Returns a public URL on the temp CDN; storage auto-expires after 24 hours."
902
+ )
903
+ });
904
+ var InputJsonSchema10 = {
905
+ type: "object",
906
+ properties: {
907
+ path: {
908
+ type: "string",
909
+ description: "Absolute local file path. Allowed: JPEG, PNG, WebP, GIF (\u226450MB) or MP4, WebM, MOV (\u2264200MB). Returns a public URL on the temp CDN; auto-expires after 24h."
910
+ }
911
+ },
912
+ required: ["path"]
913
+ };
914
+ async function run10(input, ctx) {
915
+ let url;
916
+ try {
917
+ url = await uploadLocalFile(input.path, ctx.client);
918
+ } catch (err) {
919
+ if (err instanceof SkillUploadError) {
920
+ return { kind: "text", text: `pixeldojo:upload: ${err.userMessage}` };
921
+ }
922
+ throw err;
923
+ }
924
+ return {
925
+ kind: "text",
926
+ text: `Uploaded ${path.basename(input.path)} \u2192 ${url}
927
+ (temp storage; auto-deletes in ~24h \u2014 pass this URL as image_url to :generate or :edit)`
928
+ };
929
+ }
930
+ var uploadSkill = {
931
+ name: "pixeldojo:upload",
932
+ 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.",
933
+ inputZod: InputSchema10,
934
+ inputJsonSchema: InputJsonSchema10,
935
+ allowedModels: [],
936
+ defaultModel: "",
937
+ run: run10
438
938
  };
439
939
 
440
940
  // src/skills/index.ts
441
941
  var ALL_SKILLS = [
442
942
  generateSkill,
943
+ editSkill,
443
944
  characterSkill,
444
945
  storyboardSkill,
445
946
  upscaleSkill,
446
- statusSkill
947
+ statusSkill,
948
+ fromUrlSkill,
949
+ uploadSkill,
950
+ campaignSkill,
951
+ campaignStatusSkill
447
952
  ];
448
953
  function findSkill(name) {
449
954
  return ALL_SKILLS.find((s) => s.name === name);