@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/index.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
|
-
|
|
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.
|
|
140
|
-
|
|
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
|
|
166
|
-
var
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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
|
|
190
|
-
const requested = input.model ??
|
|
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:
|
|
223
|
-
inputJsonSchema:
|
|
508
|
+
inputZod: InputSchema3,
|
|
509
|
+
inputJsonSchema: InputJsonSchema3,
|
|
224
510
|
allowedModels: [...ALLOWED],
|
|
225
|
-
defaultModel:
|
|
226
|
-
run:
|
|
511
|
+
defaultModel: DEFAULT_MODEL3,
|
|
512
|
+
run: run3
|
|
227
513
|
};
|
|
228
514
|
var ALLOWED2 = ["veo-3.1", "kling-video", "seedance-2"];
|
|
229
|
-
var
|
|
230
|
-
var
|
|
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 ${
|
|
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
|
|
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 ${
|
|
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
|
|
254
|
-
const requested = input.model ??
|
|
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:
|
|
284
|
-
inputJsonSchema:
|
|
569
|
+
inputZod: InputSchema4,
|
|
570
|
+
inputJsonSchema: InputJsonSchema4,
|
|
285
571
|
allowedModels: [...ALLOWED2],
|
|
286
|
-
defaultModel:
|
|
287
|
-
run:
|
|
572
|
+
defaultModel: DEFAULT_MODEL4,
|
|
573
|
+
run: run4
|
|
288
574
|
};
|
|
289
575
|
var ALLOWED3 = ["magnific-upscaler", "creative-upscale", "face-enhance"];
|
|
290
|
-
var
|
|
291
|
-
var
|
|
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 ${
|
|
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
|
|
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
|
|
308
|
-
const requested = input.model ??
|
|
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:
|
|
343
|
-
inputJsonSchema:
|
|
628
|
+
inputZod: InputSchema5,
|
|
629
|
+
inputJsonSchema: InputJsonSchema5,
|
|
344
630
|
allowedModels: [...ALLOWED3],
|
|
345
|
-
defaultModel:
|
|
346
|
-
run:
|
|
631
|
+
defaultModel: DEFAULT_MODEL5,
|
|
632
|
+
run: run5
|
|
347
633
|
};
|
|
348
|
-
var
|
|
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
|
|
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
|
|
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:
|
|
431
|
-
inputJsonSchema:
|
|
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:
|
|
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);
|