goatedit-mcp 1.1.7 → 1.1.9
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/bundle/goatedit-mcp.js +254 -29
- package/package.json +1 -1
package/bundle/goatedit-mcp.js
CHANGED
|
@@ -19294,14 +19294,16 @@ var BASE_TOOLS = [
|
|
|
19294
19294
|
},
|
|
19295
19295
|
{
|
|
19296
19296
|
name: "export_range",
|
|
19297
|
-
description:
|
|
19297
|
+
description: 'Sample a stretch of timeline: rendered frames spread across it, plus the measured audio for the same range. Use it to check a sequence of cuts, a transition, or a whole section in one call, where render_preview_frame answers for a single moment. Up to 6 frames come back as separate pictures; 7 to 24 come back as one tiled sheet stamped with times, which is how a motion is best judged. With video: true it encodes the range as an MP4 (up to 60 seconds) in the background: the first call starts it and answers status "running", and the same call again returns the file once it is done \u2014 the local server saves it to disk and replies with its path.',
|
|
19298
19298
|
inputSchema: {
|
|
19299
19299
|
type: "object",
|
|
19300
19300
|
properties: {
|
|
19301
19301
|
startTime: num("Start of the range in seconds."),
|
|
19302
19302
|
endTime: num("End of the range in seconds. Ranges over 10 minutes are refused."),
|
|
19303
|
-
frames: num("How many frames to sample, up to 6. Defaults to 4."),
|
|
19304
|
-
width: num("Width of each frame in pixels
|
|
19303
|
+
frames: num("How many frames to sample, up to 24; over 6 they come back as one tiled sheet. Defaults to 4."),
|
|
19304
|
+
width: num("Width of each frame in pixels (default 640), of the sheet (default 1280), or of the video (default 640)."),
|
|
19305
|
+
video: bool('Encode the range as an MP4 instead of sampling frames. Call again with the same arguments until status is "done".'),
|
|
19306
|
+
fps: num("Video frame rate. Defaults to the project's."),
|
|
19305
19307
|
skipAdjustments: SKIP_ADJUSTMENTS,
|
|
19306
19308
|
hideClipIds: HIDE_CLIP_IDS
|
|
19307
19309
|
},
|
|
@@ -19329,7 +19331,7 @@ var BASE_TOOLS = [
|
|
|
19329
19331
|
},
|
|
19330
19332
|
{
|
|
19331
19333
|
name: "clean_unused_sequences",
|
|
19332
|
-
description: "Find nested sequences that no clip uses any more \u2014 left behind when a nest clip is removed or a shot is rebuilt \u2014 and optionally delete them with their bin items. Without apply it only lists them.
|
|
19334
|
+
description: "Find nested sequences that no clip uses any more \u2014 left behind when a nest clip is removed or a shot is rebuilt \u2014 and optionally delete them with their bin items. Without apply it only lists them. A scene (a sequence once placed with add_sequence_clip) counts as a nest. Other top-level sequences, and nests reachable from one or open in a tab, are never touched; top-level ones nothing plays are listed under otherUnplayed for delete_sequence.",
|
|
19333
19335
|
inputSchema: {
|
|
19334
19336
|
type: "object",
|
|
19335
19337
|
properties: {
|
|
@@ -19338,6 +19340,18 @@ var BASE_TOOLS = [
|
|
|
19338
19340
|
},
|
|
19339
19341
|
method: "cleanUnusedSequences"
|
|
19340
19342
|
},
|
|
19343
|
+
{
|
|
19344
|
+
name: "delete_sequence",
|
|
19345
|
+
description: "Delete one or more sequences by id, with their bin items. Clips in other sequences that played them are removed too and listed in the reply. The open sequence is refused: open another first. Undo restores everything.",
|
|
19346
|
+
inputSchema: {
|
|
19347
|
+
type: "object",
|
|
19348
|
+
properties: {
|
|
19349
|
+
sequenceId: str("Sequence to delete, from list_sequences."),
|
|
19350
|
+
sequenceIds: { type: "array", items: { type: "string" }, description: "Several at once, in one undo step. Use instead of sequenceId." }
|
|
19351
|
+
}
|
|
19352
|
+
},
|
|
19353
|
+
method: "deleteSequence"
|
|
19354
|
+
},
|
|
19341
19355
|
{
|
|
19342
19356
|
name: "list_sequences",
|
|
19343
19357
|
description: "Every sequence in the project: id, name, whether it is the one open for editing, open in a tab, a nest (made by nest_clips) or top-level, its length, how many clips it holds, and the clips that play it. Every other tool edits the open sequence only \u2014 use this to find the id to pass to open_sequence.",
|
|
@@ -19596,14 +19610,17 @@ var BASE_TOOLS = [
|
|
|
19596
19610
|
},
|
|
19597
19611
|
{
|
|
19598
19612
|
name: "create_shape_clip",
|
|
19599
|
-
description: "Draw a solid shape on the timeline \u2014 a rectangle, square or circle, with an optional stroke and rounded corners. This is how you get a colour behind something: a full-frame backdrop for a title, a lower-third plate, a letterbox bar. Pass fullFrame to size a rectangle to the whole frame without working out the units. Nothing needs importing; the compositor draws it.",
|
|
19613
|
+
description: "Draw a solid shape on the timeline \u2014 a rectangle, square or circle, with an optional stroke and rounded corners, or any outline as a path (a line, an arrow, a star, an underline). This is how you get a colour behind something: a full-frame backdrop for a title, a lower-third plate, a letterbox bar. Pass fullFrame to size a rectangle to the whole frame without working out the units. A path takes `path` (SVG path data in the frame's own pixels) or `points`; it starts as a white 4-unit stroke with no fill unless color or strokeColor is given, and update_shape_clip trimEnd keyframed 0 to 1 draws it on. Nothing needs importing; the compositor draws it.",
|
|
19600
19614
|
inputSchema: {
|
|
19601
19615
|
type: "object",
|
|
19602
19616
|
properties: {
|
|
19603
19617
|
duration: num("How long the shape stays on screen, in seconds."),
|
|
19604
19618
|
startTime: num("Where it starts on the timeline, in seconds. Defaults to 0."),
|
|
19605
19619
|
trackId: str('Target video track, or "__new_video__" to pick one free across the span. Defaults to the latter.'),
|
|
19606
|
-
shapeType: { type: "string", enum: ["rectangle", "square", "circle"], description: "Shape to draw. Defaults to rectangle." },
|
|
19620
|
+
shapeType: { type: "string", enum: ["rectangle", "square", "circle", "path"], description: "Shape to draw. Defaults to rectangle, or path when path or points is given." },
|
|
19621
|
+
path: str('Path shape: SVG path data in frame pixels, e.g. "M 300 800 C 700 600 1200 1000 1620 760" for a curve across a 1920\xD71080 frame. M, L, H, V, C, S, Q and Z, absolute or relative; arcs are not supported. Each extra M starts another outline on the same layer with the same paint (not a hole). width, height and cornerRadius do not apply.'),
|
|
19622
|
+
points: { type: "array", items: { type: "object" }, description: 'Path shape as anchors instead of `path`: [{ "x": 0..1, "y": 0..1 }], fractions of the frame, with optional bezier handles hiX/hiY (incoming) and hoX/hoY (outgoing) as offsets from the anchor in the same units.' },
|
|
19623
|
+
closed: bool("Path shape: join the last anchor back to the first. Defaults to what `path` says (Z), or false for points."),
|
|
19607
19624
|
color: str('Fill colour, e.g. "#000000". Defaults to black.'),
|
|
19608
19625
|
fullFrame: bool("Size a rectangle to cover the whole frame. Overrides width and height."),
|
|
19609
19626
|
width: num("Width in design units against a 1080p-tall frame. Defaults to 300."),
|
|
@@ -19638,7 +19655,13 @@ var BASE_TOOLS = [
|
|
|
19638
19655
|
strokeCap: { type: "string", enum: ["butt", "round", "square"], description: "Ends of an open or dashed outline." },
|
|
19639
19656
|
strokeJoin: { type: "string", enum: ["miter", "round", "bevel"], description: "Outline corners." },
|
|
19640
19657
|
dash: num("Dash length for a dashed outline, design units. 0 for solid."),
|
|
19641
|
-
gap: num("Gap between dashes, design units.")
|
|
19658
|
+
gap: num("Gap between dashes, design units."),
|
|
19659
|
+
path: str("Path shapes: replace the outline, SVG path data in frame pixels (see create_shape_clip). The layer is re-centred on the new outline."),
|
|
19660
|
+
points: { type: "array", items: { type: "object" }, description: "Path shapes: replace the outline with anchors, fractions of the frame." },
|
|
19661
|
+
closed: bool("Path shapes: join the last anchor back to the first."),
|
|
19662
|
+
trimStart: num("Path shapes: where along the path drawing starts, 0..1. Keyframe it as pathTrimStart."),
|
|
19663
|
+
trimEnd: num("Path shapes: where drawing stops, 0..1. 0 hides the path; keyframing pathTrimEnd 0 to 1 with set_keyframes draws it on."),
|
|
19664
|
+
trimOffset: num("Path shapes: slides the drawn stretch along the path, degrees.")
|
|
19642
19665
|
},
|
|
19643
19666
|
required: ["clipId"]
|
|
19644
19667
|
},
|
|
@@ -19924,6 +19947,35 @@ var BASE_TOOLS = [
|
|
|
19924
19947
|
},
|
|
19925
19948
|
method: "removeBackground"
|
|
19926
19949
|
},
|
|
19950
|
+
{
|
|
19951
|
+
name: "measure_text",
|
|
19952
|
+
description: 'How big a piece of text comes out, in frame pixels and as fractions of the frame, without rendering: width, height, the box it covers at its position, how it wrapped (`lines`), and `fitScale` below 1 when it was shrunk to fit the frame. Pass text and any create_text_clip style fields to size a caption before placing it \u2014 to fit a plate behind it, or pick a fontSize that fills a width. Pass clipId to measure a clip as it is, and style fields with it to ask "what if".',
|
|
19953
|
+
inputSchema: {
|
|
19954
|
+
type: "object",
|
|
19955
|
+
properties: {
|
|
19956
|
+
text: str("The words to measure. With clipId, replaces the clip's text for this measurement only."),
|
|
19957
|
+
clipId: str("A text clip to measure. Nothing on it is changed."),
|
|
19958
|
+
time: num("With clipId: timeline time in seconds to measure at, for keyframed size or scale. Defaults to the clip's start."),
|
|
19959
|
+
...TEXT_STYLE_PROPS,
|
|
19960
|
+
posX: num("Horizontal centre as a fraction of the frame, for the box. Defaults to 0.5."),
|
|
19961
|
+
posY: num("Vertical centre as a fraction of the frame, for the box. Defaults to 0.5.")
|
|
19962
|
+
}
|
|
19963
|
+
},
|
|
19964
|
+
method: "getTextSize"
|
|
19965
|
+
},
|
|
19966
|
+
{
|
|
19967
|
+
name: "set_markers",
|
|
19968
|
+
description: 'Point markers on the timeline ruler \u2014 beats, cue points, a note to the user about a moment. With no arguments it lists every marker (scenes from set_scene included, with their duration). `add` places several at once, `remove` deletes by id, and `removeLabelPrefix` clears every point marker whose label starts with it (e.g. "Beat " to re-mark beats). Removals run before additions, all in one undo step. Scene markers are left to set_scene.',
|
|
19969
|
+
inputSchema: {
|
|
19970
|
+
type: "object",
|
|
19971
|
+
properties: {
|
|
19972
|
+
add: { type: "array", items: { type: "object" }, description: 'Markers to place: [{ "time": seconds, "label"?: "Drop", "color"?: "#eab308" }]. Colour cycles a palette when omitted.' },
|
|
19973
|
+
remove: { type: "array", items: { type: "string" }, description: "Marker ids to delete." },
|
|
19974
|
+
removeLabelPrefix: str("Delete every point marker whose label starts with this.")
|
|
19975
|
+
}
|
|
19976
|
+
},
|
|
19977
|
+
method: "setMarkers"
|
|
19978
|
+
},
|
|
19927
19979
|
{
|
|
19928
19980
|
name: "set_scene",
|
|
19929
19981
|
description: "Mark a stretch of timeline as a named scene (a ruler marker with a length), so retime_range can move or stretch it as one piece later. With start and end it makes a scene; with sceneId it changes one (start, end, label, color) or deletes it (remove: true); with no arguments it lists every scene.",
|
|
@@ -20290,7 +20342,7 @@ var BASE_TOOLS = [
|
|
|
20290
20342
|
clipId: CLIP_ID,
|
|
20291
20343
|
type: {
|
|
20292
20344
|
type: "string",
|
|
20293
|
-
description: "Effect type: blur, invert, hue, saturation, brightness, contrast, exposure, grayscale, sepia, gradientMap, vignette, filmGrain, flicker, posterizeTime, lensDistortion, chromaticAberration, lensFlare, liquidGlass, opacityFx, colorCorrection, depthMap, gradientMap, glow, pixelDissolve, motionBlur, dropShadow, reflection, colorama, outline, echo, flutedGlass, lightRays, halftone, datamosh, roughenEdges, filmStock, halation, starGlow, thermal, colorBalance, duotone, levels, monochrome, channelMixer, dither, threshold, lensBlur, directionalBlur, spinBlur, zoomBlur, sharpen, cameraShake, bulge, twirl, ripple, waveWarp, polarCoords, turbulentDisplace, elasticGrid, gridSplit, cubify, glitch, vhs, ntsc, crtScreen, ledScreen, rgbSplit, jpegGlitch, ascii, macpaint, modulation, stripe, risograph, emboss, bevel, inkBleed, paperTexture, paperCutout, crossHatch, scatter, blobTracker, edgeGlow, pageTurn, lightSweep, deepGlow, twister, fill, grid, repeater, plus transform, layer3d, model3d and showcase. Call describe_effect with a type for its parameters, their ranges and what they do before setting anything but the obvious. An unknown name comes back with the full list."
|
|
20345
|
+
description: "Effect type: blur, invert, hue, saturation, brightness, contrast, exposure, grayscale, sepia, gradientMap, vignette, filmGrain, flicker, posterizeTime, lensDistortion, chromaticAberration, lensFlare, liquidGlass, opacityFx, colorCorrection, depthMap, gradientMap, glow, pixelDissolve, motionBlur, dropShadow, reflection, colorama, outline, echo, flutedGlass, lightRays, halftone, datamosh, roughenEdges, filmStock, halation, starGlow, filmDamage, lightLeak, thermal, colorBalance, duotone, levels, monochrome, channelMixer, dither, threshold, lensBlur, directionalBlur, spinBlur, zoomBlur, sharpen, cameraShake, bulge, twirl, ripple, waveWarp, polarCoords, turbulentDisplace, elasticGrid, gridSplit, cubify, glitch, vhs, ntsc, crtScreen, ledScreen, rgbSplit, jpegGlitch, ascii, macpaint, modulation, stripe, risograph, emboss, bevel, inkBleed, paperTexture, paperCutout, crossHatch, scatter, blobTracker, edgeGlow, pageTurn, lightSweep, deepGlow, twister, fill, grid, repeater, plus transform, layer3d, model3d and showcase. Call describe_effect with a type for its parameters, their ranges and what they do before setting anything but the obvious. An unknown name comes back with the full list."
|
|
20294
20346
|
},
|
|
20295
20347
|
params: {
|
|
20296
20348
|
type: "object",
|
|
@@ -20532,6 +20584,77 @@ var BASE_TOOLS = [
|
|
|
20532
20584
|
},
|
|
20533
20585
|
method: "setProjectSettings"
|
|
20534
20586
|
},
|
|
20587
|
+
{
|
|
20588
|
+
name: "begin_request",
|
|
20589
|
+
description: 'Call once at the start of each new request from the user, before editing. It tells the editor what kind of edit this is and which words describe the style, so it can learn what this user means by them, and measures any reference video the user gave. Pass words from the request ("punchy", "cinematic"), not the request itself \u2014 nothing else is kept. References must be in the media bin (import_local_file or add_media_file first); only their measurements are kept (cut pacing, grade, colours), never frames. Replies with those measurements, to use while editing.',
|
|
20590
|
+
inputSchema: {
|
|
20591
|
+
type: "object",
|
|
20592
|
+
properties: {
|
|
20593
|
+
editType: {
|
|
20594
|
+
type: "string",
|
|
20595
|
+
enum: ["reel", "short", "ad", "promo", "trailer", "podcast", "interview", "talking-head", "vlog", "tutorial", "explainer", "documentary", "music-video", "wedding", "event", "product", "real-estate", "gaming", "montage", "slideshow", "corporate", "other"],
|
|
20596
|
+
description: "What the user is making."
|
|
20597
|
+
},
|
|
20598
|
+
styleWords: { type: "array", items: { type: "string" }, description: 'Style words from the request, e.g. ["punchy", "warm"].' },
|
|
20599
|
+
referenceMediaIds: { type: "array", items: { type: "string" }, description: "Media ids of reference videos the user gave (up to 3)." }
|
|
20600
|
+
}
|
|
20601
|
+
},
|
|
20602
|
+
method: "beginRequest"
|
|
20603
|
+
},
|
|
20604
|
+
{
|
|
20605
|
+
name: "record_choice",
|
|
20606
|
+
description: "When you offer the user several versions of an edit (one sequence each, e.g. a fast cut, a story cut, a minimal cut) and they pick one, record it here. The editor compares the pick with the others \u2014 pacing, transitions, effects, text size, music level, grade \u2014 and learns which way this user leans, which get_style_profile then reports under preferences. A pick says more than many silent edits, so offer variants for open-ended requests.",
|
|
20607
|
+
inputSchema: {
|
|
20608
|
+
type: "object",
|
|
20609
|
+
properties: {
|
|
20610
|
+
sequenceIds: { type: "array", items: { type: "string" }, description: "Every variant offered, as sequence ids (2\u20136)." },
|
|
20611
|
+
chosenSequenceId: str("The one the user picked.")
|
|
20612
|
+
},
|
|
20613
|
+
required: ["sequenceIds", "chosenSequenceId"]
|
|
20614
|
+
},
|
|
20615
|
+
method: "recordChoice"
|
|
20616
|
+
},
|
|
20617
|
+
{
|
|
20618
|
+
name: "set_style_brand",
|
|
20619
|
+
description: "Say which brand or client this project is for, so its style is learned and read apart from the user\u2019s other clients. Ask the user first \u2014 when get_style_profile reports a brandGuess, confirm it with them before setting it. name: null files the project under the user\u2019s own style again.",
|
|
20620
|
+
inputSchema: {
|
|
20621
|
+
type: "object",
|
|
20622
|
+
properties: { name: { type: ["string", "null"], description: 'Brand or client name, e.g. "Nike"; null for none.' } },
|
|
20623
|
+
required: ["name"]
|
|
20624
|
+
},
|
|
20625
|
+
method: "setStyleBrand"
|
|
20626
|
+
},
|
|
20627
|
+
{
|
|
20628
|
+
name: "get_style_profile",
|
|
20629
|
+
description: 'What this user\u2019s edits have in common, learned from their past projects: pacing, transitions, text and captions, colour, effects, audio levels, frame shape \u2014 plus the changes they keep making to the agent\u2019s work (corrections) and what their style words ("punchy", "clean") have meant before. Read it before choosing a look, and treat it as defaults: the request in front of you wins. Pass topic to get just one part. Scoped to the project\u2019s brand/client when it has one, so one editor\u2019s clients stay apart. Reports enabled: false when the user has not switched style memory on.',
|
|
20630
|
+
inputSchema: {
|
|
20631
|
+
type: "object",
|
|
20632
|
+
properties: {
|
|
20633
|
+
topic: { type: "string", enum: ["pacing", "transitions", "text", "color", "effects", "audio", "format"], description: "One part of the style. Omit for all of it." },
|
|
20634
|
+
brandId: str("Read another brand\u2019s style instead of this project\u2019s."),
|
|
20635
|
+
editType: str("Read the style for this kind of edit (reel, ad, podcast\u2026) instead of this project\u2019s own."),
|
|
20636
|
+
sheets: bool("Also return up to 2 contact sheets of the user\u2019s own exported edits that fit (people are blurred out). Off by default: each is an image.")
|
|
20637
|
+
}
|
|
20638
|
+
},
|
|
20639
|
+
method: "getStyleProfile"
|
|
20640
|
+
},
|
|
20641
|
+
{
|
|
20642
|
+
name: "set_audio_master",
|
|
20643
|
+
description: "The sequence\u2019s master bus: tag tracks as voice, music or sfx, duck music under voice, and put a safety limiter on the mix. Ducking follows the spans of clips on voice-role tracks (and nested sequences placed on them), dipping music-role tracks by duckDb, starting `attack` seconds before each line so the first word is clear. Playback, probe_audio and export all hear the same bus. With no arguments it reports the current roles and settings.",
|
|
20644
|
+
inputSchema: {
|
|
20645
|
+
type: "object",
|
|
20646
|
+
properties: {
|
|
20647
|
+
roles: { type: "object", description: 'Track roles: { "<trackId>": "voice" | "music" | "sfx" | "none" }.' },
|
|
20648
|
+
ducking: bool("Duck music-role tracks under voice-role tracks."),
|
|
20649
|
+
duckDb: num("How far music dips under voice, in dB (negative). Defaults to -12."),
|
|
20650
|
+
attack: num("Seconds the dip takes, finishing as the voice starts. Defaults to 0.25."),
|
|
20651
|
+
release: num("Seconds music takes to come back after the voice ends. Defaults to 0.6."),
|
|
20652
|
+
limiter: bool("Limit the mix so it does not clip."),
|
|
20653
|
+
ceilingDb: num("Limiter ceiling in dBFS. Defaults to -1.")
|
|
20654
|
+
}
|
|
20655
|
+
},
|
|
20656
|
+
method: "setAudioMaster"
|
|
20657
|
+
},
|
|
20535
20658
|
{
|
|
20536
20659
|
name: "apply_ai_edit",
|
|
20537
20660
|
description: "Tighten the cut from the transcript: remove filler words, remove long pauses, or both. Works on the transcript derived from the clips currently on the timeline, so it respects the cuts already made. Run it with dryRun first to see exactly which ranges would go before anything is removed.",
|
|
@@ -20578,7 +20701,7 @@ var BASE_TOOLS = [
|
|
|
20578
20701
|
},
|
|
20579
20702
|
{
|
|
20580
20703
|
name: "remove_clip",
|
|
20581
|
-
description: "Remove a clip from the timeline, leaving a gap where it was.",
|
|
20704
|
+
description: "Remove a clip from the timeline, leaving a gap where it was. Safe to retry: a clip an earlier MCP edit already removed answers success with alreadyRemoved, while an id never seen is still an error.",
|
|
20582
20705
|
inputSchema: { type: "object", properties: { clipId: CLIP_ID }, required: ["clipId"] },
|
|
20583
20706
|
method: "removeClip"
|
|
20584
20707
|
},
|
|
@@ -20670,6 +20793,20 @@ var BASE_TOOLS = [
|
|
|
20670
20793
|
},
|
|
20671
20794
|
method: "splitClip"
|
|
20672
20795
|
},
|
|
20796
|
+
{
|
|
20797
|
+
name: "loop_clip",
|
|
20798
|
+
description: "Loop a clip: repeat it back to back until it fills a length \u2014 a background plate, an ambience bed, a short animation held under a longer shot. Each pass is an ordinary clip (keyframes and effects included), and the clip\u2019s linked audio loops with it. Give `times` (total passes, the original included) or `until` (timeline time to end at; the last pass is cut short to land on it). Refuses when other clips sit in the way on the same track, naming them.",
|
|
20799
|
+
inputSchema: {
|
|
20800
|
+
type: "object",
|
|
20801
|
+
properties: {
|
|
20802
|
+
clipId: CLIP_ID,
|
|
20803
|
+
times: num("Total passes, the original included. 2 doubles it."),
|
|
20804
|
+
until: num("Timeline time, in seconds, the loop should end at.")
|
|
20805
|
+
},
|
|
20806
|
+
required: ["clipId"]
|
|
20807
|
+
},
|
|
20808
|
+
method: "loopClip"
|
|
20809
|
+
},
|
|
20673
20810
|
{
|
|
20674
20811
|
name: "generate_captions",
|
|
20675
20812
|
description: "Lay timed caption clips on the timeline. By default it reads the real per-word timings stored with the footage and maps them through each clip\u2019s in/out points, so the cards land on the words actually spoken. Passing `text` instead falls back to estimating from word count, which drifts \u2014 only do that when get_transcript shows no stored transcript. The cards go on one video track free across the whole caption run, creating a track above everything when none is, so a run never overwrites a watermark or a title. Styling arguments apply to every card, so a whole run is restyled here rather than one update per card.",
|
|
@@ -20760,7 +20897,11 @@ var SEQUENCE_EDITS = /* @__PURE__ */ new Set([
|
|
|
20760
20897
|
"groupClips",
|
|
20761
20898
|
"setProximity",
|
|
20762
20899
|
"setClipSpeed",
|
|
20763
|
-
"setAutoLayout"
|
|
20900
|
+
"setAutoLayout",
|
|
20901
|
+
"setMarkers",
|
|
20902
|
+
"setScene",
|
|
20903
|
+
"setAudioMaster",
|
|
20904
|
+
"loopClip"
|
|
20764
20905
|
]);
|
|
20765
20906
|
var EXPECTED_SEQUENCE = str(
|
|
20766
20907
|
"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."
|
|
@@ -20868,7 +21009,8 @@ function toContent(result) {
|
|
|
20868
21009
|
if (!result || typeof result !== "object") {
|
|
20869
21010
|
return [{ type: "text", text: JSON.stringify(result) ?? "null" }];
|
|
20870
21011
|
}
|
|
20871
|
-
const { _image, _images, ...rest } = result;
|
|
21012
|
+
const { _image, _images, _file: _file2, ...rest } = result;
|
|
21013
|
+
if (_file2) rest.fileNote = "A file was made but this connection cannot deliver it. Use the local GoatEdit MCP server, which saves it to disk.";
|
|
20872
21014
|
const blocks = [];
|
|
20873
21015
|
const captions = [];
|
|
20874
21016
|
for (const candidate of [_image, ...Array.isArray(_images) ? _images : []]) {
|
|
@@ -20926,6 +21068,38 @@ function findLocalFile(root, wanted, maxDepth) {
|
|
|
20926
21068
|
return null;
|
|
20927
21069
|
}
|
|
20928
21070
|
|
|
21071
|
+
// src/run-refs.ts
|
|
21072
|
+
function resolveRefs(value, refs) {
|
|
21073
|
+
if (typeof value === "string") {
|
|
21074
|
+
const m = /^\$([A-Za-z_][\w-]*)((?:\.[^.]+)*)$/.exec(value);
|
|
21075
|
+
if (!m || !refs.has(m[1])) return value;
|
|
21076
|
+
let x = refs.get(m[1]);
|
|
21077
|
+
for (const key of m[2].split(".").slice(1)) {
|
|
21078
|
+
x = x && typeof x === "object" ? x[key] : void 0;
|
|
21079
|
+
if (x === void 0) throw new Error(`${value}: step "${m[1]}" has no "${m[2].slice(1)}". It returned ${JSON.stringify(refs.get(m[1])).slice(0, 300)}`);
|
|
21080
|
+
}
|
|
21081
|
+
return x;
|
|
21082
|
+
}
|
|
21083
|
+
if (Array.isArray(value)) return value.map((v) => resolveRefs(v, refs));
|
|
21084
|
+
if (value && typeof value === "object") {
|
|
21085
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveRefs(v, refs)]));
|
|
21086
|
+
}
|
|
21087
|
+
return value;
|
|
21088
|
+
}
|
|
21089
|
+
function withRunProject(stepArgs, projectId) {
|
|
21090
|
+
if (typeof projectId !== "string" || !projectId || typeof stepArgs.projectId === "string") return stepArgs;
|
|
21091
|
+
return { ...stepArgs, projectId };
|
|
21092
|
+
}
|
|
21093
|
+
|
|
21094
|
+
// src/tab-registry.ts
|
|
21095
|
+
function indexTabs(reports, isOpen) {
|
|
21096
|
+
const index = /* @__PURE__ */ new Map();
|
|
21097
|
+
for (const r of [...reports].sort((a, b) => a.at - b.at)) {
|
|
21098
|
+
if (isOpen(r.socket)) index.set(r.projectId, r);
|
|
21099
|
+
}
|
|
21100
|
+
return index;
|
|
21101
|
+
}
|
|
21102
|
+
|
|
20929
21103
|
// src/index.ts
|
|
20930
21104
|
var WEBSOCKET_PORT = 9e3;
|
|
20931
21105
|
var BIND_HOST = "127.0.0.1";
|
|
@@ -20984,7 +21158,7 @@ var VERSION = (() => {
|
|
|
20984
21158
|
}
|
|
20985
21159
|
return "0.0.0";
|
|
20986
21160
|
})();
|
|
20987
|
-
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.`;
|
|
21161
|
+
var INSTRUCTIONS = `Edits a GoatEdit video project live in the user's browser tab at ai.goatedit.com. At the start of each new request from the user, call begin_request with the kind of edit, its style words and any reference videos, then get_style_profile for how this user edits; treat that style as defaults, the request wins. 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. To work on several projects at once (one helper agent per video), do not use use_project \u2014 it moves every caller \u2014 but pass projectId on each call, or once on run_tools for all its steps. 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.`;
|
|
20988
21162
|
var MEDIA_TYPES = {
|
|
20989
21163
|
".mp4": { mime: "video/mp4", kind: "video" },
|
|
20990
21164
|
".m4v": { mime: "video/mp4", kind: "video" },
|
|
@@ -21110,6 +21284,18 @@ async function handleGenerateVoiceover(text, projectId) {
|
|
|
21110
21284
|
}
|
|
21111
21285
|
}
|
|
21112
21286
|
}
|
|
21287
|
+
function saveFileToDisk(result) {
|
|
21288
|
+
const file = result && typeof result === "object" ? result._file : void 0;
|
|
21289
|
+
if (!file || typeof file.data !== "string") return result;
|
|
21290
|
+
fs2.mkdirSync(FRAMES_DIR, { recursive: true });
|
|
21291
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
21292
|
+
const safe = String(file.name ?? "file").replace(/[^\w.-]+/g, "_").slice(0, 80);
|
|
21293
|
+
const target = path2.join(FRAMES_DIR, `${stamp}-${safe}`);
|
|
21294
|
+
const bytes = Buffer.from(file.data, "base64");
|
|
21295
|
+
fs2.writeFileSync(target, bytes);
|
|
21296
|
+
const { _file: _file2, ...rest } = result;
|
|
21297
|
+
return { ...rest, savedTo: target, bytes: bytes.length };
|
|
21298
|
+
}
|
|
21113
21299
|
function saveFramesToDisk(result, label) {
|
|
21114
21300
|
if (!result || typeof result !== "object") return result;
|
|
21115
21301
|
const frames = [result._image, ...Array.isArray(result._images) ? result._images : []].filter((f) => f && typeof f.data === "string" && typeof f.mimeType === "string");
|
|
@@ -21228,6 +21414,14 @@ var wss = new import_websocket_server.default({
|
|
|
21228
21414
|
wss.on("error", () => {
|
|
21229
21415
|
});
|
|
21230
21416
|
var tabs = /* @__PURE__ */ new Map();
|
|
21417
|
+
var tabReports = /* @__PURE__ */ new Map();
|
|
21418
|
+
function reindexTabs() {
|
|
21419
|
+
tabs.clear();
|
|
21420
|
+
for (const [pid, r] of indexTabs(tabReports.values(), (s) => s.readyState === import_websocket.default.OPEN)) {
|
|
21421
|
+
tabs.set(pid, { ws: r.socket, projectId: pid, projectName: r.projectName });
|
|
21422
|
+
}
|
|
21423
|
+
if (lastActiveProjectId && !tabs.has(lastActiveProjectId)) lastActiveProjectId = null;
|
|
21424
|
+
}
|
|
21231
21425
|
var lastActiveProjectId = null;
|
|
21232
21426
|
var chosenProjectId = null;
|
|
21233
21427
|
var controllerSockets = /* @__PURE__ */ new Set();
|
|
@@ -21301,7 +21495,7 @@ wss.on("connection", (ws, req) => {
|
|
|
21301
21495
|
}
|
|
21302
21496
|
const target = resolveTab(projectId);
|
|
21303
21497
|
if (id !== void 0) controllerRequests.set(String(id), ws);
|
|
21304
|
-
target.ws.send(JSON.stringify({ id, method, params }));
|
|
21498
|
+
target.ws.send(JSON.stringify({ id, method, params, projectId: target.projectId }));
|
|
21305
21499
|
} catch (err) {
|
|
21306
21500
|
sendJson(ws, { id, error: err?.message ?? String(err) });
|
|
21307
21501
|
}
|
|
@@ -21314,7 +21508,6 @@ wss.on("connection", (ws, req) => {
|
|
|
21314
21508
|
return;
|
|
21315
21509
|
}
|
|
21316
21510
|
console.error("[GoatEdit MCP] Browser editor connected via WebSocket");
|
|
21317
|
-
let myProjectId = null;
|
|
21318
21511
|
ws.on("message", (data) => {
|
|
21319
21512
|
let message;
|
|
21320
21513
|
try {
|
|
@@ -21326,10 +21519,9 @@ wss.on("connection", (ws, req) => {
|
|
|
21326
21519
|
if (message.type === "status") {
|
|
21327
21520
|
if (typeof message.projectId === "string" && message.projectId) {
|
|
21328
21521
|
const pid = message.projectId;
|
|
21329
|
-
|
|
21330
|
-
myProjectId = pid;
|
|
21331
|
-
tabs.set(pid, { ws, projectId: pid, projectName: message.projectName ?? null });
|
|
21522
|
+
tabReports.set(ws, { socket: ws, projectId: pid, projectName: message.projectName ?? null, at: Date.now() });
|
|
21332
21523
|
lastActiveProjectId = pid;
|
|
21524
|
+
reindexTabs();
|
|
21333
21525
|
console.error(`[GoatEdit MCP] Active project: ${message.projectName} (${pid})`);
|
|
21334
21526
|
}
|
|
21335
21527
|
for (const controller2 of controllerSockets) sendJson(controller2, message);
|
|
@@ -21354,11 +21546,8 @@ wss.on("connection", (ws, req) => {
|
|
|
21354
21546
|
ws.on("close", () => {
|
|
21355
21547
|
console.error("[GoatEdit MCP] Browser editor disconnected");
|
|
21356
21548
|
rejectPendingOn(ws, "The editor tab disconnected");
|
|
21357
|
-
|
|
21358
|
-
|
|
21359
|
-
tabs.delete(pid);
|
|
21360
|
-
if (lastActiveProjectId === pid) lastActiveProjectId = null;
|
|
21361
|
-
}
|
|
21549
|
+
tabReports.delete(ws);
|
|
21550
|
+
reindexTabs();
|
|
21362
21551
|
});
|
|
21363
21552
|
});
|
|
21364
21553
|
var ownsPort = false;
|
|
@@ -21467,7 +21656,7 @@ async function sendToBrowser(method, params = {}, projectId) {
|
|
|
21467
21656
|
reject(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s: ${method}. Call get_timeline to see whether it landed.`));
|
|
21468
21657
|
}, REQUEST_TIMEOUT_MS);
|
|
21469
21658
|
pendingRequests.set(id, { resolve, reject, timeoutId, socket: tab.ws, method });
|
|
21470
|
-
tab.ws.send(JSON.stringify({ id, method, params }));
|
|
21659
|
+
tab.ws.send(JSON.stringify({ id, method, params, projectId: tab.projectId }));
|
|
21471
21660
|
});
|
|
21472
21661
|
}
|
|
21473
21662
|
var LOCAL_HANDLERS = {
|
|
@@ -21519,7 +21708,7 @@ var LOCAL_TOOLS = [
|
|
|
21519
21708
|
},
|
|
21520
21709
|
{
|
|
21521
21710
|
name: "run_tools",
|
|
21522
|
-
description:
|
|
21711
|
+
description: 'Run several tool calls in order in one round trip \u2014 the fast way to make a series of edits. Stops at the first failure unless continueOnError is set, and reports each step\'s result. Every step sees the edits of the steps before it. To use an id a step makes, name that step with `as` and write "$name.path" in a later step\'s args: { tool: "create_shape_clip", args: {...}, as: "bg" } then { tool: "set_keyframes", args: { clipId: "$bg.clipId", ... } }. A whole string arg is replaced; paths walk objects and arrays ("$made.clipIds.0"). With rollback, a failed run puts the whole project back as it was before the first step.',
|
|
21523
21712
|
inputSchema: {
|
|
21524
21713
|
type: "object",
|
|
21525
21714
|
properties: {
|
|
@@ -21530,12 +21719,15 @@ var LOCAL_TOOLS = [
|
|
|
21530
21719
|
type: "object",
|
|
21531
21720
|
properties: {
|
|
21532
21721
|
tool: { type: "string", description: "Tool name, e.g. move_clip." },
|
|
21533
|
-
args: { type: "object", description:
|
|
21722
|
+
args: { type: "object", description: 'That tool\'s arguments. A string "$name.path" is replaced by that value from an earlier step named `name`.' },
|
|
21723
|
+
as: { type: "string", description: "Name this step's result so later steps can refer to it." }
|
|
21534
21724
|
},
|
|
21535
21725
|
required: ["tool"]
|
|
21536
21726
|
}
|
|
21537
21727
|
},
|
|
21538
|
-
continueOnError: { type: "boolean", description: "Run the remaining steps after one fails." }
|
|
21728
|
+
continueOnError: { type: "boolean", description: "Run the remaining steps after one fails." },
|
|
21729
|
+
rollback: { type: "boolean", description: "Take a checkpoint first and restore it if any step fails, so a run either lands whole or not at all. Cannot be combined with continueOnError." },
|
|
21730
|
+
projectId: { type: "string", description: "Run every step on this project (from list_connected_projects). Use it when several agents edit different tabs at once; a step's own projectId wins." }
|
|
21539
21731
|
},
|
|
21540
21732
|
required: ["calls"]
|
|
21541
21733
|
}
|
|
@@ -21589,7 +21781,8 @@ async function callTool(name, args) {
|
|
|
21589
21781
|
const target = projectId ?? chosenProjectId;
|
|
21590
21782
|
if (name === "import_local_file") return importLocalFile(forwarded, target);
|
|
21591
21783
|
const result = await sendToBrowser(tool.method, forwarded, target);
|
|
21592
|
-
|
|
21784
|
+
const withFile = saveFileToDisk(result);
|
|
21785
|
+
return saveToDisk ? saveFramesToDisk(withFile, name) : withFile;
|
|
21593
21786
|
}
|
|
21594
21787
|
async function importLocalFile(args, target) {
|
|
21595
21788
|
const local = await callLocal("find_local_file", { filename: args.filename }, target).catch((err) => {
|
|
@@ -21612,14 +21805,32 @@ async function runTools(args) {
|
|
|
21612
21805
|
if (!Array.isArray(calls) || !calls.length) {
|
|
21613
21806
|
throw new McpError(ErrorCode.InvalidParams, "calls must be a non-empty array of { tool, args }.");
|
|
21614
21807
|
}
|
|
21808
|
+
if (args.rollback && args.continueOnError) {
|
|
21809
|
+
throw new McpError(ErrorCode.InvalidParams, "rollback and continueOnError cannot be combined: a run that keeps going after a failure has nothing to roll back to.");
|
|
21810
|
+
}
|
|
21615
21811
|
const results = [];
|
|
21616
21812
|
const images = [];
|
|
21813
|
+
const refs = /* @__PURE__ */ new Map();
|
|
21617
21814
|
let failed = 0;
|
|
21815
|
+
let checkpointId;
|
|
21816
|
+
if (args.rollback) {
|
|
21817
|
+
const made = await callTool("create_checkpoint", withRunProject({ name: "run_tools rollback" }, args.projectId));
|
|
21818
|
+
checkpointId = made?.checkpointId;
|
|
21819
|
+
}
|
|
21618
21820
|
for (let i = 0; i < calls.length; i++) {
|
|
21619
21821
|
const step = calls[i];
|
|
21620
21822
|
const tool = typeof step?.tool === "string" ? step.tool : "";
|
|
21621
21823
|
try {
|
|
21622
|
-
const
|
|
21824
|
+
const stepArgs = resolveRefs(step.args && typeof step.args === "object" ? step.args : {}, refs);
|
|
21825
|
+
const raw = await callTool(tool, withRunProject(stepArgs, args.projectId));
|
|
21826
|
+
if (typeof step.as === "string" && step.as) {
|
|
21827
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
21828
|
+
const { _image, _images, ...kept } = raw;
|
|
21829
|
+
refs.set(step.as, kept);
|
|
21830
|
+
} else {
|
|
21831
|
+
refs.set(step.as, raw);
|
|
21832
|
+
}
|
|
21833
|
+
}
|
|
21623
21834
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
21624
21835
|
const { _image, _images, ...rest } = raw;
|
|
21625
21836
|
for (const img of [_image, ...Array.isArray(_images) ? _images : []]) {
|
|
@@ -21638,7 +21849,21 @@ async function runTools(args) {
|
|
|
21638
21849
|
}
|
|
21639
21850
|
}
|
|
21640
21851
|
}
|
|
21641
|
-
|
|
21852
|
+
let rolledBack;
|
|
21853
|
+
if (failed && checkpointId) {
|
|
21854
|
+
try {
|
|
21855
|
+
await callTool("restore_checkpoint", withRunProject({ checkpointId }, args.projectId));
|
|
21856
|
+
rolledBack = true;
|
|
21857
|
+
} catch (err) {
|
|
21858
|
+
rolledBack = `failed: ${err?.message ?? err}. Restore ${checkpointId} with restore_checkpoint.`;
|
|
21859
|
+
}
|
|
21860
|
+
}
|
|
21861
|
+
return {
|
|
21862
|
+
ok: failed === 0,
|
|
21863
|
+
results,
|
|
21864
|
+
...rolledBack !== void 0 ? { rolledBack } : {},
|
|
21865
|
+
...images.length ? { _images: images } : {}
|
|
21866
|
+
};
|
|
21642
21867
|
}
|
|
21643
21868
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
21644
21869
|
const { name, arguments: rawArgs } = request.params;
|
package/package.json
CHANGED