@kolbo/mcp 1.56.0 → 1.57.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/package.json +1 -1
- package/src/apps/index.js +7 -0
- package/src/apps/theme.js +13 -0
- package/src/apps/widgets/generation.js +123 -11
- package/src/tools/_shared.js +9 -0
- package/src/tools/generate.js +89 -13
package/package.json
CHANGED
package/src/apps/index.js
CHANGED
|
@@ -69,12 +69,19 @@ const WIDGET_CSP = {
|
|
|
69
69
|
'https://fonts.googleapis.com', // Inter / JetBrains Mono stylesheet
|
|
70
70
|
'https://fonts.gstatic.com', // font files
|
|
71
71
|
'https://images.pexels.com', // stock thumbnails
|
|
72
|
+
'https://images.unsplash.com', // stock thumbnails (Unsplash)
|
|
73
|
+
'https://plus.unsplash.com', // Unsplash+ premium images
|
|
74
|
+
'https://cdn.coverr.co', // Coverr video thumbnails
|
|
75
|
+
'https://cdn.freesound.org', // Freesound waveform previews
|
|
72
76
|
// Wildcards as a second layer for hosts that do support them.
|
|
73
77
|
'https://*.kolbo.ai',
|
|
74
78
|
'https://*.digitaloceanspaces.com',
|
|
75
79
|
'https://*.cdn.digitaloceanspaces.com',
|
|
76
80
|
'https://*.pexels.com',
|
|
77
81
|
'https://*.pixabay.com',
|
|
82
|
+
'https://*.unsplash.com',
|
|
83
|
+
'https://*.coverr.co',
|
|
84
|
+
'https://*.freesound.org',
|
|
78
85
|
'https://*.sketchfab.com',
|
|
79
86
|
'https://*.cloudfront.net',
|
|
80
87
|
],
|
package/src/apps/theme.js
CHANGED
|
@@ -83,6 +83,12 @@ body {
|
|
|
83
83
|
.k-body { padding: 14px 16px; }
|
|
84
84
|
.k-prompt { color: var(--text-muted); font-size: 12.5px; margin-bottom: 10px; word-break: break-word;
|
|
85
85
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
86
|
+
.k-prompt.k-clamped, .k-caption.k-clamped { cursor: pointer; }
|
|
87
|
+
.k-prompt.expanded { -webkit-line-clamp: unset; }
|
|
88
|
+
/* Single-line media caption (scene / batch prompt under the viewer) */
|
|
89
|
+
.k-caption { font-size: 11px; color: var(--text-faint); margin: 2px 2px 0;
|
|
90
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
91
|
+
.k-caption.expanded { white-space: normal; word-break: break-word; }
|
|
86
92
|
|
|
87
93
|
/* ---- Chips ---- */
|
|
88
94
|
.k-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 12px; }
|
|
@@ -127,6 +133,13 @@ body {
|
|
|
127
133
|
animation: k-sweep 1.6s ease-in-out infinite;
|
|
128
134
|
}
|
|
129
135
|
@keyframes k-sweep { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
136
|
+
/* Batch grid: per-cell prompt caption + a cell that already finished */
|
|
137
|
+
.k-skel-cap { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
|
|
138
|
+
padding: 12px 8px 6px; font-size: 10.5px; color: #fff;
|
|
139
|
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.65));
|
|
140
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
141
|
+
.k-skel.done::after { animation: none; background: none; }
|
|
142
|
+
.k-cell-fill { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
130
143
|
.k-gen-badge {
|
|
131
144
|
position: absolute; top: 10px; left: 10px; z-index: 2;
|
|
132
145
|
display: inline-flex; align-items: center; gap: 6px;
|
|
@@ -71,12 +71,31 @@ var TOOL_TITLES = {
|
|
|
71
71
|
generate_creative_director: 'Creative Director', edit_image: 'Image Edit', edit_video: 'Video Edit'
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
+
// Long text is clamped by CSS (.k-prompt 2 lines / .k-caption 1 line). When it
|
|
75
|
+
// actually overflows, make it click-to-expand so the full prompt is readable.
|
|
76
|
+
function makeExpandable(node) {
|
|
77
|
+
if (!node) return;
|
|
78
|
+
node.classList.remove('k-clamped');
|
|
79
|
+
// Synchronous layout read — rAF would never fire in a hidden/backgrounded
|
|
80
|
+
// iframe, leaving long prompts stuck without the expand affordance.
|
|
81
|
+
if (node.scrollHeight > node.clientHeight + 2 || node.scrollWidth > node.clientWidth + 2) {
|
|
82
|
+
node.classList.add('k-clamped');
|
|
83
|
+
node.title = node.title || 'Show full text';
|
|
84
|
+
node.onclick = function () {
|
|
85
|
+
node.classList.toggle('expanded');
|
|
86
|
+
node.title = node.classList.contains('expanded') ? 'Collapse' : 'Show full text';
|
|
87
|
+
window.kolbo.notifySize();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
74
92
|
function boot(sc) {
|
|
75
93
|
if (!sc) return;
|
|
76
94
|
state = sc;
|
|
77
95
|
el('tool-title').textContent = TOOL_TITLES[sc.tool] || 'Generation';
|
|
78
96
|
el('prompt').textContent = sc.prompt || '';
|
|
79
97
|
el('prompt').style.display = sc.prompt ? '' : 'none';
|
|
98
|
+
makeExpandable(el('prompt'));
|
|
80
99
|
renderChips(sc);
|
|
81
100
|
el('credits').textContent = sc.credits_used != null ? fmtCredits(sc.credits_used) : '';
|
|
82
101
|
if (sc.phase === 'generating') renderGenerating(sc);
|
|
@@ -111,16 +130,21 @@ function iconFor(kind) {
|
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
/* ---------- generating ---------- */
|
|
133
|
+
function isBatch(sc) { return !!(sc && sc.generation_ids && sc.generation_ids.length > 1); }
|
|
134
|
+
|
|
114
135
|
function renderGenerating(sc) {
|
|
115
136
|
setPhaseChip('Generating', true);
|
|
116
|
-
var n = Math.min(sc.count || 1, 4);
|
|
137
|
+
var n = Math.min(sc.count || 1, isBatch(sc) ? 8 : 4);
|
|
117
138
|
var shape = sc.kind === 'video' || sc.kind === 'scenes' ? 'video' : (sc.kind === 'audio' ? 'video' : 'square');
|
|
118
139
|
var cells = '';
|
|
119
140
|
for (var i = 0; i < n; i++) {
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
var cap = (sc.prompts && sc.prompts[i])
|
|
142
|
+
? '<span class="k-skel-cap" title="' + esc(sc.prompts[i]) + '">' + esc(sc.prompts[i]) + '</span>' : '';
|
|
143
|
+
cells += '<div class="k-skel ' + shape + '" data-cell="' + i + '">' +
|
|
144
|
+
(i === 0 ? '<span class="k-gen-badge"><span class="k-spin"></span>Generating</span>' : '') + cap + '</div>';
|
|
122
145
|
}
|
|
123
|
-
|
|
146
|
+
// Grid class caps at n4 — the auto-fill rule handles any larger batch count.
|
|
147
|
+
el('stage').innerHTML = '<div class="k-gen-grid n' + Math.min(n, 4) + '">' + cells + '</div>';
|
|
124
148
|
renderStopButton(sc);
|
|
125
149
|
schedulePoll(sc);
|
|
126
150
|
}
|
|
@@ -134,6 +158,7 @@ function cancelSpec(sc) {
|
|
|
134
158
|
var jobId = (sc.status_args && sc.status_args.job_id) || sc.generation_id;
|
|
135
159
|
return jobId ? { tool: 'shorts_cancel', args: { job_id: jobId } } : null;
|
|
136
160
|
}
|
|
161
|
+
if (isBatch(sc)) return { batch: sc.generation_ids };
|
|
137
162
|
if (!sc.generation_id) return null;
|
|
138
163
|
return { tool: 'cancel_generation', args: { generation_id: sc.generation_id } };
|
|
139
164
|
}
|
|
@@ -150,8 +175,23 @@ function renderStopButton(sc) {
|
|
|
150
175
|
// cannot repaint the card back into "Generating".
|
|
151
176
|
cancelRequested = true;
|
|
152
177
|
clearTimeout(pollTimer);
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
// Batch: cancel every id; report combined refund. Entries that already
|
|
179
|
+
// finished return cancelled:false — only resume polling if ALL did.
|
|
180
|
+
var call = spec.batch
|
|
181
|
+
? Promise.all(spec.batch.map(function (id) {
|
|
182
|
+
return window.kolbo.callTool('cancel_generation', { generation_id: id })
|
|
183
|
+
.then(function (r) { return structured(r) || {}; })
|
|
184
|
+
.catch(function () { return {}; });
|
|
185
|
+
})).then(function (sts) {
|
|
186
|
+
var refund = 0;
|
|
187
|
+
sts.forEach(function (s) { if (s.credits_refunded) refund += s.credits_refunded; });
|
|
188
|
+
return {
|
|
189
|
+
cancelled: sts.some(function (s) { return s.cancelled !== false; }),
|
|
190
|
+
credits_refunded: refund || undefined
|
|
191
|
+
};
|
|
192
|
+
})
|
|
193
|
+
: window.kolbo.callTool(spec.tool, spec.args).then(function (r) { return structured(r) || {}; });
|
|
194
|
+
call.then(function (st) {
|
|
155
195
|
if (st.cancelled === false) {
|
|
156
196
|
// Already terminal — let the normal poll path report the real outcome
|
|
157
197
|
// instead of claiming a cancel that did not happen.
|
|
@@ -183,7 +223,7 @@ function renderCancelled(creditsRefunded) {
|
|
|
183
223
|
// Tell the model, so it does not go on to report the generation as running.
|
|
184
224
|
try {
|
|
185
225
|
window.kolbo.updateModelContext('The user cancelled generation ' +
|
|
186
|
-
((state && state.generation_id) || '') + '.' + note +
|
|
226
|
+
((state && (state.generation_ids || [state.generation_id]).filter(Boolean).join(', ')) || '') + '.' + note +
|
|
187
227
|
' Do not poll it or report it as in progress.');
|
|
188
228
|
} catch (e) {}
|
|
189
229
|
window.kolbo.notifySize();
|
|
@@ -222,6 +262,10 @@ function poll(sc) {
|
|
|
222
262
|
if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue(st.error || 'Tracking paused. The generation may still be running.');
|
|
223
263
|
return schedulePoll(sc);
|
|
224
264
|
}
|
|
265
|
+
// Batch (prompts[] fan-out): multi-id status shape { all_done, generations[] }.
|
|
266
|
+
if (isBatch(sc) && Array.isArray(st.generations)) {
|
|
267
|
+
return handleBatchStatus(sc, st);
|
|
268
|
+
}
|
|
225
269
|
if (stateName === 'completed') {
|
|
226
270
|
pollErrors = 0;
|
|
227
271
|
var r = st.result || st;
|
|
@@ -257,6 +301,68 @@ function poll(sc) {
|
|
|
257
301
|
});
|
|
258
302
|
}
|
|
259
303
|
|
|
304
|
+
/* ---------- batch (prompts[] fan-out) ---------- */
|
|
305
|
+
// Each poll round resolves when every id has completed or its wait window
|
|
306
|
+
// closed (~3 min), so finished cells fill in per round while the rest keep
|
|
307
|
+
// their skeleton. When all_done, the set renders through the scenes viewer.
|
|
308
|
+
function handleBatchStatus(sc, st) {
|
|
309
|
+
pollErrors = 0;
|
|
310
|
+
var gens = st.generations || [];
|
|
311
|
+
gens.forEach(function (g, i) {
|
|
312
|
+
if (g.state === 'completed') fillBatchCell(sc, i, g);
|
|
313
|
+
});
|
|
314
|
+
if (!st.all_done) return schedulePoll(sc);
|
|
315
|
+
|
|
316
|
+
var scenes = [], failedCount = 0, credits = 0, haveCredits = false, allUrls = [];
|
|
317
|
+
gens.forEach(function (g, i) {
|
|
318
|
+
var r = g.result || g;
|
|
319
|
+
var urls = (r && r.urls) || [];
|
|
320
|
+
if (g.state !== 'completed' || !urls.length) { failedCount++; return; }
|
|
321
|
+
allUrls = allUrls.concat(urls);
|
|
322
|
+
var c = g.credits_used != null ? g.credits_used : (r.credits_used != null ? r.credits_used : null);
|
|
323
|
+
if (c != null) { credits += c; haveCredits = true; }
|
|
324
|
+
scenes.push({
|
|
325
|
+
scene_number: i + 1,
|
|
326
|
+
title: (sc.prompts && sc.prompts[i]) || '',
|
|
327
|
+
image_urls: sc.kind === 'video' ? [] : urls,
|
|
328
|
+
video_urls: sc.kind === 'video' ? urls : []
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
if (!scenes.length) return renderError('All ' + gens.length + ' generations failed');
|
|
332
|
+
|
|
333
|
+
var done = Object.assign({}, sc, {
|
|
334
|
+
phase: 'completed', kind: 'scenes', batch: true, scenes: scenes, urls: [],
|
|
335
|
+
credits_used: haveCredits ? credits : sc.credits_used
|
|
336
|
+
});
|
|
337
|
+
state = done;
|
|
338
|
+
el('credits').textContent = done.credits_used != null ? fmtCredits(done.credits_used) : '';
|
|
339
|
+
if (failedCount) setPhaseChip(failedCount + ' failed', false);
|
|
340
|
+
renderResult(done);
|
|
341
|
+
try {
|
|
342
|
+
window.kolbo.updateModelContext(
|
|
343
|
+
'Batch generation completed (' + (sc.tool || '') + '): ' + scenes.length + ' of ' + gens.length + ' succeeded.' +
|
|
344
|
+
'\\nOutput URLs:\\n' + allUrls.join('\\n') +
|
|
345
|
+
(failedCount ? '\\nFailed: ' + failedCount : '') +
|
|
346
|
+
(haveCredits ? '\\nCredits used: ' + credits : ''));
|
|
347
|
+
} catch (e) {}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function fillBatchCell(sc, i, g) {
|
|
351
|
+
var cell = el('stage').querySelector('[data-cell="' + i + '"]');
|
|
352
|
+
if (!cell || cell.getAttribute('data-done')) return;
|
|
353
|
+
var r = g.result || g;
|
|
354
|
+
var u = (r.urls || [])[0];
|
|
355
|
+
if (!u) return;
|
|
356
|
+
cell.setAttribute('data-done', '1');
|
|
357
|
+
cell.classList.add('done');
|
|
358
|
+
var cap = (sc.prompts && sc.prompts[i])
|
|
359
|
+
? '<span class="k-skel-cap" title="' + esc(sc.prompts[i]) + '">' + esc(sc.prompts[i]) + '</span>' : '';
|
|
360
|
+
cell.innerHTML = (sc.kind === 'video'
|
|
361
|
+
? '<video class="k-cell-fill" src="' + esc(u) + '"' + (r.thumbnail_url ? ' poster="' + esc(r.thumbnail_url) + '"' : '') + ' muted playsinline preload="metadata"></video>'
|
|
362
|
+
: '<img class="k-cell-fill" src="' + esc(u) + '" alt="">') + cap;
|
|
363
|
+
window.kolbo.notifySize();
|
|
364
|
+
}
|
|
365
|
+
|
|
260
366
|
/* ---------- results ---------- */
|
|
261
367
|
function renderResult(sc) {
|
|
262
368
|
clearTimeout(pollTimer);
|
|
@@ -392,7 +498,10 @@ function wireDlButtons(root) {
|
|
|
392
498
|
function sceneItems(sc) {
|
|
393
499
|
var items = [];
|
|
394
500
|
(sc.scenes || []).forEach(function (scene) {
|
|
395
|
-
|
|
501
|
+
// Batch sets carry the raw user prompt as title — no "Scene N" framing.
|
|
502
|
+
var label = sc.batch
|
|
503
|
+
? (scene.title || 'Prompt ' + scene.scene_number)
|
|
504
|
+
: 'Scene ' + scene.scene_number + (scene.title ? ' — ' + scene.title : '');
|
|
396
505
|
(scene.image_urls || []).forEach(function (u) { items.push({ url: u, type: 'image', label: label }); });
|
|
397
506
|
(scene.video_urls || []).forEach(function (u) { items.push({ url: u, type: 'video', label: label }); });
|
|
398
507
|
});
|
|
@@ -415,8 +524,9 @@ function renderScenes(sc) {
|
|
|
415
524
|
}).join('') + '</div>';
|
|
416
525
|
el('stage').innerHTML =
|
|
417
526
|
'<div class="k-viewer">' + mediaHtml + dlBtnHTML(it.url) + '</div>' +
|
|
418
|
-
'<div
|
|
527
|
+
'<div class="k-caption" id="scene-cap">' + esc(it.label) + '</div>' +
|
|
419
528
|
thumbs;
|
|
529
|
+
makeExpandable(el('scene-cap'));
|
|
420
530
|
wireDlButtons(el('stage'));
|
|
421
531
|
if (it.type === 'image') {
|
|
422
532
|
var main = el('scene-main');
|
|
@@ -591,9 +701,11 @@ function bootPre(toolName, args) {
|
|
|
591
701
|
if (args) originArgs = args;
|
|
592
702
|
if (state) return; // real data already arrived
|
|
593
703
|
el('tool-title').textContent = TOOL_TITLES[toolName] || 'Generation';
|
|
594
|
-
if (args && (args.prompt || args.text)) {
|
|
595
|
-
el('prompt').textContent = args.prompt || args.text
|
|
704
|
+
if (args && (args.prompt || args.text || (Array.isArray(args.prompts) && args.prompts.length))) {
|
|
705
|
+
el('prompt').textContent = args.prompt || args.text ||
|
|
706
|
+
(args.prompts.length + ' prompts — ' + args.prompts.join(' · '));
|
|
596
707
|
el('prompt').style.display = '';
|
|
708
|
+
makeExpandable(el('prompt'));
|
|
597
709
|
}
|
|
598
710
|
setPhaseChip('Preparing', true);
|
|
599
711
|
if (!el('stage').innerHTML) {
|
package/src/tools/_shared.js
CHANGED
|
@@ -519,9 +519,18 @@ async function uiGenerating(p) {
|
|
|
519
519
|
reference_image: p.reference_image,
|
|
520
520
|
open_url: buildOpenUrl(p.tool, p.gen),
|
|
521
521
|
};
|
|
522
|
+
// Batch mode (prompts[] fan-out): ONE widget tracks every id in the set.
|
|
523
|
+
if (Array.isArray(p.generation_ids) && p.generation_ids.length > 1) {
|
|
524
|
+
structured.generation_ids = p.generation_ids;
|
|
525
|
+
structured.prompts = p.prompts;
|
|
526
|
+
}
|
|
522
527
|
const text = JSON.stringify({
|
|
523
528
|
status: 'submitted',
|
|
524
529
|
generation_id: p.gen.generation_id,
|
|
530
|
+
...(Array.isArray(p.generation_ids) && p.generation_ids.length > 1
|
|
531
|
+
? { batch: true, generation_ids: p.generation_ids } : {}),
|
|
532
|
+
...(p.failed_submissions && p.failed_submissions.length
|
|
533
|
+
? { failed_submissions: p.failed_submissions } : {}),
|
|
525
534
|
_widget_note: 'A live Kolbo widget is rendering this generation for the user (progress + final result + action buttons). Tell the user it is generating and the card above will update — do NOT poll in a loop. If you need the output URLs (e.g. for a follow-up edit or a report), call get_generation_status ONCE with wait=true — it blocks until done. Tracking several generations? Pass ALL their ids in generation_ids in that one call.',
|
|
526
535
|
}, null, 2);
|
|
527
536
|
return uiResult(UI.generation, text, structured);
|
package/src/tools/generate.js
CHANGED
|
@@ -37,6 +37,46 @@ const CINEMATIC_SCHEMA = z.object({
|
|
|
37
37
|
'validated against their dimension server-side. Dimensions are data-driven — never hardcode ids.'
|
|
38
38
|
);
|
|
39
39
|
|
|
40
|
+
// ─── Batch fan-out (prompts[]) ──────────────────────────────────────────────
|
|
41
|
+
// One tool call, N DIFFERENT prompts → N generations tracked by ONE widget.
|
|
42
|
+
// The manual-control twin of generate_creative_director: no orchestration pass,
|
|
43
|
+
// the user's exact prompts verbatim. Submit failures never sink the batch —
|
|
44
|
+
// successful ids proceed, failed prompts are reported alongside.
|
|
45
|
+
const MAX_BATCH_PROMPTS = 8;
|
|
46
|
+
async function submitBatch(rawPrompts, submitOne) {
|
|
47
|
+
const prompts = rawPrompts.slice(0, MAX_BATCH_PROMPTS).map((s) => String(s).trim()).filter(Boolean);
|
|
48
|
+
const settled = await Promise.allSettled(prompts.map((p) => submitOne(p)));
|
|
49
|
+
const ok = [], failed = [];
|
|
50
|
+
settled.forEach((s, i) => {
|
|
51
|
+
if (s.status === 'fulfilled' && s.value && s.value.generation_id) ok.push({ prompt: prompts[i], gen: s.value });
|
|
52
|
+
else failed.push({ prompt: prompts[i], error: (s.reason && s.reason.message) || 'submit failed' });
|
|
53
|
+
});
|
|
54
|
+
if (!ok.length) throw new Error(`All ${prompts.length} batch submissions failed: ${failed[0].error}`);
|
|
55
|
+
return { ok, failed, ids: ok.map((o) => o.gen.generation_id) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Blocking path for text-only hosts: wait for every batch member, aggregate.
|
|
59
|
+
async function pollBatch(client, batch, { interval, timeout }) {
|
|
60
|
+
const polls = await Promise.all(batch.ids.map((id) => pollOrTimedOut(client, id, { interval, timeout })));
|
|
61
|
+
const generations = polls.map((p, i) => p.timedOut
|
|
62
|
+
? { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'processing', note: 'Still running — call get_generation_status with wait=true to collect it.' }
|
|
63
|
+
: { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'completed', ...creditFields(polls[i].result), urls: p.result.result.urls });
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
batch: true,
|
|
69
|
+
generations,
|
|
70
|
+
failed_submissions: batch.failed.length ? batch.failed : undefined
|
|
71
|
+
}, null, 2)
|
|
72
|
+
}]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const promptsField = (what) => z.array(z.string()).optional().describe(
|
|
77
|
+
`BATCH MODE — several DIFFERENT prompts (2–${MAX_BATCH_PROMPTS}) generated concurrently in ONE call and rendered together in ONE combined widget. Whenever the user wants multiple distinct ${what} with their own prompts, ALWAYS pass them all here instead of making several separate calls — separate calls clutter the chat with stacked widgets. All prompts share the same model/settings. When set, \`prompt\` is ignored. For N variations of a SINGLE prompt use num_images (image tools); for an AI-planned coherent scene set use generate_creative_director.`
|
|
78
|
+
);
|
|
79
|
+
|
|
40
80
|
function registerGenerateTools(server, client, options = {}) {
|
|
41
81
|
// Only enabled by hosts that explicitly opt in (the remote HTTP connector).
|
|
42
82
|
// stdio hosts (Kolbo Code, Claude Desktop, Cursor) leave this false, so their
|
|
@@ -49,9 +89,10 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
49
89
|
// ─── generate_image ────────────────────────────────────────
|
|
50
90
|
server.tool(
|
|
51
91
|
'generate_image',
|
|
52
|
-
'Generate image(s) from a text prompt using Kolbo AI. Supports Visual DNA profiles (for character/style/product consistency), moodboards (for style direction), reference images (for composition guidance), batch generation (num_images), and web-search grounding. For EDITING an existing image, use generate_image_edit instead. For a coordinated multi-scene set (storyboard, ad campaign), use generate_creative_director. Returns the final image URL(s) when complete.',
|
|
92
|
+
'Generate image(s) from a text prompt using Kolbo AI. Supports Visual DNA profiles (for character/style/product consistency), moodboards (for style direction), reference images (for composition guidance), batch generation (num_images for variations of ONE prompt, `prompts` for SEVERAL different prompts in one combined widget), and web-search grounding. When the user wants multiple distinct images, pass all their prompts in `prompts` in ONE call — never a series of separate generate_image calls. For EDITING an existing image, use generate_image_edit instead. For a coordinated multi-scene set planned by AI from a single brief (storyboard, ad campaign), use generate_creative_director. Returns the final image URL(s) when complete.',
|
|
53
93
|
{
|
|
54
|
-
prompt: z.string().describe('Text description of the image to generate'),
|
|
94
|
+
prompt: z.string().optional().describe('Text description of the image to generate. Required unless `prompts` is provided.'),
|
|
95
|
+
prompts: promptsField('images'),
|
|
55
96
|
model: z.string().optional().describe('Model identifier — REQUIRED in practice: pick a specific model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "nano-banana-2" (versatile, text rendering, multilingual) or "gpt-image-2" (photoreal, infographics). Call list_models type="text_to_img" to see all options and pick per the user\'s intent.'),
|
|
56
97
|
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "1:1", "16:9", "9:16"). Must be a value present in the model\'s `supported_aspect_ratios` from list_models — pass an unsupported value and the API rejects. Default: "1:1"'),
|
|
57
98
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
|
|
@@ -67,12 +108,29 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
67
108
|
skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one image ungraded.'),
|
|
68
109
|
project_id: projectIdField
|
|
69
110
|
},
|
|
70
|
-
async ({ prompt, model, aspect_ratio, enhance_prompt = false, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id }) => {
|
|
111
|
+
async ({ prompt, prompts, model, aspect_ratio, enhance_prompt = false, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id }) => {
|
|
112
|
+
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
71
113
|
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
72
|
-
const
|
|
73
|
-
|
|
114
|
+
const shared = {
|
|
115
|
+
model, aspect_ratio, enhance_prompt,
|
|
74
116
|
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id
|
|
75
|
-
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Batch mode: N different prompts, one widget owning all generation ids.
|
|
120
|
+
if (prompts && prompts.length) {
|
|
121
|
+
const batch = await submitBatch(prompts, (p) => client.post('/v1/generate/image', { ...shared, prompt: p }));
|
|
122
|
+
if (ui()) return uiGenerating({
|
|
123
|
+
tool: 'generate_image', kind: 'image', gen: batch.ok[0].gen, client, model,
|
|
124
|
+
count: batch.ids.length, settings: { resolution, aspect_ratio },
|
|
125
|
+
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
126
|
+
failed_submissions: batch.failed,
|
|
127
|
+
status_args: { generation_ids: batch.ids, wait: true },
|
|
128
|
+
reference_image: reference_images?.[0]
|
|
129
|
+
});
|
|
130
|
+
return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 240000 });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const gen = await client.post('/v1/generate/image', { ...shared, prompt, num_images });
|
|
76
134
|
|
|
77
135
|
if (ui()) return uiGenerating({
|
|
78
136
|
tool: 'generate_image', kind: 'image', gen, client, model, prompt,
|
|
@@ -165,7 +223,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
165
223
|
// ─── generate_creative_director ─────────────────────────────
|
|
166
224
|
server.tool(
|
|
167
225
|
'generate_creative_director',
|
|
168
|
-
'Generate 2–8 related images or videos as one coherent set from a single creative brief. Use scene_count (NOT num_images) to set the number of scenes (1–8, default 4). Use this when the user gives a general brief ("make 4 product shots", "create a storyboard") and you are planning the scenes — it handles style consistency and runs scenes in parallel. If the user explicitly provides separate prompts for each image, use
|
|
226
|
+
'Generate 2–8 related images or videos as one coherent set from a single creative brief. Use scene_count (NOT num_images) to set the number of scenes (1–8, default 4). Use this when the user gives a general brief ("make 4 product shots", "create a storyboard") and you are planning the scenes — it handles style consistency and runs scenes in parallel. If the user explicitly provides separate prompts for each image, use ONE generate_image call with the `prompts` array instead (never parallel single-prompt calls). Supports image and video modes (workflow_type). Visual DNA and moodboard references keep character/style consistent across every scene.',
|
|
169
227
|
{
|
|
170
228
|
prompt: z.string().describe('Creative brief or concept describing the full set of scenes to generate'),
|
|
171
229
|
scene_count: z.number().optional().describe('Number of scenes/images to generate, 1–8. Default: 4. Use this — NOT num_images — to control how many outputs are created.'),
|
|
@@ -318,9 +376,10 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
318
376
|
// DNA-locked still via generate_video_from_image.
|
|
319
377
|
server.tool(
|
|
320
378
|
'generate_video',
|
|
321
|
-
'Generate a video from a text prompt using Kolbo AI. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports reference images (for style/composition guidance). Does NOT support Visual DNA — for character-consistent video use generate_elements or animate a DNA-locked still via generate_video_from_image. Returns the final video URL when complete.',
|
|
379
|
+
'Generate a video from a text prompt using Kolbo AI. For SEVERAL different videos, pass all their prompts in `prompts` in ONE call (one combined widget) — never a series of separate calls. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports reference images (for style/composition guidance). Does NOT support Visual DNA — for character-consistent video use generate_elements or animate a DNA-locked still via generate_video_from_image. Returns the final video URL when complete.',
|
|
322
380
|
{
|
|
323
|
-
prompt: z.string().describe('Text description of the video to generate'),
|
|
381
|
+
prompt: z.string().optional().describe('Text description of the video to generate. Required unless `prompts` is provided.'),
|
|
382
|
+
prompts: promptsField('videos'),
|
|
324
383
|
model: z.string().optional().describe('Model identifier — pick a SPECIFIC model, do NOT omit (omitting = Smart Select auto-pick, which we avoid). Strong current defaults: "seedance-2" (versatile) or "veo3" (Veo 3.1, cinematic + native audio); the Kling family (call list_models for exact ids like kling-video/v3/pro/text-to-video) is strongest for motion. Call list_models type="text_to_video" to see all options + check supported_durations / supported_aspect_ratios, and choose per the user\'s intent.'),
|
|
325
384
|
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "16:9"'),
|
|
326
385
|
duration: z.number().optional().describe('Duration in seconds. Must be a value in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration` (whichever the model exposes). Default: 5'),
|
|
@@ -332,11 +391,28 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
332
391
|
skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one video ungraded.'),
|
|
333
392
|
project_id: projectIdField
|
|
334
393
|
},
|
|
335
|
-
async ({ prompt, model, aspect_ratio, duration, enhance_prompt = false, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
|
|
394
|
+
async ({ prompt, prompts, model, aspect_ratio, duration, enhance_prompt = false, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
|
|
395
|
+
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
336
396
|
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
}
|
|
397
|
+
const shared = {
|
|
398
|
+
model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Batch mode: N different prompts, one widget owning all generation ids.
|
|
402
|
+
if (prompts && prompts.length) {
|
|
403
|
+
const batch = await submitBatch(prompts, (p) => client.post('/v1/generate/video', { ...shared, prompt: p }));
|
|
404
|
+
if (ui()) return uiGenerating({
|
|
405
|
+
tool: 'generate_video', kind: 'video', gen: batch.ok[0].gen, client, model,
|
|
406
|
+
count: batch.ids.length, settings: { duration, resolution, aspect_ratio },
|
|
407
|
+
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
408
|
+
failed_submissions: batch.failed,
|
|
409
|
+
status_args: { generation_ids: batch.ids, wait: true },
|
|
410
|
+
reference_image: reference_images?.[0]
|
|
411
|
+
});
|
|
412
|
+
return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 8) * 1000, timeout: 900000 });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const gen = await client.post('/v1/generate/video', { ...shared, prompt });
|
|
340
416
|
|
|
341
417
|
if (ui()) return uiGenerating({
|
|
342
418
|
tool: 'generate_video', kind: 'video', gen, client, model, prompt,
|