@kolbo/mcp 1.48.0 → 1.50.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/README.md +12 -10
- package/package.json +2 -2
- package/skill/SKILL.md +2 -1
- package/skill/references/models/music.md +1 -1
- package/src/apps/bridge.js +1 -1
- package/src/apps/index.js +14 -2
- package/src/apps/theme.js +18 -0
- package/src/apps/widgets/generation.js +109 -20
- package/src/apps/widgets/transcript.js +38 -9
- package/src/index.js +1 -1
- package/src/tools/_shared.js +59 -2
- package/src/tools/chat.js +4 -3
- package/src/tools/generate.js +96 -38
- package/src/tools/music_library.js +141 -174
- package/src/tools/projects.js +6 -3
- package/src/tools/stock_library.js +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Use [Kolbo AI](https://kolbo.ai) as native tools in Claude Code and Claude Deskt
|
|
|
4
4
|
|
|
5
5
|
Generate images, videos, music, speech, sound effects, multi-scene campaigns, and conversational chat — all from natural language in your coding environment. 100+ AI models behind Smart Select routing, with reusable Visual DNA profiles for character/style consistency.
|
|
6
6
|
|
|
7
|
-
**✨ Interactive widgets (v1.30+):** in claude.ai and
|
|
7
|
+
**✨ Interactive widgets (v1.30+):** in claude.ai, Claude Desktop, and Codex Desktop, generations render as live Kolbo cards — real-time progress with model + settings chips, an inline result gallery / video or audio player, and one-click **Animate · Edit · Recreate · Download** actions. Multi-audio generations render every track with its own player and Download button. Library and model searches render as browsable grids with audio preview. Text-only clients (Claude Code, Codex CLI, Cursor) keep the classic text responses.
|
|
8
8
|
|
|
9
9
|
## Set up — paste one prompt, or one config block (keyless, no API key)
|
|
10
10
|
|
|
@@ -113,7 +113,7 @@ Just ask your agent naturally:
|
|
|
113
113
|
|
|
114
114
|
Without the optional skill, the config block alone already exposes every tool — you just describe what you want. With the skill installed, each of these is also routed to the right MCP tool with the right defaults — UGC mode picks 9:16 + sound-off + no-captions, marketplace mode enforces compliance (pure white bg, no text, no props), product photoshoot mode uses the right aspect for the platform (2:3 Pinterest, 16:9 hero banner, 1:1 IG feed), etc. The routing logic is shared with [Kolbo Code](https://github.com/Zoharvan12/kolbo-code), so the behavior is identical however you connect.
|
|
115
115
|
|
|
116
|
-
## Available Tools (
|
|
116
|
+
## Available Tools (95)
|
|
117
117
|
|
|
118
118
|
**Generation**
|
|
119
119
|
| Tool | Description |
|
|
@@ -193,16 +193,18 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
193
193
|
|------|-------------|
|
|
194
194
|
| `publish_html_artifact` | Publish an HTML page, SVG, or Mermaid diagram and get a public shareable URL on `sites.kolbo.ai`. Pass `share_token` from a prior publish to update the same URL in place (old content kept in version history). |
|
|
195
195
|
|
|
196
|
-
**Music Library** (
|
|
196
|
+
**SYNCI Music Library** (licensed production music)
|
|
197
197
|
| Tool | Description |
|
|
198
198
|
|------|-------------|
|
|
199
|
-
| `search_music_library` | Search the licensed
|
|
200
|
-
| `analyze_script_for_music` |
|
|
201
|
-
| `browse_music_library` | Browse the catalog without a query
|
|
202
|
-
| `get_music_library_facets` | List
|
|
203
|
-
| `get_music_track_audio` | Get
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
199
|
+
| `search_music_library` | Search the licensed catalog; results contain watermarked previews only. |
|
|
200
|
+
| `analyze_script_for_music` | Turn a script or scene description into a music search. |
|
|
201
|
+
| `browse_music_library` | Browse the catalog without a query. |
|
|
202
|
+
| `get_music_library_facets` | List genres, moods, instruments, BPM, and duration filters. |
|
|
203
|
+
| `get_music_track_audio` | Get watermarked preview URLs for a track. |
|
|
204
|
+
| `acquire_clean_music_track` | Spend one SYNCI vendor credit and return clean MP3/WAV signed URLs. Idempotent with `request_id`. |
|
|
205
|
+
| `import_music_track_to_library` | Spend one vendor credit and copy a clean MP3/WAV into Kolbo's media library. |
|
|
206
|
+
| `get_music_track_related` | Get stems/alternate-version metadata (purchasing remains unsupported). |
|
|
207
|
+
| `get_music_track_lyrics` | Get lyrics metadata. |
|
|
206
208
|
|
|
207
209
|
**Stock Library** (multi-source stock media: Pexels, Pixabay, Sketchfab 3D, Music)
|
|
208
210
|
| Tool | Description |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolbo/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
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": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/ext-apps": "^1.7.4",
|
|
45
45
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
46
|
-
"form-data": "^4.0.
|
|
46
|
+
"form-data": "^4.0.6",
|
|
47
47
|
"zod": "^3.25.0"
|
|
48
48
|
},
|
|
49
49
|
"overrides": {
|
package/skill/SKILL.md
CHANGED
|
@@ -115,7 +115,8 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
|
|
|
115
115
|
| `upload_media` / `list_media` / `get_media` / `get_media_stats` / `favorite_media` / `unfavorite_media` / `delete_media` / `restore_media` / `permanently_delete_media` / `move_media` / `bulk_*_media` / `*_media_folder` | Media library — see `workflows/media-library.md` |
|
|
116
116
|
| `create_visual_dna` / `generate_character_sheet` / `list_visual_dnas` / `get_visual_dna` / `delete_visual_dna` / `*_visual_dna_folder` (5 folder tools) | Visual DNA (+ character sheet, character folders) — see `workflows/visual-dna.md` |
|
|
117
117
|
| `list_moodboards` / `get_moodboard` / `list_presets` | Style overlays |
|
|
118
|
-
| `search_stock_media` / `get_stock_sources` / `get_stock_categories` / `get_stock_collections` / `get_stock_asset` / `analyze_script_for_stock` / `import_stock_asset` | Stock library (free, no credits) — EXISTING photos / videos / 3D / SFX / music. For stock **music** use `search_stock_media` with `mediaType: "music"` (semantic vibe query, e.g. "uplifting corporate background") → `get_stock_asset` for downloads.
|
|
118
|
+
| `search_stock_media` / `get_stock_sources` / `get_stock_categories` / `get_stock_collections` / `get_stock_asset` / `analyze_script_for_stock` / `import_stock_asset` | Stock library (free, no credits) — EXISTING photos / videos / 3D / SFX / music. For stock **music** use `search_stock_media` with `mediaType: "music"` (semantic vibe query, e.g. "uplifting corporate background") → `get_stock_asset` for downloads. |
|
|
119
|
+
| `search_music_library` / `browse_music_library` / `get_music_track_audio` / `acquire_clean_music_track` / `import_music_track_to_library` | SYNCI licensed music. Search/playback is watermarked preview-only. `acquire_clean_music_track` and `import_music_track_to_library` immediately consume one SYNCI vendor credit and return/import an unwatermarked MP3/WAV; reuse `request_id` on retries. |
|
|
119
120
|
| `list_projects` / `move_session` | Projects: resolve a project NAME → the `project_id` you pass on generation/upload/doc calls; `move_session` relocates a whole session + its media when work landed in the wrong project. NOT the same as `app_builder_list_projects`. See "Projects — Where Work Lands" below. |
|
|
120
121
|
| `create_project` / `update_project` / `archive_project` / `unarchive_project` / `list_sessions` | Project lifecycle + session inventory (deletion stays in-app). Create a project when the user starts new work, then pass its id on EVERY call. |
|
|
121
122
|
| `add_project_context` / `list_project_context` / `delete_project_context` / `get_project_profile` / `regenerate_project_profile` | Project knowledge base (RAG): feed scripts/URLs/notes; `get_project_profile` = the living brief — read it to ground work in the project |
|
|
@@ -8,7 +8,7 @@ Load this file when the user wants AI-generated **music** — full songs, lyrics
|
|
|
8
8
|
|
|
9
9
|
**Kolbo MCP routing:** call `generate_music`. Suno is a model option — use `list_models({ type: "music_gen" })` to see versions. Pass `instrumental` and `duration` as separate params; pass the Style/Description text as `style` and the Lyrics as `lyrics`.
|
|
10
10
|
|
|
11
|
-
**Wants an EXISTING track, not a new song?** ("background music", "stock music", "royalty-free track") → don't generate. Use `search_stock_media` with `mediaType: "music"`
|
|
11
|
+
**Wants an EXISTING track, not a new song?** ("background music", "stock music", "royalty-free track") → don't generate. Use `search_stock_media` with `mediaType: "music"` for free Kolbo/Coverr results. For SYNCI's licensed catalog use `search_music_library`; its playback URLs are watermarked previews. Final use must call `acquire_clean_music_track` or `import_music_track_to_library`, which immediately consumes one SYNCI vendor credit and returns/imports a clean MP3/WAV. Reuse `request_id` when retrying the same action.
|
|
12
12
|
|
|
13
13
|
## CRITICAL Kolbo Platform Rules
|
|
14
14
|
|
package/src/apps/bridge.js
CHANGED
|
@@ -69,7 +69,7 @@ const BRIDGE_JS = `
|
|
|
69
69
|
} else if (m.method === 'ui/notifications/tool-input' || m.method === 'ui/notifications/tool-input-partial') {
|
|
70
70
|
// Fires while the tool is still RUNNING — lets widgets show a real
|
|
71
71
|
// "preparing" state instead of a blank card until the result lands.
|
|
72
|
-
toolInputFns.forEach(function (f) { try { f((m.params && m.params.arguments) || {}); } catch (e) {} });
|
|
72
|
+
toolInputFns.forEach(function (f) { try { f((m.params && m.params.arguments) || {}, m.params || {}); } catch (e) {} });
|
|
73
73
|
} else if (m.method === 'ui/notifications/host-context-changed') {
|
|
74
74
|
hostContext = (m.params && m.params.hostContext) || m.params || hostContext;
|
|
75
75
|
themeFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
|
package/src/apps/index.js
CHANGED
|
@@ -124,7 +124,19 @@ function appsEnabled(server, opts = {}) {
|
|
|
124
124
|
if (opts.apps === true || process.env.KOLBO_MCP_APPS === '1') return true;
|
|
125
125
|
try {
|
|
126
126
|
const caps = server?.server?.getClientCapabilities?.();
|
|
127
|
-
|
|
127
|
+
if (getUiCapability(caps) !== undefined) return true;
|
|
128
|
+
|
|
129
|
+
// Codex Desktop currently mounts MCP App resources from tool `_meta`, but
|
|
130
|
+
// its initialize handshake identifies as `codex-mcp-client` with an empty
|
|
131
|
+
// capabilities object. Without this compatibility path the host mounts a
|
|
132
|
+
// "Preparing" card while the tool takes the blocking/text fallback, so the
|
|
133
|
+
// completed media never reaches the iframe as structuredContent.
|
|
134
|
+
//
|
|
135
|
+
// Keep the desktop-origin check: Codex CLI uses the same client name but is
|
|
136
|
+
// a text surface, where returning immediately would remove the final URLs.
|
|
137
|
+
const info = server?.server?.getClientVersion?.();
|
|
138
|
+
const origin = process.env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE || '';
|
|
139
|
+
return info?.name === 'codex-mcp-client' && /codex desktop/i.test(origin);
|
|
128
140
|
} catch (_) {
|
|
129
141
|
return false;
|
|
130
142
|
}
|
|
@@ -272,9 +284,9 @@ const TOOL_WIDGETS = {
|
|
|
272
284
|
// media grid
|
|
273
285
|
list_media: UI.mediaGrid,
|
|
274
286
|
search_stock_media: UI.mediaGrid,
|
|
275
|
-
get_stock_collections: UI.mediaGrid,
|
|
276
287
|
search_music_library: UI.mediaGrid,
|
|
277
288
|
browse_music_library: UI.mediaGrid,
|
|
289
|
+
get_stock_collections: UI.mediaGrid,
|
|
278
290
|
list_presets: UI.mediaGrid,
|
|
279
291
|
list_voices: UI.mediaGrid,
|
|
280
292
|
list_visual_dnas: UI.mediaGrid,
|
package/src/apps/theme.js
CHANGED
|
@@ -259,6 +259,24 @@ html.k-fullscreen .k-actions { flex: none; padding-top: 8px; }
|
|
|
259
259
|
.k-audio-meta { flex: 1; min-width: 0; }
|
|
260
260
|
.k-audio-title { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
261
261
|
.k-audio-sub { font-size: 11px; color: var(--text-faint); }
|
|
262
|
+
.k-generated-audio {
|
|
263
|
+
display: grid; grid-template-columns: 40px minmax(0, 1fr) auto;
|
|
264
|
+
align-items: center; gap: 7px 10px;
|
|
265
|
+
}
|
|
266
|
+
.k-generated-audio .k-audio-placeholder {
|
|
267
|
+
display: flex; align-items: center; justify-content: center; color: var(--brand);
|
|
268
|
+
}
|
|
269
|
+
.k-generated-audio .k-audio-placeholder svg { width: 19px; height: 19px; }
|
|
270
|
+
.k-generated-audio .k-audio-download { white-space: nowrap; }
|
|
271
|
+
.k-generated-audio .k-audio-player {
|
|
272
|
+
grid-column: 2 / -1; display: block; width: 100%; min-width: 0; height: 34px;
|
|
273
|
+
}
|
|
274
|
+
@media (max-width: 520px) {
|
|
275
|
+
.k-generated-audio { grid-template-columns: 36px minmax(0, 1fr); }
|
|
276
|
+
.k-generated-audio .k-audio-art { width: 36px; height: 36px; }
|
|
277
|
+
.k-generated-audio .k-audio-download { grid-column: 2; justify-self: start; }
|
|
278
|
+
.k-generated-audio .k-audio-player { grid-column: 1 / -1; }
|
|
279
|
+
}
|
|
262
280
|
.k-play {
|
|
263
281
|
width: 32px; height: 32px; border-radius: 50%; flex: none; border: 1px solid rgba(255,255,255,0.18);
|
|
264
282
|
background: rgba(0,0,0,0.5); color: #fff; cursor: pointer;
|
|
@@ -17,6 +17,7 @@ const { widgetPage } = require('../html');
|
|
|
17
17
|
* settings: { duration, resolution, aspect_ratio, audio, voice, mode },
|
|
18
18
|
* reference_image, // thumbnail URL (optional)
|
|
19
19
|
* urls, thumbnail_url, title, duration, credits_used,
|
|
20
|
+
* tracks: [{ title, duration, thumbnail_url, model }], // optional audio metadata by URL index
|
|
20
21
|
* scenes: [{ scene_number, title, image_urls, video_urls }],
|
|
21
22
|
* error,
|
|
22
23
|
* open_url // "Open in Kolbo" target (optional)
|
|
@@ -53,6 +54,8 @@ const SCRIPT = `
|
|
|
53
54
|
var state = null; // current structuredContent
|
|
54
55
|
var selected = 0; // selected result index
|
|
55
56
|
var pollTimer = null;
|
|
57
|
+
var originTool = null;
|
|
58
|
+
var originArgs = {};
|
|
56
59
|
|
|
57
60
|
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
58
61
|
el('action-cancel').innerHTML = ICONS.x;
|
|
@@ -122,23 +125,26 @@ function renderGenerating(sc) {
|
|
|
122
125
|
schedulePoll(sc);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
// Poll ceilings so the card never spins
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
var MAX_POLL_MS =
|
|
129
|
-
var MAX_POLL_ERRORS =
|
|
128
|
+
// Poll ceilings so the card never spins forever. Losing widget-side tracking
|
|
129
|
+
// is not a generation failure: keep retries disabled because the paid server
|
|
130
|
+
// job may still complete.
|
|
131
|
+
var MAX_POLL_MS = 35 * 60 * 1000;
|
|
132
|
+
var MAX_POLL_ERRORS = 30;
|
|
130
133
|
var pollStart = 0, pollErrors = 0;
|
|
131
134
|
|
|
132
135
|
function schedulePoll(sc) {
|
|
133
136
|
if (!pollStart) pollStart = Date.now();
|
|
134
137
|
clearTimeout(pollTimer);
|
|
135
|
-
|
|
138
|
+
// The call itself long-waits server-side (normally up to three minutes).
|
|
139
|
+
// This short pause only separates successive wait windows.
|
|
140
|
+
var delay = 1500;
|
|
141
|
+
pollTimer = setTimeout(function () { poll(sc); }, delay);
|
|
136
142
|
}
|
|
137
143
|
function poll(sc) {
|
|
138
144
|
if (pollStart && (Date.now() - pollStart) > MAX_POLL_MS) {
|
|
139
|
-
return
|
|
145
|
+
return renderTrackingIssue('This is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
|
|
140
146
|
}
|
|
141
|
-
var args = sc.status_args || { generation_id: sc.generation_id };
|
|
147
|
+
var args = sc.status_args || { generation_id: sc.generation_id, wait: true };
|
|
142
148
|
window.kolbo.callTool(sc.poll_tool || 'get_generation_status', args).then(function (res) {
|
|
143
149
|
var st = structured(res) || {};
|
|
144
150
|
var stateName = st.state || st.phase || st.status;
|
|
@@ -146,7 +152,7 @@ function poll(sc) {
|
|
|
146
152
|
// generation state — count it toward the consecutive-error cap so a record
|
|
147
153
|
// that can't be resolved errors out fast instead of polling forever.
|
|
148
154
|
if ((res && res.isError) || st.success === false || (st.error && !stateName)) {
|
|
149
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
155
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue(st.error || 'Tracking paused. The generation may still be running.');
|
|
150
156
|
return schedulePoll(sc);
|
|
151
157
|
}
|
|
152
158
|
if (stateName === 'completed') {
|
|
@@ -163,9 +169,13 @@ function poll(sc) {
|
|
|
163
169
|
renderResult(done);
|
|
164
170
|
// Let the model know the outcome without it having to poll.
|
|
165
171
|
try {
|
|
172
|
+
var completedUrls = (done.urls || []).slice();
|
|
173
|
+
(done.scenes || []).forEach(function (scene) {
|
|
174
|
+
completedUrls = completedUrls.concat(scene.image_urls || [], scene.video_urls || []);
|
|
175
|
+
});
|
|
166
176
|
window.kolbo.updateModelContext(
|
|
167
177
|
'Generation ' + (sc.generation_id || '') + ' completed (' + (sc.tool || '') + ').' +
|
|
168
|
-
'\\nOutput URLs:\\n' +
|
|
178
|
+
'\\nOutput URLs:\\n' + completedUrls.join('\\n') +
|
|
169
179
|
(done.credits_used != null ? '\\nCredits used: ' + done.credits_used : ''));
|
|
170
180
|
} catch (e) {}
|
|
171
181
|
} else if (stateName === 'failed' || stateName === 'error' || stateName === 'cancelled') {
|
|
@@ -175,7 +185,7 @@ function poll(sc) {
|
|
|
175
185
|
schedulePoll(sc);
|
|
176
186
|
}
|
|
177
187
|
}).catch(function () {
|
|
178
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
188
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue('Tracking paused after repeated connection errors. The generation may still be running.');
|
|
179
189
|
schedulePoll(sc);
|
|
180
190
|
});
|
|
181
191
|
}
|
|
@@ -240,13 +250,35 @@ function renderVideo(sc, urls) {
|
|
|
240
250
|
|
|
241
251
|
function renderAudio(sc, urls) {
|
|
242
252
|
el('stage').innerHTML = urls.map(function (u, i) {
|
|
243
|
-
var
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
var track = (sc.tracks && sc.tracks[i]) || {};
|
|
254
|
+
var titleBase = track.title || sc.title || (TOOL_TITLES[sc.tool] || 'Audio');
|
|
255
|
+
var title = titleBase + (urls.length > 1 ? ' — Track ' + (i + 1) : '');
|
|
256
|
+
var duration = track.duration != null ? track.duration : sc.duration;
|
|
257
|
+
var artwork = track.thumbnail_url || sc.thumbnail_url;
|
|
258
|
+
return '<div class="k-audio-row k-generated-audio">' +
|
|
259
|
+
(artwork ? '<img class="k-audio-art" src="' + esc(artwork) + '" alt="">' :
|
|
260
|
+
'<div class="k-audio-art k-audio-placeholder">' + ICONS.audio + '</div>') +
|
|
246
261
|
'<div class="k-audio-meta"><div class="k-audio-title">' + esc(title) + '</div>' +
|
|
247
|
-
'<div class="k-audio-sub">' + esc(
|
|
248
|
-
'
|
|
262
|
+
'<div class="k-audio-sub">' + esc(track.model || sc.model || '') +
|
|
263
|
+
(duration ? ' · ' + fmtDur(duration) : '') + '</div></div>' +
|
|
264
|
+
'<button class="k-btn k-audio-download" data-audio-download="' + esc(u) +
|
|
265
|
+
'" aria-label="Download ' + esc(title) + '">' + ICONS.download + ' Download</button>' +
|
|
266
|
+
'<audio class="k-audio-player" src="' + esc(u) + '" controls preload="metadata" aria-label="Play ' +
|
|
267
|
+
esc(title) + '"></audio></div>';
|
|
249
268
|
}).join('');
|
|
269
|
+
Array.prototype.forEach.call(el('stage').querySelectorAll('[data-audio-download]'), function (b) {
|
|
270
|
+
b.onclick = function () {
|
|
271
|
+
window.kolbo.openLink(downloadUrl(b.getAttribute('data-audio-download')));
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
var players = el('stage').querySelectorAll('.k-audio-player');
|
|
275
|
+
Array.prototype.forEach.call(players, function (player) {
|
|
276
|
+
player.addEventListener('play', function () {
|
|
277
|
+
Array.prototype.forEach.call(players, function (other) {
|
|
278
|
+
if (other !== player) other.pause();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
250
282
|
}
|
|
251
283
|
|
|
252
284
|
function render3d(sc, urls) {
|
|
@@ -363,6 +395,22 @@ function renderError(msg) {
|
|
|
363
395
|
window.kolbo.notifySize();
|
|
364
396
|
}
|
|
365
397
|
|
|
398
|
+
function renderTrackingIssue(msg) {
|
|
399
|
+
clearTimeout(pollTimer);
|
|
400
|
+
setPhaseChip('Still working', false);
|
|
401
|
+
el('stage').innerHTML = '<div class="k-error">' + ICONS.clock + ' ' + esc(msg) + '</div>';
|
|
402
|
+
el('actions').innerHTML = '<button class="k-btn primary" id="status-btn">' + ICONS.clock + ' Check status</button>' +
|
|
403
|
+
'<button class="k-btn ghost" id="tracking-open">Open in Kolbo ' + ICONS.open + '</button>';
|
|
404
|
+
el('status-btn').onclick = function () {
|
|
405
|
+
window.kolbo.sendMessage('Check the existing Kolbo generation status without retrying it.' +
|
|
406
|
+
(state && state.generation_id ? '\\nGeneration ID: ' + state.generation_id : ''));
|
|
407
|
+
};
|
|
408
|
+
el('tracking-open').onclick = function () {
|
|
409
|
+
window.kolbo.openLink((state && state.open_url) || 'https://app.kolbo.ai');
|
|
410
|
+
};
|
|
411
|
+
window.kolbo.notifySize();
|
|
412
|
+
}
|
|
413
|
+
|
|
366
414
|
/* ---------- fullscreen viewer ---------- */
|
|
367
415
|
var isFullscreen = false;
|
|
368
416
|
function toggleFullscreen() {
|
|
@@ -417,9 +465,10 @@ function renderActions(sc) {
|
|
|
417
465
|
}
|
|
418
466
|
if (sc.kind === 'video') {
|
|
419
467
|
a.push('<button class="k-btn primary" id="btn-download">' + ICONS.download + ' Download</button>');
|
|
420
|
-
} else if (hasSingleUrl) {
|
|
468
|
+
} else if (hasSingleUrl && sc.kind !== 'audio') {
|
|
421
469
|
// Scenes (Creative Director) have no single "current" url — per-item hover
|
|
422
|
-
// download buttons cover them instead.
|
|
470
|
+
// download buttons cover them instead. Audio has a visible download button
|
|
471
|
+
// on every track so multi-output generations never download only track 1.
|
|
423
472
|
a.push('<button class="k-btn" id="btn-download">' + ICONS.download + ' Download</button>');
|
|
424
473
|
}
|
|
425
474
|
a.push('<button class="k-btn" id="btn-recreate">' + ICONS.retry + ' Recreate</button>');
|
|
@@ -471,6 +520,8 @@ function openPromptRow(placeholder, onSend) {
|
|
|
471
520
|
take many seconds (model resolution, file upload, submit). Show a live
|
|
472
521
|
shell immediately instead of a blank card. */
|
|
473
522
|
function bootPre(toolName, args) {
|
|
523
|
+
if (toolName) originTool = toolName;
|
|
524
|
+
if (args) originArgs = args;
|
|
474
525
|
if (state) return; // real data already arrived
|
|
475
526
|
el('tool-title').textContent = TOOL_TITLES[toolName] || 'Generation';
|
|
476
527
|
if (args && (args.prompt || args.text)) {
|
|
@@ -484,23 +535,61 @@ function bootPre(toolName, args) {
|
|
|
484
535
|
window.kolbo.notifySize();
|
|
485
536
|
}
|
|
486
537
|
|
|
538
|
+
function kindFromTool(tool, sc) {
|
|
539
|
+
if (sc && sc.scenes) return 'scenes';
|
|
540
|
+
if (/video|lipsync|elements|first_last/i.test(tool || '')) return 'video';
|
|
541
|
+
if (/music|speech|sound/i.test(tool || '')) return 'audio';
|
|
542
|
+
if (/3d/i.test(tool || '')) return '3d';
|
|
543
|
+
var firstUrl = sc && Array.isArray(sc.urls) ? String(sc.urls[0] || '').split('?')[0].toLowerCase() : '';
|
|
544
|
+
if (/\\.(mp4|mov|webm|mkv)$/.test(firstUrl)) return 'video';
|
|
545
|
+
if (/\\.(mp3|wav|m4a|aac|ogg|flac)$/.test(firstUrl)) return 'audio';
|
|
546
|
+
if (/\\.(glb|gltf|fbx|obj|usdz)$/.test(firstUrl)) return '3d';
|
|
547
|
+
return 'image';
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Recover a successful legacy/text result when a host mounted the iframe from
|
|
551
|
+
// declaration metadata but the server did not recognize its Apps capability.
|
|
552
|
+
function completedFromPlain(sc) {
|
|
553
|
+
if (!sc || (!Array.isArray(sc.urls) && !Array.isArray(sc.scenes))) return null;
|
|
554
|
+
return Object.assign({}, sc, {
|
|
555
|
+
widget: 'generation',
|
|
556
|
+
phase: 'completed',
|
|
557
|
+
tool: originTool,
|
|
558
|
+
kind: kindFromTool(originTool, sc),
|
|
559
|
+
prompt: originArgs.prompt || originArgs.text || sc.prompt_used || '',
|
|
560
|
+
model: sc.model || originArgs.model,
|
|
561
|
+
settings: {
|
|
562
|
+
duration: sc.duration || originArgs.duration,
|
|
563
|
+
resolution: originArgs.resolution,
|
|
564
|
+
aspect_ratio: originArgs.aspect_ratio
|
|
565
|
+
},
|
|
566
|
+
urls: sc.urls || []
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
487
570
|
/* ---------- wire host events ---------- */
|
|
488
571
|
window.kolbo.onToolResult(function (result) {
|
|
489
572
|
var sc = result.structuredContent || structured(result);
|
|
490
573
|
if (sc && (sc.phase || sc.widget)) return boot(sc);
|
|
574
|
+
var recovered = completedFromPlain(sc);
|
|
575
|
+
if (recovered) return boot(recovered);
|
|
491
576
|
// Tool errored (or returned plain text): show it instead of a dead blank card.
|
|
492
577
|
var txt = '';
|
|
493
578
|
try { txt = (result.content || []).filter(function (c) { return c.type === 'text'; }).map(function (c) { return c.text; }).join(' '); } catch (e) {}
|
|
579
|
+
if (/timed out|timeout/i.test(txt)) {
|
|
580
|
+
return renderTrackingIssue((txt || 'Tracking timed out. The generation may still be running.').slice(0, 300));
|
|
581
|
+
}
|
|
494
582
|
if (result.isError || /error|failed/i.test(txt)) {
|
|
495
583
|
renderError((txt || 'The request failed.').slice(0, 300));
|
|
496
584
|
}
|
|
497
585
|
});
|
|
498
|
-
window.kolbo.onToolInput(function (args) { bootPre(
|
|
586
|
+
window.kolbo.onToolInput(function (args, info) { bootPre(info && info.name, args); });
|
|
499
587
|
window.kolbo.ready(function (ctx) {
|
|
500
588
|
var info = ctx && ctx.toolInfo;
|
|
501
589
|
if (!state && info) {
|
|
590
|
+
originTool = (info.tool && info.tool.name) || originTool;
|
|
502
591
|
if (info.result && info.result.structuredContent) return boot(info.result.structuredContent);
|
|
503
|
-
bootPre(
|
|
592
|
+
bootPre(originTool, null);
|
|
504
593
|
}
|
|
505
594
|
});
|
|
506
595
|
`;
|
|
@@ -36,7 +36,7 @@ const BODY = `
|
|
|
36
36
|
const SCRIPT = `
|
|
37
37
|
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
38
38
|
el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink((state && state.open_url) || 'https://app.kolbo.ai'); };
|
|
39
|
-
var state = null, pollTimer = null;
|
|
39
|
+
var state = null, pollTimer = null, originArgs = {};
|
|
40
40
|
|
|
41
41
|
function boot(sc) {
|
|
42
42
|
if (!sc) return;
|
|
@@ -83,40 +83,68 @@ function boot(sc) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
var pollStart = 0, pollErrors = 0;
|
|
86
|
-
var MAX_POLL_MS =
|
|
86
|
+
var MAX_POLL_MS = 35 * 60 * 1000, MAX_POLL_ERRORS = 30;
|
|
87
87
|
function poll() {
|
|
88
88
|
if (!pollStart) pollStart = Date.now();
|
|
89
89
|
if ((Date.now() - pollStart) > MAX_POLL_MS) {
|
|
90
|
-
return
|
|
90
|
+
return trackingIssue('This transcription is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
var args = state.status_args || { generation_id: state.generation_id, wait: true };
|
|
93
|
+
window.kolbo.callTool(state.poll_tool || 'get_generation_status', args)
|
|
93
94
|
.then(function (res) {
|
|
94
95
|
var st = structured(res) || {};
|
|
95
96
|
var s = st.state || st.phase;
|
|
96
97
|
if ((res && res.isError) || st.success === false || (st.error && !s)) {
|
|
97
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
98
|
-
pollTimer = setTimeout(poll,
|
|
98
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return trackingIssue(st.error || 'Tracking paused. The transcription may still be running.');
|
|
99
|
+
pollTimer = setTimeout(poll, pollDelay()); return;
|
|
99
100
|
}
|
|
100
101
|
if (s === 'completed') {
|
|
101
102
|
var r = st.result || st;
|
|
102
103
|
boot(Object.assign({}, state, r, { phase: 'completed', credits_used: st.credits_used }));
|
|
103
104
|
} else if (s === 'failed' || s === 'cancelled') {
|
|
104
105
|
boot(Object.assign({}, state, { phase: 'failed', error: st.error }));
|
|
105
|
-
} else { pollErrors = 0; pollTimer = setTimeout(poll,
|
|
106
|
+
} else { pollErrors = 0; pollTimer = setTimeout(poll, pollDelay()); }
|
|
106
107
|
}).catch(function () {
|
|
107
|
-
if (++pollErrors >= MAX_POLL_ERRORS) return
|
|
108
|
-
pollTimer = setTimeout(poll,
|
|
108
|
+
if (++pollErrors >= MAX_POLL_ERRORS) return trackingIssue('Tracking paused after repeated connection errors. The transcription may still be running.');
|
|
109
|
+
pollTimer = setTimeout(poll, pollDelay());
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function pollDelay() {
|
|
114
|
+
// The call itself long-waits server-side; only briefly back off between
|
|
115
|
+
// successive wait windows.
|
|
116
|
+
return 1500;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function trackingIssue(msg) {
|
|
120
|
+
clearTimeout(pollTimer);
|
|
121
|
+
el('phase-chip').style.display = '';
|
|
122
|
+
el('phase-chip').textContent = 'Still working';
|
|
123
|
+
el('stage').classList.remove('k-empty');
|
|
124
|
+
el('stage').innerHTML = '<div class="k-error">' + ICONS.clock + ' ' + esc(msg) + '</div>';
|
|
125
|
+
el('actions').innerHTML = '<button class="k-btn primary" id="transcript-status">' + ICONS.clock + ' Check status</button>';
|
|
126
|
+
el('transcript-status').onclick = function () {
|
|
127
|
+
window.kolbo.sendMessage('Check the existing Kolbo transcription status without retrying it.' +
|
|
128
|
+
(state && state.generation_id ? '\\nGeneration ID: ' + state.generation_id : ''));
|
|
129
|
+
};
|
|
130
|
+
window.kolbo.notifySize();
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
window.kolbo.onToolResult(function (result) {
|
|
113
134
|
var sc = result.structuredContent || structured(result);
|
|
114
135
|
// Only boot on a real widget contract (phase present). A text-JSON
|
|
115
136
|
// fallback like { status: 'submitted' } has no phase and would render a
|
|
116
137
|
// bogus "(empty transcript)" completed view.
|
|
117
138
|
if (sc && (sc.phase || sc.widget === 'transcript')) return boot(sc);
|
|
139
|
+
if (sc && (typeof sc.text === 'string' || sc.srt_url || sc.txt_url)) {
|
|
140
|
+
return boot(Object.assign({}, sc, {
|
|
141
|
+
widget: 'transcript', phase: 'completed',
|
|
142
|
+
audio_url: originArgs.source && /^https?:\\/\\//i.test(originArgs.source) ? originArgs.source : undefined
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
118
145
|
var txt = '';
|
|
119
146
|
try { txt = (result.content || []).filter(function (c) { return c.type === 'text'; }).map(function (c) { return c.text; }).join(' '); } catch (e) {}
|
|
147
|
+
if (/timed out|timeout/i.test(txt)) return trackingIssue((txt || 'Tracking timed out.').slice(0, 300));
|
|
120
148
|
if (result.isError || /error|failed/i.test(txt)) {
|
|
121
149
|
el('phase-chip').style.display = 'none';
|
|
122
150
|
el('stage').classList.remove('k-empty');
|
|
@@ -129,6 +157,7 @@ window.kolbo.onToolResult(function (result) {
|
|
|
129
157
|
if (card) card.style.display = 'none';
|
|
130
158
|
window.kolbo.notifySize();
|
|
131
159
|
});
|
|
160
|
+
window.kolbo.onToolInput(function (args) { originArgs = args || {}; });
|
|
132
161
|
`;
|
|
133
162
|
|
|
134
163
|
function transcriptWidgetHtml() {
|
package/src/index.js
CHANGED
|
@@ -115,7 +115,7 @@ function createServer(opts = {}) {
|
|
|
115
115
|
'3. Misplaced work is fixable: `move_media` / `bulk_move_media` / `move_folder_contents` move media items between projects; `move_session` moves a whole session (plus its media) to another project. If the user says a generation landed in the wrong project, move it rather than regenerating.',
|
|
116
116
|
'4. If the user has not mentioned any project, omit `project_id` — the default bucket is correct in that case. Do not ask which project to use unless the user\'s intent is ambiguous.',
|
|
117
117
|
'5. Written deliverables (plans, briefs, scripts, research summaries) can live in Kolbo too: author them as AI Docs with `create_doc` (project-scoped, editable in the app, shareable via `share_doc`).',
|
|
118
|
-
'6.
|
|
118
|
+
'6. TIMEOUT HANDLING (applies to EVERY generate_* / edit_* / chat_send_message / transcribe_audio tool): each tool blocks and polls internally, then gives up after its own window if the job is not yet terminal. A timeout is NOT a failure — it returns `_timed_out:true` with the `generation_id` (not an error), because the job is almost always STILL RUNNING on the server (or already finished). Call `get_generation_status` with that `generation_id` and `wait=true` to keep checking until state="completed". NEVER conclude the generation failed and re-run the same tool from scratch after a `_timed_out:true` result — that wastes the user\'s credits by paying twice. DIRECTOR / BATCH JOBS follow the same convention through a dedicated tool: generate_creative_director runs its scenes (image OR video) in parallel and only reports state="completed" once EVERY scene is terminal; on `_timed_out:true` call `get_creative_director_status` (not get_generation_status) with the returned generation_id and keep checking. If scenes already carry image_urls/video_urls, they are done; do not regenerate.',
|
|
119
119
|
'7. SESSION CONTINUITY — one task, one session, always: every generation tool returns a `session_id`. For ANY follow-up, refinement, retry, or next step on the SAME task, pass that session_id back — never start fresh. BATCH RULE (critical): when a single user request produces multiple parallel generations (e.g. "animate these 5 images", "generate 3 variants"), do NOT launch them all at once without a session_id. Instead: (1) run the FIRST generation without session_id to create the session, (2) capture the session_id from its response, (3) pass that session_id to ALL remaining generations in the batch. This keeps the entire batch in one session. Exception: only omit session_id and start fresh when the user explicitly starts an unrelated new task.',
|
|
120
120
|
'8. LOCAL FILES / REFERENCE MEDIA — HOW TO HANDLE EVERY CASE: (A) User has a LOCAL file (audio, video, image, document) on their machine: if you have filesystem access (Claude Desktop / Code / IDE / any stdio MCP client) → call `upload_media` with the absolute local path OR pass the path directly to tools like `transcribe_audio` which accept local paths natively. If you have NO filesystem access (claude.ai browser/mobile) → call `media_upload_widget` IMMEDIATELY, an upload card appears, the user uploads, and a `media.kolbo.ai` CDN URL comes back — use that URL for any follow-up tool call. (B) You already have a public URL (media.kolbo.ai, any CDN, any direct link) → pass it directly to the tool. All Kolbo tools accept public URLs. NEVER search for DO Spaces keys, DigitalOcean credentials, or server-side upload credentials. NEVER ask the user to put the file on Google Drive, Dropbox, or Loom. NEVER invent or guess a URL. NEVER base64 a large file — use upload_media instead.',
|
|
121
121
|
'9. MODEL SELECTION: ALWAYS pass a specific `model` on every generation tool — do NOT omit it. Omitting falls back to "Smart Select" auto-routing, which we deliberately avoid because it hides the model choice from the user and often picks a generic default. Choose the model that best fits the task and the user\'s intent (quality, speed, style, capability). If you are unsure which model to use for a given type, call `list_models` with the matching `type` and pick the recommended/flagship one, then pass its `identifier`. Only use Smart Select (omit `model`) if the user EXPLICITLY asks you to auto-pick.',
|
package/src/tools/_shared.js
CHANGED
|
@@ -214,6 +214,45 @@ async function resolveToBuffer(source, kind, opts = {}) {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
// ─── Universal graceful-timeout convention ───────────────────────────────────
|
|
218
|
+
// pollUntilDone throws PollingTimeoutError (err.timedOut === true) when the
|
|
219
|
+
// CLIENT-SIDE poll window elapses — the generation is almost always still
|
|
220
|
+
// running server-side (or already done). Originally only
|
|
221
|
+
// generate_creative_director caught this and returned a non-throwing
|
|
222
|
+
// "_timed_out" result; every other tool let it propagate, so the MCP SDK
|
|
223
|
+
// wrapped it as isError:true and recovery depended entirely on the calling
|
|
224
|
+
// LLM reading hint text. pollOrTimedOut() is the one place that decision now
|
|
225
|
+
// lives — every generation/chat tool routes its pollUntilDone call through
|
|
226
|
+
// it instead of duplicating the try/catch. Genuine failures (state
|
|
227
|
+
// failed/cancelled → GenerationFailedError) are NOT caught here; they still
|
|
228
|
+
// throw and surface as a real tool error.
|
|
229
|
+
const { pollUntilDone } = require('../polling');
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @returns {Promise<{result: object}|{timedOut: object}>}
|
|
233
|
+
* Callers do: `const poll = await pollOrTimedOut(...); if (poll.timedOut) return poll.timedOut; const result = poll.result;`
|
|
234
|
+
*/
|
|
235
|
+
async function pollOrTimedOut(client, generationId, pollOpts) {
|
|
236
|
+
try {
|
|
237
|
+
return { result: await pollUntilDone(client, generationId, pollOpts) };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (!err || !err.timedOut) throw err;
|
|
240
|
+
return {
|
|
241
|
+
timedOut: {
|
|
242
|
+
content: [{
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: JSON.stringify({
|
|
245
|
+
state: 'processing',
|
|
246
|
+
generation_id: generationId,
|
|
247
|
+
_timed_out: true,
|
|
248
|
+
_hint: `Still running on the server after the poll window — this is NOT a failure, no credits were lost. Call get_generation_status with generation_id="${generationId}" (wait=true) to keep checking until state="completed". Do NOT re-run this tool.`
|
|
249
|
+
}, null, 2)
|
|
250
|
+
}]
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
217
256
|
/**
|
|
218
257
|
* Extract real, multiplier-adjusted credit cost from a polled getStatus
|
|
219
258
|
* response. kolbo-api returns `credits_used` (final number deducted) and
|
|
@@ -363,6 +402,19 @@ function buildOpenUrl(tool, gen) {
|
|
|
363
402
|
return url;
|
|
364
403
|
}
|
|
365
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Build the "Open in Kolbo" deep link for a PROJECT. Lands on the Media hub
|
|
407
|
+
* (all-assets view) pre-filtered to this project, where the user sees every
|
|
408
|
+
* generation/media item in it and can switch projects via the in-page selector.
|
|
409
|
+
* The media page reads `?project=<id>` on load and selects it. Returns undefined
|
|
410
|
+
* for a missing id or the default "API Generations" bucket (no useful landing —
|
|
411
|
+
* it is the catch-all, not a real workspace the user navigates to).
|
|
412
|
+
*/
|
|
413
|
+
function buildProjectUrl(projectId, opts = {}) {
|
|
414
|
+
if (!projectId || opts.is_default) return undefined;
|
|
415
|
+
return `${APP_BASE_URL}/media?project=${encodeURIComponent(projectId)}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
366
418
|
// ─── MCP Apps generation widget helpers ──────────────────────────────────────
|
|
367
419
|
// When the host renders MCP Apps (claude.ai via the remote connector, Claude
|
|
368
420
|
// Desktop over stdio), generation tools return IMMEDIATELY after submit and the
|
|
@@ -380,7 +432,7 @@ const { UI, uiResult, appsEnabled, modelIcon } = require('../apps');
|
|
|
380
432
|
* client KolboClient (for model icon lookup)
|
|
381
433
|
* model, prompt, count, settings, reference_image, estimated_seconds
|
|
382
434
|
* poll_tool widget-side status tool (default 'get_generation_status')
|
|
383
|
-
* status_args args for poll_tool (default { generation_id })
|
|
435
|
+
* status_args args for poll_tool (default { generation_id, wait: true })
|
|
384
436
|
*/
|
|
385
437
|
async function uiGenerating(p) {
|
|
386
438
|
// No ETAs anywhere — just a spinner until the poll flips to completed.
|
|
@@ -392,7 +444,10 @@ async function uiGenerating(p) {
|
|
|
392
444
|
tool: p.tool,
|
|
393
445
|
generation_id: p.gen.generation_id,
|
|
394
446
|
poll_tool: p.poll_tool || 'get_generation_status',
|
|
395
|
-
|
|
447
|
+
// Keep at most one long-wait status call in flight per widget. Without
|
|
448
|
+
// wait=true, every open card calls tools/call every few seconds, flooding
|
|
449
|
+
// the host's global progress/context stream and API rate limits.
|
|
450
|
+
status_args: p.status_args || { generation_id: p.gen.generation_id, wait: true },
|
|
396
451
|
model: p.model || 'Smart Select',
|
|
397
452
|
model_icon: icon,
|
|
398
453
|
prompt: p.prompt,
|
|
@@ -446,11 +501,13 @@ module.exports = {
|
|
|
446
501
|
guessFilename,
|
|
447
502
|
guessContentType,
|
|
448
503
|
resolveToBuffer,
|
|
504
|
+
pollOrTimedOut,
|
|
449
505
|
creditFields,
|
|
450
506
|
projectIdField,
|
|
451
507
|
projectScopeReadField,
|
|
452
508
|
inlineImageBlocks,
|
|
453
509
|
buildOpenUrl,
|
|
510
|
+
buildProjectUrl,
|
|
454
511
|
uiGenerating,
|
|
455
512
|
uiCompleted,
|
|
456
513
|
appsEnabled,
|
package/src/tools/chat.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
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 {
|
|
8
|
-
const { creditFields, projectIdField } = require('./_shared');
|
|
7
|
+
const { pollOrTimedOut, creditFields, projectIdField } = require('./_shared');
|
|
9
8
|
|
|
10
9
|
function registerChatTools(server, client) {
|
|
11
10
|
// ─── chat_send_message ─────────────────────────────────────
|
|
@@ -40,10 +39,12 @@ function registerChatTools(server, client) {
|
|
|
40
39
|
// extra time when web_search is on (may fetch + analyze multiple pages).
|
|
41
40
|
const timeout = deep_think ? 600000 : (web_search ? 240000 : 120000);
|
|
42
41
|
|
|
43
|
-
const
|
|
42
|
+
const poll = await pollOrTimedOut(client, gen.message_id, {
|
|
44
43
|
interval: (gen.poll_interval_hint || 2) * 1000,
|
|
45
44
|
timeout
|
|
46
45
|
});
|
|
46
|
+
if (poll.timedOut) return poll.timedOut;
|
|
47
|
+
const result = poll.result;
|
|
47
48
|
|
|
48
49
|
// Chat status shape (from extractResult in kolbo-api sdk/controller.js):
|
|
49
50
|
// { content, reasoning_content, image_urls?, video_urls?, audio_urls?, model, created_at }
|