@pixeldojo/mcp 0.2.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({
@@ -118,7 +185,10 @@ var InputSchema = z.object({
118
185
  ),
119
186
  aspect_ratio: z.string().optional().describe('Aspect ratio supported by the chosen model (e.g. "1:1", "16:9", "9:16").'),
120
187
  duration: z.number().int().positive().optional().describe("Video duration in seconds, when applicable."),
121
- 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
+ )
122
192
  });
123
193
  var DEFAULT_MODEL = "google-nano-banana";
124
194
  var InputJsonSchema = {
@@ -135,17 +205,67 @@ var InputJsonSchema = {
135
205
  },
136
206
  aspect_ratio: { type: "string" },
137
207
  duration: { type: "integer" },
138
- 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
+ }
139
213
  },
140
214
  required: ["prompt"]
141
215
  };
142
- async function run(input, ctx) {
143
- const apiId = input.model ?? DEFAULT_MODEL;
216
+ function buildPayload(apiId, input) {
144
217
  const body = { prompt: input.prompt };
145
218
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
146
- if (input.duration !== void 0) body.duration = input.duration;
147
- if (input.image_url) body.image_url = input.image_url;
148
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);
149
269
  const submitted = await ctx.client.run(apiId, body);
150
270
  const result = await pollJobUntilDoneOrHandoff(
151
271
  submitted.jobId,
@@ -163,39 +283,191 @@ async function run(input, ctx) {
163
283
  }
164
284
  var generateSkill = {
165
285
  name: "pixeldojo:generate",
166
- 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.",
167
287
  inputZod: InputSchema,
168
288
  inputJsonSchema: InputJsonSchema,
169
289
  allowedModels: null,
170
290
  defaultModel: DEFAULT_MODEL,
171
291
  run
172
292
  };
173
- var ALLOWED = ["ideogram-character", "flux-edit", "gpt-image-2-edit"];
174
- 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
+ ];
175
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({
176
448
  prompt: z.string().min(1).max(8e3),
177
- 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}.`),
178
450
  reference_image_url: z.string().url().optional().describe(
179
451
  "Reference image of the character. Required by flux-edit and gpt-image-2-edit; optional for ideogram-character."
180
452
  ),
181
453
  aspect_ratio: z.string().optional()
182
454
  });
183
- var InputJsonSchema2 = {
455
+ var InputJsonSchema3 = {
184
456
  type: "object",
185
457
  properties: {
186
458
  prompt: { type: "string" },
187
459
  model: {
188
460
  type: "string",
189
461
  enum: ALLOWED,
190
- description: `One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL2}.`
462
+ description: `One of: ${ALLOWED.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`
191
463
  },
192
464
  reference_image_url: { type: "string", format: "uri" },
193
465
  aspect_ratio: { type: "string" }
194
466
  },
195
467
  required: ["prompt"]
196
468
  };
197
- async function run2(input, ctx) {
198
- const requested = input.model ?? DEFAULT_MODEL2;
469
+ async function run3(input, ctx) {
470
+ const requested = input.model ?? DEFAULT_MODEL3;
199
471
  if (!ALLOWED.includes(requested)) {
200
472
  return {
201
473
  kind: "text",
@@ -203,6 +475,12 @@ async function run2(input, ctx) {
203
475
  };
204
476
  }
205
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
+ }
206
484
  const body = { prompt: input.prompt };
207
485
  if (input.aspect_ratio) body.aspect_ratio = input.aspect_ratio;
208
486
  if (input.reference_image_url) {
@@ -227,21 +505,21 @@ async function run2(input, ctx) {
227
505
  var characterSkill = {
228
506
  name: "pixeldojo:character",
229
507
  description: "Generate or edit consistent characters across shots. Pass a reference_image_url for edit-style models.",
230
- inputZod: InputSchema2,
231
- inputJsonSchema: InputJsonSchema2,
508
+ inputZod: InputSchema3,
509
+ inputJsonSchema: InputJsonSchema3,
232
510
  allowedModels: [...ALLOWED],
233
- defaultModel: DEFAULT_MODEL2,
234
- run: run2
511
+ defaultModel: DEFAULT_MODEL3,
512
+ run: run3
235
513
  };
236
514
  var ALLOWED2 = ["veo-3.1", "kling-video", "seedance-2"];
237
- var DEFAULT_MODEL3 = "veo-3.1";
238
- var InputSchema3 = z.object({
515
+ var DEFAULT_MODEL4 = "veo-3.1";
516
+ var InputSchema4 = z.object({
239
517
  brief: z.string().min(1).max(8e3).describe("Storyboard brief: scene, shots, mood, action."),
240
- 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}.`),
241
519
  aspect_ratio: z.string().optional(),
242
520
  duration: z.number().int().positive().optional()
243
521
  });
244
- var InputJsonSchema3 = {
522
+ var InputJsonSchema4 = {
245
523
  type: "object",
246
524
  properties: {
247
525
  brief: {
@@ -251,15 +529,15 @@ var InputJsonSchema3 = {
251
529
  model: {
252
530
  type: "string",
253
531
  enum: ALLOWED2,
254
- description: `One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL3}.`
532
+ description: `One of: ${ALLOWED2.join(", ")}. Defaults to ${DEFAULT_MODEL4}.`
255
533
  },
256
534
  aspect_ratio: { type: "string" },
257
535
  duration: { type: "integer" }
258
536
  },
259
537
  required: ["brief"]
260
538
  };
261
- async function run3(input, ctx) {
262
- const requested = input.model ?? DEFAULT_MODEL3;
539
+ async function run4(input, ctx) {
540
+ const requested = input.model ?? DEFAULT_MODEL4;
263
541
  if (!ALLOWED2.includes(requested)) {
264
542
  return {
265
543
  kind: "text",
@@ -288,21 +566,21 @@ async function run3(input, ctx) {
288
566
  var storyboardSkill = {
289
567
  name: "pixeldojo:storyboard",
290
568
  description: "Generate a video storyboard preview from a single brief. v1: single-shot preview; multi-shot orchestration coming.",
291
- inputZod: InputSchema3,
292
- inputJsonSchema: InputJsonSchema3,
569
+ inputZod: InputSchema4,
570
+ inputJsonSchema: InputJsonSchema4,
293
571
  allowedModels: [...ALLOWED2],
294
- defaultModel: DEFAULT_MODEL3,
295
- run: run3
572
+ defaultModel: DEFAULT_MODEL4,
573
+ run: run4
296
574
  };
297
575
  var ALLOWED3 = ["magnific-upscaler", "creative-upscale", "face-enhance"];
298
- var DEFAULT_MODEL4 = "magnific-upscaler";
299
- var InputSchema4 = z.object({
576
+ var DEFAULT_MODEL5 = "magnific-upscaler";
577
+ var InputSchema5 = z.object({
300
578
  image_url: z.string().url().describe("URL of the source image to upscale."),
301
- 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}.`),
302
580
  scale_factor: z.number().int().min(1).max(4).optional().describe("Magnific only: 1, 2, or 4. Defaults to 2."),
303
581
  creativity: z.number().min(0).max(1).optional().describe("Magnific only: 0 (preserve) \u2192 1 (creative). Defaults to 0.")
304
582
  });
305
- var InputJsonSchema4 = {
583
+ var InputJsonSchema5 = {
306
584
  type: "object",
307
585
  properties: {
308
586
  image_url: { type: "string", format: "uri" },
@@ -312,8 +590,8 @@ var InputJsonSchema4 = {
312
590
  },
313
591
  required: ["image_url"]
314
592
  };
315
- async function run4(input, ctx) {
316
- const requested = input.model ?? DEFAULT_MODEL4;
593
+ async function run5(input, ctx) {
594
+ const requested = input.model ?? DEFAULT_MODEL5;
317
595
  if (!ALLOWED3.includes(requested)) {
318
596
  return {
319
597
  kind: "text",
@@ -347,16 +625,16 @@ async function run4(input, ctx) {
347
625
  var upscaleSkill = {
348
626
  name: "pixeldojo:upscale",
349
627
  description: "Enhance an image. Magnific (default) supports configurable scale_factor and creativity; face-enhance for portraits.",
350
- inputZod: InputSchema4,
351
- inputJsonSchema: InputJsonSchema4,
628
+ inputZod: InputSchema5,
629
+ inputJsonSchema: InputJsonSchema5,
352
630
  allowedModels: [...ALLOWED3],
353
- defaultModel: DEFAULT_MODEL4,
354
- run: run4
631
+ defaultModel: DEFAULT_MODEL5,
632
+ run: run5
355
633
  };
356
- var InputSchema5 = z.object({
634
+ var InputSchema6 = z.object({
357
635
  jobId: z.string().min(1).max(200).describe("Job id returned from a prior pixeldojo:* skill handoff.")
358
636
  });
359
- var InputJsonSchema5 = {
637
+ var InputJsonSchema6 = {
360
638
  type: "object",
361
639
  properties: {
362
640
  jobId: {
@@ -368,7 +646,7 @@ var InputJsonSchema5 = {
368
646
  };
369
647
  var STUCK_HINT_MS = 5 * 60 * 1e3;
370
648
  var ABANDON_HINT_MS = 12 * 60 * 1e3;
371
- async function run5(input, ctx) {
649
+ async function run6(input, ctx) {
372
650
  const job = await ctx.client.getJob(input.jobId);
373
651
  if (job.status === "completed") {
374
652
  const urls = extractAssetUrls2(job);
@@ -435,17 +713,17 @@ function extractAssetUrls2(job) {
435
713
  var statusSkill = {
436
714
  name: "pixeldojo:status",
437
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.",
438
- inputZod: InputSchema5,
439
- inputJsonSchema: InputJsonSchema5,
716
+ inputZod: InputSchema6,
717
+ inputJsonSchema: InputJsonSchema6,
440
718
  // status is special — it doesn't dispatch to a model
441
719
  allowedModels: [],
442
720
  defaultModel: "",
443
- run: run5
721
+ run: run6
444
722
  };
445
- var InputSchema6 = z.object({
723
+ var InputSchema7 = z.object({
446
724
  url: z.string().min(1).max(2048).describe("Product page URL. Shopify, BigCommerce, WooCommerce, or any page with JSON-LD / OpenGraph product metadata.")
447
725
  });
448
- var InputJsonSchema6 = {
726
+ var InputJsonSchema7 = {
449
727
  type: "object",
450
728
  properties: {
451
729
  url: {
@@ -455,7 +733,7 @@ var InputJsonSchema6 = {
455
733
  },
456
734
  required: ["url"]
457
735
  };
458
- async function run6(input, ctx) {
736
+ async function run7(input, ctx) {
459
737
  const product = await ctx.client.extractProduct(input.url);
460
738
  const summary = [
461
739
  `Product: ${product.name}`,
@@ -475,14 +753,14 @@ function truncate(input, max) {
475
753
  var fromUrlSkill = {
476
754
  name: "pixeldojo:from_url",
477
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.",
478
- inputZod: InputSchema6,
479
- inputJsonSchema: InputJsonSchema6,
756
+ inputZod: InputSchema7,
757
+ inputJsonSchema: InputJsonSchema7,
480
758
  // No model dispatch — this skill calls a server-side scrape, not a generation.
481
759
  allowedModels: [],
482
760
  defaultModel: "",
483
- run: run6
761
+ run: run7
484
762
  };
485
- var InputSchema7 = z.object({
763
+ var InputSchema8 = z.object({
486
764
  productUrl: z.string().url().optional().describe("Product page URL. We extract name + description + images automatically."),
487
765
  product: z.object({
488
766
  name: z.string().min(1).max(200),
@@ -498,7 +776,7 @@ var InputSchema7 = z.object({
498
776
  message: "productUrl or product is required",
499
777
  path: ["productUrl"]
500
778
  });
501
- var InputJsonSchema7 = {
779
+ var InputJsonSchema8 = {
502
780
  type: "object",
503
781
  properties: {
504
782
  productUrl: {
@@ -526,7 +804,7 @@ var InputJsonSchema7 = {
526
804
  },
527
805
  oneOf: [{ required: ["productUrl"] }, { required: ["product"] }]
528
806
  };
529
- async function run7(input, ctx) {
807
+ async function run8(input, ctx) {
530
808
  const client = ctx.client;
531
809
  const result = await client.runCampaign(input);
532
810
  if (result.kind === "campaign") {
@@ -556,18 +834,18 @@ async function run7(input, ctx) {
556
834
  var campaignSkill = {
557
835
  name: "pixeldojo:campaign",
558
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.",
559
- inputZod: InputSchema7,
560
- inputJsonSchema: InputJsonSchema7,
837
+ inputZod: InputSchema8,
838
+ inputJsonSchema: InputJsonSchema8,
561
839
  // Composition skill — internal sub-jobs route to per-tool models, no
562
840
  // single allowed-models list applies here.
563
841
  allowedModels: [],
564
842
  defaultModel: "",
565
- run: run7
843
+ run: run8
566
844
  };
567
- var InputSchema8 = z.object({
845
+ var InputSchema9 = z.object({
568
846
  campaignId: z.string().min(1).max(200).describe("Campaign id returned from a prior pixeldojo:campaign handoff.")
569
847
  });
570
- var InputJsonSchema8 = {
848
+ var InputJsonSchema9 = {
571
849
  type: "object",
572
850
  properties: {
573
851
  campaignId: {
@@ -577,7 +855,7 @@ var InputJsonSchema8 = {
577
855
  },
578
856
  required: ["campaignId"]
579
857
  };
580
- async function run8(input, ctx) {
858
+ async function run9(input, ctx) {
581
859
  const client = ctx.client;
582
860
  const result = await client.getCampaign(input.campaignId);
583
861
  if (result.kind === "campaign") {
@@ -610,21 +888,63 @@ async function run8(input, ctx) {
610
888
  var campaignStatusSkill = {
611
889
  name: "pixeldojo:campaign_status",
612
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.",
613
- inputZod: InputSchema8,
614
- inputJsonSchema: InputJsonSchema8,
891
+ inputZod: InputSchema9,
892
+ inputJsonSchema: InputJsonSchema9,
615
893
  allowedModels: [],
616
894
  defaultModel: "",
617
- run: run8
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
618
936
  };
619
937
 
620
938
  // src/skills/index.ts
621
939
  var ALL_SKILLS = [
622
940
  generateSkill,
941
+ editSkill,
623
942
  characterSkill,
624
943
  storyboardSkill,
625
944
  upscaleSkill,
626
945
  statusSkill,
627
946
  fromUrlSkill,
947
+ uploadSkill,
628
948
  campaignSkill,
629
949
  campaignStatusSkill
630
950
  ];