@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 +3 -14
- package/package.json +5 -3
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +16 -28
- package/skill/VERSION +1 -1
- package/skill/references/models/music.md +1 -1
- package/skill/references/workflows/media-library.md +29 -1
- package/skill/references/workflows/troubleshooting.md +13 -0
- package/src/client.js +60 -6
- package/src/index.js +32 -3
- package/src/install.js +35 -3
- package/src/polling.js +7 -0
- package/src/progress.js +55 -0
- package/src/tools/_shared.js +137 -11
- package/src/tools/artifacts.js +23 -5
- package/src/tools/chat.js +3 -3
- package/src/tools/generate.js +44 -24
- package/src/tools/media.js +104 -12
- package/src/tools/models.js +55 -3
- package/src/tools/moodboards.js +1 -1
- package/src/tools/music_library.js +31 -4
- package/src/tools/presets.js +56 -17
- package/src/tools/projects.js +1 -1
- package/src/tools/stock_library.js +23 -3
- package/src/tools/visual_dna.js +9 -5
- package/src/tools/voices.js +10 -1
- package/skill/references/models/voice-tts.md +0 -85
- package/skill/references/workflows/app-builder.md +0 -160
- package/src/tools/app_builder.js +0 -253
- package/src/tools/shorts_creator.js +0 -404
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 (
|
|
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.
|
|
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",
|
package/skill/GENERATED.md
CHANGED
|
@@ -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@
|
|
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
|
+
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)
|
|
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,
|
|
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
|
|
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).
|
|
107
|
-
| `generate_speech` | TTS. Use `list_voices` to pick a voice
|
|
108
|
-
| `generate_sound` | Sound effects.
|
|
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,
|
|
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
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
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.
|
|
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.
|
|
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"`
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/progress.js
ADDED
|
@@ -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 };
|