@engine-room/after-effects-mcp 0.1.0 → 0.1.2
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 +117 -107
- package/bin/server.js +423 -68
- package/package.json +10 -7
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/main.js +21 -38
- package/panel/jsx/bundle.jsx +44 -5
- package/panel/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# After Effects MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Control Adobe After Effects with AI.** Describe the animation you want — a lower third, a logo reveal, an animated counter — and it gets built in your project: layers, keyframes, easing, effects, expressions and text, all editable afterwards like anything you would make by hand.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Works with Claude and other MCP clients, on macOS and Windows.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
"Build a lower third that says Chapter One, sliding in from the left with an easy ease,
|
|
9
|
-
in my house style."
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
**Requirements:** macOS · After Effects 2026 · [Node.js 20+](https://nodejs.org)
|
|
7
|
+
Requires After Effects 2026 and [Node.js 20+](https://nodejs.org).
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
## Getting started
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
### 1. Create a project folder
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
In a terminal:
|
|
19
14
|
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npx @engine-room/after-effects-mcp init my-video
|
|
17
|
+
cd my-video
|
|
23
18
|
```
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
Open that folder in your AI client. It contains everything the client needs to find the tools.
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Add this to your client's MCP config (for Claude Desktop, `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
22
|
+
*If your client uses one global configuration file rather than per-folder settings* — Claude Desktop, for example — add this to it instead. On macOS it is `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
30
23
|
|
|
31
24
|
```json
|
|
32
25
|
{
|
|
@@ -39,110 +32,130 @@ Add this to your client's MCP config (for Claude Desktop, `~/Library/Application
|
|
|
39
32
|
}
|
|
40
33
|
```
|
|
41
34
|
|
|
42
|
-
Restart the client.
|
|
35
|
+
Restart the client afterwards.
|
|
43
36
|
|
|
44
|
-
|
|
37
|
+
### 2. Connect After Effects
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
Open After Effects, then ask the AI:
|
|
47
40
|
|
|
48
|
-
>
|
|
41
|
+
> Set up After Effects.
|
|
49
42
|
|
|
50
|
-
|
|
43
|
+
It installs a small panel inside After Effects, then tells you to quit and reopen it. Ask it to check the setup again to confirm.
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
2. If it still doesn't connect, **reboot once**. On some macOS builds the Adobe preference only applies after a restart. One time only.
|
|
45
|
+
This is a one-time step per machine. Until it is done, every tool reports that it cannot reach After Effects.
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
### 3. Start working
|
|
56
48
|
|
|
57
|
-
|
|
49
|
+
With After Effects open and your project folder open in your client, describe what you want:
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
npx @engine-room/after-effects-mcp init my-video
|
|
61
|
-
```
|
|
51
|
+
> Build a lower third that says Chapter One, sliding in from the left.
|
|
62
52
|
|
|
63
|
-
|
|
53
|
+
The AI reads the current state of your comp, makes the change, and can take a screenshot to check the result.
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
my-video/
|
|
67
|
-
├── .claude/skills/house-style/SKILL.md ← your palette, type, timing. Edit this.
|
|
68
|
-
├── CLAUDE.md ← what this project is
|
|
69
|
-
└── renders/
|
|
70
|
-
```
|
|
55
|
+
Day to day, this is the only step you repeat. Create a new folder when you start a new project.
|
|
71
56
|
|
|
72
|
-
|
|
57
|
+
## Your house style
|
|
73
58
|
|
|
74
|
-
|
|
59
|
+
Each project folder contains `.claude/skills/house-style/SKILL.md`. Fill it in with your colours, fonts and motion defaults, and everything built afterwards follows them.
|
|
75
60
|
|
|
76
|
-
|
|
61
|
+
A quick way to write it: build one piece the way you want it, then ask the AI to read that comp and write it up in the house-style skill.
|
|
77
62
|
|
|
78
|
-
|
|
63
|
+
Keep one folder per client or series. To apply the same style everywhere instead, put the skill in `~/.claude/skills/house-style/`.
|
|
79
64
|
|
|
80
|
-
|
|
65
|
+
Updating the tools never touches this file.
|
|
81
66
|
|
|
82
|
-
##
|
|
67
|
+
## Updating
|
|
68
|
+
|
|
69
|
+
Two things to update: the tools, and the panel inside After Effects. The panel does not update on its own.
|
|
70
|
+
|
|
71
|
+
Restart your AI client — it picks up the current version of the tools. Then, with After Effects open, ask:
|
|
72
|
+
|
|
73
|
+
> Update the After Effects panel.
|
|
74
|
+
|
|
75
|
+
It reinstalls the panel and tells you to restart After Effects. Ask it to check the setup afterwards, which reports whether the panel matches the current version.
|
|
76
|
+
|
|
77
|
+
## Tools
|
|
83
78
|
|
|
84
79
|
| Group | Tools |
|
|
85
80
|
|---|---|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
-
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
81
|
+
| Comps (7) | `list_comps`, `get_comp`, `get_comp_tree`, `create_comp`, `set_comp`, `delete_comp`, `set_active_comp` |
|
|
82
|
+
| Layers (15) | `list_layers`, `get_layer_full`, `create_{text,shape,solid,null,adjustment,precomp,camera,light}_layer`, `duplicate_layer`, `delete_layer`, `set_layer`, `parent_layer`, `reorder_layer` |
|
|
83
|
+
| Transforms (1) | `set_transform` — position, scale, rotation, anchor, opacity; 2D and 3D; optionally keyframed |
|
|
84
|
+
| Keyframes (6) | `add_keyframe`, `remove_keyframe`, `get_keyframes`, `set_interpolation`, `set_temporal_ease`, `set_spatial_tangents` |
|
|
85
|
+
| Expressions (4) | `get_expression`, `set_expression`, `toggle_expression`, `clear_expression` |
|
|
86
|
+
| Effects (6) | `list_effects`, `add_effect`, `remove_effect`, `set_effect_param`, `set_effect_enabled`, `list_available_effects` |
|
|
87
|
+
| Text (2) | `set_text`, `add_text_animator` |
|
|
88
|
+
| Shapes (3) | `set_shape_path`, `add_shape_content`, `set_shape_property` |
|
|
89
|
+
| Masks (3) | `add_mask`, `set_mask`, `remove_mask` |
|
|
90
|
+
| Markers (2) | `add_marker`, `remove_marker` |
|
|
91
|
+
| Vision (2) | `screenshot_frame`, `screenshot_layer` |
|
|
92
|
+
| Batch (1) | `run_batch` |
|
|
93
|
+
| Explore (2) | `get_project_summary`, `find_layers` |
|
|
94
|
+
| Raw (1) | `run_jsx` |
|
|
95
|
+
| Jobs (3) | `await_job`, `get_job`, `cancel_job` |
|
|
96
|
+
| Setup (2) | `check_setup`, `setup_panel` |
|
|
97
|
+
| Issues (3) | `list_known_issues`, `log_issue`, `mark_issue_reported` |
|
|
98
|
+
|
|
99
|
+
A few notes on the ones that behave differently from the rest:
|
|
100
|
+
|
|
101
|
+
- `get_layer_full` returns a layer's transforms with their keyframes and expressions, every effect with every parameter, masks, markers and visible bounds — in one call.
|
|
102
|
+
- `run_batch` runs many operations in a single pass and counts as one undo step. Long batches stream progress.
|
|
103
|
+
- `screenshot_frame` and `screenshot_layer` are for occasional checks, not for reviewing motion frame by frame. On large comps, `downsample: 2` renders at half resolution, which is faster and keeps the image small.
|
|
104
|
+
- `run_jsx` runs arbitrary ExtendScript for anything the other tools do not cover.
|
|
105
|
+
- `log_issue` and `list_known_issues` are the notebook described below.
|
|
106
|
+
|
|
107
|
+
## When something goes wrong
|
|
108
|
+
|
|
109
|
+
These tools have rough edges. When the AI hits one and works out a way around
|
|
110
|
+
it, it writes the problem and the fix into a notebook in your project folder, at
|
|
111
|
+
`.ae-mcp/issues/` — plain text files you can read or delete. The next session
|
|
112
|
+
reads that notebook before guessing, so the same twenty minutes are never spent
|
|
113
|
+
twice on the same project.
|
|
114
|
+
|
|
115
|
+
The folder ignores itself, so it stays out of version control without you doing
|
|
116
|
+
anything.
|
|
117
|
+
|
|
118
|
+
If the problem looks like ours rather than yours, the AI will say so at the end
|
|
119
|
+
of its reply and offer to pass it on. You can also start that yourself:
|
|
112
120
|
|
|
113
121
|
```
|
|
114
|
-
|
|
115
|
-
│ stdio (JSON-RPC)
|
|
116
|
-
▼
|
|
117
|
-
MCP server (Node)
|
|
118
|
-
│ HTTP + WebSocket on 127.0.0.1
|
|
119
|
-
▼
|
|
120
|
-
CEP panel inside After Effects
|
|
121
|
-
│ evalScript
|
|
122
|
-
▼
|
|
123
|
-
ExtendScript → After Effects
|
|
122
|
+
/report-ae-issue
|
|
124
123
|
```
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
It writes the report, shows it to you, and only sends it once you say yes.
|
|
126
|
+
Nothing about your own work — comp names, file paths, clients — goes into it.
|
|
127
|
+
Sending needs the [GitHub CLI](https://cli.github.com); without it you get a
|
|
128
|
+
prefilled link to click instead.
|
|
127
129
|
|
|
128
130
|
## Troubleshooting
|
|
129
131
|
|
|
130
|
-
Ask
|
|
132
|
+
Ask the AI to check the After Effects setup. It reports which part is broken and what to do about it.
|
|
131
133
|
|
|
132
|
-
| Symptom | Cause |
|
|
134
|
+
| Symptom | Cause and fix |
|
|
133
135
|
|---|---|
|
|
134
|
-
| "Cannot reach the After Effects panel" | AE
|
|
135
|
-
| Tools worked before, now
|
|
136
|
-
| Panel never loads, setup looks correct |
|
|
137
|
-
|
|
|
136
|
+
| "Cannot reach the After Effects panel" | AE is not running, or the panel is not installed. Ask the AI to set up After Effects, then restart AE. |
|
|
137
|
+
| Tools worked before, now fail inside AE | The tools were updated but the panel was not. Ask the AI to update the panel, then restart AE. |
|
|
138
|
+
| Panel never loads, setup looks correct | On macOS, reboot once. Some builds cache the Adobe setting until a restart. |
|
|
139
|
+
| A panel answers but the setup check reports none installed | An older install is still serving. Remove it from the CEP extensions folder, reinstall the panel, restart AE. |
|
|
140
|
+
| Need the panel's own log | In AE: Window → Extensions → AE MCP Bridge. |
|
|
141
|
+
|
|
142
|
+
## Platforms
|
|
143
|
+
|
|
144
|
+
macOS and Windows. Two things differ, and are handled for you:
|
|
145
|
+
|
|
146
|
+
| | macOS | Windows |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| Panel location | `~/Library/Application Support/Adobe/CEP/extensions` | `%APPDATA%\Adobe\CEP\extensions` |
|
|
149
|
+
| Unsigned-panel setting | `defaults` preference | `HKCU\Software\Adobe\CSXS.*` registry value |
|
|
150
|
+
|
|
151
|
+
macOS is the more exercised of the two; [issue reports](https://github.com/Engine-Room-Games/after-effects-mcp/issues) are welcome.
|
|
138
152
|
|
|
139
153
|
## Limitations
|
|
140
154
|
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
- **Not covered:** render queue, footage import and replacement, AE preferences. Preferences are excluded by design — this tool animates, it doesn't reconfigure your app.
|
|
155
|
+
- The panel is unsigned, so loading it requires Adobe's `PlayerDebugMode`, which the setup step enables. This is Adobe's documented path for unsigned extensions.
|
|
156
|
+
- `saveFrameToPng` is community-known rather than officially documented. It works, but alpha edges can be imperfect on some comps.
|
|
157
|
+
- A long synchronous loop in `run_jsx` will freeze the After Effects UI. Use `run_batch` for bulk work.
|
|
158
|
+
- Not covered: the render queue, footage import and replacement, and application preferences.
|
|
146
159
|
|
|
147
160
|
## Development
|
|
148
161
|
|
|
@@ -150,26 +163,23 @@ Ask Claude to run `check_setup` first — it reports exactly which link in the c
|
|
|
150
163
|
git clone https://github.com/Engine-Room-Games/after-effects-mcp.git
|
|
151
164
|
cd after-effects-mcp
|
|
152
165
|
npm install && npm run build
|
|
153
|
-
npm run install:panel
|
|
154
|
-
npm run doctor
|
|
166
|
+
npm run install:panel
|
|
167
|
+
npm run doctor
|
|
155
168
|
```
|
|
156
169
|
|
|
157
|
-
| Command |
|
|
170
|
+
| Command | Purpose |
|
|
158
171
|
|---|---|
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
172
|
+
| `make build` | Compile TypeScript and the ExtendScript bundle |
|
|
173
|
+
| `make jsx` | Rebuild `bundle.jsx` and hot-reload it into a running After Effects |
|
|
174
|
+
| `make watch` | TypeScript watch mode |
|
|
175
|
+
| `make doctor` | Diagnose the install |
|
|
176
|
+
| `make verify` | Build, check version strings agree, dry-run the package |
|
|
177
|
+
| `make release` | Bump the patch version, tag, and publish |
|
|
178
|
+
| `make release 1.1.0` | Set an explicit version, tag, and publish |
|
|
165
179
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
curl -X POST http://127.0.0.1:7777/reload-jsx
|
|
170
|
-
```
|
|
180
|
+
Adding a tool takes three edits: a zod schema in `packages/shared/src/schemas.ts`, a handler in the matching `packages/jsx/*.jsx` module, and a description in `packages/mcp-server/src/tools/descriptions.ts`. Registration is automatic.
|
|
171
181
|
|
|
172
|
-
|
|
182
|
+
See [CLAUDE.md](CLAUDE.md) for the architecture, the ExtendScript conventions, and the known-fragile areas.
|
|
173
183
|
|
|
174
184
|
## License
|
|
175
185
|
|
package/bin/server.js
CHANGED
|
@@ -65,16 +65,49 @@ description: The visual and motion style for this project \u2014 palette, type,
|
|
|
65
65
|
- _Anything that should always or never happen. E.g. "never put text directly on
|
|
66
66
|
footage \u2014 always on a rounded chip", "keep total runtime under 8 seconds"._
|
|
67
67
|
`;
|
|
68
|
+
var REPORT_COMMAND = `---
|
|
69
|
+
description: Send a problem you hit with the After Effects tools to the people who maintain them
|
|
70
|
+
argument-hint: "[what went wrong, in your own words]"
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
# Report a problem with the After Effects tools
|
|
74
|
+
|
|
75
|
+
Assume the person you are helping is a motion designer, not a developer, and may
|
|
76
|
+
never have used GitHub. Do the technical part for them.
|
|
77
|
+
|
|
78
|
+
1. **Find what to report.** Call \`list_known_issues\` with \`status: "unreported"\`.
|
|
79
|
+
It returns what earlier sessions wrote down, plus \`repo\`, \`newIssueUrl\`,
|
|
80
|
+
\`serverVersion\` and \`platform\`. List the entries in plain sentences \u2014 not raw
|
|
81
|
+
titles \u2014 and ask which to send. If there is nothing recorded but \`$ARGUMENTS\`
|
|
82
|
+
describes a problem, ask what they were doing and what happened, then
|
|
83
|
+
\`log_issue\` it first. If there is nothing at all, say so and stop.
|
|
84
|
+
|
|
85
|
+
2. **Draft it short.** Title: one concrete line. Body: **What happens** (the
|
|
86
|
+
failing call and exact error), **Why** if known, **Workaround**, and
|
|
87
|
+
**Environment** (\`after-effects-mcp <serverVersion> \xB7 <platform> \xB7 After
|
|
88
|
+
Effects 2026\`). Leave out their own work \u2014 comp names, file paths, client
|
|
89
|
+
names, anything about the video.
|
|
90
|
+
|
|
91
|
+
3. **Show it and ask.** This posts publicly, so get a real yes.
|
|
92
|
+
|
|
93
|
+
4. **Send it.** \`gh issue create --repo <repo> --title "..." --body "..."\`. If
|
|
94
|
+
\`gh\` is missing or not logged in, do not install it \u2014 build a prefilled link
|
|
95
|
+
instead by URL-encoding the title and body onto \`<newIssueUrl>\` as
|
|
96
|
+
\`?title=\u2026&body=\u2026\`, and tell them to open it and press the green button.
|
|
97
|
+
|
|
98
|
+
5. **Close the loop.** On success call \`mark_issue_reported\` with the entry id
|
|
99
|
+
and URL, then give them the link. If they decline, leave the entry unreported.
|
|
100
|
+
`;
|
|
68
101
|
var CLAUDE_MD = `# {{NAME}}
|
|
69
102
|
|
|
70
|
-
After Effects project folder.
|
|
103
|
+
After Effects project folder. The tools drive After Effects directly from here.
|
|
71
104
|
|
|
72
105
|
## How to work in this folder
|
|
73
106
|
|
|
74
107
|
1. Open After Effects with the project you want to work on.
|
|
75
108
|
2. Describe what you want in plain language \u2014 "build a lower third that says
|
|
76
109
|
Chapter One and slides in from the left".
|
|
77
|
-
3.
|
|
110
|
+
3. The current state of the comp is read, the change is made, and you see it.
|
|
78
111
|
|
|
79
112
|
## Style
|
|
80
113
|
|
|
@@ -82,6 +115,14 @@ The look of everything built here is defined in
|
|
|
82
115
|
\`.claude/skills/house-style/SKILL.md\`. Edit that file to change the defaults \u2014
|
|
83
116
|
palette, type, timing, layout. It is read automatically.
|
|
84
117
|
|
|
118
|
+
## When a tool misbehaves
|
|
119
|
+
|
|
120
|
+
Check \`list_known_issues\` before guessing \u2014 an earlier session may already have
|
|
121
|
+
solved it. When you work out a fix for something that cost real time and was the
|
|
122
|
+
tool's fault rather than yours, record it with \`log_issue\` so the next session
|
|
123
|
+
does not pay for it again, and offer at the end of your reply to pass it on.
|
|
124
|
+
\`/report-ae-issue\` sends it to the maintainers.
|
|
125
|
+
|
|
85
126
|
## Conventions for this project
|
|
86
127
|
|
|
87
128
|
<!-- Anything specific to this project rather than to your general style:
|
|
@@ -99,10 +140,11 @@ var MCP_JSON = `{
|
|
|
99
140
|
}
|
|
100
141
|
}
|
|
101
142
|
`;
|
|
143
|
+
var KNOWN_FLAGS = ["--no-mcp", "--with-mcp"];
|
|
102
144
|
function parseInitArgs(argv) {
|
|
103
145
|
const positional = argv.filter((a) => !a.startsWith("-"));
|
|
104
|
-
const withMcp = argv.includes("--
|
|
105
|
-
const unknown = argv.filter((a) => a.startsWith("-") && a
|
|
146
|
+
const withMcp = !argv.includes("--no-mcp");
|
|
147
|
+
const unknown = argv.filter((a) => a.startsWith("-") && !KNOWN_FLAGS.includes(a));
|
|
106
148
|
if (unknown.length > 0) return { error: `Unknown option: ${unknown[0]}` };
|
|
107
149
|
if (positional.length === 0) return { error: "Missing target directory." };
|
|
108
150
|
if (positional.length > 1) return { error: `Expected one directory, got ${positional.length}.` };
|
|
@@ -113,7 +155,7 @@ function runInit(argv) {
|
|
|
113
155
|
if ("error" in parsed) {
|
|
114
156
|
process.stderr.write(`${parsed.error}
|
|
115
157
|
|
|
116
|
-
Usage: npx @engine-room/after-effects-mcp init <directory> [--
|
|
158
|
+
Usage: npx @engine-room/after-effects-mcp init <directory> [--no-mcp]
|
|
117
159
|
`);
|
|
118
160
|
return 1;
|
|
119
161
|
}
|
|
@@ -122,6 +164,7 @@ Usage: npx @engine-room/after-effects-mcp init <directory> [--with-mcp]
|
|
|
122
164
|
const files = [
|
|
123
165
|
["CLAUDE.md", CLAUDE_MD.replace("{{NAME}}", name)],
|
|
124
166
|
[path.join(".claude", "skills", "house-style", "SKILL.md"), HOUSE_STYLE],
|
|
167
|
+
[path.join(".claude", "commands", "report-ae-issue.md"), REPORT_COMMAND],
|
|
125
168
|
[path.join("renders", ".gitkeep"), ""]
|
|
126
169
|
];
|
|
127
170
|
if (parsed.withMcp) files.push([".mcp.json", MCP_JSON]);
|
|
@@ -146,18 +189,18 @@ Delete them first, or choose a different directory.
|
|
|
146
189
|
``,
|
|
147
190
|
` CLAUDE.md what this project is`,
|
|
148
191
|
` .claude/skills/house-style/SKILL.md your look \u2014 edit this first`,
|
|
192
|
+
` .claude/commands/report-ae-issue.md /report-ae-issue \u2014 tell the maintainers something broke`,
|
|
149
193
|
` renders/ exports land here`,
|
|
150
|
-
...parsed.withMcp ? [` .mcp.json connects
|
|
194
|
+
...parsed.withMcp ? [` .mcp.json connects your client to After Effects`] : [],
|
|
151
195
|
``,
|
|
152
196
|
`Next:`,
|
|
153
|
-
` 1. Open the folder in
|
|
154
|
-
` 2.
|
|
155
|
-
` 3.
|
|
197
|
+
` 1. Open the folder in your MCP client: cd ${parsed.dir}`,
|
|
198
|
+
` 2. Open After Effects, then ask it to set up After Effects.`,
|
|
199
|
+
` 3. Fill in .claude/skills/house-style/SKILL.md with your palette, type and timing.`,
|
|
156
200
|
...parsed.withMcp ? [] : [
|
|
157
201
|
``,
|
|
158
|
-
`This folder has no .mcp.json
|
|
159
|
-
`
|
|
160
|
-
`re-run with --with-mcp to add a project-level connection instead.`
|
|
202
|
+
`This folder has no .mcp.json, so your client must already provide the`,
|
|
203
|
+
`After Effects tools some other way.`
|
|
161
204
|
],
|
|
162
205
|
``
|
|
163
206
|
].join("\n");
|
|
@@ -262,7 +305,10 @@ __export(schemas_exports, {
|
|
|
262
305
|
ListAvailableEffects: () => ListAvailableEffects,
|
|
263
306
|
ListComps: () => ListComps,
|
|
264
307
|
ListEffects: () => ListEffects,
|
|
308
|
+
ListKnownIssues: () => ListKnownIssues,
|
|
265
309
|
ListLayers: () => ListLayers,
|
|
310
|
+
LogIssue: () => LogIssue,
|
|
311
|
+
MarkIssueReported: () => MarkIssueReported,
|
|
266
312
|
OpSchemas: () => OpSchemas,
|
|
267
313
|
ParentLayer: () => ParentLayer,
|
|
268
314
|
PropertyPath: () => PropertyPath,
|
|
@@ -669,6 +715,21 @@ var SetupPanel = z2.object({
|
|
|
669
715
|
enableDebugMode: z2.boolean().default(true).optional().describe("Also enable Adobe's PlayerDebugMode preference, which AE requires to load this unsigned panel. Default true."),
|
|
670
716
|
force: z2.boolean().default(false).optional().describe("Replace an existing symlinked (development) install with a copy. Default false.")
|
|
671
717
|
}).strict();
|
|
718
|
+
var LogIssue = z2.object({
|
|
719
|
+
title: z2.string().min(3).describe("One line naming the problem, specific enough to recognise again. Becomes the entry's id."),
|
|
720
|
+
symptom: z2.string().min(3).describe("What went wrong, including the exact error text and the call that produced it."),
|
|
721
|
+
workaround: z2.string().min(3).describe("What actually worked \u2014 concrete enough for the next session to apply without rediscovering it."),
|
|
722
|
+
cause: z2.string().optional().describe("Why it happens, if you worked it out."),
|
|
723
|
+
tools: z2.array(z2.string()).optional().describe("Tool names involved, e.g. ['set_temporal_ease'].")
|
|
724
|
+
}).strict();
|
|
725
|
+
var ListKnownIssues = z2.object({
|
|
726
|
+
status: z2.enum(["all", "unreported", "reported"]).default("all").optional(),
|
|
727
|
+
tool: z2.string().optional().describe("Only entries about this tool, e.g. 'set_temporal_ease'. Omit for everything.")
|
|
728
|
+
}).strict();
|
|
729
|
+
var MarkIssueReported = z2.object({
|
|
730
|
+
id: z2.string().describe("The entry id returned by log_issue or list_known_issues."),
|
|
731
|
+
url: z2.string().optional().describe("Link to the issue that was opened.")
|
|
732
|
+
}).strict();
|
|
672
733
|
var AwaitJob = z2.object({ jobId: z2.string(), timeoutMs: z2.number().int().positive().default(6e5).optional() });
|
|
673
734
|
var GetJob = z2.object({ jobId: z2.string() });
|
|
674
735
|
var CancelJob = z2.object({ jobId: z2.string() });
|
|
@@ -748,7 +809,11 @@ var OpSchemas = {
|
|
|
748
809
|
cancel_job: CancelJob,
|
|
749
810
|
// setup
|
|
750
811
|
check_setup: CheckSetup,
|
|
751
|
-
setup_panel: SetupPanel
|
|
812
|
+
setup_panel: SetupPanel,
|
|
813
|
+
// issue journal
|
|
814
|
+
log_issue: LogIssue,
|
|
815
|
+
list_known_issues: ListKnownIssues,
|
|
816
|
+
mark_issue_reported: MarkIssueReported
|
|
752
817
|
};
|
|
753
818
|
|
|
754
819
|
// src/util/errors.ts
|
|
@@ -1106,15 +1171,17 @@ var descriptions = {
|
|
|
1106
1171
|
cancel_job: "Set cancel flag; chunked loop stops at next boundary.",
|
|
1107
1172
|
// ---------- setup ----------
|
|
1108
1173
|
check_setup: "Diagnose the After Effects connection: panel installed, up to date, Adobe debug preference on, AE running, bridge answering. Read-only and safe to call any time. Call this FIRST whenever another tool reports it cannot reach After Effects, then relay `nextSteps` to the user in plain language.",
|
|
1109
|
-
setup_panel: "Install or refresh the After Effects panel and enable the Adobe preference AE needs to load it. Run this when check_setup reports the panel is missing or out of date. It writes to the user's Adobe CEP extensions folder and sets a user-level Adobe preference \u2014 tell the user what it will do before calling it. Afterwards, AE must be restarted; if the preference was newly enabled, a one-time Mac reboot may also be needed."
|
|
1174
|
+
setup_panel: "Install or refresh the After Effects panel and enable the Adobe preference AE needs to load it. Run this when check_setup reports the panel is missing or out of date. It writes to the user's Adobe CEP extensions folder and sets a user-level Adobe preference \u2014 tell the user what it will do before calling it. Afterwards, AE must be restarted; if the preference was newly enabled, a one-time Mac reboot may also be needed.",
|
|
1175
|
+
// ---------- issue journal ----------
|
|
1176
|
+
list_known_issues: "Problems earlier sessions hit with these tools, with the workarounds that worked. Read it when a tool fails in a way you don't immediately understand \u2014 pass `tool` to narrow it to that one \u2014 and before nontrivial work. It can save you rediscovering a fix that already cost someone an hour. Also returns the repo and server version needed to report one.",
|
|
1177
|
+
log_issue: "Record a problem you hit and the workaround that got past it, so the next session doesn't rediscover it. Log only what cost real effort and will recur: a tool failing for a non-obvious reason, an argument shape the schema didn't imply, AE behaving unlike the docs. Not your own typos, not one-off user mistakes. Call list_known_issues first and reuse the same title to extend an existing entry rather than duplicating it. If the result comes back with reported:false, then AFTER you have finished the actual work, close your reply by telling the user in plain language that something took much longer than it should have and offering to pass it to the people who maintain this tool \u2014 phrase it for a motion designer, in terms of what actually happened, and don't say 'GitHub issue' or 'bug report' unless they say it first.",
|
|
1178
|
+
mark_issue_reported: "Record that a journal entry has been sent to the maintainers, with the resulting URL. Call it only once the issue really exists, so later sessions don't ask the user to report the same thing twice."
|
|
1110
1179
|
};
|
|
1111
1180
|
|
|
1112
1181
|
// src/setup/check.ts
|
|
1113
|
-
import { execFile } from "node:child_process";
|
|
1114
1182
|
import crypto from "node:crypto";
|
|
1115
1183
|
import fs4 from "node:fs";
|
|
1116
1184
|
import path4 from "node:path";
|
|
1117
|
-
import { promisify } from "node:util";
|
|
1118
1185
|
|
|
1119
1186
|
// src/setup/paths.ts
|
|
1120
1187
|
import fs3 from "node:fs";
|
|
@@ -1123,6 +1190,9 @@ import path3 from "node:path";
|
|
|
1123
1190
|
import { createRequire } from "node:module";
|
|
1124
1191
|
import { fileURLToPath } from "node:url";
|
|
1125
1192
|
var BUNDLE_ID = "games.engine-room.ae-mcp";
|
|
1193
|
+
function isSupportedPlatform() {
|
|
1194
|
+
return process.platform === "darwin" || process.platform === "win32";
|
|
1195
|
+
}
|
|
1126
1196
|
function packageRoot() {
|
|
1127
1197
|
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
1128
1198
|
for (let i = 0; i < 8; i++) {
|
|
@@ -1133,6 +1203,14 @@ function packageRoot() {
|
|
|
1133
1203
|
}
|
|
1134
1204
|
return path3.dirname(fileURLToPath(import.meta.url));
|
|
1135
1205
|
}
|
|
1206
|
+
function packageVersion() {
|
|
1207
|
+
try {
|
|
1208
|
+
const pkg = JSON.parse(fs3.readFileSync(path3.join(packageRoot(), "package.json"), "utf8"));
|
|
1209
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
1210
|
+
} catch {
|
|
1211
|
+
return "unknown";
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1136
1214
|
function panelSourceDir() {
|
|
1137
1215
|
const candidates = [
|
|
1138
1216
|
// The live workspace copy comes first so a git checkout always installs
|
|
@@ -1147,6 +1225,10 @@ function panelSourceDir() {
|
|
|
1147
1225
|
return null;
|
|
1148
1226
|
}
|
|
1149
1227
|
function cepExtensionsDir() {
|
|
1228
|
+
if (process.platform === "win32") {
|
|
1229
|
+
const appData = process.env.APPDATA ?? path3.join(os2.homedir(), "AppData", "Roaming");
|
|
1230
|
+
return path3.join(appData, "Adobe", "CEP", "extensions");
|
|
1231
|
+
}
|
|
1150
1232
|
return path3.join(os2.homedir(), "Library", "Application Support", "Adobe", "CEP", "extensions");
|
|
1151
1233
|
}
|
|
1152
1234
|
function installedPanelDir() {
|
|
@@ -1178,27 +1260,84 @@ function copyRecursive(src, dst) {
|
|
|
1178
1260
|
}
|
|
1179
1261
|
}
|
|
1180
1262
|
|
|
1181
|
-
// src/setup/
|
|
1263
|
+
// src/setup/platform.ts
|
|
1264
|
+
import { execFile } from "node:child_process";
|
|
1265
|
+
import { promisify } from "node:util";
|
|
1182
1266
|
var exec = promisify(execFile);
|
|
1183
1267
|
var CSXS_VERSIONS = [12, 11, 10, 9];
|
|
1184
|
-
async function
|
|
1268
|
+
async function isDebugModeOn() {
|
|
1185
1269
|
for (const v of CSXS_VERSIONS) {
|
|
1186
1270
|
try {
|
|
1187
|
-
|
|
1188
|
-
|
|
1271
|
+
if (process.platform === "win32") {
|
|
1272
|
+
const { stdout } = await exec("reg", [
|
|
1273
|
+
"query",
|
|
1274
|
+
`HKCU\\Software\\Adobe\\CSXS.${v}`,
|
|
1275
|
+
"/v",
|
|
1276
|
+
"PlayerDebugMode"
|
|
1277
|
+
]);
|
|
1278
|
+
if (/PlayerDebugMode\s+REG_SZ\s+1\b/.test(stdout)) {
|
|
1279
|
+
return { on: true, detail: `enabled (CSXS.${v})` };
|
|
1280
|
+
}
|
|
1281
|
+
} else {
|
|
1282
|
+
const { stdout } = await exec("defaults", [
|
|
1283
|
+
"read",
|
|
1284
|
+
`com.adobe.CSXS.${v}`,
|
|
1285
|
+
"PlayerDebugMode"
|
|
1286
|
+
]);
|
|
1287
|
+
if (stdout.trim() === "1") return { on: true, detail: `enabled (CSXS.${v})` };
|
|
1288
|
+
}
|
|
1189
1289
|
} catch {
|
|
1190
1290
|
}
|
|
1191
1291
|
}
|
|
1192
1292
|
return { on: false, detail: "not enabled for any CSXS version" };
|
|
1193
1293
|
}
|
|
1194
|
-
async function
|
|
1294
|
+
async function enableDebugMode() {
|
|
1295
|
+
const enabled = [];
|
|
1296
|
+
for (const v of CSXS_VERSIONS) {
|
|
1297
|
+
try {
|
|
1298
|
+
if (process.platform === "win32") {
|
|
1299
|
+
await exec("reg", [
|
|
1300
|
+
"add",
|
|
1301
|
+
`HKCU\\Software\\Adobe\\CSXS.${v}`,
|
|
1302
|
+
"/v",
|
|
1303
|
+
"PlayerDebugMode",
|
|
1304
|
+
"/t",
|
|
1305
|
+
"REG_SZ",
|
|
1306
|
+
"/d",
|
|
1307
|
+
"1",
|
|
1308
|
+
"/f"
|
|
1309
|
+
]);
|
|
1310
|
+
} else {
|
|
1311
|
+
await exec("defaults", [
|
|
1312
|
+
"write",
|
|
1313
|
+
`com.adobe.CSXS.${v}`,
|
|
1314
|
+
"PlayerDebugMode",
|
|
1315
|
+
"1"
|
|
1316
|
+
]);
|
|
1317
|
+
}
|
|
1318
|
+
enabled.push(v);
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return enabled;
|
|
1323
|
+
}
|
|
1324
|
+
async function isAfterEffectsRunning() {
|
|
1195
1325
|
try {
|
|
1326
|
+
if (process.platform === "win32") {
|
|
1327
|
+
const { stdout: stdout2 } = await exec("tasklist", ["/FI", "IMAGENAME eq AfterFX.exe", "/NH"]);
|
|
1328
|
+
return /AfterFX\.exe/i.test(stdout2);
|
|
1329
|
+
}
|
|
1196
1330
|
const { stdout } = await exec("pgrep", ["-f", "Adobe After Effects"]);
|
|
1197
1331
|
return stdout.trim().length > 0;
|
|
1198
1332
|
} catch {
|
|
1199
1333
|
return false;
|
|
1200
1334
|
}
|
|
1201
1335
|
}
|
|
1336
|
+
function debugModeLocation() {
|
|
1337
|
+
return process.platform === "win32" ? "the PlayerDebugMode value under HKEY_CURRENT_USER\\Software\\Adobe\\CSXS.*" : "Adobe's PlayerDebugMode preference";
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/setup/check.ts
|
|
1202
1341
|
function sha256(file) {
|
|
1203
1342
|
try {
|
|
1204
1343
|
return crypto.createHash("sha256").update(fs4.readFileSync(file)).digest("hex");
|
|
@@ -1217,12 +1356,12 @@ async function bridgeReachable(port) {
|
|
|
1217
1356
|
}
|
|
1218
1357
|
async function checkSetup() {
|
|
1219
1358
|
const checks = [];
|
|
1220
|
-
const
|
|
1359
|
+
const supported = isSupportedPlatform();
|
|
1221
1360
|
checks.push({
|
|
1222
1361
|
name: "platform",
|
|
1223
|
-
ok:
|
|
1362
|
+
ok: supported,
|
|
1224
1363
|
detail: process.platform,
|
|
1225
|
-
fix:
|
|
1364
|
+
fix: supported ? void 0 : "After Effects runs only on macOS and Windows, so this server supports only those two."
|
|
1226
1365
|
});
|
|
1227
1366
|
const source = panelSourceDir();
|
|
1228
1367
|
checks.push({
|
|
@@ -1231,12 +1370,12 @@ async function checkSetup() {
|
|
|
1231
1370
|
detail: source ?? "not found in this installation",
|
|
1232
1371
|
fix: source ? void 0 : "The package is missing its CEP panel assets \u2014 reinstall the server."
|
|
1233
1372
|
});
|
|
1234
|
-
const debugMode = await
|
|
1373
|
+
const debugMode = await isDebugModeOn();
|
|
1235
1374
|
checks.push({
|
|
1236
1375
|
name: "cepDebugMode",
|
|
1237
1376
|
ok: debugMode.on,
|
|
1238
1377
|
detail: debugMode.detail,
|
|
1239
|
-
fix: debugMode.on ? void 0 :
|
|
1378
|
+
fix: debugMode.on ? void 0 : `Run the setup_panel tool. After Effects only loads unsigned panels when ${debugModeLocation()} is set.`
|
|
1240
1379
|
});
|
|
1241
1380
|
const installed = installedPanelDir();
|
|
1242
1381
|
const isInstalled = fs4.existsSync(path4.join(installed, "CSXS", "manifest.xml"));
|
|
@@ -1257,7 +1396,7 @@ async function checkSetup() {
|
|
|
1257
1396
|
fix: upToDate ? void 0 : "Run setup_panel to refresh it, then restart After Effects."
|
|
1258
1397
|
});
|
|
1259
1398
|
}
|
|
1260
|
-
const running = await
|
|
1399
|
+
const running = await isAfterEffectsRunning();
|
|
1261
1400
|
checks.push({
|
|
1262
1401
|
name: "afterEffectsRunning",
|
|
1263
1402
|
ok: running,
|
|
@@ -1272,6 +1411,14 @@ async function checkSetup() {
|
|
|
1272
1411
|
detail: bridge.detail,
|
|
1273
1412
|
fix: bridge.ok ? void 0 : "If the other checks pass, restart After Effects so the panel reloads."
|
|
1274
1413
|
});
|
|
1414
|
+
if (bridge.ok && !isInstalled) {
|
|
1415
|
+
checks.push({
|
|
1416
|
+
name: "panelIdentity",
|
|
1417
|
+
ok: false,
|
|
1418
|
+
detail: `a panel is answering on port ${port}, but not the one at ${installed}`,
|
|
1419
|
+
fix: `An older install is serving the bridge. Look in ${cepExtensionsDir()} for a differently named folder, remove it, then run setup_panel and restart After Effects.`
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1275
1422
|
const ready = checks.every((c) => c.ok);
|
|
1276
1423
|
return { ready, checks, nextSteps: buildNextSteps(checks, ready) };
|
|
1277
1424
|
}
|
|
@@ -1287,7 +1434,13 @@ function buildNextSteps(checks, ready) {
|
|
|
1287
1434
|
steps.push("Run the setup_panel tool \u2014 it installs the After Effects panel and enables the Adobe preference that lets AE load it.");
|
|
1288
1435
|
}
|
|
1289
1436
|
if (needsDebug) {
|
|
1290
|
-
steps.push(
|
|
1437
|
+
steps.push(
|
|
1438
|
+
process.platform === "win32" ? "Quit and reopen After Effects so it re-reads the registry." : "Quit and reopen After Effects. If the panel still does not connect, restart the Mac once \u2014 the Adobe preference sometimes only takes effect after a reboot."
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
const identity = by("panelIdentity");
|
|
1442
|
+
if (identity && identity.ok === false) {
|
|
1443
|
+
steps.push(identity.fix);
|
|
1291
1444
|
}
|
|
1292
1445
|
if (by("afterEffectsRunning")?.ok === false) {
|
|
1293
1446
|
steps.push("Open After Effects 2026.");
|
|
@@ -1302,38 +1455,15 @@ function buildNextSteps(checks, ready) {
|
|
|
1302
1455
|
}
|
|
1303
1456
|
|
|
1304
1457
|
// src/setup/install.ts
|
|
1305
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
1306
1458
|
import fs5 from "node:fs";
|
|
1307
1459
|
import path5 from "node:path";
|
|
1308
|
-
import { promisify as promisify2 } from "node:util";
|
|
1309
|
-
var exec2 = promisify2(execFile2);
|
|
1310
|
-
var CSXS_VERSIONS2 = [12, 11, 10, 9];
|
|
1311
|
-
async function isDebugModeOn() {
|
|
1312
|
-
for (const v of CSXS_VERSIONS2) {
|
|
1313
|
-
try {
|
|
1314
|
-
const { stdout } = await exec2("defaults", ["read", `com.adobe.CSXS.${v}`, "PlayerDebugMode"]);
|
|
1315
|
-
if (stdout.trim() === "1") return true;
|
|
1316
|
-
} catch {
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
return false;
|
|
1320
|
-
}
|
|
1321
|
-
async function enableDebugMode() {
|
|
1322
|
-
const enabled = [];
|
|
1323
|
-
for (const v of CSXS_VERSIONS2) {
|
|
1324
|
-
try {
|
|
1325
|
-
await exec2("defaults", ["write", `com.adobe.CSXS.${v}`, "PlayerDebugMode", "1"]);
|
|
1326
|
-
enabled.push(v);
|
|
1327
|
-
} catch {
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
return enabled;
|
|
1331
|
-
}
|
|
1332
1460
|
async function installPanel(opts = {}) {
|
|
1333
1461
|
const actions = [];
|
|
1334
1462
|
const notes = [];
|
|
1335
|
-
if (
|
|
1336
|
-
throw new Error(
|
|
1463
|
+
if (!isSupportedPlatform()) {
|
|
1464
|
+
throw new Error(
|
|
1465
|
+
`After Effects runs only on macOS and Windows, so setup_panel cannot install anything on ${process.platform}.`
|
|
1466
|
+
);
|
|
1337
1467
|
}
|
|
1338
1468
|
const source = panelSourceDir();
|
|
1339
1469
|
if (!source) {
|
|
@@ -1377,16 +1507,16 @@ async function installPanel(opts = {}) {
|
|
|
1377
1507
|
}
|
|
1378
1508
|
let rebootRecommended = false;
|
|
1379
1509
|
if (opts.enableDebugMode !== false) {
|
|
1380
|
-
const
|
|
1381
|
-
if (
|
|
1382
|
-
actions.push(
|
|
1510
|
+
const existing2 = await isDebugModeOn();
|
|
1511
|
+
if (existing2.on) {
|
|
1512
|
+
actions.push(`PlayerDebugMode was already enabled \u2014 ${existing2.detail}.`);
|
|
1383
1513
|
} else {
|
|
1384
1514
|
const versions = await enableDebugMode();
|
|
1385
1515
|
if (versions.length === 0) {
|
|
1386
|
-
notes.push(
|
|
1516
|
+
notes.push(`Failed to set ${debugModeLocation()}. Without it After Effects will refuse to load this panel.`);
|
|
1387
1517
|
} else {
|
|
1388
1518
|
actions.push(`Enabled PlayerDebugMode for CSXS ${versions.join(", ")} so AE will load the unsigned panel.`);
|
|
1389
|
-
rebootRecommended =
|
|
1519
|
+
rebootRecommended = process.platform === "darwin";
|
|
1390
1520
|
}
|
|
1391
1521
|
}
|
|
1392
1522
|
}
|
|
@@ -1403,6 +1533,208 @@ async function installPanel(opts = {}) {
|
|
|
1403
1533
|
};
|
|
1404
1534
|
}
|
|
1405
1535
|
|
|
1536
|
+
// src/issues/journal.ts
|
|
1537
|
+
import fs6 from "node:fs";
|
|
1538
|
+
import os3 from "node:os";
|
|
1539
|
+
import path6 from "node:path";
|
|
1540
|
+
var REPO = "Engine-Room-Games/after-effects-mcp";
|
|
1541
|
+
var NEW_ISSUE_URL = `https://github.com/${REPO}/issues/new`;
|
|
1542
|
+
var SECTION_SYMPTOM = "What went wrong";
|
|
1543
|
+
var SECTION_CAUSE = "Why";
|
|
1544
|
+
var SECTION_WORKAROUND = "What worked";
|
|
1545
|
+
function journalRoot() {
|
|
1546
|
+
const override = process.env.AE_MCP_HOME?.trim();
|
|
1547
|
+
if (override && override.length > 0) return { dir: override, scope: "project" };
|
|
1548
|
+
const cwd = process.cwd();
|
|
1549
|
+
const unusable = cwd === path6.parse(cwd).root || cwd === os3.homedir();
|
|
1550
|
+
if (!unusable) {
|
|
1551
|
+
try {
|
|
1552
|
+
fs6.accessSync(cwd, fs6.constants.W_OK);
|
|
1553
|
+
return { dir: path6.join(cwd, ".ae-mcp"), scope: "project" };
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return { dir: path6.join(os3.homedir(), ".after-effects-mcp"), scope: "home" };
|
|
1558
|
+
}
|
|
1559
|
+
function journalDir() {
|
|
1560
|
+
return path6.join(journalRoot().dir, "issues");
|
|
1561
|
+
}
|
|
1562
|
+
function ensureJournalDir() {
|
|
1563
|
+
const { dir } = journalRoot();
|
|
1564
|
+
const issues = path6.join(dir, "issues");
|
|
1565
|
+
fs6.mkdirSync(issues, { recursive: true });
|
|
1566
|
+
const ignore = path6.join(dir, ".gitignore");
|
|
1567
|
+
if (!fs6.existsSync(ignore)) fs6.writeFileSync(ignore, "*\n", "utf8");
|
|
1568
|
+
return issues;
|
|
1569
|
+
}
|
|
1570
|
+
function slugify(text) {
|
|
1571
|
+
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60).replace(/-+$/g, "");
|
|
1572
|
+
return slug.length > 0 ? slug : `issue-${Date.now()}`;
|
|
1573
|
+
}
|
|
1574
|
+
function today() {
|
|
1575
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1576
|
+
}
|
|
1577
|
+
function oneLine(text) {
|
|
1578
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1579
|
+
}
|
|
1580
|
+
function entryPath(id) {
|
|
1581
|
+
const dir = path6.resolve(journalDir());
|
|
1582
|
+
const file = path6.resolve(dir, `${id}.md`);
|
|
1583
|
+
if (path6.dirname(file) !== dir) throw new Error(`Invalid issue id: ${id}`);
|
|
1584
|
+
return file;
|
|
1585
|
+
}
|
|
1586
|
+
function render(entry) {
|
|
1587
|
+
const lines = [
|
|
1588
|
+
"---",
|
|
1589
|
+
`id: ${entry.id}`,
|
|
1590
|
+
`title: ${oneLine(entry.title)}`,
|
|
1591
|
+
`tools: ${entry.tools.join(", ")}`,
|
|
1592
|
+
`firstSeen: ${entry.firstSeen}`,
|
|
1593
|
+
`lastSeen: ${entry.lastSeen}`,
|
|
1594
|
+
`occurrences: ${entry.occurrences}`,
|
|
1595
|
+
`reported: ${entry.reported}`,
|
|
1596
|
+
`issueUrl: ${entry.issueUrl ?? ""}`,
|
|
1597
|
+
"---",
|
|
1598
|
+
"",
|
|
1599
|
+
`## ${SECTION_SYMPTOM}`,
|
|
1600
|
+
"",
|
|
1601
|
+
entry.symptom.trim(),
|
|
1602
|
+
""
|
|
1603
|
+
];
|
|
1604
|
+
if (entry.cause && entry.cause.trim().length > 0) {
|
|
1605
|
+
lines.push(`## ${SECTION_CAUSE}`, "", entry.cause.trim(), "");
|
|
1606
|
+
}
|
|
1607
|
+
lines.push(`## ${SECTION_WORKAROUND}`, "", entry.workaround.trim(), "");
|
|
1608
|
+
return lines.join("\n");
|
|
1609
|
+
}
|
|
1610
|
+
function readSections(body) {
|
|
1611
|
+
const marks = [];
|
|
1612
|
+
for (const heading of [SECTION_SYMPTOM, SECTION_CAUSE, SECTION_WORKAROUND]) {
|
|
1613
|
+
const m = new RegExp(`^##[ \\t]+${heading}[ \\t]*$`, "im").exec(body);
|
|
1614
|
+
if (m) marks.push({ key: heading.toLowerCase(), from: m.index, to: m.index + m[0].length });
|
|
1615
|
+
}
|
|
1616
|
+
marks.sort((a, b) => a.from - b.from);
|
|
1617
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1618
|
+
marks.forEach((mark, i) => {
|
|
1619
|
+
const end = i + 1 < marks.length ? marks[i + 1].from : body.length;
|
|
1620
|
+
sections.set(mark.key, body.slice(mark.to, end).trim());
|
|
1621
|
+
});
|
|
1622
|
+
return sections;
|
|
1623
|
+
}
|
|
1624
|
+
function parse(text, fallbackId) {
|
|
1625
|
+
const meta = {};
|
|
1626
|
+
let body = text;
|
|
1627
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
1628
|
+
if (fm) {
|
|
1629
|
+
for (const line of fm[1].split(/\r?\n/)) {
|
|
1630
|
+
const sep = line.indexOf(":");
|
|
1631
|
+
if (sep <= 0) continue;
|
|
1632
|
+
meta[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
1633
|
+
}
|
|
1634
|
+
body = text.slice(fm[0].length);
|
|
1635
|
+
}
|
|
1636
|
+
const sections = readSections(body);
|
|
1637
|
+
const occurrences = Number.parseInt(meta.occurrences ?? "1", 10);
|
|
1638
|
+
return {
|
|
1639
|
+
id: meta.id || fallbackId,
|
|
1640
|
+
title: meta.title || fallbackId.replace(/-/g, " "),
|
|
1641
|
+
tools: (meta.tools ?? "").split(",").map((t) => t.trim()).filter((t) => t.length > 0),
|
|
1642
|
+
firstSeen: meta.firstSeen || "",
|
|
1643
|
+
lastSeen: meta.lastSeen || meta.firstSeen || "",
|
|
1644
|
+
occurrences: Number.isFinite(occurrences) && occurrences > 0 ? occurrences : 1,
|
|
1645
|
+
reported: meta.reported === "true",
|
|
1646
|
+
issueUrl: meta.issueUrl && meta.issueUrl.length > 0 ? meta.issueUrl : void 0,
|
|
1647
|
+
// A hand-edited file with no recognised headings still has its text kept,
|
|
1648
|
+
// rather than being silently reduced to an empty entry.
|
|
1649
|
+
symptom: sections.get(SECTION_SYMPTOM.toLowerCase()) ?? (sections.size === 0 ? body.trim() : ""),
|
|
1650
|
+
cause: sections.get(SECTION_CAUSE.toLowerCase()) || void 0,
|
|
1651
|
+
workaround: sections.get(SECTION_WORKAROUND.toLowerCase()) ?? ""
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function readEntry(file) {
|
|
1655
|
+
try {
|
|
1656
|
+
return parse(fs6.readFileSync(file, "utf8"), path6.basename(file, ".md"));
|
|
1657
|
+
} catch {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
function logIssue(input) {
|
|
1662
|
+
const id = slugify(input.title);
|
|
1663
|
+
const file = entryPath(id);
|
|
1664
|
+
const existing = fs6.existsSync(file) ? readEntry(file) : null;
|
|
1665
|
+
const entry = {
|
|
1666
|
+
id,
|
|
1667
|
+
title: oneLine(input.title),
|
|
1668
|
+
tools: input.tools ?? existing?.tools ?? [],
|
|
1669
|
+
firstSeen: existing?.firstSeen || today(),
|
|
1670
|
+
lastSeen: today(),
|
|
1671
|
+
// Repeats are worth counting: an entry seen five times is the one most
|
|
1672
|
+
// worth reporting, and the count is the only evidence of that.
|
|
1673
|
+
occurrences: (existing?.occurrences ?? 0) + 1,
|
|
1674
|
+
// Reporting state belongs to the entry, not to this sighting — a fresh
|
|
1675
|
+
// description of a known problem must not un-report it.
|
|
1676
|
+
reported: existing?.reported ?? false,
|
|
1677
|
+
issueUrl: existing?.issueUrl,
|
|
1678
|
+
symptom: input.symptom,
|
|
1679
|
+
// A cause worked out once is not lost because a later sighting was logged
|
|
1680
|
+
// without one.
|
|
1681
|
+
cause: input.cause ?? existing?.cause,
|
|
1682
|
+
workaround: input.workaround
|
|
1683
|
+
};
|
|
1684
|
+
ensureJournalDir();
|
|
1685
|
+
fs6.writeFileSync(file, render(entry), "utf8");
|
|
1686
|
+
return {
|
|
1687
|
+
id,
|
|
1688
|
+
path: file,
|
|
1689
|
+
occurrences: entry.occurrences,
|
|
1690
|
+
previouslyLogged: existing !== null,
|
|
1691
|
+
reported: entry.reported,
|
|
1692
|
+
issueUrl: entry.issueUrl
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
function listIssues(status = "all", tool) {
|
|
1696
|
+
const { scope } = journalRoot();
|
|
1697
|
+
const dir = journalDir();
|
|
1698
|
+
let entries = [];
|
|
1699
|
+
try {
|
|
1700
|
+
entries = fs6.readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => readEntry(path6.join(dir, f))).filter((e) => e !== null);
|
|
1701
|
+
} catch {
|
|
1702
|
+
entries = [];
|
|
1703
|
+
}
|
|
1704
|
+
const wanted = tool?.trim().toLowerCase();
|
|
1705
|
+
const filtered = entries.filter((e) => {
|
|
1706
|
+
const byStatus = status === "all" ? true : status === "reported" ? e.reported : !e.reported;
|
|
1707
|
+
if (!byStatus) return false;
|
|
1708
|
+
if (!wanted) return true;
|
|
1709
|
+
return e.tools.some((t) => t.toLowerCase() === wanted) || e.title.toLowerCase().includes(wanted);
|
|
1710
|
+
});
|
|
1711
|
+
filtered.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen) || b.occurrences - a.occurrences);
|
|
1712
|
+
return {
|
|
1713
|
+
dir,
|
|
1714
|
+
scope,
|
|
1715
|
+
repo: REPO,
|
|
1716
|
+
newIssueUrl: NEW_ISSUE_URL,
|
|
1717
|
+
serverVersion: packageVersion(),
|
|
1718
|
+
platform: process.platform,
|
|
1719
|
+
count: filtered.length,
|
|
1720
|
+
issues: filtered
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
function markReported(id, url) {
|
|
1724
|
+
const file = entryPath(slugify(id));
|
|
1725
|
+
const entry = fs6.existsSync(file) ? readEntry(file) : null;
|
|
1726
|
+
if (!entry) {
|
|
1727
|
+
const known = listIssues("all").issues.map((e) => e.id);
|
|
1728
|
+
throw new Error(
|
|
1729
|
+
`No journal entry with id "${id}".` + (known.length > 0 ? ` Known ids: ${known.join(", ")}` : "")
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
entry.reported = true;
|
|
1733
|
+
if (url) entry.issueUrl = oneLine(url);
|
|
1734
|
+
fs6.writeFileSync(file, render(entry), "utf8");
|
|
1735
|
+
return entry;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1406
1738
|
// src/util/pngImage.ts
|
|
1407
1739
|
function imageContent(meta, base64) {
|
|
1408
1740
|
return {
|
|
@@ -1417,13 +1749,22 @@ function imageContent(meta, base64) {
|
|
|
1417
1749
|
var { OpSchemas: OpSchemas2 } = schemas_exports;
|
|
1418
1750
|
var VISION_OPS = /* @__PURE__ */ new Set(["screenshot_frame", "screenshot_layer"]);
|
|
1419
1751
|
var ASYNC_OPS = /* @__PURE__ */ new Set(["run_batch"]);
|
|
1420
|
-
var SERVER_OPS = /* @__PURE__ */ new Set([
|
|
1752
|
+
var SERVER_OPS = /* @__PURE__ */ new Set([
|
|
1753
|
+
"await_job",
|
|
1754
|
+
"get_job",
|
|
1755
|
+
"cancel_job",
|
|
1756
|
+
"check_setup",
|
|
1757
|
+
"setup_panel",
|
|
1758
|
+
"log_issue",
|
|
1759
|
+
"list_known_issues",
|
|
1760
|
+
"mark_issue_reported"
|
|
1761
|
+
]);
|
|
1421
1762
|
var AwaitJobSchema = schemas_exports.AwaitJob;
|
|
1422
1763
|
var GetJobSchema = schemas_exports.GetJob;
|
|
1423
1764
|
var CancelJobSchema = schemas_exports.CancelJob;
|
|
1424
1765
|
function createServer() {
|
|
1425
1766
|
const server = new Server(
|
|
1426
|
-
{ name: "after-effects-mcp", version: "0.1.
|
|
1767
|
+
{ name: "after-effects-mcp", version: "0.1.2" },
|
|
1427
1768
|
{ capabilities: { tools: {}, logging: {} } }
|
|
1428
1769
|
);
|
|
1429
1770
|
const bridge = new HttpClient();
|
|
@@ -1482,6 +1823,19 @@ function createServer() {
|
|
|
1482
1823
|
const installed = await installPanel({ enableDebugMode: a.enableDebugMode, force: a.force });
|
|
1483
1824
|
return textResult({ ...installed, setup: await checkSetup() });
|
|
1484
1825
|
}
|
|
1826
|
+
if (name === "log_issue") {
|
|
1827
|
+
const a = schemas_exports.LogIssue.parse(rawArgs);
|
|
1828
|
+
return textResult(logIssue(a));
|
|
1829
|
+
}
|
|
1830
|
+
if (name === "list_known_issues") {
|
|
1831
|
+
const a = schemas_exports.ListKnownIssues.parse(rawArgs);
|
|
1832
|
+
return textResult(listIssues(a.status ?? "all", a.tool));
|
|
1833
|
+
}
|
|
1834
|
+
if (name === "mark_issue_reported") {
|
|
1835
|
+
const a = schemas_exports.MarkIssueReported.parse(rawArgs);
|
|
1836
|
+
const entry = markReported(a.id, a.url);
|
|
1837
|
+
return textResult({ ok: true, id: entry.id, reported: true, issueUrl: entry.issueUrl });
|
|
1838
|
+
}
|
|
1485
1839
|
} catch (e) {
|
|
1486
1840
|
return errorResult(e.message);
|
|
1487
1841
|
}
|
|
@@ -1578,11 +1932,12 @@ client (Claude Code, Claude Desktop, \u2026) starts it. You do not normally run
|
|
|
1578
1932
|
by hand.
|
|
1579
1933
|
|
|
1580
1934
|
Commands:
|
|
1581
|
-
init <directory> [--
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
--
|
|
1935
|
+
init <directory> [--no-mcp] Create a project folder containing the client
|
|
1936
|
+
configuration and a house-style skill you can
|
|
1937
|
+
edit. Pass --no-mcp if your client already
|
|
1938
|
+
provides these tools.
|
|
1939
|
+
--help Show this message.
|
|
1940
|
+
--version Print the version.
|
|
1586
1941
|
`;
|
|
1587
1942
|
async function main() {
|
|
1588
1943
|
const argv = process.argv.slice(2);
|
|
@@ -1609,7 +1964,7 @@ ${USAGE}`);
|
|
|
1609
1964
|
await server.connect(transport);
|
|
1610
1965
|
logger.info("MCP server running on stdio");
|
|
1611
1966
|
}
|
|
1612
|
-
var VERSION = "0.1.
|
|
1967
|
+
var VERSION = "0.1.2";
|
|
1613
1968
|
main().catch((e) => {
|
|
1614
1969
|
logger.error("fatal", e.message);
|
|
1615
1970
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@engine-room/after-effects-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Control Adobe After Effects with AI — describe the animation you want and it gets built: layers, keyframes, effects, expressions and text, all editable afterwards.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Engine Room",
|
|
8
8
|
"homepage": "https://github.com/Engine-Room-Games/after-effects-mcp#readme",
|
|
@@ -15,14 +15,16 @@
|
|
|
15
15
|
"url": "https://github.com/Engine-Room-Games/after-effects-mcp/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
|
-
"mcp",
|
|
19
|
-
"model-context-protocol",
|
|
20
18
|
"after-effects",
|
|
21
19
|
"adobe",
|
|
20
|
+
"ai",
|
|
22
21
|
"animation",
|
|
23
22
|
"motion-graphics",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
23
|
+
"video",
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"claude",
|
|
27
|
+
"extendscript"
|
|
26
28
|
],
|
|
27
29
|
"bin": {
|
|
28
30
|
"after-effects-mcp": "bin/server.js"
|
|
@@ -37,7 +39,8 @@
|
|
|
37
39
|
"LICENSE"
|
|
38
40
|
],
|
|
39
41
|
"os": [
|
|
40
|
-
"darwin"
|
|
42
|
+
"darwin",
|
|
43
|
+
"win32"
|
|
41
44
|
],
|
|
42
45
|
"engines": {
|
|
43
46
|
"node": ">=20"
|
package/panel/CSXS/manifest.xml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.1.
|
|
2
|
+
<ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.1.2"
|
|
3
3
|
ExtensionBundleName="AE MCP Bridge" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
4
4
|
<ExtensionList>
|
|
5
|
-
<Extension Id="games.engine-room.ae-mcp.panel" Version="0.1.
|
|
5
|
+
<Extension Id="games.engine-room.ae-mcp.panel" Version="0.1.2" />
|
|
6
6
|
</ExtensionList>
|
|
7
7
|
<ExecutionEnvironment>
|
|
8
8
|
<HostList>
|
package/panel/client/main.js
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
var fs = require("fs");
|
|
11
11
|
var os = require("os");
|
|
12
12
|
var http = require("http");
|
|
13
|
-
var execFileSync = require("child_process").execFileSync;
|
|
14
13
|
var WebSocket;
|
|
15
14
|
try { WebSocket = require("ws"); }
|
|
16
15
|
catch (e) {
|
|
@@ -145,45 +144,30 @@
|
|
|
145
144
|
})();
|
|
146
145
|
});
|
|
147
146
|
}
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
var outH = Math.max(1, Math.round(height / factor));
|
|
155
|
-
try {
|
|
156
|
-
// sips takes height then width.
|
|
157
|
-
execFileSync("sips", ["-z", String(outH), String(outW), file], { stdio: "ignore" });
|
|
158
|
-
return { width: outW, height: outH, downsample: factor };
|
|
159
|
-
} catch (e) {
|
|
160
|
-
log("warn", "downsample " + factor + "x failed, returning full resolution: " + e.message);
|
|
161
|
-
return {
|
|
162
|
-
width: width,
|
|
163
|
-
height: height,
|
|
164
|
-
downsample: 1,
|
|
165
|
-
warning: "downsample=" + factor + " was requested but `sips` failed (" + e.message +
|
|
166
|
-
"); returning the full-resolution frame instead.",
|
|
167
|
-
};
|
|
168
|
-
}
|
|
147
|
+
// Read the true pixel dimensions out of the PNG's IHDR chunk rather than
|
|
148
|
+
// computing them, so what we report is always what the client received.
|
|
149
|
+
function pngDimensions(buf) {
|
|
150
|
+
if (buf.length < 24) return null;
|
|
151
|
+
if (buf.readUInt32BE(12) !== 0x49484452) return null; // "IHDR"
|
|
152
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
169
153
|
}
|
|
170
154
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
155
|
+
// Downsampling happens in ExtendScript via the comp's resolutionFactor, so
|
|
156
|
+
// the file on disk is already the right size by the time we get here.
|
|
157
|
+
function readPngAsBase64(file) {
|
|
158
|
+
// A cold render of a heavy 4K comp can take well over 15s — measured on a
|
|
159
|
+
// real project. Five seconds silently failed screenshots that were simply
|
|
160
|
+
// still rendering.
|
|
161
|
+
return waitForPngFile(file, 120000).then(function () {
|
|
177
162
|
var buf = fs.readFileSync(file);
|
|
178
163
|
var b64 = buf.toString("base64");
|
|
179
164
|
try { fs.unlinkSync(file); } catch (e) {}
|
|
165
|
+
var dims = pngDimensions(buf);
|
|
180
166
|
return {
|
|
181
167
|
base64: b64,
|
|
182
168
|
bytes: buf.length,
|
|
183
|
-
width: dims.width,
|
|
184
|
-
height: dims.height,
|
|
185
|
-
downsample: dims.downsample,
|
|
186
|
-
warning: dims.warning,
|
|
169
|
+
width: dims ? dims.width : null,
|
|
170
|
+
height: dims ? dims.height : null,
|
|
187
171
|
};
|
|
188
172
|
});
|
|
189
173
|
}
|
|
@@ -214,15 +198,16 @@
|
|
|
214
198
|
function handleOp(op, args, progressToken) {
|
|
215
199
|
// Vision ops: run JSX, then read PNG and base64-encode on Node side.
|
|
216
200
|
if (op === "screenshot_frame" || op === "screenshot_layer") {
|
|
217
|
-
var factor = (args && args.downsample) ? Math.round(args.downsample) : 1;
|
|
218
201
|
return runOp(op, args).then(function (info) {
|
|
219
|
-
return readPngAsBase64(info.path
|
|
220
|
-
|
|
202
|
+
return readPngAsBase64(info.path).then(function (img) {
|
|
203
|
+
return {
|
|
204
|
+
// Dimensions come from the PNG itself, not from arithmetic on the
|
|
205
|
+
// comp size, so they cannot disagree with the image sent.
|
|
221
206
|
width: img.width,
|
|
222
207
|
height: img.height,
|
|
223
208
|
fullWidth: info.width,
|
|
224
209
|
fullHeight: info.height,
|
|
225
|
-
downsample:
|
|
210
|
+
downsample: info.downsample,
|
|
226
211
|
time: info.time,
|
|
227
212
|
compId: info.compId,
|
|
228
213
|
layerId: info.layerId,
|
|
@@ -230,8 +215,6 @@
|
|
|
230
215
|
base64: img.base64,
|
|
231
216
|
bytes: img.bytes,
|
|
232
217
|
};
|
|
233
|
-
if (img.warning) out.warning = img.warning;
|
|
234
|
-
return out;
|
|
235
218
|
});
|
|
236
219
|
});
|
|
237
220
|
}
|
package/panel/jsx/bundle.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-generated bundle. Do not edit directly — edit files in packages/jsx/.
|
|
2
|
-
// Generated 2026-08-
|
|
2
|
+
// Generated 2026-08-11T09:11:46.741Z
|
|
3
3
|
|
|
4
4
|
// ===== core.jsx =====
|
|
5
5
|
|
|
@@ -1146,14 +1146,47 @@ function __tmpPngPath() {
|
|
|
1146
1146
|
return folder.fsName + "/" + name;
|
|
1147
1147
|
}
|
|
1148
1148
|
|
|
1149
|
+
function __clampDownsample(v) {
|
|
1150
|
+
if (v === undefined || v === null) return 1;
|
|
1151
|
+
var n = Math.round(v);
|
|
1152
|
+
if (!(n > 1)) return 1;
|
|
1153
|
+
return n > 8 ? 8 : n;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// saveFrameToPng honours the comp's resolutionFactor, so AE can render the
|
|
1157
|
+
// reduced frame directly instead of writing full size and resampling
|
|
1158
|
+
// afterwards. That is faster (a quarter of the pixels at factor 2) and needs no
|
|
1159
|
+
// external image tool, which is what makes it work off macOS.
|
|
1160
|
+
function __saveFrameAt(comp, time, file, factor) {
|
|
1161
|
+
if (factor <= 1) {
|
|
1162
|
+
comp.saveFrameToPng(time, file);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
var previous = comp.resolutionFactor;
|
|
1166
|
+
try {
|
|
1167
|
+
comp.resolutionFactor = [factor, factor];
|
|
1168
|
+
comp.saveFrameToPng(time, file);
|
|
1169
|
+
} finally {
|
|
1170
|
+
// Restore unconditionally — a failed render must never leave the user
|
|
1171
|
+
// looking at a half-resolution comp.
|
|
1172
|
+
comp.resolutionFactor = previous;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1149
1176
|
OPS.screenshot_frame = noUndo(function (args) {
|
|
1150
1177
|
var c = getCompById(args.compId);
|
|
1151
1178
|
var t = (args.time !== undefined && args.time !== null) ? args.time : c.time;
|
|
1179
|
+
var ds = __clampDownsample(args.downsample);
|
|
1152
1180
|
var path = __tmpPngPath();
|
|
1153
1181
|
var f = new File(path);
|
|
1154
1182
|
// saveFrameToPng is async-ish; the panel polls the file's existence/size.
|
|
1155
|
-
c
|
|
1156
|
-
return {
|
|
1183
|
+
__saveFrameAt(c, t, f, ds);
|
|
1184
|
+
return {
|
|
1185
|
+
path: path,
|
|
1186
|
+
width: c.width, height: c.height,
|
|
1187
|
+
downsample: ds,
|
|
1188
|
+
time: t, compId: c.id
|
|
1189
|
+
};
|
|
1157
1190
|
});
|
|
1158
1191
|
|
|
1159
1192
|
OPS.screenshot_layer = noUndo(function (args) {
|
|
@@ -1168,10 +1201,11 @@ OPS.screenshot_layer = noUndo(function (args) {
|
|
|
1168
1201
|
ll.solo = false;
|
|
1169
1202
|
}
|
|
1170
1203
|
l.solo = true;
|
|
1204
|
+
var ds = __clampDownsample(args.downsample);
|
|
1171
1205
|
var path = __tmpPngPath();
|
|
1172
1206
|
var f = new File(path);
|
|
1173
1207
|
try {
|
|
1174
|
-
c
|
|
1208
|
+
__saveFrameAt(c, t, f, ds);
|
|
1175
1209
|
} finally {
|
|
1176
1210
|
// restore
|
|
1177
1211
|
l.solo = false;
|
|
@@ -1179,7 +1213,12 @@ OPS.screenshot_layer = noUndo(function (args) {
|
|
|
1179
1213
|
try { c.layer(prevSolo[j].idx).solo = prevSolo[j].solo; } catch (e) {}
|
|
1180
1214
|
}
|
|
1181
1215
|
}
|
|
1182
|
-
return {
|
|
1216
|
+
return {
|
|
1217
|
+
path: path,
|
|
1218
|
+
width: c.width, height: c.height,
|
|
1219
|
+
downsample: ds,
|
|
1220
|
+
time: t, compId: c.id, layerId: l.id
|
|
1221
|
+
};
|
|
1183
1222
|
});
|
|
1184
1223
|
|
|
1185
1224
|
|