ffmpeg-skill 1.0.2 → 1.0.4

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 CHANGED
@@ -349,11 +349,11 @@ Benchmarks live in `tests/bench_*.py`, agent evals in [evals/](evals/), results
349
349
  ```bash
350
350
  npx ffmpeg-skill # Claude Code → ~/.claude/skills/ffmpeg-skill
351
351
  npx ffmpeg-skill --cursor # Cursor → ~/.cursor/skills/ffmpeg-skill
352
- npx ffmpeg-skill --codex # Codex → ~/.codex/skills/ffmpeg-skill
352
+ npx ffmpeg-skill --codex # Codex → ~/.agents/skills/ffmpeg-skill (Cursor reads this location too)
353
353
  npx ffmpeg-skill --all # all three
354
354
  npx ffmpeg-skill --project # this project → ./.claude/skills/ffmpeg-skill
355
355
  npx ffmpeg-skill --dir ./my-skills
356
- npx ffmpeg-skill --uninstall # remove from the selected targets
356
+ npx ffmpeg-skill --uninstall # remove from the selected targets (--codex also clears the older ~/.codex/skills location)
357
357
  ```
358
358
 
359
359
  Without Node: clone this repository and copy `SKILL.md`, `scripts/`, `references/` and `mcp/` into your agent's skills directory.
@@ -395,7 +395,7 @@ CI (`.github/workflows/ci.yml`) runs on every pull request and on pushes to `mai
395
395
 
396
396
  `tests/test_contract.py` runs on all three OSes, but a handful of its tests build a fake `ffmpeg` as a `#!/bin/sh` script on a PATH shim to force specific FFmpeg 6/7/8/9 fixture layouts through `doctor`'s parser — that technique isn't portable to Windows, so `test_dry_run_never_runs_ffmpeg_and_writes_nothing` and the whole `DoctorDetectionTests` class (fixture-driven layout parsing) are individually `skipIf`'d there and show as `skipped`, not silently absent, in the Windows job's log. Everything else — contract schema, `reencodes_*`, `doctor.tools`, MCP derivation, and every tool exercised through the contract, including `cut.py`'s provenance fields — runs against the real Windows `ffmpeg` on every PR. See [references/ci-platform-pitfalls.md](references/ci-platform-pitfalls.md) for this and other per-OS behaviour differences already diagnosed, before spending a CI cycle re-diagnosing a platform-only failure.
397
397
 
398
- **Releasing** is fully automated end to end, including the version number itself — a PR doesn't need to touch `package.json`, `docs/contract.md`, or `CHANGELOG.md` at all. Once a PR merges to `main`, `.github/workflows/release.yml` takes it from there: if nobody bumped the version by hand, it asks [release-drafter](.github/release-drafter.yml) to resolve the next version from the labels on every PR merged since the last tag (`major`/`minor`/`feature`/`enhancement`/`fix`/`bug`/... an unlabeled PR defaults to `patch`). Most PRs don't need a label added by hand: `release-drafter.yml`'s `autolabel` job applies one automatically from the PR's title/changed files (`Fix ...` → `fix`, `Add ...`/`feat ...` → `feature`, `docs`/`.md`/`.github/` changes → `chore`) as soon as it's opened — add a label yourself only to override that, or for `major`. It bumps `package.json` and `docs/contract.md`, writes a `CHANGELOG.md` section listing those PRs (and any issues they closed), and pushes that commit to `main` itself. Either way — auto-bumped or hand-bumped in the PR — it then creates the `vX.Y.Z` tag, publishes a GitHub Release with notes extracted from `CHANGELOG.md`'s matching section, and publishes the package to npm. A PR that still wants to write its own version bump and `CHANGELOG.md` prose (e.g. to explain the "why" of a release by hand) can — the automation only fills in when nobody made that call already. A push to `main` with nothing new to release is a no-op. npm publishing needs an `NPM_TOKEN` repo secret (an npm access token with publish rights on this package) — without it the tag and GitHub Release still happen, only the npm step is skipped. A repo that depends on this one (an editing skill, an agent) should pin an `ffmpeg-skill` version by tag or npm version, not by tracking `main` — a merged-but-not-yet-released commit on `main` can be ahead of the last published npm version for the few minutes between merge and this workflow completing.
398
+ **Releasing** is fully automated end to end, including the version number itself — a PR doesn't need to touch `package.json`, `docs/contract.md`, or `CHANGELOG.md` at all. Once a PR merges to `main`, `.github/workflows/release.yml` takes it from there: if nobody bumped the version by hand, it asks [release-drafter](.github/release-drafter.yml) to resolve the next version from the labels on every PR merged since the last tag (`minor`/`feature`/`enhancement` → minor, `fix`/`bug`/`patch` patch, an unlabeled PR defaults to `patch`; PRs labelled `chore`, `ci`, `docs` or `dependencies` are excluded, so a merge that changes no shipped file releases nothing). Most PRs don't need a label added by hand: `release-drafter.yml`'s `autolabel` job applies one automatically from the PR's title/changed files (`Fix ...` → `fix`, `Add ...`/`feat ...` → `feature`, `docs`/`.md`/`.github/`/`build(deps)` changes → `chore`) as soon as it's opened — add a label yourself only to override that. **A major version is never chosen automatically**: no label rule produces `major`, and the workflow refuses to auto-bump across a major boundary even if someone applies that label — a real major release is a deliberate `package.json` bump in a PR, which the manual path below already handles. It bumps `package.json` and `docs/contract.md`, writes a `CHANGELOG.md` section listing those PRs (and any issues they closed), and pushes that commit to `main` itself. Either way — auto-bumped or hand-bumped in the PR — it then creates the `vX.Y.Z` tag, publishes a GitHub Release with notes extracted from `CHANGELOG.md`'s matching section, and publishes the package to npm. A PR that still wants to write its own version bump and `CHANGELOG.md` prose (e.g. to explain the "why" of a release by hand) can — the automation only fills in when nobody made that call already. A push to `main` with nothing new to release is a no-op. npm publishing needs an `NPM_TOKEN` repo secret (an npm access token with publish rights on this package) — without it the tag and GitHub Release still happen, only the npm step is skipped. A repo that depends on this one (an editing skill, an agent) should pin an `ffmpeg-skill` version by tag or npm version, not by tracking `main` — a merged-but-not-yet-released commit on `main` can be ahead of the last published npm version for the few minutes between merge and this workflow completing.
399
399
 
400
400
  Contributing a change: see [CONTRIBUTING.md](CONTRIBUTING.md).
401
401
 
package/SKILL.md CHANGED
@@ -118,7 +118,7 @@ This skill cuts, joins, measures, syncs, exports and checks files — it execute
118
118
 
119
119
  The line in general: if the same input and the same explicit parameters always produce the same, verifiable output, it belongs here. If the "right" answer depends on taste, content understanding, or what looks or sounds good, it belongs to whichever skill or agent makes that judgement — this skill only ever executes parameters it's given, never infers them from what something looks or sounds like.
120
120
 
121
- If a request needs an FFmpeg feature none of the 28 scripts expose, say so and name the closest built-in option (`--dry-run` to show what would run, or a documented limitation) — never fall back to guessing a raw `ffmpeg`/`ffprobe` invocation or a hand-built filter graph outside `scripts/*.py`. A raw command bypasses every guarantee this skill makes (no shell, typed arguments, verification afterwards); it is exactly the failure mode this skill exists to prevent, so it is never the fallback when a script's flag doesn't cover something.
121
+ If a request needs an FFmpeg feature none of the 40 scripts expose, say so and name the closest built-in option (`--dry-run` to show what would run, or a documented limitation) — never fall back to guessing a raw `ffmpeg`/`ffprobe` invocation or a hand-built filter graph outside `scripts/*.py`. A raw command bypasses every guarantee this skill makes (no shell, typed arguments, verification afterwards); it is exactly the failure mode this skill exists to prevent, so it is never the fallback when a script's flag doesn't cover something.
122
122
 
123
123
  ## Request → script
124
124
 
package/bin/install.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * npx ffmpeg-skill # Claude Code
9
9
  * npx ffmpeg-skill --cursor # Cursor (~/.cursor/skills/ffmpeg-skill)
10
- * npx ffmpeg-skill --codex # Codex (~/.codex/skills/ffmpeg-skill)
10
+ * npx ffmpeg-skill --codex # Codex (~/.agents/skills/ffmpeg-skill)
11
11
  * npx ffmpeg-skill --all # all of the above
12
12
  * npx ffmpeg-skill --dir ./skills # custom parent directory
13
13
  * npx ffmpeg-skill --project # ./.claude/skills/ffmpeg-skill in the current project
@@ -69,7 +69,10 @@ if (!want.claude && !want.cursor && !want.codex && !customDir && !project) want.
69
69
 
70
70
  if (want.claude) targets.push({ label: 'Claude Code', dir: path.join(home, '.claude', 'skills', SKILL_NAME) });
71
71
  if (want.cursor) targets.push({ label: 'Cursor', dir: path.join(home, '.cursor', 'skills', SKILL_NAME) });
72
- if (want.codex) targets.push({ label: 'Codex', dir: path.join(home, '.codex', 'skills', SKILL_NAME) });
72
+ // Codex reads user-level skills from ~/.agents/skills (its docs list $HOME/.agents/skills,
73
+ // .agents/skills up the repo tree, and /etc/codex/skills), not ~/.codex/skills -- the
74
+ // latter was this installer's original guess and is not a location current Codex scans.
75
+ if (want.codex) targets.push({ label: 'Codex', dir: path.join(home, '.agents', 'skills', SKILL_NAME), legacy: path.join(home, '.codex', 'skills', SKILL_NAME) });
73
76
  if (project) targets.push({ label: 'project (.claude/skills)', dir: path.join(process.cwd(), '.claude', 'skills', SKILL_NAME) });
74
77
  if (customDir) targets.push({ label: 'custom', dir: path.join(path.resolve(customDir), SKILL_NAME) });
75
78
 
@@ -106,6 +109,12 @@ for (const t of targets) {
106
109
  if (has('--uninstall')) {
107
110
  fs.rmSync(t.dir, { recursive: true, force: true });
108
111
  console.log(`removed ${t.label}: ${t.dir}`);
112
+ // A copy left by the installer's earlier, wrong guess at this agent's directory (see the
113
+ // Codex target above) would otherwise survive every uninstall from here on.
114
+ if (t.legacy && fs.existsSync(t.legacy)) {
115
+ fs.rmSync(t.legacy, { recursive: true, force: true });
116
+ console.log(`removed ${t.label} (older install location): ${t.legacy}`);
117
+ }
109
118
  continue;
110
119
  }
111
120
  // Copy into a scratch directory next to the real target first, then swap it into place
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ffmpeg-skill",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Agent Skill that gives coding agents (Claude Code, Cursor, Codex) a local video editor: 40 FFmpeg tools with a machine-readable contract, contract-derived MCP server, FFmpeg capability detection, probe-first / verify-last workflow. Cut, join, silence removal, fit, captions and karaoke, overlays, motion graphics, HDR to SDR, LUTs, audio clean-up and typed dynamics, sync with drift correction, multicam, loudness, delivery checks, project rendering, batch. No API keys, no cloud, no dependencies.",
5
5
  "keywords": [
6
6
  "ffmpeg",
@@ -30,23 +30,43 @@ Caught twice while adding capability metadata for new flags (`caption.py --mode
30
30
  after a test failure revealed it. Do that grep first, every time `required`/`optional`
31
31
  changes.
32
32
 
33
- ## Git tag push and GitHub Release creation are not reachable from this environment
33
+ ## Git tag push and GitHub Release creation are not reachable from this environment — so they are not done from it
34
34
 
35
35
  The git credentials available here can push to `refs/heads/*` (branches) but not
36
36
  `refs/tags/*` — confirmed by a 403 straight from the git-receive-pack endpoint, not an
37
37
  auth failure, meaning it's a deliberate scope restriction, not a bug to route around.
38
- The GitHub MCP tool surface has no `create_release`/`create_tag` equivalent either
39
- (`create_branch`, `create_pull_request`, `create_or_update_file` exist; nothing for
40
- releases). A direct call to the GitHub REST API's `/releases` endpoint with a raw token
41
- is also blocked by the outbound proxy itself (its own 403, pointing at Anthropic's docs,
42
- not GitHub's).
38
+ The GitHub MCP tool surface has no `create_release`/`create_tag` equivalent either, and a
39
+ direct call to the GitHub REST API's `/releases` endpoint with a raw token is blocked by
40
+ the outbound proxy itself.
43
41
 
44
42
  Confirmed once (retried the tag push a second time "just in case" before accepting it).
45
- Don't retry either path a second time if `git push origin <tag>` 403s, or no
46
- release-creation tool is found in one `ToolSearch` pass, say so once and hand the user
47
- the two-minute browser-only path instead (open the repo's `/releases/new`, type the new
48
- tag name in the tag fieldGitHub creates it from the target branch on publish, no git
49
- command needed).
43
+ Don't retry either path a second time. Since 0.16.11 this is moot for releases:
44
+ `.github/workflows/release.yml` creates the tag, the GitHub Release and the npm publish
45
+ from GitHub Actions on every push to `main` (README, "Development" "Releasing"), so a
46
+ session never needs to push a tag at all merging the PR is the release. The one thing
47
+ still worth knowing: that workflow's own `git push origin HEAD:main` (the automatic
48
+ version-bump commit) and its tag run under the built-in `GITHUB_TOKEN`, which by design
49
+ triggers no further workflow runs, so a red `tests` run for that bump commit is not
50
+ "missing" — it is never scheduled; the PR run before the merge is the one that counted.
51
+
52
+ ## A throwaway probe must be pointed at a copy in a directory you have just verified with `pwd`, never at "the repo, probably"
53
+
54
+ While validating the auto-bump script for `release.yml`, a scratch run was meant to
55
+ execute against a temporary copy of the repo. It ran with the real checkout as its
56
+ working directory instead — a `cd` into the scratch path happened in a shell whose
57
+ working directory was reset between commands — and its `git add -A && git commit`,
58
+ `git tag v0.16.12` and two empty commits landed on the real branch and moved the real
59
+ local `v0.16.12` tag. Nothing was pushed, and `git reflog` plus a `git fetch --force` of
60
+ the tag from `origin` restored everything, but it was only noticed because `git status`
61
+ showed files the session had not edited.
62
+
63
+ Rule: a probe that runs `git commit`, `git tag`, `rm -rf`, or writes into the tree goes
64
+ in a directory created *and* verified in the same command (`cd "$DIR" && pwd && ...`),
65
+ not one assumed from an earlier `cd`; prefer a fixture built from a few `printf` lines
66
+ over a copy of the whole checkout (the copy carries the real `.git`, so a mistake there
67
+ is a mistake in the real history); and run `git status` on the real repo before
68
+ committing anything afterwards. `.claude/skills/destructive-operations/SKILL.md` says the
69
+ same thing for the tools themselves — it applies to the person testing them too.
50
70
 
51
71
  ## A quantitative test failing three different ways across fixture redesigns means the platform, not the fixture, is the problem
52
72
 
@@ -83,3 +103,31 @@ shallow clone leaves no tag reachable to diff against, which would make the test
83
103
  skip itself in CI, not fail). If this test ever needs to skip a genuinely changelog-less
84
104
  closed issue (a pure process note, a duplicate, a revert of an unreleased change), name the
85
105
  exemption in the test itself with a reason — don't just widen the regex or drop the check.
106
+
107
+ ## Automation that can publish must not take its "major" cue from text it did not write
108
+
109
+ On 2026-09-11, three routine Dependabot merges (`actions/upload-artifact` 4→7,
110
+ `actions/setup-node` 4→7, `dependabot/fetch-metadata` 2→3) were published to npm as
111
+ **1.0.0, 1.0.1 and 1.0.2**. The release pipeline (`release.yml`, since #129) resolves the next
112
+ version from PR labels; an autolabeler rule added the same day applied `major` to any PR whose
113
+ *body* contained the literal breaking-change marker. Dependabot PR bodies quote the upstream
114
+ project's release notes verbatim, and upload-artifact's v5.0.0 notes contain exactly that
115
+ phrase — about *their* Node runtime, nothing to do with this package. One label, three
116
+ accidental majors, in nine minutes, with every job green.
117
+
118
+ Two things made it worse than one bad rule: every chore merge released at all (so the first
119
+ accident was followed by two more before anyone looked), and nothing in the pipeline treated
120
+ "the major number changed" as different from any other bump.
121
+
122
+ Fixes (this commit): no autolabeler rule produces `major` any more; `chore`/`ci`/`docs`/
123
+ `dependencies` PRs are excluded from version resolution so they release nothing; `release.yml`
124
+ refuses to auto-bump across a major boundary regardless of labels; and the release job is
125
+ serialised (`concurrency`) so back-to-back merges cannot race on the bump push.
126
+
127
+ The general rule: a pipeline that publishes must never derive an irreversible decision (a
128
+ major bump, a publish, a tag) from text it did not author — PR bodies, commit messages and
129
+ release notes are quotations as often as they are statements. Match on labels a person
130
+ applied, or on files changed, and make the irreversible step refuse anything surprising rather
131
+ than assume the surprise was intended. And after wiring any such automation, watch the first
132
+ few real runs' *results* (npm, tags) rather than their exit codes: the three runs here were
133
+ "success" by every check the job had.