@kolbo/mcp 1.53.1 → 1.55.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 CHANGED
@@ -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 (101)
116
+ ## Available Tools (117)
117
117
 
118
118
  **Generation**
119
119
  | Tool | Description |
@@ -123,7 +123,7 @@ Without the optional skill, the config block alone already exposes every tool
123
123
  | `generate_video` | Text → video |
124
124
  | `generate_video_from_image` | Still image + motion prompt → video |
125
125
  | `generate_video_from_video` | Input video → restyled video, or burn in subtitles (video-to-video). `prompt` optional — prompt-less models (VEED Subtitles, Act Two, Wan Animate) use `preset` / `source_language` / `translation_language`, plus `srt_content` / `srt_file_url` / `vocabulary` / `customization` for VEED |
126
- | `generate_elements` | Reference images/videos + prompt → animated video |
126
+ | `generate_elements` | Reference images/videos/audio + prompt → animated video |
127
127
  | `generate_first_last_frame` | First frame + last frame → interpolated video |
128
128
  | `generate_lipsync` | Source image/video + audio → lipsynced video (Sync-3 adds active-speaker selection, emotion, model mode, temperature) |
129
129
  | `generate_creative_director` | One brief → N coordinated scenes (image or video) |
@@ -175,6 +175,7 @@ Every generation tool also accepts an optional `project_id` arg that routes the
175
175
  | Tool | Description |
176
176
  |------|-------------|
177
177
  | `media_upload_widget` | Open an in-chat upload card so claude.ai users can upload LOCAL files (image / video / audio / document) — chat attachments are unreachable from remote MCP, so this is the way to bring them in. Returns stable CDN URLs |
178
+ | `create_upload_ticket` | Get a short-lived upload ticket and POST local files yourself — no upload card, no user interaction. For agents with shell access (Claude Code, Codex, Cursor, CI) talking to Kolbo over a remote connector |
178
179
  | `upload_media` | Upload a local file (path or URL), or inline `source_base64` + `filename`, → stable Kolbo CDN URL for reuse |
179
180
  | `list_media` | Browse media library — filter by `project_id`, `folder_id`, `type`, `category` (ai / uploaded / edited / favorites / training-lab), `source_type`, `sort`, `search`, pagination |
180
181
  | `list_media_folders` | List the user's media folders (owned + shared) — discover `folder_id` values to pass to `list_media` |
@@ -227,17 +228,6 @@ Every generation tool also accepts an optional `project_id` arg that routes the
227
228
  | `analyze_script_for_stock` | AI: turn a script into b-roll search terms (`queries[]`, `mediaType`, `keywords`). |
228
229
  | `import_stock_asset` | Copy a stock asset into the media library (CDN copy, stable URL). Free. |
229
230
 
230
- **Shorts Creator** (long video → viral vertical shorts, two-phase)
231
- | Tool | Description |
232
- |------|-------------|
233
- | `shorts_analyze` | Phase 1: analyze a long video (Kolbo media-library URL, ≤30 min) → AI-picked best moments with titles, hooks, scores, accent beats. Flat 15 credits. Polls until moments are ready (~1-3 min). |
234
- | `shorts_list_presets` | List restyle presets (identifier, name, preview video, default mode/subtitle style). |
235
- | `shorts_get_transcript` | Word-level Scribe transcript of the source video (`words`, `language`, `sourceDuration`) — the base for the Review & Edit workflow (build `delete_ranges` cuts + edited `srt_content`). |
236
- | `shorts_estimate` | Price a selection before rendering — free. Per-short credits + chunk counts. `delete_ranges` cuts shorten the effective duration (cheaper). |
237
- | `shorts_render` | Phase 2: render up to 5 shorts (15-90s each) from picked moments — `accents` mode (restyle strongest beats, cheaper) or `full` (restyle everything), optional burned-in subtitles. Per short: optional `delete_ranges` (cut dead air, absolute source seconds, ≥8s must remain) and `srt_content` (user-edited SRT, cut-timeline times, ≤200KB). Polls until done (~5-20 min), returns final URLs. Failed shorts auto-refund. |
238
- | `shorts_status` | One-shot job state read (moments / shorts / phase) — resume after a timeout. |
239
- | `shorts_cancel` | Cancel a job and refund unused credits. |
240
-
241
231
  **Discovery & Account**
242
232
  | Tool | Description |
243
233
  |------|-------------|
@@ -253,7 +243,6 @@ Every generation tool also accepts an optional `project_id` arg that routes the
253
243
  | `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
254
244
  | `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Custom chat agents (reusable named personas; `description` is the system instruction) |
255
245
  | `get_creative_director_status` | Re-check a Creative Director batch by generation_id until all parallel scenes finish (use after a `_timed_out` Director run) |
256
- | `app_builder_list_projects` / `app_builder_create_session` / `app_builder_generate_app` / `app_builder_edit_app` / `app_builder_get_build_status` / `app_builder_get_session` / `app_builder_list_sessions` / `app_builder_list_generations` / `app_builder_delete_session` | **App Builder (preview)** — full React app generation that auto-provisions a GitHub repo + Supabase DB + a live deployment URL. Different surface from the generation tools (4-layer mental model: project → session → app → end-users). The app embeds `@kolbo/app-sdk` so visitors can call Kolbo AI from the browser; the OWNER pays for visitor AI usage. Read the App Builder workflow doc before the first turn. |
257
246
  | `list_sessions` | Enumerate sessions across all types, filterable by project and type |
258
247
  | `add_project_context` / `list_project_context` / `delete_project_context` / `get_project_profile` / `regenerate_project_profile` | Project knowledge base (RAG): feed scripts/URLs/notes, read the synthesized living brief |
259
248
  | `create_moodboard` / `update_moodboard` / `delete_moodboard` | Build/edit moodboards from image URLs (AI style analysis → master prompt) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.53.1",
3
+ "version": "1.55.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": {
@@ -10,8 +10,10 @@
10
10
  "start": "node src/index.js",
11
11
  "smoke": "node scripts/smoke.js",
12
12
  "check-parity": "node scripts/check-parity.js",
13
- "prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js && node scripts/check-widget-fields.js",
14
- "check-widget-fields": "node scripts/check-widget-fields.js"
13
+ "prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js && node scripts/check-widget-fields.js && node scripts/check-skill-tools.js && node scripts/check-install.js",
14
+ "check-widget-fields": "node scripts/check-widget-fields.js",
15
+ "check-skill-tools": "node scripts/check-skill-tools.js",
16
+ "check-install": "node scripts/check-install.js"
15
17
  },
16
18
  "keywords": [
17
19
  "kolbo",
@@ -1,7 +1,7 @@
1
1
  # AUTO-GENERATED — do not edit
2
2
 
3
3
  This skill/ tree is mirrored from kolbo-code (the single source of truth)
4
- by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@d70e982.
4
+ by .github/workflows/sync-skill-to-plugin.yml — synced from kolbo-code@8a08c7e.
5
5
 
6
6
  It is the skill that 'npx @kolbo/mcp install' deploys into the user's agent.
7
7
  To change it, edit packages/opencode/skills/kolbo/ in kolbo-code and push;
package/skill/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 0.7.2
2
+ version: 0.7.5
3
3
  name: kolbo
4
4
  description: |
5
5
  Generate, edit, or analyze creative media via the Kolbo AI MCP server:
@@ -8,14 +8,14 @@ description: |
8
8
  consistency), Marketing Studio (UGC + DTC ads + product photoshoot +
9
9
  marketplace cards), Creative Director (multi-scene batches), HTML artifact
10
10
  publishing (presentations, landing pages, dashboards), AI Docs (project
11
- documents you author and share), and the App Builder.
11
+ documents you author and share).
12
12
 
13
13
  Use when the user wants to generate, create, make, edit, animate, or
14
14
  transcribe media: images, video, music, voice/TTS, sound effects, 3D models,
15
15
  UGC or TV-spot ads, product / lifestyle / hero shots, Amazon or marketplace
16
- listings, presentations, landing pages, dashboards, or 'build me an app';
16
+ listings, presentations, landing pages, or dashboards;
17
17
  to reuse a character or brand (Visual DNA, brand kits); or to save a written
18
- plan / brief / script / research doc into their Kolbo project (AI Docs).
18
+ plan / brief / script / research into a Kolbo project (AI Docs).
19
19
 
20
20
  NOT for: video editing / FFmpeg (use video-production), motion graphics
21
21
  (use remotion-best-practices), code editing, or general chat.
@@ -83,7 +83,6 @@ For multi-scene / batch work this pairs with `generate_creative_director` (see b
83
83
  | **Transcribe** or **analyze** audio/video | `references/workflows/transcription.md` |
84
84
  | **Scrape brand/product info** before generating + persist as `.kolbo/brand-kits/<slug>.md` | `references/workflows/research-first.md` |
85
85
  | Browse, manage, or present existing **media library** items | `references/workflows/media-library.md` |
86
- | Use the **App Builder** (React app generation) — preview, ask first | `references/workflows/app-builder.md` |
87
86
  | Confirm **cost** or validate **resolution / aspect / duration** against model caps | `references/workflows/cost-and-validation.md` |
88
87
  | Hit an **auth / MCP / 429** issue | `references/workflows/troubleshooting.md` |
89
88
 
@@ -103,21 +102,23 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
103
102
  | `generate_elements` | Reference-driven video. **Primary route for DNA → video.** |
104
103
  | `generate_first_last_frame` | Keyframe interpolation between two frames. |
105
104
  | `generate_lipsync` | Lipsync audio to an image or video face. |
106
- | `generate_music` | Music generation (Suno + variants). Controls: `style`, `title`, `lyrics`, `instrumental`, `vocal_gender`, `negative_tags`, `duration_seconds`, plus Suno fine-controls (`style_weight`, `weirdness`, `audio_weight`, `persona_id`, `singing_dna_id`/`singing_voice_id`). |
107
- | `generate_speech` | TTS. Use `list_voices` to pick a voice, then steer delivery with per-provider style/emotion controls — see `references/models/voice-tts.md`. |
108
- | `generate_sound` | Sound effects. Controls: `duration`, `prompt_influence`, plus provider-specific `cfg_strength` (Stable Audio), `sound_loop`/`sound_tempo`/`sound_key` (Kie), and Seed-Audio `seed_*` params. |
105
+ | `generate_music` | Music generation (Suno + variants). |
106
+ | `generate_speech` | TTS. Use `list_voices` to pick a voice. |
107
+ | `generate_sound` | Sound effects. |
109
108
  | `generate_3d` | 3D models from text / single image / multi-view. Returns GLB/FBX/OBJ/USDZ. |
110
109
 
111
- ### Discovery, Library, Visual DNA, Moodboards, Chat, App Builder, Publishing
110
+ ### Discovery, Library, Visual DNA, Moodboards, Chat, Publishing
112
111
  | Tool | Purpose |
113
112
  |------|---------|
114
- | `list_models` / `list_voices` / `check_credits` / `get_generation_status` / `get_session_usage` | Discovery + status |
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` |
113
+ | `list_models` / `list_voices` / `check_credits` / `get_generation_status` / `cancel_generation` / `get_session_usage` | Discovery + status. `list_models` with no args returns the recommended shortlist out of ~428 — pass `type` for a full category with per-model caps. `cancel_generation` stops an in-flight job and refunds what it can: use it when the user changes their mind mid-generation instead of letting it run. |
114
+ | `upload_media` / `create_upload_ticket` / `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`. Getting a LOCAL file in depends on where the server runs: `upload_media` with a path only works on a local (stdio) install; over a remote connector use `create_upload_ticket` and POST the file yourself. |
116
115
  | `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
116
  | `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. |
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. |
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 "ProjectsWhere Work Lands" below. |
117
+ | `list_color_palettes` / `analyze_color_palette` / `create_color_palette` / `update_color_palette` / `delete_color_palette` / `activate_color_palette` / `deactivate_color_palette` | **Color DNA sticky and account-wide.** At most one palette is active at a time; while it is, it strict-grades **every** image and video generation automatically, with no per-call argument. `analyze_color_palette` pulls colors out of 1-5 image URLs for free and does NOT save. `create_color_palette` defaults `is_active: true`, which activates it and deactivates any other. Per-generation opt-out: `skip_color_palette: true` on `generate_image` / `generate_image_edit` / `generate_video` / `generate_video_from_image`. |
118
+ | `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Custom chat agents — reusable named personas for `chat_send_message`. The agent's `description` IS the system instruction. Resolve a name the user mentions ("use my SEO agent") to an id with `list_agents`, then pass `agent_id`. Global/preset agents are read-only; only the user's own can be updated or deleted. |
119
+ | `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. The older `*_music_library` tools are deprecated adapters over this prefer the stock tools, except for the licensed-catalog tools in the next row. |
120
+ | `search_music_library` / `browse_music_library` / `get_music_library_facets` / `get_music_track_audio` / `get_music_track_lyrics` / `get_music_track_related` / `analyze_script_for_music` / `acquire_clean_music_track` / `import_music_track_to_library` | **SYNCI licensed music** — a commercially licensed catalog, not free stock. Discovery and previews are free but **watermarked**; there is no unwatermarked URL until you pay. `acquire_clean_music_track` (or `import_music_track_to_library`, which also copies it to the media library) **CHARGES CREDITS** for the clean master — confirm with the user first, and pass a stable `requestId` so a retry doesn't buy it twice. `analyze_script_for_music` turns a script into search terms for `search_music_library`. Use this family when the user needs music cleared for commercial use; use `search_stock_media` with `mediaType: "music"` when free stock will do. |
121
+ | `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. See "Projects — Where Work Lands" below. |
121
122
  | `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. |
122
123
  | `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 |
123
124
  | `create_moodboard` / `update_moodboard` / `delete_moodboard` | Moodboards from image URLs → AI master style prompt → pass `moodboard_id` to generation tools |
@@ -125,7 +126,6 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
125
126
  | `trim_video` | Frame-accurate trim of a Kolbo-hosted video (tool waits and returns the URL). `edit_video` also gained `remove_background`. |
126
127
  | `create_doc` / `list_docs` / `get_doc` / `update_doc` / `share_doc` / `delete_doc` | AI Docs (Magic Pad): YOU author full HTML documents (plans, briefs, scripts, research) saved into the user's project, editable in the Kolbo app. `share_doc` returns a public link. `update_doc` content replaces the WHOLE doc — `get_doc` first. |
127
128
  | `chat_send_message` / `chat_list_conversations` / `chat_get_messages` | Kolbo chat with optional `media_urls` (up to 10 per call) |
128
- | `app_builder_*` (9 tools) | Full React app generation (preview) — see `workflows/app-builder.md`. Different surface from generation tools: produces a deployed app with GitHub repo + Supabase DB + live URL. Don't confuse `session_id` types, don't confuse `app_builder_generate_app` with `generate_image`. |
129
129
  | `publish_html_artifact` | Publish HTML / SVG / Mermaid to `sites.kolbo.ai`. Server dedupes by content hash. Strict CSP. |
130
130
 
131
131
  ## ⚠️ If the User Names a Tool, USE THAT TOOL (HARD RULE)
@@ -163,7 +163,6 @@ A user-named tool — in any language — overrides every other rule. Recognized
163
163
  - User named one → use it. Model identifiers resolve leniently — shorthand like `"z-image"` or `"nano banana 2"` auto-resolves to the exact identifier, so don't over-engineer exact-id lookups (`list_models` is still authoritative for constraints, caps, and pricing).
164
164
  - Auto-select → only from "Auto-selectable" section (models with a `summary`). Cheapest fit. Prefer `[RECOMMENDED]` when cost is similar.
165
165
  - Never auto-select from "Named-only" section.
166
- - **ALWAYS pass an explicit `model` — never omit it.** Omitting routes to the backend "Smart Select" auto-router, which we deliberately avoid: it hides the model choice and the generation used to show just "Auto" instead of the model that ran. YOU pick the specific model (steps above); only omit `model` when the user explicitly asks for auto-pick, or for chat media analysis (video/audio `media_urls`, which routes to Gemini vision).
167
166
  4. **Validate inputs** against model caps — see `references/workflows/cost-and-validation.md`.
168
167
  5. **How calls work**: each tool blocks until generation is fully complete. Images: seconds. Video: minutes. Multiple tool calls in one response run concurrently. On hosts with live widgets the tool instead returns `submitted` instantly — the card updates on its own; you only need `get_generation_status` when a follow-up step needs the output URLs.
169
168
  6. **Checking status — NEVER poll in a loop**: `get_generation_status` takes `wait=true` (blocks server-side until done, ~3 min) and `generation_ids` (check MANY generations in ONE call — returns `all_done` + which are still running). One `wait=true` call replaces any polling loop. If it comes back with some still processing, call it ONCE more with `wait=true` and the remaining ids.
@@ -177,19 +176,8 @@ Everything in Kolbo — sessions, generations, media, docs — lives inside a PR
177
176
 
178
177
  1. **User names a project** ("in my Acme project", "for the film") → call `list_projects` ONCE to resolve the name to an ObjectId, then pass that id as `project_id` on **EVERY** subsequent `generate_*` / `upload_media` / `create_doc` / `chat_send_message` call in the conversation. It is **per-call, NOT sticky** — any call that omits it silently lands in the default "API Generations" bucket (`is_default: true`).
179
178
  2. **No project mentioned** → omit `project_id`; the default bucket is correct. Don't ask unless intent is ambiguous.
180
- 3. **`list_projects` `app_builder_list_projects`** both return the SAME Kolbo projects (different endpoints, different shapes). Use `list_projects` for generation/media/chat flows; use `app_builder_list_projects` when scoping an App Builder session. Never substitute one for the other.
181
- 4. **Work landed in the wrong project? MOVE it, never regenerate**: `move_session` relocates a whole session + all its media (works for any session type — the `session_id` from generation responses, chats, transcriptions); `move_media` / `bulk_move_media` / `move_folder_contents` relocate individual media items.
179
+ 3. **Work landed in the wrong project? MOVE it, never regenerate**: `move_session` relocates a whole session + all its media (works for any session type — the `session_id` from generation responses, chats, transcriptions); `move_media` / `bulk_move_media` / `move_folder_contents` relocate individual media items.
182
180
 
183
- ## 🧱 App Builder ≠ regular Kolbo project (4-layer mental model)
184
-
185
- App Builder is a separate surface that builds and runs full React apps. It shares the **Kolbo project** layer with regular generations (a single Kolbo project can hold BOTH app-builder sessions AND image/video sessions), but every layer above it is distinct. The single biggest routing mistake is treating an `app_builder_generate_app` call as a media-generation call, or passing an App Builder `session_id` to `generate_image`. Read `workflows/app-builder.md` before the first App Builder turn — the four layers are:
186
-
187
- 1. **Kolbo project** (shared) — `list_projects` / `app_builder_list_projects`
188
- 2. **App Builder session** — `app_builder_create_session(project_id)` → returns a `session_id` distinct from any chat/generation session id
189
- 3. **The app itself** — `deployment_url` + `github_repo_url` + `supabase_url` + `supabase_anon_key` (auto-provisioned on first successful build)
190
- 4. **App end-users** — per-app JWT, hit `/api/apps/:appId/ai/*`; the OWNER of the app pays for their AI usage (NOT surfaced as MCP tools — owned by `@kolbo/app-sdk` in the deployed bundle)
191
-
192
- Heuristic: deliverable is a **single file** (image / video / doc / artifact) → regular tools. Deliverable is "**an app** that does X" or "my **users** can do Y" → App Builder. App Builder is **preview** — don't proactively advertise; if the user asks, load `workflows/app-builder.md` and confirm they're an opted-in owner.
193
181
 
194
182
  ## Cost Awareness — Quick Rules
195
183
 
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.2
1
+ 0.7.5
@@ -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"` 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.
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"` (semantic vibe query "tense cinematic pulse", "uplifting corporate background") → `get_stock_asset` for download URLs. Free, no credits. The older `*_music_library` tools are deprecated adapters over the stock library prefer the stock tools.
12
12
 
13
13
  ## CRITICAL Kolbo Platform Rules
14
14
 
@@ -32,11 +32,39 @@ When you produce a media file LOCALLY — `ffmpeg` via the `video-production` sk
32
32
 
33
33
  5. **Files served via `file://` won't render inline** in the chat as `<video>` / `<img>` — the desktop WebView blocks file:// for security. Don't try to embed; just link.
34
34
 
35
+ ## Local files — pick by where the SERVER runs, not by which client you are
36
+
37
+ A tool call executes on the Kolbo server. Handing it `C:\Users\...` or `/Users/...`
38
+ gives it a *string*, not the bytes — so a local path only resolves when server and
39
+ client share a filesystem. Choose by transport:
40
+
41
+ | Situation | Call |
42
+ |---|---|
43
+ | **Local (stdio) install** — `npx @kolbo/mcp` on the same machine | `upload_media` with the absolute path |
44
+ | **Remote connector + you can run shell commands** (Claude Code, Codex, Cursor, CI) | `create_upload_ticket`, then POST each file to `upload_url` |
45
+ | **Remote connector, no filesystem** (claude.ai web/mobile) | `media_upload_widget` — the user picks the file |
46
+
47
+ `create_upload_ticket` returns `upload_url` + a short-lived `token`. Upload with
48
+ multipart field `file` and `Authorization: Bearer <token>`; the stable CDN URL comes
49
+ back at `media.url`. One POST per file, ticket reusable for a batch. Then pass those
50
+ URLs to any generation tool.
51
+
52
+ ```bash
53
+ curl -X POST "<upload_url>" -H "Authorization: Bearer <token>" -F "file=@/abs/path/clip.mp3"
54
+ ```
55
+
56
+ Do **not** fall back to `upload_media`'s `source_base64` for anything but a tiny file —
57
+ it pushes the whole file through the model's context, twice. And do not reach for
58
+ cloud credentials or a bucket of your own; the ticket is the sanctioned path.
59
+
60
+ On Windows, give `curl` a native `C:/Users/...` path — a Git Bash `/c/Users/...` path
61
+ fails to open (exit 26).
62
+
35
63
  ## Routing — user says → call
36
64
 
37
65
  | User says | Call |
38
66
  |---|---|
39
- | "Upload this file" / "host this" / "give me a public URL for this" | `upload_media` |
67
+ | "Upload this file" / "host this" / "give me a public URL for this" | `upload_media` — but see "Local files" below if it's a path on the user's disk |
40
68
  | "Show my media" / "list my images/videos" / "what do I have?" | `list_media` (pass `type` / `category` / `project_id` / `folder_id` / `search`) |
41
69
  | "Show my favorites" / "list starred items" | `list_media` with `category=favorites` |
42
70
  | "List everything in project X" | `list_media` with `project_id=X` |
@@ -30,6 +30,19 @@ This re-wires the MCP configuration automatically. Then restart the session.
30
30
 
31
31
  Wait 60s for the window to reset, retry only the failed calls. For batch image work prefer `generate_creative_director` over multiple `generate_image` calls. Full rate-limit details + retry sequence: see SKILL.md "Rate Limiting & Batch Generation".
32
32
 
33
+ ## Every generation comes back with an unexpected colour cast
34
+
35
+ An active **Color DNA** palette is the usual cause. It is sticky and account-wide:
36
+ once activated it strict-grades every image and video generation, with no
37
+ per-call argument and nothing in the prompt to hint at it — so the user
38
+ experiences it as "all my images suddenly look brown" long after they set it.
39
+
40
+ Call `list_color_palettes` and look for `is_active: true`. Then either
41
+ `deactivate_color_palette` (clears it for everything) or pass
42
+ `skip_color_palette: true` on the single generation. Don't try to counteract the
43
+ grade by writing colours into the prompt — the palette is applied after, and the
44
+ prompt loses.
45
+
33
46
  ## Checking generation status without spinning
34
47
 
35
48
  `get_generation_status` supports `wait=true` (blocks server-side until the generation reaches a final state, up to ~3 min) and `generation_ids` (many ids in one call → returns `all_done`, `still_processing`, and per-generation results). **Never call it repeatedly in a loop** — one `wait=true` call replaces the loop. If some generations are still running after the wait window, call it ONCE more with `wait=true` and only the `still_processing` ids.
package/src/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
+ const progress = require('./progress');
4
5
 
5
6
  /**
6
7
  * Kolbo API HTTP client wrapper
@@ -26,6 +27,26 @@ class KolboApiError extends Error {
26
27
  }
27
28
  }
28
29
 
30
+ // Per-REQUEST ceilings. These are not the generation timeouts — a long job is a
31
+ // poll LOOP of many short requests, and pollUntilDone only checks its own
32
+ // deadline BETWEEN polls. So an individual request that never settles hangs the
33
+ // whole tool straight past its declared window, with no error, forever. Bound
34
+ // each request instead; the loop keeps its own budget.
35
+ const REQUEST_TIMEOUT_MS = Number(process.env.KOLBO_HTTP_TIMEOUT_MS) || 120000;
36
+ // Uploads legitimately run long — up to 25MB/file over a slow uplink.
37
+ const UPLOAD_TIMEOUT_MS = Number(process.env.KOLBO_UPLOAD_TIMEOUT_MS) || 600000;
38
+
39
+ // AbortSignal.timeout is Node 18+; package engines already require >=18.
40
+ function timeoutSignal(ms) {
41
+ return typeof AbortSignal !== 'undefined' && AbortSignal.timeout
42
+ ? AbortSignal.timeout(ms)
43
+ : undefined;
44
+ }
45
+
46
+ function isAbortError(err) {
47
+ return err && (err.name === 'TimeoutError' || err.name === 'AbortError');
48
+ }
49
+
29
50
  // ---------------------------------------------------------------------------
30
51
  // Partner / whitelabel resolution (mirrors CLI's brand/partner.ts)
31
52
  // ---------------------------------------------------------------------------
@@ -263,7 +284,23 @@ class KolboClient {
263
284
  options.body = JSON.stringify(body);
264
285
  }
265
286
 
266
- const response = await fetch(url, options);
287
+ options.signal = timeoutSignal(REQUEST_TIMEOUT_MS);
288
+
289
+ let response;
290
+ try {
291
+ response = await fetch(url, options);
292
+ } catch (err) {
293
+ if (isAbortError(err)) {
294
+ throw new KolboApiError(
295
+ `Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s: ${method} ${reqPath}. ` +
296
+ 'The job may still be running server-side — poll get_generation_status before retrying. ' +
297
+ 'Raise KOLBO_HTTP_TIMEOUT_MS if this is a legitimately slow endpoint.',
298
+ { code: 'REQUEST_TIMEOUT', status: 504 }
299
+ );
300
+ }
301
+ throw err;
302
+ }
303
+
267
304
  let data;
268
305
  try {
269
306
  data = await response.json();
@@ -296,6 +333,10 @@ class KolboClient {
296
333
  });
297
334
  }
298
335
 
336
+ const generationId = data?.generation_id || data?.generationId;
337
+ if (method === 'POST' && generationId) {
338
+ await progress.generation(generationId);
339
+ }
299
340
  return data;
300
341
  }
301
342
 
@@ -346,11 +387,24 @@ class KolboClient {
346
387
  const body = formData.getBuffer();
347
388
  headers['Content-Length'] = String(body.length);
348
389
 
349
- const response = await fetch(url, {
350
- method: 'POST',
351
- headers,
352
- body
353
- });
390
+ let response;
391
+ try {
392
+ response = await fetch(url, {
393
+ method: 'POST',
394
+ headers,
395
+ body,
396
+ signal: timeoutSignal(UPLOAD_TIMEOUT_MS)
397
+ });
398
+ } catch (err) {
399
+ if (isAbortError(err)) {
400
+ throw new KolboApiError(
401
+ `Upload timed out after ${UPLOAD_TIMEOUT_MS / 1000}s: POST ${reqPath} ` +
402
+ `(${Math.round(body.length / 1024)}KB). Raise KOLBO_UPLOAD_TIMEOUT_MS for slow links.`,
403
+ { code: 'UPLOAD_TIMEOUT', status: 504 }
404
+ );
405
+ }
406
+ throw err;
407
+ }
354
408
 
355
409
  let data;
356
410
  try {
package/src/index.js CHANGED
@@ -59,6 +59,7 @@
59
59
  const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
60
60
  const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
61
61
  const KolboClient = require('./client');
62
+ const { LOCAL_FILE_ROUTING, attachFileInputHints } = require('./tools/_shared');
62
63
  const { registerGenerateTools } = require('./tools/generate');
63
64
  const { registerModelTools } = require('./tools/models');
64
65
  const { registerChatTools } = require('./tools/chat');
@@ -109,7 +110,7 @@ function createServer(opts = {}) {
109
110
  // The single most common failure mode is project confusion — spell out
110
111
  // the project contract here so every client gets it without a skill file.
111
112
  instructions: [
112
- 'LOCAL FILES: never upload a user file yourself with cloud credentials, a shell command, or an S3/Spaces script — Kolbo owns this. Over a remote connector (claude.ai / Claude Desktop) the server cannot see local paths at all: call `media_upload_widget` so the user picks the file, then pass the returned https:// URL to the generation/transcription tool. Use `upload_media` only when the path IS reachable from where the MCP server runs (local stdio installs). `list_media` returns URLs for assets already in the library.',
113
+ 'LOCAL FILES: never upload a user file with your own cloud credentials, an S3/Spaces script, or a third-party host — Kolbo owns this. ' + LOCAL_FILE_ROUTING,
113
114
  'PROMPT CONVENTIONS (Kolbo-specific — these change the OUTPUT, not just the metadata):',
114
115
  'A. Visual DNA: passing `visual_dna_ids` is not enough — every DNA in play must ALSO be tagged inside the prompt text as `@Name`, using the DNA name (e.g. "@Kobi walks into frame"). Moodboards are referenced the same way with `#Name`. Resolve names via `list_visual_dnas` / `list_moodboards`.',
115
116
  'B. The full Kolbo skill is available to you as MCP RESOURCES under `kolbo://skill/`. Read `kolbo://skill/SKILL.md` first — it is the core rules plus a routing index — then read the matching `kolbo://skill/references/...` file before writing prompts for a specific model or workflow (per-model prompt rules, Visual DNA workflow, Creative Director, marketing, cost validation). Do this instead of guessing; the references exist precisely because the rules differ per model.',
@@ -122,11 +123,20 @@ function createServer(opts = {}) {
122
123
  '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`).',
123
124
  '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.',
124
125
  '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.',
125
- '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.',
126
+ '8. LOCAL FILES / REFERENCE MEDIA — HOW TO HANDLE EVERY CASE. (A) User has a LOCAL file (audio, video, image, document) on their machine. What matters is WHERE THIS SERVER RUNS, not what your client can do — your own filesystem access is irrelevant if the server is somewhere else. On a LOCAL stdio install (server and client share a machine) → call `upload_media` with the absolute path, or pass the path straight to tools like `transcribe_audio` that accept local paths. Over a REMOTE connector the server cannot see that path no matter how capable you are, so a local path will always fail: if you can run shell commands or issue HTTP requests → call `create_upload_ticket` and POST the file to the returned upload_url yourself (fastest, no user interaction); if you cannot → call `media_upload_widget` IMMEDIATELY, the user uploads, and a `media.kolbo.ai` CDN URL comes back for any follow-up call. (B) You already have a public URL (media.kolbo.ai, any CDN, any direct link) → pass it directly; 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 anything but a tiny file — it costs context in proportion to file size; use the ticket or the widget instead.',
126
127
  '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.',
127
- '10. IMAGE EDITING: for ANY prompt-driven / content edit of an existing image — "make it night", changing scene/lighting/colors, adding/removing/replacing objects, restyling — use `generate_image_edit` (it runs on strong dedicated editing models, same as image generation). Do NOT use `edit_image` for content edits — `edit_image` is ONLY for mechanical enhancements (upscale, reframe, remove-background, skin retouch). Its `magic_edit` operation is deprecated in favor of `generate_image_edit`.'
128
+ '10. IMAGE EDITING: for ANY prompt-driven / content edit of an existing image — "make it night", changing scene/lighting/colors, adding/removing/replacing objects, restyling — use `generate_image_edit` (it runs on strong dedicated editing models, same as image generation). Do NOT use `edit_image` for content edits — `edit_image` is ONLY for mechanical enhancements (upscale, expand/outpaint, remove-background, skin retouch). Its `magic_edit` operation is deprecated in favor of `generate_image_edit`. EXPANDING AN IMAGE: to widen/extend/uncrop an image or fit it into a wider frame while KEEPING the existing artwork, use `edit_image` with operation="zoom_out" (outpainting — original pixels preserved; size it with `zoom_out_percentage` or the `expand_left/right/top/bottom` pixel args). The "reframe" operation is NOT this: it re-generates the whole picture at a new aspect ratio and the subject comes back re-imagined. Only pick "reframe" when the user wants the shot re-taken, never when they want their image extended.'
128
129
  ].join('\n')
129
130
  });
131
+ const progress = require('./progress');
132
+ const tool = server.tool.bind(server);
133
+ server.tool = (...args) => {
134
+ const index = args.length - 1;
135
+ const handler = args[index];
136
+ if (typeof handler !== 'function') return tool(...args);
137
+ args[index] = (params, extra) => progress.run(extra, () => handler(params, extra));
138
+ return tool(...args);
139
+ };
130
140
 
131
141
  // Register all tools. `inlineImages` (off by default) is opt-in: only the
132
142
  // remote HTTP host enables it, so stdio clients (Kolbo Code / Desktop / Cursor)
@@ -155,6 +165,11 @@ function createServer(opts = {}) {
155
165
  // Serve skill/ as standard MCP resources so connector clients — which never
156
166
  // run `npx @kolbo/mcp install` — can still read the operating guidance.
157
167
  registerSkillResources(server);
168
+ // Every media-input tool advertises the local-file route that works on THIS
169
+ // transport. Without it, a remote-connector model reads "absolute local path",
170
+ // sees no filesystem, and tells the user Kolbo cannot accept their file —
171
+ // the single most-reported failure, despite the upload tools existing.
172
+ attachFileInputHints(server, toolOptions);
158
173
  // Declaration-level `_meta['ui/resourceUri']` on every widget-carrying tool —
159
174
  // claude.ai prepares the widget iframe from tools/list, not from the result.
160
175
  attachToolWidgetMeta(server);
@@ -165,6 +180,20 @@ function createServer(opts = {}) {
165
180
  async function main() {
166
181
  const server = createServer();
167
182
 
183
+ // Node kills the process on an unhandled rejection / uncaught exception. In a
184
+ // long-lived stdio server that is not a stack trace the user ever sees — the
185
+ // host just reports "MCP server disconnected", mid-conversation, with the
186
+ // generation still running server-side. A tool error is recoverable; a dead
187
+ // process is not, so log to stderr (stdout is the JSON-RPC channel) and stay
188
+ // up. Only the stdio entrypoint does this — an embedding host (kolbo-api)
189
+ // keeps its own process semantics.
190
+ process.on('unhandledRejection', (err) => {
191
+ console.error('[kolbo-mcp] unhandled rejection (server staying up):', err);
192
+ });
193
+ process.on('uncaughtException', (err) => {
194
+ console.error('[kolbo-mcp] uncaught exception (server staying up):', err);
195
+ });
196
+
168
197
  // Start the server with stdio transport
169
198
  const transport = new StdioServerTransport();
170
199
  await server.connect(transport);
package/src/install.js CHANGED
@@ -29,7 +29,12 @@ function targets() {
29
29
  }
30
30
  return [
31
31
  { name: 'Claude Desktop', file: desktop, restart: 'Fully quit and reopen Claude Desktop' },
32
- { name: 'Claude Code', file: path.join(home, '.claude', 'settings.json'), restart: 'Restart Claude Code' },
32
+ // Claude Code reads user-scope MCP servers from ~/.claude.json NOT from
33
+ // ~/.claude/settings.json. settings.json accepts an mcpServers key without
34
+ // complaint and Claude Code ignores it completely, so writing there made
35
+ // `install` report success while the user got no Kolbo tools at all.
36
+ // Verified with `claude mcp list`: only ~/.claude.json entries load.
37
+ { name: 'Claude Code', file: path.join(home, '.claude.json'), restart: 'Restart Claude Code', probeDir: path.join(home, '.claude') },
33
38
  { name: 'Cursor', file: path.join(home, '.cursor', 'mcp.json'), restart: 'Restart Cursor' },
34
39
  ];
35
40
  }
@@ -38,8 +43,11 @@ function configure(t) {
38
43
  const dir = path.dirname(t.file);
39
44
  const fileExists = fs.existsSync(t.file);
40
45
  // Only touch an app that looks installed (its config file or parent dir exists)
41
- // so we don't create configs for apps the user doesn't have.
42
- if (!fileExists && !fs.existsSync(dir)) return { ...t, status: 'not found' };
46
+ // so we don't create configs for apps the user doesn't have. `probeDir` exists
47
+ // for configs that live directly in $HOME (~/.claude.json) dirname there is
48
+ // the home dir, which always exists, so it would "detect" every app.
49
+ const probe = t.probeDir || dir;
50
+ if (!fileExists && !fs.existsSync(probe)) return { ...t, status: 'not found' };
43
51
 
44
52
  let cfg = {};
45
53
  if (fileExists) {
@@ -86,6 +94,20 @@ function installSkill(t) {
86
94
  }
87
95
  }
88
96
 
97
+ // Versions before this fix wrote the Claude Code entry into
98
+ // ~/.claude/settings.json, where Claude Code silently ignores it. Anyone who ran
99
+ // those is left with a dead entry that makes it look configured. Detect and say
100
+ // so rather than editing a file we no longer own.
101
+ function staleClaudeCodeEntry() {
102
+ const f = path.join(os.homedir(), '.claude', 'settings.json');
103
+ try {
104
+ const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
105
+ return cfg.mcpServers && cfg.mcpServers.kolbo ? f : null;
106
+ } catch (_) {
107
+ return null;
108
+ }
109
+ }
110
+
89
111
  async function run() {
90
112
  const out = (s = '') => process.stdout.write(s + '\n');
91
113
  const results = targets().map(configure);
@@ -114,6 +136,16 @@ async function run() {
114
136
  return 0;
115
137
  }
116
138
 
139
+ const stale = staleClaudeCodeEntry();
140
+ if (stale) {
141
+ out(' Note: an older Kolbo entry is sitting in');
142
+ out(` ${stale}`);
143
+ out(' Claude Code does not read MCP servers from that file, so it never did');
144
+ out(' anything. Safe to delete the "kolbo" key there; the real one is now in');
145
+ out(` ${path.join(os.homedir(), '.claude.json')}`);
146
+ out();
147
+ }
148
+
117
149
  out(' Done — no API key needed.');
118
150
  if (newlyConfigured.length) {
119
151
  const steps = [...new Set(newlyConfigured.map((r) => r.restart))];
package/src/polling.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * Poll a generation until it reaches a terminal state
3
3
  */
4
4
 
5
+ const progress = require('./progress');
6
+
5
7
  class PollingTimeoutError extends Error {
6
8
  constructor(generationId, timeoutMs) {
7
9
  const seconds = Math.round(timeoutMs / 1000);
@@ -77,6 +79,7 @@ async function pollUntilDone(client, generationId, options = {}) {
77
79
  throw err;
78
80
  }
79
81
  const backoff = Math.min(interval * Math.pow(1.5, Math.min(transientFailures - 1, 5)), 30000);
82
+ await progress.tick(); // same keepalive as the normal path — backoff waits up to 30s
80
83
  await new Promise((resolve) => setTimeout(resolve, backoff));
81
84
  continue;
82
85
  }
@@ -96,6 +99,10 @@ async function pollUntilDone(client, generationId, options = {}) {
96
99
  throw new GenerationFailedError(generationId, 'generation was cancelled');
97
100
  }
98
101
 
102
+ // Still running: put a byte on the wire before going quiet again, so no
103
+ // intermediary mistakes a 3-minute wait for a dead connection.
104
+ await progress.tick();
105
+
99
106
  // Wait before next poll
100
107
  await new Promise(resolve => setTimeout(resolve, interval));
101
108
  }
@@ -0,0 +1,55 @@
1
+ const { AsyncLocalStorage } = require('async_hooks');
2
+
3
+ const storage = new AsyncLocalStorage();
4
+
5
+ function run(extra, fn) {
6
+ return storage.run(extra, fn);
7
+ }
8
+
9
+ async function generation(id) {
10
+ const extra = storage.getStore();
11
+ const token = extra?._meta?.progressToken;
12
+ if (!id || token === undefined) return;
13
+ // A progress notification is a courtesy — the generation is already submitted
14
+ // and paid for. If the send fails (host cancelled the request, transport
15
+ // already closed), swallowing it is mandatory: letting it reject would turn a
16
+ // running generation into a tool error and make the user pay twice.
17
+ try {
18
+ await sendGenerationProgress(extra, token, id);
19
+ } catch (_) { /* notification-only; never fail the tool */ }
20
+ }
21
+
22
+ /**
23
+ * Keepalive. A tool that waits out a long generation holds one request open and
24
+ * puts NOTHING on the wire for up to 3 minutes (get_generation_status wait=true
25
+ * is a GET, so not even the submit notification fires). Intermediaries that
26
+ * treat a silent connection as a dead one — corporate proxies, VPNs, some load
27
+ * balancers — hang up, and the user reports it as "Kolbo disconnected". Every
28
+ * poll iteration ticks, so the connection is never quiet for longer than one
29
+ * poll interval (5-15s). No timer to leak: the poll loop IS the clock.
30
+ */
31
+ async function tick() {
32
+ const extra = storage.getStore();
33
+ const token = extra?._meta?.progressToken;
34
+ if (token === undefined) return;
35
+ extra.__kolboProgress = (extra.__kolboProgress || 0) + 1;
36
+ try {
37
+ await extra.sendNotification({
38
+ method: 'notifications/progress',
39
+ params: { progressToken: token, progress: extra.__kolboProgress }
40
+ });
41
+ } catch (_) { /* keepalive only; never fail the tool */ }
42
+ }
43
+
44
+ async function sendGenerationProgress(extra, token, id) {
45
+ await extra.sendNotification({
46
+ method: 'notifications/progress',
47
+ params: {
48
+ progressToken: token,
49
+ progress: 0,
50
+ message: JSON.stringify({ generation_id: id })
51
+ }
52
+ });
53
+ }
54
+
55
+ module.exports = { run, generation, tick };