@kolbo/mcp 1.31.2 → 1.31.4

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.31.2",
3
+ "version": "1.31.4",
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": {
package/src/apps/theme.js CHANGED
@@ -159,6 +159,18 @@ body {
159
159
  .k-media img, .k-media video { display: block; width: 100%; height: 100%; object-fit: cover; }
160
160
  .k-media.selected { outline: 2px solid var(--brand); outline-offset: 1px; }
161
161
 
162
+ /* ---- Per-item hover download button (multi-image grids, CD scenes, viewer) ---- */
163
+ .k-dl {
164
+ position: absolute; top: 8px; right: 8px; z-index: 3;
165
+ width: 30px; height: 30px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18);
166
+ background: rgba(0, 0, 0, 0.62); color: #fff; font-size: 14px; line-height: 1;
167
+ display: inline-flex; align-items: center; justify-content: center;
168
+ cursor: pointer; opacity: 0; transition: opacity 150ms var(--smooth), background 150ms var(--smooth);
169
+ }
170
+ .k-media:hover .k-dl, .k-viewer:hover .k-dl { opacity: 1; }
171
+ .k-dl:hover { background: var(--brand); border-color: var(--brand); }
172
+ .k-viewer { position: relative; }
173
+
162
174
  /* Keep the whole completed card under the host's iframe height cap (~800px):
163
175
  header + prompt + chips + viewer + thumbs + actions + footer must all fit,
164
176
  or claude.ai adds an inner scrollbar. Click the image to expand in-Claude. */
@@ -171,7 +171,7 @@ function renderImages(sc, urls) {
171
171
  selected = Math.min(selected, urls.length - 1);
172
172
  // If the host CSP still blocks the image, degrade to open-in-browser rows
173
173
  // instead of a broken empty viewer.
174
- var viewer = '<div class="k-viewer"><img id="main-img" src="' + esc(urls[selected]) + '" alt="" onerror="window.__imgFail && window.__imgFail()"></div>';
174
+ var viewer = '<div class="k-viewer"><img id="main-img" src="' + esc(urls[selected]) + '" alt="" onerror="window.__imgFail && window.__imgFail()">' + dlBtnHTML(urls[selected]) + '</div>';
175
175
  window.__imgFail = function () { renderLinks(urls); window.kolbo.notifySize(); };
176
176
  // Click → expand into an in-Claude fullscreen viewer (all actions stay
177
177
  // available); click again (or Exit) collapses back. Hosts that refuse
@@ -187,10 +187,14 @@ function renderImages(sc, urls) {
187
187
  }).join('') + '</div>';
188
188
  }
189
189
  el('stage').innerHTML = viewer + thumbs;
190
+ wireDlButtons(el('stage'));
190
191
  Array.prototype.forEach.call(el('stage').querySelectorAll('.k-thumb'), function (t) {
191
192
  t.onclick = function () {
192
193
  selected = +t.getAttribute('data-i');
193
194
  el('main-img').src = state.urls[selected];
195
+ // Keep the viewer's hover download pointing at the newly selected image.
196
+ var dl = el('stage').querySelector('.k-viewer .k-dl');
197
+ if (dl) dl.setAttribute('data-dl', state.urls[selected]);
194
198
  Array.prototype.forEach.call(el('stage').querySelectorAll('.k-thumb'), function (x) { x.classList.remove('active'); });
195
199
  t.classList.add('active');
196
200
  };
@@ -199,7 +203,9 @@ function renderImages(sc, urls) {
199
203
 
200
204
  function renderVideo(sc, urls) {
201
205
  el('stage').innerHTML = '<div class="k-viewer"><video id="main-video" src="' + esc(urls[0]) + '"' +
202
- (sc.thumbnail_url ? ' poster="' + esc(sc.thumbnail_url) + '"' : '') + ' controls playsinline></video></div>';
206
+ (sc.thumbnail_url ? ' poster="' + esc(sc.thumbnail_url) + '"' : '') + ' controls playsinline></video>' +
207
+ dlBtnHTML(urls[0]) + '</div>';
208
+ wireDlButtons(el('stage'));
203
209
  }
204
210
 
205
211
  function renderAudio(sc, urls) {
@@ -238,20 +244,62 @@ function renderLinks(urls) {
238
244
  });
239
245
  }
240
246
 
247
+ // Small hover download button attached to a media cell (per-item downloads —
248
+ // batch grids and CD scenes have no single "current" url for the action row).
249
+ function dlBtnHTML(u) {
250
+ return '<button class="k-dl" data-dl="' + esc(u) + '" title="Download" aria-label="Download">⬇</button>';
251
+ }
252
+ function wireDlButtons(root) {
253
+ Array.prototype.forEach.call((root || document).querySelectorAll('.k-dl[data-dl]'), function (b) {
254
+ b.onclick = function (e) {
255
+ e.stopPropagation();
256
+ window.kolbo.openLink(downloadUrl(b.getAttribute('data-dl')));
257
+ };
258
+ });
259
+ }
260
+
241
261
  function renderScenes(sc) {
242
262
  el('stage').innerHTML = sc.scenes.map(function (scene) {
243
263
  var media = (scene.video_urls || []).map(function (u) {
244
- return '<div class="k-media"><video src="' + esc(u) + '" controls playsinline></video></div>';
264
+ return '<div class="k-media"><video src="' + esc(u) + '" controls playsinline></video>' + dlBtnHTML(u) + '</div>';
245
265
  }).join('') + (scene.image_urls || []).map(function (u) {
246
- return '<div class="k-media"><img src="' + esc(u) + '" alt=""></div>';
266
+ return '<div class="k-media" data-focus="' + esc(u) + '"><img src="' + esc(u) + '" alt="">' + dlBtnHTML(u) + '</div>';
247
267
  }).join('');
248
268
  return '<div style="margin-bottom:14px"><div style="font-size:12px;font-weight:600;color:var(--text-muted);margin-bottom:8px">Scene ' +
249
269
  esc(scene.scene_number) + (scene.title ? ' — ' + esc(scene.title) : '') + '</div>' +
250
270
  '<div class="k-gen-grid n2">' + media + '</div></div>';
251
271
  }).join('');
272
+ wireDlButtons(el('stage'));
273
+ // Click a scene image → fullscreen focus on THAT image (videos keep their
274
+ // native controls; clicking them shouldn't hijack playback).
275
+ Array.prototype.forEach.call(el('stage').querySelectorAll('.k-media[data-focus]'), function (m) {
276
+ m.style.cursor = 'zoom-in';
277
+ m.onclick = function () { focusMedia(m.getAttribute('data-focus')); };
278
+ });
252
279
  renderActions(sc);
253
280
  }
254
281
 
282
+ // Fullscreen a single item out of a multi-item grid (Creative Director
283
+ // scenes). Exit restores the grid.
284
+ function focusMedia(url) {
285
+ window.kolbo.requestDisplayMode('fullscreen').then(function (res) {
286
+ if (!(res && res.mode === 'fullscreen')) return window.kolbo.openLink(url);
287
+ isFullscreen = true;
288
+ el('stage').innerHTML = '<div class="k-viewer"><img id="focus-img" src="' + esc(url) + '" alt="" style="cursor:zoom-out">' + dlBtnHTML(url) + '</div>';
289
+ wireDlButtons(el('stage'));
290
+ el('focus-img').onclick = exitFocus;
291
+ applyFullscreen(true, exitFocus);
292
+ window.kolbo.notifySize();
293
+ }).catch(function () { window.kolbo.openLink(url); });
294
+ }
295
+ function exitFocus() {
296
+ window.kolbo.requestDisplayMode('inline').catch(function () {});
297
+ isFullscreen = false;
298
+ applyFullscreen(false);
299
+ renderScenes(state); // restore the grid
300
+ window.kolbo.notifySize();
301
+ }
302
+
255
303
  function renderError(msg) {
256
304
  clearTimeout(pollTimer);
257
305
 
@@ -281,14 +329,14 @@ function toggleFullscreen() {
281
329
  if (!isFullscreen) window.kolbo.openLink(state.urls && state.urls[selected]);
282
330
  });
283
331
  }
284
- function applyFullscreen(on) {
332
+ function applyFullscreen(on, exitHandler) {
285
333
  document.documentElement.classList.toggle('k-fullscreen', on);
286
334
  var c = el('phase-chip');
287
335
  if (on) {
288
336
  c.style.display = '';
289
337
  c.innerHTML = '✕ ' + esc('Exit');
290
338
  c.style.cursor = 'pointer';
291
- c.onclick = toggleFullscreen;
339
+ c.onclick = exitHandler || toggleFullscreen;
292
340
  } else {
293
341
  c.style.display = 'none';
294
342
  c.onclick = null;
@@ -311,6 +359,7 @@ function currentUrl() {
311
359
 
312
360
  function renderActions(sc) {
313
361
  var a = [];
362
+ var hasSingleUrl = !!(sc.urls && sc.urls.length);
314
363
  if (sc.kind === 'image') {
315
364
  a.push('<button class="k-btn primary" id="btn-animate">🎬 Animate</button>');
316
365
  a.push('<button class="k-btn" id="btn-edit">✏️ Edit</button>');
@@ -318,7 +367,9 @@ function renderActions(sc) {
318
367
  if (sc.kind === 'video') {
319
368
  a.push('<button class="k-btn primary" id="btn-download">⬇ Download</button>');
320
369
  a.push('<button class="k-btn" id="btn-analyze">📊 Analyze</button>');
321
- } else {
370
+ } else if (hasSingleUrl) {
371
+ // Scenes (Creative Director) have no single "current" url — per-item hover
372
+ // download buttons cover them instead.
322
373
  a.push('<button class="k-btn" id="btn-download">⬇ Download</button>');
323
374
  }
324
375
  a.push('<button class="k-btn" id="btn-recreate">↻ Recreate</button>');
@@ -118,7 +118,12 @@ function useItem(i) {
118
118
 
119
119
  window.kolbo.onToolResult(function (result) {
120
120
  var sc = result.structuredContent || structured(result);
121
- if (sc) boot(sc);
121
+ if (sc && sc.items) return boot(sc);
122
+ // No grid data (empty result set, error, or timeout path returned plain
123
+ // text) — collapse instead of showing a dead "Loading…" card forever.
124
+ var card = document.querySelector('.k-card');
125
+ if (card) card.style.display = 'none';
126
+ window.kolbo.notifySize();
122
127
  });
123
128
  `;
124
129
 
@@ -97,7 +97,23 @@ function poll() {
97
97
 
98
98
  window.kolbo.onToolResult(function (result) {
99
99
  var sc = result.structuredContent || structured(result);
100
- if (sc) boot(sc);
100
+ // Only boot on a real widget contract (phase present). A text-JSON
101
+ // fallback like { status: 'submitted' } has no phase and would render a
102
+ // bogus "(empty transcript)" completed view.
103
+ if (sc && (sc.phase || sc.widget === 'transcript')) return boot(sc);
104
+ var txt = '';
105
+ try { txt = (result.content || []).filter(function (c) { return c.type === 'text'; }).map(function (c) { return c.text; }).join(' '); } catch (e) {}
106
+ if (result.isError || /error|failed/i.test(txt)) {
107
+ el('phase-chip').style.display = 'none';
108
+ el('stage').classList.remove('k-empty');
109
+ el('stage').innerHTML = '<div class="k-error">⚠ ' + esc((txt || 'Transcription failed').slice(0, 300)) + '</div>';
110
+ window.kolbo.notifySize();
111
+ return;
112
+ }
113
+ // No usable data — collapse instead of a dead "Loading…" card.
114
+ var card = document.querySelector('.k-card');
115
+ if (card) card.style.display = 'none';
116
+ window.kolbo.notifySize();
101
117
  });
102
118
  `;
103
119
 
@@ -184,9 +184,23 @@ async function resolveToBuffer(source, kind, opts = {}) {
184
184
  }
185
185
 
186
186
  if (!path.isAbsolute(source)) {
187
- throw new Error(`Local file paths must be absolute: ${source}`);
187
+ throw new Error(
188
+ `Local file paths must be absolute: ${source}. ` +
189
+ `If you are using Kolbo over a remote connector (e.g. claude.ai), local files are not reachable — ` +
190
+ `pass a public https:// URL instead (upload the file somewhere first, or use a URL from list_media).`
191
+ );
192
+ }
193
+ let stat;
194
+ try {
195
+ stat = fs.statSync(source);
196
+ } catch (err) {
197
+ throw new Error(
198
+ `Local file not found or unreadable: ${source}. ` +
199
+ `If you are using Kolbo over a remote connector (e.g. claude.ai), local file paths are not reachable — ` +
200
+ `pass a public https:// URL instead (upload the file somewhere first, or use a URL from list_media).` +
201
+ (err && err.code ? ` [${err.code}]` : '')
202
+ );
188
203
  }
189
- const stat = fs.statSync(source);
190
204
  if (stat.size > maxBytes) {
191
205
  throw new Error(`File ${source} (${stat.size} bytes) exceeds ${maxBytes}-byte limit`);
192
206
  }