@kolbo/mcp 1.73.1 → 1.75.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/skill/SKILL.md +1 -0
- package/src/apps/widgets/generation.js +46 -1
- package/src/tools/_shared.js +5 -0
- package/src/tools/generate.js +38 -11
- package/src/tools/models.js +24 -2
- package/src/tools/visual_dna.js +12 -3
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -222,6 +222,7 @@ Full tables + formulas in `references/workflows/cost-and-validation.md`. Quick r
|
|
|
222
222
|
- **Otherwise confirm** via the labeled-question card: the parameters + the credit cost, suggest a cheaper alternative if one fits, wait for the user's pick. Never fire on defaults the user didn't choose.
|
|
223
223
|
- **Batch totalling 100+ credits**: run `check_credits` first.
|
|
224
224
|
- **Quote real cost**: after firing, log `credits_used` (from the tool result) to `.kolbo/production.md` — never `base × count`.
|
|
225
|
+
- **Video/lipsync `credit` is per-SECOND, not per-clip**: `total = credit × duration`. This is the universal rule for video/firstlast/elements/motion_graphic/cast types, not a per-model exception — `list_models` states it inline now. The one carve-out is a model with `flat_credit_by_resolution` set.
|
|
225
226
|
- **Never state "credits remaining" from arithmetic** (opening balance − generation costs). Coding/chat usage deducts credits too, so the math is always wrong. Report cost only; if the user asks for their balance, call `check_credits` fresh at that moment.
|
|
226
227
|
|
|
227
228
|
## Rate Limiting & Batch Generation
|
|
@@ -70,7 +70,8 @@ var TOOL_TITLES = {
|
|
|
70
70
|
generate_first_last_frame: 'First–Last Frame', generate_lipsync: 'Lipsync',
|
|
71
71
|
generate_music: 'Music Generation', generate_speech: 'Text to Speech',
|
|
72
72
|
generate_sound: 'Sound Effect', generate_3d: '3D Generation',
|
|
73
|
-
generate_creative_director: 'Creative Director', edit_image: 'Image Edit', edit_video: 'Video Edit'
|
|
73
|
+
generate_creative_director: 'Creative Director', edit_image: 'Image Edit', edit_video: 'Video Edit',
|
|
74
|
+
get_generation_status: 'Generations'
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
// Long text is clamped by CSS (.k-prompt 2 lines / .k-caption 1 line). When it
|
|
@@ -591,6 +592,7 @@ function renderResult(sc) {
|
|
|
591
592
|
// voice that actually ran, so the finished card must not keep the guess.
|
|
592
593
|
renderChips(sc);
|
|
593
594
|
setPhaseChip('', false);
|
|
595
|
+
if (sc.kind === 'status' && Array.isArray(sc.items)) return renderStatusGrid(sc);
|
|
594
596
|
if (sc.batch && sc.scenes && sc.scenes.length) return renderBatchGrid(sc);
|
|
595
597
|
if (sc.kind === 'scenes' && sc.scenes && sc.scenes.length) return renderScenes(sc);
|
|
596
598
|
var urls = sc.urls || [];
|
|
@@ -773,6 +775,49 @@ function renderBatchGrid(sc) {
|
|
|
773
775
|
window.kolbo.notifySize();
|
|
774
776
|
}
|
|
775
777
|
|
|
778
|
+
// get_generation_status checking SEVERAL ids in one call (the "run these N
|
|
779
|
+
// generations in parallel, then check on all of them" pattern). Each item is
|
|
780
|
+
// independently image/video/audio and independently pending/completed/failed
|
|
781
|
+
// — a single generation-in-progress tile grid can't express that. Completed
|
|
782
|
+
// items get a real thumbnail (same tile markup as renderBatchGrid, so the two
|
|
783
|
+
// grids read as one visual language); pending/failed items get the same
|
|
784
|
+
// spinner/error badge language renderGenerating/renderError already use, so
|
|
785
|
+
// nothing here is a new visual pattern, only a new combination of them.
|
|
786
|
+
function renderStatusGrid(sc) {
|
|
787
|
+
var items = sc.items;
|
|
788
|
+
if (!items.length) return renderError('No results');
|
|
789
|
+
el('stage').innerHTML = '<div class="k-gen-grid n' + Math.min(items.length, 4) + '">' +
|
|
790
|
+
items.map(function (it, i) {
|
|
791
|
+
var cap = it.title ? '<span class="k-skel-cap" title="' + esc(it.title) + '">' + esc(it.title) + '</span>' : '';
|
|
792
|
+
if (it.state === 'completed' && it.url) {
|
|
793
|
+
// Classify by extension, same as every other reference/thumb in this
|
|
794
|
+
// file — the tool only knows a url came back, not what kind it is.
|
|
795
|
+
it.kind = refKind(it.url, 'image');
|
|
796
|
+
var shape = it.kind === 'video' ? 'video' : 'square';
|
|
797
|
+
return '<div class="k-skel done ' + shape + '" data-focus="' + i + '">' +
|
|
798
|
+
(it.kind === 'video'
|
|
799
|
+
? '<video class="k-cell-fill" src="' + esc(it.url) + '" controls playsinline preload="metadata"></video>'
|
|
800
|
+
: it.kind === 'audio'
|
|
801
|
+
? '<div class="k-cell-fill" style="display:flex;align-items:center;justify-content:center">' + ICONS.sound + '</div>'
|
|
802
|
+
: '<img class="k-cell-fill" src="' + esc(it.url) + '" alt="" loading="lazy" style="cursor:zoom-in">') +
|
|
803
|
+
cap + dlBtnHTML(it.url) + '</div>';
|
|
804
|
+
}
|
|
805
|
+
var failed = it.state === 'failed' || it.state === 'cancelled';
|
|
806
|
+
var badge = failed
|
|
807
|
+
? '<span class="k-gen-badge" style="background:var(--error,#e5484d)"><span aria-hidden="true">✕</span>' + esc(it.state) + '</span>'
|
|
808
|
+
: '<span class="k-gen-badge"><span class="k-spin"></span>' + esc(it.state || 'processing') + '</span>';
|
|
809
|
+
return '<div class="k-skel square">' + badge + cap + '</div>';
|
|
810
|
+
}).join('') + '</div>';
|
|
811
|
+
wireDlButtons(el('stage'));
|
|
812
|
+
Array.prototype.forEach.call(el('stage').querySelectorAll('[data-focus]'), function (cell) {
|
|
813
|
+
var it = items[+cell.getAttribute('data-focus')];
|
|
814
|
+
if (it.kind !== 'image') return; // <video controls> owns its own clicks
|
|
815
|
+
cell.onclick = function () { focusMedia(it.url); };
|
|
816
|
+
});
|
|
817
|
+
renderActions(sc);
|
|
818
|
+
window.kolbo.notifySize();
|
|
819
|
+
}
|
|
820
|
+
|
|
776
821
|
function renderScenes(sc) {
|
|
777
822
|
var items = sceneItems(sc);
|
|
778
823
|
if (!items.length) return renderError('No completed scenes received');
|
package/src/tools/_shared.js
CHANGED
|
@@ -650,6 +650,11 @@ async function uiCompleted(p, textPayload, extraContent) {
|
|
|
650
650
|
title: p.title,
|
|
651
651
|
duration: p.duration,
|
|
652
652
|
scenes: p.scenes,
|
|
653
|
+
// Independent per-item results (get_generation_status checking several ids
|
|
654
|
+
// at once) — each one carries its OWN state/media, unlike `scenes`/`urls`
|
|
655
|
+
// above which assume everything finished together. Only set when the
|
|
656
|
+
// caller actually has this shape; every existing caller is unaffected.
|
|
657
|
+
...(Array.isArray(p.items) ? { items: p.items } : {}),
|
|
653
658
|
credits_used: p.credits_used,
|
|
654
659
|
open_url: buildOpenUrl(p.tool, p.gen),
|
|
655
660
|
};
|
package/src/tools/generate.js
CHANGED
|
@@ -978,18 +978,45 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
978
978
|
};
|
|
979
979
|
}
|
|
980
980
|
|
|
981
|
+
const text = JSON.stringify({
|
|
982
|
+
all_done: pending.length === 0,
|
|
983
|
+
completed: results.filter(r => r.state === 'completed').length,
|
|
984
|
+
failed: results.filter(r => r.state === 'failed' || r.state === 'cancelled').length,
|
|
985
|
+
still_processing: pending.map(r => r.generation_id),
|
|
986
|
+
_hint: pending.length === 0 ? doneHint : pendingHint,
|
|
987
|
+
generations: results
|
|
988
|
+
}, null, 2);
|
|
989
|
+
|
|
990
|
+
// Checking SEVERAL ids in one call — the "run N generations in parallel,
|
|
991
|
+
// then check on all of them" pattern — is the one shape hasGeneratedOutput
|
|
992
|
+
// (the host's card-selection check) cannot see into: media here sits one
|
|
993
|
+
// level under EACH ARRAY ITEM's own `result`, and nothing walks arrays
|
|
994
|
+
// looking for it. Without structuredContent the card degrades to a plain
|
|
995
|
+
// status list — ids and state, no thumbnails, even for completed items
|
|
996
|
+
// whose media is sitting right there. uiCompleted gives it the same rich
|
|
997
|
+
// per-item render every generate_* tool already gets; kind:'status' picks
|
|
998
|
+
// renderStatusGrid, the one path here that mixes independent
|
|
999
|
+
// pending/completed/failed items in one grid instead of assuming they all
|
|
1000
|
+
// finished together.
|
|
1001
|
+
if (ui()) {
|
|
1002
|
+
return uiCompleted({
|
|
1003
|
+
tool: 'get_generation_status', kind: 'status', client,
|
|
1004
|
+
model: 'Generations', gen: { generation_id: ids[0] },
|
|
1005
|
+
settings: {},
|
|
1006
|
+
items: results.map(r => {
|
|
1007
|
+
const res = r.result || {};
|
|
1008
|
+
return {
|
|
1009
|
+
id: r.generation_id,
|
|
1010
|
+
state: r.state,
|
|
1011
|
+
title: res.prompt_used || res.prompt || undefined,
|
|
1012
|
+
url: Array.isArray(res.urls) ? res.urls[0] : undefined,
|
|
1013
|
+
};
|
|
1014
|
+
}),
|
|
1015
|
+
}, text);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
981
1018
|
return {
|
|
982
|
-
content: [{
|
|
983
|
-
type: 'text',
|
|
984
|
-
text: JSON.stringify({
|
|
985
|
-
all_done: pending.length === 0,
|
|
986
|
-
completed: results.filter(r => r.state === 'completed').length,
|
|
987
|
-
failed: results.filter(r => r.state === 'failed' || r.state === 'cancelled').length,
|
|
988
|
-
still_processing: pending.map(r => r.generation_id),
|
|
989
|
-
_hint: pending.length === 0 ? doneHint : pendingHint,
|
|
990
|
-
generations: results
|
|
991
|
-
}, null, 2)
|
|
992
|
-
}]
|
|
1019
|
+
content: [{ type: 'text', text }]
|
|
993
1020
|
};
|
|
994
1021
|
}
|
|
995
1022
|
);
|
package/src/tools/models.js
CHANGED
|
@@ -103,7 +103,7 @@ function registerModelTools(server, client, options = {}) {
|
|
|
103
103
|
// ─── list_models ───────────────────────────────────────────
|
|
104
104
|
server.tool(
|
|
105
105
|
'list_models',
|
|
106
|
-
'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to enumerate the catalog with exact identifiers — `format: "json"` + `type` returns the full raw model documents (every constraint field, for programmatic comparison / cap validation before submitting a generation); `format: "json"` alone returns a compact index of EVERY model and its identifier. Default `format: "text"` returns the human-readable summary. NEVER guess a model identifier: call this tool.',
|
|
106
|
+
'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to enumerate the catalog with exact identifiers — `format: "json"` + `type` returns the full raw model documents (every constraint field, for programmatic comparison / cap validation before submitting a generation); `format: "json"` alone returns a compact index of EVERY model and its identifier. Default `format: "text"` returns the human-readable summary. NEVER guess a model identifier: call this tool. ⚠️ COST: for any model whose type is video / firstlast / elements / motion_graphic / cast, `credit` is a PER-SECOND rate, not a per-clip price — multiply by the requested `duration` before quoting cost to the user (e.g. `credit: 9` at `duration: 8` is 72 credits, not 9). This is the universal rule, not a per-model exception. The one carve-out is a model with `flat_credit_by_resolution` set — those charge the flat rate regardless of duration. Every other model type (image, audio, 3D, per-token text) already bills flat per generation as `credit` states.',
|
|
107
107
|
{
|
|
108
108
|
type: z.string().optional().describe('Filter by DB type name: "text_to_img", "image_editing", "text_to_video", "img_to_video", "draw_to_video", "video_to_video", "elements", "firstlastgenerations", "lipsync-image", "lipsync-video", "music_gen", "text_to_speech", "text_to_sound", "stt", "text". Legacy aliases also accepted: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync" (both lipsync types), "three_d" (all 3D types), "first_last_frame", "transcription". Omit for all models.'),
|
|
109
109
|
format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" is the source of truth for identifiers and caps: with `type` it returns the raw model documents from the API (identifier, credit, supported_durations, supported_resolutions, supported_aspect_ratios, max_reference_images, max_visual_dna, max_video_duration, …) for EVERY model of that type; without `type` it returns a compact index of every model in the catalog and its exact identifier. Use it whenever you need an identifier you have not seen listed, or must verify a cap before passing a value that might exceed a model-specific limit.'),
|
|
@@ -334,9 +334,31 @@ function registerModelTools(server, client, options = {}) {
|
|
|
334
334
|
// Text models bill per token — the flat `credit` is not what the user pays,
|
|
335
335
|
// so show the real per-1K rates when the API supplies them. Without this the
|
|
336
336
|
// "cheapest model that fits" rule is unusable for chat.
|
|
337
|
+
//
|
|
338
|
+
// Video-type models are the same problem in a different shape: kolbo-api's
|
|
339
|
+
// credit engine (credManagment.js) treats "charge per second of requested
|
|
340
|
+
// duration" as the UNIVERSAL rule for any type in
|
|
341
|
+
// [video, firstlast, elements, motion_graphic, cast] — not a per-model
|
|
342
|
+
// exception, the default. So `credit: 9` on a model with duration 8 is
|
|
343
|
+
// really 72 credits, and nothing in the catalog said so: an agent quoting
|
|
344
|
+
// cost from the bare `credit` field alone is wrong by exactly the
|
|
345
|
+
// requested duration, every time. `flat_credit_by_resolution` is the one
|
|
346
|
+
// carve-out — those models are charged the flat rate regardless of
|
|
347
|
+
// duration, so they're excluded here the same way credManagment.js
|
|
348
|
+
// excludes them (resolveFlatCredit wins over the multiplier).
|
|
349
|
+
const PER_SECOND_TYPES = ['video', 'firstlast', 'elements', 'motion_graphic', 'cast'];
|
|
350
|
+
const isPerSecondVideo = m => {
|
|
351
|
+
const types = Array.isArray(m.types) ? m.types : (m.type ? [m.type] : []);
|
|
352
|
+
const billedPerSecond = types.some(t => PER_SECOND_TYPES.some(kw => String(t).includes(kw)));
|
|
353
|
+
const hasFlatOverride = m.flat_credit_by_resolution && typeof m.flat_credit_by_resolution === 'object'
|
|
354
|
+
&& Object.keys(m.flat_credit_by_resolution).length > 0;
|
|
355
|
+
return billedPerSecond && !hasFlatOverride;
|
|
356
|
+
};
|
|
337
357
|
const cost = m => (m.output_token_rate != null
|
|
338
358
|
? `${m.input_token_rate ?? '?'}/${m.output_token_rate} credits per 1K tokens (in/out)`
|
|
339
|
-
:
|
|
359
|
+
: isPerSecondVideo(m)
|
|
360
|
+
? `${m.credit} credits/second (× requested duration — NOT a flat per-clip price)`
|
|
361
|
+
: `${m.credit} credits`);
|
|
340
362
|
const formatModel = m =>
|
|
341
363
|
`${m.identifier} (${m.name}) - ${cost(m)}${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}${m.summary ? ` — ${detailed ? m.summary : brief(m.summary)}` : ''}${detailed ? formatSpecs(m) : ''}`;
|
|
342
364
|
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -30,10 +30,12 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
30
30
|
prompt_helper: z.string().optional().describe('Optional description/notes to guide DNA extraction'),
|
|
31
31
|
images: z.array(z.string()).optional().describe('Array of image sources (URLs or absolute local paths). Max 4.'),
|
|
32
32
|
video: z.string().optional().describe('Optional video source (URL or absolute local path)'),
|
|
33
|
-
audio: z.string().optional().describe('Optional audio source (URL or absolute local path)'),
|
|
33
|
+
audio: z.string().optional().describe('Optional audio source (URL or absolute local path) — the character\'s voice, 5-30s of clean speech. Stored on the DNA and used two ways: (1) as REFERENCE AUDIO in video generation — attaching this DNA to an image-to-video generation on a model with audio slots (Seedance 2.x, Wan 3.0) auto-attaches the clip and tells the model it is that character\'s voice; (2) as the source for a real speaking voice, but ONLY when you ask for one — see `voice_source`.'),
|
|
34
|
+
voice_source: z.enum(['none', 'clone', 'assign', 'design']).optional().describe('What to do about a SPEAKING voice. **Pass "none" when the audio is just a reference clip** (the usual case for video work) — the clip is stored and usable as video reference audio, and nothing else happens. "clone" mints an ElevenLabs voice from the uploaded audio, which consumes a voice slot and may EVICT another of the user\'s voices to free one; it also makes the DNA addressable as `dna_<id>` in text-to-speech. "assign" points at an existing voice (pass `assigned_voice_id`). "design" generates a voice from the character\'s look. ⚠️ Omitting this while passing `audio` keeps the legacy behaviour and CLONES — pass "none" explicitly unless the user asked for a voice.'),
|
|
35
|
+
assigned_voice_id: z.string().optional().describe('Voice to attach when voice_source="assign" — a `custom_<id>` from the user\'s clones or a voice_id from `list_voices`.'),
|
|
34
36
|
character_sheet_url: z.string().optional().describe('URL of a reference sheet (from `generate_character_sheet`, any sheet_type) to set as the DNA\'s primary reference. Works for ALL DNA types — character turnaround, product detail sheet, location sheet, or style board — and is the single biggest consistency booster. Omit only when the user declines.')
|
|
35
37
|
},
|
|
36
|
-
async ({ name, dna_type, prompt_helper, images, video, audio, character_sheet_url }) => {
|
|
38
|
+
async ({ name, dna_type, prompt_helper, images, video, audio, voice_source, assigned_voice_id, character_sheet_url }) => {
|
|
37
39
|
if (!name || !name.trim()) {
|
|
38
40
|
throw new Error('name is required');
|
|
39
41
|
}
|
|
@@ -58,6 +60,10 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
58
60
|
if (dna_type) form.append('dnaType', dna_type);
|
|
59
61
|
if (prompt_helper) form.append('promptHelper', prompt_helper);
|
|
60
62
|
if (character_sheet_url) form.append('characterSheetUrl', character_sheet_url);
|
|
63
|
+
// Omitted stays omitted: the server infers 'clone' from a present audio clip, which is the
|
|
64
|
+
// long-standing behaviour older installs depend on. Only an explicit choice is forwarded.
|
|
65
|
+
if (voice_source) form.append('voiceSource', voice_source);
|
|
66
|
+
if (assigned_voice_id) form.append('assignedVoiceId', assigned_voice_id);
|
|
61
67
|
|
|
62
68
|
for (const f of imageFiles) {
|
|
63
69
|
form.append('images', f.buffer, { filename: f.filename, contentType: f.contentType });
|
|
@@ -125,8 +131,11 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
125
131
|
const total = result.total != null ? result.total : (result.count || dnas.length);
|
|
126
132
|
// Full profiles measured 74,310 chars — the embedded analysis/description
|
|
127
133
|
// blobs are large and the model only needs enough to pick an id.
|
|
134
|
+
// `has_voice_reference` rides along because it changes what a DNA DOES in a generation:
|
|
135
|
+
// such a DNA brings its own voice as reference audio on models with audio slots. Without
|
|
136
|
+
// it the model has to fetch each DNA in full just to find out.
|
|
128
137
|
const text = compactList(dnas, {
|
|
129
|
-
fields: ['id', 'name', 'type', 'folder_id', 'tags', 'thumbnail'],
|
|
138
|
+
fields: ['id', 'name', 'type', 'folder_id', 'tags', 'thumbnail', 'has_voice_reference'],
|
|
130
139
|
cap: 60,
|
|
131
140
|
total,
|
|
132
141
|
note: 'Narrow with `search`, `tags`, or `collection`, or pass `page`/`limit` for the rest; get_visual_dna returns one in full.',
|