goatedit-mcp 1.1.0 → 1.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 +4 -0
- package/bundle/goatedit-mcp.js +545 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,10 @@ up to 500 files); `/media/<path>` serves from it with byte ranges, so video
|
|
|
112
112
|
seeks, and paths that resolve outside it are a 404. Generated voiceovers are
|
|
113
113
|
written there too.
|
|
114
114
|
|
|
115
|
+
`import_local_file` looks in that folder first: a file found there (by name,
|
|
116
|
+
or by the path `list_local_assets` gives) is imported from this server, with no
|
|
117
|
+
GoatEdit bridge needed. Only a miss falls through to the bridge.
|
|
118
|
+
|
|
115
119
|
---
|
|
116
120
|
|
|
117
121
|
## Tools
|
package/bundle/goatedit-mcp.js
CHANGED
|
@@ -3232,8 +3232,8 @@ var require_utils = __commonJS({
|
|
|
3232
3232
|
}
|
|
3233
3233
|
return ind;
|
|
3234
3234
|
}
|
|
3235
|
-
function removeDotSegments(
|
|
3236
|
-
let input =
|
|
3235
|
+
function removeDotSegments(path3) {
|
|
3236
|
+
let input = path3;
|
|
3237
3237
|
const output = [];
|
|
3238
3238
|
let nextSlash = -1;
|
|
3239
3239
|
let len = 0;
|
|
@@ -3485,8 +3485,8 @@ var require_schemes = __commonJS({
|
|
|
3485
3485
|
wsComponent.secure = void 0;
|
|
3486
3486
|
}
|
|
3487
3487
|
if (wsComponent.resourceName) {
|
|
3488
|
-
const [
|
|
3489
|
-
wsComponent.path =
|
|
3488
|
+
const [path3, query] = wsComponent.resourceName.split("?");
|
|
3489
|
+
wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
|
|
3490
3490
|
wsComponent.query = query;
|
|
3491
3491
|
wsComponent.resourceName = void 0;
|
|
3492
3492
|
}
|
|
@@ -6879,12 +6879,12 @@ var require_dist = __commonJS({
|
|
|
6879
6879
|
throw new Error(`Unknown format "${name}"`);
|
|
6880
6880
|
return f;
|
|
6881
6881
|
};
|
|
6882
|
-
function addFormats(ajv, list,
|
|
6882
|
+
function addFormats(ajv, list, fs3, exportName) {
|
|
6883
6883
|
var _a3;
|
|
6884
6884
|
var _b;
|
|
6885
6885
|
(_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6886
6886
|
for (const f of list)
|
|
6887
|
-
ajv.addFormat(f,
|
|
6887
|
+
ajv.addFormat(f, fs3[f]);
|
|
6888
6888
|
}
|
|
6889
6889
|
module.exports = exports = formatsPlugin;
|
|
6890
6890
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -10811,10 +10811,10 @@ function mergeDefs(...defs) {
|
|
|
10811
10811
|
function cloneDef(schema) {
|
|
10812
10812
|
return mergeDefs(schema._zod.def);
|
|
10813
10813
|
}
|
|
10814
|
-
function getElementAtPath(obj,
|
|
10815
|
-
if (!
|
|
10814
|
+
function getElementAtPath(obj, path3) {
|
|
10815
|
+
if (!path3)
|
|
10816
10816
|
return obj;
|
|
10817
|
-
return
|
|
10817
|
+
return path3.reduce((acc, key) => acc?.[key], obj);
|
|
10818
10818
|
}
|
|
10819
10819
|
function promiseAllObject(promisesObj) {
|
|
10820
10820
|
const keys = Object.keys(promisesObj);
|
|
@@ -11223,11 +11223,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
11223
11223
|
}
|
|
11224
11224
|
return false;
|
|
11225
11225
|
}
|
|
11226
|
-
function prefixIssues(
|
|
11226
|
+
function prefixIssues(path3, issues) {
|
|
11227
11227
|
return issues.map((iss) => {
|
|
11228
11228
|
var _a3;
|
|
11229
11229
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
11230
|
-
iss.path.unshift(
|
|
11230
|
+
iss.path.unshift(path3);
|
|
11231
11231
|
return iss;
|
|
11232
11232
|
});
|
|
11233
11233
|
}
|
|
@@ -11374,16 +11374,16 @@ function flattenError(error2, mapper = (issue2) => issue2.message) {
|
|
|
11374
11374
|
}
|
|
11375
11375
|
function formatError(error2, mapper = (issue2) => issue2.message) {
|
|
11376
11376
|
const fieldErrors = { _errors: [] };
|
|
11377
|
-
const processError = (error3,
|
|
11377
|
+
const processError = (error3, path3 = []) => {
|
|
11378
11378
|
for (const issue2 of error3.issues) {
|
|
11379
11379
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
11380
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
11380
|
+
issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
|
|
11381
11381
|
} else if (issue2.code === "invalid_key") {
|
|
11382
|
-
processError({ issues: issue2.issues }, [...
|
|
11382
|
+
processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
|
|
11383
11383
|
} else if (issue2.code === "invalid_element") {
|
|
11384
|
-
processError({ issues: issue2.issues }, [...
|
|
11384
|
+
processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
|
|
11385
11385
|
} else {
|
|
11386
|
-
const fullpath = [...
|
|
11386
|
+
const fullpath = [...path3, ...issue2.path];
|
|
11387
11387
|
if (fullpath.length === 0) {
|
|
11388
11388
|
fieldErrors._errors.push(mapper(issue2));
|
|
11389
11389
|
} else {
|
|
@@ -19144,8 +19144,8 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
|
19144
19144
|
// src/index.ts
|
|
19145
19145
|
import crypto from "crypto";
|
|
19146
19146
|
import http from "http";
|
|
19147
|
-
import
|
|
19148
|
-
import
|
|
19147
|
+
import fs2 from "fs";
|
|
19148
|
+
import path2 from "path";
|
|
19149
19149
|
import { execFileSync } from "child_process";
|
|
19150
19150
|
import os from "os";
|
|
19151
19151
|
import { fileURLToPath } from "url";
|
|
@@ -19155,6 +19155,14 @@ var num = (description) => ({ type: "number", description });
|
|
|
19155
19155
|
var str = (description) => ({ type: "string", description });
|
|
19156
19156
|
var bool = (description) => ({ type: "boolean", description });
|
|
19157
19157
|
var CLIP_ID = str("ID of the clip, as returned by get_timeline.");
|
|
19158
|
+
var SKIP_ADJUSTMENTS = bool(
|
|
19159
|
+
"Render without any adjustment layer (a global camera move, a grade). For frames you will bake into a clip that sits under that layer, which would otherwise apply it twice. The timeline is not changed."
|
|
19160
|
+
);
|
|
19161
|
+
var HIDE_CLIP_IDS = {
|
|
19162
|
+
type: "array",
|
|
19163
|
+
items: { type: "string" },
|
|
19164
|
+
description: "Render as if these clips were not there. The timeline is not changed."
|
|
19165
|
+
};
|
|
19158
19166
|
var TEXT_STYLE_PROPS = {
|
|
19159
19167
|
fontSize: num("Font size in pixels, authored against a 1080p-tall frame."),
|
|
19160
19168
|
fontFamily: str("Font family name."),
|
|
@@ -19171,10 +19179,24 @@ var TEXT_STYLE_PROPS = {
|
|
|
19171
19179
|
textShadowColor: str("Drop shadow colour."),
|
|
19172
19180
|
lineHeight: num("Line advance as a multiple of the line height. Defaults to 1.2."),
|
|
19173
19181
|
letterSpacing: num("Extra tracking in pixels @1080p."),
|
|
19182
|
+
textUnderline: bool("Underline the text."),
|
|
19183
|
+
textShadowBlur: num("Drop shadow softness in pixels @1080p. 0 is a hard shadow."),
|
|
19184
|
+
textShadowOffsetX: num("Drop shadow offset to the right, pixels @1080p."),
|
|
19185
|
+
textShadowOffsetY: num("Drop shadow offset downward, pixels @1080p."),
|
|
19186
|
+
textGradientStart: str("Fill the words with a gradient from this colour (top) \u2026"),
|
|
19187
|
+
textGradientEnd: str("\u2026 to this one (bottom). Set both."),
|
|
19188
|
+
textAccentColor: str("Colour for accent words. Mark an accent word in the text as *word*."),
|
|
19189
|
+
textAccentFontFamily: str(`Font for accent words, e.g. "'Great Vibes', cursive" for a script word inside heavy caps.`),
|
|
19190
|
+
textAccentScale: num("Size of accent words relative to the rest, e.g. 1.8."),
|
|
19191
|
+
textAccentGradientStart: str("Gradient start colour for accent words."),
|
|
19192
|
+
textAccentGradientEnd: str("Gradient end colour for accent words."),
|
|
19193
|
+
textSwooshUnderline: bool("Hand-drawn swoosh under the last line."),
|
|
19194
|
+
textSticker: { type: "string", enum: ["ghost", "arrow", "pen", "star", "flame"], description: "Small sticker drawn beside the accent word." },
|
|
19195
|
+
textLayoutMode: { type: "string", enum: ["side-grid", "luxury-stack", "editorial-hero", "slanted-red", "double-hero"], description: "Editorial layout engine for multi-word titles." },
|
|
19174
19196
|
posX: num("Horizontal position as a fraction of the frame, 0..1. 0.5 is centre."),
|
|
19175
19197
|
posY: num("Vertical position as a fraction of the frame, 0..1. 0.5 is centre.")
|
|
19176
19198
|
};
|
|
19177
|
-
var
|
|
19199
|
+
var BASE_TOOLS = [
|
|
19178
19200
|
{
|
|
19179
19201
|
name: "get_session_status",
|
|
19180
19202
|
description: "Check whether a GoatEdit editor tab is currently attached to this session, and which project it has open. Call this first if any other tool reports the editor is not connected.",
|
|
@@ -19237,7 +19259,9 @@ var TOOLS = [
|
|
|
19237
19259
|
description: "Write the frame to disk through the local bridge and return its path instead of the image itself. A frame is around 200KB, and inline it is paid for twice \u2014 as tokens and as a cached object \u2014 so a review pass over twenty cuts is expensive. With this set, read the returned path with your own file tools. Falls back to an inline image, with a warning, when the bridge is not running."
|
|
19238
19260
|
},
|
|
19239
19261
|
width: num("Width of the returned image in pixels, up to 1280. Defaults to 1024."),
|
|
19240
|
-
outlines: bool(`Draw every layer's box, handles and anchor point over the picture, with a timecode \u2014 the look of the editor's selection, on every layer at once. The same option the Export dialog offers as "Show layer outlines".`)
|
|
19262
|
+
outlines: bool(`Draw every layer's box, handles and anchor point over the picture, with a timecode \u2014 the look of the editor's selection, on every layer at once. The same option the Export dialog offers as "Show layer outlines".`),
|
|
19263
|
+
skipAdjustments: SKIP_ADJUSTMENTS,
|
|
19264
|
+
hideClipIds: HIDE_CLIP_IDS
|
|
19241
19265
|
}
|
|
19242
19266
|
},
|
|
19243
19267
|
method: "renderFrame"
|
|
@@ -19263,7 +19287,9 @@ var TOOLS = [
|
|
|
19263
19287
|
startTime: num("Start of the range in seconds."),
|
|
19264
19288
|
endTime: num("End of the range in seconds. Ranges over 10 minutes are refused."),
|
|
19265
19289
|
frames: num("How many frames to sample, up to 6. Defaults to 4."),
|
|
19266
|
-
width: num("Width of each frame in pixels. Defaults to 640.")
|
|
19290
|
+
width: num("Width of each frame in pixels. Defaults to 640."),
|
|
19291
|
+
skipAdjustments: SKIP_ADJUSTMENTS,
|
|
19292
|
+
hideClipIds: HIDE_CLIP_IDS
|
|
19267
19293
|
},
|
|
19268
19294
|
required: ["startTime", "endTime"]
|
|
19269
19295
|
},
|
|
@@ -19356,7 +19382,7 @@ var TOOLS = [
|
|
|
19356
19382
|
},
|
|
19357
19383
|
{
|
|
19358
19384
|
name: "get_timeline",
|
|
19359
|
-
description: 'Get just the timeline \u2014 all tracks with their clips, ids, start times and durations. Cheaper than get_project_state; use it before
|
|
19385
|
+
description: 'Get just the timeline \u2014 all tracks with their clips, ids, start times and durations. Cheaper than get_project_state; use it before your first edit so you have current clip ids. After that, each edit\u2019s reply lists the clips it added, changed and removed under timelineChanges, so a re-read is only needed when that says it was truncated. Animation data (keyframes, motion tracking, masks, per-word caption timings) is reported as counts under *Summary keys rather than in full, so a clip showing keyframeSummary is animated even though the keyframes themselves are not listed; get_clip returns them. By default the view is lean: a field at its usual value is left out (no volume means 1, no isMuted means false, no opacity means 1), and effects are listed by id and type only. Pass detail: "full" when you need effect parameters.',
|
|
19360
19386
|
inputSchema: {
|
|
19361
19387
|
type: "object",
|
|
19362
19388
|
properties: {
|
|
@@ -19368,6 +19394,40 @@ var TOOLS = [
|
|
|
19368
19394
|
},
|
|
19369
19395
|
method: "getTimeline"
|
|
19370
19396
|
},
|
|
19397
|
+
{
|
|
19398
|
+
name: "get_clip",
|
|
19399
|
+
description: "Get clips in full, animation included: every keyframe on every property (time in seconds from the clip\u2019s start, value, ease \u2014 the shape set_keyframes takes), effect parameters and their keyframes, mask shapes, crop, key and transform. Use it to copy or adjust a clip\u2019s animation; get_timeline only counts keyframes. Per-frame data (motion tracking, roto point sets per frame, caption word timings) stays a count, as does any property with more than 300 keyframes.",
|
|
19400
|
+
inputSchema: {
|
|
19401
|
+
type: "object",
|
|
19402
|
+
properties: {
|
|
19403
|
+
clipIds: { type: "array", items: { type: "string" }, description: "Clip ids from get_timeline, up to 50." }
|
|
19404
|
+
},
|
|
19405
|
+
required: ["clipIds"]
|
|
19406
|
+
},
|
|
19407
|
+
method: "getClips"
|
|
19408
|
+
},
|
|
19409
|
+
{
|
|
19410
|
+
name: "create_checkpoint",
|
|
19411
|
+
description: "Save a copy of the whole project (every sequence and the media bin) to return to with restore_checkpoint. Take one before a long scripted change: undo goes back one step at a time and a big rebuild can outrun it. Kept in the editor tab until it reloads, the newest 20 per project.",
|
|
19412
|
+
inputSchema: { type: "object", properties: { name: str("Optional label.") } },
|
|
19413
|
+
method: "createCheckpoint"
|
|
19414
|
+
},
|
|
19415
|
+
{
|
|
19416
|
+
name: "list_checkpoints",
|
|
19417
|
+
description: "List the checkpoints saved in this tab for the open project.",
|
|
19418
|
+
inputSchema: { type: "object", properties: {} },
|
|
19419
|
+
method: "listCheckpoints"
|
|
19420
|
+
},
|
|
19421
|
+
{
|
|
19422
|
+
name: "restore_checkpoint",
|
|
19423
|
+
description: "Put the whole project back as it was at a checkpoint, as one undoable step. Everything done since is undone at once, in every sequence.",
|
|
19424
|
+
inputSchema: {
|
|
19425
|
+
type: "object",
|
|
19426
|
+
properties: { checkpointId: str("Id from create_checkpoint or list_checkpoints.") },
|
|
19427
|
+
required: ["checkpointId"]
|
|
19428
|
+
},
|
|
19429
|
+
method: "restoreCheckpoint"
|
|
19430
|
+
},
|
|
19371
19431
|
{
|
|
19372
19432
|
name: "get_media_bin",
|
|
19373
19433
|
description: "List every media file imported into the project, with id, name, type and duration.",
|
|
@@ -19418,7 +19478,7 @@ var TOOLS = [
|
|
|
19418
19478
|
inputSchema: {
|
|
19419
19479
|
type: "object",
|
|
19420
19480
|
properties: {
|
|
19421
|
-
trackId: str('Target track id from get_timeline,
|
|
19481
|
+
trackId: str('Target track id from get_timeline, "__new_video__" to let the editor pick a free video track, or "__new_track__" for a new video track on top.'),
|
|
19422
19482
|
mediaFileId: str("Media bin file id from get_media_bin."),
|
|
19423
19483
|
startTime: num("Where the clip starts on the timeline, in seconds."),
|
|
19424
19484
|
duration: num("Length of the clip on the timeline, in seconds."),
|
|
@@ -19525,6 +19585,152 @@ var TOOLS = [
|
|
|
19525
19585
|
},
|
|
19526
19586
|
method: "addShapeClip"
|
|
19527
19587
|
},
|
|
19588
|
+
{
|
|
19589
|
+
name: "update_shape_clip",
|
|
19590
|
+
description: "Change a shape after it was made \u2014 fill, size, corners, outline, and a dashed or rounded outline \u2014 without deleting it and losing its id, keyframes and effects.",
|
|
19591
|
+
inputSchema: {
|
|
19592
|
+
type: "object",
|
|
19593
|
+
properties: {
|
|
19594
|
+
clipId: CLIP_ID,
|
|
19595
|
+
color: str("Fill colour."),
|
|
19596
|
+
width: num("Width in design units against a 1080p-tall frame."),
|
|
19597
|
+
height: num("Height in design units against a 1080p-tall frame."),
|
|
19598
|
+
cornerRadius: num("Corner radius in design units."),
|
|
19599
|
+
strokeColor: str("Outline colour."),
|
|
19600
|
+
strokeWidth: num("Outline width in design units."),
|
|
19601
|
+
strokeCap: { type: "string", enum: ["butt", "round", "square"], description: "Ends of an open or dashed outline." },
|
|
19602
|
+
strokeJoin: { type: "string", enum: ["miter", "round", "bevel"], description: "Outline corners." },
|
|
19603
|
+
dash: num("Dash length for a dashed outline, design units. 0 for solid."),
|
|
19604
|
+
gap: num("Gap between dashes, design units.")
|
|
19605
|
+
},
|
|
19606
|
+
required: ["clipId"]
|
|
19607
|
+
},
|
|
19608
|
+
method: "updateShapeClip"
|
|
19609
|
+
},
|
|
19610
|
+
{
|
|
19611
|
+
name: "add_null",
|
|
19612
|
+
description: "Add a Null Object: an invisible handle that other clips are parented to (set_parent). Move, scale or rotate the null and everything under it follows, orbiting its position \u2014 the After Effects way to group a polaroid with its caption, or make a whole layout drift as one.",
|
|
19613
|
+
inputSchema: {
|
|
19614
|
+
type: "object",
|
|
19615
|
+
properties: {
|
|
19616
|
+
startTime: num("Seconds. Defaults to 0."),
|
|
19617
|
+
duration: num("Seconds."),
|
|
19618
|
+
x: num("Position as a fraction of the frame width. Defaults to 0.5."),
|
|
19619
|
+
y: num("Position as a fraction of the frame height. Defaults to 0.5."),
|
|
19620
|
+
name: str("Display name."),
|
|
19621
|
+
trackId: str('Video track, or "__new_video__" (default) for a free one.')
|
|
19622
|
+
},
|
|
19623
|
+
required: ["duration"]
|
|
19624
|
+
},
|
|
19625
|
+
method: "addNull"
|
|
19626
|
+
},
|
|
19627
|
+
{
|
|
19628
|
+
name: "set_parent",
|
|
19629
|
+
description: "Parent a clip to another clip or null, After Effects style. The clip stays exactly where it is when parented; from then on it follows the parent's move, scale and rotation, orbiting the parent's position and spreading as it scales. parentId null unparents. position / scale / rotation pick-whip a single channel to a different clip (null cuts that channel loose): a channel parent passes on only its own change. Parent first, then animate the parent.",
|
|
19630
|
+
inputSchema: {
|
|
19631
|
+
type: "object",
|
|
19632
|
+
properties: {
|
|
19633
|
+
clipId: CLIP_ID,
|
|
19634
|
+
parentId: { type: ["string", "null"], description: "Parent clip id, or null to unparent." },
|
|
19635
|
+
position: { type: ["string", "null"], description: "Take position only from this clip." },
|
|
19636
|
+
scale: { type: ["string", "null"], description: "Take scale only from this clip." },
|
|
19637
|
+
rotation: { type: ["string", "null"], description: "Take rotation only from this clip." }
|
|
19638
|
+
},
|
|
19639
|
+
required: ["clipId"]
|
|
19640
|
+
},
|
|
19641
|
+
method: "setParent"
|
|
19642
|
+
},
|
|
19643
|
+
{
|
|
19644
|
+
name: "group_clips",
|
|
19645
|
+
description: "Group clips under a new null spanning all of them, so they move, scale and rotate as one; each keeps its place. Returns the group's clipId \u2014 animate that. With layout set, the null becomes an Auto Layout frame and the clips flow inside it (tune it with set_auto_layout).",
|
|
19646
|
+
inputSchema: {
|
|
19647
|
+
type: "object",
|
|
19648
|
+
properties: {
|
|
19649
|
+
clipIds: { type: "array", items: { type: "string" }, description: "Clips to group." },
|
|
19650
|
+
name: str("Group name."),
|
|
19651
|
+
layout: { type: "string", enum: ["none", "horizontal", "vertical", "wrap", "orbit"], description: "Defaults to none: a plain group." },
|
|
19652
|
+
gap: num("Gap between laid-out clips, design units. Defaults to 16."),
|
|
19653
|
+
x: num("Group pivot, fraction of the frame width. Defaults to the members' average."),
|
|
19654
|
+
y: num("Group pivot, fraction of the frame height."),
|
|
19655
|
+
trackId: str('Video track for the null, or "__new_video__" (default).')
|
|
19656
|
+
},
|
|
19657
|
+
required: ["clipIds"]
|
|
19658
|
+
},
|
|
19659
|
+
method: "groupClips"
|
|
19660
|
+
},
|
|
19661
|
+
{
|
|
19662
|
+
name: "set_proximity",
|
|
19663
|
+
description: "Drive one property of a clip by how close it is to another clip on screen \u2014 a card that brightens as a cursor nears it, a label that grows when a photo slides over it. Distance is measured between where both are drawn (parents and layout included), as a fraction of the frame width. At `near` or closer the property is nearValue, at `far` or beyond it is farValue, linear between.",
|
|
19664
|
+
inputSchema: {
|
|
19665
|
+
type: "object",
|
|
19666
|
+
properties: {
|
|
19667
|
+
clipId: CLIP_ID,
|
|
19668
|
+
targetClipId: str("The clip whose distance drives it."),
|
|
19669
|
+
property: { type: "string", enum: ["opacity", "scaleX", "scaleY", "posX", "posY", "rotation"], description: "Defaults to opacity." },
|
|
19670
|
+
near: num("Distance for nearValue. Defaults to 0.05."),
|
|
19671
|
+
far: num("Distance for farValue. Defaults to 0.3."),
|
|
19672
|
+
nearValue: num("Defaults to 1."),
|
|
19673
|
+
farValue: num("Defaults to 0."),
|
|
19674
|
+
remove: bool("Remove the driver.")
|
|
19675
|
+
},
|
|
19676
|
+
required: ["clipId"]
|
|
19677
|
+
},
|
|
19678
|
+
method: "setProximity"
|
|
19679
|
+
},
|
|
19680
|
+
{
|
|
19681
|
+
name: "set_clip_speed",
|
|
19682
|
+
description: "Retime a video or audio clip like the Speed panel: speed (0.1\u201310) keeps the source range and changes the clip length (stopping at the next clip); ramp applies a speed curve \u2014 a preset name (montage, hero, bullet, jumpCut, flashIn, flashOut) or [{x 0..1, y rate}] points; null flattens it. reversed plays it backwards; holdFrame freezes from that many seconds into the clip (null releases). Linked audio is retimed with it.",
|
|
19683
|
+
inputSchema: {
|
|
19684
|
+
type: "object",
|
|
19685
|
+
properties: {
|
|
19686
|
+
clipId: CLIP_ID,
|
|
19687
|
+
speed: num("Playback rate, 1 = normal."),
|
|
19688
|
+
ramp: { type: ["string", "array", "null"], description: "Preset id, or points [{x, y}], or null." },
|
|
19689
|
+
reversed: bool("Play backwards."),
|
|
19690
|
+
holdFrame: { type: ["number", "null"], description: "Freeze from this clip-relative time; null removes the hold." }
|
|
19691
|
+
},
|
|
19692
|
+
required: ["clipId"]
|
|
19693
|
+
},
|
|
19694
|
+
method: "setClipSpeed"
|
|
19695
|
+
},
|
|
19696
|
+
{
|
|
19697
|
+
name: "set_auto_layout",
|
|
19698
|
+
description: "Figma-style Auto Layout. On a frame (group_clips with a layout, or any clip with children): mode, gap, padding, alignment, resizing, clip content, reverse, stack order, orbit settings. Children are spaced by the room they take at their own scale, and the frame's own scale and rotation act on the whole arrangement. On a child: absolute (leave the flow and sit at its own position inside the frame), grow (share of free space when resizing is fill), min/max size.",
|
|
19699
|
+
inputSchema: {
|
|
19700
|
+
type: "object",
|
|
19701
|
+
properties: {
|
|
19702
|
+
clipId: CLIP_ID,
|
|
19703
|
+
enabled: bool("Switch the layout on or off without losing it."),
|
|
19704
|
+
mode: { type: "string", enum: ["none", "horizontal", "vertical", "wrap", "orbit"] },
|
|
19705
|
+
gap: num("Gap between children, design units."),
|
|
19706
|
+
gapV: num("Gap between rows in wrap mode."),
|
|
19707
|
+
padding: num("All four paddings at once."),
|
|
19708
|
+
paddingLeft: num("Design units."),
|
|
19709
|
+
paddingRight: num("Design units."),
|
|
19710
|
+
paddingTop: num("Design units."),
|
|
19711
|
+
paddingBottom: num("Design units."),
|
|
19712
|
+
primaryAlign: { type: "string", enum: ["start", "center", "end", "space-between", "space-around", "space-evenly"] },
|
|
19713
|
+
counterAlign: { type: "string", enum: ["start", "center", "end", "stretch"] },
|
|
19714
|
+
horizontalResizing: { type: "string", enum: ["fixed", "hug", "fill"] },
|
|
19715
|
+
verticalResizing: { type: "string", enum: ["fixed", "hug", "fill"] },
|
|
19716
|
+
clipContent: bool("Clip children to the frame."),
|
|
19717
|
+
reverse: bool("Reverse the flow order."),
|
|
19718
|
+
stackOrder: { type: "string", enum: ["firstOnTop", "lastOnTop"] },
|
|
19719
|
+
orbitRadius: num("Orbit mode radius, design units."),
|
|
19720
|
+
orbitSpeed: num("Orbit mode degrees per second."),
|
|
19721
|
+
orbitStartAngle: num("Orbit mode start angle, degrees."),
|
|
19722
|
+
orbitRotateChildren: bool("Turn children to face along the orbit."),
|
|
19723
|
+
absolute: bool("Child only: leave the flow."),
|
|
19724
|
+
grow: num("Child only: share of free space with fill resizing."),
|
|
19725
|
+
minWidth: num("Design units."),
|
|
19726
|
+
maxWidth: num("Design units."),
|
|
19727
|
+
minHeight: num("Design units."),
|
|
19728
|
+
maxHeight: num("Design units.")
|
|
19729
|
+
},
|
|
19730
|
+
required: ["clipId"]
|
|
19731
|
+
},
|
|
19732
|
+
method: "setAutoLayout"
|
|
19733
|
+
},
|
|
19528
19734
|
{
|
|
19529
19735
|
name: "set_shape_gradient",
|
|
19530
19736
|
description: "Give a shape or a pen path a gradient fill or a gradient stroke, replacing its flat colour. Works on anything with a shapeType, including paths drawn with the pen tool. The ramp is laid out against the shape's own bounding box, so it stays put on the object when the shape is moved, scaled or rotated rather than sliding across it. Pass null to clear one and go back to the flat colour. gradientKeyframes animates it \u2014 a sweep that turns, a glow that shifts colour.",
|
|
@@ -19695,12 +19901,69 @@ var TOOLS = [
|
|
|
19695
19901
|
scaleX: num("Horizontal scale, overriding scale."),
|
|
19696
19902
|
scaleY: num("Vertical scale, overriding scale."),
|
|
19697
19903
|
rotation: num("Rotation in degrees."),
|
|
19698
|
-
opacity: num("Opacity from 0 to 1.")
|
|
19904
|
+
opacity: num("Opacity from 0 to 1."),
|
|
19905
|
+
blendMode: {
|
|
19906
|
+
type: "string",
|
|
19907
|
+
enum: [
|
|
19908
|
+
"normal",
|
|
19909
|
+
"multiply",
|
|
19910
|
+
"screen",
|
|
19911
|
+
"overlay",
|
|
19912
|
+
"darken",
|
|
19913
|
+
"lighten",
|
|
19914
|
+
"color-dodge",
|
|
19915
|
+
"color-burn",
|
|
19916
|
+
"hard-light",
|
|
19917
|
+
"soft-light",
|
|
19918
|
+
"difference",
|
|
19919
|
+
"exclusion",
|
|
19920
|
+
"hue",
|
|
19921
|
+
"saturation",
|
|
19922
|
+
"color",
|
|
19923
|
+
"luminosity"
|
|
19924
|
+
],
|
|
19925
|
+
description: "How the clip mixes with the layers beneath it. screen for light leaks, flares and dust shot on black; multiply for paper textures, ink and shadows shot on white; overlay or soft-light for texture over a picture. normal sets it back."
|
|
19926
|
+
}
|
|
19699
19927
|
},
|
|
19700
19928
|
required: ["clipId"]
|
|
19701
19929
|
},
|
|
19702
19930
|
method: "setClipTransform"
|
|
19703
19931
|
},
|
|
19932
|
+
{
|
|
19933
|
+
name: "set_ultra_key",
|
|
19934
|
+
description: "Green screen: knock a colour out of a video or image clip so the tracks beneath show through \u2014 a paper unfold, a smoke burst or a presenter shot on green or blue. Unset values keep what the clip already has (or the defaults: green, tolerance 0.18, softness 0.1, spill 0.4). Raise tolerance when green is left behind, softness for a gentler edge, spill when edges carry a green tint. Check the result with render_preview_frame; pass remove to take the key off.",
|
|
19935
|
+
inputSchema: {
|
|
19936
|
+
type: "object",
|
|
19937
|
+
properties: {
|
|
19938
|
+
clipId: CLIP_ID,
|
|
19939
|
+
keyColor: str('Colour to remove, as "#rrggbb". Defaults to "#00ff00"; pick "#0000ff" for blue screen, or the sampled backdrop colour when the green is off-pure.'),
|
|
19940
|
+
tolerance: num("How close to the key colour a pixel must be to vanish, 0..1. Defaults to 0.18."),
|
|
19941
|
+
softness: num("Width of the soft edge around the key, 0..1. Defaults to 0.1."),
|
|
19942
|
+
spill: num("How strongly green fringing is desaturated on edges, 0..1. Defaults to 0.4."),
|
|
19943
|
+
enabled: bool("Switch the key off without losing its settings. Defaults to true."),
|
|
19944
|
+
remove: bool("Remove the key entirely.")
|
|
19945
|
+
},
|
|
19946
|
+
required: ["clipId"]
|
|
19947
|
+
},
|
|
19948
|
+
method: "setUltraKey"
|
|
19949
|
+
},
|
|
19950
|
+
{
|
|
19951
|
+
name: "set_track_matte",
|
|
19952
|
+
description: "After Effects track matte: another clip\u2019s transparency (alpha) or brightness (luma) becomes this clip\u2019s transparency, optionally inverted. Use it to reveal an image through a moving shape \u2014 a torn-paper or ink-bleed clip, a wipe, text \u2014 without baking. The matte clip stops drawing once it is used as a matte unless keepSourceVisible is set. Name the matte with sourceClipId (any clip, on any track; give it the same transform as the clip it cuts); without one, the matte is whatever is live on the nearest video track above. Unset values keep what the clip already has. Pass remove to take the matte off.",
|
|
19953
|
+
inputSchema: {
|
|
19954
|
+
type: "object",
|
|
19955
|
+
properties: {
|
|
19956
|
+
clipId: CLIP_ID,
|
|
19957
|
+
type: { type: "string", enum: ["alpha", "alphaInverted", "luma", "lumaInverted"], description: "Defaults to alpha." },
|
|
19958
|
+
sourceClipId: str("Clip that supplies the matte."),
|
|
19959
|
+
keepSourceVisible: bool("Keep drawing the matte clip as well. Defaults to false."),
|
|
19960
|
+
enabled: bool("Switch the matte off without losing it. Defaults to true."),
|
|
19961
|
+
remove: bool("Remove the matte.")
|
|
19962
|
+
},
|
|
19963
|
+
required: ["clipId"]
|
|
19964
|
+
},
|
|
19965
|
+
method: "setTrackMatte"
|
|
19966
|
+
},
|
|
19704
19967
|
{
|
|
19705
19968
|
name: "set_keyframes",
|
|
19706
19969
|
description: "Animate one property of a clip over time. set_clip_transform sets a value; this is what makes it move \u2014 a card sliding on, a background drifting, a photo pushing in. Times are in seconds RELATIVE TO THE CLIP's start, and each keyframe may carry its own outgoing easing as a CSS cubic-bezier ([0.22, 1, 0.36, 1] is a good decelerate). Two or more keyframes make motion; one holds a value. Writing keyframes for a property overrides whatever flat value the clip carries for it, and replaces any track already on that property \u2014 pass clear to remove one.",
|
|
@@ -19780,6 +20043,70 @@ var TOOLS = [
|
|
|
19780
20043
|
},
|
|
19781
20044
|
method: "describeEffect"
|
|
19782
20045
|
},
|
|
20046
|
+
{
|
|
20047
|
+
name: "list_presets",
|
|
20048
|
+
description: "List animation presets. The user\u2019s own saved presets always come back in full; the built-in library (a couple of hundred title moves, lower thirds, logo reveals, backgrounds and more) is summarised by category unless you pass category or query. Apply one with apply_preset.",
|
|
20049
|
+
inputSchema: {
|
|
20050
|
+
type: "object",
|
|
20051
|
+
properties: {
|
|
20052
|
+
category: {
|
|
20053
|
+
type: "string",
|
|
20054
|
+
enum: ["text", "motion", "paper", "liquid", "glitch", "logo", "lower-third", "transition", "bg"],
|
|
20055
|
+
description: "List the built-in presets of one category."
|
|
20056
|
+
},
|
|
20057
|
+
query: str("Search built-in presets by name, description or tag.")
|
|
20058
|
+
}
|
|
20059
|
+
},
|
|
20060
|
+
method: "listPresets"
|
|
20061
|
+
},
|
|
20062
|
+
{
|
|
20063
|
+
name: "save_preset",
|
|
20064
|
+
description: 'Save clips as a reusable preset: their transform keyframes (stored as fractions of the clip, so the move stretches to whatever it is applied to), effects, transition, and text or shape styling. It is kept in this browser and shows under My Presets in the Presets panel, across reloads. One clip makes a preset that can be laid onto another clip; several make a group that is added as new clips. Or pass `preset` instead of clipIds to save a definition directly \u2014 the only way to make a preset with media layers: each layer is a video or image (by URL) sized to the clip the preset is applied to, drawn over it (role "over") or cutting it as its track matte (role "matte"), e.g. a torn-paper overlay plus a luma matte that reveals a photo.',
|
|
20065
|
+
inputSchema: {
|
|
20066
|
+
type: "object",
|
|
20067
|
+
properties: {
|
|
20068
|
+
clipIds: { type: "array", items: { type: "string" }, description: "The clips to capture." },
|
|
20069
|
+
preset: {
|
|
20070
|
+
type: "object",
|
|
20071
|
+
description: 'A preset definition instead of clipIds: { name, category?, description?, tags?, id? (custom_\u2026 to replace one), clips?: [], layers: [{ label, role: "over"|"matte", matteType?: "alpha"|"alphaInverted"|"luma"|"lumaInverted", start (s from the clip start), dur?, followLongSide? (quarter-turn to match a portrait clip), copyEffects? (effect types copied from the clip, e.g. ["dropShadow"]), blendMode?, media: { url, name, type: "video"|"image", duration, width, height } }] }.'
|
|
20072
|
+
},
|
|
20073
|
+
name: str("Preset name."),
|
|
20074
|
+
category: {
|
|
20075
|
+
type: "string",
|
|
20076
|
+
enum: ["text", "motion", "paper", "liquid", "glitch", "logo", "lower-third", "transition", "bg"],
|
|
20077
|
+
description: "Shelf it is filed under. Defaults to motion."
|
|
20078
|
+
},
|
|
20079
|
+
tags: { type: "array", items: { type: "string" }, description: "Optional search tags." },
|
|
20080
|
+
description: str("Optional one-line description.")
|
|
20081
|
+
}
|
|
20082
|
+
},
|
|
20083
|
+
method: "savePreset"
|
|
20084
|
+
},
|
|
20085
|
+
{
|
|
20086
|
+
name: "apply_preset",
|
|
20087
|
+
description: "Apply a preset (saved or built-in; ids from list_presets). With clipId, a one-clip preset is laid onto that clip, keeping its media and place and replacing animation only on the properties the preset sets. Without clipId, the preset\u2019s clips are added as new clips at time (default: the playhead). A preset with media layers (list_presets says needsClipId) needs clipId: its layers go on new tracks directly above that clip, sized to its box, starting with it.",
|
|
20088
|
+
inputSchema: {
|
|
20089
|
+
type: "object",
|
|
20090
|
+
properties: {
|
|
20091
|
+
presetId: str("Preset id from list_presets."),
|
|
20092
|
+
clipId: str("Optional clip to apply it to."),
|
|
20093
|
+
time: num("Where new clips start, in seconds, when clipId is not given. Defaults to the playhead."),
|
|
20094
|
+
trackId: str("Optional track for new clips.")
|
|
20095
|
+
},
|
|
20096
|
+
required: ["presetId"]
|
|
20097
|
+
},
|
|
20098
|
+
method: "applyPreset"
|
|
20099
|
+
},
|
|
20100
|
+
{
|
|
20101
|
+
name: "delete_preset",
|
|
20102
|
+
description: "Delete one of the user\u2019s saved presets. Built-in presets cannot be deleted.",
|
|
20103
|
+
inputSchema: {
|
|
20104
|
+
type: "object",
|
|
20105
|
+
properties: { presetId: str("Saved preset id from list_presets.") },
|
|
20106
|
+
required: ["presetId"]
|
|
20107
|
+
},
|
|
20108
|
+
method: "deletePreset"
|
|
20109
|
+
},
|
|
19783
20110
|
{
|
|
19784
20111
|
name: "set_text_animation",
|
|
19785
20112
|
description: "Give a text clip a staggered entrance \u2014 the words rising one after another, letters typing on, a karaoke highlight sweeping through, a mask wipe travelling over the glyphs. The compositor animates per letter, word or line, so a title arrives instead of cutting on. Naming a preset alone uses the timing it was designed with; unit, stagger and durationIn override it. The response reports how long the entrance takes and warns when it outlasts the clip.",
|
|
@@ -19824,7 +20151,7 @@ var TOOLS = [
|
|
|
19824
20151
|
},
|
|
19825
20152
|
{
|
|
19826
20153
|
name: "import_local_file",
|
|
19827
|
-
description: 'Import a media file from the user\'s own computer by FILENAME, through the local bridge. Use this when someone refers to a file they have rather than a URL \u2014 add_media_file cannot reach a path, because a browser tab cannot read one. The bridge searches only the folders its operator shared with --allow-dir, and only video, audio and image files inside them; anything else is not refused so much as invisible. Pass a bare filename ("interview-take-3.mp4"), not a path: the bridge does the finding, and a caller that cannot name a directory cannot be steered out of one. Requires the bridge to be running \u2014 `uvx goatedit-bridge --allow-dir <folder>` \u2014 with its token pasted into the editor.',
|
|
20154
|
+
description: 'Import a media file from the user\'s own computer by FILENAME, through the local bridge. Use this when someone refers to a file they have rather than a URL \u2014 add_media_file cannot reach a path, because a browser tab cannot read one. The bridge searches only the folders its operator shared with --allow-dir, and only video, audio and image files inside them; anything else is not refused so much as invisible. Pass a bare filename ("interview-take-3.mp4"), not a path: the bridge does the finding, and a caller that cannot name a directory cannot be steered out of one. Requires the bridge to be running \u2014 `uvx goatedit-bridge --allow-dir <folder>` \u2014 with its token pasted into the editor. The local goatedit-mcp server needs no bridge for files in its own assets folder: it finds them itself, and also takes a path relative to that folder as list_local_assets gives it.',
|
|
19828
20155
|
inputSchema: {
|
|
19829
20156
|
type: "object",
|
|
19830
20157
|
properties: {
|
|
@@ -19982,7 +20309,7 @@ var TOOLS = [
|
|
|
19982
20309
|
properties: {
|
|
19983
20310
|
clipId: CLIP_ID,
|
|
19984
20311
|
newStartTime: num("New timeline start time in seconds."),
|
|
19985
|
-
newTrackId: str('Track to move onto. Defaults to the clip\u2019s current track. Accepts "__new_video__" to pick a video track free across the clip\u2019s span.')
|
|
20312
|
+
newTrackId: str('Track to move onto. Defaults to the clip\u2019s current track. Accepts "__new_video__" to pick a video track free across the clip\u2019s span, or "__new_track__" for a new video track on top.')
|
|
19986
20313
|
},
|
|
19987
20314
|
required: ["clipId", "newStartTime"]
|
|
19988
20315
|
},
|
|
@@ -20106,6 +20433,90 @@ var TOOLS = [
|
|
|
20106
20433
|
method: "redo"
|
|
20107
20434
|
}
|
|
20108
20435
|
];
|
|
20436
|
+
var SEQUENCE_EDITS = /* @__PURE__ */ new Set([
|
|
20437
|
+
"addClip",
|
|
20438
|
+
"addClips",
|
|
20439
|
+
"addTextClip",
|
|
20440
|
+
"updateTextClip",
|
|
20441
|
+
"addShapeClip",
|
|
20442
|
+
"addAdjustmentLayer",
|
|
20443
|
+
"addTitleCard",
|
|
20444
|
+
"insertRipple",
|
|
20445
|
+
"setClipAudio",
|
|
20446
|
+
"setClipTransform",
|
|
20447
|
+
"setUltraKey",
|
|
20448
|
+
"setTrackMatte",
|
|
20449
|
+
"setShapeGradient",
|
|
20450
|
+
"setMask",
|
|
20451
|
+
"setKeyframes",
|
|
20452
|
+
"setClipEffect",
|
|
20453
|
+
"setTextAnimation",
|
|
20454
|
+
"addHtmlClip",
|
|
20455
|
+
"setColorGrade",
|
|
20456
|
+
"setClipDenoise",
|
|
20457
|
+
"addTransition",
|
|
20458
|
+
"applyAiEdit",
|
|
20459
|
+
"batchEdit",
|
|
20460
|
+
"clearTrack",
|
|
20461
|
+
"nestClips",
|
|
20462
|
+
"addSequenceClip",
|
|
20463
|
+
"removeClip",
|
|
20464
|
+
"moveClip",
|
|
20465
|
+
"trimClip",
|
|
20466
|
+
"splitClip",
|
|
20467
|
+
"rippleDeleteClip",
|
|
20468
|
+
"generateCaptions",
|
|
20469
|
+
"applyPreset",
|
|
20470
|
+
"updateShapeClip",
|
|
20471
|
+
"addNull",
|
|
20472
|
+
"setParent",
|
|
20473
|
+
"groupClips",
|
|
20474
|
+
"setProximity",
|
|
20475
|
+
"setClipSpeed",
|
|
20476
|
+
"setAutoLayout"
|
|
20477
|
+
]);
|
|
20478
|
+
var EXPECTED_SEQUENCE = str(
|
|
20479
|
+
"Optional guard: the sequence id this edit is meant for (from list_sequences). If the tab has a different sequence open, the call is refused instead of editing the wrong one."
|
|
20480
|
+
);
|
|
20481
|
+
var PLACING_EDITS = /* @__PURE__ */ new Set([
|
|
20482
|
+
"addClip",
|
|
20483
|
+
"addClips",
|
|
20484
|
+
"addTextClip",
|
|
20485
|
+
"addShapeClip",
|
|
20486
|
+
"addAdjustmentLayer",
|
|
20487
|
+
"addHtmlClip",
|
|
20488
|
+
"addSequenceClip",
|
|
20489
|
+
"moveClip",
|
|
20490
|
+
"trimClip",
|
|
20491
|
+
"batchEdit",
|
|
20492
|
+
"generateCaptions",
|
|
20493
|
+
"addNull",
|
|
20494
|
+
"groupClips",
|
|
20495
|
+
"setClipSpeed"
|
|
20496
|
+
]);
|
|
20497
|
+
var OVERWRITE = {
|
|
20498
|
+
type: "boolean",
|
|
20499
|
+
description: 'Placing, moving or lengthening a clip onto a span of a named track that other clips already hold is refused, naming them, because it would cut or delete them. Pass true to replace them anyway. Use trackId "__new_video__" for a free video track or "__new_track__" for a new one on top instead.'
|
|
20500
|
+
};
|
|
20501
|
+
var NEW_TRACK_ABOVE = str(
|
|
20502
|
+
'With trackId "__new_track__": put the new track directly above this video track instead of on top of everything \u2014 for a layer that belongs with a clip, below a full-length adjustment layer.'
|
|
20503
|
+
);
|
|
20504
|
+
var NEW_TRACK_BELOW = str('With trackId "__new_track__": put the new track directly below this video track.');
|
|
20505
|
+
var TOOLS = BASE_TOOLS.map((tool) => {
|
|
20506
|
+
if (!tool.method || !SEQUENCE_EDITS.has(tool.method)) return tool;
|
|
20507
|
+
return {
|
|
20508
|
+
...tool,
|
|
20509
|
+
inputSchema: {
|
|
20510
|
+
...tool.inputSchema,
|
|
20511
|
+
properties: {
|
|
20512
|
+
...tool.inputSchema.properties,
|
|
20513
|
+
expectedSequenceId: EXPECTED_SEQUENCE,
|
|
20514
|
+
...PLACING_EDITS.has(tool.method) ? { overwrite: OVERWRITE } : {},
|
|
20515
|
+
...PLACING_EDITS.has(tool.method) && tool.method !== "trimClip" ? { aboveTrackId: NEW_TRACK_ABOVE, belowTrackId: NEW_TRACK_BELOW } : {}
|
|
20516
|
+
}
|
|
20517
|
+
}
|
|
20518
|
+
};
|
|
20519
|
+
});
|
|
20109
20520
|
var TOOLS_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
20110
20521
|
function editDistance(a, b) {
|
|
20111
20522
|
const rows = [Array.from({ length: b.length + 1 }, (_, j) => j)];
|
|
@@ -20174,6 +20585,51 @@ function toContent(result) {
|
|
|
20174
20585
|
return blocks;
|
|
20175
20586
|
}
|
|
20176
20587
|
|
|
20588
|
+
// src/find-local-file.ts
|
|
20589
|
+
import fs from "fs";
|
|
20590
|
+
import path from "path";
|
|
20591
|
+
function findLocalFile(root, wanted, maxDepth) {
|
|
20592
|
+
const name = wanted.trim();
|
|
20593
|
+
if (!name || !fs.existsSync(root)) return null;
|
|
20594
|
+
if (name.includes("/") || name.includes(path.sep)) {
|
|
20595
|
+
const full = path.resolve(root, name);
|
|
20596
|
+
if (!full.startsWith(root + path.sep)) return null;
|
|
20597
|
+
try {
|
|
20598
|
+
return fs.statSync(full).isFile() ? full : null;
|
|
20599
|
+
} catch {
|
|
20600
|
+
return null;
|
|
20601
|
+
}
|
|
20602
|
+
}
|
|
20603
|
+
const target = name.toLowerCase();
|
|
20604
|
+
let level = [root];
|
|
20605
|
+
for (let depth = 0; depth <= maxDepth && level.length; depth++) {
|
|
20606
|
+
const hits = [];
|
|
20607
|
+
const next = [];
|
|
20608
|
+
for (const dir of level) {
|
|
20609
|
+
let entries;
|
|
20610
|
+
try {
|
|
20611
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20612
|
+
} catch {
|
|
20613
|
+
continue;
|
|
20614
|
+
}
|
|
20615
|
+
for (const entry of entries) {
|
|
20616
|
+
if (entry.name.startsWith(".")) continue;
|
|
20617
|
+
const full = path.join(dir, entry.name);
|
|
20618
|
+
if (entry.isDirectory()) next.push(full);
|
|
20619
|
+
else if (entry.isFile() && entry.name.toLowerCase() === target) hits.push(full);
|
|
20620
|
+
}
|
|
20621
|
+
}
|
|
20622
|
+
if (hits.length > 1) {
|
|
20623
|
+
throw new Error(
|
|
20624
|
+
`Several files are named "${name}": ${hits.map((h) => path.relative(root, h)).sort().join(", ")}. Pass one of these paths as filename.`
|
|
20625
|
+
);
|
|
20626
|
+
}
|
|
20627
|
+
if (hits.length === 1) return hits[0];
|
|
20628
|
+
level = next;
|
|
20629
|
+
}
|
|
20630
|
+
return null;
|
|
20631
|
+
}
|
|
20632
|
+
|
|
20177
20633
|
// src/index.ts
|
|
20178
20634
|
var WEBSOCKET_PORT = 9e3;
|
|
20179
20635
|
var BIND_HOST = "127.0.0.1";
|
|
@@ -20201,11 +20657,11 @@ function isLoopbackHost(host) {
|
|
|
20201
20657
|
}
|
|
20202
20658
|
}
|
|
20203
20659
|
var CONTROLLER_TOKEN = process.env.GOATEDIT_CONTROLLER_TOKEN || crypto.randomBytes(32).toString("hex");
|
|
20204
|
-
var CONTROLLER_TOKEN_FILE =
|
|
20660
|
+
var CONTROLLER_TOKEN_FILE = path2.join(os.homedir(), ".goatedit", "mcp-controller-token");
|
|
20205
20661
|
function writeControllerToken() {
|
|
20206
|
-
|
|
20207
|
-
|
|
20208
|
-
|
|
20662
|
+
fs2.mkdirSync(path2.dirname(CONTROLLER_TOKEN_FILE), { recursive: true, mode: 448 });
|
|
20663
|
+
fs2.writeFileSync(CONTROLLER_TOKEN_FILE, CONTROLLER_TOKEN, { mode: 384 });
|
|
20664
|
+
fs2.chmodSync(CONTROLLER_TOKEN_FILE, 384);
|
|
20209
20665
|
}
|
|
20210
20666
|
function hasControllerToken(req) {
|
|
20211
20667
|
const header = req.headers.authorization || "";
|
|
@@ -20215,24 +20671,24 @@ function hasControllerToken(req) {
|
|
|
20215
20671
|
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
20216
20672
|
}
|
|
20217
20673
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
20218
|
-
var LOCAL_ASSETS_DIR =
|
|
20219
|
-
process.env.GOATEDIT_ASSETS_DIR ||
|
|
20674
|
+
var LOCAL_ASSETS_DIR = path2.resolve(
|
|
20675
|
+
process.env.GOATEDIT_ASSETS_DIR || path2.join(os.homedir(), "Movies")
|
|
20220
20676
|
);
|
|
20221
|
-
var FRAMES_DIR =
|
|
20222
|
-
process.env.GOATEDIT_FRAMES_DIR ||
|
|
20677
|
+
var FRAMES_DIR = path2.resolve(
|
|
20678
|
+
process.env.GOATEDIT_FRAMES_DIR || path2.join(os.homedir(), ".goatedit", "frames")
|
|
20223
20679
|
);
|
|
20224
20680
|
var VERSION = (() => {
|
|
20225
|
-
const here =
|
|
20681
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
20226
20682
|
for (const rel of ["../package.json", "../../../package.json"]) {
|
|
20227
20683
|
try {
|
|
20228
|
-
const pkg = JSON.parse(
|
|
20684
|
+
const pkg = JSON.parse(fs2.readFileSync(path2.join(here, rel), "utf8"));
|
|
20229
20685
|
if (pkg.name === "goatedit-mcp") return String(pkg.version);
|
|
20230
20686
|
} catch {
|
|
20231
20687
|
}
|
|
20232
20688
|
}
|
|
20233
20689
|
return "0.0.0";
|
|
20234
20690
|
})();
|
|
20235
|
-
var INSTRUCTIONS = `Edits a GoatEdit video project live in the user's browser tab at ai.goatedit.com. Call get_timeline before
|
|
20691
|
+
var INSTRUCTIONS = `Edits a GoatEdit video project live in the user's browser tab at ai.goatedit.com. Call get_timeline before your first edit \u2014 clip ids change after every split, trim or delete, and a stale id is rejected. Each edit's reply carries timelineChanges (clips added, with their ids and placement; clips changed; ids removed), so take the next ids from there instead of reading the timeline again. get_timeline is lean by default; pass detail: "full" only when you need effect parameters, and trackId or from/to to read part of it. Several edits in a row go faster as one run_tools call. With more than one tab open, call list_connected_projects, then use_project to pick one. render_preview_frame and preview_media accept saveToDisk: true, which writes the frame to a file and returns its path instead of the image. If a tool reports the editor is not connected, ask the user to open ai.goatedit.com and switch on Local in the AI status menu.`;
|
|
20236
20692
|
var MEDIA_TYPES = {
|
|
20237
20693
|
".mp4": { mime: "video/mp4", kind: "video" },
|
|
20238
20694
|
".m4v": { mime: "video/mp4", kind: "video" },
|
|
@@ -20253,18 +20709,18 @@ var MEDIA_TYPES = {
|
|
|
20253
20709
|
".json": { mime: "application/json", kind: "lottie" }
|
|
20254
20710
|
};
|
|
20255
20711
|
function mediaUrl(rel) {
|
|
20256
|
-
return `http://localhost:${WEBSOCKET_PORT}/media/${rel.split(
|
|
20712
|
+
return `http://localhost:${WEBSOCKET_PORT}/media/${rel.split(path2.sep).map(encodeURIComponent).join("/")}`;
|
|
20257
20713
|
}
|
|
20258
20714
|
var MAX_LISTED_ASSETS = 500;
|
|
20259
20715
|
var MAX_ASSET_DEPTH = 4;
|
|
20260
20716
|
async function handleListLocalAssets(args = {}) {
|
|
20261
|
-
if (!
|
|
20717
|
+
if (!fs2.existsSync(LOCAL_ASSETS_DIR)) {
|
|
20262
20718
|
throw new Error(
|
|
20263
20719
|
`Assets directory does not exist: ${LOCAL_ASSETS_DIR}. Set GOATEDIT_ASSETS_DIR to a folder of media.`
|
|
20264
20720
|
);
|
|
20265
20721
|
}
|
|
20266
|
-
const start =
|
|
20267
|
-
if (start !== LOCAL_ASSETS_DIR && !start.startsWith(LOCAL_ASSETS_DIR +
|
|
20722
|
+
const start = path2.resolve(LOCAL_ASSETS_DIR, args.folder || "");
|
|
20723
|
+
if (start !== LOCAL_ASSETS_DIR && !start.startsWith(LOCAL_ASSETS_DIR + path2.sep)) {
|
|
20268
20724
|
throw new Error("folder must be inside the assets directory.");
|
|
20269
20725
|
}
|
|
20270
20726
|
const files = [];
|
|
@@ -20272,7 +20728,7 @@ async function handleListLocalAssets(args = {}) {
|
|
|
20272
20728
|
const walk = (dir, depth) => {
|
|
20273
20729
|
let entries;
|
|
20274
20730
|
try {
|
|
20275
|
-
entries =
|
|
20731
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
20276
20732
|
} catch {
|
|
20277
20733
|
return;
|
|
20278
20734
|
}
|
|
@@ -20283,17 +20739,17 @@ async function handleListLocalAssets(args = {}) {
|
|
|
20283
20739
|
return;
|
|
20284
20740
|
}
|
|
20285
20741
|
if (entry.name.startsWith(".")) continue;
|
|
20286
|
-
const full =
|
|
20742
|
+
const full = path2.join(dir, entry.name);
|
|
20287
20743
|
if (entry.isDirectory()) {
|
|
20288
20744
|
if (depth < MAX_ASSET_DEPTH) walk(full, depth + 1);
|
|
20289
20745
|
continue;
|
|
20290
20746
|
}
|
|
20291
|
-
const type = MEDIA_TYPES[
|
|
20747
|
+
const type = MEDIA_TYPES[path2.extname(entry.name).toLowerCase()];
|
|
20292
20748
|
if (!type || !entry.isFile()) continue;
|
|
20293
|
-
const rel =
|
|
20749
|
+
const rel = path2.relative(LOCAL_ASSETS_DIR, full);
|
|
20294
20750
|
let bytes = 0;
|
|
20295
20751
|
try {
|
|
20296
|
-
bytes =
|
|
20752
|
+
bytes = fs2.statSync(full).size;
|
|
20297
20753
|
} catch {
|
|
20298
20754
|
}
|
|
20299
20755
|
files.push({ name: entry.name, path: rel, type: type.kind, bytes, url: mediaUrl(rel) });
|
|
@@ -20306,6 +20762,14 @@ async function handleListLocalAssets(args = {}) {
|
|
|
20306
20762
|
...truncated ? { truncated: true, note: `Stopped at ${MAX_LISTED_ASSETS} files. Pass folder to list one subfolder.` } : {}
|
|
20307
20763
|
};
|
|
20308
20764
|
}
|
|
20765
|
+
async function handleFindLocalFile(args = {}) {
|
|
20766
|
+
const hit = findLocalFile(LOCAL_ASSETS_DIR, String(args.filename ?? ""), MAX_ASSET_DEPTH);
|
|
20767
|
+
if (!hit) return null;
|
|
20768
|
+
const type = MEDIA_TYPES[path2.extname(hit).toLowerCase()];
|
|
20769
|
+
if (!type || type.kind === "lottie") return null;
|
|
20770
|
+
const rel = path2.relative(LOCAL_ASSETS_DIR, hit);
|
|
20771
|
+
return { name: path2.basename(hit), path: rel, url: mediaUrl(rel), contentType: type.mime };
|
|
20772
|
+
}
|
|
20309
20773
|
function audioDuration(file) {
|
|
20310
20774
|
try {
|
|
20311
20775
|
const out = execFileSync("afinfo", [file], { encoding: "utf8" });
|
|
@@ -20323,10 +20787,10 @@ async function handleGenerateVoiceover(text, projectId) {
|
|
|
20323
20787
|
throw new Error("generate_voiceover needs macOS `say`. Not available on this platform.");
|
|
20324
20788
|
}
|
|
20325
20789
|
const filename = `voiceover_${Date.now()}.wav`;
|
|
20326
|
-
const filePath =
|
|
20327
|
-
|
|
20328
|
-
const scriptPath =
|
|
20329
|
-
|
|
20790
|
+
const filePath = path2.join(LOCAL_ASSETS_DIR, filename);
|
|
20791
|
+
fs2.mkdirSync(LOCAL_ASSETS_DIR, { recursive: true });
|
|
20792
|
+
const scriptPath = path2.join(LOCAL_ASSETS_DIR, `.voiceover_${Date.now()}.txt`);
|
|
20793
|
+
fs2.writeFileSync(scriptPath, text, "utf8");
|
|
20330
20794
|
try {
|
|
20331
20795
|
execFileSync("say", ["-o", filePath, "-f", scriptPath], { stdio: "ignore" });
|
|
20332
20796
|
const duration3 = audioDuration(filePath) ?? text.split(/\s+/).length * 0.45 + 1;
|
|
@@ -20344,7 +20808,7 @@ async function handleGenerateVoiceover(text, projectId) {
|
|
|
20344
20808
|
throw new Error(`Failed to generate voiceover: ${err.message}`);
|
|
20345
20809
|
} finally {
|
|
20346
20810
|
try {
|
|
20347
|
-
|
|
20811
|
+
fs2.unlinkSync(scriptPath);
|
|
20348
20812
|
} catch {
|
|
20349
20813
|
}
|
|
20350
20814
|
}
|
|
@@ -20353,14 +20817,14 @@ function saveFramesToDisk(result, label) {
|
|
|
20353
20817
|
if (!result || typeof result !== "object") return result;
|
|
20354
20818
|
const frames = [result._image, ...Array.isArray(result._images) ? result._images : []].filter((f) => f && typeof f.data === "string" && typeof f.mimeType === "string");
|
|
20355
20819
|
if (!frames.length) return result;
|
|
20356
|
-
|
|
20820
|
+
fs2.mkdirSync(FRAMES_DIR, { recursive: true });
|
|
20357
20821
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
20358
20822
|
const safe = label.replace(/[^\w.-]+/g, "_").slice(0, 60);
|
|
20359
20823
|
const saved = frames.map((f, i) => {
|
|
20360
20824
|
const ext = f.mimeType === "image/png" ? "png" : f.mimeType === "image/webp" ? "webp" : "jpg";
|
|
20361
|
-
const file =
|
|
20825
|
+
const file = path2.join(FRAMES_DIR, `${stamp}-${safe}${frames.length > 1 ? `-${i}` : ""}.${ext}`);
|
|
20362
20826
|
const bytes = Buffer.from(f.data, "base64");
|
|
20363
|
-
|
|
20827
|
+
fs2.writeFileSync(file, bytes);
|
|
20364
20828
|
return { path: file, bytes: bytes.length, ...f.caption ? { caption: f.caption } : {} };
|
|
20365
20829
|
});
|
|
20366
20830
|
const { _image, _images, ...rest } = result;
|
|
@@ -20371,8 +20835,8 @@ function saveFramesToDisk(result, label) {
|
|
|
20371
20835
|
};
|
|
20372
20836
|
}
|
|
20373
20837
|
function serveFile(req, res, filePath) {
|
|
20374
|
-
const size =
|
|
20375
|
-
const type = MEDIA_TYPES[
|
|
20838
|
+
const size = fs2.statSync(filePath).size;
|
|
20839
|
+
const type = MEDIA_TYPES[path2.extname(filePath).toLowerCase()]?.mime ?? "application/octet-stream";
|
|
20376
20840
|
const headers = { "Content-Type": type, "Accept-Ranges": "bytes" };
|
|
20377
20841
|
const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range || "");
|
|
20378
20842
|
if (range && (range[1] || range[2])) {
|
|
@@ -20395,7 +20859,7 @@ function serveFile(req, res, filePath) {
|
|
|
20395
20859
|
res.end();
|
|
20396
20860
|
return;
|
|
20397
20861
|
}
|
|
20398
|
-
|
|
20862
|
+
fs2.createReadStream(filePath, { start, end }).pipe(res);
|
|
20399
20863
|
return;
|
|
20400
20864
|
}
|
|
20401
20865
|
res.writeHead(200, { ...headers, "Content-Length": size });
|
|
@@ -20403,7 +20867,7 @@ function serveFile(req, res, filePath) {
|
|
|
20403
20867
|
res.end();
|
|
20404
20868
|
return;
|
|
20405
20869
|
}
|
|
20406
|
-
|
|
20870
|
+
fs2.createReadStream(filePath).pipe(res);
|
|
20407
20871
|
}
|
|
20408
20872
|
var httpServer = http.createServer((req, res) => {
|
|
20409
20873
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
@@ -20441,9 +20905,9 @@ var httpServer = http.createServer((req, res) => {
|
|
|
20441
20905
|
res.end("Bad path");
|
|
20442
20906
|
return;
|
|
20443
20907
|
}
|
|
20444
|
-
const filePath =
|
|
20445
|
-
const inRoot = filePath === LOCAL_ASSETS_DIR || filePath.startsWith(LOCAL_ASSETS_DIR +
|
|
20446
|
-
if (inRoot &&
|
|
20908
|
+
const filePath = path2.resolve(LOCAL_ASSETS_DIR, filename);
|
|
20909
|
+
const inRoot = filePath === LOCAL_ASSETS_DIR || filePath.startsWith(LOCAL_ASSETS_DIR + path2.sep);
|
|
20910
|
+
if (inRoot && fs2.existsSync(filePath) && fs2.statSync(filePath).isFile()) {
|
|
20447
20911
|
serveFile(req, res, filePath);
|
|
20448
20912
|
} else {
|
|
20449
20913
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
@@ -20640,7 +21104,7 @@ function connectUpstream() {
|
|
|
20640
21104
|
upstreamReady = new Promise((resolve, reject) => {
|
|
20641
21105
|
let token;
|
|
20642
21106
|
try {
|
|
20643
|
-
token = process.env.GOATEDIT_CONTROLLER_TOKEN ||
|
|
21107
|
+
token = process.env.GOATEDIT_CONTROLLER_TOKEN || fs2.readFileSync(CONTROLLER_TOKEN_FILE, "utf8").trim();
|
|
20644
21108
|
} catch {
|
|
20645
21109
|
reject(new Error(notConnectedMessage()));
|
|
20646
21110
|
return;
|
|
@@ -20712,7 +21176,8 @@ async function sendToBrowser(method, params = {}, projectId) {
|
|
|
20712
21176
|
var LOCAL_HANDLERS = {
|
|
20713
21177
|
list_connected_projects: async () => connectedProjects(),
|
|
20714
21178
|
list_local_assets: async (args) => handleListLocalAssets(args),
|
|
20715
|
-
generate_voiceover: async (args, projectId) => handleGenerateVoiceover(args.text, projectId)
|
|
21179
|
+
generate_voiceover: async (args, projectId) => handleGenerateVoiceover(args.text, projectId),
|
|
21180
|
+
find_local_file: async (args) => handleFindLocalFile(args)
|
|
20716
21181
|
};
|
|
20717
21182
|
async function callLocal(method, args, projectId) {
|
|
20718
21183
|
await ensureTransport();
|
|
@@ -20821,9 +21286,26 @@ async function callTool(name, args) {
|
|
|
20821
21286
|
const invalid = validateArgs(tool, forwarded);
|
|
20822
21287
|
if (invalid) throw new McpError(ErrorCode.InvalidParams, invalid);
|
|
20823
21288
|
const target = projectId ?? chosenProjectId;
|
|
21289
|
+
if (name === "import_local_file") return importLocalFile(forwarded, target);
|
|
20824
21290
|
const result = await sendToBrowser(tool.method, forwarded, target);
|
|
20825
21291
|
return saveToDisk ? saveFramesToDisk(result, name) : result;
|
|
20826
21292
|
}
|
|
21293
|
+
async function importLocalFile(args, target) {
|
|
21294
|
+
const local = await callLocal("find_local_file", { filename: args.filename }, target).catch((err) => {
|
|
21295
|
+
if (/^Several files/.test(err?.message ?? "")) throw err;
|
|
21296
|
+
return null;
|
|
21297
|
+
});
|
|
21298
|
+
if (local) {
|
|
21299
|
+
return sendToBrowser("importLocalFile", { ...args, localUrl: local.url, contentType: local.contentType, name: local.name }, target);
|
|
21300
|
+
}
|
|
21301
|
+
try {
|
|
21302
|
+
return await sendToBrowser("importLocalFile", args, target);
|
|
21303
|
+
} catch (err) {
|
|
21304
|
+
throw new Error(
|
|
21305
|
+
`${err?.message ?? err} It is not in this server's assets directory (${LOCAL_ASSETS_DIR}) either: put it there, or start this server with GOATEDIT_ASSETS_DIR set to its folder. list_local_assets shows what is there.`
|
|
21306
|
+
);
|
|
21307
|
+
}
|
|
21308
|
+
}
|
|
20827
21309
|
async function runTools(args) {
|
|
20828
21310
|
const calls = args.calls;
|
|
20829
21311
|
if (!Array.isArray(calls) || !calls.length) {
|
package/package.json
CHANGED