ffmpeg-skill 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kajisho5
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # ffmpeg-skill
2
+
3
+ **Give your coding agent a video editor.** Local FFmpeg, Python standard library, nothing else.
4
+
5
+ ![before / after demo](assets/demo.gif)
6
+
7
+ ```bash
8
+ npx ffmpeg-skill
9
+ ```
10
+
11
+ `ffmpeg-skill` is an [Agent Skill](https://docs.anthropic.com/en/docs/agents-and-tools/agent-skills) for Claude Code, Cursor, Codex and any other agent that reads `SKILL.md`. It teaches the agent a fixed editing workflow (probe → edit losslessly where possible → verify) and ships eight small CLI scripts that do the actual work with `ffmpeg`/`ffprobe`. Think of it as the fully local FFmpeg counterpart to cloud video-agent tools such as browser-use/video-use.
12
+
13
+ **No API keys. No cloud. No dependencies.** If `ffmpeg` and `python3` are on your PATH, it works — offline, on any footage you'd rather not upload.
14
+
15
+ ## Features
16
+
17
+ - **Probe first, verify last** — the skill forces the agent to read real duration/fps/resolution before editing and to check the result after, so you get "final.mp4: 59.98 s, 1080×1920, 30 fps" instead of guesses.
18
+ - **Lossless when possible** — cuts and joins use stream copy by default; re-encoding only happens when it must (frame-accurate cuts, filters, format changes).
19
+ - **Cut & join** segments with `mm:ss` / `hh:mm:ss.ms` times.
20
+ - **Captions** — burn SRT/ASS with font, size, colour, outline and position control; generate SRT from a plain timed-text file.
21
+ - **Fit** to an exact duration (pitch-preserving speed change or trim) and to 16:9 / 9:16 / 1:1 / 4:5 by padding or cropping.
22
+ - **Multicam / external-audio sync** — offset detection by cross-correlation implemented in pure Python (no numpy).
23
+ - **Loudness** — two-pass EBU R128 normalisation to −14 LUFS (or any target) with true-peak ceiling.
24
+ - **Overlays** — logos, watermarks and titles with position, time range, opacity and fades.
25
+ - **Export presets** — YouTube, Instagram Reels/Shorts/TikTok, X, ProRes 422 HQ master, H.265, GIF — all tagged BT.709.
26
+ - **Agent-friendly CLI** — every script has `--help`, prints the output path on stdout, exits non-zero with a reason on stderr, and names outputs `<input>_<operation>.<ext>` by default.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ # Claude Code (default) → ~/.claude/skills/ffmpeg-skill
32
+ npx ffmpeg-skill
33
+
34
+ # Cursor → ~/.cursor/skills/ffmpeg-skill
35
+ npx ffmpeg-skill --cursor
36
+
37
+ # Codex → ~/.codex/skills/ffmpeg-skill
38
+ npx ffmpeg-skill --codex
39
+
40
+ # everything, or a project-local copy, or a custom directory
41
+ npx ffmpeg-skill --all
42
+ npx ffmpeg-skill --project
43
+ npx ffmpeg-skill --dir ./my-skills
44
+ ```
45
+
46
+ Or without Node: clone this repo and copy `SKILL.md` and `scripts/` into your agent's skills directory.
47
+
48
+ You also need FFmpeg:
49
+
50
+ | OS | Command |
51
+ |----|---------|
52
+ | macOS | `brew install ffmpeg` |
53
+ | Ubuntu / Debian | `sudo apt install ffmpeg` |
54
+ | Windows | `winget install Gyan.FFmpeg` |
55
+
56
+ ## Usage
57
+
58
+ Once installed, just talk to your agent. Five things you can say to Claude Code:
59
+
60
+ 1. **"Take `interview.mp4`, keep 0:45–3:10 and 5:00–6:30, and make it exactly 60 seconds for Reels."**
61
+ → `probe.py` → `cut.py --segments 0:45-3:10,5:00-6:30` → `fit.py --duration 60 --aspect 9:16 --fit crop` → `export.py --preset reels` → `probe.py` to confirm 60.0 s at 1080×1920.
62
+ 2. **"Burn these captions in, white text with a black outline at the top, in Japanese."**
63
+ → `caption.py --text cues.txt --font "Noto Sans CJK JP" --position top --outline 3`.
64
+ 3. **"The lav mic recording is out of sync with the camera — fix it and normalise to −14 LUFS."**
65
+ → `sync.py camera.mp4 lav.wav --replace-audio` → `loudness.py` → report the detected offset and final LUFS.
66
+ 4. **"Put our logo in the top-right corner for the whole video at 80% opacity, and a title card for the first 4 seconds."**
67
+ → `overlay.py --image logo.png --position top-right --scale 220 --opacity 0.8` → `overlay.py --text "…" --start 0 --end 4 --fade 0.4`.
68
+ 5. **"Give me a ProRes master and an H.265 copy of the final cut."**
69
+ → `export.py --preset prores` and `export.py --preset h265`.
70
+
71
+ The scripts also work on their own:
72
+
73
+ ```bash
74
+ python3 ~/.claude/skills/ffmpeg-skill/scripts/probe.py input.mp4 --compact
75
+ python3 ~/.claude/skills/ffmpeg-skill/scripts/fit.py input.mp4 --duration 60 --aspect 9:16
76
+ ```
77
+
78
+ More examples: [examples/README.md](examples/README.md). To see everything run end-to-end on generated footage: `bash examples/make_demo.sh`.
79
+
80
+ ## Scripts
81
+
82
+ | Script | What it does |
83
+ |--------|--------------|
84
+ | `probe.py` | Duration, fps (+ VFR detection), resolution, codecs, colour space, audio channels as JSON |
85
+ | `cut.py` | In/out or multi-segment cuts, lossless `-c copy` first, re-encode fallback, `--accurate` for frame-exact |
86
+ | `caption.py` | Burn SRT/ASS (font, size, colour, outline, position); build SRT from timed plain text |
87
+ | `fit.py` | Fit to a duration (speed or trim) and/or aspect ratio (pad or crop), force constant fps |
88
+ | `sync.py` | Detect offset between two recordings by audio cross-correlation; output aligned video/audio |
89
+ | `loudness.py` | Two-pass EBU R128 `loudnorm` to −14 LUFS / −1 dBTP (or custom), video stream-copied |
90
+ | `overlay.py` | Composite image/logo or drawtext title with position, time range, opacity, fade |
91
+ | `export.py` | Presets: `youtube`, `youtube4k`, `reels`, `x`, `prores`, `h265`, `gif` |
92
+
93
+ All scripts: Python 3.9+, standard library only, `--help`, non-zero exit + stderr message on failure.
94
+
95
+ ## Requirements
96
+
97
+ - FFmpeg 5.0+ with `libx264`, `libx265`, `libass` and `prores_ks` (the default builds from Homebrew, apt and gyan.dev include all of them)
98
+ - Python 3.9+
99
+ - Node 16+ only for the `npx` installer
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ bash examples/make_demo.sh # generates footage, runs every script, rebuilds assets/demo.gif
105
+ python3 tests/test_all.py # end-to-end tests (needs ffmpeg)
106
+ node bin/install.js --dir /tmp/skills # try the installer without touching ~/.claude
107
+ ```
108
+
109
+ ## License
110
+
111
+ [MIT](LICENSE)
package/SKILL.md ADDED
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: ffmpeg-skill
3
+ description: Professional video editing with local FFmpeg — cut, caption, fit to duration/aspect, sync multicam audio, normalise loudness, overlay logos and export platform presets, all from Python stdlib scripts with no cloud or API keys.
4
+ ---
5
+
6
+ # ffmpeg-skill
7
+
8
+ You are editing video for the user with FFmpeg through the scripts in `scripts/`.
9
+ Everything runs locally. Nothing is uploaded, no keys are needed, and the only
10
+ requirements are `ffmpeg`/`ffprobe` on PATH and Python 3.9+.
11
+
12
+ Run scripts with `python3 <skill-dir>/scripts/<name>.py ...`. Every script has
13
+ `--help`, exits non-zero on failure with the reason on stderr, prints the output
14
+ path on stdout, and defaults the output name to `<input>_<operation>.<ext>`.
15
+
16
+ ## Workflow (always follow this order)
17
+
18
+ 1. **Probe first.** Run `probe.py` on every input before touching it. Read the
19
+ duration, fps, resolution, codecs, audio channels and the
20
+ `variable_frame_rate_suspected` flag. Plan the edit from real numbers, never
21
+ from assumptions about the file.
22
+ 2. **Prefer lossless.** If the request can be satisfied without re-encoding
23
+ (plain cuts on keyframes, remuxing, audio-only changes), do not re-encode.
24
+ `cut.py` and `loudness.py` stream-copy video by default; only pass
25
+ `--accurate` to `cut.py` when the user needs frame-exact cuts.
26
+ 3. **Chain operations in a sensible order.** Cut → fit → caption/overlay →
27
+ sync/loudness → export. Do the destructive/aspect changes before burning
28
+ text so captions are sized for the final frame. Re-encode as few times as
29
+ possible: if several re-encoding steps are needed, keep intermediates at
30
+ CRF 18 (the default) and only use `export.py` for the last step.
31
+ 4. **Verify the output.** Run `probe.py` on each result and confirm duration,
32
+ resolution, fps and audio match what was requested. Report those numbers to
33
+ the user (e.g. "final.mp4: 59.98 s, 1080x1920, 30 fps, AAC stereo").
34
+ 5. **Keep the user's originals.** Never overwrite the source file. Write new
35
+ files next to the input or where the user asked.
36
+
37
+ ## Request → script
38
+
39
+ | User says | Do |
40
+ |-----------|----|
41
+ | "what's in this file", "how long is it", "is it 4K" | `probe.py input.mp4` |
42
+ | "cut from 1:20 to 2:05", "trim the first 10 seconds" | `cut.py input.mp4 --start 1:20 --end 2:05` |
43
+ | "keep only these parts", "remove the middle" | `cut.py input.mp4 --segments 0-1:00,1:30-2:00` |
44
+ | "make it exactly 60 seconds", "fit it in 30s" | `fit.py input.mp4 --duration 60` (speed) or `--method trim` |
45
+ | "make it vertical / for TikTok / 9:16", "square for Instagram" | `fit.py input.mp4 --aspect 9:16 --fit pad` (or `--fit crop`) |
46
+ | "add subtitles from this SRT", "burn in captions" | `caption.py input.mp4 --srt subs.srt` |
47
+ | "caption it with these lines" (plain text with times) | `caption.py input.mp4 --text cues.txt` |
48
+ | "put our logo top-right", "add a watermark" | `overlay.py input.mp4 --image logo.png --position top-right --scale 200` |
49
+ | "add a title for the first 4 seconds" | `overlay.py input.mp4 --text "Title" --position top --start 0 --end 4 --fade 0.4` |
50
+ | "sync the lav mic to the camera", "line up the two cameras" | `sync.py camera.mp4 mic.wav --replace-audio` / `sync.py camA.mp4 camB.mp4 --trim-second` |
51
+ | "fix the audio levels", "normalise to -14 LUFS" | `loudness.py input.mp4` (`-I -16 --tp -1.5` for podcasts, `-I -23` for broadcast) |
52
+ | "export for YouTube / Reels / X", "give me a ProRes master", "make it HEVC" | `export.py input.mp4 --preset youtube|reels|x|prores|h265` |
53
+ | "make a GIF preview" | `export.py input.mp4 --preset gif` |
54
+
55
+ ## Scripts
56
+
57
+ ### probe.py — inspect
58
+ ```
59
+ probe.py INPUT... [--compact] [--field duration|video.fps|...]
60
+ ```
61
+ JSON with `duration`, `video{codec,width,height,fps,pix_fmt,color_space,rotation,variable_frame_rate_suspected}`,
62
+ `audio{codec,channels,sample_rate}`. `--compact` gives one line per file.
63
+
64
+ ### cut.py — cut / join segments
65
+ ```
66
+ cut.py INPUT [--start T] [--end T | --duration T] [--segments A-B,C-D,...] [--accurate] [-o OUT]
67
+ ```
68
+ Times accept `12.5`, `1:30`, `00:01:30.250`. Default is `-c copy` (snaps to
69
+ keyframes, instant, lossless); if the snapped result deviates more than
70
+ `--tolerance` (0.5 s) from the request, that segment is re-encoded automatically
71
+ (x264 CRF 18). `--accurate` always re-encodes; `--tolerance -1` never does.
72
+ Multiple segments are concatenated in the order given. stderr reports whether
73
+ the result was "lossless stream copy" or "re-encoded".
74
+
75
+ ### fit.py — target duration and/or aspect
76
+ ```
77
+ fit.py INPUT [--duration T --method speed|trim [--from-center] [--max-speed 4]]
78
+ [--aspect 16:9|9:16|1:1|4:5|W:H --fit pad|crop [--width W] [--pad-color black]]
79
+ [--fps N] [-o OUT]
80
+ ```
81
+ `speed` retimes video and audio together (pitch-preserving `atempo`); it
82
+ refuses factors beyond `--max-speed`. `trim` keeps the head (or the middle with
83
+ `--from-center`). `--fps` forces constant frame rate — use it on VFR sources.
84
+
85
+ ### caption.py — subtitles
86
+ ```
87
+ caption.py INPUT --srt FILE | --ass FILE | --text CUES.txt [--write-srt OUT.srt]
88
+ [--font NAME] [--fonts-dir DIR] [--size N] [--color RRGGBB] [--outline N] [--outline-color RRGGBB]
89
+ [--bold] [--box] [--position bottom|top|center|top-left|...] [--margin N] [-o OUT]
90
+ caption.py --text CUES.txt --write-srt OUT.srt # generate the SRT only
91
+ ```
92
+ Text cue format, one per line: `0:00-0:03 Hello`, `00:00:03.500 --> 00:00:06 Two | lines`.
93
+ Lines without a time run for `--auto-seconds` (3 s) after the previous cue. `|` is a line break.
94
+
95
+ ### overlay.py — logo, image, title
96
+ ```
97
+ overlay.py INPUT --image PNG [--scale W | --scale-percent P] | --text "..." [--font-file F.ttf] [--font-size N] [--box]
98
+ [--position top-right|bottom-left|center|X,Y] [--margin N] [--start T] [--end T] [--fade S] [--opacity 0-1] [-o OUT]
99
+ ```
100
+ Alpha in PNGs is respected. Fades apply to the overlay only; the video keeps playing.
101
+
102
+ ### sync.py — offset detection and alignment
103
+ ```
104
+ sync.py REFERENCE SECOND [--json] [--max-offset 30] [--analyze-seconds 120]
105
+ [--replace-audio | --trim-second] [-o OUT]
106
+ ```
107
+ Cross-correlates loudness envelopes (pure Python FFT, ~5 ms resolution).
108
+ Positive offset = the second recording started later. `--replace-audio` writes
109
+ the reference video with the second file's audio aligned (video stream copied).
110
+ `--trim-second` writes the second file shifted to the reference timeline.
111
+ Check `confidence` (0–1); below ~0.3 the match is doubtful — try a longer
112
+ `--analyze-seconds` or a window that contains a clear event (clap).
113
+
114
+ ### loudness.py — EBU R128 normalisation
115
+ ```
116
+ loudness.py INPUT [-I -14] [--tp -1] [--lra 11] [--measure-only] [-o OUT]
117
+ ```
118
+ Two-pass `loudnorm`: measure, then apply with measured values (linear mode when
119
+ the true-peak ceiling allows). Video is stream-copied; audio becomes AAC in
120
+ video containers or the codec matching the extension (.wav → PCM, .flac, .mp3).
121
+
122
+ ### export.py — delivery presets
123
+ ```
124
+ export.py INPUT --preset youtube|youtube4k|reels|x|prores|h265|gif [--fit pad|crop] [--no-scale] [--allow-long] [--crf N] [-o OUT]
125
+ export.py --list
126
+ ```
127
+ Scales into the preset frame (pad by default), tags BT.709, sets `+faststart`,
128
+ trims to platform maximums (Reels 90 s, X 140 s) unless `--allow-long`.
129
+
130
+ ## Gotchas
131
+
132
+ - **Variable frame rate (phone/screen recordings).** `probe.py` sets
133
+ `variable_frame_rate_suspected` when `r_frame_rate` and `avg_frame_rate`
134
+ disagree. VFR causes audio drift and bad concat joins. Fix it early with
135
+ `fit.py --fps 30` (or 60) before cutting with `--accurate`, captioning or
136
+ syncing; add `--accurate` to `cut.py` because copy-cuts on VFR are unreliable.
137
+ - **Audio drift / sync.** Don't mix files with different frame rates or sample
138
+ rates in one `cut.py --segments` join without re-encoding (`--accurate`).
139
+ After `sync.py`, verify by running it again on the output: the offset should
140
+ be ~0. If dialogue drifts over a long recording, the clocks differ; align the
141
+ start with `sync.py`, then fix drift with `fit.py --duration` on the audio-only
142
+ track to match the reference length.
143
+ - **Colour.** All H.264/H.265 outputs are tagged BT.709 (`-colorspace`,
144
+ `-color_primaries`, `-color_trc`) and `yuv420p`. If `probe.py` shows a
145
+ BT.2020/HDR source (`color_transfer` smpte2084 or arib-std-b67), tell the
146
+ user the output will be tagged as SDR BT.709 without tone mapping, and add
147
+ `-vf zscale=t=linear:npl=100,tonemap=hable,zscale=p=bt709:t=bt709:m=bt709`
148
+ manually with ffmpeg if they need a proper SDR conversion.
149
+ - **CJK and other non-Latin text.** libass and drawtext need a font that has
150
+ the glyphs. Check with `fc-list | grep -i cjk`. Then either name it
151
+ (`caption.py --font "Noto Sans CJK JP"`) or point at the file
152
+ (`overlay.py --font-file /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc`,
153
+ `caption.py --fonts-dir ./fonts --font "Noto Sans CJK JP"`). Without a
154
+ matching font you get boxes, not an error. Install: `apt install fonts-noto-cjk`,
155
+ `brew install --cask font-noto-sans-cjk`.
156
+ - **Keyframe cuts.** A lossless `cut.py` result may start up to one GOP (often
157
+ 1–10 s) earlier than requested; the script re-encodes automatically when the
158
+ deviation exceeds 0.5 s. If the user insists on lossless output, pass
159
+ `--tolerance -1` and tell them the cut lands on the nearest earlier keyframe.
160
+ - **Rotation metadata.** Phone footage often has a `rotation` tag; `probe.py`
161
+ reports it and `fit.py` accounts for it when computing the output frame.
162
+ - **Odd dimensions.** `yuv420p` needs even width/height; `fit.py` and
163
+ `export.py` round to even values automatically.
164
+ - **Speed.** Re-encodes use x264 `medium`. For long files add `--preset veryfast`
165
+ to intermediates and keep the default for the final export.
package/bin/install.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ffmpeg-skill installer.
4
+ *
5
+ * Copies SKILL.md and scripts/ into the skills directory of one or more
6
+ * coding agents. Default target is Claude Code (~/.claude/skills/ffmpeg-skill).
7
+ *
8
+ * npx ffmpeg-skill # Claude Code
9
+ * npx ffmpeg-skill --cursor # Cursor (~/.cursor/skills/ffmpeg-skill)
10
+ * npx ffmpeg-skill --codex # Codex (~/.codex/skills/ffmpeg-skill)
11
+ * npx ffmpeg-skill --all # all of the above
12
+ * npx ffmpeg-skill --dir ./skills # custom parent directory
13
+ * npx ffmpeg-skill --project # ./.claude/skills/ffmpeg-skill in the current project
14
+ * npx ffmpeg-skill --uninstall # remove from the selected targets
15
+ */
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+ const { spawnSync } = require('child_process');
22
+
23
+ const SKILL_NAME = 'ffmpeg-skill';
24
+ const ROOT = path.resolve(__dirname, '..');
25
+ const PAYLOAD = ['SKILL.md', 'scripts'];
26
+
27
+ const args = process.argv.slice(2);
28
+ const has = (flag) => args.includes(flag);
29
+ const optValue = (flag) => {
30
+ const i = args.indexOf(flag);
31
+ return i !== -1 && args[i + 1] ? args[i + 1] : null;
32
+ };
33
+
34
+ if (has('--help') || has('-h')) {
35
+ console.log(fs.readFileSync(__filename, 'utf8').split('*/')[0].replace(/^\/\*\*?\s?|^\s\*\s?/gm, ''));
36
+ process.exit(0);
37
+ }
38
+
39
+ const home = os.homedir();
40
+ const targets = [];
41
+ const want = { claude: has('--claude'), cursor: has('--cursor'), codex: has('--codex') };
42
+ if (has('--all')) want.claude = want.cursor = want.codex = true;
43
+ const customDir = optValue('--dir');
44
+ const project = has('--project');
45
+
46
+ if (!want.claude && !want.cursor && !want.codex && !customDir && !project) want.claude = true;
47
+
48
+ if (want.claude) targets.push({ label: 'Claude Code', dir: path.join(home, '.claude', 'skills', SKILL_NAME) });
49
+ if (want.cursor) targets.push({ label: 'Cursor', dir: path.join(home, '.cursor', 'skills', SKILL_NAME) });
50
+ if (want.codex) targets.push({ label: 'Codex', dir: path.join(home, '.codex', 'skills', SKILL_NAME) });
51
+ if (project) targets.push({ label: 'project (.claude/skills)', dir: path.join(process.cwd(), '.claude', 'skills', SKILL_NAME) });
52
+ if (customDir) targets.push({ label: 'custom', dir: path.join(path.resolve(customDir), SKILL_NAME) });
53
+
54
+ function copyRecursive(src, dst) {
55
+ const stat = fs.statSync(src);
56
+ if (stat.isDirectory()) {
57
+ fs.mkdirSync(dst, { recursive: true });
58
+ for (const entry of fs.readdirSync(src)) {
59
+ if (entry === '__pycache__') continue;
60
+ copyRecursive(path.join(src, entry), path.join(dst, entry));
61
+ }
62
+ } else {
63
+ fs.copyFileSync(src, dst);
64
+ if (src.endsWith('.py')) fs.chmodSync(dst, 0o755);
65
+ }
66
+ }
67
+
68
+ function checkFfmpeg() {
69
+ const r = spawnSync('ffmpeg', ['-version'], { encoding: 'utf8' });
70
+ if (r.error || r.status !== 0) {
71
+ console.warn('\n warning: ffmpeg was not found on PATH. The skill needs FFmpeg to run:');
72
+ console.warn(' macOS: brew install ffmpeg');
73
+ console.warn(' Ubuntu: sudo apt install ffmpeg');
74
+ console.warn(' Windows: winget install Gyan.FFmpeg\n');
75
+ return false;
76
+ }
77
+ console.log(` found ${r.stdout.split('\n')[0]}`);
78
+ return true;
79
+ }
80
+
81
+ let failed = false;
82
+ for (const t of targets) {
83
+ try {
84
+ if (has('--uninstall')) {
85
+ fs.rmSync(t.dir, { recursive: true, force: true });
86
+ console.log(`removed ${t.label}: ${t.dir}`);
87
+ continue;
88
+ }
89
+ fs.rmSync(t.dir, { recursive: true, force: true });
90
+ fs.mkdirSync(t.dir, { recursive: true });
91
+ for (const item of PAYLOAD) copyRecursive(path.join(ROOT, item), path.join(t.dir, item));
92
+ console.log(`installed ${t.label}: ${t.dir}`);
93
+ } catch (err) {
94
+ failed = true;
95
+ console.error(`failed for ${t.label} (${t.dir}): ${err.message}`);
96
+ }
97
+ }
98
+
99
+ if (!has('--uninstall')) {
100
+ checkFfmpeg();
101
+ console.log('\nDone. Ask your agent things like "make this video 60 seconds and add captions".');
102
+ }
103
+ process.exit(failed ? 1 : 0);
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "ffmpeg-skill",
3
+ "version": "0.1.0",
4
+ "description": "Agent Skill that lets coding agents (Claude Code, Cursor, Codex) do professional video editing with local FFmpeg. No API keys, no cloud, no dependencies.",
5
+ "keywords": ["ffmpeg", "video", "agent-skill", "claude-code", "cursor", "codex", "skill", "video-editing"],
6
+ "license": "MIT",
7
+ "author": "kajisho5",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kajisho5/ffmpeg-skill.git"
11
+ },
12
+ "homepage": "https://github.com/kajisho5/ffmpeg-skill#readme",
13
+ "bugs": "https://github.com/kajisho5/ffmpeg-skill/issues",
14
+ "bin": {
15
+ "ffmpeg-skill": "bin/install.js"
16
+ },
17
+ "files": [
18
+ "bin/",
19
+ "scripts/",
20
+ "SKILL.md",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "test": "python3 tests/test_all.py",
26
+ "demo": "bash examples/make_demo.sh"
27
+ },
28
+ "engines": {
29
+ "node": ">=16"
30
+ }
31
+ }