@kolbo/mcp 1.70.4 → 1.71.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 +2 -1
- package/package.json +1 -1
- package/src/apps/index.js +3 -0
- package/src/apps/widgets/generation.js +39 -0
- package/src/index.js +2 -2
- package/src/toolAnnotations.js +2 -0
- package/src/tools/generate.js +5 -5
- package/src/tools/models.js +2 -0
- package/src/tools/projects.js +92 -18
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
|
|
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
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
|
|
122
|
-
'2. `list_projects` lists the user\'s platform projects (for generations/media/chat). Do not confuse a generation `session_id` with
|
|
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`).',
|
package/src/toolAnnotations.js
CHANGED
|
@@ -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',
|
package/src/tools/generate.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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('
|
|
989
|
-
model: z.string().optional().describe('Model identifier.
|
|
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 family — never 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.'),
|
package/src/tools/models.js
CHANGED
|
@@ -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
|
package/src/tools/projects.js
CHANGED
|
@@ -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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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:
|
|
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',
|