@atomcut/mcp 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/package.json +52 -0
- package/skill/design-systems/atomcut-default/DESIGN.md +107 -0
- package/skill/skills/atomcut/SKILL.md +95 -0
- package/skill/skills/atomcut/references/api.md +88 -0
- package/skill/skills/atomcut/references/color-and-grading.md +45 -0
- package/skill/skills/atomcut/references/editing.md +51 -0
- package/skill/skills/atomcut/references/effects.md +32 -0
- package/skill/skills/atomcut/references/fills-strokes-vectors.md +55 -0
- package/skill/skills/atomcut/references/layers-and-layout.md +62 -0
- package/skill/skills/atomcut/references/media-and-generation.md +59 -0
- package/skill/skills/atomcut/references/motion.md +62 -0
- package/skill/skills/atomcut/references/recipes.md +49 -0
- package/skill/skills/atomcut/references/reporting.md +29 -0
- package/skill/skills/atomcut/references/review.md +44 -0
- package/skill/skills/atomcut/references/sound.md +40 -0
- package/skill/skills/atomcut/references/type-and-captions.md +52 -0
- package/src/cli.mjs +194 -0
- package/src/protocol.ts +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @atomcut/mcp
|
|
2
|
+
|
|
3
|
+
Let **Claude Code**, **Claude Desktop**, **Cursor** — any [MCP](https://modelcontextprotocol.io) client — drive a running [AtomCut](https://atomcut.net) editor: design, animate, edit video, generate media, all live on your canvas.
|
|
4
|
+
|
|
5
|
+
| Your agent app | Add AtomCut |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Claude Code | `claude mcp add -s user atomcut -- npx -y @atomcut/mcp` |
|
|
8
|
+
| Codex | `codex mcp add atomcut -- npx -y @atomcut/mcp` |
|
|
9
|
+
| Gemini CLI | `gemini mcp add -s user atomcut npx -y @atomcut/mcp` |
|
|
10
|
+
| VS Code | `code --add-mcp '{"name":"atomcut","command":"npx","args":["-y","@atomcut/mcp"]}'` |
|
|
11
|
+
| Cursor · Claude Desktop · anything else | a stdio server: command `npx`, args `["-y", "@atomcut/mcp"]` |
|
|
12
|
+
|
|
13
|
+
Then in AtomCut: **Agents ▸ Use your agent app ▸ Link**. That's it — the tab stays linked and reconnects whenever your agent starts a session. Ask for anything: "cut the pauses out of this interview and caption it".
|
|
14
|
+
|
|
15
|
+
It works with whatever model your agent app runs — MCP is between the app and AtomCut, not the model.
|
|
16
|
+
|
|
17
|
+
What your agent gets:
|
|
18
|
+
|
|
19
|
+
- **Every editor verb as a tool** — the same ~70 commands AtomCut's own agent uses (create and style layers, keyframes, effects, grading, timeline edits, captions, AI media jobs), with live JSON schemas.
|
|
20
|
+
- **Eyes** — `view_render` / `view_frames` return real renders as images, so it checks its own work.
|
|
21
|
+
- **The AtomCut skill** as resources (`atomcut://skill/SKILL.md` and references) — how to work well in the app, plus the default design system.
|
|
22
|
+
- **Safety** — the bridge listens on `127.0.0.1` only and trusts only AtomCut's own origins (`app.atomcut.net`, the desktop app, `localhost`); a tab served from anywhere else must present the pairing token printed on start (also in `~/.atomcut-mcp`; extra origins via `ATOMCUT_MCP_ORIGINS`); every change is labelled with your agent's name in AtomCut's Undo menu, one step per request.
|
|
23
|
+
|
|
24
|
+
MIT licensed.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atomcut/mcp",
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Drive a running AtomCut editor from Claude Code, Claude Desktop, Cursor or any MCP client \u2014 every editor verb as a tool, renders as images, the AtomCut skill as resources.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"atomcut-mcp": "./src/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./protocol": {
|
|
11
|
+
"types": "./src/protocol.ts",
|
|
12
|
+
"default": "./src/protocol.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/cli.mjs",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
18
|
+
"prepack": "node scripts/pack-skill.mjs"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"typescript": "^5.7.2"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"ws": "^8.21.3"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"atomcut",
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"motion-design",
|
|
33
|
+
"video-editing"
|
|
34
|
+
],
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/novincode/atomcut"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://atomcut.net/docs/ai/claude-code",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"src/cli.mjs",
|
|
45
|
+
"src/protocol.ts",
|
|
46
|
+
"skill",
|
|
47
|
+
"README.md"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: AtomCut Default
|
|
3
|
+
description: The default design system for work made in AtomCut — a type scale, spacing, safe areas, colour roles and motion tokens that make any comp look deliberate. Replace it with a brand's own system when there is one.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# AtomCut Default design system
|
|
8
|
+
|
|
9
|
+
Tokens are **relative to the comp** so one system serves a 1080×1920 reel and a 3840×2160 film. `U` = the comp's **short side / 100** (10.8px on 1080×1920 and 1920×1080; 21.6px on 4K).
|
|
10
|
+
|
|
11
|
+
## Grid and safe areas
|
|
12
|
+
|
|
13
|
+
- **Margin**: 6U from each edge for primary content (≈ 65px at 1080). Title-safe = 90% of the frame; action-safe = 95%.
|
|
14
|
+
- **Vertical video (9:16)**: keep text out of the bottom 15% and the right 12% — platform UI lives there. Top 8% is for status bars.
|
|
15
|
+
- **Columns**: landscape 12 columns, gutter 2U; vertical 4 columns, gutter 2U. Align edges to columns; centre only what stands alone.
|
|
16
|
+
- **Rhythm**: vertical spacing in multiples of 1U; related items 1–2U apart, groups 4–6U apart.
|
|
17
|
+
|
|
18
|
+
## Type scale (steep, ≈×1.6 — hierarchy has to read in the half-second a viewer gives it)
|
|
19
|
+
|
|
20
|
+
| Role | Size | Weight | Tracking | Leading |
|
|
21
|
+
|---|---|---|---|---|
|
|
22
|
+
| Display | 16U | 800 | −3% | 0.95 |
|
|
23
|
+
| Title | 10U | 700–800 | −2% | 1.0 |
|
|
24
|
+
| Heading | 6.5U | 700 | −1% | 1.1 |
|
|
25
|
+
| Body | 4U | 400–500 | 0 | 1.4 |
|
|
26
|
+
| Caption (on video) | 4.5–5.5U | 700 | 0 | 1.15 |
|
|
27
|
+
| Kicker / label (caps) | 2.6U | 700 | +18% | 1.0 |
|
|
28
|
+
| Fine print | 2.2U | 500 | +2% | 1.3 |
|
|
29
|
+
|
|
30
|
+
- Display fits **≤ 12 characters a line** (≤ 8 on 9:16); longer lines step down to Title. Never more than three sizes in one shot.
|
|
31
|
+
- Numbers in data or timers: tabular figures (`fontFeatures` `tnum`).
|
|
32
|
+
|
|
33
|
+
Families: **Inter** for workhorse text; **Space Grotesk** or **Sora** for geometric display; **Playfair Display** / **Fraunces** when editorial or luxury; **JetBrains Mono** for code and data. Two families at most per piece.
|
|
34
|
+
|
|
35
|
+
## Colour roles
|
|
36
|
+
|
|
37
|
+
A composition uses roles, not arbitrary colours:
|
|
38
|
+
|
|
39
|
+
| Role | Default | Use |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `ground` | `#0B0B12` | background (dark default; invert for light pieces) |
|
|
42
|
+
| `surface` | `#15151E` | cards, pills, panels |
|
|
43
|
+
| `ink` | `#F4F4FA` | primary text |
|
|
44
|
+
| `ink-2` | `#A9A9C4` | secondary text |
|
|
45
|
+
| `accent` | `#9C7CFF` | one focal colour per shot |
|
|
46
|
+
| `accent-2` | `#FF5C8A` | rare second accent (states, highlights) |
|
|
47
|
+
| `line` | `#FFFFFF1F` | hairlines and dividers |
|
|
48
|
+
|
|
49
|
+
Light pieces swap the ground and ink: `ground #F6F5F2`, `surface #FFFFFF`, `ink #121218`, `ink-2 #5B5B6E`, `line #1212181A`; the accents stay.
|
|
50
|
+
|
|
51
|
+
- One accent per shot, used on 5–10% of the frame.
|
|
52
|
+
- Text contrast ≥ 4.5:1 against what is actually behind it (check the render).
|
|
53
|
+
- Gradients stay within a hue family: ground → a deeper or warmer version of itself.
|
|
54
|
+
- On footage, text sits on a scrim (ground at 40–60% alpha), a pill (`surface`), or with a soft shadow (0 2U 6U #000 at 50%).
|
|
55
|
+
|
|
56
|
+
## Shape language
|
|
57
|
+
|
|
58
|
+
- Corner radius: 1.5U small (chips, tags), 3U cards, full (height/2) pills.
|
|
59
|
+
- Strokes: 0.2U hairlines, 0.5U emphasis. Round caps for anything drawn on.
|
|
60
|
+
- Shadows: soft and low — one light source, top-left.
|
|
61
|
+
|
|
62
|
+
## Motion tokens
|
|
63
|
+
|
|
64
|
+
| Token | Duration | Easing |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `micro` | 200ms | ease-out |
|
|
67
|
+
| `enter` | 600ms | ease-out-expo |
|
|
68
|
+
| `enter-large` | 900ms | ease-out-expo |
|
|
69
|
+
| `exit` | 400ms | ease-in |
|
|
70
|
+
| `move` | 700ms | ease-in-out |
|
|
71
|
+
| `settle` | 500ms | ease-out-back (sparingly) |
|
|
72
|
+
|
|
73
|
+
- **Stagger**: letters 18ms · words 90–120ms · lines 150ms · list items 80ms.
|
|
74
|
+
- **Distance**: entrances travel 2–4U; big reveals up to 8U. Nothing slides in from off-frame unless it is a transition.
|
|
75
|
+
- **Order**: kicker → title → supporting text → decoration. The eye should never have two things arriving at once.
|
|
76
|
+
- **Holds**: a finished title holds ≥ 1.2s (+ 50ms per word) before anything changes.
|
|
77
|
+
- **Loops**: continuous drift uses `linear` or `sin()` expressions at 0.1–0.3 Hz — calm, not busy.
|
|
78
|
+
|
|
79
|
+
## Captions
|
|
80
|
+
|
|
81
|
+
- Focus word: `ink`, weight 800, with an `accent` pill (radius full, padding 0.6U × 0.3U) or an accent colour — one or the other, not both.
|
|
82
|
+
- Read words: `ink` at 100%; unread words: `ink` at 55% (lyrics: read 35%, upcoming 55%).
|
|
83
|
+
- 2 lines at most on screen; 3–5 words a caption for talk, 1–2 for punchy social.
|
|
84
|
+
|
|
85
|
+
## Imagery and generated media
|
|
86
|
+
|
|
87
|
+
When drawing or shooting with AI, brief it in the system's terms so it belongs next to the type:
|
|
88
|
+
|
|
89
|
+
- **Palette**: name `ground` and `accent` in the prompt ("deep ink-blue shadows, a single violet accent light"); avoid saturated rainbow scenes.
|
|
90
|
+
- **Light**: one motivated key, soft fill, gentle falloff; cinematic contrast, not HDR.
|
|
91
|
+
- **Composition**: leave the title's area calm (say where: "empty upper third"); subject on a third, not dead centre, unless it is a product hero.
|
|
92
|
+
- **Finish**: natural film grain at most; no text, logos or watermarks in the image — type is set in AtomCut.
|
|
93
|
+
- **Footage**: slow, deliberate camera moves (push-in, lateral track); 24–30 fps feel; 4–8 s shots.
|
|
94
|
+
- **Voice**: warm, clear, mid-pace; conversational rather than announcer. **Music**: minimal and modern, 90–110 bpm, instrumental under voice, clean ending.
|
|
95
|
+
|
|
96
|
+
## Sound tokens
|
|
97
|
+
|
|
98
|
+
- UI pops and clicks: volume 0.15–0.35, randomness 0.4–0.6, pitch 0.9–1.2. Never on every letter — per word or per element.
|
|
99
|
+
- Whooshes on big moves only; one per transition.
|
|
100
|
+
- Voice sits at full level; music ducks under it (−12 dB).
|
|
101
|
+
|
|
102
|
+
## Do / don't
|
|
103
|
+
|
|
104
|
+
- Do align to the grid and share edges; don't nudge by eye.
|
|
105
|
+
- Do let one element lead; don't animate everything at once.
|
|
106
|
+
- Do check the busiest frame for legibility; don't trust a single still.
|
|
107
|
+
- Don't stack more than two effects on text. Don't use pure `#000`/`#FFF` for large fields — use `ground`/`ink`.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: atomcut
|
|
3
|
+
description: How to design, animate and edit inside AtomCut — the motion design and video studio — through its command API. Use for any request to create or change anything in an AtomCut project.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Working in AtomCut
|
|
8
|
+
|
|
9
|
+
AtomCut is a motion design and video studio: an infinite canvas of **comps** (artboards, each with its own timeline and fps), layers that animate, effects, colour grading, captions, sound and export. You drive it through commands (tools). The user sees every change live, can undo your whole turn in one step, and can message you while you work.
|
|
10
|
+
|
|
11
|
+
You are the senior motion designer, video editor and art director on the job. The bar is: **it looks intentional**. Hierarchy, rhythm, restraint, and motion that means something.
|
|
12
|
+
|
|
13
|
+
## The loop
|
|
14
|
+
|
|
15
|
+
1. **Orient** — `doc_overview` first (comps, sizes, selection). `layers_list` for the comp you will touch. Read no more than the task needs.
|
|
16
|
+
2. **Plan in one breath** — what the result looks like, the layers, the timing. For anything with more than two layers, decide the layout grid and type scale before creating anything (see the design system).
|
|
17
|
+
3. **Build** — prefer one `api_batch` per idea (a card and its contents, a set of staggered keys). Use `$0.id` to reference earlier results inside a batch.
|
|
18
|
+
4. **Look** — `view_render` after visible changes; `view_frames` for anything that moves. Pick the cheapest detail that answers your question (`glance` for layout, `look` default, `inspect` for type).
|
|
19
|
+
5. **Refine** — fix what you saw: overlaps, cramped margins, weak contrast, jumpy easing, text off the safe area. Look again.
|
|
20
|
+
6. **Report** — tell the user what you made in two or three sentences, and `report_issue` anything you could not do.
|
|
21
|
+
|
|
22
|
+
## Coordinates, time, identity
|
|
23
|
+
|
|
24
|
+
- Comp space, **origin at the comp's centre, +y down**. A 1080×1920 comp spans x −540…540, y −960…960. `transform.x/y` place the layer's **pivot** (centre by default).
|
|
25
|
+
- Auto layout containers are the exception: their box is placed by its **top-left** (`layout.x/y`).
|
|
26
|
+
- Times are **milliseconds**. Layer start/end are comp time; **keyframe times are relative to the layer's start** (use `clock: "comp"` in `keyframes_set` to think in comp time).
|
|
27
|
+
- Layers are addressed by id (`clip_…`, `group_…`). Commands also accept an exact, unique layer **name** — name things well and you can refer to them by name.
|
|
28
|
+
|
|
29
|
+
## The command map
|
|
30
|
+
|
|
31
|
+
| Job | Commands |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Read | `doc_overview`, `layers_list`, `layer_get`, `layer_properties`, `media_list` |
|
|
34
|
+
| Learn the shape of anything | `api_describe` (any type, any path), `catalog_list` / `catalog_get` (effects, animations, templates, transitions, easings) |
|
|
35
|
+
| Make layers | `layer_create` (text · shape · path from SVG `d` · media · nested comp · adjustment) |
|
|
36
|
+
| Change values | `layer_set` (property paths; keys when keyed), `text_set`, `text_style` (a word or range) |
|
|
37
|
+
| Change structure | `layer_patch` (anything else in a layer's data, schema-validated), `template_apply` |
|
|
38
|
+
| Motion | `animation_set` (in/out presets), `keyframes_set` / `keyframes_clear`, `expression_set` |
|
|
39
|
+
| Effects & colour | `effects_add` / `effects_update`, `grade_node` / `grade_effect` / `grade_read` |
|
|
40
|
+
| Structure | `layers_group` (+ auto layout), `layout_set`, `layers_nest`, `layers_restack`, `layers_duplicate`, `layers_delete` |
|
|
41
|
+
| Time | `timeline_move`, `timeline_trim`, `timeline_split`, `timeline_cut_ranges` (cut + ripple in sync), `timeline_close_gaps`, `transition_add`, `audio_detach` / `audio_rejoin` (J/L-cuts) |
|
|
42
|
+
| Comps | `frame_create`, `frame_update`, `frame_delete` |
|
|
43
|
+
| See | `view_render`, `view_frames` |
|
|
44
|
+
| Media | `media_import`, `media_inspect`, `audio_analyze`, `font_install`, `sounds_list` |
|
|
45
|
+
| Words from sound | `media_transcribe` (text · pauses · captions · `lyrics` timing), `media_remove_pauses` |
|
|
46
|
+
| Make with AI | `generate_image` (draw · edit · references), `generate_video` (from words · a still · footage), `generate_speech` + `voices_list`, `generate_sound`, `generate_music` |
|
|
47
|
+
| Fix with AI | `media_remove_background` (cut-out), `media_upscale`, `audio_isolate` (clean voice) |
|
|
48
|
+
| Session | `session_set` (select, playhead, open a comp, working range) |
|
|
49
|
+
| Many steps | `api_batch` |
|
|
50
|
+
| Diagnose / improve | `app_console`, `report_issue`, `history_undo` (your own steps only) |
|
|
51
|
+
|
|
52
|
+
Commands you do not see in your tool list are not available in this session. An AI media job whose provider is not connected fails with a hint naming who can do it — pass that on to the user rather than improvise.
|
|
53
|
+
|
|
54
|
+
## Three levels of writing — use the highest that fits
|
|
55
|
+
|
|
56
|
+
1. **Verbs** (`layer_create`, `animation_set`, `effects_add`, `layers_group`…) — they keep invariants you would otherwise break (placement, key rules, caption cues, ids).
|
|
57
|
+
2. **`layer_set` by property path** — any animatable value: `x`, `y`, `rotation`, `scale`, `opacity`, `fontSize`, `fill.<paintId>.color`, `lfx.<effectId>.<field>`, `stroke.<id>.width`… `layer_properties` lists every path a layer has, with current values and ranges. After Effects rule: once a property has keys, writing it adds a key at `atMs`.
|
|
58
|
+
3. **`layer_patch`** — everything else (lists, modes, options): `set`, `merge`, `insert`, `remove`, `move` by path (`effects[id=lfx_1].enabled`, `fills[0]`, `strokes[-]`). Missing ids are minted for you. The whole layer is re-validated; an invalid patch changes nothing and names the wrong field. `api_describe` first when you have not written that shape before.
|
|
59
|
+
|
|
60
|
+
The API describes itself from the live schemas, so nothing in it is ever out of date: a field or effect that ships next month appears in `api_describe`, `layer_properties` and `catalog_list` the day it lands. Trust them over memory.
|
|
61
|
+
|
|
62
|
+
## Craft, in brief
|
|
63
|
+
|
|
64
|
+
Read the references for depth. The non-negotiables:
|
|
65
|
+
|
|
66
|
+
- **Hierarchy**: one focal point per moment. Size, weight, colour and motion all agree on what matters most.
|
|
67
|
+
- **Type**: two families at most, a clear scale (see the design system), tight tracking on big display type, generous line height on body. Never set on-screen text under ~3% of the comp's short side for video.
|
|
68
|
+
- **Space**: consistent margins from a grid; keep text inside title-safe (90%). Align edges; do not eyeball what should share an edge — use auto layout.
|
|
69
|
+
- **Motion**: every move has a reason and an easing. Entrances ease **out**, exits ease **in**, moves between two rests ease **in-out**. Stagger 30–80ms per unit. Overlap actions; nothing starts from a dead stop in unison unless that is the point.
|
|
70
|
+
- **Colour**: a restrained palette — a dominant, a neutral, one accent. When text sits on imagery, fix contrast with a scrim, shadow or highlight pill before adding weight.
|
|
71
|
+
- **Restraint**: one hero effect per shot. Glow, glass and grain are seasoning.
|
|
72
|
+
|
|
73
|
+
## Knowledge on demand
|
|
74
|
+
|
|
75
|
+
Read with `skill_read` (pack "atomcut") when the task touches the subject — not before:
|
|
76
|
+
|
|
77
|
+
- `references/api.md` — conventions, batching, property path grammar, errors and recovery
|
|
78
|
+
- `references/layers-and-layout.md` — layer kinds, transforms and pivots, groups, auto layout, nesting, components, masks and booleans
|
|
79
|
+
- `references/type-and-captions.md` — typography, ranges, text effects, templates, text on a path, captions, lyric styles
|
|
80
|
+
- `references/motion.md` — animation principles, presets, keyframes, easing, motion paths, expressions, modulators, motion blur
|
|
81
|
+
- `references/fills-strokes-vectors.md` — paints, gradients, patterns, strokes and draw-on, shapes, pen paths, booleans, drawing and flipbooks
|
|
82
|
+
- `references/color-and-grading.md` — palettes, contrast, the grade graph, building a look from a feeling
|
|
83
|
+
- `references/effects.md` — the effect families and when each earns its place
|
|
84
|
+
- `references/editing.md` — cutting, pacing, dead air, J/L-cuts, speed, reframing, long → short, versions
|
|
85
|
+
- `references/sound.md` — audio layers, sounds on animation, audio effects, voiceover
|
|
86
|
+
- `references/media-and-generation.md` — attachments, import, references; drawing and editing images, shooting and editing video, voices, sound effects, music, cut-outs, upscaling, provenance
|
|
87
|
+
- `references/review.md` — looking efficiently, and the checklist before you call anything done
|
|
88
|
+
- `references/recipes.md` — worked builds: title card, lower third, lyric captions, kinetic type, reel, promo, logo reveal
|
|
89
|
+
- `references/reporting.md` — when and how to file `report_issue`
|
|
90
|
+
|
|
91
|
+
The enabled design system (`<design_system>`) sets the tokens: type scale, spacing, colour roles, motion timing. Follow it unless the user asks otherwise; a user's own design system wins over the default.
|
|
92
|
+
|
|
93
|
+
## When you cannot do something
|
|
94
|
+
|
|
95
|
+
Say so plainly, do what you can, and `report_issue` with an honest analysis — what you tried, what happened, what should have happened, and the fix in API or skill terms. Reports are how this skill and the API improve. Never fake a result, and never claim something looks right that you did not look at.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# The API — conventions and patterns
|
|
2
|
+
|
|
3
|
+
Every command is a tool. Names use `_` in tool calls (`layer_create`) and `.` inside `api_batch` (`layer.create`); both work anywhere a command id is accepted.
|
|
4
|
+
|
|
5
|
+
## Results and errors
|
|
6
|
+
|
|
7
|
+
- Results are compact JSON. Reads summarise; ask for more with `fields`, a narrower `path`, or a `group`.
|
|
8
|
+
- A failed call changes nothing. Errors say which field is wrong and what to do: read the `Hint:` line, fix, retry. Three kinds:
|
|
9
|
+
- **input** — the arguments did not match the schema (`style.fontSize: Expected number`). Fix the named field.
|
|
10
|
+
- **api** — the request was valid but cannot apply (`No layer with id…`, `"glow" does not apply to a shape`). The hint names alternatives.
|
|
11
|
+
- **internal** — a bug. Retry once with a different approach, then `report_issue` (category `api-bug`).
|
|
12
|
+
- Typos get "Did you mean": `rotaton` → `rotation`.
|
|
13
|
+
|
|
14
|
+
## Reading, cheapest first
|
|
15
|
+
|
|
16
|
+
| Need | Call |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Comps, sizes, selection, playhead | `doc_overview` |
|
|
19
|
+
| What is in a comp, where, when | `layers_list` (bounds and flags per layer) |
|
|
20
|
+
| One layer's data | `layer_get` (bulky parts folded — ask with `fields: ["subpaths"]`) |
|
|
21
|
+
| What a layer can animate, and current values | `layer_properties` (optionally one `group`) |
|
|
22
|
+
| The exact shape of a type | `api_describe {type, path}` — e.g. `{type:"text", path:"style"}`, `{type:"layerEffect", path:"[type=glass]"}`, `{type:"paint"}`, `{type:"layout"}` |
|
|
23
|
+
| What exists to choose | `catalog_list {catalog, query}` then `catalog_get` for fields/knobs |
|
|
24
|
+
|
|
25
|
+
## Batching
|
|
26
|
+
|
|
27
|
+
`api_batch` runs calls in order and returns every result. Reference an earlier result with a string that is exactly `$<index>` or `$<index>.<path>`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{ "calls": [
|
|
31
|
+
{ "command": "layer.create", "input": { "kind": "shape", "shape": "rect", "width": 640, "height": 180, "fill": "#15151C", "cornerRadius": 28 } },
|
|
32
|
+
{ "command": "layer.create", "input": { "kind": "text", "text": "New drop", "placement": { "above": "$0.id" } } },
|
|
33
|
+
{ "command": "layers.group", "input": { "ids": ["$0.id", "$1.id"], "name": "Chip", "layout": { "padding": { "top": 24, "right": 40, "bottom": 24, "left": 40 } } } }
|
|
34
|
+
] }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Stops at the first failure (later calls are skipped and listed) unless `continueOnError: true`. Batches do not nest. Up to 40 calls.
|
|
38
|
+
|
|
39
|
+
Parallel tool calls in one turn also work: reads run together; writes run in the order you wrote them.
|
|
40
|
+
|
|
41
|
+
## Property paths (`layer_set`, `keyframes_set`, `expression_set`)
|
|
42
|
+
|
|
43
|
+
One grammar everywhere — keyframes, drivers, expressions, modulators, the node board:
|
|
44
|
+
|
|
45
|
+
| Path | Means |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `x` `y` `rotation` `scale` `scaleY` `skewX` `skewY` `anchor.x` `anchor.y` | transform (scaleY present = non-uniform) |
|
|
48
|
+
| `opacity` `feather` `borderRadius.tl…` `crop.top…` `content.x/y/scale/rotation` | layer box and picture |
|
|
49
|
+
| `width` `height` `radius.tl…` | shape geometry |
|
|
50
|
+
| `fontSize` `letterSpacing` `lineHeight` `color` `fontFamily` `fontWeight` `italic` `maxWidth` `text` | typography (text layers) |
|
|
51
|
+
| `fill.<paintId>` / `fill.<paintId>.opacity` / `fill.<paintId>.stop.<stopId>.color` | paints |
|
|
52
|
+
| `stroke.<strokeId>.width` / `.trim.end` … | strokes |
|
|
53
|
+
| `lfx.<effectId>.<field>` | a layer effect's field |
|
|
54
|
+
| `tfx.<effectId>.<field>` | a text effect's field |
|
|
55
|
+
| `afx.<effectId>.<field>` | an audio effect's field |
|
|
56
|
+
| `lfx.<gradeCard>.n.<node>.fx.<effect>.<field>` | a value inside a colour grade node |
|
|
57
|
+
| `audioMix.volume` `playback.speed` | sound level; playback speed |
|
|
58
|
+
| `mblur.*` | the layer's shutter |
|
|
59
|
+
|
|
60
|
+
Never guess a path: `layer_properties` lists them for the actual layer, with ranges.
|
|
61
|
+
|
|
62
|
+
## Patching (`layer_patch`)
|
|
63
|
+
|
|
64
|
+
Ops: `set` (replace), `merge` (shallow-merge an object), `insert` (into a list; `index` or append), `remove`, `move` (`to` index). Path steps: `a.b`, `[2]`, `[id=fx_1]` (prefer — stable), `[type=glass]`, `[-]` (append).
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{ "id": "Title", "ops": [
|
|
68
|
+
{ "op": "merge", "path": "style", "value": { "align": "left", "textCase": "upper" } },
|
|
69
|
+
{ "op": "insert", "path": "strokes", "value": { "paint": { "type": "solid", "color": "#000000" }, "width": 6, "align": "outside" } },
|
|
70
|
+
{ "op": "set", "path": "effects[type=shadow].enabled", "value": false }
|
|
71
|
+
] }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Missing `id`s in anything you insert are minted. The whole layer is re-validated; nothing lands unless all of it is valid. Some fields belong to verbs and are refused here: `text` (use `text_set`), timing (`timeline_*`), `frameId`, `kind`, `id`.
|
|
75
|
+
|
|
76
|
+
## Timing rules
|
|
77
|
+
|
|
78
|
+
- `startMs`/`durationMs` are comp time; placement floors a new layer onto the comp's frame grid.
|
|
79
|
+
- Keyframe `at` is **layer-relative** by default. A key can sit before 0 or after the end (it still shapes the curve).
|
|
80
|
+
- Easing on a key shapes the segment **leaving** it. Shorthand: `"linear"`, a preset (`ease-out`, `ease-in`, `ease-in-out`, `ease-out-quart`, `ease-out-expo`, `ease-out-back`, `ease-in-out-back`, `ease-out-elastic`) or `"cubic-bezier(x1,y1,x2,y2)"`.
|
|
81
|
+
|
|
82
|
+
## Undo
|
|
83
|
+
|
|
84
|
+
All your writes in one user request are ONE undo step, labelled "AI · <their request>". If the user edits in between, your next writes start a new step. `history_undo` reverts your own most recent step only — useful to back out a wrong direction wholesale.
|
|
85
|
+
|
|
86
|
+
## When something is missing
|
|
87
|
+
|
|
88
|
+
If no verb, path or patch can express what you need, that is an API gap: do the closest honest thing, tell the user, and `report_issue` (`api-gap`) with the exact capability you needed.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Colour and grading
|
|
2
|
+
|
|
3
|
+
## Palettes
|
|
4
|
+
|
|
5
|
+
- Build from roles: ground, surface, ink, secondary ink, one accent (the design system names them). Use the accent on 5–10% of the frame.
|
|
6
|
+
- Harmonies that stay tasteful: **analogous** (neighbours on the wheel) for calm; **complementary accent** (one small warm spot in a cool scene) for focus; **monochrome + one accent** for premium.
|
|
7
|
+
- Dark scenes: never pure black — tint the ground toward the accent's hue (`#0B0A12` for violet, `#0A0F12` for teal).
|
|
8
|
+
- Saturation budget: one saturated element; everything else muted.
|
|
9
|
+
- Contrast: body text ≥ 4.5:1 against what is behind it *at the busiest frame*.
|
|
10
|
+
|
|
11
|
+
## The grade graph
|
|
12
|
+
|
|
13
|
+
A colour grade is an effect card (`grade`) holding a node graph, like DaVinci Resolve: **Source → correctors → Output**. Each corrector holds an ordinary stack of colour effects; nodes can run in series, in parallel (mixed back in), or as an "outside" node (the inverse of another node's key — grade the background of a secondary).
|
|
14
|
+
|
|
15
|
+
1. `grade_node {id, name: "Balance"}` — creates the grade with its first corrector.
|
|
16
|
+
2. `grade_effect {id, nodeId, type, values}` — put colour cards in it. `catalog_list layer-effects` with query `color` lists them (`adjust`, `colorGrade`, `gradientMap`, `vignette`, `posterize`, `overlay` …); `catalog_get` shows each field and range.
|
|
17
|
+
3. More nodes in order: `grade_node {after: <nodeId>, name: "Look"}`; parallel: `{parallelTo}`; secondary background: `{outsideOf}`.
|
|
18
|
+
4. `grade_read` shows the graph. Every value keyframes at `lfx.<card>.n.<node>.fx.<effect>.<field>` — animate a grade (a look that warms as the sun comes up).
|
|
19
|
+
|
|
20
|
+
Keys (qualifier by hue/sat/lum, depth, power windows that can follow a tracker) limit a node to part of the picture: edit with `layer_patch` on the node's `key` (`api_describe {type:"layerEffect", path:"[type=grade]"}`).
|
|
21
|
+
|
|
22
|
+
Grade the **whole edit** with an adjustment layer on top (`layer_create {kind:"adjustment"}`) and put the grade on it.
|
|
23
|
+
|
|
24
|
+
## Node order that works
|
|
25
|
+
|
|
26
|
+
1. **Balance** — exposure and white balance: neutral whites, blacks just above clipping, contrast to taste.
|
|
27
|
+
2. **Match** — make shots agree (same skin, same sky) before styling.
|
|
28
|
+
3. **Look** — the creative intent (below).
|
|
29
|
+
4. **Secondaries** — skin protection, sky, a product colour — keyed nodes.
|
|
30
|
+
5. **Finish** — vignette, grain, halation-style glow, gentle sharpening.
|
|
31
|
+
|
|
32
|
+
## A look from a feeling
|
|
33
|
+
|
|
34
|
+
| The user says | Move |
|
|
35
|
+
|---|---|
|
|
36
|
+
| warm, nostalgic, golden hour | temperature +, highlights toward amber, shadows toward teal-brown, saturation −10%, lifted blacks, soft grain |
|
|
37
|
+
| cinematic, blockbuster | teal shadows / orange skin (keep skin natural), contrast +, saturation of blues/greens down, slight vignette |
|
|
38
|
+
| clean, commercial, Apple-ish | neutral whites, high clarity, gentle contrast, saturation natural, no grain |
|
|
39
|
+
| moody, noir, tense | exposure −, contrast +, desaturate toward monochrome, cool shadows, crushed blacks, vignette |
|
|
40
|
+
| dreamy, soft, romantic | lifted blacks, low contrast, pastel highlights (pink/peach), glow/bloom, grain |
|
|
41
|
+
| retro 70s film | warm mids, faded blacks, greens toward olive, posterized highlights slightly, strong grain |
|
|
42
|
+
| cyberpunk, neon night | magenta/cyan split, saturated highlights, deep blue shadows, glow on lights |
|
|
43
|
+
| documentary, honest | neutral, restrained contrast, preserve skin, nothing stylised |
|
|
44
|
+
|
|
45
|
+
Always look at the result (`view_render` at `inspect` on a frame with a face or the key subject). Skin is the reference — if faces look wrong, the grade is wrong.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Editing video like a professional
|
|
2
|
+
|
|
3
|
+
## Reading the material first
|
|
4
|
+
|
|
5
|
+
- `media_list` and `layers_list` for what exists; `media_inspect` on a video for a contact sheet of its content.
|
|
6
|
+
- Speech: `media_transcribe {id}` gives the words and the pauses (in comp time for a layer). This is the script you edit against.
|
|
7
|
+
- Sound: `audio_analyze {id}` gives loudness and every silent stretch (`compSilences` for a layer).
|
|
8
|
+
|
|
9
|
+
## Cutting
|
|
10
|
+
|
|
11
|
+
- **`timeline_cut_ranges {id, ranges, ripple}`** takes stretches of comp time out of a layer in one step (one undo): razor, delete, ripple. `ripple: "sync"` (default) keeps the comp in sync like an NLE's sync-lock — captions and titles after a cut slide back with their words, a music bed spanning it is shortened instead of jumping. `"lane"` closes only this layer's lane; `"none"` leaves holes for B-roll.
|
|
12
|
+
- **Dead air**: `media_remove_pauses {id, minGapMs: 600, padMs: 120}` hears the words and cuts every longer pause plus the dead head and tail. `dryRun: true` returns the ranges first. Leave natural pauses after a punchline or a question — raise `minGapMs` (900–1200) for a calm interview, lower it (350–450) for punchy social.
|
|
13
|
+
- **"Cut the bullshit"** — the editorial loop:
|
|
14
|
+
1. `media_transcribe {id}`; decide what stays (the strongest lines, in an order that tells the story);
|
|
15
|
+
2. from the word timings, collect the ranges that go — cut in the pauses, ~100ms after the last kept word and ~80ms before the next;
|
|
16
|
+
3. `timeline_cut_ranges {id, ranges}` — one call, the comp stays in sync.
|
|
17
|
+
- For a single cut: `timeline_split {id, atMs}`, `layers_delete`, `timeline_close_gaps {ids}`.
|
|
18
|
+
- Cut on action or on a sentence boundary, never mid-word. A cut in the middle of a movement hides the join.
|
|
19
|
+
- Jump cuts on a talking head are fine for social; soften them with a punch-in (below) or a B-roll cover.
|
|
20
|
+
|
|
21
|
+
## J-cuts and L-cuts
|
|
22
|
+
|
|
23
|
+
The audio leads (J) or trails (L) the picture across a cut — it is what makes an edit flow. `audio_detach {id}` separates a video's own sound into a linked audio layer (the video is muted, the sound stays in sync). Then trim the sound on its own: for a J-cut, extend the incoming shot's audio to start 300–800ms before its picture (`timeline_trim` its start earlier) and let the outgoing picture run over it; for an L-cut, let the outgoing sound run past the picture cut. `audio_rejoin` puts a sound back. A separate music or voice layer just needs `timeline_move` / `timeline_trim`.
|
|
24
|
+
|
|
25
|
+
## Pacing
|
|
26
|
+
|
|
27
|
+
- Social: a new visual beat every 1.5–3s; the hook in the first 1s (the most striking line or image first, not the intro).
|
|
28
|
+
- Explainers: 3–6s per shot; let key visuals hold.
|
|
29
|
+
- Music: cut on beats (downbeats for big changes, off-beats for energy). Place cuts on the frame of the transient.
|
|
30
|
+
|
|
31
|
+
## Speed
|
|
32
|
+
|
|
33
|
+
`layer_set {values: {"playback.speed": 1.5}}` changes a clip's playback speed; key `playback.speed` for ramps (fast through the boring part, slow into the moment). Keep speech between 0.9× and 1.15× (voice keeps pitch on voice-tagged audio). Slow motion needs high-fps footage to look smooth.
|
|
34
|
+
|
|
35
|
+
## Reframing and punch-ins
|
|
36
|
+
|
|
37
|
+
- A vertical cut from horizontal footage: `frame_create {width:1080, height:1920}`, then `layer_create {kind:"comp", sourceFrameId: <original comp>}` or the media itself, scaled to cover (`scale` ≈ 1920/1080 = 1.78 for 16:9 → 9:16) and positioned on the subject; key `x` to follow the action.
|
|
38
|
+
- Punch-in on a talking head: key the picture's `content.scale` 1 → 1.12 at a cut point (hard, not eased) to fake a second camera; or ease `content.scale` slowly (1 → 1.05 over the shot) for a push-in.
|
|
39
|
+
- Crop without moving the picture: `crop.*`; move the picture inside the window: `content.x/y`.
|
|
40
|
+
|
|
41
|
+
## Transitions
|
|
42
|
+
|
|
43
|
+
Mostly hard cuts. `transition_add` for a reason: `cross-dissolve` for time passing, `dip-to-color`/`fade` for section breaks, `whip-pan`/`push` for energy between similar shots, `zoom`/`glitch-cut`/`shake-cut` for social beats. 200–500ms. One transition style per piece.
|
|
44
|
+
|
|
45
|
+
## Versions (long and short)
|
|
46
|
+
|
|
47
|
+
Build the long version first. For a short version: duplicate the comp's content into a new comp (`frame_create` + nest, or `layers_duplicate` then move), keep the hook and the peak, cut everything between to fit the target (15s, 30s, 60s), re-time music to end on a beat. Same captions and look — only the edit changes. Offer both aspect ratios when the destination is social.
|
|
48
|
+
|
|
49
|
+
## Captions and titles in an edit
|
|
50
|
+
|
|
51
|
+
Caption every social cut (most watch muted): `media_transcribe {id, captions: {maxWords: 3}}`, then style consistently. Lower thirds for names: 1.5–3s after the person starts talking, hold ≥ 3s.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Effects
|
|
2
|
+
|
|
3
|
+
Effects are ordered stacks; the first card applies first. Three domains:
|
|
4
|
+
|
|
5
|
+
- **Layer effects** (`effects`, path `lfx.<id>.<field>`) — process the whole layer's pixels.
|
|
6
|
+
- **Text effects** (`style.textEffects`, `tfx.…`) — per-glyph: shadow, glow, extrude, echo. They move with each letter's animation.
|
|
7
|
+
- **Audio effects** (`audioEffects`, `afx.…`) — EQ, reverb, echo, compressor, saturator.
|
|
8
|
+
|
|
9
|
+
`effects_add {id, domain, type, values}` · `effects_update {id, effectId, values|enabled|index|remove}` · `catalog_list layer-effects` (with `query`) · `catalog_get layer-effects <type>` for fields, ranges and defaults. Every numeric field keyframes and can be driven by an expression or modulator.
|
|
10
|
+
|
|
11
|
+
## The families and when they earn a place
|
|
12
|
+
|
|
13
|
+
| Family | Types | Use for |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Shadow & glow | shadow, innerShadow, glow, longShadow | separation from the background (soft shadow), neon and light (glow), graphic depth (long shadow) |
|
|
16
|
+
| Blur | blur, directionalBlur, zoomBlur, spinBlur, sharpen | depth of field on backgrounds, speed (directional), impact (zoom), focus pulls (animated blur) |
|
|
17
|
+
| Colour | adjust, grade, colorGrade, gradientMap, posterize, overlay, vignette | correction and looks (see colour-and-grading.md) |
|
|
18
|
+
| Material | glass | liquid-glass panels over busy backgrounds — refracts what is behind; one pane per shot |
|
|
19
|
+
| Light | shine, lightRays, lensFlare, lightning | a sweep across a logo (shine), god rays, a flare on a reveal, energy |
|
|
20
|
+
| Analog | grain, filmDamage, scanlines, noise | texture and era; grain at low strength ties graphics to footage |
|
|
21
|
+
| Distort | rgbSplit, bulge, wave, twirl, lensDistort, goo, roughenEdges, glitch | glitch transitions, liquid/organic motion, hand-made edges |
|
|
22
|
+
| Stylize | stroke, halftone, pixelate, bevel, chromaKey | outlines, print looks, retro games, green screen — `chromaKey` is live and free for a FLAT backdrop (green screen, studio white); for a real-world background use `media_remove_background` (AI cut-out, image or video) |
|
|
23
|
+
|
|
24
|
+
## Rules of thumb
|
|
25
|
+
|
|
26
|
+
- One hero effect per shot. Stack at most two on text.
|
|
27
|
+
- Shadows: large blur, low opacity, offset downward — a soft lift, not a smudge.
|
|
28
|
+
- Glow reads best on dark grounds with a saturated source colour.
|
|
29
|
+
- Glass needs something interesting behind it; over a flat colour it is invisible.
|
|
30
|
+
- Animated effects (a shine sweep, a glitch burst, a blur pull) should be brief — 200–600ms — and tied to a beat or a reveal.
|
|
31
|
+
- Chroma key: `chromaKey` on the footage, then tune the key colour, tolerance and spill.
|
|
32
|
+
- Background removal on a still image exists in the editor (context menu); through the API use `generate_image` edits with `parentAssetId` when a provider can, or tell the user.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Fills, strokes, shapes and vectors
|
|
2
|
+
|
|
3
|
+
## Paint (fills and strokes)
|
|
4
|
+
|
|
5
|
+
Every visual layer has ordered `fills` and `strokes` (bottom → top), Figma-style. A paint is one of (`api_describe paint`):
|
|
6
|
+
|
|
7
|
+
| type | use |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `solid` | a colour (`#RRGGBB`, `#RRGGBBAA`, or `var:<id>` for a colour variable) |
|
|
10
|
+
| `linear` / `radial` / `angular` | gradient: `stops [{offset 0–1, color}]`, `start`/`end` in the layer's box (0–1) |
|
|
11
|
+
| `media` | an image/video as fill: `source` self or an asset, `fit` cover/contain/fill/tile, `focus`, `adjust` |
|
|
12
|
+
| `pattern` | stripes · grid · checks · dots · squares · cross · triangles · zigzag · waves · noise · image — with scale, angle, spacing, thickness, colours |
|
|
13
|
+
| `variable` | a paint variable (shared styles) |
|
|
14
|
+
|
|
15
|
+
Each paint has `opacity` and `blendMode`. Shortcut: `layer_create {fill: "#FF3366"}`; for gradients pass a paint object; stops and ids are minted.
|
|
16
|
+
|
|
17
|
+
Craft:
|
|
18
|
+
- Gradients: neighbouring hues (analogous) or a colour to a darker version of itself. Avoid muddy complements through grey. Two to three stops.
|
|
19
|
+
- A soft radial glow (colour at 60–70% alpha → same colour at 0) behind a subject adds depth cheaply.
|
|
20
|
+
- Patterns at low opacity with `overlay`/`soft-light` blend add texture without noise.
|
|
21
|
+
- Animate paint: `fill.<paintId>` (colour), `fill.<paintId>.stop.<stopId>.color`, `.opacity`, gradient angle fields — `layer_properties` lists them.
|
|
22
|
+
|
|
23
|
+
## Strokes
|
|
24
|
+
|
|
25
|
+
A stroke = paint + `width`, `align` (center/inside/outside), `cap`, `join`, `dash`, `dashOffset`, plus the pipeline: `trim` (start/end/offset — **draw-on**), `offset`, `profile` (taper), `wave`, `roughness`, `scatter`, `nib` (calligraphic), `markers` (arrowheads). Insert with `layer_patch` (`insert` into `strokes`).
|
|
26
|
+
|
|
27
|
+
- **Draw-on**: key `stroke.<id>.trim.end` 0 → 1 (ease-in-out, 800–1500ms), or `animation_set preset "draw"` on an outline layer.
|
|
28
|
+
- Hand-drawn: `roughness` + round caps; marker: thick + taper `profile`; technical: thin + dashes.
|
|
29
|
+
- Outline text: an `outside` stroke on a text layer (Impact/outline looks).
|
|
30
|
+
|
|
31
|
+
## Shapes
|
|
32
|
+
|
|
33
|
+
`layer_create {kind:"shape", shape: rect|ellipse|polygon|star, width, height, fill, stroke, cornerRadius, pointCount, innerRatio}`. Animatable: `width`, `height`, `radius.tl/tr/br/bl` (per-corner), star/polygon radius; ellipse arcs (`arcStartDeg`, `arcSweepDeg`, `arcRatio` — pies, rings, progress arcs) via `layer_patch`/`layer_set`.
|
|
34
|
+
|
|
35
|
+
- Pill: rect with corner radius = height/2.
|
|
36
|
+
- Progress ring: ellipse with `arcRatio` 0.85 and keyed `arcSweepDeg`.
|
|
37
|
+
- Any box can round its corners (`borderRadius`) — images and video too.
|
|
38
|
+
|
|
39
|
+
## Paths (pen)
|
|
40
|
+
|
|
41
|
+
`layer_create {kind:"path", d: "<SVG path data>", fill, stroke}` — any vector outline, in the layer's own space (the layer's pivot is its box centre). Compose shapes as SVG: `M` move, `L` line, `C` cubic, `Q` quad, `A` arc, `Z` close. One path layer = one piece of artwork (a compound path — holes included); separate pieces that need their own colour or motion are separate layers.
|
|
42
|
+
|
|
43
|
+
- Keep path coordinates around 0,0 and place the layer with `x/y`.
|
|
44
|
+
- Icons: draw simple geometric marks from primitives; for real icons ask the user or use the Library.
|
|
45
|
+
- Morphing: path geometry keyframes on the `subpaths` property — same point count on both ends morphs cleanly.
|
|
46
|
+
|
|
47
|
+
## Booleans and masks
|
|
48
|
+
|
|
49
|
+
Group the pieces, set the group's `mode` (`union` · `subtract` · `intersect` · `exclude` · `mask`) and mark the cutters `isStencil: true`. Non-destructive: move a cutter and the result updates. A mask group reveals content only inside the stencil — animated stencils make wipes and reveals.
|
|
50
|
+
|
|
51
|
+
## Drawing, painting, flipbooks
|
|
52
|
+
|
|
53
|
+
- The editor has raster brushes, vector brushes, an eraser and a fill bucket for **hand** drawing in drawing/flipbook comps. Painting strokes pixel by pixel is **not available through the API**. Build illustration from shapes and paths instead, or generate an image (`generate_image`) when a painted look is wanted. If a request needs real brushwork, tell the user and `report_issue` (`api-gap`) so the need is counted.
|
|
54
|
+
- Frame-by-frame (flipbooks): each cell is a nested comp; you can read and restyle cells as layers but not draw into them.
|
|
55
|
+
- A layer can step at its own `fps` (12 = on twos) for a hand-animated feel.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Layers, structure and layout
|
|
2
|
+
|
|
3
|
+
## Comps and layers
|
|
4
|
+
|
|
5
|
+
- A **comp** (frame) is an artboard with its own size, fps, duration and timeline. Pages hold comps. Source comps of nested layers live off-page and are reached by entering their layer.
|
|
6
|
+
- A **layer** (clip) is one of: `text`, `shape` (rect · ellipse · polygon · star), `path` (vector outline), `image`, `video`, `audio`, `nested` (a comp inside a comp), `group`, `waveform`, `raster` (paint), `adjustment` (grades what is beneath it).
|
|
7
|
+
- A caption is a text layer carrying word cues (`caption`). Text on a path is a text layer riding another layer's outline (`path`).
|
|
8
|
+
- Stacking: the top of `layers_list` renders on top. Lanes (tracks) are kind-agnostic.
|
|
9
|
+
|
|
10
|
+
## Transform and pivot
|
|
11
|
+
|
|
12
|
+
`transform.x/y` place the **pivot** (`anchor`), stored as a fraction of the layer's own box: `{0.5,0.5}` centre, `{0,0.5}` left-middle, `{0,0}` top-left. Rotation and scale act about it.
|
|
13
|
+
|
|
14
|
+
- Left-aligned stacks: set `anchor.x: 0` on each line and give them the same `x`. Now edges align exactly and text grows to the right.
|
|
15
|
+
- A mirrored layer is a **negative scale** (there is no flip flag).
|
|
16
|
+
- `scale` alone is uniform; set `scaleY` for non-uniform.
|
|
17
|
+
- Cropped media has two transforms: `transform` moves the window; `content.*` pans and zooms the picture inside it (a punch-in without moving the frame).
|
|
18
|
+
|
|
19
|
+
## Groups
|
|
20
|
+
|
|
21
|
+
`layers_group {ids, name}` wraps layers in one; `layers_ungroup` dissolves. A group's time span is a window over its members. Groups have **modes** (`layer_patch` → `mode`):
|
|
22
|
+
|
|
23
|
+
| mode | effect |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `pass` | a plain group |
|
|
26
|
+
| `union` | members merge into one silhouette (effects apply once to the whole) |
|
|
27
|
+
| `mask` | content shows only inside the stencil |
|
|
28
|
+
| `subtract` / `intersect` / `exclude` | vector booleans: content − / ∩ / ⊻ stencil |
|
|
29
|
+
|
|
30
|
+
Mark the cutter(s) with `isStencil: true` (`layer_patch`) — any number of stencils union first.
|
|
31
|
+
|
|
32
|
+
## Auto layout
|
|
33
|
+
|
|
34
|
+
A group with `layout` ARRANGES its members like Figma/CSS flexbox. Use it for anything that should stay aligned when text changes: buttons, chips, cards, lower thirds, lists, lyric lines.
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{ "ids": ["Pill", "Label"], "name": "Button",
|
|
38
|
+
"layout": { "flow": "row", "gap": 12, "padding": { "top": 18, "right": 32, "bottom": 18, "left": 32 }, "alignX": "center", "alignY": "center" } }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Spec (`api_describe layout`): `flow` row · column · wrap · grid (`columns`), `gap`, `lineGap`, `spaceBetween`, `padding`, `alignX`/`alignY` (start · center · end), `width`/`height` (`null` = hug), min/max sizes, `clipContent`. The box's **top-left** is `layout.x/y` (comp space).
|
|
42
|
+
|
|
43
|
+
- **A rect + a text grouped with layout** — the rect becomes the container's background and hugs the text (a button). `layout: {}` infers a layout from how members already sit.
|
|
44
|
+
- Per member (`layoutItem` via `layer_patch`): `fillX`/`fillY` (stretch), `absolute` (opt out and place freely — a badge on a card's corner), min/max.
|
|
45
|
+
- Change it later with `layout_set`; `layout: null` removes it, keeping members where they are.
|
|
46
|
+
|
|
47
|
+
## Nesting, components, reuse
|
|
48
|
+
|
|
49
|
+
- `layers_nest {ids}` precomposes layers into their own comp — one layer outside, a timeline inside. Pixels do not move. Use it to animate a finished card as a unit, reuse a sequence, or give a group its own fps/duration.
|
|
50
|
+
- `layer_create {kind:"comp", sourceFrameId}` places a comp inside another (a scene in an edit, a card reused three times).
|
|
51
|
+
- Components (instances that update together) are made in the editor; treat instance layers like nested comps.
|
|
52
|
+
|
|
53
|
+
## Adjustment layers
|
|
54
|
+
|
|
55
|
+
`layer_create {kind:"adjustment"}` spans the comp and applies its effect stack to everything **below** it — a grade over a whole edit, a vignette, grain. Its crop/feather/radius limit the region.
|
|
56
|
+
|
|
57
|
+
## Organising a project
|
|
58
|
+
|
|
59
|
+
- **Name every layer** for what it is ("Title", "Price tag", "BG gradient"). You and the user both navigate by names.
|
|
60
|
+
- One idea, one group. Nest when a unit has its own timing.
|
|
61
|
+
- Keep the stack readable top→bottom: overlays, text, subject, backgrounds.
|
|
62
|
+
- Put media in folders (`media_import {folder}`); generated media files itself under "AI generated".
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Media, references and generation
|
|
2
|
+
|
|
3
|
+
## Files the user gives you
|
|
4
|
+
|
|
5
|
+
Attachments arrive as `<attachment id="att_…" name="…" type="…" size="…" />` (images also arrive as pictures you can see). The bytes stay in the chat until a tool uses the id:
|
|
6
|
+
|
|
7
|
+
| Attachment | What to do |
|
|
8
|
+
|---|---|
|
|
9
|
+
| image / video / audio to use | `media_import {attachmentId, folder?, place?}` — moves the bytes into the project once; `place` puts it on the timeline. Attachments can also be passed straight to AI jobs (`edit`, `from`, `references`, `id`) without importing first |
|
|
10
|
+
| font file | `font_install {attachmentId}` → the family name to set as `fontFamily` |
|
|
11
|
+
| a sound effect for animation | import it, then use `custom: {assetId}` in a layer's `sounds` |
|
|
12
|
+
| a reference ("make it like this") | `media_inspect {attachmentId}` — a video becomes a contact sheet of frames; study it (below) |
|
|
13
|
+
| text, Markdown, JSON, CSV, subtitles | already inlined in the message — read it |
|
|
14
|
+
|
|
15
|
+
## Reverse-engineering a reference
|
|
16
|
+
|
|
17
|
+
When the user says "design like this", decompose before building:
|
|
18
|
+
|
|
19
|
+
1. **Layout** — grid, margins, alignment, the size relationship between elements.
|
|
20
|
+
2. **Type** — category (geometric sans, grotesk, serif, script), weight, case, tracking, scale ratio.
|
|
21
|
+
3. **Colour** — ground, accent, how much of each; gradients and their direction.
|
|
22
|
+
4. **Motion** (video) — what moves first, the easing character (snappy expo vs. soft), stagger, direction, durations; count frames between contact-sheet tiles to estimate timing.
|
|
23
|
+
5. **Texture** — grain, glow, blur, shadows.
|
|
24
|
+
|
|
25
|
+
Say in one line what you took from it, build with AtomCut's own pieces, then compare your `view_frames` with the reference and adjust. Recreate the idea, not a copy of someone's brand.
|
|
26
|
+
|
|
27
|
+
## Making and fixing media with AI
|
|
28
|
+
|
|
29
|
+
Each job runs on whichever connection the user routed to it (Agents → Connections). A job with nothing connected fails with a hint naming who can do it — tell the user; do not work around it. Everything lands in the Media folder **"AI generated"** with its provider, model and prompt on the asset. Edits keep the original and point back at it (`parentAssetId`) — never destroy the user's media. The user sees the layer being worked on shimmer on the canvas and timeline, and can undo the whole thing in one step.
|
|
30
|
+
|
|
31
|
+
### Pictures
|
|
32
|
+
|
|
33
|
+
- **Draw**: `generate_image {prompt, aspect, count, place?}`. Prompt like an art director: subject, composition (where the empty space goes — leave room for type), light, lens, palette, medium or style, mood. Say "no text" when type will be set in AtomCut; it will be sharper, editable and on-brand. Match `aspect` to where it goes.
|
|
34
|
+
- **Edit**: `generate_image {prompt, edit: <image layer | asset | attachment>}` changes that picture ("remove the cup", "make it dusk", "same product on marble"). On an image LAYER the result swaps in place — transform, timing, effects and keys kept (`replace: false` to place it beside instead). Describe the change, not the whole scene again.
|
|
35
|
+
- **Follow references**: `references: [ids]` — a style, a product, a face to keep consistent across shots.
|
|
36
|
+
- **Transparency**: `transparent: true` for stickers and cut-out objects (models that cannot draw alpha get a cut-out pass when one is connected).
|
|
37
|
+
- **Cut out a subject**: `media_remove_background {id}` — image → PNG with alpha, video → WebM with alpha. For a flat green/white backdrop the live `chromaKey` effect is cheaper and adjustable.
|
|
38
|
+
- **More resolution**: `media_upscale {id, factor: 2}` — the layer keeps its size on the canvas, only sharper. Use before punching in on small footage or a low-res logo.
|
|
39
|
+
|
|
40
|
+
### Footage
|
|
41
|
+
|
|
42
|
+
- **Shoot**: `generate_video {prompt, aspect, durationS, audio?, place?}` — takes minutes; say so before starting. Prompt like a DP: subject and action, camera move (slow push-in, orbit, handheld), lens, light, mood, and the sound if the model scores it (`audio: true`). 4–8 s clips, cut together, beat one long take.
|
|
43
|
+
- **Animate a still**: `from: <image>` makes it the first frame — keeps a brand look or a generated product consistent.
|
|
44
|
+
- **Edit footage**: `edit: <video>` ("make it snow", "change the jacket to red"). A video LAYER's edit lands on the lane above, aligned in time, so it can be compared, masked or cross-faded. Short clips edit best.
|
|
45
|
+
|
|
46
|
+
### Voice, sound, music, words
|
|
47
|
+
|
|
48
|
+
- `generate_speech {text, voice?, style?}` · `voices_list {query}` — see sound.md.
|
|
49
|
+
- `generate_sound {prompt, durationS}` · `generate_music {prompt, durationS, instrumental}` — see sound.md.
|
|
50
|
+
- `audio_isolate {id}` — clean up a noisy voice.
|
|
51
|
+
- `media_transcribe {id}` — words, pauses, captions; `lyrics` aligns known text. `media_remove_pauses {id}` tightens speech. See editing.md and type-and-captions.md.
|
|
52
|
+
|
|
53
|
+
### Cost and patience
|
|
54
|
+
|
|
55
|
+
Video and music take minutes and cost the user money. Do the cheap thing first (draw a still, check it with `view_render`, then animate it), generate one version unless asked for options, and never loop regenerating hoping for luck — change the prompt with intent.
|
|
56
|
+
|
|
57
|
+
## Organising media
|
|
58
|
+
|
|
59
|
+
Name imports meaningfully, use `folder` to group (Footage, Music, SFX, Logos). Check `media_list` before importing the same thing twice.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Motion
|
|
2
|
+
|
|
3
|
+
## Principles that make motion read as designed
|
|
4
|
+
|
|
5
|
+
1. **Purpose** — motion guides the eye to what matters next. If a move does not direct attention or express something, cut it.
|
|
6
|
+
2. **Easing** — nothing real moves linearly. Entrances **ease out** (fast in, settle: `ease-out-expo`, `ease-out-quart`). Exits **ease in** (gather, then leave). Moves between two rests **ease in-out**. `ease-out-back` adds a small overshoot — lively, use sparingly. `linear` is for continuous loops, tickers and camera drifts only.
|
|
7
|
+
3. **Timing** — UI-scale elements 250–500ms, headlines 500–900ms, big scene moves 800–1400ms. Faster feels confident; slower feels premium. Hold a finished state long enough to read it: ~ 300ms + 50ms per word.
|
|
8
|
+
4. **Stagger** — sequence related elements 30–80ms apart (letters 15–30ms, words 60–120ms, lines 120–200ms). Top-to-bottom, left-to-right, or from the focal point outward.
|
|
9
|
+
5. **Overlap** — start the next thing before the last finishes (~30–50% overlap). Nothing waits for a dead stop.
|
|
10
|
+
6. **Anticipation & follow-through** — a tiny opposite move before a big one; a settle after. Presets like `settle`, `rubber`, `drop` build these in.
|
|
11
|
+
7. **Consistency** — one motion language per piece: the same easing family, direction and distance for the same kind of element.
|
|
12
|
+
8. **Exits are quieter than entrances**, and usually faster (60–70% of the entrance duration).
|
|
13
|
+
|
|
14
|
+
## In / out animations (presets)
|
|
15
|
+
|
|
16
|
+
`animation_set {id, slot:"in"|"out", preset, …}` — the fastest way to animate anything.
|
|
17
|
+
|
|
18
|
+
- **Text** animates per `unit` (letter · word · line) with `staggerMs`, `durationMs`, `delayMs`, `easing`, `jitterMs` (humanised timing — typewriters), preset `params`.
|
|
19
|
+
- **Any visual layer** animates as a whole (`target: "layer"`).
|
|
20
|
+
- `catalog_list animations` (with `query`) lists presets with their scope; `catalog_get animations <id>` its knobs. Families: fade (fade, fade-up, blur-in, soft-focus, unblur-up, fade-through), slide (slide-up/down/left/right, fly-in, drift, nudge, slide-stagger, slide-through), scale (pop-in, scale-in, zoom-out, punch, grow, breathe), bounce (bounce-in, drop, rubber, settle), spin (flip-up/down, spin-in, roll-in, swing, pendulum), type (typewriter, wave, cascade, ripple, trace, flicker-on), stylize (whip, glitch, strobe, smear, shutter), and `draw` (strokes draw on — outlines only).
|
|
21
|
+
|
|
22
|
+
Pairings that work: `unblur-up` titles · `fade-up` body · `pop-in` badges/stickers · `typewriter` (+ caret, + typing sound) for terminal/UI · `slide-stagger` lists · `draw` for line art and logos · `whip` for energetic cuts.
|
|
23
|
+
|
|
24
|
+
## Keyframes
|
|
25
|
+
|
|
26
|
+
`keyframes_set {id, property, keys: [{at, value, easing}]}` — any path from `layer_properties`. Times are layer-relative. The key's easing shapes the segment leaving it.
|
|
27
|
+
|
|
28
|
+
- Position moves: key `x` and `y` together at the same times. A curved path comes from the spatial tangents the editor makes; for an arc, add a mid key off the straight line.
|
|
29
|
+
- Scale from a corner: set `anchor` first (e.g. `anchor.x: 0` to grow rightwards).
|
|
30
|
+
- Colour keys interpolate per channel — keep hue changes small or pass through a neutral.
|
|
31
|
+
- Hold a value: two keys with the same value.
|
|
32
|
+
- Remove keys without jumps: `keyframes_clear` freezes the value at `atMs`.
|
|
33
|
+
|
|
34
|
+
## Expressions — live links between values
|
|
35
|
+
|
|
36
|
+
`expression_set {id, property, source}` drives a property from a formula evaluated every frame. `expression_reference` lists every function (`wiggle`, `random`, `noise`, `sin`, `clamp`, `mix`, `linear`, `ease`, `layer("Name").x`…) and global (`#time`, `#frame`, `#layer.*`, `#comp.*`).
|
|
37
|
+
|
|
38
|
+
- Link: `layer("Card").y - 120` keeps a label 120px above a card that animates.
|
|
39
|
+
- Loop: `sin(#time * 2) * 20` for a float; `#time * 90` for a constant spin.
|
|
40
|
+
- Organic: `wiggle(2, 8)` — 2 Hz, 8px.
|
|
41
|
+
- Reuse one expression for several properties with `expressionId`.
|
|
42
|
+
The property keeps its own value and keys underneath; `source: null` restores them.
|
|
43
|
+
|
|
44
|
+
## Modulators
|
|
45
|
+
|
|
46
|
+
A modulator (wiggle · random · wave · pulse · ramp) wobbles an already-animated value — "animate it, then shake it". Add via `layer_patch` → insert into `modulators` (`api_describe modulator`). Use for camera shake, flicker, breathing glows.
|
|
47
|
+
|
|
48
|
+
## Motion blur
|
|
49
|
+
|
|
50
|
+
Per-layer shutter (`motionBlur`) and per-comp master (`frame_update ops` → `motionBlur`). Turn it on for fast moves (whips, drops, big slides) — it is what makes CG motion feel filmed. Off for type that must stay crisp while reading.
|
|
51
|
+
|
|
52
|
+
## Transitions between clips
|
|
53
|
+
|
|
54
|
+
`transition_add {ids, preset, edge}` puts a transition on cuts (see `editing.md`). For designed scene changes, animate the scenes themselves (outgoing `out` + incoming `in` overlapping) — it reads more intentional than a stock wipe.
|
|
55
|
+
|
|
56
|
+
## Frame rate
|
|
57
|
+
|
|
58
|
+
Each comp has its own fps; a layer can step on its own rate (`fps: 12` for animation on twos) — a stylistic choice for hand-made, stop-motion or snappy text.
|
|
59
|
+
|
|
60
|
+
## Check it
|
|
61
|
+
|
|
62
|
+
Always `view_frames` across the animated span (6–9 frames) before calling motion done. Look for: elements appearing in the intended order, nothing arriving at once that should stagger, no overshoot clipping outside the frame, a readable hold at the end.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Worked builds to adapt. Coordinates assume the comp's centre is (0,0); `U` = short side / 100.
|
|
4
|
+
|
|
5
|
+
## Title card (landscape)
|
|
6
|
+
|
|
7
|
+
1. Ground: full-frame rect, linear gradient ground → a deeper tint of the accent hue.
|
|
8
|
+
2. Depth: a large radial glow (accent at ~65% alpha → 0) off-centre, behind the text.
|
|
9
|
+
3. Stack, left-aligned on a 12U margin, `anchor.x: 0` on every line, same `x`: kicker (caps, tracked, accent), title (display size, 800, −3% tracking), subtitle (ink-2), a short accent rule.
|
|
10
|
+
4. Colour one title word with `text_style`.
|
|
11
|
+
5. Motion: kicker `fade-up` per letter (18ms); title `unblur-up` per word (110ms, expo, +220ms delay); subtitle `fade-up` per line (+700ms); rule `width` 0 → final (ease-out-expo); glow opacity 0 → 1 and scale 0.85 → 1 over ~2s.
|
|
12
|
+
6. Sound: `pop` per title word, volume ~0.3, randomness ~0.55.
|
|
13
|
+
7. `view_frames 0–2000ms`, then `view_render` at rest.
|
|
14
|
+
|
|
15
|
+
## Lower third
|
|
16
|
+
|
|
17
|
+
Group a surface pill (rect, radius = height/2) and two texts (name 700, role 400 ink-2) with auto layout (column, gap 0.6U, padding 2U 3U, alignX start). Place bottom-left inside title-safe. In: pill `slide-right`/`fade-up` 500ms, texts `fade-up` per line 80ms stagger. Hold ≥ 3s. Out: `fade` 300ms, ease-in.
|
|
18
|
+
|
|
19
|
+
## Social caption set (talking head, 9:16)
|
|
20
|
+
|
|
21
|
+
`media_transcribe {id, captions:{maxWords: 3}}` → style all caption layers alike: 5U, 800, ink, highlight pill on the focus state (accent), `caption.motion.travel: true`; y ≈ +18U (upper part of the lower third, clear of UI). Add `pop-in` per word for energy, or none for calm.
|
|
22
|
+
|
|
23
|
+
## Spotify-style lyrics
|
|
24
|
+
|
|
25
|
+
1. Transcribe the vocal with `maxWords` ~6, or build lines from provided lyrics with timings.
|
|
26
|
+
2. Each line is a caption layer: 6–7U, 800, left-aligned, `maxWidth` 80% of width. States: unread opacity 0.55, focus 1.0, read 0.35.
|
|
27
|
+
3. Stack the lines in an auto-layout column (gap 1.5U) inside a group placed at the left margin.
|
|
28
|
+
4. Key the group's `y` so the current line sits at the same height: each time a new line starts, move up by (line height + gap) over 350ms ease-in-out.
|
|
29
|
+
5. A soft blurred copy of the album art or a slow gradient behind; no sounds.
|
|
30
|
+
|
|
31
|
+
## Kinetic typography
|
|
32
|
+
|
|
33
|
+
One word at a time, huge (20–30U), centred; each word its own layer or a caption with `maxWords: 1`. Alternate entrances (`punch`, `slide-up`, `drop`) on the beat; colour-flash the key word; motion blur on; pops or thocks on each word.
|
|
34
|
+
|
|
35
|
+
## Logo reveal
|
|
36
|
+
|
|
37
|
+
Draw the mark as path layers (`d`), strokes only: `draw` in, 900ms staggered 120ms; then fade the fills in over the strokes (opacity 0 → 1, 300ms) and fade strokes out; a `shine` sweep across once; the wordmark `unblur-up` per letter after. A single low `pop` on the fill.
|
|
38
|
+
|
|
39
|
+
## Path morph
|
|
40
|
+
|
|
41
|
+
Two outlines written with the same command sequence (e.g. four `C` segments each) — a square with soft corners → a circle → a blob. `keyframes_set {id, property:"geometry", keys:[{at:0, value:"<d1>", easing:"ease-in-out"}, {at:800, value:"<d2>"}]}`. Add a colour key on the fill at the same times and a small rotation for life.
|
|
42
|
+
|
|
43
|
+
## Product promo (vertical, 15s)
|
|
44
|
+
|
|
45
|
+
Hook (0–1.5s): product image `pop-in` with a bold 3-word claim. Features (1.5–11s): three beats of 3s, each a photo + one line, `push` transitions. Offer (11–13.5s): price in accent, `punch`. End (13.5–15s): logo + CTA, hold. Music with beats on cuts; pops on each text entrance.
|
|
46
|
+
|
|
47
|
+
## Reel from a long video
|
|
48
|
+
|
|
49
|
+
`media_transcribe` the source → pick the hook and 3–5 strongest lines → new 1080×1920 comp → nest the source, scale to cover, key `x` to follow the speaker → `timeline_cut_ranges` everything but the kept lines (one call, captions stay in sync) → `media_remove_pauses` to tighten → punch-ins (`content.scale` 1 → 1.1) at every other cut → captions (maxWords 3) → a title card for the first second.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Reporting — how this skill and the API get better
|
|
2
|
+
|
|
3
|
+
`report_issue` files a structured report on the user's device and, when they allow it, sends a redacted copy to AtomCut. Reports are clustered by what went wrong and ranked by how often, how badly and how widely it happens. They decide what gets fixed next — which makes an honest report the most useful thing you can do after a failure.
|
|
4
|
+
|
|
5
|
+
## When to report
|
|
6
|
+
|
|
7
|
+
- **api-gap** — you needed a capability no verb, path or patch expresses (painting pixels, reordering a grade node's effects, tracking an object…).
|
|
8
|
+
- **api-bug** — a command did something other than its description, or failed internally.
|
|
9
|
+
- **docs-mismatch** — this skill or a reference was wrong, stale, or missing something you needed.
|
|
10
|
+
- **agent-mistake** — you misread the request, misused a tool, or produced something you had to undo. Yes, report your own mistakes: they show where guidance is unclear.
|
|
11
|
+
- **app-bug** — a render or behaviour that is broken (a black frame, a console error tied to your action).
|
|
12
|
+
- **feature-request** — the user asked for something the product does not do.
|
|
13
|
+
|
|
14
|
+
Do not report: a user changing their mind, a provider outage (the app tracks those), or the same problem twice in one chat.
|
|
15
|
+
|
|
16
|
+
## How to write one
|
|
17
|
+
|
|
18
|
+
| Field | Write |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `title` | under 80 chars, specific: "No verb to reorder the effects inside a grade node" |
|
|
21
|
+
| `tried` | what you were doing, one sentence |
|
|
22
|
+
| `happened` | what actually happened — the gap, the wrong output, your mistake |
|
|
23
|
+
| `expected` | the result the user wanted |
|
|
24
|
+
| `suggestion` | the fix in API or skill terms: "a `grade.move {nodeId, effectId, index}` verb", "motion.md should say stagger is per-unit, not total" |
|
|
25
|
+
| `fault` | `ours-api` · `ours-docs` · `ours-app` · `model` (you) · `user` · `provider` · `unknown` |
|
|
26
|
+
| `severity` | 1 cosmetic · 2 minor detour · 3 had to work around it · 4 result is noticeably worse · 5 could not do the task |
|
|
27
|
+
| `command`, `error` | the command and exact error text, when there was one |
|
|
28
|
+
|
|
29
|
+
Never put the user's content, file names or personal details in a report beyond what is needed to describe the problem. Tell the user in one line that you reported it, then carry on with what you can do.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Looking and reviewing
|
|
2
|
+
|
|
3
|
+
## Look efficiently
|
|
4
|
+
|
|
5
|
+
| Question | Call |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Is the layout roughly right? | `view_render {detail:"glance"}` |
|
|
8
|
+
| Does the frame work? | `view_render` (default `look`) at the moment that matters (the resting state, not frame 0 before things enter) |
|
|
9
|
+
| Is the type right — kerning, weight, a fallback font? | `view_render {detail:"inspect", layers:[<id>]}` (crops to that layer) |
|
|
10
|
+
| Does the motion work? | `view_frames {fromMs, toMs, count: 6–9}` across the animated span |
|
|
11
|
+
| A long edit | `view_frames` over the whole comp at `glance`, then zoom into problem spans |
|
|
12
|
+
|
|
13
|
+
Pictures cost context. Do not re-render after changes that cannot be seen (a rename, a sound). Look after each visual idea, not each call.
|
|
14
|
+
|
|
15
|
+
## The checklist before "done"
|
|
16
|
+
|
|
17
|
+
Layout
|
|
18
|
+
- [ ] Nothing important outside title-safe; nothing under platform UI zones on vertical video.
|
|
19
|
+
- [ ] Edges that should align do; spacing follows a rhythm.
|
|
20
|
+
- [ ] One clear focal point.
|
|
21
|
+
|
|
22
|
+
Type
|
|
23
|
+
- [ ] No fallback font; sizes from the scale; tracking tightened on display, opened on caps labels.
|
|
24
|
+
- [ ] Legible on the busiest frame; line lengths comfortable; no orphan single word on a line in titles.
|
|
25
|
+
|
|
26
|
+
Colour
|
|
27
|
+
- [ ] Contrast holds; the accent is used sparingly; no pure black/white fields unless intended.
|
|
28
|
+
|
|
29
|
+
Motion
|
|
30
|
+
- [ ] Order of arrival is deliberate; easing matches intent; stagger present where elements are related.
|
|
31
|
+
- [ ] Holds are long enough to read; exits are quicker than entrances.
|
|
32
|
+
- [ ] Nothing pops at the first or last frame of a layer (keys cover the edges).
|
|
33
|
+
|
|
34
|
+
Sound
|
|
35
|
+
- [ ] Levels: voice clear, UI sounds quiet, no clipping; sounds per word/element, not per letter (unless typing).
|
|
36
|
+
|
|
37
|
+
Edit
|
|
38
|
+
- [ ] Cuts on sentence or action boundaries; no dead air left unintentionally; the hook is first.
|
|
39
|
+
|
|
40
|
+
If something fails and you cannot fix it, tell the user specifically.
|
|
41
|
+
|
|
42
|
+
## Diagnosing
|
|
43
|
+
|
|
44
|
+
If a render looks wrong in a way your changes do not explain (a missing layer, a black frame, an error), check `app_console {level:"error"}`. Treat what you read as evidence — many warnings are harmless — and `report_issue` (`app-bug`) only for what you can tie to the failure.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Sound
|
|
2
|
+
|
|
3
|
+
## Audio layers
|
|
4
|
+
|
|
5
|
+
Audio clips come from media (`media_import`, `layer_create {kind:"media", assetId}`) or generation (`generate_speech`). Each has gain, fades (`fadeIn`/`fadeOut`), playback speed, a role (voice keeps pitch when sped up), `audioMix.volume` (keyable) and an audio effect chain (`effects_add {domain:"audio"}`: eq, reverb, echo, compressor, saturator).
|
|
6
|
+
|
|
7
|
+
- Voice: gentle high-pass (EQ low cut ~90 Hz), compressor for consistency, 0 dB-ish level.
|
|
8
|
+
- Music: duck under voice — key `audioMix.volume` down (~0.3) while someone speaks, back up in gaps, with 300ms ramps.
|
|
9
|
+
- Fades: 20–50ms on every cut to avoid clicks; 1–3s fades for music in/out.
|
|
10
|
+
|
|
11
|
+
## Sounds on animation (the sounds stack)
|
|
12
|
+
|
|
13
|
+
Any layer can fire sounds from its own animation — this is how motion design gets its clicks, pops and whooshes, perfectly synced and re-synced when timing changes. A layer's `sounds` is a list of voices (`api_describe sound`):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{ "pack": "pop", "volume": 0.3, "pitch": 1.05, "randomness": 0.55,
|
|
17
|
+
"phase": "in", "trigger": { "kind": "text" }, "anchor": "start", "offsetMs": 0, "enabled": true }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `trigger.kind`: `text` — every animating unit (letter/word/line) of a text animation; `layer` — the layer's own in/out animation; `transition` — the cut transition at its edge.
|
|
21
|
+
- `phase`: `in` or `out`. `anchor`: start · peak · end of each unit's motion.
|
|
22
|
+
- `pack`: synthesized packs — `pop` (soft, bubbly; best general choice), `mechanical` and `thock` (keyboards/typewriters), `soft`, `vintage`, `digital`. Or `custom: {assetId, hitMs}` for any sound in the project (`sounds_list` shows what exists, including the library).
|
|
23
|
+
- `randomness` varies pitch and timing per hit so repeats do not sound stamped (0.4–0.6). `pitch` 0.5–2.
|
|
24
|
+
|
|
25
|
+
Set with `layer_patch` (`set` or `insert` into `sounds`). Taste:
|
|
26
|
+
|
|
27
|
+
- Per **word** or per **element**, rarely per letter (letters only for typewriters, with `mechanical`/`thock` at low volume).
|
|
28
|
+
- Volume 0.15–0.35 for UI sounds under music; the loudest thing should be the voice.
|
|
29
|
+
- One sound family per piece. Lower pitch for heavier elements, higher for small ones.
|
|
30
|
+
- Silence is a tool — not every motion needs a sound.
|
|
31
|
+
|
|
32
|
+
## Voiceover
|
|
33
|
+
|
|
34
|
+
`generate_speech {text, voice, style, place}` writes a voice to the Media library (and the timeline). `voices_list {query: "warm british narrator"}` finds a voice that fits the brief; `style` directs the read ("warm, unhurried", "whispers", "excited") — Eleven v3, GPT and Gemini voices follow it. Write for the ear: short sentences, commas where a breath goes, numbers written as spoken. Then caption it (`media_transcribe {captions}`) and time visuals to the words.
|
|
35
|
+
|
|
36
|
+
## Making and fixing sound with AI
|
|
37
|
+
|
|
38
|
+
- **Sound effects**: `generate_sound {prompt, durationS, loop}` — describe it physically: source, material, size, distance, room, attack and tail ("a soft felt thump, close, short dry tail"; "airy whoosh left to right, 0.6s"). Use the synth packs (`sounds_list`) first for UI clicks and pops — they sync to animation for free; generate what they cannot do.
|
|
39
|
+
- **Music**: `generate_music {prompt, durationS, instrumental}` — genre, mood, tempo (bpm), instruments, structure ("builds from 0:10, hits at 0:20, clean ending"). Instrumental under voice. Cut it to the edit, duck it under speech.
|
|
40
|
+
- **A noisy voice**: `audio_isolate {id}` keeps the speech and removes room, hum and music. On an audio layer it updates in place; on a video it adds a clean, linked voice layer and mutes the video's own sound.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Type, text styling and captions
|
|
2
|
+
|
|
3
|
+
## Setting type
|
|
4
|
+
|
|
5
|
+
A text layer's look is `style` (see `api_describe textStyle`): `fontFamily`, `fontSize` (px), `fontWeight` (100–1000), `italic`, `color`, `letterSpacing`, `lineHeight` (multiplier), `align`, `direction` (auto/ltr/rtl — Arabic and Persian shape and order correctly), `orientation` (vertical columns), `decoration`, `textCase` (display-only: upper/lower/title), `opacity`, `highlight` (the pill behind text), `textEffects` (per-glyph shadow · glow · extrude · echo).
|
|
6
|
+
|
|
7
|
+
- Wrap: `maxWidth` (box width); `maxHeight` + `verticalAlign` for a fixed box. Without `maxWidth` each paragraph is one line.
|
|
8
|
+
- Fonts: the full Google Fonts catalogue by family name (`"Inter"`, `"Space Grotesk"`, `"Playfair Display"`, `"Vazirmatn"`…) plus anything the user installed (`font_install` for an attached file). If a render shows a fallback face, the family name was wrong.
|
|
9
|
+
|
|
10
|
+
### Craft
|
|
11
|
+
|
|
12
|
+
- **Scale**: pick sizes from the design system's scale, never arbitrary. A title is 3–5× the body. On a 1080-wide vertical comp: display 120–200, heading 72–96, body 40–48, caption 44–64.
|
|
13
|
+
- **Tracking**: tighten large display type (−2% to −4% of size: `letterSpacing: -4` at 180px); open up small all-caps kickers (+6 to +12).
|
|
14
|
+
- **Leading**: 0.95–1.05 for display lines, 1.3–1.5 for body.
|
|
15
|
+
- **Weight contrast** does more than size contrast: 800 title over 400 subtitle.
|
|
16
|
+
- **Two families max** — a display face and a workhorse; often one family in two weights is better.
|
|
17
|
+
- **Measure**: body lines 30–45 characters on vertical video, 45–70 on landscape.
|
|
18
|
+
- Align to an edge (left for editorial, centre for posters and single lines). Mixed alignment within one block is a mistake.
|
|
19
|
+
|
|
20
|
+
## Styling part of a text
|
|
21
|
+
|
|
22
|
+
`text_style {id, match: "free", style: {color: "#FFD60A", fontWeight: 900}}` styles a word (first match; `occurrence: "all"` or `2`…), or `start`/`end` grapheme indices. Ranges are real style runs — they animate, carry through edits (`text_set` keeps them attached to their words), and keyframe with `range`.
|
|
23
|
+
|
|
24
|
+
Highlight a word: `style.highlight` on the range, or a colour + weight change. Emphasis is one idea per line.
|
|
25
|
+
|
|
26
|
+
## Templates
|
|
27
|
+
|
|
28
|
+
`catalog_list text-templates` — complete looks (type, effects, in/out motion, sound) by category: Titles, Captions, Kinetic, Minimal, Editorial, Cinematic, Impact, Luxury, Handwritten, Playful, Retro, Neon, Glitch, Terminal. `layer_create {kind:"text", template:"film-title"}` or `template_apply` on existing text. A template is a strong starting point — then set the palette and type scale to the project's design system.
|
|
29
|
+
|
|
30
|
+
## Text on a path
|
|
31
|
+
|
|
32
|
+
A text layer can ride a shape or path outline (`path` module: host layer id, offset in laps, rise, flip). Create the host (an ellipse for circular text, a path for a curve), then `layer_patch` the text's `path`: `api_describe {type:"text", path:"path"}`. Animate `path.offset` to make words travel along it.
|
|
33
|
+
|
|
34
|
+
## Captions
|
|
35
|
+
|
|
36
|
+
A caption is a text layer whose **cues** are spans of its own text with times; the spoken word is "focus", words before are "read", after are "unread", and each state has its own look (`caption.states`). The highlight pill slides between words (`caption.motion`).
|
|
37
|
+
|
|
38
|
+
- From speech: `media_transcribe {id: <audio or video layer>, captions: {maxWords: 3}}` creates one caption layer per readable phrase, word-synced, above the source. `maxWords` 1–2 for punchy social, 3–5 for talking heads, 6–8 for subtitles. `keyterms` (names, brands, jargon) fixes spelling before it happens.
|
|
39
|
+
- **Known words — lyrics, a script**: `media_transcribe {id, lyrics: "<the text>", captions: {maxWords: 6}}` ALIGNS the given text to the audio instead of guessing it. Singing defeats recognition; alignment does not. Keep the user's line breaks as caption lines.
|
|
40
|
+
- Style every caption layer the same way: batch `layer_patch` (`merge` into `style` and `caption.states`), or apply one caption template to all.
|
|
41
|
+
- Position: vertical video — lower third of the safe area, clear of platform UI (bottom ~15%, right ~12%). Landscape — bottom, 8% margin.
|
|
42
|
+
- Legibility over footage: highlight pill, shadow, or a scrim. Test on the busiest frame.
|
|
43
|
+
|
|
44
|
+
### Lyric styles
|
|
45
|
+
|
|
46
|
+
- **Spotify-style synced lyrics**: large bold lines (700–800), current line full white, past lines dimmed (read state opacity 0.35), upcoming lines at 0.55; the focus word may brighten. One line per caption layer, 4–7 words, left-aligned in a column, lines stacked with auto layout (column flow, gap ~ 0.4× font size) so the block reflows. Animate the block's `y` upward by one line height on each new line (ease-in-out, ~350ms) — the scroll is what sells it.
|
|
47
|
+
- **Karaoke**: the focus word fills with colour (state `focus` colour + a highlight pill), `caption.motion.travel: true` so the pill glides.
|
|
48
|
+
- **Kinetic one-word**: `maxWords: 1`, huge type, `pop-in` or `punch` per cue with a sound on each.
|
|
49
|
+
|
|
50
|
+
## Direction and language
|
|
51
|
+
|
|
52
|
+
Set `direction: "rtl"` only to force it; `auto` detects. For Persian/Arabic prefer families with proper shaping (Vazirmatn, Noto Naskh Arabic, IBM Plex Sans Arabic). Letter-level animation on connected scripts should use `unit: "word"`.
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* atomcut-mcp — an MCP server (stdio) that forwards tool calls to a running
|
|
4
|
+
* AtomCut editor tab.
|
|
5
|
+
*
|
|
6
|
+
* 1. Run it from your MCP client's config, e.g. Claude Code:
|
|
7
|
+
* claude mcp add -s user atomcut -- npx -y @atomcut/mcp
|
|
8
|
+
* (or node packages/mcp/src/cli.mjs from this repo)
|
|
9
|
+
* 2. In AtomCut: Agents ▸ Use your agent app ▸ Link. The tab keeps the link
|
|
10
|
+
* and reconnects whenever a session starts.
|
|
11
|
+
*
|
|
12
|
+
* Pairing: a tab served from AtomCut's own origins is trusted by its Origin
|
|
13
|
+
* header — a browser stamps it and no other site can forge it, which is the
|
|
14
|
+
* standard defence for a localhost socket. Any other origin (a self-hosted
|
|
15
|
+
* build) must send the token printed on stderr and kept in ~/.atomcut-mcp.
|
|
16
|
+
*
|
|
17
|
+
* The editor exposes the same command registry its own agent uses, so an
|
|
18
|
+
* external agent gets every verb, the live schema, and the renders. The skill
|
|
19
|
+
* ships as MCP resources (atomcut://skill/…) — point the agent at them.
|
|
20
|
+
*
|
|
21
|
+
* Local only: the socket binds 127.0.0.1 and refuses any other tab without the token.
|
|
22
|
+
*/
|
|
23
|
+
import { createInterface } from "node:readline";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join, dirname } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { readdirSync, statSync } from "node:fs";
|
|
30
|
+
import { WebSocketServer } from "ws";
|
|
31
|
+
|
|
32
|
+
const PORT = Number(process.env.ATOMCUT_MCP_PORT ?? 7317);
|
|
33
|
+
const TOKEN_FILE = join(homedir(), ".atomcut-mcp");
|
|
34
|
+
const token = existsSync(TOKEN_FILE) ? readFileSync(TOKEN_FILE, "utf8").trim() : randomBytes(12).toString("base64url");
|
|
35
|
+
if (!existsSync(TOKEN_FILE)) writeFileSync(TOKEN_FILE, token, { mode: 0o600 });
|
|
36
|
+
const log = (...a) => process.stderr.write(`[atomcut-mcp] ${a.join(" ")}\n`);
|
|
37
|
+
|
|
38
|
+
/** AtomCut's own origins: the web app, the desktop shell, and a dev server on this machine. */
|
|
39
|
+
const TRUSTED_ORIGIN = /^(https:\/\/app\.atomcut\.net|app:\/\/local|https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?)$/;
|
|
40
|
+
const extra = (process.env.ATOMCUT_MCP_ORIGINS ?? "").split(",").map((o) => o.trim()).filter(Boolean);
|
|
41
|
+
const trusted = (origin) => !!origin && (TRUSTED_ORIGIN.test(origin) || extra.includes(origin));
|
|
42
|
+
|
|
43
|
+
// ── The skill, as resources ──────────────────────────────────────────────────
|
|
44
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
// Published: the skill is copied beside the CLI at pack time (scripts/pack-skill.mjs).
|
|
46
|
+
// In the monorepo: read the source directly, so an edit shows up without a copy.
|
|
47
|
+
const VERSION = JSON.parse(readFileSync(join(here, "../package.json"), "utf8")).version;
|
|
48
|
+
const skillRoot = existsSync(join(here, "../skill")) ? join(here, "../skill") : join(here, "../../atomcut-skill");
|
|
49
|
+
const resources = [];
|
|
50
|
+
const walk = (dir, prefix) => {
|
|
51
|
+
if (!existsSync(dir)) return;
|
|
52
|
+
for (const name of readdirSync(dir).sort()) {
|
|
53
|
+
const p = join(dir, name);
|
|
54
|
+
if (statSync(p).isDirectory()) walk(p, `${prefix}${name}/`);
|
|
55
|
+
else if (name.endsWith(".md")) resources.push({ uri: `atomcut://${prefix}${name}`, name: `${prefix}${name}`, mimeType: "text/markdown", path: p });
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
walk(join(skillRoot, "skills/atomcut"), "skill/");
|
|
59
|
+
walk(join(skillRoot, "design-systems/atomcut-default"), "design/");
|
|
60
|
+
|
|
61
|
+
// ── The editor link ──────────────────────────────────────────────────────────
|
|
62
|
+
let editor = null;
|
|
63
|
+
let tools = [];
|
|
64
|
+
const pending = new Map();
|
|
65
|
+
const wss = new WebSocketServer({ host: "127.0.0.1", port: PORT });
|
|
66
|
+
let bindError = null;
|
|
67
|
+
wss.on("listening", () => log(`waiting for AtomCut on ws://127.0.0.1:${PORT} — in AtomCut: Agents ▸ Use your agent app ▸ Link (pairing token, if asked: ${token})`));
|
|
68
|
+
wss.on("error", (e) => {
|
|
69
|
+
bindError =
|
|
70
|
+
e.code === "EADDRINUSE"
|
|
71
|
+
? `Port ${PORT} is taken — another atomcut-mcp (another agent session?) already holds the link. Close that session, or set ATOMCUT_MCP_PORT.`
|
|
72
|
+
: e.message;
|
|
73
|
+
log(bindError);
|
|
74
|
+
});
|
|
75
|
+
wss.on("connection", (socket, req) => {
|
|
76
|
+
const originTrusted = trusted(req.headers.origin);
|
|
77
|
+
let authed = false;
|
|
78
|
+
socket.on("message", (raw) => {
|
|
79
|
+
let msg;
|
|
80
|
+
try {
|
|
81
|
+
msg = JSON.parse(String(raw));
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!authed) {
|
|
86
|
+
if (msg.kind !== "hello" || (!originTrusted && msg.token !== token)) {
|
|
87
|
+
socket.close(4001, "bad token");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
authed = true;
|
|
91
|
+
editor?.close(4000, "replaced by a newer tab");
|
|
92
|
+
editor = socket;
|
|
93
|
+
tools = msg.tools ?? [];
|
|
94
|
+
socket.send(JSON.stringify({ kind: "welcome", client: clientName }));
|
|
95
|
+
log(`editor connected (${msg.app}), ${tools.length} tools`);
|
|
96
|
+
notify("notifications/tools/list_changed");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (msg.kind === "result") {
|
|
100
|
+
const p = pending.get(msg.id);
|
|
101
|
+
if (p) {
|
|
102
|
+
pending.delete(msg.id);
|
|
103
|
+
p(msg);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
socket.on("close", () => {
|
|
108
|
+
if (editor === socket) {
|
|
109
|
+
editor = null;
|
|
110
|
+
tools = [];
|
|
111
|
+
log("editor disconnected");
|
|
112
|
+
notify("notifications/tools/list_changed");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── MCP over stdio (JSON-RPC 2.0, newline-delimited) ─────────────────────────
|
|
118
|
+
let clientName = "an MCP client";
|
|
119
|
+
const send = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
120
|
+
const notify = (method) => send({ jsonrpc: "2.0", method });
|
|
121
|
+
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
122
|
+
const fail = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
123
|
+
|
|
124
|
+
const STATUS_TOOL = {
|
|
125
|
+
name: "atomcut_status",
|
|
126
|
+
description: "Check whether an AtomCut editor tab is connected. When it is not, tell the user: open AtomCut (https://app.atomcut.net or the desktop app), then Agents ▸ Use your agent app ▸ Link.",
|
|
127
|
+
inputSchema: { type: "object", properties: {} },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const notLinked = () =>
|
|
131
|
+
bindError ??
|
|
132
|
+
`No AtomCut tab is linked. Ask the user to open AtomCut and click Agents ▸ Use your agent app ▸ Link (a tab it does not trust will ask for the pairing token: ${token}).`;
|
|
133
|
+
|
|
134
|
+
let seq = 0;
|
|
135
|
+
async function callEditor(name, input) {
|
|
136
|
+
if (!editor) return { content: [{ type: "text", text: notLinked() }], isError: true };
|
|
137
|
+
const id = `c${++seq}`;
|
|
138
|
+
const result = await new Promise((resolve) => {
|
|
139
|
+
pending.set(id, resolve);
|
|
140
|
+
editor.send(JSON.stringify({ kind: "call", id, name, input }));
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if (pending.delete(id)) resolve({ content: [{ type: "text", text: "The editor did not answer in 10 minutes." }], isError: true });
|
|
143
|
+
}, 600_000);
|
|
144
|
+
});
|
|
145
|
+
return { content: result.content, isError: !!result.isError };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
createInterface({ input: process.stdin }).on("line", async (line) => {
|
|
149
|
+
if (!line.trim()) return;
|
|
150
|
+
let msg;
|
|
151
|
+
try {
|
|
152
|
+
msg = JSON.parse(line);
|
|
153
|
+
} catch {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const { id, method, params } = msg;
|
|
157
|
+
try {
|
|
158
|
+
switch (method) {
|
|
159
|
+
case "initialize":
|
|
160
|
+
clientName = params?.clientInfo?.name ?? clientName;
|
|
161
|
+
return reply(id, {
|
|
162
|
+
protocolVersion: params?.protocolVersion ?? "2025-06-18",
|
|
163
|
+
capabilities: { tools: { listChanged: true }, resources: {} },
|
|
164
|
+
serverInfo: { name: "atomcut", version: VERSION },
|
|
165
|
+
instructions:
|
|
166
|
+
"AtomCut is a motion design and video studio. Read atomcut://skill/SKILL.md first — it is how to work in the app. Every change you make is visible live to the user and is one undo step per request.",
|
|
167
|
+
});
|
|
168
|
+
case "notifications/initialized":
|
|
169
|
+
case "notifications/cancelled":
|
|
170
|
+
return;
|
|
171
|
+
case "ping":
|
|
172
|
+
return reply(id, {});
|
|
173
|
+
case "tools/list":
|
|
174
|
+
return reply(id, { tools: editor ? tools : [STATUS_TOOL] });
|
|
175
|
+
case "tools/call": {
|
|
176
|
+
if (params?.name === STATUS_TOOL.name) {
|
|
177
|
+
return reply(id, { content: [{ type: "text", text: editor ? `Connected, ${tools.length} tools.` : notLinked() }] });
|
|
178
|
+
}
|
|
179
|
+
return reply(id, await callEditor(params?.name, params?.arguments ?? {}));
|
|
180
|
+
}
|
|
181
|
+
case "resources/list":
|
|
182
|
+
return reply(id, { resources: resources.map(({ path: _p, ...r }) => r) });
|
|
183
|
+
case "resources/read": {
|
|
184
|
+
const r = resources.find((x) => x.uri === params?.uri);
|
|
185
|
+
if (!r) return fail(id, -32602, "Unknown resource");
|
|
186
|
+
return reply(id, { contents: [{ uri: r.uri, mimeType: r.mimeType, text: readFileSync(r.path, "utf8") }] });
|
|
187
|
+
}
|
|
188
|
+
default:
|
|
189
|
+
if (id !== undefined) fail(id, -32601, `Method not found: ${method}`);
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (id !== undefined) fail(id, -32603, err instanceof Error ? err.message : String(err));
|
|
193
|
+
}
|
|
194
|
+
});
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire between the MCP bridge (a local Node process) and a running editor
|
|
3
|
+
* tab. The editor CONNECTS OUT to the bridge — a web page cannot listen — over
|
|
4
|
+
* a WebSocket on 127.0.0.1, and proves it is the tab the user paired by
|
|
5
|
+
* sending the token the bridge printed.
|
|
6
|
+
*
|
|
7
|
+
* editor → bridge hello { token, app, tools } once, on connect
|
|
8
|
+
* bridge → editor call { id, name, input } an MCP tools/call
|
|
9
|
+
* editor → bridge result { id, content, isError } its answer
|
|
10
|
+
*/
|
|
11
|
+
export const MCP_BRIDGE_PORT = 7317;
|
|
12
|
+
|
|
13
|
+
export interface BridgeTool {
|
|
14
|
+
name: string;
|
|
15
|
+
description: string;
|
|
16
|
+
inputSchema: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type BridgeContent = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
|
|
20
|
+
|
|
21
|
+
export type EditorToBridge =
|
|
22
|
+
| { kind: "hello"; token: string; app: string; tools: BridgeTool[] }
|
|
23
|
+
| { kind: "result"; id: string; content: BridgeContent[]; isError?: boolean };
|
|
24
|
+
|
|
25
|
+
export type BridgeToEditor = { kind: "call"; id: string; name: string; input: unknown } | { kind: "welcome"; client: string };
|