@kolbo/mcp 1.70.4 → 1.71.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -248,7 +248,8 @@ Every generation tool also accepts an optional `project_id` arg that routes the
248
248
  | `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
249
249
  | `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Custom chat agents (reusable named personas; `description` is the system instruction) |
250
250
  | `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) |
251
- | `list_sessions` | Enumerate sessions across all types, filterable by project and type |
251
+ | `list_sessions` | Enumerate sessions across all types, filterable by project, `type`, and `types[]` |
252
+ | `rename_session` / `delete_session` / `restore_session` | Rename a session; soft-delete leftovers after a move; restore from trash |
252
253
  | `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 |
253
254
  | `create_moodboard` / `update_moodboard` / `delete_moodboard` | Build/edit moodboards from image URLs (AI style analysis → master prompt) |
254
255
  | `clone_voice` / `import_elevenlabs_voice` / `delete_voice` | Custom voices: clone from an audio sample, import by ElevenLabs ID, delete |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.70.4",
3
+ "version": "1.71.1",
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": {
@@ -1,6 +1,6 @@
1
1
  # AUTO-GENERATED — do not edit
2
2
 
3
- This tree is mirrored from kolbo-code@9e4903a, the single source of truth.
3
+ This tree is mirrored from kolbo-code@c5b2132, the single source of truth.
4
4
  Canonical source: packages/opencode/skills/kolbo/
5
5
  Distribution: .github/workflows/sync-skill-to-plugin.yml
6
6
 
package/skill/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 0.8.2
2
+ version: 0.8.4
3
3
  name: kolbo
4
4
  description: |
5
5
  Generate, edit, analyze, and direct creative media through Kolbo AI: images,
@@ -58,8 +58,8 @@ For multi-scene / batch work this pairs with `generate_creative_director` (see b
58
58
  | If the user wants to… | Read first |
59
59
  |---|---|
60
60
  | Direct, develop, audit, or continue a **film / episode / connected scene / complex performance** with continuity, acting, dialogue, music, blocking, or physics | `references/workflows/filmmaking.md` |
61
- | Generate a **Seedance 2.5** video | `references/models/seedance25.md`; also load `references/workflows/filmmaking.md` for narrative, performance, or cross-shot continuity |
62
- | Generate a **Seedance 2 / 2.0** video | `references/models/seedance.md` |
61
+ | Generate a **Seedance 2.5** video | `references/models/seedance25.md` + Locked Intro in `references/models/seedance.md`. For narrative/continuity also load `references/workflows/filmmaking.md` but compile the prompt as Locked Intro, NOT the SCENE CONTEXT / OPTICS / ACTION pack |
62
+ | Generate a **Seedance 2 / 2.0** video **or Elements** (`generate_elements`) | `references/models/seedance.md` — same Locked Intro. Elements is NOT a different prompt language |
63
63
  | Generate a **GPT Image 2** image | `references/models/gpt-image.md` |
64
64
  | Generate a **Nano Banana / Gemini** image | `references/models/nano-banana.md` |
65
65
  | Generate a **Veo 3 / 3.1** video | `references/models/veo.md` |
@@ -94,7 +94,7 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
94
94
  | `generate_video` | Text-to-video. Does **not** support Visual DNA — use `generate_elements` for character-consistent video. |
95
95
  | `generate_video_from_image` | Animate a still. Prompt describes motion, not subject. |
96
96
  | `generate_video_from_video` | Restyle/transform an existing video. Keeps original motion. |
97
- | `generate_elements` | Reference-driven video. **Primary route for DNA → video.** |
97
+ | `generate_elements` | Reference-driven video. **Primary route for DNA → video.** Prompt = Seedance Locked Intro (`Total` + `[GLOBAL LOOK]` / `[CAST]` / `[LOCATION]` + `SHOT N`). Every DNA in `visual_dna_ids` must also be `@Name` in that prompt. |
98
98
  | `generate_first_last_frame` | Keyframe interpolation between two frames. |
99
99
  | `generate_lipsync` | Lipsync audio to an image or video face. |
100
100
  | `generate_music` | Music generation (Suno + variants). |
@@ -114,7 +114,8 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
114
114
  | `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. |
115
115
  | `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. |
116
116
  | `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. |
117
- | `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. |
117
+ | `create_project` / `update_project` / `archive_project` / `unarchive_project` / `list_sessions` / `rename_session` / `delete_session` / `restore_session` | Project lifecycle + session inventory. `list_sessions` returns `project_id` + `types[]` on every row. Soft-delete leftover empty sessions after a move; `restore_session` undoes trash. Create a project when the user starts new work, then pass its id on EVERY call. |
118
+ | `bulk_move_sessions` / `list_session_generations` / `move_generations_to_session` / `split_session` / `undo_session_organization` | Reorganize many sessions or generations. `list_session_generations` is an inventory (not a live generation card). |
118
119
  | `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 |
119
120
  | `create_moodboard` / `update_moodboard` / `delete_moodboard` | Moodboards from image URLs → AI master style prompt → pass `moodboard_id` to generation tools |
120
121
  | `clone_voice` / `import_elevenlabs_voice` / `delete_voice` | Custom voices (clone CHARGES CREDITS — confirm first; new voices show in `list_voices`) |
@@ -123,6 +124,25 @@ Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/sr
123
124
  | `chat_send_message` / `chat_list_conversations` / `chat_get_messages` | Kolbo chat with optional `media_urls` (up to 10 per call) |
124
125
  | `publish_html_artifact` | Publish HTML / SVG / Mermaid to `sites.kolbo.ai`. Server dedupes by content hash. Strict CSP. |
125
126
 
127
+ ## ⚠️ Visual DNA `@Name` in the prompt (HARD RULE — always on)
128
+
129
+ Passing `visual_dna_ids` is **not enough**. For every DNA in that array you MUST also write `@ExactStoredName` in the prompt text (the `name` from `list_visual_dnas` / `create_visual_dna`). The engine binds identity by parsing `@tags`. No `@tag` → the DNA is wasted.
130
+
131
+ - Right: `visual_dna_ids: ["vdna_…"]` + prompt `@Zohar walks into frame`
132
+ - Wrong: `Zohar's`, `Zohar`, `the left man`, `the man on the LEFT`, `Visual DNA anchors: the man on the LEFT…` — none of these bind
133
+ - Never invent a role label or possessive as a substitute for `@Name`
134
+ - Same rule for moodboards: `#ExactBoardName`
135
+
136
+ Resolve names with `list_visual_dnas` first. Full binding rules: `references/workflows/visual-dna.md`.
137
+
138
+ ## ⚠️ Seedance / Elements prompt contract (HARD RULE)
139
+
140
+ `generate_elements`, Seedance 2, and Seedance 2.5 share **one** compile shape — the Locked Intro in `references/models/seedance.md`:
141
+
142
+ `Total: Xs / N shots / AR` → `[GLOBAL LOOK – LOCKED, APPLIES TO EVERY SHOT]` → `[CAST – IDENTICAL IN EVERY SHOT]` (each person is `@DNAName`) → `[LOCATION]` → `SHOT N — 0:00–0:02 — …`
143
+
144
+ Do **not** default Elements to `SCENE CONTEXT` / `OPTICS` / `ACTION` / `ACTIVE REFERENCES` department packs (those live in filmmaking audit/contracts for other models). Do not load `seedance-2-prompting` SCENE CONTEXT as the Elements format.
145
+
126
146
  ## ⚠️ If the User Names a Tool, USE THAT TOOL (HARD RULE)
127
147
 
128
148
  A user-named tool — in any language — overrides every other rule. Recognized aliases:
@@ -172,9 +192,9 @@ Model types for `list_models`: `text_to_img`, `image_editing`, `text_to_video`,
172
192
 
173
193
  Everything in Kolbo — sessions, generations, media, docs — lives inside a PROJECT. Getting this wrong is the #1 user complaint ("my work went to the wrong project").
174
194
 
175
- 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`). Accounts often hold hundreds of projects, so pass `list_projects({ search: "acme" })` rather than listing everything; the list is paginated (50/page) and hides archived projects unless you pass `include_archived: true`.
176
- 2. **No project mentioned** → omit `project_id`; the default bucket is correct. Don't ask unless intent is ambiguous.
177
- 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.
195
+ 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 **same** id as `project_id` on **EVERY** subsequent `generate_*` / `upload_media` / `create_doc` / `chat_send_message` call in **this conversation**. There is no server-side sticky store omitting it on any later call silently lands in the default "API Generations" bucket (`is_default: true`). Once resolved, treat that id as required for the rest of the conversation. Accounts often hold hundreds of projects, so pass `list_projects({ search: "acme" })` rather than listing everything; the list is paginated (50/page) and hides archived projects unless you pass `include_archived: true`.
196
+ 2. **No project mentioned** → omit `project_id`; the default bucket is correct. Don't ask unless intent is ambiguous. If `list_sessions` already returned a `project_id` for the work you are continuing, keep passing that id.
197
+ 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. Empty leftover sessions after a move: `delete_session` (soft-delete; `restore_session` undoes it). `rename_session` only changes the sidebar title.
178
198
 
179
199
 
180
200
  ## Cost Awareness — Quick Rules
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.2
1
+ 0.8.4
@@ -9,13 +9,16 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
9
9
 
10
10
  **Kolbo MCP routing:** Seedance is a video model — call `generate_video` (text-to-video) or `generate_elements` (when video references / Visual DNA / first-last frames are involved). Run `list_models({ type: "text_to_video" })` and pick a Seedance variant by name.
11
11
 
12
- ## Universal Rules (apply to EVERY Seedance prompt)
12
+ **Elements uses this same file.** `generate_elements` is not a second prompt language. Do not write `SCENE CONTEXT` / `OPTICS` / `ACTION` department packs for Elements or Seedance — those are filmmaking audit contracts, not the generation compile shape.
13
13
 
14
+ ## Universal Rules (apply to EVERY Seedance / Elements prompt)
15
+
16
+ - **Visual DNA names are immutable anchors:** when `visual_dna_ids` is passed, every DNA MUST appear in the prompt as the exact literal `@DNA_name` (CAST + every shot it is in). Never "Zohar's", "the left man", "the man on the LEFT", a nickname, or a Visual DNA anchors paragraph without `@tags`.
14
17
  - **First line ALWAYS declares shot structure**: total duration, shot count, aspect ratio. Example: `Total: 15s / 6 shots / 16:9`. Put it at the BOTTOM of the prompt too. For connected narrative sequences the proven phrasing is `N connected cinematic shots, 15 seconds total, 16:9, Multishot ON` — use it and keep `Multishot ON` for any multi-shot story.
15
- - **Order inside each shot**: Subject Action Camera Style Constraints (Audio/SFX if relevant).
16
- - **Prompt length**: aim for ~120–280 words TOTAL across all shots combined (not per shot). Shorter than ~120 words = random output. Longer risks the 8000-char cap below and makes the model forget the opening. For 6-shot prompts, keep each shot 1–2 tight sentences.
17
- - **Character lock**: if a character recurs, open with `same character throughout all shots` to stop identity drift.
18
- - **Max 3 shots per single-shot prompt; max 6 shots in a multi-shot montage.** More causes drift.
18
+ - **Then the Locked Intro** `[GLOBAL LOOK]` / `[CAST]` / `[LOCATION]` before any shot. A one-liner `same character throughout` is not a character lock.
19
+ - **Order inside each shot**: Subject Action Camera Constraints (Audio/SFX if relevant). Do NOT restack GLOBAL LOOK style inside the shot.
20
+ - **Prompt length**: simple single-idea pieces ~120–280 words. Locked-intro cinematic typically 400–900 words. Shorter than ~120 words = random output. The 8000-char cap below always wins.
21
+ - **Shot count is user-directed.** If the user asks for N shots, deliver exactly N in one prompt unless they ask to split.
19
22
  - **Always describe at least one camera movement per shot.**
20
23
  - **Tell Seedance what the camera is NOT doing** (e.g. `no cuts, no zoom, natural head movement`) — this is what locks POV.
21
24
  - **Final prompt is always English**, wrapped in a copy-ready code block. Detect intent in any language and reply in the user's language, but the prompt itself is English.
@@ -26,6 +29,32 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
26
29
  - **Never** split into multiple prompts, multiple code blocks, or "part 1 / part 2" to evade the cap.
27
30
  - Before outputting, internally count the characters of the final prompt as a single string. If > 8000, rewrite tighter and re-count. Repeat until ≤ 8000. Only then show the user.
28
31
 
32
+ ## Locked Intro (DEFAULT for any multi-shot cinematic — including Elements)
33
+
34
+ After the Total line, every multi-shot prompt — and any piece with recurring people or a recurring place — opens with three locked blocks. Skip only for: true single-shot POV/orb, 3×3 grid-panel mode, or video-edit tasks.
35
+
36
+ ```
37
+ Total: Xs / N shots / AR
38
+ N connected cinematic shots, Xs total, AR, Multishot ON
39
+
40
+ [GLOBAL LOOK – LOCKED, APPLIES TO EVERY SHOT]
41
+ <body>, <lens family>, <film stock>, <aspect> spherical, <stop>. <DoF, grain, grade as law>. <movement grammar>. <performance + audio law>.
42
+
43
+ [CAST – IDENTICAL IN EVERY SHOT]
44
+ @Exact_DNA_name: age, build, hair, face, wardrobe, signature details.
45
+ PROP: recurring object.
46
+
47
+ [LOCATION]
48
+ Place in materials + light + color field. Blocking. Background LIFE.
49
+
50
+ SHOT 1 — 0:00–0:02 — Medium / camera position
51
+ (physical verbs, timed acting, quoted dialogue)
52
+
53
+ Total: Xs / N shots / AR
54
+ ```
55
+
56
+ When a Visual DNA exists, its exact `@DNA_name` IS the cast name — never place a nickname before it or substitute one later. For plain image refs use `@ImageN`.
57
+
29
58
  ## The 5 Formats
30
59
 
31
60
  ### 1. Transformations (highest-performing format)
@@ -62,16 +91,13 @@ Load this file when the user wants a **Seedance 2 / Seedance 2.0** (ByteDance) v
62
91
 
63
92
  ### 6. Reference-Anchored Cinematic Sequence (multi-character / named references — highest-fidelity format)
64
93
 
65
- Use whenever the user gives named characters or multiple reference images (`@Image1`, `@Image2`, …) — a tactical unit clearing a bunker, a duel between two referenced characters, a war scene. **This is always an Elements-mode prompt** (route the card to `elements`). Structure:
94
+ Use whenever the user gives named characters, Visual DNAs, or multiple reference images (`@Image1`, `@Image2`, …). **This is always an Elements-mode prompt** (`generate_elements`). Structure:
66
95
 
67
- 1. **Labeled scene header FIRST** (grounds the scene before any shot):
68
- - `Time of day:`hour + light quality + atmosphere (dust, haze, heavy silence before action).
69
- - `Location:` — the environment in concrete physical detail (materials, wear, light direction, high-contrast blown-out entrance, etc.).
70
- - `Characters:`ONE line per person: `Name @ImageN wardrobe, position in frame, what they carry`. End with "All must match their character references exactly."
71
- 2. **REFERENCE CONSISTENCY block** — map every reference and pin what must NOT change: `Reference Image 1 is <X>. Preserve exact face, hair, anatomy, wardrobe, colors, props.` Add per-character energy/aura color rules, and any already-established story state (e.g. "the gem is already shattered — no intact gem, no red glow"). End with "Do not redesign, morph, recolor, or swap either character, their clothing, anatomy, weapons, or the environment."
72
- 3. **Shots** — either titled (`Shot 1 — Medium Wide / Tactical Positioning`) or timecoded (`SHOT 1 — 0:00–0:03`); timecodes must sum to the total duration. Under each shot use **Camera → Action → Audio** in that order.
73
- 4. **Continuity** — to chain a series, open with `Begin as a seamless continuation from <the exact last beat of the previous video>.`
74
- 5. Close with whichever **Power Blocks** below actually apply (this format usually warrants all three; a simpler scene may need only AUDIO).
96
+ 1. **Locked Intro FIRST** (GLOBAL LOOK / CAST / LOCATION). CAST is one line per person: `@Exact_DNA_name` or `Name @ImageN — wardrobe, position, what they carry`. End CAST with "All must match their character references exactly." Never "the man on the LEFT" without an `@tag`.
97
+ 2. **REFERENCE CONSISTENCY block**map every reference and pin what must NOT change: `@Zohar` / `Reference Image 1 is <X>. Preserve exact face, hair, anatomy, wardrobe, colors, props.`
98
+ 3. **Shots** — timecoded `SHOT N 0:00–0:03`. Re-use the same `@DNA_name` in every shot that person appears. Camera Action Audio.
99
+ 4. **Continuity**ONLY when the user will attach the previous video / last frame as an input. Otherwise the prompt is STANDALONE.
100
+ 5. Close with whichever **Power Blocks** below actually apply.
75
101
 
76
102
  ## Power Blocks (CONDITIONAL — add ONLY the ones the shot actually needs; never pad a simple prompt)
77
103
 
@@ -93,7 +119,7 @@ These elevate rich cinematic / reference-anchored sequences. For a short, tight,
93
119
 
94
120
  ## Universal Craft Layer (apply on top of any format above)
95
121
 
96
- > This is the universal film-direction layer that lifts every prompt above the boilerplate. **Deep-dive reference:** `~/.kolbo/skills/seedance-2-prompting/SKILL.md` (Craft Editionfull block structure, every optical technique, and the pre-flight checklist).
122
+ > Craft layer for observables (FOV degrees, km/h, Kelvin). Do **not** switch the compile shape to that skill's SCENE CONTEXT / OPTICS / ACTION packLocked Intro above is the only default for Seedance and Elements.
97
123
 
98
124
  ### Core principle
99
125
 
@@ -226,4 +252,4 @@ Seedance 2 lives in the **Video** category. Route the prompt card by the INPUTS:
226
252
 
227
253
  ## Seedance + Visual DNA / References
228
254
 
229
- When a character must stay consistent, pair Seedance with Visual DNA via `generate_elements` (NOT `generate_video` — text-to-video silently drops `visual_dna_ids`). Tag the DNA inside the prompt with `@<dna-name>`see `workflows/visual-dna.md`. For grid/storyboard inputs, the source frame is `@image1`.
255
+ When a character must stay consistent, pair Seedance with Visual DNA via `generate_elements` (NOT `generate_video` — text-to-video silently drops `visual_dna_ids`). Use the exact literal `@DNA_name` as the character name in CAST and re-anchor every shot where it appears never a nickname, alias, possessive (`Zohar's`), or spatial label (`the left man`). See `workflows/visual-dna.md`. For grid/storyboard inputs, the source frame is `@image1`.
@@ -9,6 +9,8 @@ Load this file when the user wants a **Seedance 2.5** video (they said "2.5" / "
9
9
 
10
10
  **Kolbo MCP routing:** `generate_video` or `generate_elements` (refs / Visual DNA / first-last). Run `list_models({ type: "text_to_video" })` and pick the Seedance 2.5 variant by name.
11
11
 
12
+ **Audio:** Seedance 2.5 still emits real synced audio. `list_models` may show `sound_generation_type: none` because there is no in-app toggle (`sound_baked_in: true`). Do not tell the user the model is silent.
13
+
12
14
  ## What's NEW in 2.5 (verified — never hedge)
13
15
 
14
16
  - **Duration 4–30 seconds**, whole seconds. 30s IS supported.
@@ -62,4 +64,4 @@ Skip CORE STYLE / SUBJECT / ENVIRONMENT — the three locked blocks already own
62
64
 
63
65
  ## Where to run in Kolbo
64
66
 
65
- Same routing as Seedance 2 (`first_last_frame` / `elements` / `image_to_video` / `text_to_video`). Pair Visual DNA with `generate_elements` and tag `@<dna-name>` inside the prompt.
67
+ Same routing as Seedance 2 (`first_last_frame` / `elements` / `image_to_video` / `text_to_video`). Pair Visual DNA with `generate_elements` and write the exact `@DNA_name` in CAST and every shot — never "Zohar's" / "the left man" / an untagged "Visual DNA anchors" paragraph. Elements uses this same Locked Intro; do not compile SCENE CONTEXT / OPTICS / ACTION packs.
@@ -93,27 +93,19 @@ Before writing, establish:
93
93
 
94
94
  Prompt-length limits apply to the entire compiled generation prompt as one string, including whitespace, headers, timecodes, dialogue, audio, and locks. Count after compilation. For the current Seedance 2.5 adapter snapshot, the hard ceiling is 30,000 characters; never borrow that number for another model.
95
95
 
96
- For a serious controlled shot, prefer this logical order, omitting irrelevant blocks:
96
+ **Seedance 2 / Seedance 2.5 / `generate_elements` — Locked Intro is the only compile shape.** Read `references/models/seedance.md` (and `seedance25.md` for 2.5 caps). Do not emit the SCENE CONTEXT / OPTICS / ACTION department pack below as the generation prompt. Every Visual DNA in play must be `@ExactName` in CAST and in each shot never "the left man" or a possessive.
97
97
 
98
98
  ```text
99
- SCENE CONTEXT
100
- ACTIVE REFERENCES
101
- LOCATION MAP
102
- FIRST FRAME AND SPATIAL BLOCKING
103
- FORMAT MODE
104
- OPTICS
105
- CAMERA
106
- ACTION TIMING / SHOTS
107
- PHYSICS
108
- LIGHTING
109
- ACTING TASKS
110
- DIALOGUE
111
- AUDIO / MUSIC
112
- STYLE
113
- POSITIVE LOCKS
99
+ Total: Xs / N shots / AR
100
+ [GLOBAL LOOK – LOCKED, APPLIES TO EVERY SHOT]
101
+ [CAST – IDENTICAL IN EVERY SHOT] ← @DNAName per person
102
+ [LOCATION]
103
+ SHOT N — 0:00–0:02 — …
114
104
  ```
115
105
 
116
- For anchored or exploratory work, collapse compatible blocks and protect only non-negotiables. Never manufacture a rigid skeleton when a looser model-native prompt is more likely to succeed.
106
+ The SCENE CONTEXT pack in `prompt-contracts.md` is an **audit / pre-compile checklist** for non-Seedance models and Workbench diagnosis not the default Elements prompt.
107
+
108
+ For anchored or exploratory work on other models, collapse compatible blocks and protect only non-negotiables. Never manufacture a rigid skeleton when a looser model-native prompt is more likely to succeed.
117
109
 
118
110
  ## Preserve continuity
119
111
 
@@ -81,7 +81,8 @@ Whenever a generation call passes `visual_dna_ids` (even just one), the prompt M
81
81
  **Use the actual stored name, programmatically.** When you call `list_visual_dnas` (or `create_visual_dna`), read the `name` field off the response and use that exact string after the `@`. Do NOT:
82
82
 
83
83
  - Translate the name into another language ("אסתר" / "esther" / "אסתי" — pick whichever string is in `name` and use ONLY that one).
84
- - Invent a friendlier alias ("the model", "המודל", "her").
84
+ - Invent a friendlier alias ("the model", "המודל", "her", "Zohar's", "the left man", "the man on the LEFT").
85
+ - Write a "Visual DNA anchors:" prose block that describes position/wardrobe but never writes `@ExactName`.
85
86
  - Write the character's name in plain text without the `@` prefix.
86
87
  - Drop the `@name` when only one DNA is passed — the engine still needs the binding so it knows the DNA is the *subject* and not a passive style.
87
88
 
package/src/apps/index.js CHANGED
@@ -465,7 +465,10 @@ const TOOL_WIDGETS = {
465
465
  media_upload_widget: UI.upload,
466
466
  // generic list widget — flat record lists with no natural thumbnail
467
467
  list_projects: UI.list,
468
+ // Must stay list.html — mapping this to generation.html mounts "Kolbo Generation /
469
+ // Preparing" empty cards for every session row.
468
470
  list_sessions: UI.list,
471
+ list_session_generations: UI.list,
469
472
  list_project_context: UI.list,
470
473
  list_agents: UI.list,
471
474
  list_docs: UI.list,
@@ -91,8 +91,40 @@ function makeExpandable(node) {
91
91
  }
92
92
  }
93
93
 
94
+ function renderList(sc) {
95
+ state = sc;
96
+ el('tool-title').textContent = sc.title || 'List';
97
+ el('prompt').style.display = 'none';
98
+ el('chips').innerHTML = '';
99
+ el('credits').textContent = '';
100
+ var items = sc.items || [];
101
+ var total = sc.total != null ? sc.total : items.length;
102
+ setPhaseChip(total + (total === 1 ? ' item' : ' items'), false);
103
+ if (!items.length) {
104
+ el('stage').innerHTML = '<div class="k-empty">Nothing here yet</div>';
105
+ } else {
106
+ el('stage').innerHTML = items.slice(0, 40).map(function (item) {
107
+ return '<div class="k-audio-row"><div class="k-audio-meta"><div class="k-audio-title">' +
108
+ esc(item.title || 'Untitled') + '</div>' +
109
+ (item.subtitle ? '<div class="k-audio-sub">' + esc(item.subtitle) + '</div>' : '') +
110
+ '</div>' +
111
+ (item.badge ? '<span class="k-chip" style="flex:none">' + esc(item.badge) + '</span>' : '') +
112
+ '</div>';
113
+ }).join('');
114
+ }
115
+ window.kolbo.notifySize();
116
+ }
117
+
118
+ function isListPayload(sc, toolName) {
119
+ if (!sc && /^list_/.test(toolName || '')) return true;
120
+ if (!sc) return false;
121
+ if (sc.widget === 'list' || sc.widget === 'catalog' || sc.widget === 'media-grid') return sc.widget === 'list';
122
+ return Array.isArray(sc.items) && !sc.phase && !sc.generation_id;
123
+ }
124
+
94
125
  function boot(sc) {
95
126
  if (!sc) return;
127
+ if (isListPayload(sc, sc.tool)) return renderList(sc);
96
128
  state = sc;
97
129
  el('tool-title').textContent = TOOL_TITLES[sc.tool] || 'Generation';
98
130
  el('prompt').textContent = sc.prompt || '';
@@ -800,6 +832,13 @@ function bootPre(toolName, args) {
800
832
  if (toolName) originTool = toolName;
801
833
  if (args) originArgs = args;
802
834
  if (state) return; // real data already arrived
835
+ if (/^list_/.test(toolName || '')) {
836
+ el('tool-title').textContent = toolName === 'list_sessions' ? 'Sessions' : 'List';
837
+ setPhaseChip('Loading', true);
838
+ el('stage').innerHTML = '';
839
+ el('prompt').style.display = 'none';
840
+ return;
841
+ }
803
842
  el('tool-title').textContent = TOOL_TITLES[toolName] || 'Generation';
804
843
  if (args && (args.prompt || args.text || (Array.isArray(args.prompts) && args.prompts.length))) {
805
844
  el('prompt').textContent = args.prompt || args.text ||
package/src/index.js CHANGED
@@ -118,8 +118,8 @@ function createServer(opts = {}) {
118
118
  '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.',
119
119
  'PROJECT CONTRACT (read this before generating anything):',
120
120
  'Everything in Kolbo lives inside a PROJECT — sessions, generations, and media are all project-scoped.',
121
- '1. When the user names a project ("in my Acme project", "for the summer campaign"), call `list_projects` ONCE to resolve the name to an id, then pass that id as `project_id` on EVERY subsequent generate_* / chat_send_message / upload_media call in the conversation. The target project is per-call, NOT sticky — any call that omits `project_id` silently lands in the default "API Generations" bucket (flagged is_default:true), which users experience as their work going to the wrong project.',
122
- '2. `list_projects` lists the user\'s platform projects (for generations/media/chat). Do not confuse a generation `session_id` with any other session type — they are not interchangeable.',
121
+ '1. When the user names a project ("in my Acme project", "for the summer campaign"), call `list_projects` ONCE to resolve the name to an id, then pass that SAME id as `project_id` on EVERY subsequent generate_* / chat_send_message / upload_media / create_doc call in THIS conversation. There is no server-side sticky store omitting `project_id` on any later call silently lands in the default "API Generations" bucket (flagged is_default:true). Once resolved, treat that id as required for the rest of the conversation.',
122
+ '2. `list_projects` lists the user\'s platform projects (for generations/media/chat). `list_sessions` returns `project_id` on every row — echo that id on follow-up generate/chat/upload calls for work in that session. Do not confuse a generation `session_id` with a project id; they are not interchangeable. Empty leftover sessions after `move_session` can be removed with `delete_session` (soft-delete; `restore_session` undoes it).',
123
123
  '3. Misplaced work is fixable: `move_media` / `bulk_move_media` / `move_folder_contents` move media items between projects; `move_session` moves a whole session (plus its media) to another project. If the user says a generation landed in the wrong project, move it rather than regenerating.',
124
124
  '4. If the user has not mentioned any project, omit `project_id` — the default bucket is correct in that case. Do not ask which project to use unless the user\'s intent is ambiguous.',
125
125
  '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`).',
@@ -44,6 +44,7 @@ const PRIVATE_WRITE = [
44
44
  'create_color_palette', 'activate_color_palette', 'deactivate_color_palette',
45
45
  'move_session', 'bulk_move_sessions', 'move_generations_to_session',
46
46
  'split_session', 'undo_session_organization',
47
+ 'rename_session', 'restore_session',
47
48
  'create_project', 'update_project',
48
49
  'archive_project', 'unarchive_project', 'add_project_context',
49
50
  'create_agent',
@@ -73,6 +74,7 @@ const DESTRUCTIVE_WRITE = [
73
74
  'delete_media_folder', 'delete_media', 'permanently_delete_media',
74
75
  'bulk_delete_media', 'bulk_permanently_delete_media',
75
76
  'unshare_media_folder',
77
+ 'delete_session',
76
78
  'delete_project_context', 'regenerate_project_profile',
77
79
  'update_agent', 'delete_agent', 'update_doc', 'delete_doc',
78
80
  'delete_review_asset', 'delete_review_collection',
@@ -445,7 +445,7 @@ function registerGenerateTools(server, client, options = {}) {
445
445
  reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject). **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
446
446
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.'),
447
447
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.'),
448
- sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1, Kling V3/2.6, PixVerse V6). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
448
+ sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Honored by `sound_generation_type: "native"` models (Veo 3.1, Kling V3/2.6, PixVerse V6). Seedance 2.x reports type "none" (no toggle) but `sound_baked_in: true` — those still emit real audio; do not tell the user the model is silent. Omit to use `sound_enabled_by_default`. Pass `false` only when the user asks for silent AND the model is native (not baked-in). Enabling sound may apply `sound_credit_multiplier` to cost.'),
449
449
  skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one video ungraded.'),
450
450
  project_id: projectIdField,
451
451
  session_id: sessionIdField
@@ -526,7 +526,7 @@ function registerGenerateTools(server, client, options = {}) {
526
526
  enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: false — only pass true if the user explicitly asks to enhance/improve the prompt.'),
527
527
  visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false` the model ignores DNA entirely.**'),
528
528
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions.'),
529
- sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1 Lite, Kling V3 4K, PixVerse V6, Kling 2.6/v3). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
529
+ sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Honored by `sound_generation_type: "native"` models (Veo 3.1 Lite, Kling V3 4K, PixVerse V6). Seedance 2.x reports type "none" (no toggle) but `sound_baked_in: true` — those still emit real audio; do not tell the user the model is silent. Omit to use `sound_enabled_by_default`. Pass `false` only when the user asks for silent AND the model is native (not baked-in). Enabling sound may apply `sound_credit_multiplier` to cost.'),
530
530
  skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one video ungraded.'),
531
531
  project_id: projectIdField,
532
532
  session_id: sessionIdField
@@ -983,10 +983,10 @@ function registerGenerateTools(server, client, options = {}) {
983
983
  // ─── generate_elements ─────────────────────────────────────
984
984
  server.tool(
985
985
  'generate_elements',
986
- 'Generate a video from reference elements (images, videos, and/or audio) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". IMPORTANT: different models accept different numbers of inputs — call list_models type="elements" and read elements_max_images / elements_max_videos / elements_max_audio on the chosen model before generating. For text-only → video use generate_video instead. For animating a single still image use generate_video_from_image. Returns the final video URL when complete.',
986
+ 'Generate a video from reference elements (images, videos, and/or audio) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". PROMPT CONTRACT (Seedance / Elements): Locked Intro only — Total line, then [GLOBAL LOOK] / [CAST] / [LOCATION] / SHOT N. Do NOT write SCENE CONTEXT / OPTICS / ACTION department packs. Every Visual DNA in visual_dna_ids MUST also appear in the prompt as @ExactDNAName (e.g. "@Zohar walks…") — never "Zohar\'s" or "the man on the left" as a substitute. IMPORTANT: different models accept different numbers of inputs — call list_models type="elements" and read elements_max_images / elements_max_videos / elements_max_audio on the chosen model before generating. For text-only → video use generate_video instead. For animating a single still image use generate_video_from_image. Returns the final video URL when complete.',
987
987
  {
988
- prompt: z.string().describe('Text description of the desired video / animation'),
989
- model: z.string().optional().describe('Model identifier. Use list_models type="elements" to see options (Seedance 2, Kling O3 Reference, Grok Imagine, Veo 3.1, etc.). Check elements_max_images / elements_max_videos / elements_max_audio on the model. Pick a SPECIFIC model do NOT omit (omitting = Smart Select auto-pick, which we avoid); call list_models for this type and choose the model that best fits the user\'s intent.'),
988
+ prompt: z.string().describe('Locked Intro prompt (Seedance/Elements): Total line, [GLOBAL LOOK], [CAST] with @ExactDNAName for every visual_dna_ids entry, [LOCATION], then SHOT N. Not SCENE CONTEXT/OPTICS/ACTION packs. Never substitute "the left man" or "Zohar\'s" for @Name.'),
989
+ model: z.string().optional().describe('Model identifier. If the user already named a family (Grok / Kling / Veo / Seedance / …), pass THAT familynever default to Seedance because Elements often uses it. Use list_models type="elements" for exact ids and elements_max_* caps. Do NOT omit (omitting = Smart Select).'),
990
990
  reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). **Cap: pass at most `elements_max_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
991
991
  reference_videos: z.array(z.string()).optional().describe('Array of reference video URLs for models that accept video inputs. **Cap: pass at most `elements_max_videos` URLs from list_models — if the cap is 0 the model rejects videos.**'),
992
992
  reference_audio_urls: z.array(z.string()).optional().describe('Array of reference audio URLs for models that accept audio inputs. **Cap: pass at most `elements_max_audio` URLs from list_models.** `audio_url` remains supported as the legacy single-track form.'),
@@ -266,6 +266,8 @@ function registerModelTools(server, client, options = {}) {
266
266
  ? ` (${m.sound_credit_multiplier}×)`
267
267
  : '';
268
268
  parts.push(`sound: native${mult}${m.sound_enabled_by_default ? ' on-by-default' : ''}`);
269
+ } else if (m.sound_baked_in) {
270
+ parts.push('sound: baked-in (always on; type=none hides the toggle — still real audio)');
269
271
  }
270
272
 
271
273
  // Prompt constraints
@@ -129,16 +129,26 @@ function registerProjectTools(server, client, options = {}) {
129
129
  async ({ session_id, type }) => {
130
130
  const path = `/v1/sessions/${encodeURIComponent(session_id)}/generations`;
131
131
  const result = await client.get(path + (type ? `?type=${encodeURIComponent(type)}` : ''));
132
- return {
133
- content: [{
134
- type: 'text',
135
- text: JSON.stringify({
136
- session: result.session,
137
- generations: result.generations,
138
- _hint: 'Pass the `id` values to `move_generations_to_session` (into an existing session) or `split_session` (into a new one). `in_flight: true` means it is still running and cannot be moved yet.'
139
- }, null, 2)
140
- }]
141
- };
132
+ const generations = result.generations || [];
133
+ const text = JSON.stringify({
134
+ session: result.session,
135
+ generations,
136
+ _hint: 'This is an inventory, not a live generation. Pass the `id` values to `move_generations_to_session` (into an existing session) or `split_session` (into a new one). `in_flight: true` means it is still running and cannot be moved yet.'
137
+ }, null, 2);
138
+ if (ui()) {
139
+ return uiResult(UI.list, text, {
140
+ widget: 'list',
141
+ title: (result.session && result.session.name) || 'Session generations',
142
+ items: generations.map((g) => ({
143
+ id: g.id,
144
+ title: (g.prompt && String(g.prompt).slice(0, 80)) || g.id,
145
+ subtitle: [g.status, g.output_count ? g.output_count + ' outputs' : null].filter(Boolean).join(' · '),
146
+ badge: g.in_flight ? 'running' : g.status
147
+ })),
148
+ total: generations.length
149
+ });
150
+ }
151
+ return { content: [{ type: 'text', text }] };
142
152
  }
143
153
  );
144
154
 
@@ -277,32 +287,49 @@ function registerProjectTools(server, client, options = {}) {
277
287
  // ─── list_sessions ─────────────────────────────────────────
278
288
  server.tool(
279
289
  'list_sessions',
280
- 'List the user\'s sessions across ALL generation types (image, video, music, chat, transcription…), newest-activity first. Use to answer "what\'s in this project?", to find a session_id for `move_session`, or to locate past work. Filter by `project_id` and/or `type`.',
290
+ 'List the user\'s sessions across ALL generation types (image, video, music, chat, transcription…), newest-activity first. Each row includes `session_id`, `name`, pipe `type`, `types[]`, and `project_id` — pass that `project_id` on later generate/chat/upload calls for work in this conversation. Use to answer "what\'s in this project?", to find a session_id for `move_session` / `rename_session` / `delete_session`, or to locate past work. Filter by `project_id` and/or `type` (one key) and/or `types` (several keys).',
281
291
  {
282
292
  project_id: z.string().optional().describe('Restrict to one project (ObjectId from list_projects).'),
283
- type: z.string().optional().describe('Restrict to one session type: image, video, video_from_image, music, speech, sound, image_edit, creative_director, chat, elements, first_last_frame, lipsync, video_from_video, transcription, global_image_edit, global_video_edit, shorts.'),
293
+ type: z.string().optional().describe('Restrict to one session type (pipe string on the row stays). Keys: image, video, video_from_image, music, speech, sound, image_edit, creative_director, chat, elements, first_last_frame, lipsync, video_from_video, transcription, global_image_edit, global_video_edit, shorts.'),
294
+ types: z.array(z.string()).optional().describe('Restrict to several session types at once (same keys as `type`). Additive with `type`.'),
284
295
  page: z.number().optional().describe('Page number, 1-indexed. Default: 1'),
285
296
  limit: z.number().optional().describe('Results per page, max 50. Default: 20')
286
297
  },
287
- async ({ project_id, type, page, limit }) => {
298
+ async ({ project_id, type, types, page, limit }) => {
288
299
  const params = new URLSearchParams();
289
300
  if (project_id) params.set('project_id', project_id);
290
301
  if (type) params.set('type', type);
302
+ if (Array.isArray(types) && types.length) params.set('types', types.join(','));
291
303
  if (page) params.set('page', String(page));
292
304
  if (limit) params.set('limit', String(limit));
293
305
  const qs = params.toString();
294
306
  const result = await client.get(`/v1/sessions${qs ? '?' + qs : ''}`);
295
- const sessions = result.sessions || [];
296
- const text = JSON.stringify({ sessions, pagination: result.pagination || null }, null, 2);
307
+ const sessions = (result.sessions || []).map((s) => {
308
+ const kinds = Array.isArray(s.types)
309
+ ? s.types
310
+ : String(s.type || '').split('|').filter(Boolean);
311
+ return { ...s, type: s.type, types: kinds, project_id: s.project_id || null };
312
+ });
313
+ const text = JSON.stringify({
314
+ sessions,
315
+ pagination: result.pagination || null,
316
+ _hint: 'Each row has project_id — pass it on every later generate/chat/upload in this conversation. Empty leftover sessions after a move: delete_session.'
317
+ }, null, 2);
297
318
 
298
319
  if (ui()) {
299
320
  return uiResult(UI.list, text, {
300
321
  widget: 'list',
301
- title: 'Sessions' + (type ? ' — ' + type : ''),
322
+ title: 'Sessions' + (type ? ' — ' + type : '') + ' (' + sessions.length + ')',
302
323
  items: sessions.map(s => ({
303
324
  id: s.session_id,
304
- title: s.name || s.type,
305
- subtitle: s.type + (s.updated_at ? ' · ' + String(s.updated_at).slice(0, 10) : '')
325
+ title: s.name || s.types[0] || s.type || 'Session',
326
+ subtitle: [
327
+ s.session_id,
328
+ (s.types || []).join(', '),
329
+ s.project_id ? 'project ' + s.project_id : null,
330
+ s.updated_at ? String(s.updated_at).slice(0, 10) : null
331
+ ].filter(Boolean).join(' · '),
332
+ badge: (s.types && s.types[0]) || undefined
306
333
  })),
307
334
  total: sessions.length
308
335
  });
@@ -312,6 +339,53 @@ function registerProjectTools(server, client, options = {}) {
312
339
  }
313
340
  );
314
341
 
342
+ server.tool(
343
+ 'rename_session',
344
+ 'Rename a session the user can see in the Kolbo sidebar. Use after `list_sessions` when they say "call this Hero Sequence" or leftover API daily names should become human titles. Does not move the session or its media.',
345
+ {
346
+ session_id: z.string().describe('Session ObjectId from `list_sessions` or a generate_* result.'),
347
+ name: z.string().describe('New sidebar title (1–200 characters).'),
348
+ type: z.string().optional().describe('Optional session type hint to speed up the lookup. Omit if unsure.')
349
+ },
350
+ async ({ session_id, name, type }) => {
351
+ const body = { name };
352
+ if (type) body.type = type;
353
+ const result = await client.patch(`/v1/sessions/${encodeURIComponent(session_id)}`, body);
354
+ return { content: [{ type: 'text', text: JSON.stringify(result.session || result, null, 2) }] };
355
+ }
356
+ );
357
+
358
+ server.tool(
359
+ 'delete_session',
360
+ 'Soft-delete a session (trash). Use for empty leftover sessions after `move_session` / `move_generations_to_session` / `split_session`, or when the user asks to remove a session. Does not permanently wipe media on its own — restore with `restore_session` if they change their mind.',
361
+ {
362
+ session_id: z.string().describe('Session ObjectId from `list_sessions`.'),
363
+ type: z.string().optional().describe('Optional session type hint to speed up the lookup. Omit if unsure.')
364
+ },
365
+ async ({ session_id, type }) => {
366
+ const result = await client.delete(
367
+ `/v1/sessions/${encodeURIComponent(session_id)}`,
368
+ type ? { type } : {}
369
+ );
370
+ return { content: [{ type: 'text', text: JSON.stringify(result.session || result, null, 2) }] };
371
+ }
372
+ );
373
+
374
+ server.tool(
375
+ 'restore_session',
376
+ 'Restore a session previously removed with `delete_session` (clears deletedAt). Use when the user undoes a trash action in this conversation.',
377
+ {
378
+ session_id: z.string().describe('Session ObjectId that was just soft-deleted.'),
379
+ type: z.string().optional().describe('Optional session type hint to speed up the lookup. Omit if unsure.')
380
+ },
381
+ async ({ session_id, type }) => {
382
+ const body = {};
383
+ if (type) body.type = type;
384
+ const result = await client.post(`/v1/sessions/${encodeURIComponent(session_id)}/restore`, body);
385
+ return { content: [{ type: 'text', text: JSON.stringify(result.session || result, null, 2) }] };
386
+ }
387
+ );
388
+
315
389
  // ─── Project context / knowledge base (NotebookLM-style) ───
316
390
  server.tool(
317
391
  'add_project_context',