@kolbo/mcp 1.52.1 → 1.53.1
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 +17 -0
- package/src/apps/theme.js +3 -2
- package/src/apps/widgets/catalog.js +16 -5
- package/src/apps/widgets/list.js +101 -0
- package/src/apps/widgets/mediaGrid.js +26 -15
- package/src/tools/agents.js +25 -5
- package/src/tools/docs.js +21 -2
- package/src/tools/media.js +19 -9
- package/src/tools/models.js +12 -6
- package/src/tools/projects.js +60 -13
- package/src/tools/visual_dna.js +18 -1
package/package.json
CHANGED
package/src/apps/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const { mediaGridWidgetHtml } = require('./widgets/mediaGrid');
|
|
|
21
21
|
const { catalogWidgetHtml } = require('./widgets/catalog');
|
|
22
22
|
const { transcriptWidgetHtml } = require('./widgets/transcript');
|
|
23
23
|
const { uploadWidgetHtml } = require('./widgets/upload');
|
|
24
|
+
const { listWidgetHtml } = require('./widgets/list');
|
|
24
25
|
|
|
25
26
|
const UI = {
|
|
26
27
|
generation: 'ui://kolbo/generation.html',
|
|
@@ -28,6 +29,7 @@ const UI = {
|
|
|
28
29
|
catalog: 'ui://kolbo/catalog.html',
|
|
29
30
|
transcript: 'ui://kolbo/transcript.html',
|
|
30
31
|
upload: 'ui://kolbo/upload.html',
|
|
32
|
+
list: 'ui://kolbo/list.html',
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const WIDGET_BUILDERS = {
|
|
@@ -36,6 +38,7 @@ const WIDGET_BUILDERS = {
|
|
|
36
38
|
[UI.catalog]: catalogWidgetHtml,
|
|
37
39
|
[UI.transcript]: transcriptWidgetHtml,
|
|
38
40
|
[UI.upload]: uploadWidgetHtml,
|
|
41
|
+
[UI.list]: listWidgetHtml,
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
// Widgets are pure functions of source — build once per process.
|
|
@@ -93,6 +96,7 @@ function registerApps(server) {
|
|
|
93
96
|
[UI.catalog, 'Kolbo Model Catalog Widget'],
|
|
94
97
|
[UI.transcript, 'Kolbo Transcription Widget'],
|
|
95
98
|
[UI.upload, 'Kolbo Upload Widget'],
|
|
99
|
+
[UI.list, 'Kolbo List Widget'],
|
|
96
100
|
]) {
|
|
97
101
|
registerAppResource(
|
|
98
102
|
server, name, uri,
|
|
@@ -292,8 +296,21 @@ const TOOL_WIDGETS = {
|
|
|
292
296
|
list_visual_dnas: UI.mediaGrid,
|
|
293
297
|
list_moodboards: UI.mediaGrid,
|
|
294
298
|
shorts_analyze: UI.mediaGrid,
|
|
299
|
+
// NOTE: list_color_palettes' handler has always called uiResult(UI.mediaGrid, ...)
|
|
300
|
+
// (see color_palettes.js) but was missing here — hosts that prepare the widget
|
|
301
|
+
// iframe from the tool DECLARATION (claude.ai reads tools/list, not the result)
|
|
302
|
+
// never saw it as widget-carrying. Result-level _meta alone isn't enough.
|
|
303
|
+
list_color_palettes: UI.mediaGrid,
|
|
295
304
|
// upload widget
|
|
296
305
|
media_upload_widget: UI.upload,
|
|
306
|
+
// generic list widget — flat record lists with no natural thumbnail
|
|
307
|
+
list_projects: UI.list,
|
|
308
|
+
list_sessions: UI.list,
|
|
309
|
+
list_project_context: UI.list,
|
|
310
|
+
list_agents: UI.list,
|
|
311
|
+
list_docs: UI.list,
|
|
312
|
+
list_media_folders: UI.list,
|
|
313
|
+
list_visual_dna_folders: UI.list,
|
|
297
314
|
};
|
|
298
315
|
|
|
299
316
|
function attachToolWidgetMeta(server) {
|
package/src/apps/theme.js
CHANGED
|
@@ -242,8 +242,9 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
|
|
|
242
242
|
.k-cell { position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--border);
|
|
243
243
|
background: var(--surface); cursor: pointer; transition: transform 250ms var(--spring); }
|
|
244
244
|
.k-cell:hover { transform: translateY(-2px) scale(1.01); }
|
|
245
|
-
.k-cell .k-cell-media { aspect-ratio: 1; background: #000; }
|
|
245
|
+
.k-cell .k-cell-media { position: relative; aspect-ratio: 1; background: #000; }
|
|
246
246
|
.k-cell .k-cell-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
247
|
+
.k-cell-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
|
247
248
|
.k-cell .k-cell-label { padding: 6px 8px; font-size: 11px; color: var(--text-muted);
|
|
248
249
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
249
250
|
.k-cell .k-cell-sub { padding: 0 8px 7px; font-size: 10px; color: var(--text-faint);
|
|
@@ -274,7 +275,7 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
|
|
|
274
275
|
@media (max-width: 520px) {
|
|
275
276
|
.k-generated-audio { grid-template-columns: 36px minmax(0, 1fr); }
|
|
276
277
|
.k-generated-audio .k-audio-art { width: 36px; height: 36px; }
|
|
277
|
-
.k-generated-audio .k-
|
|
278
|
+
.k-generated-audio .k-btn { grid-column: 2; justify-self: start; }
|
|
278
279
|
.k-generated-audio .k-audio-player { grid-column: 1 / -1; }
|
|
279
280
|
}
|
|
280
281
|
.k-play {
|
|
@@ -31,9 +31,20 @@ el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openL
|
|
|
31
31
|
var state = null;
|
|
32
32
|
|
|
33
33
|
function boot(sc) {
|
|
34
|
-
if (!sc || !sc.groups) return;
|
|
34
|
+
if (!sc || !sc.groups || !sc.groups.length) return false;
|
|
35
35
|
state = sc;
|
|
36
36
|
el('title').textContent = sc.title || 'AI Models';
|
|
37
|
+
// Internal lookup (Claude checking caps mid-task): a full catalog card would
|
|
38
|
+
// be chat spam, an empty card would look broken. One expandable row instead.
|
|
39
|
+
if (sc.compact) {
|
|
40
|
+
el('stage').innerHTML = '<div class="k-audio-row" id="expand" style="cursor:pointer;padding:8px">' +
|
|
41
|
+
'<div class="k-audio-meta"><div class="k-audio-title" style="font-size:12px">Kolbo AI models</div>' +
|
|
42
|
+
'<div class="k-audio-sub" style="font-size:10.5px">Tap to browse what you can generate</div></div>' +
|
|
43
|
+
'<span class="k-chip" style="flex:none;padding:2px 7px;font-size:10px">Browse</span></div>';
|
|
44
|
+
el('expand').onclick = function () { sc.compact = false; boot(sc); };
|
|
45
|
+
window.kolbo.notifySize();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
37
48
|
var shown = sc.groups.reduce(function (n, g) { return n + g.models.length; }, 0);
|
|
38
49
|
el('count-chip').style.display = '';
|
|
39
50
|
el('count-chip').textContent = sc.total_available && sc.total_available > shown
|
|
@@ -60,13 +71,13 @@ function boot(sc) {
|
|
|
60
71
|
};
|
|
61
72
|
});
|
|
62
73
|
window.kolbo.notifySize();
|
|
74
|
+
return true;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
window.kolbo.onToolResult(function (result) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
// was attached on purpose. Collapse instead of showing a dead card.
|
|
78
|
+
if (boot(result.structuredContent || structured(result))) return;
|
|
79
|
+
// No catalog data reached the iframe (host/version mismatch, or a filter
|
|
80
|
+
// that matched nothing) — collapse instead of showing a dead card.
|
|
70
81
|
var card = document.querySelector('.k-card');
|
|
71
82
|
if (card) card.style.display = 'none';
|
|
72
83
|
window.kolbo.notifySize();
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { widgetPage } = require('../html');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic list widget — for tools that return a flat list of named records
|
|
7
|
+
* with no natural thumbnail (projects, sessions, agents, docs, folders,
|
|
8
|
+
* knowledge-base sources). Reuses the exact row shell mediaGrid's audio rows
|
|
9
|
+
* and catalog's model rows already use (`.k-audio-row` / `.k-audio-meta` /
|
|
10
|
+
* `.k-chip` / `.k-btn` — no new CSS needed).
|
|
11
|
+
*
|
|
12
|
+
* structuredContent contract:
|
|
13
|
+
* {
|
|
14
|
+
* widget: 'list',
|
|
15
|
+
* title: 'Your Projects',
|
|
16
|
+
* items: [{
|
|
17
|
+
* id, title, subtitle, // main two lines
|
|
18
|
+
* badge, // small pill, e.g. role/status ("owner", "default", "shared")
|
|
19
|
+
* meta, // small trailing text, e.g. a date or count
|
|
20
|
+
* open_url, // optional — renders an "Open" button (openLink)
|
|
21
|
+
* use_hint // optional — row becomes clickable, sends this on click
|
|
22
|
+
* }],
|
|
23
|
+
* total
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const BODY = `
|
|
28
|
+
<div class="k-card">
|
|
29
|
+
<div class="k-head">
|
|
30
|
+
<span class="k-logo" id="logo"></span>
|
|
31
|
+
<span class="k-title" id="title"></span>
|
|
32
|
+
<span class="k-spacer"></span>
|
|
33
|
+
<span class="k-chip" id="count-chip" style="display:none"></span>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="k-body"><div id="stage" class="k-empty">Loading…</div></div>
|
|
36
|
+
<div class="k-footer"><span>Powered by <a href="#" id="kolbo-link">Kolbo.AI</a></span></div>
|
|
37
|
+
</div>
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const SCRIPT = `
|
|
41
|
+
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
42
|
+
el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
|
|
43
|
+
var state = null;
|
|
44
|
+
|
|
45
|
+
function boot(sc) {
|
|
46
|
+
if (!sc || !sc.items) return;
|
|
47
|
+
state = sc;
|
|
48
|
+
el('title').textContent = sc.title || 'List';
|
|
49
|
+
var total = sc.total != null ? sc.total : sc.items.length;
|
|
50
|
+
el('count-chip').style.display = '';
|
|
51
|
+
el('count-chip').textContent = total + (total === 1 ? ' item' : ' items');
|
|
52
|
+
if (!sc.items.length) { el('stage').innerHTML = '<div class="k-empty">Nothing here yet</div>'; return; }
|
|
53
|
+
el('stage').innerHTML = sc.items.slice(0, 40).map(itemHTML).join('');
|
|
54
|
+
el('stage').classList.remove('k-empty');
|
|
55
|
+
wire();
|
|
56
|
+
window.kolbo.notifySize();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function itemHTML(item, i) {
|
|
60
|
+
var clickable = !!item.use_hint;
|
|
61
|
+
return '<div class="k-audio-row" data-i="' + i + '"' + (clickable ? ' style="cursor:pointer"' : '') + '>' +
|
|
62
|
+
monogram(item.title || '?') +
|
|
63
|
+
'<div class="k-audio-meta"><div class="k-audio-title">' + esc(item.title || '') + '</div>' +
|
|
64
|
+
(item.subtitle ? '<div class="k-audio-sub">' + esc(item.subtitle) + '</div>' : '') + '</div>' +
|
|
65
|
+
(item.badge ? '<span class="k-chip" style="flex:none">' + esc(item.badge) + '</span>' : '') +
|
|
66
|
+
(item.meta ? '<span style="flex:none;font-size:10.5px;color:var(--text-faint)">' + esc(item.meta) + '</span>' : '') +
|
|
67
|
+
(item.open_url ? '<button class="k-btn" data-open="' + i + '">Open</button>' : '') +
|
|
68
|
+
'</div>';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function wire() {
|
|
72
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-open]'), function (b) {
|
|
73
|
+
b.onclick = function (e) {
|
|
74
|
+
e.stopPropagation();
|
|
75
|
+
window.kolbo.openLink(state.items[+b.getAttribute('data-open')].open_url);
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
Array.prototype.forEach.call(document.querySelectorAll('.k-audio-row'), function (row) {
|
|
79
|
+
var item = state.items[+row.getAttribute('data-i')];
|
|
80
|
+
if (!item || !item.use_hint) return;
|
|
81
|
+
row.onclick = function () {
|
|
82
|
+
var msg = item.use_hint.replace('{TITLE}', item.title || '').replace('{ID}', item.id || '');
|
|
83
|
+
window.kolbo.sendMessage(msg);
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.kolbo.onToolResult(function (result) {
|
|
89
|
+
var sc = result.structuredContent || structured(result);
|
|
90
|
+
if (sc && sc.items) return boot(sc);
|
|
91
|
+
var card = document.querySelector('.k-card');
|
|
92
|
+
if (card) card.style.display = 'none';
|
|
93
|
+
window.kolbo.notifySize();
|
|
94
|
+
});
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
function listWidgetHtml() {
|
|
98
|
+
return widgetPage({ title: 'Kolbo List', body: BODY, script: SCRIPT });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { listWidgetHtml };
|
|
@@ -38,7 +38,6 @@ const SCRIPT = `
|
|
|
38
38
|
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
39
39
|
el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
|
|
40
40
|
var state = null;
|
|
41
|
-
var playing = null;
|
|
42
41
|
|
|
43
42
|
function boot(sc) {
|
|
44
43
|
if (!sc || !sc.items) return;
|
|
@@ -63,12 +62,15 @@ function boot(sc) {
|
|
|
63
62
|
|
|
64
63
|
function cellHTML(item, i) {
|
|
65
64
|
var idx = state.items.indexOf(item);
|
|
65
|
+
var isVideo = item.media_type === 'video' && item.url;
|
|
66
66
|
var media = item.thumbnail
|
|
67
67
|
? '<img src="' + esc(item.thumbnail) + '" loading="lazy" alt="">'
|
|
68
68
|
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-faint);font-size:22px">' +
|
|
69
69
|
kindIcon(item.media_type) + '</div>';
|
|
70
70
|
return '<div class="k-cell" data-i="' + idx + '">' +
|
|
71
|
-
'<div class="k-cell-media">' + media +
|
|
71
|
+
'<div class="k-cell-media">' + media +
|
|
72
|
+
(isVideo ? '<button class="k-play k-cell-play" data-video-play="' + esc(item.url) + '">' + ICONS.play + '</button>' : '') +
|
|
73
|
+
'</div>' +
|
|
72
74
|
'<div class="k-cell-label">' + esc(item.title || '') + '</div>' +
|
|
73
75
|
(item.subtitle ? '<div class="k-cell-sub">' + esc(item.subtitle) + '</div>' : '') +
|
|
74
76
|
'</div>';
|
|
@@ -76,13 +78,14 @@ function cellHTML(item, i) {
|
|
|
76
78
|
|
|
77
79
|
function audioRowHTML(item) {
|
|
78
80
|
var idx = state.items.indexOf(item);
|
|
79
|
-
|
|
81
|
+
var src = item.preview_audio || item.url;
|
|
82
|
+
return '<div class="k-audio-row k-generated-audio" data-i="' + idx + '">' +
|
|
80
83
|
(item.thumbnail ? '<img class="k-audio-art" src="' + esc(item.thumbnail) + '">' : '<div class="k-audio-art"></div>') +
|
|
81
84
|
'<div class="k-audio-meta"><div class="k-audio-title">' + esc(item.title || '') + '</div>' +
|
|
82
85
|
'<div class="k-audio-sub">' + esc(item.subtitle || '') + '</div></div>' +
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'
|
|
86
|
+
'<button class="k-btn" data-use="' + idx + '">Use</button>' +
|
|
87
|
+
(src ? '<audio class="k-audio-player" src="' + esc(src) + '" controls preload="none" aria-label="Play ' + esc(item.title || '') + '"></audio>' : '') +
|
|
88
|
+
'</div>';
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
function wire() {
|
|
@@ -92,17 +95,25 @@ function wire() {
|
|
|
92
95
|
Array.prototype.forEach.call(document.querySelectorAll('[data-use]'), function (b) {
|
|
93
96
|
b.onclick = function (e) { e.stopPropagation(); useItem(+b.getAttribute('data-use')); };
|
|
94
97
|
});
|
|
95
|
-
|
|
98
|
+
// Native <audio controls> already gives play/pause + a seek bar + duration;
|
|
99
|
+
// just stop clicks on it from bubbling up to the row's "Use" handler, and
|
|
100
|
+
// pause any other preview when a new one starts.
|
|
101
|
+
var players = document.querySelectorAll('.k-audio-player');
|
|
102
|
+
Array.prototype.forEach.call(players, function (player) {
|
|
103
|
+
player.onclick = function (e) { e.stopPropagation(); };
|
|
104
|
+
player.addEventListener('play', function () {
|
|
105
|
+
Array.prototype.forEach.call(players, function (other) { if (other !== player) other.pause(); });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// Video cells stay a static thumbnail until the play overlay is clicked,
|
|
109
|
+
// then swap in a native <video controls> for an in-place scrubber.
|
|
110
|
+
Array.prototype.forEach.call(document.querySelectorAll('[data-video-play]'), function (b) {
|
|
96
111
|
b.onclick = function (e) {
|
|
97
112
|
e.stopPropagation();
|
|
98
|
-
var url = b.getAttribute('data-play');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
playing = new Audio(url);
|
|
103
|
-
playing.play();
|
|
104
|
-
b.innerHTML = ICONS.pause;
|
|
105
|
-
playing.onended = function () { b.innerHTML = ICONS.play; };
|
|
113
|
+
var url = b.getAttribute('data-video-play');
|
|
114
|
+
var holder = b.parentNode;
|
|
115
|
+
holder.innerHTML = '<video src="' + esc(url) + '" controls autoplay playsinline style="width:100%;height:100%;object-fit:cover"></video>';
|
|
116
|
+
holder.querySelector('video').onclick = function (ev) { ev.stopPropagation(); };
|
|
106
117
|
};
|
|
107
118
|
});
|
|
108
119
|
}
|
package/src/tools/agents.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
|
+
const { UI, uiResult, appsEnabled } = require('../apps');
|
|
7
8
|
|
|
8
|
-
function registerAgentTools(server, client) {
|
|
9
|
+
function registerAgentTools(server, client, options = {}) {
|
|
10
|
+
const ui = () => appsEnabled(server, options);
|
|
9
11
|
// ─── list_agents ───────────────────────────────────────────
|
|
10
12
|
server.tool(
|
|
11
13
|
'list_agents',
|
|
@@ -14,10 +16,28 @@ function registerAgentTools(server, client) {
|
|
|
14
16
|
async ({ search }) => {
|
|
15
17
|
const qs = search ? `?search=${encodeURIComponent(search)}` : '';
|
|
16
18
|
const result = await client.get(`/v1/agents${qs}`);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const agents = result.agents || [];
|
|
20
|
+
const text = JSON.stringify({
|
|
21
|
+
agents,
|
|
22
|
+
count: result.count || agents.length
|
|
23
|
+
}, null, 2);
|
|
24
|
+
|
|
25
|
+
if (ui()) {
|
|
26
|
+
return uiResult(UI.list, text, {
|
|
27
|
+
widget: 'list',
|
|
28
|
+
title: 'Your Agents',
|
|
29
|
+
items: agents.map(a => ({
|
|
30
|
+
id: a.id,
|
|
31
|
+
title: (a.emoji ? a.emoji + ' ' : '') + a.name,
|
|
32
|
+
subtitle: a.description,
|
|
33
|
+
badge: a.is_global ? 'preset' : null,
|
|
34
|
+
use_hint: 'Use my "{TITLE}" agent (agent_id: {ID}) for this conversation.'
|
|
35
|
+
})),
|
|
36
|
+
total: agents.length
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { content: [{ type: 'text', text }] };
|
|
21
41
|
}
|
|
22
42
|
);
|
|
23
43
|
|
package/src/tools/docs.js
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const { projectIdField } = require('./_shared');
|
|
8
|
+
const { UI, uiResult, appsEnabled } = require('../apps');
|
|
8
9
|
|
|
9
10
|
const CONTENT_GUIDE = 'HTML body content. Use clean semantic HTML the in-app editor understands: <h1>-<h3>, <p>, <ul>/<ol>/<li>, <table>, <blockquote>, <strong>/<em>, <a>. No <script>/<style>/<iframe> (stripped server-side). Write the FULL document yourself — this is where you author the doc.';
|
|
10
11
|
|
|
11
|
-
function registerDocTools(server, client) {
|
|
12
|
+
function registerDocTools(server, client, options = {}) {
|
|
13
|
+
const ui = () => appsEnabled(server, options);
|
|
12
14
|
// ─── create_doc ────────────────────────────────────────────
|
|
13
15
|
server.tool(
|
|
14
16
|
'create_doc',
|
|
@@ -50,7 +52,24 @@ function registerDocTools(server, client) {
|
|
|
50
52
|
if (limit) params.set('limit', String(limit));
|
|
51
53
|
const qs = params.toString();
|
|
52
54
|
const result = await client.get(`/v1/docs${qs ? '?' + qs : ''}`);
|
|
53
|
-
|
|
55
|
+
const docs = result.docs || [];
|
|
56
|
+
const text = JSON.stringify({ docs, pagination: result.pagination || null }, null, 2);
|
|
57
|
+
|
|
58
|
+
if (ui()) {
|
|
59
|
+
return uiResult(UI.list, text, {
|
|
60
|
+
widget: 'list',
|
|
61
|
+
title: 'Your AI Docs',
|
|
62
|
+
items: docs.map(d => ({
|
|
63
|
+
id: d.id,
|
|
64
|
+
title: d.title,
|
|
65
|
+
subtitle: (d.updated_at ? String(d.updated_at).slice(0, 10) : '') + (d.is_shared ? ' · shared' : ''),
|
|
66
|
+
open_url: d.share_url || null
|
|
67
|
+
})),
|
|
68
|
+
total: docs.length
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { content: [{ type: 'text', text }] };
|
|
54
73
|
}
|
|
55
74
|
);
|
|
56
75
|
|
package/src/tools/media.js
CHANGED
|
@@ -227,15 +227,25 @@ function registerMediaTools(server, client, options = {}) {
|
|
|
227
227
|
{},
|
|
228
228
|
async () => {
|
|
229
229
|
const result = await client.get('/v1/media/folders');
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
230
|
+
const folders = result.folders || [];
|
|
231
|
+
const text = JSON.stringify({ folders, count: result.count || 0 }, null, 2);
|
|
232
|
+
|
|
233
|
+
if (ui()) {
|
|
234
|
+
return uiResult(UI.list, text, {
|
|
235
|
+
widget: 'list',
|
|
236
|
+
title: 'Media Folders',
|
|
237
|
+
items: folders.map(f => ({
|
|
238
|
+
id: f.id,
|
|
239
|
+
title: f.name,
|
|
240
|
+
subtitle: f.description,
|
|
241
|
+
meta: (f.item_count || 0) + (f.item_count === 1 ? ' item' : ' items'),
|
|
242
|
+
use_hint: 'List media in my "{TITLE}" folder (folder_id: {ID}).'
|
|
243
|
+
})),
|
|
244
|
+
total: folders.length
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { content: [{ type: 'text', text }] };
|
|
239
249
|
}
|
|
240
250
|
);
|
|
241
251
|
|
package/src/tools/models.js
CHANGED
|
@@ -49,7 +49,7 @@ function modelChips(m) {
|
|
|
49
49
|
// deliberately EXCLUDED — we always want a specific model chosen (server
|
|
50
50
|
// instruction #9): auto-routing hides the model choice and the generation
|
|
51
51
|
// metadata used to read just "Auto".
|
|
52
|
-
function buildCatalogStructured(models, type) {
|
|
52
|
+
function buildCatalogStructured(models, type, compact) {
|
|
53
53
|
const groups = [];
|
|
54
54
|
const byName = new Map();
|
|
55
55
|
const isAuto = (m) => /^auto$|smart.select/i.test(String(m.name || '')) || /smart-select|k_auto/i.test(String(m.identifier || ''));
|
|
@@ -78,6 +78,7 @@ function buildCatalogStructured(models, type) {
|
|
|
78
78
|
widget: 'catalog',
|
|
79
79
|
title: 'Kolbo AI Models' + (type ? ' — ' + type : ''),
|
|
80
80
|
total_available: models.length,
|
|
81
|
+
compact: compact === true,
|
|
81
82
|
groups,
|
|
82
83
|
};
|
|
83
84
|
}
|
|
@@ -91,11 +92,16 @@ function registerModelTools(server, client, options = {}) {
|
|
|
91
92
|
{
|
|
92
93
|
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.'),
|
|
93
94
|
format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" returns the raw model documents from the API — use this when you need to programmatically verify caps (max_reference_images, max_visual_dna, max_video_duration, supported_aspect_ratios, etc.) before passing an array/value that might exceed a model-specific limit. The JSON form is the source of truth; the text form is a convenience preview.'),
|
|
94
|
-
display_catalog: z.boolean().optional().describe('Set true
|
|
95
|
+
display_catalog: z.boolean().optional().describe('Set true when the USER explicitly asked to see/browse the available models — the visual catalog opens expanded. Leave unset for internal lookups (verifying a model name, checking caps before a generation): the catalog stays collapsed to a single row the user can tap to browse.')
|
|
95
96
|
},
|
|
96
97
|
async ({ type, format, display_catalog }) => {
|
|
97
|
-
//
|
|
98
|
-
//
|
|
98
|
+
// The tool DECLARATION always carries widget meta, so hosts that mount
|
|
99
|
+
// from tools/list (Claude Code desktop) prepare an iframe on EVERY call.
|
|
100
|
+
// Returning plain text for internal lookups left that iframe with no
|
|
101
|
+
// data — a dead, empty "Widget from Kolbo list_models" shell. Always
|
|
102
|
+
// ship structuredContent; `compact` tells the widget to render a single
|
|
103
|
+
// "Browse models" row (expandable) instead of the full catalog, which is
|
|
104
|
+
// what display_catalog was really asking for.
|
|
99
105
|
const showCatalog = display_catalog === true;
|
|
100
106
|
const path = type ? `/v1/models?type=${encodeURIComponent(type)}` : '/v1/models';
|
|
101
107
|
const result = await client.get(path);
|
|
@@ -106,7 +112,7 @@ function registerModelTools(server, client, options = {}) {
|
|
|
106
112
|
// resolution multipliers, supports_* flags, prompt-length limits, etc.).
|
|
107
113
|
if (format === 'json') {
|
|
108
114
|
const text = JSON.stringify({ count: result.count, models: result.models }, null, 2);
|
|
109
|
-
if (ui()
|
|
115
|
+
if (ui()) return uiResult(UI.catalog, text, buildCatalogStructured(result.models, type, !showCatalog));
|
|
110
116
|
return { content: [{ type: 'text', text }] };
|
|
111
117
|
}
|
|
112
118
|
|
|
@@ -277,7 +283,7 @@ function registerModelTools(server, client, options = {}) {
|
|
|
277
283
|
}
|
|
278
284
|
|
|
279
285
|
const text = `Available models (${result.count}):\n\n${sections.join('\n\n')}\n\nUse the "identifier" value as the "model" parameter in generate tools. For programmatic cap validation, re-call with format: "json".`;
|
|
280
|
-
if (ui()
|
|
286
|
+
if (ui()) return uiResult(UI.catalog, text, buildCatalogStructured(result.models, type, !showCatalog));
|
|
281
287
|
return { content: [{ type: 'text', text }] };
|
|
282
288
|
}
|
|
283
289
|
);
|
package/src/tools/projects.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const { buildProjectUrl } = require('./_shared');
|
|
8
|
+
const { UI, uiResult, appsEnabled } = require('../apps');
|
|
8
9
|
|
|
9
|
-
function registerProjectTools(server, client) {
|
|
10
|
+
function registerProjectTools(server, client, options = {}) {
|
|
11
|
+
const ui = () => appsEnabled(server, options);
|
|
10
12
|
// ─── list_projects ─────────────────────────────────────────
|
|
11
13
|
server.tool(
|
|
12
14
|
'list_projects',
|
|
@@ -21,16 +23,28 @@ function registerProjectTools(server, client) {
|
|
|
21
23
|
is_default: !!p.is_default,
|
|
22
24
|
open_url: buildProjectUrl(p.id, { is_default: !!p.is_default })
|
|
23
25
|
}));
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
const text = JSON.stringify({
|
|
27
|
+
projects,
|
|
28
|
+
count: projects.length,
|
|
29
|
+
_hint: 'Pass the chosen `id` as `project_id` on any generate_* tool to drop the generation into that project. Omit project_id to use the project flagged is_default:true. `open_url` opens that project\'s media in the web app (share it with the user).'
|
|
30
|
+
}, null, 2);
|
|
31
|
+
|
|
32
|
+
if (ui()) {
|
|
33
|
+
return uiResult(UI.list, text, {
|
|
34
|
+
widget: 'list',
|
|
35
|
+
title: 'Your Projects',
|
|
36
|
+
items: projects.map(p => ({
|
|
37
|
+
id: p.id,
|
|
38
|
+
title: p.name,
|
|
39
|
+
subtitle: p.role + (p.is_default ? ' · default' : ''),
|
|
40
|
+
open_url: p.open_url,
|
|
41
|
+
use_hint: 'Use my "{TITLE}" project (project_id: {ID}) for what I do next.'
|
|
42
|
+
})),
|
|
43
|
+
total: projects.length
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { content: [{ type: 'text', text }] };
|
|
34
48
|
}
|
|
35
49
|
);
|
|
36
50
|
|
|
@@ -131,7 +145,23 @@ function registerProjectTools(server, client) {
|
|
|
131
145
|
if (limit) params.set('limit', String(limit));
|
|
132
146
|
const qs = params.toString();
|
|
133
147
|
const result = await client.get(`/v1/sessions${qs ? '?' + qs : ''}`);
|
|
134
|
-
|
|
148
|
+
const sessions = result.sessions || [];
|
|
149
|
+
const text = JSON.stringify({ sessions, pagination: result.pagination || null }, null, 2);
|
|
150
|
+
|
|
151
|
+
if (ui()) {
|
|
152
|
+
return uiResult(UI.list, text, {
|
|
153
|
+
widget: 'list',
|
|
154
|
+
title: 'Sessions' + (type ? ' — ' + type : ''),
|
|
155
|
+
items: sessions.map(s => ({
|
|
156
|
+
id: s.session_id,
|
|
157
|
+
title: s.name || s.type,
|
|
158
|
+
subtitle: s.type + (s.updated_at ? ' · ' + String(s.updated_at).slice(0, 10) : '')
|
|
159
|
+
})),
|
|
160
|
+
total: sessions.length
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { content: [{ type: 'text', text }] };
|
|
135
165
|
}
|
|
136
166
|
);
|
|
137
167
|
|
|
@@ -162,7 +192,24 @@ function registerProjectTools(server, client) {
|
|
|
162
192
|
{ project_id: z.string().describe('Project ObjectId.') },
|
|
163
193
|
async ({ project_id }) => {
|
|
164
194
|
const result = await client.get(`/v1/projects/${encodeURIComponent(project_id)}/context`);
|
|
165
|
-
|
|
195
|
+
const sources = result.sources || [];
|
|
196
|
+
const text = JSON.stringify({ sources, count: result.count || 0 }, null, 2);
|
|
197
|
+
|
|
198
|
+
if (ui()) {
|
|
199
|
+
return uiResult(UI.list, text, {
|
|
200
|
+
widget: 'list',
|
|
201
|
+
title: 'Project Knowledge Base',
|
|
202
|
+
items: sources.map(s => ({
|
|
203
|
+
id: s.file_key,
|
|
204
|
+
title: s.title || s.type,
|
|
205
|
+
subtitle: s.type + ' · ' + s.status,
|
|
206
|
+
open_url: s.url || null
|
|
207
|
+
})),
|
|
208
|
+
total: sources.length
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { content: [{ type: 'text', text }] };
|
|
166
213
|
}
|
|
167
214
|
);
|
|
168
215
|
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -210,7 +210,24 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
210
210
|
{},
|
|
211
211
|
async () => {
|
|
212
212
|
const result = await client.get('/v1/visual-dna/folders');
|
|
213
|
-
|
|
213
|
+
const folders = result.folders || [];
|
|
214
|
+
const text = JSON.stringify({ folders, count: result.count || 0 }, null, 2);
|
|
215
|
+
|
|
216
|
+
if (ui()) {
|
|
217
|
+
return uiResult(UI.list, text, {
|
|
218
|
+
widget: 'list',
|
|
219
|
+
title: 'Visual DNA Folders',
|
|
220
|
+
items: folders.map(f => ({
|
|
221
|
+
id: f.id,
|
|
222
|
+
title: f.name,
|
|
223
|
+
meta: f.item_count != null ? (f.item_count + (f.item_count === 1 ? ' DNA' : ' DNAs')) : null,
|
|
224
|
+
use_hint: 'List Visual DNAs in my "{TITLE}" folder (folder_id: {ID}).'
|
|
225
|
+
})),
|
|
226
|
+
total: folders.length
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { content: [{ type: 'text', text }] };
|
|
214
231
|
}
|
|
215
232
|
);
|
|
216
233
|
|