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