@kolbo/mcp 1.91.4 → 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 +18 -1
- package/package.json +1 -1
- package/skill/GENERATED.md +1 -1
- package/skill/SKILL.md +4 -2
- package/skill/references/workflows/adobe.md +52 -23
- package/skill/references/workflows/after-effects-motion.md +193 -0
- package/skill/references/workflows/davinci-resolve.md +115 -0
- package/src/index.js +2 -0
- package/src/toolAnnotations.js +8 -0
- package/src/tools/adobe.js +154 -0
- package/src/tools/resolve.js +249 -0
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ Every generation tool also accepts an optional `project_id` arg that routes the
|
|
|
264
264
|
| `blender_get_command_status` | Read bounded command status/result/error plus absolute `expires_at`; records and idempotency claims expire after 24 hours, and `awaiting_approval` means stop and wait for the user |
|
|
265
265
|
|
|
266
266
|
**Premiere Pro & After Effects Bridge**
|
|
267
|
-
Requires the Kolbo Studio panel open in Premiere Pro or After Effects with **AI agents** switched on in its header. Every edit is approved by the editor inside the panel;
|
|
267
|
+
Requires the Kolbo Studio panel open in Premiere Pro or After Effects with **AI agents** switched on in its header. Every edit is approved by the editor inside the panel; scripts are shown in full before they run.
|
|
268
268
|
|
|
269
269
|
| Tool | Description |
|
|
270
270
|
|------|-------------|
|
|
@@ -275,8 +275,25 @@ Requires the Kolbo Studio panel open in Premiere Pro or After Effects with **AI
|
|
|
275
275
|
| `adobe_place_on_timeline` | Import and place one Kolbo media item at the playhead of the work sequence / active comp (no time or track control in v1) |
|
|
276
276
|
| `adobe_create_sequence` | Create and open a Premiere Pro sequence matching the open sequence's settings, no dialog (Premiere only) |
|
|
277
277
|
| `adobe_import_captions` | Import a Kolbo-hosted SRT onto the active Premiere sequence (Premiere only) |
|
|
278
|
+
| `adobe_edit_composition` | After Effects: one undo step of structured edits - comps, timed and trimmed Kolbo media, titles with stroke, solids, transforms, keyframes, audio fades, layer deletion |
|
|
279
|
+
| `adobe_run_script` | Approval-gated ExtendScript for real motion graphics (shape layers, trim paths, text animators, effects, expressions, cameras); the editor reviews the exact code |
|
|
280
|
+
| `adobe_capture_frame` | Render a comp/sequence frame to the Kolbo library so the agent can check its work |
|
|
278
281
|
| `adobe_get_command_status` | Read command status/result/error; `awaiting_approval` means stop and wait for the editor |
|
|
279
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
|
+
|
|
280
297
|
**Discovery & Account**
|
|
281
298
|
| Tool | Description |
|
|
282
299
|
|------|-------------|
|
package/package.json
CHANGED
package/skill/GENERATED.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AUTO-GENERATED — do not edit
|
|
2
2
|
|
|
3
|
-
This tree is mirrored from kolbo-code@
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
-
|
|
26
|
-
-
|
|
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
|
-
##
|
|
59
|
+
## Scripts (`adobe_run_script`)
|
|
29
60
|
|
|
30
|
-
- `
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- `
|
|
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`
|
|
40
|
-
2.
|
|
41
|
-
3. A command is complete only when `adobe_get_command_status` shows `succeeded`.
|
|
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.
|
package/src/toolAnnotations.js
CHANGED
|
@@ -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 = [
|
|
@@ -105,12 +106,19 @@ const OPEN_WORLD_WRITE = [
|
|
|
105
106
|
'publish_html_artifact', 'create_review_share_link', 'blender_capture_viewport',
|
|
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',
|
|
109
|
+
'adobe_capture_frame',
|
|
110
|
+
// Resolve imports add Media Pool items; captures add a Kolbo library image.
|
|
111
|
+
'resolve_import_media', 'resolve_capture_frame',
|
|
108
112
|
];
|
|
109
113
|
|
|
110
114
|
const OPEN_WORLD_DESTRUCTIVE = [
|
|
111
115
|
'share_doc', 'revoke_review_share_link',
|
|
112
116
|
'blender_apply_operations', 'blender_import_media', 'blender_render',
|
|
113
117
|
'blender_undo', 'blender_file_operation', 'blender_execute_python',
|
|
118
|
+
// Can delete layers; approved once per batch in the Kolbo panel.
|
|
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',
|
|
114
122
|
];
|
|
115
123
|
|
|
116
124
|
const CONTRACT_GROUPS = [
|
package/src/tools/adobe.js
CHANGED
|
@@ -31,6 +31,114 @@ const mediaUrl = z.string().url().max(4096).optional()
|
|
|
31
31
|
const mediaKind = z.enum(['video', 'image', 'audio']).optional()
|
|
32
32
|
.describe('Media kind. Inferred from the Kolbo record or file extension when omitted.');
|
|
33
33
|
|
|
34
|
+
// ─── adobe_edit_composition operation schema (mirrors kolbo-api adobe/schemas.js) ───
|
|
35
|
+
const seconds = (min = 0) => z.number().finite().min(min).max(3600);
|
|
36
|
+
const color = () => z.array(z.number().finite().min(0).max(1)).length(3).describe('[r, g, b], each 0-1');
|
|
37
|
+
const point = () => z.array(z.number().finite().min(-100000).max(100000)).length(2).describe('[x, y] in composition pixels; [0, 0] is top-left');
|
|
38
|
+
const layerRef = z.union([z.number().int().min(1).max(10000), noControls(128)])
|
|
39
|
+
.describe('Layer name (exact) or 1-based index, 1 = top layer');
|
|
40
|
+
// Titles may span lines; every other control character is refused.
|
|
41
|
+
const TITLE_CONTROL = new RegExp('[\\x00-\\x09\\x0b\\x0c\\x0e-\\x1f]');
|
|
42
|
+
const titleText = z.string().min(1).max(500).refine(
|
|
43
|
+
(value) => value.trim().length > 0 && !TITLE_CONTROL.test(value),
|
|
44
|
+
'Text must be 1-500 characters; newlines allowed, no other control characters.',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const compOperation = z.discriminatedUnion('op', [
|
|
48
|
+
z.object({
|
|
49
|
+
op: z.literal('comp.create'),
|
|
50
|
+
name: noControls(128),
|
|
51
|
+
width: z.number().int().min(16).max(8192).optional().describe('Default 1920'),
|
|
52
|
+
height: z.number().int().min(16).max(8192).optional().describe('Default 1080'),
|
|
53
|
+
frame_rate: z.number().finite().min(1).max(120).optional().describe('Default 30'),
|
|
54
|
+
duration_seconds: seconds(0.1),
|
|
55
|
+
background_color: color().optional(),
|
|
56
|
+
}).strict(),
|
|
57
|
+
z.object({
|
|
58
|
+
op: z.literal('comp.update'),
|
|
59
|
+
name: noControls(128).optional(),
|
|
60
|
+
duration_seconds: seconds(0.1).optional(),
|
|
61
|
+
background_color: color().optional(),
|
|
62
|
+
}).strict(),
|
|
63
|
+
z.object({
|
|
64
|
+
op: z.literal('layer.add_media'),
|
|
65
|
+
media_id: mediaId,
|
|
66
|
+
url: mediaUrl,
|
|
67
|
+
kind: mediaKind,
|
|
68
|
+
name: noControls(128).optional().describe('Layer name; defaults to the file name. Name layers you will animate later.'),
|
|
69
|
+
start_seconds: seconds().optional().describe('Where the layer starts on the comp timeline. Default 0.'),
|
|
70
|
+
trim_start_seconds: seconds().optional().describe('Seconds skipped at the head of the source clip. Default 0.'),
|
|
71
|
+
duration_seconds: seconds(0.04).optional().describe('Visible length. Default: rest of the source (stills: rest of the comp).'),
|
|
72
|
+
fit: z.enum(['cover', 'contain', 'none']).optional().describe('Scale to fill the frame (cover, default), fit inside it, or keep size.'),
|
|
73
|
+
}).strict(),
|
|
74
|
+
z.object({
|
|
75
|
+
op: z.literal('layer.add_text'),
|
|
76
|
+
text: titleText,
|
|
77
|
+
name: noControls(128).optional(),
|
|
78
|
+
start_seconds: seconds().optional(),
|
|
79
|
+
duration_seconds: seconds(0.04),
|
|
80
|
+
font: noControls(128).optional().describe('PostScript font name. Default "Arial-BoldMT".'),
|
|
81
|
+
font_size: z.number().finite().min(4).max(1000).optional().describe('Default 100'),
|
|
82
|
+
color: color().optional().describe('Default white'),
|
|
83
|
+
stroke_color: color().optional().describe('Outline colour. Default black when stroke_width is set.'),
|
|
84
|
+
stroke_width: z.number().finite().min(0).max(100).optional().describe('Outline in pixels. Use 2-6 whenever text sits over bright or busy footage.'),
|
|
85
|
+
position: point().optional().describe('Centre of the text block. Default: centre of the comp.'),
|
|
86
|
+
}).strict(),
|
|
87
|
+
z.object({
|
|
88
|
+
op: z.literal('layer.add_solid'),
|
|
89
|
+
color: color(),
|
|
90
|
+
name: noControls(128).optional(),
|
|
91
|
+
start_seconds: seconds().optional(),
|
|
92
|
+
duration_seconds: seconds(0.04).optional(),
|
|
93
|
+
}).strict(),
|
|
94
|
+
z.object({
|
|
95
|
+
op: z.literal('layer.update'),
|
|
96
|
+
layer: layerRef,
|
|
97
|
+
new_name: noControls(128).optional(),
|
|
98
|
+
start_seconds: seconds().optional(),
|
|
99
|
+
trim_start_seconds: seconds().optional(),
|
|
100
|
+
duration_seconds: seconds(0.04).optional(),
|
|
101
|
+
position: point().optional(),
|
|
102
|
+
scale: z.number().finite().min(0).max(10000).optional().describe('Percent, uniform'),
|
|
103
|
+
opacity: z.number().finite().min(0).max(100).optional(),
|
|
104
|
+
rotation: z.number().finite().min(-36000).max(36000).optional().describe('Degrees'),
|
|
105
|
+
audio_levels: z.number().finite().min(-96).max(24).optional().describe('dB'),
|
|
106
|
+
enabled: z.boolean().optional(),
|
|
107
|
+
}).strict(),
|
|
108
|
+
z.object({
|
|
109
|
+
op: z.literal('layer.animate'),
|
|
110
|
+
layer: layerRef,
|
|
111
|
+
property: z.enum(['opacity', 'scale', 'position', 'rotation', 'audio_levels'])
|
|
112
|
+
.describe('opacity 0-100, scale percent, rotation degrees, audio_levels dB, position [x, y]'),
|
|
113
|
+
keyframes: z.array(z.object({
|
|
114
|
+
time_seconds: seconds().describe('Comp time of this key'),
|
|
115
|
+
value: z.union([z.number().finite().min(-36000).max(36000), point()]),
|
|
116
|
+
}).strict()).min(1).max(50),
|
|
117
|
+
easing: z.enum(['ease', 'linear']).optional().describe('Default ease'),
|
|
118
|
+
}).strict(),
|
|
119
|
+
z.object({
|
|
120
|
+
op: z.literal('layer.delete'),
|
|
121
|
+
layer: layerRef,
|
|
122
|
+
}).strict(),
|
|
123
|
+
]);
|
|
124
|
+
const compOperations = z.array(compOperation).min(1).max(100)
|
|
125
|
+
.superRefine((ops, ctx) => {
|
|
126
|
+
ops.forEach((operation, opIndex) => {
|
|
127
|
+
if (operation.op !== 'layer.animate') return;
|
|
128
|
+
const wantsPoint = operation.property === 'position';
|
|
129
|
+
operation.keyframes.forEach((key, index) => {
|
|
130
|
+
if (Array.isArray(key.value) !== wantsPoint) {
|
|
131
|
+
ctx.addIssue({
|
|
132
|
+
code: z.ZodIssueCode.custom,
|
|
133
|
+
path: [opIndex, 'keyframes', index, 'value'],
|
|
134
|
+
message: wantsPoint ? 'position keyframes need [x, y]' : `${operation.property} keyframes need a number`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
})
|
|
140
|
+
.refine((ops) => Buffer.byteLength(JSON.stringify(ops), 'utf8') <= 256 * 1024, 'Operations must be at most 256 KiB as JSON.');
|
|
141
|
+
|
|
34
142
|
function text(value) {
|
|
35
143
|
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
36
144
|
}
|
|
@@ -162,6 +270,52 @@ function registerAdobeTools(server, client) {
|
|
|
162
270
|
}
|
|
163
271
|
);
|
|
164
272
|
|
|
273
|
+
server.tool(
|
|
274
|
+
'adobe_edit_composition',
|
|
275
|
+
'After Effects only. Apply a batch of structured edits to a composition as ONE undo step: create or update a comp, add Kolbo media (trimmed and timed), text titles and solids, change layer timing/transform/opacity/audio, animate with keyframes, or delete layers. Times are in seconds on the composition timeline. Layers are addressed by the exact name you gave them or by 1-based index (1 = top). New layers stack on top; solids go to the bottom. Operations run in order and stop at the first failure (earlier ones stay applied). The editor approves the whole batch once in the Kolbo panel. Read workflows/adobe.md before the first call; verify with adobe_get_timeline afterwards.',
|
|
276
|
+
{
|
|
277
|
+
session_id: sessionId,
|
|
278
|
+
operations: compOperations,
|
|
279
|
+
idempotency_key: idempotencyKey,
|
|
280
|
+
},
|
|
281
|
+
async (args) => {
|
|
282
|
+
for (const operation of args.operations) {
|
|
283
|
+
if (operation.op === 'layer.add_media') mediaSource(operation, 'adobe_edit_composition layer.add_media');
|
|
284
|
+
}
|
|
285
|
+
return command(client, 'comp.edit', args, { operations: args.operations });
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
server.tool(
|
|
290
|
+
'adobe_run_script',
|
|
291
|
+
'Run ExtendScript inside the connected After Effects (or Premiere Pro) for real motion graphics: shape layers, trim paths, text animators, effects, masks, expressions, cameras, precomps. `code` is a FUNCTION BODY: use log(...) for progress and `return` a JSON-serialisable result. In After Effects the whole script is one undo step. The editor sees the exact code in the Kolbo panel and must approve it. Scripts have full access to the project and the computer, so never read or write files, call system.callSystem, or touch the network unless the user explicitly asked. Read workflows/after-effects-motion.md before writing motion graphics, then verify with adobe_capture_frame.',
|
|
292
|
+
{
|
|
293
|
+
session_id: sessionId,
|
|
294
|
+
code: z.string().min(1).max(64 * 1024).refine(
|
|
295
|
+
(value) => value.trim().length > 0 && Buffer.byteLength(value, 'utf8') <= 64 * 1024,
|
|
296
|
+
'Script must be non-empty and at most 64 KiB as UTF-8.',
|
|
297
|
+
),
|
|
298
|
+
purpose: noControls(500).describe('Plain-language reason shown to the editor next to the code.'),
|
|
299
|
+
idempotency_key: idempotencyKey,
|
|
300
|
+
},
|
|
301
|
+
async (args) => command(client, 'script.run', args, { code: args.code, purpose: args.purpose })
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
server.tool(
|
|
305
|
+
'adobe_capture_frame',
|
|
306
|
+
'Render one frame of the active After Effects composition (PNG) or Premiere Pro sequence (JPEG) and save it to the Kolbo media library. The result carries the image `url` - look at it to check your motion graphics or edit before telling the user it is done. The editor approves it in the Kolbo panel.',
|
|
307
|
+
{
|
|
308
|
+
session_id: sessionId,
|
|
309
|
+
time_seconds: z.number().finite().min(0).max(3600).optional().describe('Frame time. Default: current playhead / comp time.'),
|
|
310
|
+
project_id: z.string().min(1).max(128).regex(SAFE_ID).optional().describe('Kolbo project to file the capture in.'),
|
|
311
|
+
idempotency_key: idempotencyKey,
|
|
312
|
+
},
|
|
313
|
+
async (args) => command(client, 'frame.capture', args, {
|
|
314
|
+
...(args.time_seconds !== undefined ? { time_seconds: args.time_seconds } : {}),
|
|
315
|
+
...(args.project_id ? { project_id: args.project_id } : {}),
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
|
|
165
319
|
server.tool(
|
|
166
320
|
'adobe_get_command_status',
|
|
167
321
|
'Read the state, expires_at and bounded result/error of one Adobe 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 panel.',
|
|
@@ -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 };
|