@kolbo/mcp 1.82.1 → 1.82.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.82.1",
3
+ "version": "1.82.3",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -280,8 +280,12 @@ function renderChips(sc) {
280
280
  var s = sc.settings || {};
281
281
  var kind = displayKind(sc);
282
282
  if (kind) h += chip(iconFor(kind) + ' ' + kind);
283
- if (s.duration) h += chip(ICONS.clock + ' ' + fmtDur(s.duration) + (s.shots > 1 ? ' · ' + s.shots + ' shots' : ''));
284
- else if (s.shots > 1) h += chip(s.shots + ' shots');
283
+ // Shot count when we know it, otherwise just say multishot is on the
284
+ // provider picks the count in that case, and showing nothing made an
285
+ // enabled toggle look like it had not applied.
286
+ var shotLabel = s.shots > 1 ? (s.shots + ' shots') : (s.multi_shot ? 'multishot' : '');
287
+ if (s.duration) h += chip(ICONS.clock + ' ' + fmtDur(s.duration) + (shotLabel ? ' · ' + shotLabel : ''));
288
+ else if (shotLabel) h += chip(shotLabel);
285
289
  if (s.resolution) h += chip(esc(s.resolution));
286
290
  if (s.aspect_ratio) h += chip(esc(s.aspect_ratio));
287
291
  if (s.quality) h += chip(esc(s.quality) + ' quality');
@@ -441,6 +445,47 @@ function iconFor(kind) {
441
445
  /* ---------- generating ---------- */
442
446
  function isBatch(sc) { return !!(sc && sc.generation_ids && sc.generation_ids.length > 1); }
443
447
 
448
+ // Columns for a batch grid. Video caps at 2 per row: a 16:9 cell in a 4-up
449
+ // grid lands around 200px inside a chat card, which is too small to judge a
450
+ // shot and far too small to scrub once <video controls> appears. Images stay
451
+ // 4-up — they read fine as contact-sheet tiles.
452
+ function gridCols(count, kind) {
453
+ return Math.min(count, kind === 'video' ? 2 : 4);
454
+ }
455
+
456
+ // Batch prompts routinely share a long identical preamble (a locked global
457
+ // look, style rules, duration). Clamping from the left then renders EVERY
458
+ // caption as the same string — four cells all reading "Total: 8s [GLOBAL LOOK
459
+ // - LOCKED]…" identify nothing. Drop the shared prefix so the caption shows
460
+ // the part that actually differs. Full text stays in the tooltip.
461
+ function batchCaptions(prompts) {
462
+ var list = (prompts || []).map(function (p) { return String(p == null ? '' : p); });
463
+ if (list.length < 2) return list;
464
+ var first = list[0], n = first.length;
465
+ for (var i = 1; i < list.length && n > 0; i++) {
466
+ var j = 0;
467
+ while (j < n && j < list[i].length && list[i].charAt(j) === first.charAt(j)) j++;
468
+ n = j;
469
+ }
470
+ // Short shared runs are just coincidence ("A ", "The "), not a preamble.
471
+ if (n < 12) return list;
472
+ // Back up to a word boundary so a label never starts mid-word.
473
+ var cut = n;
474
+ while (cut > 0 && !/\s/.test(first.charAt(cut - 1))) cut--;
475
+ if (!cut) cut = n;
476
+ return list.map(function (p) {
477
+ var rest = p.slice(cut).trim();
478
+ // A prompt identical to the prefix has nothing distinct to show — keep it
479
+ // whole rather than rendering an empty chip.
480
+ return rest ? '… ' + rest : p;
481
+ });
482
+ }
483
+ function capAt(sc, i) {
484
+ if (!sc || !sc.prompts || !sc.prompts[i]) return '';
485
+ if (!sc._caps) sc._caps = batchCaptions(sc.prompts);
486
+ return sc._caps[i] || sc.prompts[i];
487
+ }
488
+
444
489
  function renderGenerating(sc) {
445
490
  setPhaseChip('Generating', true);
446
491
  var n = Math.min(sc.count || 1, isBatch(sc) ? 8 : 4);
@@ -449,11 +494,11 @@ function renderGenerating(sc) {
449
494
  var cells = '';
450
495
  for (var i = 0; i < n; i++) {
451
496
  var cap = (sc.prompts && sc.prompts[i])
452
- ? '<span class="k-skel-cap" title="' + esc(sc.prompts[i]) + '">' + esc(sc.prompts[i]) + '</span>' : '';
497
+ ? '<span class="k-skel-cap" title="' + esc(sc.prompts[i]) + '">' + esc(capAt(sc, i)) + '</span>' : '';
453
498
  cells += '<div class="k-skel ' + shape + '" data-cell="' + i + '">' + cap + '</div>';
454
499
  }
455
500
  // Grid class caps at n4 — the auto-fill rule handles any larger batch count.
456
- el('stage').innerHTML = '<div class="k-gen-grid n' + Math.min(n, 4) + '">' + cells + '</div>';
501
+ el('stage').innerHTML = '<div class="k-gen-grid n' + gridCols(n, kind) + '">' + cells + '</div>';
457
502
  renderStopButton(sc);
458
503
  schedulePoll(sc);
459
504
  }
@@ -709,7 +754,7 @@ function handleBatchStatus(sc, st) {
709
754
  }
710
755
  scenes.push({
711
756
  scene_number: i + 1,
712
- title: (sc.prompts && sc.prompts[i]) || '',
757
+ title: capAt(sc, i),
713
758
  image_urls: sc.kind === 'video' ? [] : urls,
714
759
  video_urls: sc.kind === 'video' ? urls : []
715
760
  });
@@ -743,7 +788,7 @@ function fillBatchCell(sc, i, g) {
743
788
  cell.setAttribute('data-done', '1');
744
789
  cell.classList.add('done');
745
790
  var cap = (sc.prompts && sc.prompts[i])
746
- ? '<span class="k-skel-cap" title="' + esc(sc.prompts[i]) + '">' + esc(sc.prompts[i]) + '</span>' : '';
791
+ ? '<span class="k-skel-cap" title="' + esc(sc.prompts[i]) + '">' + esc(capAt(sc, i)) + '</span>' : '';
747
792
  if (sc.prompts && sc.prompts[i]) cell.title = sc.prompts[i];
748
793
  // The controls attribute is not optional here: this cell is a FINISHED result
749
794
  // the user is meant to watch, and without it the tile was a muted poster with
@@ -939,7 +984,7 @@ function renderBatchGrid(sc) {
939
984
  var items = sceneItems(sc);
940
985
  if (!items.length) return renderError('No completed results received');
941
986
  var shape = items[0].type === 'video' ? 'video' : 'square';
942
- el('stage').innerHTML = '<div class="k-gen-grid n' + Math.min(items.length, 4) + '">' +
987
+ el('stage').innerHTML = '<div class="k-gen-grid n' + gridCols(items.length, items[0].type) + '">' +
943
988
  items.map(function (it, i) {
944
989
  return '<div class="k-skel done ' + shape + '" data-focus="' + i + '"' +
945
990
  (it.label ? ' title="' + esc(it.label) + '"' : '') + '>' +
@@ -186,6 +186,9 @@ const videoSettings = (a = {}) => ({
186
186
  // echoed back on start, then to per-shot × shots.
187
187
  duration: a.duration ?? a.reported_duration ?? (a.shot_duration && a.shots ? a.shot_duration * a.shots : undefined),
188
188
  ...(a.shots > 1 ? { shots: a.shots } : {}),
189
+ // Multishot with no explicit count: the provider decides how many, so there
190
+ // is no number to show — but the card still has to say the toggle applied.
191
+ ...(a.multi_shot ? { multi_shot: true } : {}),
189
192
  resolution: a.resolution,
190
193
  aspect_ratio: a.aspect_ratio,
191
194
  ...(a.mode ? { mode: a.mode } : {}),
@@ -1374,7 +1377,14 @@ function registerGenerateTools(server, client, options = {}) {
1374
1377
  settings: videoSettings({
1375
1378
  duration,
1376
1379
  reported_duration: startResponse?.duration ?? startResponse?.result?.duration,
1377
- shots: multi_shot_count ?? (Array.isArray(multi_shots) ? multi_shots.length : undefined),
1380
+ // `multi_shots` is a boolean (z.boolean()); the old
1381
+ // `Array.isArray(multi_shots) ? multi_shots.length : undefined`
1382
+ // branch could never be true, so enabling multishot WITHOUT an
1383
+ // explicit count left shots undefined and the card silently showed
1384
+ // no multishot indicator at all. Carry the flag so the chip can
1385
+ // render even when the shot count is the provider's to decide.
1386
+ shots: multi_shot_count,
1387
+ multi_shot: multi_shots === true || undefined,
1378
1388
  resolution, aspect_ratio, enhance_prompt, visual_dna_ids, preset_id,
1379
1389
  }),
1380
1390
  // Already bucketed and deduped above, so a URL handed to `files`
@@ -1396,7 +1406,14 @@ function registerGenerateTools(server, client, options = {}) {
1396
1406
  settings: videoSettings({
1397
1407
  duration,
1398
1408
  reported_duration: result.result?.duration,
1399
- shots: multi_shot_count ?? (Array.isArray(multi_shots) ? multi_shots.length : undefined),
1409
+ // `multi_shots` is a boolean (z.boolean()); the old
1410
+ // `Array.isArray(multi_shots) ? multi_shots.length : undefined`
1411
+ // branch could never be true, so enabling multishot WITHOUT an
1412
+ // explicit count left shots undefined and the card silently showed
1413
+ // no multishot indicator at all. Carry the flag so the chip can
1414
+ // render even when the shot count is the provider's to decide.
1415
+ shots: multi_shot_count,
1416
+ multi_shot: multi_shots === true || undefined,
1400
1417
  resolution, aspect_ratio, enhance_prompt, visual_dna_ids, preset_id,
1401
1418
  }),
1402
1419
  reference_images: [...urlsOf('image'), ...(keyframes || []).map((keyframe) => keyframe.image_url)],
@@ -102,7 +102,7 @@ function registerVisualDnaTools(server, client, options = {}) {
102
102
  {
103
103
  scope: z.enum(['all', 'personal', 'global', 'organization']).optional().describe('Default: "personal" — the user\'s own DNAs (plus a shared project\'s when project_id is set). "global" = the ~1000 system cast/preset DNAs, for browsing when the user needs a character and has none of their own. "organization" = org-shared. "all" = everything, rarely wanted.'),
104
104
  search: z.string().optional().describe('Search by name, tags, or description (case-insensitive). Matches at WORD STARTS, so "man" finds "Man"/"Manager" but not "woman" or "romantic". Name matches are ranked first.'),
105
- collection: z.string().optional().describe('Filter global presets by collection: cast, influencers, props, locations, styles, glamour, street'),
105
+ collection: z.string().optional().describe('Filter global presets by collection: cast, influencers, props, locations, styles, street'),
106
106
  tags: z.string().optional().describe('Comma-separated tags to filter by (OR logic)'),
107
107
  page: z.number().optional().describe('Page number, 1-indexed. Default: 1. Needed to reach the global cast beyond the first page.'),
108
108
  limit: z.number().optional().describe('Results per page, max 100. Default: 50'),