@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 +551 -46
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +551 -46
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +551 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +551 -46
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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.
|
|
142
|
-
|
|
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
|
|
168
|
-
var
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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
|
|
192
|
-
const requested = input.model ??
|
|
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:
|
|
225
|
-
inputJsonSchema:
|
|
510
|
+
inputZod: InputSchema3,
|
|
511
|
+
inputJsonSchema: InputJsonSchema3,
|
|
226
512
|
allowedModels: [...ALLOWED],
|
|
227
|
-
defaultModel:
|
|
228
|
-
run:
|
|
513
|
+
defaultModel: DEFAULT_MODEL3,
|
|
514
|
+
run: run3
|
|
229
515
|
};
|
|
230
516
|
var ALLOWED2 = ["veo-3.1", "kling-video", "seedance-2"];
|
|
231
|
-
var
|
|
232
|
-
var
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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
|
|
256
|
-
const requested = input.model ??
|
|
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:
|
|
286
|
-
inputJsonSchema:
|
|
571
|
+
inputZod: InputSchema4,
|
|
572
|
+
inputJsonSchema: InputJsonSchema4,
|
|
287
573
|
allowedModels: [...ALLOWED2],
|
|
288
|
-
defaultModel:
|
|
289
|
-
run:
|
|
574
|
+
defaultModel: DEFAULT_MODEL4,
|
|
575
|
+
run: run4
|
|
290
576
|
};
|
|
291
577
|
var ALLOWED3 = ["magnific-upscaler", "creative-upscale", "face-enhance"];
|
|
292
|
-
var
|
|
293
|
-
var
|
|
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 ${
|
|
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
|
|
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
|
|
310
|
-
const requested = input.model ??
|
|
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:
|
|
345
|
-
inputJsonSchema:
|
|
630
|
+
inputZod: InputSchema5,
|
|
631
|
+
inputJsonSchema: InputJsonSchema5,
|
|
346
632
|
allowedModels: [...ALLOWED3],
|
|
347
|
-
defaultModel:
|
|
348
|
-
run:
|
|
633
|
+
defaultModel: DEFAULT_MODEL5,
|
|
634
|
+
run: run5
|
|
349
635
|
};
|
|
350
|
-
var
|
|
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
|
|
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
|
|
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:
|
|
433
|
-
inputJsonSchema:
|
|
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:
|
|
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);
|