@kolbo/mcp 1.84.0 → 1.84.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/apps/index.js +11 -0
- package/src/apps/widgets/mediaGrid.js +48 -11
- package/src/tools/_shared.js +12 -0
- package/src/tools/generate.js +8 -0
- package/src/tools/media.js +11 -1
- package/src/tools/visual_dna.js +10 -6
package/package.json
CHANGED
package/src/apps/index.js
CHANGED
|
@@ -88,6 +88,17 @@ const WIDGET_CSP = {
|
|
|
88
88
|
'https://assets.sketchfab.com',
|
|
89
89
|
'https://sketchfab-prod-media.s3.amazonaws.com',
|
|
90
90
|
|
|
91
|
+
// Voice PREVIEW audio hosts. list_voices ships every voice with a
|
|
92
|
+
// preview_url and the card renders a real <audio> for it, but 150 of the
|
|
93
|
+
// 864 production voices store that preview on a provider host rather than a
|
|
94
|
+
// Kolbo bucket: 138 google voices on storage.googleapis.com and 12 on
|
|
95
|
+
// api.us.elevenlabs.io. media-src blocked both, so every google voice — the
|
|
96
|
+
// entire Hebrew set — rendered a player stuck at 0:00 / 0:00 with no error
|
|
97
|
+
// anywhere. The Spaces-hosted previews come free via HOST_MAP above; these
|
|
98
|
+
// two do not, because they are not ours.
|
|
99
|
+
'https://storage.googleapis.com',
|
|
100
|
+
'https://api.us.elevenlabs.io',
|
|
101
|
+
|
|
91
102
|
// Default SYNCI catalog project. Any production override must be reviewed
|
|
92
103
|
// and added here as an exact hostname before deployment.
|
|
93
104
|
'https://gfbpxdkripkbbrcvoyeh.supabase.co',
|
|
@@ -48,17 +48,17 @@ function boot(sc) {
|
|
|
48
48
|
var audioItems = sc.items.filter(function (i) { return i.media_type === 'audio'; });
|
|
49
49
|
var visualItems = sc.items.filter(function (i) { return i.media_type !== 'audio'; });
|
|
50
50
|
var h = '';
|
|
51
|
+
// Render everything currently in state — each PAGE is already capped
|
|
52
|
+
// server-side (GRID_CAP), so this grows one page per Load more instead of
|
|
53
|
+
// being pinned forever. The old hard slices (24 visual / 12 audio) meant an
|
|
54
|
+
// appended page could never actually appear, and the count below tallied all
|
|
55
|
+
// items while only 36 were drawn, so the button also lied about the total.
|
|
51
56
|
if (visualItems.length) {
|
|
52
|
-
h += '<div class="k-grid">' + visualItems.
|
|
57
|
+
h += '<div class="k-grid">' + visualItems.map(cellHTML).join('') + '</div>';
|
|
53
58
|
}
|
|
54
59
|
if (audioItems.length) {
|
|
55
|
-
h += audioItems.
|
|
60
|
+
h += audioItems.map(audioRowHTML).join('');
|
|
56
61
|
}
|
|
57
|
-
// Only a fixed page ever renders here (24 visual + 12 audio) — with a library
|
|
58
|
-
// in the thousands there was no way to reach the rest short of asking in
|
|
59
|
-
// chat. A visible "Load more" turns that into one click; it sends a message
|
|
60
|
-
// rather than calling the tool directly, since the widget has no host API
|
|
61
|
-
// to invoke a tool itself — same mechanism every other "Use" action here uses.
|
|
62
62
|
var shown = visualItems.length + audioItems.length;
|
|
63
63
|
if (sc.total != null && sc.total > shown) {
|
|
64
64
|
h += '<button class="k-btn" id="load-more" style="width:100%;margin-top:10px">Load more (' +
|
|
@@ -145,13 +145,50 @@ function wire() {
|
|
|
145
145
|
});
|
|
146
146
|
var loadMore = el('load-more');
|
|
147
147
|
if (loadMore) {
|
|
148
|
-
loadMore.onclick = function () {
|
|
149
|
-
window.kolbo.sendMessage('Show me the next page of this same media search (already saw ' +
|
|
150
|
-
(state.items.length) + ' of ' + state.total + ' results).');
|
|
151
|
-
};
|
|
148
|
+
loadMore.onclick = function () { fetchNextPage(loadMore); };
|
|
152
149
|
}
|
|
153
150
|
}
|
|
154
151
|
|
|
152
|
+
// Fetch the next page IN the widget and append it.
|
|
153
|
+
//
|
|
154
|
+
// This used to sendMessage() a request for "the next page of this same media
|
|
155
|
+
// search", on the stated belief that a widget has no way to invoke a tool. It
|
|
156
|
+
// does — window.kolbo.callTool, the same bridge call every generation card
|
|
157
|
+
// polls status with. And the message could not have worked anyway: the payload
|
|
158
|
+
// carried no page number and none of the filters, so the model had nothing to
|
|
159
|
+
// reconstruct the query from and would re-run page 1 or something else. The
|
|
160
|
+
// button appeared to do nothing.
|
|
161
|
+
function fetchNextPage(btn) {
|
|
162
|
+
if (!state || !state.page_tool || btn.disabled) return;
|
|
163
|
+
var next = (state.page || 1) + 1;
|
|
164
|
+
btn.disabled = true;
|
|
165
|
+
var label = btn.textContent;
|
|
166
|
+
btn.innerHTML = '<span class="k-spin"></span> Loading';
|
|
167
|
+
var args = {};
|
|
168
|
+
var q = state.query || {};
|
|
169
|
+
for (var k in q) { if (q[k] !== undefined && q[k] !== null && q[k] !== '') args[k] = q[k]; }
|
|
170
|
+
args.page = next;
|
|
171
|
+
if (state.page_size) args.page_size = state.page_size;
|
|
172
|
+
|
|
173
|
+
window.kolbo.callTool(state.page_tool, args).then(function (res) {
|
|
174
|
+
var sc = structured(res);
|
|
175
|
+
var more = (sc && sc.items) || [];
|
|
176
|
+
if (!more.length) {
|
|
177
|
+
// Nothing came back: say so rather than restoring a button that still
|
|
178
|
+
// looks like it has pages behind it.
|
|
179
|
+
btn.textContent = 'No more results';
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
state.items = (state.items || []).concat(more);
|
|
183
|
+
state.page = (sc && sc.page) || next;
|
|
184
|
+
if (sc && sc.total != null) state.total = sc.total;
|
|
185
|
+
boot(state); // re-renders the grid + a fresh Load more button
|
|
186
|
+
}).catch(function () {
|
|
187
|
+
btn.disabled = false;
|
|
188
|
+
btn.textContent = label;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
155
192
|
function useItem(i) {
|
|
156
193
|
var item = state.items[i];
|
|
157
194
|
if (!item || !item.id) return;
|
package/src/tools/_shared.js
CHANGED
|
@@ -851,6 +851,18 @@ async function uiCompleted(p, textPayload, extraContent) {
|
|
|
851
851
|
// above which assume everything finished together. Only set when the
|
|
852
852
|
// caller actually has this shape; every existing caller is unaffected.
|
|
853
853
|
...(Array.isArray(p.items) ? { items: p.items } : {}),
|
|
854
|
+
// Transcription payload. get_generation_status is the ONLY way the live
|
|
855
|
+
// transcript widget learns its result, and it reads text/srt_url/txt_url off
|
|
856
|
+
// this object — but uiCompleted is shaped for the generation card and
|
|
857
|
+
// dropped every one, so a finished transcription rendered "(empty
|
|
858
|
+
// transcript)" with no SRT/TXT buttons while the text sat in the status
|
|
859
|
+
// response. structuredContent SHADOWS the text block on widget hosts, so
|
|
860
|
+
// omitting a field here is the same as deleting it.
|
|
861
|
+
...(typeof p.text === 'string' ? { text: p.text } : {}),
|
|
862
|
+
...(p.srt_url ? { srt_url: p.srt_url } : {}),
|
|
863
|
+
...(p.word_by_word_srt_url ? { word_by_word_srt_url: p.word_by_word_srt_url } : {}),
|
|
864
|
+
...(p.txt_url ? { txt_url: p.txt_url } : {}),
|
|
865
|
+
...(p.audio_url ? { audio_url: p.audio_url } : {}),
|
|
854
866
|
// The voice, by name and portrait. uiGenerating has carried this since the
|
|
855
867
|
// chips were introduced; uiCompleted never did, so it silently dropped a
|
|
856
868
|
// resolved voice its caller had already looked up — every FINISHED speech
|
package/src/tools/generate.js
CHANGED
|
@@ -1118,6 +1118,14 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1118
1118
|
state: single.state,
|
|
1119
1119
|
urls: done ? urls : undefined,
|
|
1120
1120
|
thumbnail_url: res.thumbnail_url,
|
|
1121
|
+
// Transcription results ride the same status tool as media
|
|
1122
|
+
// generations; without these the transcript widget merges a payload
|
|
1123
|
+
// with no transcript in it and renders "(empty transcript)".
|
|
1124
|
+
text: typeof res.text === 'string' ? res.text : undefined,
|
|
1125
|
+
srt_url: res.srt_url || undefined,
|
|
1126
|
+
word_by_word_srt_url: res.word_by_word_srt_url || undefined,
|
|
1127
|
+
txt_url: res.txt_url || undefined,
|
|
1128
|
+
audio_url: res.audio_url || undefined,
|
|
1121
1129
|
// The refs the server actually conditioned on (reference_details) —
|
|
1122
1130
|
// the live card merges this payload over its submit-time state, so
|
|
1123
1131
|
// the finished card shows every reference, including server-side
|
package/src/tools/media.js
CHANGED
|
@@ -303,7 +303,17 @@ function registerMediaTools(server, client, options = {}) {
|
|
|
303
303
|
title: 'Media Library',
|
|
304
304
|
items,
|
|
305
305
|
total: totalItems != null ? totalItems : media.length,
|
|
306
|
-
shown: Math.min(media.length, GRID_CAP)
|
|
306
|
+
shown: Math.min(media.length, GRID_CAP),
|
|
307
|
+
// Everything "Load more" needs to fetch page N+1 ITSELF. The button used
|
|
308
|
+
// to send a chat message asking the model to run the next page, on the
|
|
309
|
+
// belief that a widget cannot invoke a tool — it can
|
|
310
|
+
// (window.kolbo.callTool, the same call every generation card polls
|
|
311
|
+
// with). Worse, the payload carried no page and no filters, so the model
|
|
312
|
+
// could not reconstruct the query either and typically re-ran page 1.
|
|
313
|
+
page_tool: 'list_media',
|
|
314
|
+
page: page || 1,
|
|
315
|
+
page_size: page_size || 50,
|
|
316
|
+
query: { project_id, folder_id, type, category, source_type, sort, search }
|
|
307
317
|
});
|
|
308
318
|
}
|
|
309
319
|
);
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -28,6 +28,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
28
28
|
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).'),
|
|
29
29
|
dna_type: z.string().optional().describe('Type: "character", "style", "product", "scene", "environment". Default: "character"'),
|
|
30
30
|
prompt_helper: z.string().optional().describe('Optional description/notes to guide DNA extraction'),
|
|
31
|
+
description: z.string().optional().describe('Alias for `prompt_helper`. Accepted because every tool here REPORTS this field as `description`, so callers round-trip that name back in; without the alias zod stripped it and the notes were silently lost. Ignored when `prompt_helper` is also given.'),
|
|
31
32
|
images: z.array(z.string()).optional().describe('Array of image sources (URLs or absolute local paths). Max 4. All of them can reach the model on later gens — same subject, same vibe only; no extra heroes on character DNAs, no main characters on environment DNAs.'),
|
|
32
33
|
video: z.string().optional().describe('Optional video source (URL or absolute local path)'),
|
|
33
34
|
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`.'),
|
|
@@ -35,7 +36,8 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
35
36
|
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`.'),
|
|
36
37
|
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.')
|
|
37
38
|
},
|
|
38
|
-
async ({ name, dna_type, prompt_helper, images, video, audio, voice_source, assigned_voice_id, character_sheet_url }) => {
|
|
39
|
+
async ({ name, dna_type, prompt_helper, description, images, video, audio, voice_source, assigned_voice_id, character_sheet_url }) => {
|
|
40
|
+
const helper = prompt_helper !== undefined ? prompt_helper : description;
|
|
39
41
|
if (!name || !name.trim()) {
|
|
40
42
|
throw new Error('name is required');
|
|
41
43
|
}
|
|
@@ -58,7 +60,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
58
60
|
const form = new FormData();
|
|
59
61
|
form.append('name', name);
|
|
60
62
|
if (dna_type) form.append('dnaType', dna_type);
|
|
61
|
-
if (
|
|
63
|
+
if (helper) form.append('promptHelper', helper);
|
|
62
64
|
if (character_sheet_url) form.append('characterSheetUrl', character_sheet_url);
|
|
63
65
|
// Omitted stays omitted: the server infers 'clone' from a present audio clip, which is the
|
|
64
66
|
// long-standing behaviour older installs depend on. Only an explicit choice is forwarded.
|
|
@@ -193,6 +195,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
193
195
|
name: z.string().optional().describe('New name. Same no-space single-token rule as create_visual_dna — @Name binding stops at the first space.'),
|
|
194
196
|
dna_type: z.string().optional().describe('Type: "character", "style", "product", "scene", "environment". Changing type re-analyzes the profile.'),
|
|
195
197
|
prompt_helper: z.string().optional().describe('New description / intent notes. Replaces the old ones and re-synthesizes the DNA analysis when the text actually changes. Pass "" to clear.'),
|
|
198
|
+
description: z.string().optional().describe('Alias for `prompt_helper`. Accepted because every tool here REPORTS this field as `description`, so callers round-trip that name back in; without the alias zod stripped it and the edit silently did nothing. Ignored when `prompt_helper` is also given.'),
|
|
196
199
|
images: z.array(z.string()).optional().describe('Full replacement still set (URLs or absolute local paths). Max 4. Omit to keep current stills. Same purity rules as create: one identity, one vibe.'),
|
|
197
200
|
video: z.string().optional().describe('Replacement video source (URL or absolute local path).'),
|
|
198
201
|
audio: z.string().optional().describe('Replacement audio source (URL or absolute local path).'),
|
|
@@ -208,14 +211,15 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
208
211
|
specific_age: z.number().optional().describe('Character attribute (character DNAs).')
|
|
209
212
|
},
|
|
210
213
|
async ({
|
|
211
|
-
visual_dna_id, name, dna_type, prompt_helper, images, video, audio,
|
|
214
|
+
visual_dna_id, name, dna_type, prompt_helper, description, images, video, audio,
|
|
212
215
|
character_sheet_url, remove_character_sheet,
|
|
213
216
|
gender, ethnicity, body_type, hair_color, eye_color, skin_tone, age_range, specific_age
|
|
214
217
|
}) => {
|
|
218
|
+
const helper = prompt_helper !== undefined ? prompt_helper : description;
|
|
215
219
|
const imageList = Array.isArray(images) ? images.filter(Boolean) : [];
|
|
216
220
|
if (imageList.length > 4) throw new Error('Maximum 4 images allowed');
|
|
217
221
|
const hasMedia = imageList.length > 0 || !!video || !!audio;
|
|
218
|
-
const hasMeta = name !== undefined || dna_type !== undefined ||
|
|
222
|
+
const hasMeta = name !== undefined || dna_type !== undefined || helper !== undefined
|
|
219
223
|
|| character_sheet_url !== undefined || remove_character_sheet !== undefined
|
|
220
224
|
|| gender !== undefined || ethnicity !== undefined || body_type !== undefined
|
|
221
225
|
|| hair_color !== undefined || eye_color !== undefined || skin_tone !== undefined
|
|
@@ -240,7 +244,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
240
244
|
const body = { ...attrs };
|
|
241
245
|
if (name !== undefined) body.name = name;
|
|
242
246
|
if (dna_type !== undefined) body.dna_type = dna_type;
|
|
243
|
-
if (
|
|
247
|
+
if (helper !== undefined) body.prompt_helper = helper;
|
|
244
248
|
if (character_sheet_url !== undefined) body.character_sheet_url = character_sheet_url;
|
|
245
249
|
if (remove_character_sheet !== undefined) body.remove_character_sheet = remove_character_sheet;
|
|
246
250
|
const result = await client.put(path, body);
|
|
@@ -264,7 +268,7 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
264
268
|
const form = new FormData();
|
|
265
269
|
if (name !== undefined) form.append('name', name);
|
|
266
270
|
if (dna_type) form.append('dnaType', dna_type);
|
|
267
|
-
if (
|
|
271
|
+
if (helper !== undefined) form.append('promptHelper', helper);
|
|
268
272
|
if (character_sheet_url) form.append('characterSheetUrl', character_sheet_url);
|
|
269
273
|
if (remove_character_sheet !== undefined) {
|
|
270
274
|
form.append('removeCharacterSheet', remove_character_sheet ? 'true' : 'false');
|