@kolbo/mcp 1.92.0 → 1.93.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
@@ -280,6 +280,20 @@ Requires the Kolbo Studio panel open in Premiere Pro or After Effects with **AI
280
280
  | `adobe_capture_frame` | Render a comp/sequence frame to the Kolbo library so the agent can check its work |
281
281
  | `adobe_get_command_status` | Read command status/result/error; `awaiting_approval` means stop and wait for the editor |
282
282
 
283
+ **DaVinci Resolve Bridge**
284
+ Requires DaVinci Resolve Studio with the Kolbo plugin open (Workspace → Workflow Integrations → Kolbo AI) and **AI agents** switched on in its header. Every edit is approved by the editor inside the plugin; scripts are shown in full before they run.
285
+
286
+ | Tool | Description |
287
+ |------|-------------|
288
+ | `resolve_list_sessions` | List the caller's DaVinci Resolve windows connected through the Kolbo plugin |
289
+ | `resolve_get_project` | Queue a read-only project inspection: timelines, frame rate, resolution, playhead (no approval) |
290
+ | `resolve_get_timeline` | Queue a read-only inspection of the current timeline: clips per track with start/end, markers, playhead |
291
+ | `resolve_import_media` | Import one Kolbo media id or Kolbo-owned HTTPS URL into the Kolbo.AI Media Pool bin |
292
+ | `resolve_edit_timeline` | Structured edits: new timelines, timed and trimmed Kolbo clips on chosen tracks, transitions, audio fades, Fusion titles, markers, clip deletion |
293
+ | `resolve_run_script` | Approval-gated JavaScript against Resolve's scripting API (colour, Fusion, render jobs); the editor reviews the exact code |
294
+ | `resolve_capture_frame` | Export a timeline frame to the Kolbo library so the agent can check its work |
295
+ | `resolve_get_command_status` | Read command status/result/error; `awaiting_approval` means stop and wait for the editor |
296
+
283
297
  **Discovery & Account**
284
298
  | Tool | Description |
285
299
  |------|-------------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.92.0",
3
+ "version": "1.93.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@972d2b4, the single source of truth.
3
+ This tree is mirrored from kolbo-code@b066208, 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
@@ -102,7 +102,9 @@ For multi-scene / batch work this pairs with `generate_creative_director` (see b
102
102
  | Confirm **cost** or validate **resolution / aspect / duration** against model caps | `references/workflows/cost-and-validation.md` |
103
103
  | Hit an **auth / MCP / 429** issue | `references/workflows/troubleshooting.md` |
104
104
  | Inspect or change a connected **Blender** scene, render, import Kolbo media, or run approved Blender Python | `references/workflows/blender.md` |
105
- | Inspect or edit an open **Premiere Pro / After Effects** project — timeline, import Kolbo media, place clips, sequences, captions | `references/workflows/adobe.md` |
105
+ | Inspect or edit an open **Premiere Pro / After Effects** project — timeline, Kolbo media, sequences, captions, After Effects edits and titles | `references/workflows/adobe.md` |
106
+ | Build **motion graphics** in After Effects — shape layers, animated text, effects, expressions, logo reveals | `references/workflows/after-effects-motion.md` |
107
+ | Edit, grade or render Kolbo media in **DaVinci Resolve** (Studio 21.1+, Blackmagic's MCP) | `references/workflows/davinci-resolve.md` |
106
108
  | Create or edit a saved **Video Editor** timeline, clips, trims, speed or captions | `references/workflows/video-editor.md` |
107
109
 
108
110
  Each `references/models/*.md` mirrors the matching skill prompt in `kolbo-api/src/config/systemPrompt.js` — same battle-tuned rules that power Kolbo's web-app help widget. Keep parity (see `packages/opencode/CLAUDE.md` "MCP & Skill Sync Rule").
@@ -160,7 +162,7 @@ Font tools (when exposed by the installed MCP): `list_fonts`, `get_font`, `uploa
160
162
  | `create_review_asset` / `add_review_version` / `set_review_status` / `create_review_comment` / `reply_review_comment` / `resolve_review_comment` / `unresolve_review_comment` / `create_review_collection` / `create_review_share_link` / `revoke_review_share_link` / `get_review_storage_usage` (+ list/get/update/delete siblings) | **Kolbo Review** — Frame.io-style client review: asset = media + appended versions (new cut = `add_review_version`, never delete+recreate), timecoded comments per version, approve/request-changes status, guest share links (no Kolbo account; comment-only unless `canSetStatus`). 5GB review storage cap. See `workflows/review-collections.md`. |
161
163
  | `publish_html_artifact` | Publish HTML / SVG / Mermaid to `sites.kolbo.ai`. Server dedupes by content hash. Strict CSP. |
162
164
  | `blender_list_sessions` / `blender_get_scene` / `blender_search_docs` / `blender_capture_viewport` / `blender_apply_operations` / `blender_import_media` / `blender_render` / `blender_undo` / `blender_file_operation` / `blender_execute_python` / `blender_get_command_status` | Connected Blender control through the Kolbo extension. Every tool crosses into an external desktop host; read `workflows/blender.md` before the first call. |
163
- | `adobe_list_sessions` / `adobe_get_project` / `adobe_get_timeline` / `adobe_import_media` / `adobe_place_on_timeline` / `adobe_create_sequence` / `adobe_import_captions` / `adobe_get_command_status` | Connected Premiere Pro / After Effects control through the Kolbo panel. Every edit needs the editor's approval in the panel; read `workflows/adobe.md` before the first call. |
165
+ | `adobe_list_sessions` / `adobe_get_project` / `adobe_get_timeline` / `adobe_import_media` / `adobe_place_on_timeline` / `adobe_create_sequence` / `adobe_import_captions` / `adobe_edit_composition` / `adobe_run_script` / `adobe_capture_frame` / `adobe_get_command_status` | Connected Premiere Pro / After Effects control through the Kolbo panel. Every change needs the editor's approval in the panel; read `workflows/adobe.md` before the first call and `workflows/after-effects-motion.md` before any motion-graphics script. |
164
166
 
165
167
  ## ⚠️ Edit in place — never delete+recreate (HARD RULE — always on)
166
168
 
@@ -1,41 +1,70 @@
1
1
  # Premiere Pro & After Effects Workflow
2
2
 
3
- Use these rules whenever the user wants an agent to inspect or edit an open Premiere Pro or After Effects project through Kolbo. The Kolbo panel inside the Adobe app is a separate desktop authority boundary: a Kolbo account is necessary, but the editor's approval inside the panel is the final gate for every edit.
3
+ Use these rules whenever the user wants an agent to inspect, edit or animate an open Premiere Pro or After Effects project through Kolbo. The Kolbo panel inside the Adobe app is a separate desktop authority boundary: a Kolbo account is necessary, but the editor's approval inside the panel is the final gate for every change.
4
+
5
+ For motion graphics in After Effects (shape layers, animated text, effects, expressions), also read `references/workflows/after-effects-motion.md` before writing any script.
4
6
 
5
7
  ## Connect and target safely
6
8
 
7
9
  1. Call `adobe_list_sessions` before the first Adobe action.
8
- 2. If no session is listed, ask the user to open **Window → Extensions → Kolbo Studio** in Premiere Pro or After Effects, sign in, and click the **AI agents** (robot) button in the panel header until its dot turns green. Do not substitute any other Kolbo tool for the panel relay.
9
- 3. If one session is active, `session_id` may be omitted. If several are active, show each session's `name` (Premiere Pro / After Effects), `adobe_version`, platform and id, and ask which to target. Never guess.
10
+ 2. If no session is listed, ask the user to open **Window → Extensions → Kolbo Studio** in Premiere Pro or After Effects and sign in. The **AI agents** switch in the panel header connects automatically when signed in; if its dot is not green, ask them to click it. The panel must be signed into the same Kolbo account as this connector - sessions are private per account.
11
+ 3. If one session is active, `session_id` may be omitted. If several are active, show each session's `name` (Premiere Pro / After Effects), `adobe_version` and id, and ask which to target. Never guess.
10
12
  4. Keep the chosen `session_id` on every later Adobe call in the task. Re-list after a disconnect or app restart; ids are process-scoped.
11
13
 
12
- There is no MCP logout tool. The editor disconnects from the panel header button.
13
-
14
- ## Inspect before changing
15
-
16
- - Start with `adobe_get_project`, then `adobe_get_timeline` for the active Premiere sequence or After Effects composition. Both are read-only and run without approval.
17
- - Timeline responses are bounded; pass `max_clips` when you only need the first clips.
14
+ There is no MCP logout tool. The editor turns agents off from the panel header.
18
15
 
19
16
  ## Command lifecycle and approval
20
17
 
21
18
  Every Adobe tool except `adobe_list_sessions` and `adobe_get_command_status` returns a **command record**, not the result. Poll `adobe_get_command_status` with its `command_id` until the status is terminal: `succeeded`, `failed`, `denied` or `canceled`.
22
19
 
23
- - `awaiting_approval` is not a polling state. Stop and tell the user to approve or deny the request in the Kolbo panel, then check once more after they answer.
24
- - `denied` is final. Do not retry the same edit with cosmetic changes; ask the user what they want instead.
25
- - "Allow for this session" lives only in the panel's memory and ends when the editor disconnects. Never tell the user it persists, and never ask them to enable it for you.
26
- - Pass a stable `idempotency_key` when a timeout may cause you to retry the same command. A new intent needs a new key.
20
+ - Reads (`adobe_get_project`, `adobe_get_timeline`) run without approval. Everything else waits for **Allow once / Allow for this session / Deny** in the panel.
21
+ - `awaiting_approval` is not a polling state. Tell the user to approve or deny in the Kolbo panel, then check once more after they answer.
22
+ - `denied` is final. Do not retry the same change with cosmetic edits; ask the user what they want instead.
23
+ - "Allow for this session" lives only in the panel's memory and ends on disconnect. Never tell the user it persists and never ask them to enable it for you.
24
+ - Pass a stable `idempotency_key` when a timeout may make you retry the same command. A new intent needs a new key.
25
+
26
+ ## Choosing the right tool
27
+
28
+ | Goal | Tool |
29
+ |---|---|
30
+ | See the project / active sequence or comp (tracks, clips with start/end, playhead; or comp layers) | `adobe_get_project`, `adobe_get_timeline` |
31
+ | Put a Kolbo clip in the bin, or at the Premiere playhead | `adobe_import_media`, `adobe_place_on_timeline` |
32
+ | New Premiere sequence (no dialog, copies the open sequence's settings) | `adobe_create_sequence` |
33
+ | Captions onto the active Premiere sequence | `transcribe_audio` → `adobe_import_captions` with the `.srt` URL |
34
+ | After Effects edit: comps, timed/trimmed clips, titles, solids, fades, keyframes, music | `adobe_edit_composition` |
35
+ | After Effects motion graphics beyond those operations | `adobe_run_script` (read `after-effects-motion.md`) |
36
+ | Check what it actually looks like | `adobe_capture_frame` → look at the returned `url` |
37
+
38
+ Prefer `adobe_edit_composition` whenever its operations are enough: it is validated, one undo step, and easier for the editor to approve than code.
39
+
40
+ ## After Effects composition edits (`adobe_edit_composition`)
41
+
42
+ - One call applies up to 100 operations in order as **one undo step**. It stops at the first failing operation; earlier operations stay applied. Read the error, fix that operation, and continue from there - do not replay the whole batch.
43
+ - Start a new piece with `comp.create` (defaults 1920×1080, 30 fps). Later operations in the same batch target it.
44
+ - Times are **seconds on the composition timeline**. For media: `start_seconds` = where it begins, `trim_start_seconds` = seconds skipped at the head of the source, `duration_seconds` = visible length.
45
+ - Crossfade = overlap two shots by 0.3–1 s and animate the upper shot's opacity 0 → 100 across the overlap. New layers stack on top, so add the later shot after the earlier one.
46
+ - **Name every layer you will address later** and use that exact name in `layer.update` / `layer.animate`. Indexes shift as layers are added (1 = top).
47
+ - `fit: "cover"` fills the frame (default), `"contain"` letterboxes, `"none"` keeps source size. Keyframed `scale` values are absolute percentages, so animate scale on titles and solids, not on fitted media, unless you first read the fitted scale from `adobe_get_timeline`.
48
+ - Titles default to Arial Bold, white, centred. Add `stroke_width` 3–6 (black stroke) whenever text sits over bright or busy footage - white text on a light shot is invisible.
49
+ - Music: add audio with `layer.add_media`, then animate `audio_levels` from 0 dB to about −40 dB over the last 1.5–2 s for a clean fade-out.
50
+ - Solids are sent to the bottom automatically (backgrounds).
51
+
52
+ ## Media rules
53
+
54
+ - Media accepts exactly one Kolbo `media_id` (preferred) or a Kolbo-owned HTTPS `url`. Third-party hosts, HTTP, private network and guessed URLs are rejected; import third-party files into Kolbo first.
55
+ - Generate first, wait for success, then pass the real media id. Never place a still-running generation.
56
+ - `adobe_place_on_timeline` has no time or track control: it uses the Premiere work sequence playhead or the active After Effects comp. For timed After Effects edits use `adobe_edit_composition`.
57
+ - `adobe_create_sequence` and `adobe_import_captions` are Premiere Pro only; `adobe_edit_composition` is After Effects only. The wrong app fails with `UNSUPPORTED_HOST`.
27
58
 
28
- ## Media, sequences and captions
59
+ ## Scripts (`adobe_run_script`)
29
60
 
30
- - `adobe_import_media` and `adobe_place_on_timeline` accept exactly one Kolbo `media_id` (preferred) or a Kolbo-owned HTTPS `url`. Third-party hosts, HTTP, private network and guessed URLs are rejected; import third-party files into Kolbo first.
31
- - To generate then edit: run the Kolbo generation tool, wait for its successful result, then pass the real media id. Never place a still-running generation.
32
- - `adobe_place_on_timeline` drops the clip into a free track at the playhead of the Premiere work sequence, or into the active After Effects composition. There is no time, track, trim or transition control in v1; do not promise one. Ask the user to position the playhead first when placement matters.
33
- - `adobe_create_sequence` and `adobe_import_captions` are Premiere Pro only. In After Effects they fail with `UNSUPPORTED_HOST`.
34
- - For captions, create the SRT with `transcribe_audio`, then pass its Kolbo-hosted `.srt` URL to `adobe_import_captions`.
35
- - There is no raw ExtendScript tool. If the user asks for an edit outside these commands, say it is not available through agents yet and suggest doing it in the panel or the app.
61
+ - `code` is a **function body**: call `log(...)` for progress and `return` a JSON-serialisable summary. In After Effects the whole script is one undo step.
62
+ - The editor reads the exact code before approving. Keep scripts focused, named and commented; give a plain `purpose`.
63
+ - Scripts have full access to the project and the computer. Never read or write files, call `system.callSystem`, or use the network unless the user explicitly asked for exactly that.
64
+ - On `SCRIPT_ERROR` the message includes the line and the last log lines. Fix the specific problem; do not resend the same script.
36
65
 
37
66
  ## Completion proof
38
67
 
39
- 1. Re-read with `adobe_get_timeline` or `adobe_get_project` after an edit.
40
- 2. Report what changed, which session was targeted, and that the editor approved it.
41
- 3. A command is complete only when `adobe_get_command_status` shows `succeeded`. An accepted enqueue is not proof the edit happened.
68
+ 1. Re-read with `adobe_get_timeline` after an edit.
69
+ 2. For anything visual - titles, motion graphics, layout, crossfades - call `adobe_capture_frame` at 2–4 representative times and **look at the images** before reporting. Check legibility, contrast, framing and timing.
70
+ 3. Report what changed, which session was targeted, and that the editor approved it. A command is complete only when `adobe_get_command_status` shows `succeeded`.
@@ -0,0 +1,193 @@
1
+ # After Effects Motion Graphics
2
+
3
+ Read this before calling `adobe_run_script` for motion graphics. Connection, approval and completion rules are in `references/workflows/adobe.md`; ordinary cuts, titles and fades belong in `adobe_edit_composition` instead of a script.
4
+
5
+ Every snippet below was run in After Effects 26.3 through `adobe_run_script`.
6
+
7
+ ## Design before code
8
+
9
+ Plan the piece as beats before writing anything: what the viewer should notice first, second and last, with times.
10
+
11
+ - **Timing.** UI and title moves: 0.3–0.6 s. Logo builds and reveals: 1–2 s. Hold readable text for at least 1.5 s plus 0.3 s per word. Leave 0.5 s of breathing room before the end.
12
+ - **Easing.** Nothing real moves at constant speed. Ease into rests (`KeyframeEase` influence 70–90). Use linear only for continuous motion (spins, scrolling, constant drift).
13
+ - **Overlap and offset.** Stagger related elements by 2–4 frames (0.07–0.13 s) instead of moving them together. Let secondary elements settle after the primary one.
14
+ - **Overshoot.** A pop reads as alive when scale goes 0 → 115 → 100 within about 0.6 s. Use it sparingly: one hero element per beat.
15
+ - **Hierarchy.** One dominant element per frame. Size, contrast and motion should all agree on what matters.
16
+ - **Legibility.** Text needs contrast: dark scrim, stroke or shadow over busy footage. Keep text inside title-safe (about 10% margin: x 192–1728, y 108–972 at 1080p).
17
+ - **Restraint.** Two typefaces maximum, a palette of 2–4 colours, and one idea per shot. Remove before adding.
18
+
19
+ ## Build loop
20
+
21
+ 1. `adobe_get_timeline` to see what exists.
22
+ 2. Write the script in named sections (background, main element, typography, outro). Name every layer.
23
+ 3. `adobe_run_script` with a clear `purpose`. Return a small summary (comp name, layer names).
24
+ 4. `adobe_capture_frame` at the key beats (mid-reveal, settled, outro) and look at every image.
25
+ 5. Fix in a follow-up script that edits the named layers; do not rebuild everything.
26
+
27
+ ## ExtendScript rules
28
+
29
+ After Effects scripting is ES3: use `var` and `function`. There is no `let`/`const`, arrow functions, template strings, `Array.prototype.forEach/map/indexOf`, or `Object.keys` - use `for` loops. `JSON` is available. The script body receives `log()` and must `return` its result.
30
+
31
+ Use property **match names** (`'ADBE Transform Group'`, `'ADBE Position'`), not display names; they work in every UI language.
32
+
33
+ ## Foundation
34
+
35
+ ```js
36
+ var W = 1920, H = 1080, DUR = 6, FPS = 30;
37
+ var comp = app.project.items.addComp('Logo Reveal', W, H, 1, DUR, FPS);
38
+ comp.bgColor = [0.02, 0.02, 0.05];
39
+ comp.motionBlur = true; // also set layer.motionBlur = true on moving layers
40
+ comp.openInViewer();
41
+
42
+ function tr(layer, name) { return layer.property('ADBE Transform Group').property(name); }
43
+ // 'ADBE Anchor Point', 'ADBE Position', 'ADBE Scale', 'ADBE Rotate Z', 'ADBE Opacity'
44
+
45
+ // Keys with ease on every dimension (spatial properties take one ease value).
46
+ function ease(prop, times, values, influence) {
47
+ for (var i = 0; i < times.length; i++) prop.setValueAtTime(times[i], values[i]);
48
+ var spatial = prop.propertyValueType === PropertyValueType.TwoD_SPATIAL || prop.propertyValueType === PropertyValueType.ThreeD_SPATIAL;
49
+ var dims = spatial || !(prop.value instanceof Array) ? 1 : prop.value.length;
50
+ for (var k = 1; k <= prop.numKeys; k++) {
51
+ var e = [];
52
+ for (var d = 0; d < dims; d++) e.push(new KeyframeEase(0, influence || 80));
53
+ prop.setTemporalEaseAtKey(k, e, e);
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Backgrounds
59
+
60
+ ```js
61
+ var bg = comp.layers.addSolid([0, 0, 0], 'Background', W, H, 1, DUR);
62
+ var ramp = bg.property('ADBE Effect Parade').addProperty('ADBE Ramp'); // Gradient Ramp
63
+ ramp.property('ADBE Ramp-0001').setValue([W / 2, H * 0.4]); // start point
64
+ ramp.property('ADBE Ramp-0002').setValue([0.16, 0.13, 0.42, 1]); // start colour (RGBA)
65
+ ramp.property('ADBE Ramp-0003').setValue([W / 2, H * 1.25]); // end point
66
+ ramp.property('ADBE Ramp-0004').setValue([0.01, 0.01, 0.03, 1]); // end colour
67
+ ramp.property('ADBE Ramp-0005').setValue(2); // 1 linear, 2 radial
68
+ ```
69
+
70
+ ## Shape layers
71
+
72
+ ```js
73
+ var ring = comp.layers.addShape();
74
+ ring.name = 'Ring';
75
+ var group = ring.property('ADBE Root Vectors Group').addProperty('ADBE Vector Group');
76
+ var contents = group.property('ADBE Vectors Group');
77
+
78
+ contents.addProperty('ADBE Vector Shape - Ellipse').property('ADBE Vector Ellipse Size').setValue([380, 380]);
79
+ // Rounded rectangle instead:
80
+ // var rect = contents.addProperty('ADBE Vector Shape - Rect');
81
+ // rect.property('ADBE Vector Rect Size').setValue([900, 500]);
82
+ // rect.property('ADBE Vector Rect Roundness').setValue(48);
83
+ // Custom path:
84
+ // var shape = new Shape(); shape.vertices = [[-200, 0], [0, -120], [200, 0]]; shape.closed = false;
85
+ // contents.addProperty('ADBE Vector Shape - Group').property('ADBE Vector Shape').setValue(shape);
86
+
87
+ var stroke = contents.addProperty('ADBE Vector Graphic - Stroke');
88
+ stroke.property('ADBE Vector Stroke Color').setValue([0.42, 0.55, 1, 1]);
89
+ stroke.property('ADBE Vector Stroke Width').setValue(16);
90
+ stroke.property('ADBE Vector Stroke Line Cap').setValue(2); // round caps
91
+ // Solid fill: contents.addProperty('ADBE Vector Graphic - Fill').property('ADBE Vector Fill Color').setValue([1, 1, 1, 1]);
92
+ // Gradient fill: contents.addProperty('ADBE Vector Graphic - G-Fill') with 'ADBE Vector Grad Start Pt' / 'ADBE Vector Grad End Pt'
93
+
94
+ // Draw-on with Trim Paths (add it after the shape and stroke).
95
+ var trim = contents.addProperty('ADBE Vector Filter - Trim');
96
+ ease(trim.property('ADBE Vector Trim End'), [0.2, 1.5], [0, 100], 85);
97
+ // Endless loader: trim.property('ADBE Vector Trim End').setValue(25); trim.property('ADBE Vector Trim Offset').expression = 'time * 180';
98
+
99
+ tr(ring, 'ADBE Position').setValue([W / 2, 420]); // shape contents are centred on the layer position
100
+ ring.motionBlur = true;
101
+ ```
102
+
103
+ Overshoot pop on a second shape: `ease(tr(core, 'ADBE Scale'), [1.2, 1.55, 1.8], [[0, 0], [118, 118], [100, 100]], 70);`
104
+
105
+ ## Typography
106
+
107
+ ```js
108
+ var word = comp.layers.addText('KOLBO'); // use '\r' for line breaks
109
+ var textProp = word.property('ADBE Text Properties').property('ADBE Text Document');
110
+ var doc = textProp.value;
111
+ doc.resetCharStyle();
112
+ doc.font = 'Arial-BoldMT'; // PostScript name
113
+ doc.fontSize = 190;
114
+ doc.tracking = 180;
115
+ doc.autoLeading = false; doc.leading = 110; // multi-line spacing
116
+ doc.fontCapsOption = FontCapsOption.FONT_ALL_CAPS; // doc.allCaps is read-only
117
+ doc.applyFill = true; doc.fillColor = [1, 1, 1];
118
+ // Outline for busy backgrounds: doc.applyStroke = true; doc.strokeColor = [0, 0, 0]; doc.strokeWidth = 5; doc.strokeOverFill = false;
119
+ doc.justification = ParagraphJustification.CENTER_JUSTIFY;
120
+ textProp.setValue(doc);
121
+
122
+ // Centre the anchor on the visible text so Position means "centre of the text".
123
+ var box = word.sourceRectAtTime(0, false);
124
+ tr(word, 'ADBE Anchor Point').setValue([box.left + box.width / 2, box.top + box.height / 2]);
125
+ tr(word, 'ADBE Position').setValue([W / 2, 760]);
126
+ ```
127
+
128
+ Per-character reveal with a text animator (characters rise and fade in left to right):
129
+
130
+ ```js
131
+ var animator = word.property('ADBE Text Properties').property('ADBE Text Animators').addProperty('ADBE Text Animator');
132
+ var animProps = animator.property('ADBE Text Animator Properties');
133
+ animProps.addProperty('ADBE Text Position 3D').setValue([0, 140, 0]); // offset while inside the range
134
+ animProps.addProperty('ADBE Text Opacity').setValue(0);
135
+ // Pop instead of rise: animProps.addProperty('ADBE Text Scale 3D').setValue([0, 0, 100]);
136
+ var selector = animator.property('ADBE Text Selectors').addProperty('ADBE Text Selector');
137
+ // Softer falloff: selector.property('ADBE Text Range Advanced').property('ADBE Text Range Shape').setValue(2); // ramp up
138
+ ease(selector.property('ADBE Text Percent Offset'), [1.6, 2.7], [0, 100], 75);
139
+ word.motionBlur = true;
140
+ ```
141
+
142
+ ## Effects
143
+
144
+ ```js
145
+ var fx = layer.property('ADBE Effect Parade');
146
+ var glow = fx.addProperty('ADBE Glo2'); // Glow
147
+ glow.property('ADBE Glo2-0003').setValue(70); // radius
148
+ glow.property('ADBE Glo2-0004').setValue(1.6); // intensity
149
+ fx.addProperty('ADBE Gaussian Blur 2').property('ADBE Gaussian Blur 2-0001').setValue(8); // blurriness
150
+ fx.addProperty('ADBE Drop Shadow').property('ADBE Drop Shadow-0005').setValue(40); // softness
151
+ fx.addProperty('ADBE Fill').property('ADBE Fill-0002').setValue([1, 0.4, 0.2, 1]); // colour
152
+ ```
153
+
154
+ ## Structure, mattes and 3D
155
+
156
+ ```js
157
+ var ctrl = comp.layers.addNull(); ctrl.name = 'Controller';
158
+ card.parent = ctrl; // move everything together
159
+
160
+ tr(ctrl, 'ADBE Position').expression = 'wiggle(2, 12)'; // organic drift
161
+ tr(card, 'ADBE Rotate Z').expression = 'loopOut("pingpong")'; // after at least two keys
162
+ tr(ring, 'ADBE Rotate Z').expression = 'time * 24'; // constant spin
163
+
164
+ var pre = comp.layers.precompose([card.index], 'Card Precomp', true); // returns the new CompItem
165
+
166
+ fill.moveAfter(matte);
167
+ fill.setTrackMatte(matte, TrackMatteType.ALPHA); // reveal fill through the matte's alpha
168
+
169
+ floor.threeDLayer = true;
170
+ var cam = comp.layers.addCamera('Camera', [W / 2, H / 2]);
171
+ ease(tr(cam, 'ADBE Position'), [0, 5], [[W / 2, H / 2, -2400], [W / 2, H / 2, -1800]], 60); // slow push-in
172
+ ```
173
+
174
+ ## Outro
175
+
176
+ Fade the group together over the last 0.5–0.7 s, holding the current value first so earlier animation is kept:
177
+
178
+ ```js
179
+ var layers = [ring, core, word, tag];
180
+ for (var i = 0; i < layers.length; i++) {
181
+ var op = tr(layers[i], 'ADBE Opacity');
182
+ op.setValueAtTime(DUR - 0.7, op.valueAtTime(DUR - 0.7, false));
183
+ op.setValueAtTime(DUR, 0);
184
+ }
185
+ return { comp: comp.name, layers: comp.numLayers };
186
+ ```
187
+
188
+ ## Checklist before reporting
189
+
190
+ - Captured frames at mid-reveal, settled state and outro, and looked at them.
191
+ - Text is legible over its background and inside title-safe.
192
+ - Every moving element eases; stagger and overshoot are deliberate, not everywhere.
193
+ - Layers are named and the script returned a summary the user can read.
@@ -0,0 +1,115 @@
1
+ # DaVinci Resolve Workflow
2
+
3
+ Use this when the user wants Kolbo media edited, graded or rendered in DaVinci Resolve. Kolbo generates and hosts the media; **Blackmagic's own DaVinci Resolve MCP server** drives Resolve. The agent uses both connectors side by side.
4
+
5
+ Everything below was run against DaVinci Resolve Studio 21.1.0.17 through Blackmagic's server.
6
+
7
+ ## Requirements - check before promising anything
8
+
9
+ - **DaVinci Resolve Studio 21.1 or later.** The free edition has no MCP server and no external scripting.
10
+ - A **local** agent: Claude Desktop, Claude Code or Codex on the same computer as Resolve. Browser ChatGPT and claude.ai can generate media with Kolbo but cannot reach Resolve.
11
+ - Connect Resolve's server from **File → Setup AI Assistants** in Resolve, and set **Preferences → System → General → External scripting using** to **Local**.
12
+ - Resolve must be running; the server's `launch_resolve` tool can start it.
13
+
14
+ If the Resolve tools are missing from the conversation, say so and give these steps. Do not try to control Resolve any other way.
15
+
16
+ ## Blackmagic's tools (not Kolbo's)
17
+
18
+ | Tool | Use |
19
+ |---|---|
20
+ | `get_resolve_status`, `launch_resolve` | Is Resolve running / start it |
21
+ | `get_whats_new` (`since` is required, e.g. `"21.0"`) | Features newer than your training |
22
+ | `search_scripting_api`, `get_scripting_api`, `get_scripting_docs` | Look up exact API signatures before writing a script |
23
+ | `run_script` | Sandboxed Python: Resolve API only, no files, network or processes |
24
+ | `run_script_unsafe` | Python with full system access - required for importing files or downloading media |
25
+ | `generate_lut`, `update_dctl`, `list_luts`, `list_dctls` | Colour transforms |
26
+
27
+ Scripts get `resolve` and the current `project` pre-injected and return data by assigning `result`.
28
+
29
+ ## Workflow
30
+
31
+ 1. **Generate or find media with Kolbo** (`generate_video`, `generate_music`, `list_media`, …) and wait for success.
32
+ 2. **Get the files onto disk.** In Claude Code or Codex, download the Kolbo URLs with the shell. In Claude Desktop, download inside `run_script_unsafe` with `urllib.request`. Only download Kolbo-hosted URLs.
33
+ 3. **Protect the user's work.** Call `pm.SaveProject()` first. For anything experimental, build in a new project (`pm.CreateProject(name)`) and reload the original project at the end. Projects opened in 21.1 cannot be opened in 20.x, so never convert a user's project as a side effect.
34
+ 4. **Import, cut and finish** with `run_script_unsafe` (see recipe).
35
+ 5. **Verify visually.** Set the playhead and call `project.ExportCurrentFrameAsStill(path)` at representative times, then look at the stills before reporting.
36
+ 6. Optionally render (`AddRenderJob` / `StartRendering`) and upload the result back to Kolbo with `upload_media` so it lands in the user's library.
37
+
38
+ ## Verified gotchas
39
+
40
+ - **`MediaPool.ImportMedia` needs plain path strings.** The dict form in the 21.1 stubs (`[{"FilePath": ...}]`) returned `None`. On Windows, backslash paths worked.
41
+ - **File import fails in `run_script`**; use `run_script_unsafe` for anything that touches files.
42
+ - **`Timeline.InsertFusionTitleIntoTimeline("Text+")` is a ripple insert** into every unlocked track: it splits the clips and music under the playhead. With those tracks locked it inserts nothing. For a title over a shot, build it inside that clip's Fusion comp (recipe below).
43
+ - `AppendToTimeline` `startFrame` / `endFrame` are **source frames** at the clip's own frame rate (`GetClipProperty("FPS")`). `recordFrame` is a timeline frame; timelines start at `timeline.GetStartFrame()` (86400 = 01:00:00:00 at 24 fps).
44
+ - New projects default to 24 fps and UHD output.
45
+
46
+ ## Recipe: cut, transition, music fade, title
47
+
48
+ ```python
49
+ pm = resolve.GetProjectManager()
50
+ original = project.GetName()
51
+ pm.SaveProject()
52
+ proj = pm.CreateProject("Kolbo Edit") or pm.LoadProject("Kolbo Edit")
53
+ mp = proj.GetMediaPool()
54
+
55
+ paths = [r"C:\media\shot1.mp4", r"C:\media\shot2.mp4", r"C:\media\music.mp3"]
56
+ items = {item.GetName(): item for item in mp.ImportMedia(paths)}
57
+ shot1, shot2, music = items["shot1.mp4"], items["shot2.mp4"], items["music.mp3"]
58
+
59
+ tl = mp.CreateEmptyTimeline("Kolbo Promo")
60
+ proj.SetCurrentTimeline(tl)
61
+ fps = float(proj.GetSetting("timelineFrameRate"))
62
+ start = tl.GetStartFrame()
63
+
64
+ def src(item, a, b):
65
+ clip_fps = float(item.GetClipProperty("FPS") or fps)
66
+ return int(a * clip_fps), int(b * clip_fps) - 1
67
+
68
+ s1, e1 = src(shot1, 0.5, 6.5)
69
+ s2, e2 = src(shot2, 1.0, 7.0)
70
+ clips = mp.AppendToTimeline([
71
+ {"mediaPoolItem": shot1, "startFrame": s1, "endFrame": e1, "mediaType": 1, "trackIndex": 1, "recordFrame": start},
72
+ {"mediaPoolItem": shot2, "startFrame": s2, "endFrame": e2, "mediaType": 1, "trackIndex": 1, "recordFrame": start + int(6 * fps)},
73
+ ])
74
+ audio = mp.AppendToTimeline([{"mediaPoolItem": music, "startFrame": 0, "endFrame": int(12 * fps) - 1,
75
+ "mediaType": 2, "trackIndex": 1, "recordFrame": start}])
76
+
77
+ clips[0].AddTransition({"type": "Cross Dissolve", "category": "simple", "position": "end",
78
+ "alignment": "center", "duration": int(fps)})
79
+ audio[0].SetFades({"FadeIn": int(0.5 * fps), "FadeOut": int(2 * fps)})
80
+
81
+ # Title inside shot 1's Fusion comp, fading in and out (Blend keyframes are clip frames).
82
+ comp = clips[0].AddFusionComp()
83
+ media_in, media_out = comp.FindTool("MediaIn1"), comp.FindTool("MediaOut1")
84
+ text = comp.AddTool("TextPlus", -32768, -32768)
85
+ text.SetInput("StyledText", "KOLBO x DAVINCI RESOLVE")
86
+ text.SetInput("Size", 0.085)
87
+ text.SetInput("Font", "Arial")
88
+ text.SetInput("Style", "Bold")
89
+ merge = comp.AddTool("Merge", -32768, -32768)
90
+ merge.ConnectInput("Background", media_in)
91
+ merge.ConnectInput("Foreground", text)
92
+ media_out.ConnectInput("Input", merge)
93
+ merge.AddModifier("Blend", "BezierSpline")
94
+ for value, frame in ((0.0, 6), (1.0, 24), (1.0, 96), (0.0, 120)):
95
+ merge.SetInput("Blend", value, frame)
96
+
97
+ pm.SaveProject()
98
+ result = {"project": proj.GetName(), "timeline": tl.GetName(), "original": original}
99
+ ```
100
+
101
+ Then verify, and restore the user's project when you are done:
102
+
103
+ ```python
104
+ tl = project.GetCurrentTimeline()
105
+ tl.SetCurrentTimecode("01:00:02:00")
106
+ ok = project.ExportCurrentFrameAsStill(r"C:\media\check-2s.png")
107
+ resolve.GetProjectManager().SaveProject()
108
+ resolve.GetProjectManager().LoadProject("<original project name>")
109
+ result = {"still": ok}
110
+ ```
111
+
112
+ ## Completion proof
113
+
114
+ - Look at exported stills at the title, the transition and the end before reporting.
115
+ - Report which project and timeline you built, that the original project was saved and restored, and where any render landed.
package/src/index.js CHANGED
@@ -82,6 +82,7 @@ const { registerAudioStemTools } = require('./tools/audio_stems');
82
82
  const { registerAnalyzeTools } = require('./tools/analyze');
83
83
  const { registerBlenderTools } = require('./tools/blender');
84
84
  const { registerAdobeTools } = require('./tools/adobe');
85
+ const { registerResolveTools } = require('./tools/resolve');
85
86
  const { registerApps, attachToolWidgetMeta } = require('./apps');
86
87
  const { attachToolAnnotations } = require('./toolAnnotations');
87
88
 
@@ -203,6 +204,7 @@ function createServer(opts = {}) {
203
204
  registerAudioStemTools(server, client, toolOptions);
204
205
  registerBlenderTools(server, client, toolOptions);
205
206
  registerAdobeTools(server, client, toolOptions);
207
+ registerResolveTools(server, client, toolOptions);
206
208
 
207
209
  // MCP Apps widget resources (ui://kolbo/*). Registering resources is inert
208
210
  // for text-only hosts — they never fetch them.
@@ -36,6 +36,7 @@ const OPEN_WORLD_READ_ONLY = [
36
36
  'blender_list_sessions', 'blender_get_scene', 'blender_search_docs',
37
37
  'blender_get_command_status',
38
38
  'adobe_list_sessions', 'adobe_get_project', 'adobe_get_timeline', 'adobe_get_command_status',
39
+ 'resolve_list_sessions', 'resolve_get_project', 'resolve_get_timeline', 'resolve_get_command_status',
39
40
  ];
40
41
 
41
42
  const PRIVATE_WRITE = [
@@ -106,6 +107,8 @@ const OPEN_WORLD_WRITE = [
106
107
  // Adobe edits add bins, clips, sequences or caption tracks; none delete or overwrite.
107
108
  'adobe_import_media', 'adobe_place_on_timeline', 'adobe_create_sequence', 'adobe_import_captions',
108
109
  'adobe_capture_frame',
110
+ // Resolve imports add Media Pool items; captures add a Kolbo library image.
111
+ 'resolve_import_media', 'resolve_capture_frame',
109
112
  ];
110
113
 
111
114
  const OPEN_WORLD_DESTRUCTIVE = [
@@ -114,6 +117,8 @@ const OPEN_WORLD_DESTRUCTIVE = [
114
117
  'blender_undo', 'blender_file_operation', 'blender_execute_python',
115
118
  // Can delete layers; approved once per batch in the Kolbo panel.
116
119
  'adobe_edit_composition', 'adobe_run_script',
120
+ // Can delete timeline clips; approved once per batch in the Kolbo plugin.
121
+ 'resolve_edit_timeline', 'resolve_run_script',
117
122
  ];
118
123
 
119
124
  const CONTRACT_GROUPS = [
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ const { z } = require('zod');
4
+ const { isTrustedMediaUrl } = require('./blender');
5
+
6
+ // DaVinci Resolve control through the Kolbo Resolve plugin relay. Same relay
7
+ // contract as Blender and Adobe (/v1/resolve mirrors /v1/adobe): commands are
8
+ // queued on the server, delivered to the open Kolbo plugin in Resolve Studio,
9
+ // approved there by the editor, and executed with Resolve's scripting API.
10
+
11
+ const SAFE_ID = /^[A-Za-z0-9._:-]+$/;
12
+
13
+ const noControls = (max) => z.string().min(1).max(max).refine(
14
+ (value) => value.trim().length > 0 && !/[\x00-\x1f]/.test(value),
15
+ `Must be non-empty and contain no control characters (max ${max}).`,
16
+ );
17
+ const sessionId = z.string().min(1).max(128).regex(SAFE_ID).optional().describe(
18
+ 'Target Resolve session id from resolve_list_sessions. Omit only when exactly one DaVinci Resolve plugin is connected.'
19
+ );
20
+ const idempotencyKey = z.string()
21
+ .min(1)
22
+ .max(128)
23
+ .regex(SAFE_ID)
24
+ .optional()
25
+ .describe('Optional replay-safe key. Reuse it only when retrying the same command.');
26
+ const mediaId = z.string().min(1).max(128).regex(SAFE_ID).optional()
27
+ .describe('Kolbo media library id (preferred). Provide exactly one of media_id or url.');
28
+ const mediaUrl = z.string().url().max(4096).optional()
29
+ .describe('Kolbo-owned HTTPS media URL. Third-party hosts are rejected; import them into Kolbo first.');
30
+ const mediaKind = z.enum(['video', 'image', 'audio']).optional()
31
+ .describe('Media kind. Inferred from the Kolbo record or file extension when omitted.');
32
+
33
+ // ─── resolve_edit_timeline operation schema (mirrors kolbo-api resolve/schemas.js) ───
34
+ const seconds = (min = 0, max = 4 * 3600) => z.number().finite().min(min).max(max);
35
+ const track = () => z.number().int().min(1).max(50);
36
+ const clipRef = z.union([z.number().int().min(1).max(10000), noControls(128)])
37
+ .describe('Clip position on the track (1 = leftmost) or exact clip name');
38
+ const TITLE_CONTROL = new RegExp('[\\x00-\\x09\\x0b\\x0c\\x0e-\\x1f]');
39
+ const titleText = z.string().min(1).max(500).refine(
40
+ (value) => value.trim().length > 0 && !TITLE_CONTROL.test(value),
41
+ 'Text must be 1-500 characters; newlines allowed, no other control characters.',
42
+ );
43
+ const unit = () => z.number().finite().min(0).max(1);
44
+
45
+ const timelineOperation = z.discriminatedUnion('op', [
46
+ z.object({
47
+ op: z.literal('timeline.create'),
48
+ name: noControls(128).describe('New empty timeline at the project frame rate and resolution; it becomes the current timeline.'),
49
+ }).strict(),
50
+ z.object({
51
+ op: z.literal('clip.append'),
52
+ media_id: mediaId,
53
+ url: mediaUrl,
54
+ kind: mediaKind,
55
+ name: noControls(128).optional(),
56
+ track_index: track().optional().describe('Target track (video and/or audio). Missing tracks are added. Default 1.'),
57
+ record_seconds: seconds().optional().describe('Timeline position in seconds from the timeline start. Default: end of the timeline.'),
58
+ trim_start_seconds: seconds().optional().describe('Seconds skipped at the head of the source clip. Default 0.'),
59
+ duration_seconds: seconds(0.04).optional().describe('Length on the timeline. Default: rest of the source (stills: 5 s).'),
60
+ media_type: z.enum(['video', 'audio', 'both']).optional().describe('Which part of the clip to place. Default both (video files) or audio (audio files).'),
61
+ }).strict(),
62
+ z.object({
63
+ op: z.literal('clip.transition'),
64
+ track_index: track().describe('Video track'),
65
+ clip: clipRef,
66
+ position: z.enum(['start', 'end']).optional().describe('Default end'),
67
+ duration_seconds: seconds(0.04, 30).optional().describe('Default 1'),
68
+ transition: z.enum(['Cross Dissolve', 'Dip To Color Dissolve', 'Smooth Cut', 'Additive Dissolve', 'Blur Dissolve']).optional().describe('Default Cross Dissolve'),
69
+ }).strict(),
70
+ z.object({
71
+ op: z.literal('clip.delete'),
72
+ track_type: z.enum(['video', 'audio']),
73
+ track_index: track(),
74
+ clip: clipRef,
75
+ ripple: z.boolean().optional().describe('Close the gap. Default false.'),
76
+ }).strict(),
77
+ z.object({
78
+ op: z.literal('audio.fade'),
79
+ track_index: track().describe('Audio track'),
80
+ clip: clipRef,
81
+ fade_in_seconds: seconds(0, 600).optional(),
82
+ fade_out_seconds: seconds(0, 600).optional(),
83
+ }).strict(),
84
+ z.object({
85
+ op: z.literal('title.add'),
86
+ track_index: track().describe('Video track of the clip the title sits on'),
87
+ clip: clipRef,
88
+ text: titleText,
89
+ font: noControls(128).optional().describe('Font family. Default "Arial".'),
90
+ size: unit().optional().describe('Text size relative to frame height, 0.01-1. Default 0.08.'),
91
+ color: z.array(unit()).length(3).optional().describe('[r, g, b], each 0-1. Default white.'),
92
+ position: z.array(unit()).length(2).optional().describe('[x, y] centre, 0-1; [0.5, 0.5] is the frame centre, y grows upward. Default [0.5, 0.5].'),
93
+ fade_seconds: seconds(0, 10).optional().describe('Fade in at the clip start and out at its end. Default 0.5.'),
94
+ }).strict(),
95
+ z.object({
96
+ op: z.literal('marker.add'),
97
+ time_seconds: seconds().describe('Seconds from the timeline start'),
98
+ color: z.enum(['Blue', 'Cyan', 'Green', 'Yellow', 'Red', 'Pink', 'Purple', 'Fuchsia', 'Rose', 'Lavender', 'Sky', 'Mint', 'Lemon', 'Sand', 'Cocoa', 'Cream']).optional(),
99
+ name: noControls(128).optional(),
100
+ note: noControls(1000).optional(),
101
+ duration_seconds: seconds(0.04).optional(),
102
+ }).strict(),
103
+ ]);
104
+ const timelineOperations = z.array(timelineOperation).min(1).max(100)
105
+ .superRefine((ops, ctx) => {
106
+ ops.forEach((operation, index) => {
107
+ if (operation.op === 'audio.fade' && operation.fade_in_seconds === undefined && operation.fade_out_seconds === undefined) {
108
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: [index], message: 'audio.fade needs fade_in_seconds or fade_out_seconds' });
109
+ }
110
+ });
111
+ })
112
+ .refine((ops) => Buffer.byteLength(JSON.stringify(ops), 'utf8') <= 256 * 1024, 'Operations must be at most 256 KiB as JSON.');
113
+
114
+ function text(value) {
115
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
116
+ }
117
+
118
+ function envelope(type, args, payload) {
119
+ return {
120
+ ...(args.session_id ? { session_id: args.session_id } : {}),
121
+ command_type: type,
122
+ payload,
123
+ ...(args.idempotency_key ? { idempotency_key: args.idempotency_key } : {}),
124
+ };
125
+ }
126
+
127
+ function command(client, type, args, payload) {
128
+ return client.post('/v1/resolve/commands', envelope(type, args, payload)).then(text);
129
+ }
130
+
131
+ function mediaSource(args, toolName) {
132
+ if (Boolean(args.media_id) === Boolean(args.url)) {
133
+ throw new Error(`${toolName} requires exactly one of media_id or url.`);
134
+ }
135
+ if (args.url && !isTrustedMediaUrl(args.url)) {
136
+ throw new Error(`${toolName} accepts only Kolbo-owned HTTPS media URLs. Import third-party files into Kolbo first.`);
137
+ }
138
+ return {
139
+ ...(args.media_id ? { media_id: args.media_id } : { url: args.url }),
140
+ ...(args.kind ? { kind: args.kind } : {}),
141
+ ...(args.name ? { name: args.name } : {}),
142
+ };
143
+ }
144
+
145
+ function registerResolveTools(server, client) {
146
+ server.tool(
147
+ 'resolve_list_sessions',
148
+ 'List the caller\'s DaVinci Resolve Studio windows that have the Kolbo plugin connected to AI agents. Call this before the first Resolve command. If none are listed, ask the user to open Workspace → Workflow Integrations → Kolbo AI in Resolve Studio and sign in.',
149
+ {
150
+ page: z.number().int().min(1).default(1),
151
+ page_size: z.number().int().min(1).max(100).default(25),
152
+ },
153
+ async ({ page, page_size }) => text(await client.get(`/v1/resolve/sessions?page=${page}&page_size=${page_size}`))
154
+ );
155
+
156
+ server.tool(
157
+ 'resolve_get_project',
158
+ 'Queue a read-only inspection of the open DaVinci Resolve project: name, timelines, current timeline, frame rate, resolution and playhead. No approval is needed. Returns a command record; poll resolve_get_command_status for the result.',
159
+ {
160
+ session_id: sessionId,
161
+ idempotency_key: idempotencyKey,
162
+ },
163
+ async (args) => command(client, 'project.get', args, {})
164
+ );
165
+
166
+ server.tool(
167
+ 'resolve_get_timeline',
168
+ 'Queue a read-only inspection of the current DaVinci Resolve timeline: frame rate, duration, playhead, markers and every clip per video/audio track (track, position, name, start_seconds, end_seconds). No approval is needed. The clip list is truncated to max_clips (default 200).',
169
+ {
170
+ session_id: sessionId,
171
+ max_clips: z.number().int().min(1).max(1000).optional(),
172
+ idempotency_key: idempotencyKey,
173
+ },
174
+ async (args) => command(client, 'timeline.get', args, {
175
+ ...(args.max_clips ? { max_clips: args.max_clips } : {}),
176
+ })
177
+ );
178
+
179
+ server.tool(
180
+ 'resolve_import_media',
181
+ 'Import one Kolbo media item into the Kolbo.AI bin of the open DaVinci Resolve project\'s Media Pool without touching the timeline. The editor must approve it in the Kolbo plugin. Returns a command record; poll resolve_get_command_status.',
182
+ {
183
+ session_id: sessionId,
184
+ media_id: mediaId,
185
+ url: mediaUrl,
186
+ kind: mediaKind,
187
+ name: noControls(128).optional().describe('File name. Sanitized by the plugin.'),
188
+ idempotency_key: idempotencyKey,
189
+ },
190
+ async (args) => command(client, 'media.import', args, mediaSource(args, 'resolve_import_media'))
191
+ );
192
+
193
+ server.tool(
194
+ 'resolve_edit_timeline',
195
+ 'DaVinci Resolve. Apply a batch of structured timeline edits: create a timeline, place Kolbo media at exact times on chosen tracks with trims, add transitions, fade audio, put animated titles over a clip (built in its Fusion comp), add markers, or delete clips. Times are seconds from the timeline start. Clips are addressed by track plus position (1 = leftmost) or exact name. Operations run in order and stop at the first failure (earlier ones stay applied). The editor approves the whole batch once in the Kolbo plugin. Read workflows/davinci-resolve.md first; verify with resolve_get_timeline and resolve_capture_frame.',
196
+ {
197
+ session_id: sessionId,
198
+ operations: timelineOperations,
199
+ idempotency_key: idempotencyKey,
200
+ },
201
+ async (args) => {
202
+ for (const operation of args.operations) {
203
+ if (operation.op === 'clip.append') mediaSource(operation, 'resolve_edit_timeline clip.append');
204
+ }
205
+ return command(client, 'timeline.edit', args, { operations: args.operations });
206
+ }
207
+ );
208
+
209
+ server.tool(
210
+ 'resolve_run_script',
211
+ 'Run JavaScript against DaVinci Resolve\'s scripting API inside the Kolbo plugin, for anything resolve_edit_timeline does not cover (colour, Fusion, render jobs, project settings). `code` is an ASYNC FUNCTION BODY with `resolve`, `project`, `timeline` and `log(...)` in scope; every Resolve API call returns a promise, so `await` it, and `return` a JSON-serialisable result. The editor sees the exact code in the Kolbo plugin and must approve it. Scripts have full access to the project and the computer, so never read or write files or touch the network unless the user explicitly asked.',
212
+ {
213
+ session_id: sessionId,
214
+ code: z.string().min(1).max(64 * 1024).refine(
215
+ (value) => value.trim().length > 0 && Buffer.byteLength(value, 'utf8') <= 64 * 1024,
216
+ 'Script must be non-empty and at most 64 KiB as UTF-8.',
217
+ ),
218
+ purpose: noControls(500).describe('Plain-language reason shown to the editor next to the code.'),
219
+ idempotency_key: idempotencyKey,
220
+ },
221
+ async (args) => command(client, 'script.run', args, { code: args.code, purpose: args.purpose })
222
+ );
223
+
224
+ server.tool(
225
+ 'resolve_capture_frame',
226
+ 'Export one frame of the current DaVinci Resolve timeline and save it to the Kolbo media library. The result carries the image `url` - look at it to check the edit or title before telling the user it is done. Moves the playhead to time_seconds when given. The editor approves it in the Kolbo plugin.',
227
+ {
228
+ session_id: sessionId,
229
+ time_seconds: z.number().finite().min(0).max(4 * 3600).optional().describe('Seconds from the timeline start. Default: current playhead.'),
230
+ project_id: z.string().min(1).max(128).regex(SAFE_ID).optional().describe('Kolbo project to file the capture in.'),
231
+ idempotency_key: idempotencyKey,
232
+ },
233
+ async (args) => command(client, 'frame.capture', args, {
234
+ ...(args.time_seconds !== undefined ? { time_seconds: args.time_seconds } : {}),
235
+ ...(args.project_id ? { project_id: args.project_id } : {}),
236
+ })
237
+ );
238
+
239
+ server.tool(
240
+ 'resolve_get_command_status',
241
+ 'Read the state, expires_at and bounded result/error of one DaVinci Resolve command owned by the caller. awaiting_approval is not a polling state: stop and ask the user to approve or deny it in the Kolbo plugin.',
242
+ {
243
+ command_id: z.string().min(1).max(128).regex(SAFE_ID),
244
+ },
245
+ async ({ command_id: id }) => text(await client.get(`/v1/resolve/commands/${encodeURIComponent(id)}`))
246
+ );
247
+ }
248
+
249
+ module.exports = { registerResolveTools };