@kolbo/mcp 1.72.1 → 1.72.3
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/skill/GENERATED.md +1 -1
- package/skill/references/workflows/production-log.md +46 -1
- package/skill/references/workflows/visual-dna.md +11 -1
- package/src/apps/index.js +4 -0
- package/src/apps/theme.js +9 -0
- package/src/apps/widgets/generation.js +35 -6
- package/src/tools/generate.js +64 -21
- package/src/tools/visual_dna.js +26 -1
package/package.json
CHANGED
package/skill/GENERATED.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AUTO-GENERATED — do not edit
|
|
2
2
|
|
|
3
|
-
This tree is mirrored from kolbo-code@
|
|
3
|
+
This tree is mirrored from kolbo-code@6d0fcfe, the single source of truth.
|
|
4
4
|
Canonical source: packages/opencode/skills/kolbo/
|
|
5
5
|
Distribution: .github/workflows/sync-skill-to-plugin.yml
|
|
6
6
|
|
|
@@ -16,9 +16,50 @@ Read `.kolbo/production.md` **before** acting on any of these signals:
|
|
|
16
16
|
|
|
17
17
|
If the file is missing and the user is referencing prior media, ask the user — do not guess from chat.
|
|
18
18
|
|
|
19
|
+
## ⚠️ Approval gates — the user is usually still iterating (READ THIS FIRST)
|
|
20
|
+
|
|
21
|
+
Most media work is an **approval loop**, not a single shot. The user generates, looks, asks for another take, and keeps going until satisfied. This is the normal case for **images, image sets, Visual DNAs, moodboards, and videos** alike.
|
|
22
|
+
|
|
23
|
+
The log records **what the user approved** — not everything you produced. Getting this wrong is expensive in both directions: log too eagerly and take 3 of 7 is enshrined as "the character"; log too late and the approved URL is gone after compaction.
|
|
24
|
+
|
|
25
|
+
**The loop:**
|
|
26
|
+
|
|
27
|
+
1. **Generate** candidates.
|
|
28
|
+
2. **Present them so the user can actually judge.** Never ask "approve?" over bare URLs or ids — the user cannot see those. Use the widget-carrying tools:
|
|
29
|
+
- Visual DNAs → `list_visual_dnas` (renders a thumbnail media grid; `create_visual_dna` returns text only, so follow it with this)
|
|
30
|
+
- Moodboards → `list_moodboards`
|
|
31
|
+
- Images / videos / audio → show the returned URLs as markdown images/links
|
|
32
|
+
Say plainly which ones are in play, e.g. "created `@maya` and `@maya_alt` — here they are".
|
|
33
|
+
3. **Ask for a decision** and name the options ("keep the first, redo the second, or both?").
|
|
34
|
+
4. **Repeat** until the user is satisfied. Log nothing as approved during this stage.
|
|
35
|
+
5. **On approval → update `.kolbo/production.md` immediately**, in the same turn, before your next tool call or final reply.
|
|
36
|
+
|
|
37
|
+
**Never write an artifact into the log as approved without the user's approval.**
|
|
38
|
+
|
|
39
|
+
**If the user genuinely doesn't care** — "whatever you think", "you pick", "don't care", or they hand you the whole job — then **you decide**. Choose, say in one line which you picked and why, and log it as usual with `(agent-selected)`. Do not stall a production waiting for an approval the user has already delegated to you.
|
|
40
|
+
|
|
41
|
+
**Don't lose candidate URLs while iterating.** Recording a candidate is not the same as claiming approval, and compaction will eat unlogged URLs. Park in-flight takes under a `#### Candidates (pending approval)` bullet, and on approval promote the winner to a normal entry and mark the rest `(rejected)`:
|
|
42
|
+
|
|
43
|
+
```md
|
|
44
|
+
2. **Rainy street walk** — neon reflections, slow dolly
|
|
45
|
+
#### Candidates (pending approval)
|
|
46
|
+
- take 1: https://...02-rain-a.png (flux-2-pro, 2026-08-17)
|
|
47
|
+
- take 2: https://...02-rain-b.png (flux-2-pro, 2026-08-17)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
after the user picks take 2:
|
|
51
|
+
|
|
52
|
+
```md
|
|
53
|
+
2. **Rainy street walk** — neon reflections, slow dolly
|
|
54
|
+
- still: https://...02-rain-b.png (flux-2-pro · 1K, approved 2026-08-17)
|
|
55
|
+
- take 1: https://...02-rain-a.png (rejected 2026-08-17)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Re-confirm state after a long loop.** When a session has churned through many takes, restate the approved set before moving on — "so we're locked on: @maya, moodboard #noir, scenes 1-3" — and make the log match. An approval loop that ends without a written-down approved state is how the wrong asset ships.
|
|
59
|
+
|
|
19
60
|
## When to WRITE to it
|
|
20
61
|
|
|
21
|
-
**Immediately
|
|
62
|
+
**Immediately once an artifact is approved** (see the approval gates above), before your next tool call or your final reply. The runtime will inject a reminder after generation tool results — treat that as a hard rule, not a suggestion. Where there is no approval loop — the user asked for one thing and got it, or delegated the choice to you — approval is implicit and you log right away.
|
|
22
63
|
|
|
23
64
|
Tools that REQUIRE logging:
|
|
24
65
|
- `generate_image`, `generate_image_edit`, `edit_image`
|
|
@@ -52,6 +93,8 @@ Stub for first creation:
|
|
|
52
93
|
|
|
53
94
|
**Brief:** <paraphrase of user's overall goal in 1-3 sentences>
|
|
54
95
|
**Now working on:** <the immediate next step>
|
|
96
|
+
**Approved:** <locked assets — DNAs, moodboards, scenes; "nothing yet" if still iterating>
|
|
97
|
+
**Awaiting approval:** <what you've presented and are waiting on; omit when nothing is pending>
|
|
55
98
|
**Last updated:** <ISO date>
|
|
56
99
|
|
|
57
100
|
---
|
|
@@ -107,6 +150,8 @@ When a user request supersedes a previous artifact (e.g., "redo scene 2 with mor
|
|
|
107
150
|
4. **Do not log failures.** Only successful generations.
|
|
108
151
|
5. **Resolve user references via the log, not chat history.** If the user says "scene 3," use the URL the log says is scene 3, even if a later tool_result mentioned a different URL.
|
|
109
152
|
6. **One file per workspace.** Multiple concurrent productions go under separate `## Production: <name>` headings inside the same file.
|
|
153
|
+
7. **Approved state is user-granted, never assumed.** A generation succeeding is not approval. Only the user's "yes" — or their explicit delegation of the choice to you — promotes a candidate to an approved entry. Silence is not approval; neither is the user moving on to another topic.
|
|
154
|
+
8. **The `## 🎯 Now` block names what is locked.** Keep an `**Approved:**` line there listing the currently-approved cast, DNAs, moodboards, and scenes, so the approved state survives compaction and is the first thing you read next session.
|
|
110
155
|
|
|
111
156
|
## Bulk Generation Entry Shape
|
|
112
157
|
|
|
@@ -207,7 +207,7 @@ Read `max_visual_dna` from `list_models` for the exact cap, AND `supports_visual
|
|
|
207
207
|
|
|
208
208
|
**Step 1 — Generate both images in parallel (one `generate_image` call each, fire simultaneously):**
|
|
209
209
|
|
|
210
|
-
1. **4-angle character sheet** — prompt: `"[character description], character reference sheet showing front view, back view, left side view, right side view, four panels arranged in a 2x2 grid, neutral solid background, full body, photorealistic"`, aspect ratio `16:9`
|
|
210
|
+
1. **4-angle character sheet** — prompt: `"[character description], character reference sheet showing front view, back view, left side view, right side view, four panels arranged in a 2x2 grid, neutral solid background, full body, photorealistic"`, aspect ratio `16:9` (or `3:2` — always landscape, see the aspect-ratio rule below)
|
|
211
211
|
2. **Close-up portrait** — prompt: `"[character description], close-up portrait, face and shoulders, neutral solid background, soft studio lighting, photorealistic"`, aspect ratio `1:1`
|
|
212
212
|
|
|
213
213
|
**Step 2 — Call `create_visual_dna`** with:
|
|
@@ -250,3 +250,13 @@ Tools: `list_visual_dna_folders`, `create_visual_dna_folder` (`name`, optional h
|
|
|
250
250
|
- When the user is about to create a **character** DNA, proactively OFFER it: "want me to generate a character sheet first? It makes the character far more consistent and costs a few credits." Run it only on a yes.
|
|
251
251
|
- Flow: `generate_character_sheet {image_urls}` → show the sheet → `create_visual_dna {name, images, character_sheet_url: <url>}`.
|
|
252
252
|
- For non-character DNAs (style/product/environment), skip it.
|
|
253
|
+
|
|
254
|
+
### ⚠️ Aspect ratio — character sheets and bibles are LANDSCAPE
|
|
255
|
+
|
|
256
|
+
Default every character sheet, turnaround, and character/production **bible** sheet to **`3:2` or `16:9`** unless the user asks for something else.
|
|
257
|
+
|
|
258
|
+
These are multi-panel grids laid out side by side — front, back, left, right, plus detail callouts. A square or portrait frame forces the panels to stack, which shrinks each one and costs the engine the very facial and body detail the sheet exists to capture. Landscape gives each panel usable width.
|
|
259
|
+
|
|
260
|
+
- Character sheet / turnaround / bible sheet → `3:2` or `16:9`
|
|
261
|
+
- Close-up portrait reference → `1:1` (a single panel, so the grid logic doesn't apply)
|
|
262
|
+
- Only deviate when the user explicitly names a different ratio.
|
package/src/apps/index.js
CHANGED
|
@@ -447,6 +447,10 @@ const TOOL_WIDGETS = {
|
|
|
447
447
|
generate_speech: UI.generation,
|
|
448
448
|
generate_sound: UI.generation,
|
|
449
449
|
generate_3d: UI.generation,
|
|
450
|
+
// Declared explicitly so Apps hosts prepare the card from tools/list rather
|
|
451
|
+
// than inferring it from the `generate_*` name; result-level _meta alone is
|
|
452
|
+
// not enough for hosts that read the declaration (see the note below).
|
|
453
|
+
generate_character_sheet: UI.generation,
|
|
450
454
|
edit_image: UI.generation,
|
|
451
455
|
edit_video: UI.generation,
|
|
452
456
|
// transcript viewer
|
package/src/apps/theme.js
CHANGED
|
@@ -85,6 +85,15 @@ body {
|
|
|
85
85
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
86
86
|
.k-prompt.k-clamped, .k-caption.k-clamped { cursor: pointer; }
|
|
87
87
|
.k-prompt.expanded { -webkit-line-clamp: unset; }
|
|
88
|
+
/* @VisualDNA / #Moodboard mentions are load-bearing prompt syntax, not prose —
|
|
89
|
+
the server resolves them to the actual asset. Mark them so a glance at the
|
|
90
|
+
prompt shows which references it pulls in. */
|
|
91
|
+
.k-mention {
|
|
92
|
+
display: inline; padding: 1px 5px; border-radius: 5px;
|
|
93
|
+
background: var(--brand-soft); color: var(--brand);
|
|
94
|
+
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
95
|
+
font-size: 0.94em; font-weight: 500;
|
|
96
|
+
}
|
|
88
97
|
/* Single-line media caption (scene / batch prompt under the viewer) */
|
|
89
98
|
.k-caption { font-size: 11px; color: var(--text-faint); margin: 2px 2px 0;
|
|
90
99
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
@@ -127,7 +127,7 @@ function boot(sc) {
|
|
|
127
127
|
if (isListPayload(sc, sc.tool)) return renderList(sc);
|
|
128
128
|
state = sc;
|
|
129
129
|
el('tool-title').textContent = TOOL_TITLES[sc.tool] || 'Generation';
|
|
130
|
-
el('prompt').
|
|
130
|
+
el('prompt').innerHTML = promptHTML(sc.prompt);
|
|
131
131
|
el('prompt').style.display = sc.prompt ? '' : 'none';
|
|
132
132
|
makeExpandable(el('prompt'));
|
|
133
133
|
renderChips(sc);
|
|
@@ -144,6 +144,22 @@ function boot(sc) {
|
|
|
144
144
|
function modelLabel(sc) { return sc.model_name || sc.model; }
|
|
145
145
|
function voiceLabel(sc) { return sc.voice_name || sc.voice || (sc.settings || {}).voice; }
|
|
146
146
|
|
|
147
|
+
// @VisualDNA / #Moodboard mentions are the tag syntax the server resolves into
|
|
148
|
+
// real reference assets — rendering them as flat prose hid the single most
|
|
149
|
+
// consequential part of the prompt. Escape FIRST, then wrap: the pattern only
|
|
150
|
+
// matches after a boundary, so an email or a #fff hex never lights up.
|
|
151
|
+
var MENTION_RE = /(^|[\s([{"'>])([@#][A-Za-z][\w-]*)/g;
|
|
152
|
+
// #ff8800 / #fff are hex colors, and prompts are full of them. A moodboard tag
|
|
153
|
+
// that happens to be 3 or 6 hex letters loses this coin flip; a grading note
|
|
154
|
+
// mistaken for a moodboard is the worse read.
|
|
155
|
+
var HEX_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
156
|
+
function promptHTML(text) {
|
|
157
|
+
return esc(text || '').replace(MENTION_RE, function (m, pre, tag) {
|
|
158
|
+
if (HEX_RE.test(tag)) return m;
|
|
159
|
+
return pre + '<span class="k-mention">' + tag + '</span>';
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
147
163
|
function renderChips(sc) {
|
|
148
164
|
var h = modelChipHTML(modelLabel(sc), sc.model_icon);
|
|
149
165
|
var s = sc.settings || {};
|
|
@@ -154,9 +170,17 @@ function renderChips(sc) {
|
|
|
154
170
|
if (s.quality) h += chip(esc(s.quality) + ' quality');
|
|
155
171
|
if (s.enhance_prompt) h += chip(ICONS.sparkle + ' enhanced');
|
|
156
172
|
if (s.web_search) h += chip('web search');
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
// Ids where we have them (title = the id, so it can be copied / reused),
|
|
174
|
+
// falling back to the old count/boolean shape for payloads generated before
|
|
175
|
+
// the ids were carried.
|
|
176
|
+
var dnaIds = s.visual_dna_ids || [];
|
|
177
|
+
if (dnaIds.length) h += chipT(dnaIds.length + ' Visual DNA', dnaIds.join('\\n'));
|
|
178
|
+
else if (s.visual_dna) h += chip(s.visual_dna + ' Visual DNA');
|
|
179
|
+
var mbIds = s.moodboard_ids || (s.moodboard_id ? [s.moodboard_id] : []);
|
|
180
|
+
if (mbIds.length) h += chipT(mbIds.length > 1 ? mbIds.length + ' moodboards' : 'moodboard', mbIds.join('\\n'));
|
|
181
|
+
else if (s.moodboard) h += chip('moodboard');
|
|
182
|
+
if (s.preset_id) h += chipT('preset', s.preset_id);
|
|
183
|
+
else if (s.preset) h += chip('preset');
|
|
160
184
|
if (s.cinematic) h += chip('cinematic');
|
|
161
185
|
if (s.audio) h += chip(ICONS.sound + ' audio');
|
|
162
186
|
var voice = voiceLabel(sc);
|
|
@@ -176,6 +200,11 @@ function renderChips(sc) {
|
|
|
176
200
|
el('chips').innerHTML = h;
|
|
177
201
|
}
|
|
178
202
|
function chip(inner) { return '<span class="k-chip">' + inner + '</span>'; }
|
|
203
|
+
// Same chip with a hover title — used to surface the asset id behind a
|
|
204
|
+
// "2 Visual DNA" / "preset" label without spending chip width on it.
|
|
205
|
+
function chipT(inner, title) {
|
|
206
|
+
return '<span class="k-chip" title="' + esc(title) + '">' + inner + '</span>';
|
|
207
|
+
}
|
|
179
208
|
function iconFor(kind) {
|
|
180
209
|
switch (kind) {
|
|
181
210
|
case 'image': return ICONS.image;
|
|
@@ -841,8 +870,8 @@ function bootPre(toolName, args) {
|
|
|
841
870
|
}
|
|
842
871
|
el('tool-title').textContent = TOOL_TITLES[toolName] || 'Generation';
|
|
843
872
|
if (args && (args.prompt || args.text || (Array.isArray(args.prompts) && args.prompts.length))) {
|
|
844
|
-
el('prompt').
|
|
845
|
-
(args.prompts.length + ' prompts — ' + args.prompts.join(' · '));
|
|
873
|
+
el('prompt').innerHTML = promptHTML(args.prompt || args.text ||
|
|
874
|
+
(args.prompts.length + ' prompts — ' + args.prompts.join(' · ')));
|
|
846
875
|
el('prompt').style.display = '';
|
|
847
876
|
makeExpandable(el('prompt'));
|
|
848
877
|
}
|
package/src/tools/generate.js
CHANGED
|
@@ -101,16 +101,35 @@ async function pollBatch(client, batch, { interval, timeout }) {
|
|
|
101
101
|
// only values that were really supplied ever surface. This used to be
|
|
102
102
|
// `{ resolution, aspect_ratio }` only — `quality` (and every knob below it) was
|
|
103
103
|
// silently dropped, so three calls at low/medium/high rendered identical cards.
|
|
104
|
+
// `visual_dna`/`moodboard`/`preset` used to collapse to a count and two
|
|
105
|
+
// booleans, which told nobody WHICH asset was applied — not the user reading
|
|
106
|
+
// the card, and not the agent reading the tool result, so a follow-up call
|
|
107
|
+
// could not reuse the same DNA or preset without re-listing. Carry the ids.
|
|
108
|
+
const refSettings = (a = {}) => ({
|
|
109
|
+
enhance_prompt: a.enhance_prompt || undefined,
|
|
110
|
+
web_search: a.enable_web_search || undefined,
|
|
111
|
+
visual_dna_ids: (a.visual_dna_ids && a.visual_dna_ids.length) ? a.visual_dna_ids : undefined,
|
|
112
|
+
moodboard_id: a.moodboard_id || undefined,
|
|
113
|
+
moodboard_ids: (a.moodboard_ids && a.moodboard_ids.length) ? a.moodboard_ids : undefined,
|
|
114
|
+
preset_id: a.preset_id || undefined,
|
|
115
|
+
cinematic: a.cinematic ? true : undefined,
|
|
116
|
+
});
|
|
117
|
+
|
|
104
118
|
const imageSettings = (a = {}) => ({
|
|
105
119
|
resolution: a.resolution,
|
|
106
120
|
aspect_ratio: a.aspect_ratio,
|
|
107
121
|
quality: a.quality,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
...refSettings(a),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Same block for the video tools, which carried only { duration, resolution,
|
|
126
|
+
// aspect_ratio } — a DNA-anchored video card showed no sign a DNA was in play.
|
|
127
|
+
const videoSettings = (a = {}) => ({
|
|
128
|
+
duration: a.duration,
|
|
129
|
+
resolution: a.resolution,
|
|
130
|
+
aspect_ratio: a.aspect_ratio,
|
|
131
|
+
...(a.mode ? { mode: a.mode } : {}),
|
|
132
|
+
...refSettings(a),
|
|
114
133
|
});
|
|
115
134
|
|
|
116
135
|
const promptsField = (what) => z.array(z.string()).max(MAX_BATCH_PROMPTS).optional().describe(
|
|
@@ -220,9 +239,11 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
220
239
|
'generate_image_edit',
|
|
221
240
|
'THE tool for ANY prompt-driven / content edit of an existing image — changing the scene ("make it night", "change the sky to sunset"), adding/removing/replacing objects, restyling, recoloring, compositing, or any "edit this image to…" request. This is the image-editing equivalent of generate_image and runs on strong dedicated editing models (nano-banana-2, gpt-image-2). Provide the source image URL(s) in `source_images` and the instruction in `prompt`. Supports Visual DNA profiles, moodboards, and Kolbo image-editing presets. PRESET CONTRACT: if the user requests a preset, call list_presets type="image_edit" and pass its exact id as `preset_id`; never silently omit it. Do NOT use `edit_image` for these — that tool is only for mechanical enhancements (upscale/reframe/remove-background/skin). For a brand-new image from scratch, use generate_image. Returns the edited image URL(s) when complete.',
|
|
222
241
|
{
|
|
223
|
-
prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
|
|
242
|
+
prompt: z.string().optional().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset"). Required unless `prompts` is provided.'),
|
|
243
|
+
prompts: promptsField('edits of the SAME source images'),
|
|
224
244
|
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). Many text-to-image ids double as editors: the server auto-routes a base id to its editing variant when source_images is present (e.g. "gpt-image-2" → gpt-image-2/edit, "nano-banana-2" → nano-banana-2-image-editing) — passing the bare id is fine, no need to hunt for the "/edit" suffix yourself. BUT this only works for models that actually have a registered edit variant. For prompt-driven photoreal photo edits (object removal, keep-this-person/remove-the-rest, crowd cleanup, inpainting) the ONLY auto-pick defaults are "nano-banana-2" or "gpt-image-2" (use GPT Image 2 when the image needs readable text). Do NOT auto-pick Flux 2 / flux-2/edit / Flux Klein — those are generate-from-scratch / style models; use them only if the user names Flux. If unsure, confirm the model appears in `list_models type="image_editing"` and choose by the strengths summary — Flux edit variants are named-only.'),
|
|
225
245
|
source_images: z.array(z.string()).describe('PIXEL-ACCURATE compositing. Array of source image URLs whose pixel content is composited into the output. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.** Three modes the model auto-detects from input shape: (1) Single image → edit/transform that image. (2) Multiple images, one base + others → composite the others into the base. (3) Multiple images with no clear base → generate a new scene that pixel-accurately embeds the supplied images at positions described in the prompt. Mode 3 is the canonical pattern for thumbnails / branded compositions where exact-pixel logo + face fidelity matter. Refer to source images in the prompt by ordinal position ("FIRST source image", "SECOND source image") or use @image1/@image2 tags. Add "composite AS-IS, do not redraw or restyle" to lock pixels.'),
|
|
246
|
+
reference_images: z.array(z.string()).optional().describe('STYLE/COMPOSITION inspiration, alongside `source_images` on the same call — does NOT embed reference pixels. Use when the edit should follow a look sampled from other images ("re-light this shot like these references"). The pixels that must survive the edit go in `source_images`; these only steer the look. **Cap: `source_images` + `reference_images` together must not exceed `max_reference_images` from list_models for the chosen model.**'),
|
|
226
247
|
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "1:1"'),
|
|
227
248
|
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.'),
|
|
228
249
|
num_images: z.number().optional().describe('Number of output images. Default: 1'),
|
|
@@ -230,23 +251,45 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
230
251
|
moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
|
|
231
252
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
|
|
232
253
|
resolution: z.string().optional().describe('Image resolution tier: "1K" / "2K" / "3K" / "4K". Model-dependent — call list_models and read supported_resolutions. Default: "1K" for most edit models.'),
|
|
254
|
+
quality: z.string().optional().describe('Quality tier for edit models that support it (e.g. "low", "medium", "high", "auto"). Check list_models → supported_qualities on the chosen model. "auto" is normalised to "medium" on gpt-image-2. Omit to use the model default.'),
|
|
233
255
|
preset_id: z.string().optional().describe('Exact preset ID from list_presets type="image_edit" to apply an image-editing preset. If the user requests a preset, resolve and pass it; do not silently omit it.'),
|
|
234
256
|
cinematic: CINEMATIC_SCHEMA,
|
|
235
257
|
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 edit ungraded.'),
|
|
236
258
|
project_id: projectIdField,
|
|
237
259
|
session_id: sessionIdField
|
|
238
260
|
},
|
|
239
|
-
async ({ prompt, model, source_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, cinematic, skip_color_palette, project_id, session_id }) => {
|
|
261
|
+
async ({ prompt, prompts, model, source_images, reference_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id }) => {
|
|
262
|
+
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
240
263
|
model = await canonicalModelId(client, model, 'image_editing'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
244
|
-
}
|
|
264
|
+
const shared = {
|
|
265
|
+
model, source_images, reference_images, aspect_ratio, enhance_prompt,
|
|
266
|
+
visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id, session_id
|
|
267
|
+
};
|
|
268
|
+
const settings = imageSettings(shared);
|
|
269
|
+
|
|
270
|
+
// Batch mode: N different edit instructions against the SAME source
|
|
271
|
+
// images, one widget owning every generation id. Same contract as
|
|
272
|
+
// generate_image — without it, "give me 4 variations of this edit" came
|
|
273
|
+
// back as four stacked cards.
|
|
274
|
+
if (prompts && prompts.length) {
|
|
275
|
+
const batch = await submitBatch(prompts, (p) => client.post('/v1/generate/image-edit', { ...shared, prompt: p }));
|
|
276
|
+
if (ui()) return uiGenerating({
|
|
277
|
+
tool: 'generate_image_edit', kind: 'image', gen: batch.ok[0].gen, client, model,
|
|
278
|
+
count: batch.ids.length, settings,
|
|
279
|
+
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
280
|
+
failed_submissions: batch.failed,
|
|
281
|
+
status_args: { generation_ids: batch.ids, wait: true },
|
|
282
|
+
reference_images: source_images
|
|
283
|
+
});
|
|
284
|
+
return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 240000 });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const gen = await client.post('/v1/generate/image-edit', { ...shared, prompt, num_images });
|
|
245
288
|
|
|
246
289
|
if (ui()) return uiGenerating({
|
|
247
290
|
tool: 'generate_image_edit', kind: 'image', gen, client, model, prompt,
|
|
248
291
|
count: num_images,
|
|
249
|
-
settings
|
|
292
|
+
settings,
|
|
250
293
|
reference_images: source_images
|
|
251
294
|
});
|
|
252
295
|
|
|
@@ -308,7 +351,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
308
351
|
if (ui()) return uiGenerating({
|
|
309
352
|
tool: 'generate_creative_director', kind: 'scenes', gen, client, model, prompt,
|
|
310
353
|
count: scene_count || 4,
|
|
311
|
-
settings: { duration, resolution, aspect_ratio, mode: workflow_type || 'image' },
|
|
354
|
+
settings: videoSettings({ duration, resolution, aspect_ratio, mode: workflow_type || 'image', enhance_prompt, visual_dna_ids, moodboard_id, moodboard_ids }),
|
|
312
355
|
reference_images,
|
|
313
356
|
poll_tool: 'get_creative_director_status',
|
|
314
357
|
status_args: { generation_id: gen.generation_id, wait: true }
|
|
@@ -462,7 +505,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
462
505
|
const batch = await submitBatch(prompts, (p) => client.post('/v1/generate/video', { ...shared, prompt: p }));
|
|
463
506
|
if (ui()) return uiGenerating({
|
|
464
507
|
tool: 'generate_video', kind: 'video', gen: batch.ok[0].gen, client, model,
|
|
465
|
-
count: batch.ids.length, settings: { duration, resolution, aspect_ratio },
|
|
508
|
+
count: batch.ids.length, settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, preset_id }),
|
|
466
509
|
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
467
510
|
failed_submissions: batch.failed,
|
|
468
511
|
status_args: { generation_ids: batch.ids, wait: true },
|
|
@@ -475,7 +518,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
475
518
|
|
|
476
519
|
if (ui()) return uiGenerating({
|
|
477
520
|
tool: 'generate_video', kind: 'video', gen, client, model, prompt,
|
|
478
|
-
settings: { duration, resolution, aspect_ratio },
|
|
521
|
+
settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, preset_id }),
|
|
479
522
|
reference_images
|
|
480
523
|
});
|
|
481
524
|
|
|
@@ -546,7 +589,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
546
589
|
const batch = await submitBatch(items, (it) => client.post('/v1/generate/video/from-image', { ...shared, image_url: it.image_url, prompt: it.prompt }));
|
|
547
590
|
if (ui()) return uiGenerating({
|
|
548
591
|
tool: 'generate_video_from_image', kind: 'video', gen: batch.ok[0].gen, client, model,
|
|
549
|
-
count: batch.ids.length, settings: { duration, resolution, aspect_ratio },
|
|
592
|
+
count: batch.ids.length, settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, visual_dna_ids }),
|
|
550
593
|
generation_ids: batch.ids, prompts: batch.ok.map((o) => o.prompt),
|
|
551
594
|
failed_submissions: batch.failed,
|
|
552
595
|
status_args: { generation_ids: batch.ids, wait: true },
|
|
@@ -559,7 +602,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
559
602
|
|
|
560
603
|
if (ui()) return uiGenerating({
|
|
561
604
|
tool: 'generate_video_from_image', kind: 'video', gen, client, model, prompt,
|
|
562
|
-
settings: { duration, resolution, aspect_ratio },
|
|
605
|
+
settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, visual_dna_ids }),
|
|
563
606
|
reference_images: [image_url]
|
|
564
607
|
});
|
|
565
608
|
|
|
@@ -1050,7 +1093,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1050
1093
|
|
|
1051
1094
|
if (ui()) return uiGenerating({
|
|
1052
1095
|
tool: 'generate_elements', kind: 'video', gen: startResponse, client, model, prompt,
|
|
1053
|
-
settings: { duration, resolution, aspect_ratio },
|
|
1096
|
+
settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, visual_dna_ids, preset_id }),
|
|
1054
1097
|
reference_images: [
|
|
1055
1098
|
...(reference_images || []),
|
|
1056
1099
|
...(keyframes || []).map((keyframe) => keyframe.image_url),
|
|
@@ -1138,7 +1181,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1138
1181
|
|
|
1139
1182
|
if (ui()) return uiGenerating({
|
|
1140
1183
|
tool: 'generate_first_last_frame', kind: 'video', gen: startResponse, client, model, prompt,
|
|
1141
|
-
settings: { duration, resolution, aspect_ratio },
|
|
1184
|
+
settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, visual_dna_ids }),
|
|
1142
1185
|
reference_images: [first_frame_url || first_frame, last_frame_url || last_frame]
|
|
1143
1186
|
.filter((source) => /^https?:\/\//i.test(source || ''))
|
|
1144
1187
|
});
|
|
@@ -1359,7 +1402,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1359
1402
|
if (ui()) return uiGenerating({
|
|
1360
1403
|
tool: 'generate_video_from_video', kind: 'video', gen: startResponse, client, model,
|
|
1361
1404
|
prompt: prompt || (preset ? `Subtitles preset: ${preset}` : undefined),
|
|
1362
|
-
settings: { duration, resolution, aspect_ratio, mode: preset ? 'subtitles' : 'restyle' },
|
|
1405
|
+
settings: videoSettings({ duration, resolution, aspect_ratio, mode: preset ? 'subtitles' : 'restyle', enhance_prompt, visual_dna_ids }),
|
|
1363
1406
|
reference_images: [...(reference_images || []), ...(elements || [])]
|
|
1364
1407
|
});
|
|
1365
1408
|
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -8,6 +8,10 @@ const FormData = require('form-data');
|
|
|
8
8
|
const { resolveToBuffer: sharedResolveToBuffer, VISUAL_DNA_MAX_BYTES, projectScopeReadField, compactList } = require('./_shared');
|
|
9
9
|
const { UI, uiResult, listResult, appsEnabled } = require('../apps');
|
|
10
10
|
|
|
11
|
+
// Reference sheets are a blocking multi-panel render; the 120s client default
|
|
12
|
+
// aborted them mid-flight while the server finished and charged anyway.
|
|
13
|
+
const CHARACTER_SHEET_TIMEOUT_MS = Number(process.env.KOLBO_CHARACTER_SHEET_TIMEOUT_MS) || 600000;
|
|
14
|
+
|
|
11
15
|
// Visual DNA caps reference media at 25MB per file (stricter than the
|
|
12
16
|
// default _shared.resolveToBuffer cap — DNA profiles only need enough
|
|
13
17
|
// source signal to extract features, not full-quality media).
|
|
@@ -200,12 +204,33 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
200
204
|
sheet_type: z.enum(['character', 'character_headless', 'character_bible', 'product', 'environment', 'style']).optional().describe('Sheet layout. character = front/back/face turnaround. character_headless = wardrobe/body refs with a headless front panel (use when clothing must change without fighting the face sheet). character_bible = denser production model-sheet (turnaround + faces + wardrobe + color swatches). product / environment / style = matching DNA types. Defaults to character.')
|
|
201
205
|
},
|
|
202
206
|
async ({ image_urls, sheet_type }) => {
|
|
203
|
-
|
|
207
|
+
// The endpoint is blocking and a 2K multi-panel sheet routinely runs past the
|
|
208
|
+
// 120s default: the MCP aborted while kolbo-api kept going, finished, and
|
|
209
|
+
// billed — the user saw "Failed" for a sheet they had already paid for.
|
|
210
|
+
const result = await client.post(
|
|
211
|
+
'/v1/visual-dna/character-sheet',
|
|
212
|
+
{ image_urls, ...(sheet_type ? { sheet_type } : {}) },
|
|
213
|
+
{ timeoutMs: CHARACTER_SHEET_TIMEOUT_MS },
|
|
214
|
+
);
|
|
215
|
+
// `urls` is NOT redundant with character_sheet_url. Every generation-card
|
|
216
|
+
// reader keys on `urls` — the MCP's own widget (apps/widgets/generation.js),
|
|
217
|
+
// kolbo-code's kolbo-operation.ts mediaUrls(), and its operation.js urlsOf().
|
|
218
|
+
// Kolbo Code mounts a generation card for ANY tool named `generate_*`, so
|
|
219
|
+
// returning only character_sheet_url gave the card zero URLs and it rendered
|
|
220
|
+
// "No output received / Failed" on top of a sheet that generated fine and was
|
|
221
|
+
// already billed — and "Try Again" then double-charged. `widget`/`phase` mark
|
|
222
|
+
// the payload as a completed generation so the card stops falling back to the
|
|
223
|
+
// stale `phase: "review"` envelope built before the tool ran.
|
|
224
|
+
const urls = result.character_sheet_url ? [result.character_sheet_url] : [];
|
|
204
225
|
return {
|
|
205
226
|
content: [{
|
|
206
227
|
type: 'text',
|
|
207
228
|
text: JSON.stringify({
|
|
208
229
|
character_sheet_url: result.character_sheet_url,
|
|
230
|
+
urls,
|
|
231
|
+
widget: 'generation',
|
|
232
|
+
phase: 'completed',
|
|
233
|
+
kind: 'image',
|
|
209
234
|
credits_used: result.credits_used,
|
|
210
235
|
_hint: 'Show the sheet to the user, then pass character_sheet_url to create_visual_dna as that DNA\'s reference.'
|
|
211
236
|
}, null, 2)
|