@kolbo/mcp 1.35.1 → 1.37.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/README.md +4 -0
- package/package.json +1 -1
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +5 -2
- package/skill/VERSION +1 -1
- package/skill/references/workflows/visual-dna.md +7 -0
- package/src/index.js +5 -1
- package/src/tools/agents.js +78 -0
- package/src/tools/generate.js +114 -14
- package/src/tools/presets.js +29 -0
- package/src/tools/visual_dna.js +27 -3
package/README.md
CHANGED
|
@@ -230,11 +230,15 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
230
230
|
| `list_models` | Current model catalog with costs and capabilities |
|
|
231
231
|
| `list_voices` | TTS voices (presets + cloned) |
|
|
232
232
|
| `list_presets` | Generation presets across image/video/music/text-to-video catalogs |
|
|
233
|
+
| `list_cinematic_presets` | "Cinema mode" presets grouped by dimension (camera, lens, focal_length, aperture, angle, shot_type, color_palette, lighting) — pass ids via the `cinematic` arg on `generate_image` / `generate_image_edit`. Only when the user wants a specific cinematic look |
|
|
233
234
|
| `list_projects` | List owned + shared projects (id, name, role, is_default) — call first to resolve a project name into the `project_id` you pass to generation tools |
|
|
234
235
|
| `move_session` | Move a session (generation, chat, transcription…) and ALL its media to another project |
|
|
235
236
|
| `create_doc` / `list_docs` / `get_doc` / `update_doc` / `share_doc` / `delete_doc` | AI Docs (Magic Pad): author project-scoped HTML documents, edit them, get public share links |
|
|
237
|
+
| `generate_character_sheet` | Generate a multi-angle character sheet from reference images (credits) → pass URL to create_visual_dna for stronger character consistency |
|
|
236
238
|
| `list_visual_dna_folders` / `create_visual_dna_folder` / `update_visual_dna_folder` / `delete_visual_dna_folder` / `move_visual_dna_to_folder` | Organize Visual DNA characters into user folders (create/rename/recolor/delete + move DNAs in/out) |
|
|
237
239
|
| `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
|
|
240
|
+
| `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Custom chat agents (reusable named personas; `description` is the system instruction) |
|
|
241
|
+
| `get_creative_director_status` | Re-check a Creative Director batch by generation_id until all parallel scenes finish (use after a `_timed_out` Director run) |
|
|
238
242
|
| `list_sessions` | Enumerate sessions across all types, filterable by project and type |
|
|
239
243
|
| `add_project_context` / `list_project_context` / `delete_project_context` / `get_project_profile` / `regenerate_project_profile` | Project knowledge base (RAG): feed scripts/URLs/notes, read the synthesized living brief |
|
|
240
244
|
| `create_moodboard` / `update_moodboard` / `delete_moodboard` | Build/edit moodboards from image URLs (AI style analysis → master prompt) |
|
package/package.json
CHANGED
package/skill/GENERATED.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AUTO-GENERATED — do not edit
|
|
2
2
|
|
|
3
3
|
This skill/ tree is mirrored from kolbo-code (the single source of truth)
|
|
4
|
-
by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@
|
|
4
|
+
by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@82f9072.
|
|
5
5
|
|
|
6
6
|
It is the skill that 'npx @kolbo/mcp install' deploys into the user's agent.
|
|
7
7
|
To change it, edit packages/opencode/skills/kolbo/ in kolbo-code and push;
|
package/skill/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 0.7.
|
|
2
|
+
version: 0.7.1
|
|
3
3
|
name: kolbo
|
|
4
4
|
description: |
|
|
5
5
|
Generate, edit, or analyze creative media via the Kolbo AI MCP server:
|
|
@@ -113,7 +113,7 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
113
113
|
|------|---------|
|
|
114
114
|
| `list_models` / `list_voices` / `check_credits` / `get_generation_status` / `get_session_usage` | Discovery + status |
|
|
115
115
|
| `upload_media` / `list_media` / `get_media` / `get_media_stats` / `favorite_media` / `unfavorite_media` / `delete_media` / `restore_media` / `permanently_delete_media` / `move_media` / `bulk_*_media` / `*_media_folder` | Media library — see `workflows/media-library.md` |
|
|
116
|
-
| `create_visual_dna` / `list_visual_dnas` / `get_visual_dna` / `delete_visual_dna` / `*_visual_dna_folder` (5 folder tools) | Visual DNA + character folders — see `workflows/visual-dna.md` |
|
|
116
|
+
| `create_visual_dna` / `generate_character_sheet` / `list_visual_dnas` / `get_visual_dna` / `delete_visual_dna` / `*_visual_dna_folder` (5 folder tools) | Visual DNA (+ character sheet, character folders) — see `workflows/visual-dna.md` |
|
|
117
117
|
| `list_moodboards` / `get_moodboard` / `list_presets` | Style overlays |
|
|
118
118
|
| `search_stock_media` / `get_stock_sources` / `get_stock_categories` / `get_stock_collections` / `get_stock_asset` / `analyze_script_for_stock` / `import_stock_asset` | Stock library (free, no credits) — EXISTING photos / videos / 3D / SFX / music. For stock **music** use `search_stock_media` with `mediaType: "music"` (semantic vibe query, e.g. "uplifting corporate background") → `get_stock_asset` for downloads. The older `*_music_library` tools are deprecated adapters over this — prefer the stock tools. |
|
|
119
119
|
| `list_projects` / `move_session` | Projects: resolve a project NAME → the `project_id` you pass on generation/upload/doc calls; `move_session` relocates a whole session + its media when work landed in the wrong project. NOT the same as `app_builder_list_projects`. See "Projects — Where Work Lands" below. |
|
|
@@ -264,6 +264,9 @@ Chat renders markdown natively. `` = inline image. `[label](url)` = l
|
|
|
264
264
|
|
|
265
265
|
Avoid bare URL dumps and HTML `<table>` grids — canvas already provides a gallery.
|
|
266
266
|
|
|
267
|
+
- **3D models (`generate_3d`)**: NEVER paste the raw GLB/FBX/OBJ/USDZ file URLs as a link list. The result renders as a card with a live 3D preview + Download buttons ("View in Canvas"). Just say the model is ready and describe it — the user grabs formats from the card.
|
|
268
|
+
- **NEVER surface a raw provider domain** (e.g. `*.fal.media`, `replicate.delivery`) in chat or `.kolbo/production.md`. All Kolbo outputs are Kolbo-hosted; if you ever see a provider URL in a result, treat it as a bug — report it, don't echo it.
|
|
269
|
+
|
|
267
270
|
**After `generate_creative_director` completes** — share results as individual URLs, one per scene. Do NOT create an HTML grid artifact.
|
|
268
271
|
|
|
269
272
|
**Always** record every URL in `.kolbo/production.md` — see `references/workflows/production-log.md`.
|
package/skill/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.7.
|
|
1
|
+
0.7.1
|
|
@@ -242,3 +242,10 @@ Tools: `list_visual_dna_folders`, `create_visual_dna_folder` (`name`, optional h
|
|
|
242
242
|
- **Deleting a folder never deletes DNAs** — contents move back to root (`items_moved_to_root` in the response). Mention this instead of asking for confirmation on non-empty folders.
|
|
243
243
|
- **Creating many characters for one production?** Create the folder FIRST, then `move_visual_dna_to_folder` each DNA right after `create_visual_dna` — don't leave a big cast unsorted at root.
|
|
244
244
|
- To list a folder's contents: `list_visual_dnas` and filter by each profile's `folder_id` (there is no server-side folder filter).
|
|
245
|
+
|
|
246
|
+
## Character sheet — offer it for character DNAs
|
|
247
|
+
|
|
248
|
+
`generate_character_sheet` builds a multi-angle turnaround from reference image URLs — the strongest consistency booster for a character DNA. It CHARGES CREDITS, so:
|
|
249
|
+
- 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.
|
|
250
|
+
- Flow: `generate_character_sheet {image_urls}` → show the sheet → `create_visual_dna {name, images, character_sheet_url: <url>}`.
|
|
251
|
+
- For non-character DNAs (style/product/environment), skip it.
|
package/src/index.js
CHANGED
|
@@ -69,6 +69,7 @@ const { registerPresetTools } = require('./tools/presets');
|
|
|
69
69
|
const { registerAppBuilderTools } = require('./tools/app_builder');
|
|
70
70
|
const { registerArtifactTools } = require('./tools/artifacts');
|
|
71
71
|
const { registerProjectTools } = require('./tools/projects');
|
|
72
|
+
const { registerAgentTools } = require('./tools/agents');
|
|
72
73
|
const { registerDocTools } = require('./tools/docs');
|
|
73
74
|
const { registerVoiceTools } = require('./tools/voices');
|
|
74
75
|
const { registerMusicLibraryTools } = require('./tools/music_library');
|
|
@@ -115,7 +116,9 @@ function createServer(opts = {}) {
|
|
|
115
116
|
'2. `list_projects` lists the user\'s platform projects (for generations/media/chat). `app_builder_list_projects` is a DIFFERENT tool that scopes App Builder coding sessions only — never use one where the other is meant.',
|
|
116
117
|
'3. Misplaced work is fixable: `move_media` / `bulk_move_media` / `move_folder_contents` move media items between projects; `move_session` moves a whole session (plus its media) to another project. If the user says a generation landed in the wrong project, move it rather than regenerating.',
|
|
117
118
|
'4. If the user has not mentioned any project, omit `project_id` — the default bucket is correct in that case. Do not ask which project to use unless the user\'s intent is ambiguous.',
|
|
118
|
-
'5. Written deliverables (plans, briefs, scripts, research summaries) can live in Kolbo too: author them as AI Docs with `create_doc` (project-scoped, editable in the app, shareable via `share_doc`).'
|
|
119
|
+
'5. Written deliverables (plans, briefs, scripts, research summaries) can live in Kolbo too: author them as AI Docs with `create_doc` (project-scoped, editable in the app, shareable via `share_doc`).',
|
|
120
|
+
'6. DIRECTOR / BATCH JOBS: generate_creative_director runs its scenes (image OR video) in parallel and only reports state="completed" once EVERY scene is terminal. Video batches can take many minutes. If the tool returns `_timed_out:true`, the batch is STILL RUNNING on the server — call `get_creative_director_status` with the returned generation_id and keep checking until state="completed" to collect all scene outputs. NEVER conclude a Director run failed and fall back to plain generate_image/generate_video without first checking status — doing so wastes the user\'s credits by paying twice. If scenes already carry image_urls/video_urls, they are done; do not regenerate.',
|
|
121
|
+
'7. SESSION CONTINUITY: keep one workflow in ONE session. chat_send_message and the generation tools return a `session_id` — for follow-ups, refinements, retries, or additional steps on the SAME task/theme, pass that same `session_id` back on the next call instead of starting fresh. Only OMIT session_id (start a new session) when the user genuinely switches to an unrelated task. Do not open a new conversation/session for every message of the same workflow — it fragments the user\'s history and loses context.'
|
|
119
122
|
].join('\n')
|
|
120
123
|
});
|
|
121
124
|
|
|
@@ -135,6 +138,7 @@ function createServer(opts = {}) {
|
|
|
135
138
|
registerAppBuilderTools(server, client, toolOptions);
|
|
136
139
|
registerArtifactTools(server, client, toolOptions);
|
|
137
140
|
registerProjectTools(server, client, toolOptions);
|
|
141
|
+
registerAgentTools(server, client, toolOptions);
|
|
138
142
|
registerDocTools(server, client, toolOptions);
|
|
139
143
|
registerMusicLibraryTools(server, client, toolOptions);
|
|
140
144
|
registerStockLibraryTools(server, client, toolOptions);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
|
|
2
|
+
* CONTRACT. Never rename, remove, or break an existing tool/arg — old cached
|
|
3
|
+
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
|
+
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
|
+
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
|
|
8
|
+
function registerAgentTools(server, client) {
|
|
9
|
+
// ─── list_agents ───────────────────────────────────────────
|
|
10
|
+
server.tool(
|
|
11
|
+
'list_agents',
|
|
12
|
+
'List the user\'s custom chat agents (personal + any global/org preset agents visible to them). A custom agent is a reusable, named persona for the chat tool — its `description` is the system instruction the model adopts. Use this to resolve an agent NAME the user mentioned into its id, or to show what agents exist. Returns id, name, description, emoji, is_global. Personal agents (is_global:false) are editable/deletable; global presets are not.',
|
|
13
|
+
{ search: z.string().optional().describe('Optional case-insensitive name filter.') },
|
|
14
|
+
async ({ search }) => {
|
|
15
|
+
const qs = search ? `?search=${encodeURIComponent(search)}` : '';
|
|
16
|
+
const result = await client.get(`/v1/agents${qs}`);
|
|
17
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
18
|
+
agents: result.agents || [],
|
|
19
|
+
count: result.count || (result.agents || []).length
|
|
20
|
+
}, null, 2) }] };
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// ─── create_agent ──────────────────────────────────────────
|
|
25
|
+
server.tool(
|
|
26
|
+
'create_agent',
|
|
27
|
+
'Create a reusable custom chat agent (a named persona for the chat tool). The `description` IS the agent\'s system instruction — write it as the persona + behavior you want ("You are a senior creative director. Turn any brief into a structured shot list…"). Use when the user wants a persistent, reusable assistant ("make me a creative-director agent", "set up a support-triage bot"). For a ONE-OFF persona on a single conversation, pass `system_prompt` to chat_send_message instead — no need to create an agent. Plan limits apply (server rejects when the custom-agent cap is reached).',
|
|
28
|
+
{
|
|
29
|
+
name: z.string().optional().describe('Agent name. If omitted, a name is generated from the description.'),
|
|
30
|
+
description: z.string().describe('The agent persona + instructions (max 2000 chars). This becomes the system prompt the model adopts in every conversation that uses the agent.'),
|
|
31
|
+
emoji: z.string().optional().describe('Optional emoji avatar (auto-picked if omitted).'),
|
|
32
|
+
thumbnail: z.string().optional().describe('Optional thumbnail image URL.')
|
|
33
|
+
},
|
|
34
|
+
async ({ name, description, emoji, thumbnail }) => {
|
|
35
|
+
const body = { description };
|
|
36
|
+
if (name !== undefined) body.name = name;
|
|
37
|
+
if (emoji !== undefined) body.emoji = emoji;
|
|
38
|
+
if (thumbnail !== undefined) body.thumbnail = thumbnail;
|
|
39
|
+
const result = await client.post('/v1/agents', body);
|
|
40
|
+
return { content: [{ type: 'text', text: JSON.stringify({ agent: result.agent, _hint: 'Reuse this agent by selecting it in the chat tool. Its description is the system instruction applied to every conversation using it.' }, null, 2) }] };
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// ─── update_agent ──────────────────────────────────────────
|
|
45
|
+
server.tool(
|
|
46
|
+
'update_agent',
|
|
47
|
+
'Update a custom chat agent\'s name, description (persona/instructions), or emoji/thumbnail. Only personal agents you own can be edited — global preset agents are protected. Resolve the agent id with list_agents first.',
|
|
48
|
+
{
|
|
49
|
+
agent_id: z.string().describe('Agent id (from list_agents).'),
|
|
50
|
+
name: z.string().optional().describe('New name.'),
|
|
51
|
+
description: z.string().optional().describe('New persona/instructions (replaces the old description; max 2000 chars).'),
|
|
52
|
+
emoji: z.string().optional().describe('New emoji avatar.'),
|
|
53
|
+
thumbnail: z.string().optional().describe('New thumbnail image URL.')
|
|
54
|
+
},
|
|
55
|
+
async ({ agent_id, name, description, emoji, thumbnail }) => {
|
|
56
|
+
const body = {};
|
|
57
|
+
if (name !== undefined) body.name = name;
|
|
58
|
+
if (description !== undefined) body.description = description;
|
|
59
|
+
if (emoji !== undefined) body.emoji = emoji;
|
|
60
|
+
if (thumbnail !== undefined) body.thumbnail = thumbnail;
|
|
61
|
+
const result = await client.put(`/v1/agents/${encodeURIComponent(agent_id)}`, body);
|
|
62
|
+
return { content: [{ type: 'text', text: JSON.stringify({ agent: result.agent }, null, 2) }] };
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ─── delete_agent ──────────────────────────────────────────
|
|
67
|
+
server.tool(
|
|
68
|
+
'delete_agent',
|
|
69
|
+
'Delete a custom chat agent you own. Global preset agents cannot be deleted. This removes the agent config only — it does not touch any conversations that used it.',
|
|
70
|
+
{ agent_id: z.string().describe('Agent id (from list_agents).') },
|
|
71
|
+
async ({ agent_id }) => {
|
|
72
|
+
const result = await client.delete(`/v1/agents/${encodeURIComponent(agent_id)}`);
|
|
73
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { registerAgentTools };
|
package/src/tools/generate.js
CHANGED
|
@@ -9,6 +9,34 @@ const { pollUntilDone } = require('../polling');
|
|
|
9
9
|
const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, uiCompleted, appsEnabled } = require('./_shared');
|
|
10
10
|
const { UI, uiResult, canonicalModelId } = require('../apps');
|
|
11
11
|
|
|
12
|
+
// ─── Cinematic Dimensions schema (shared by generate_image + generate_image_edit) ───
|
|
13
|
+
// Kolbo's "Cinema mode": eight independent photographic dimensions, each an OPTIONAL
|
|
14
|
+
// preset id fetched from list_cinematic_presets. Any dimension left unset = "Auto" (the
|
|
15
|
+
// enhancer decides). Applies ONLY when the user explicitly wants a deliberate cinematic
|
|
16
|
+
// look — omit the whole object for a normal generation. Ids are validated server-side
|
|
17
|
+
// against their dimension (passing a lighting id in the camera slot is a 400). The
|
|
18
|
+
// selected fragments are woven into the prompt before enhancement.
|
|
19
|
+
// NOTE: dimensions are data-driven — always call list_cinematic_presets for the live set
|
|
20
|
+
// and valid ids; the keys below mirror the current catalog (Genre was removed).
|
|
21
|
+
const cinematicDim = (label) => z.string().nullable().optional()
|
|
22
|
+
.describe(`${label} preset id from list_cinematic_presets. Omit or null for Auto.`);
|
|
23
|
+
const CINEMATIC_SCHEMA = z.object({
|
|
24
|
+
camera: cinematicDim('Camera body/format (e.g. ARRI Alexa, 16mm film, smartphone)'),
|
|
25
|
+
lens: cinematicDim('Lens character (e.g. anamorphic, vintage prime, macro)'),
|
|
26
|
+
focal_length: cinematicDim('Focal length / field of view (e.g. 24mm wide, 85mm portrait)'),
|
|
27
|
+
aperture: cinematicDim('Aperture / depth of field (e.g. f/1.4 shallow, f/8 deep)'),
|
|
28
|
+
angle: cinematicDim('Camera angle (e.g. low angle, birds-eye, eye level)'),
|
|
29
|
+
shot_type: cinematicDim('Shot type / framing (e.g. close-up, wide shot, full shot)'),
|
|
30
|
+
color_palette: cinematicDim('Color grade (e.g. teal & orange, technicolor, classic B&W)'),
|
|
31
|
+
lighting: cinematicDim('Lighting technique (e.g. rim light, golden hour, low-key)'),
|
|
32
|
+
}).optional().describe(
|
|
33
|
+
'Cinema mode — an optional deliberate photographic treatment. Pass preset ids obtained from ' +
|
|
34
|
+
'list_cinematic_presets (at most one per dimension). Only include a dimension the user actually ' +
|
|
35
|
+
'wants; every omitted/null dimension is Auto (the enhancer completes the look in the spirit of the ' +
|
|
36
|
+
'ones you set). Omit the whole object entirely for an ordinary, non-cinematic generation. Ids are ' +
|
|
37
|
+
'validated against their dimension server-side. Dimensions are data-driven — never hardcode ids.'
|
|
38
|
+
);
|
|
39
|
+
|
|
12
40
|
function registerGenerateTools(server, client, options = {}) {
|
|
13
41
|
// Only enabled by hosts that explicitly opt in (the remote HTTP connector).
|
|
14
42
|
// stdio hosts (Kolbo Code, Claude Desktop, Cursor) leave this false, so their
|
|
@@ -34,13 +62,14 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
34
62
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding for the prompt (useful for current events, brand references, real-world accuracy). Default: false'),
|
|
35
63
|
resolution: z.string().optional().describe('Image resolution tier: "1K" (~1024px), "2K" (Full HD), "3K" (QHD), or "4K" (UHD). Model-dependent — call list_models and read supported_resolutions on the chosen model. Read resolution_multipliers on the same model to predict credit cost. Omit to use the model default.'),
|
|
36
64
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="image" to apply a saved style preset to this generation.'),
|
|
65
|
+
cinematic: CINEMATIC_SCHEMA,
|
|
37
66
|
project_id: projectIdField
|
|
38
67
|
},
|
|
39
|
-
async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, project_id }) => {
|
|
68
|
+
async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, cinematic, project_id }) => {
|
|
40
69
|
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
41
70
|
const gen = await client.post('/v1/generate/image', {
|
|
42
71
|
prompt, model, aspect_ratio, enhance_prompt, num_images,
|
|
43
|
-
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, project_id
|
|
72
|
+
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, cinematic, project_id
|
|
44
73
|
});
|
|
45
74
|
|
|
46
75
|
if (ui()) return uiGenerating({
|
|
@@ -85,13 +114,14 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
85
114
|
moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
|
|
86
115
|
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
|
|
87
116
|
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.'),
|
|
117
|
+
cinematic: CINEMATIC_SCHEMA,
|
|
88
118
|
project_id: projectIdField
|
|
89
119
|
},
|
|
90
|
-
async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, project_id }) => {
|
|
120
|
+
async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, project_id }) => {
|
|
91
121
|
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
92
122
|
const gen = await client.post('/v1/generate/image-edit', {
|
|
93
123
|
prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
|
|
94
|
-
visual_dna_ids, moodboard_id, enable_web_search, resolution, project_id
|
|
124
|
+
visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, project_id
|
|
95
125
|
});
|
|
96
126
|
|
|
97
127
|
if (ui()) return uiGenerating({
|
|
@@ -151,11 +181,43 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
151
181
|
enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id
|
|
152
182
|
});
|
|
153
183
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
184
|
+
const cdStatusUrl = `/v1/generate/creative-director/${gen.generation_id}/status`;
|
|
185
|
+
// Video mode runs N scenes as parallel video generations, each of which
|
|
186
|
+
// can take several minutes — a whole batch routinely exceeds the 10-min
|
|
187
|
+
// image window. Give video batches 30 min (the server watchdog finalizes
|
|
188
|
+
// stuck batches around 15-30 min, so this aligns client + server).
|
|
189
|
+
const cdTimeout = workflow_type === 'video' ? 1800000 : 600000;
|
|
190
|
+
|
|
191
|
+
let result;
|
|
192
|
+
try {
|
|
193
|
+
result = await pollUntilDone(client, gen.generation_id, {
|
|
194
|
+
interval: (gen.poll_interval_hint || 5) * 1000,
|
|
195
|
+
timeout: cdTimeout,
|
|
196
|
+
statusUrl: cdStatusUrl
|
|
197
|
+
});
|
|
198
|
+
} catch (err) {
|
|
199
|
+
// On a client-side poll timeout the batch is almost always STILL
|
|
200
|
+
// running (or already finished) on the server. Don't lose the work:
|
|
201
|
+
// return whatever scenes have landed plus the exact re-check path,
|
|
202
|
+
// instead of throwing an opaque timeout at the agent.
|
|
203
|
+
if (err && err.timedOut) {
|
|
204
|
+
let partial = null;
|
|
205
|
+
try { partial = await client.get(cdStatusUrl); } catch (_) { /* ignore */ }
|
|
206
|
+
const doneScenes = (partial?.scenes || []).filter(s => s.status === 'completed').map(s => ({
|
|
207
|
+
scene_number: s.scene_number, title: s.title, image_urls: s.image_urls, video_urls: s.video_urls
|
|
208
|
+
}));
|
|
209
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
210
|
+
state: partial?.state || 'processing',
|
|
211
|
+
generation_id: gen.generation_id,
|
|
212
|
+
scenes: doneScenes,
|
|
213
|
+
total_scenes: partial?.scenes?.length || 0,
|
|
214
|
+
completed_scenes: doneScenes.length,
|
|
215
|
+
_timed_out: true,
|
|
216
|
+
_hint: `Still running after the poll window. Call get_creative_director_status with generation_id="${gen.generation_id}" to keep checking until state="completed" — do NOT re-run generate_creative_director.`
|
|
217
|
+
}, null, 2) }] };
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
159
221
|
|
|
160
222
|
const scenes = (result.scenes || [])
|
|
161
223
|
.filter(s => s.status === 'completed')
|
|
@@ -187,6 +249,42 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
187
249
|
}
|
|
188
250
|
);
|
|
189
251
|
|
|
252
|
+
// ─── get_creative_director_status ──────────────────────────
|
|
253
|
+
// Creative Director uses a DEDICATED status route (per-scene state + all
|
|
254
|
+
// scene image/video URLs). The generic get_generation_status hits
|
|
255
|
+
// /v1/generate/:id/status and CANNOT read a CD batch — so a long or
|
|
256
|
+
// backgrounded CD run (especially parallel VIDEO scenes that exceed the
|
|
257
|
+
// blocking poll window) needs this tool to be re-checked until done.
|
|
258
|
+
server.tool(
|
|
259
|
+
'get_creative_director_status',
|
|
260
|
+
'Check the status of a Creative Director batch (from generate_creative_director) by its generation_id. Returns overall state ("processing" until EVERY scene is terminal, then "completed"/"failed") plus each scene\'s number, title, per-scene status, and image_urls/video_urls. Use this to resume checking a batch that was still running when generate_creative_director returned `_timed_out: true` — poll it until state="completed" to collect ALL parallel scene outputs at once. Do NOT use the generic get_generation_status for Creative Director ids; it points at the wrong endpoint.',
|
|
261
|
+
{
|
|
262
|
+
generation_id: z.string().describe('The Creative Director generation_id returned by generate_creative_director.')
|
|
263
|
+
},
|
|
264
|
+
async ({ generation_id }) => {
|
|
265
|
+
const status = await client.get(`/v1/generate/creative-director/${encodeURIComponent(generation_id)}/status`);
|
|
266
|
+
const scenes = (status.scenes || []).map(s => ({
|
|
267
|
+
scene_number: s.scene_number,
|
|
268
|
+
status: s.status,
|
|
269
|
+
title: s.title,
|
|
270
|
+
image_urls: s.image_urls || null,
|
|
271
|
+
video_urls: s.video_urls || null
|
|
272
|
+
}));
|
|
273
|
+
const completed = scenes.filter(s => s.status === 'completed').length;
|
|
274
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
275
|
+
state: status.state,
|
|
276
|
+
generation_id,
|
|
277
|
+
progress: status.progress,
|
|
278
|
+
scenes,
|
|
279
|
+
total_scenes: scenes.length,
|
|
280
|
+
completed_scenes: completed,
|
|
281
|
+
_hint: status.state === 'completed'
|
|
282
|
+
? 'All scenes terminal. Every completed scene\'s image_urls/video_urls are final.'
|
|
283
|
+
: 'Still running — call get_creative_director_status again in a few seconds until state="completed".'
|
|
284
|
+
}, null, 2) }] };
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
190
288
|
// ─── generate_video ────────────────────────────────────────
|
|
191
289
|
// NOTE: text-to-video does NOT support Visual DNA — the textToVideoGeneration
|
|
192
290
|
// controller in kolbo-api never reads visualDnaIds. For character-consistent
|
|
@@ -204,12 +302,13 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
204
302
|
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject). **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
|
|
205
303
|
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.'),
|
|
206
304
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.'),
|
|
305
|
+
sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1, Kling V3/2.6, PixVerse V6). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
|
|
207
306
|
project_id: projectIdField
|
|
208
307
|
},
|
|
209
|
-
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, project_id }) => {
|
|
308
|
+
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, project_id }) => {
|
|
210
309
|
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
211
310
|
const gen = await client.post('/v1/generate/video', {
|
|
212
|
-
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, project_id
|
|
311
|
+
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, project_id
|
|
213
312
|
});
|
|
214
313
|
|
|
215
314
|
if (ui()) return uiGenerating({
|
|
@@ -252,13 +351,14 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
252
351
|
duration: z.number().optional().describe('Duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
|
|
253
352
|
enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
|
|
254
353
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false` the model ignores DNA entirely.**'),
|
|
255
|
-
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions.
|
|
354
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions.'),
|
|
355
|
+
sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1 Lite, Kling V3 4K, PixVerse V6, Kling 2.6/v3). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
|
|
256
356
|
project_id: projectIdField
|
|
257
357
|
},
|
|
258
|
-
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, project_id }) => {
|
|
358
|
+
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, project_id }) => {
|
|
259
359
|
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
260
360
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
261
|
-
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, project_id
|
|
361
|
+
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, project_id
|
|
262
362
|
});
|
|
263
363
|
|
|
264
364
|
if (ui()) return uiGenerating({
|
package/src/tools/presets.js
CHANGED
|
@@ -50,6 +50,35 @@ function registerPresetTools(server, client, options = {}) {
|
|
|
50
50
|
return { content: [{ type: 'text', text }] };
|
|
51
51
|
}
|
|
52
52
|
);
|
|
53
|
+
|
|
54
|
+
// ─── list_cinematic_presets ────────────────────────────────
|
|
55
|
+
server.tool(
|
|
56
|
+
'list_cinematic_presets',
|
|
57
|
+
'List Kolbo "Cinema mode" presets for image generation/editing — a deliberate photographic ' +
|
|
58
|
+
'treatment layered onto the prompt. Returns presets grouped by DIMENSION (data-driven from the ' +
|
|
59
|
+
'live catalog; today: camera, lens, focal_length, aperture, angle, shot_type, color_palette, ' +
|
|
60
|
+
'lighting). Each preset has id, name, description, thumbnail. ONLY call this when the user wants a ' +
|
|
61
|
+
'specific cinematic look; then pass the chosen ids via the `cinematic` arg of generate_image / ' +
|
|
62
|
+
'generate_image_edit — at most one id per dimension. "Auto" is the absence of a selection: omit a ' +
|
|
63
|
+
'dimension (or the whole `cinematic` object) to let the enhancer decide. For an ordinary generation ' +
|
|
64
|
+
'do not call this at all. Never hardcode ids — dimensions and presets change; always fetch here.',
|
|
65
|
+
{},
|
|
66
|
+
async () => {
|
|
67
|
+
const result = await client.get('/v1/cinematic-presets');
|
|
68
|
+
// The public route serves the raw grouped map ({ camera:[...], lens:[...] });
|
|
69
|
+
// the SDK envelope wraps it as { dimensions:{...} }. Accept either shape.
|
|
70
|
+
const dimensions = (result && result.dimensions) || result || {};
|
|
71
|
+
const text = JSON.stringify({
|
|
72
|
+
dimensions,
|
|
73
|
+
available_dimensions: Object.keys(dimensions),
|
|
74
|
+
_usage_hint: 'Include ONLY the dimensions the user actually wants; pass their ids as the ' +
|
|
75
|
+
'`cinematic` arg on generate_image / generate_image_edit, e.g. {"camera":"<id>","lighting":"<id>"}. ' +
|
|
76
|
+
'Every omitted/null dimension is Auto — the enhancer completes the look in the spirit of the ones ' +
|
|
77
|
+
'you set. Omit the whole object for a non-cinematic generation. Ids are validated per-dimension server-side.'
|
|
78
|
+
}, null, 2);
|
|
79
|
+
return { content: [{ type: 'text', text }] };
|
|
80
|
+
}
|
|
81
|
+
);
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
module.exports = { registerPresetTools };
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -20,16 +20,17 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
20
20
|
// ─── create_visual_dna ─────────────────────────────────────
|
|
21
21
|
server.tool(
|
|
22
22
|
'create_visual_dna',
|
|
23
|
-
'Create a Visual DNA profile from reference media. Each item in images/video/audio can be a public URL or an absolute local file path. Max 4 images, 1 video, 1 audio. Files capped at 25MB each.',
|
|
23
|
+
'Create a Visual DNA profile from reference media. Each item in images/video/audio can be a public URL or an absolute local file path. Max 4 images, 1 video, 1 audio. Files capped at 25MB each. For CHARACTER DNAs, a multi-angle character sheet dramatically improves consistency — offer to generate one with `generate_character_sheet` first, then pass its URL as `character_sheet_url` here (see that tool).',
|
|
24
24
|
{
|
|
25
25
|
name: z.string().describe('Name of the Visual DNA profile. **Pick a short, lowercase, no-space single token** (e.g. `maya`, `tokyo_neon`, `brand_red`, `esther_model`) — never names with spaces (`Sarah Johnson` ❌). The user/LLM types this as `@<name>` inside generation prompts, and the @ parser stops at the first space, so `@Sarah Johnson` matches only `Sarah` and the binding silently drops. Multi-word concepts should use underscores or be a single token. Names are case-insensitive on lookup, but **reserved** values rejected on creation: `Image1`, `Image2`, …, `Video1`, …, `Audio1`, … (any-language characters allowed; max 100 chars).'),
|
|
26
26
|
dna_type: z.string().optional().describe('Type: "character", "style", "product", "scene", "environment". Default: "character"'),
|
|
27
27
|
prompt_helper: z.string().optional().describe('Optional description/notes to guide DNA extraction'),
|
|
28
28
|
images: z.array(z.string()).optional().describe('Array of image sources (URLs or absolute local paths). Max 4.'),
|
|
29
29
|
video: z.string().optional().describe('Optional video source (URL or absolute local path)'),
|
|
30
|
-
audio: z.string().optional().describe('Optional audio source (URL or absolute local path)')
|
|
30
|
+
audio: z.string().optional().describe('Optional audio source (URL or absolute local path)'),
|
|
31
|
+
character_sheet_url: z.string().optional().describe('URL of a multi-angle character sheet (from `generate_character_sheet`) to set as the DNA\'s primary reference. Strongly recommended for character DNAs — it is the single biggest consistency booster. Omit for non-character DNAs or when the user declines.')
|
|
31
32
|
},
|
|
32
|
-
async ({ name, dna_type, prompt_helper, images, video, audio }) => {
|
|
33
|
+
async ({ name, dna_type, prompt_helper, images, video, audio, character_sheet_url }) => {
|
|
33
34
|
if (!name || !name.trim()) {
|
|
34
35
|
throw new Error('name is required');
|
|
35
36
|
}
|
|
@@ -53,6 +54,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
53
54
|
form.append('name', name);
|
|
54
55
|
if (dna_type) form.append('dnaType', dna_type);
|
|
55
56
|
if (prompt_helper) form.append('promptHelper', prompt_helper);
|
|
57
|
+
if (character_sheet_url) form.append('characterSheetUrl', character_sheet_url);
|
|
56
58
|
|
|
57
59
|
for (const f of imageFiles) {
|
|
58
60
|
form.append('images', f.buffer, { filename: f.filename, contentType: f.contentType });
|
|
@@ -159,6 +161,28 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
159
161
|
}
|
|
160
162
|
);
|
|
161
163
|
|
|
164
|
+
// ─── generate_character_sheet ──────────────────────────────
|
|
165
|
+
server.tool(
|
|
166
|
+
'generate_character_sheet',
|
|
167
|
+
'Generate a multi-angle character sheet (turnaround) from 1+ reference image URLs — the same step the in-app Visual DNA wizard offers. The sheet is the single strongest consistency booster for a character DNA. CHARGES CREDITS, so when the user is about to create a character DNA, OFFER this first ("want me to generate a character sheet for stronger consistency? it costs a few credits") and only run it on a yes. Returns `character_sheet_url` — pass it as `character_sheet_url` to `create_visual_dna`.',
|
|
168
|
+
{
|
|
169
|
+
image_urls: z.array(z.string()).min(1).describe('Reference image URLs of the character (front/side/varied angles work best). Use generated-image URLs or upload_media output.')
|
|
170
|
+
},
|
|
171
|
+
async ({ image_urls }) => {
|
|
172
|
+
const result = await client.post('/v1/visual-dna/character-sheet', { image_urls });
|
|
173
|
+
return {
|
|
174
|
+
content: [{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: JSON.stringify({
|
|
177
|
+
character_sheet_url: result.character_sheet_url,
|
|
178
|
+
credits_used: result.credits_used,
|
|
179
|
+
_hint: 'Show the sheet to the user, then pass character_sheet_url to create_visual_dna as that DNA\'s reference.'
|
|
180
|
+
}, null, 2)
|
|
181
|
+
}]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
162
186
|
// ─── Visual DNA folders (organize characters) ──────────────
|
|
163
187
|
// Folders are user-scoped and flat. Only PERSONAL Visual DNAs can live in
|
|
164
188
|
// folders — global/organization presets are rejected by the server.
|