diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 9109913..97df00e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "watch", - "version": "0.3.0", + "version": "0.3.1", "description": "Give an agent video evidence: timestamped frames and native captions, with optional local WhisperX or Groq/OpenAI transcription.", "author": { "name": "Bradley Bonanno" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 936832f..7fedf36 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "watch", - "version": "0.3.0", + "version": "0.3.1", "description": "Give an agent video evidence: timestamped frames and native captions, with optional local WhisperX or Groq/OpenAI transcription.", "author": { "name": "Bradley Bonanno" diff --git a/AGENTS.md b/AGENTS.md index 6edc8ae..df4473c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,8 @@ Agent Skills package that gives an agent a video input. Installable across Claud - The product is the slash-command-invoked skill (`/watch [question]`), not a CLI. `scripts/watch.py` is implementation. Features must work across every harness the skill installs into, not just Claude Code. - **The skill is one self-contained folder: `skills/watch/`.** SKILL.md and `scripts/` are siblings inside it. This is what lets `npx skills add` copy a working skill as a unit — do NOT move SKILL.md or `scripts/` back to the repo root, or non-Claude installers will copy SKILL.md without the scripts. - **Path resolution is harness-agnostic.** SKILL.md resolves `SKILL_DIR` as the directory of the SKILL.md the model just Read, then runs `${SKILL_DIR}/scripts/...`. Do NOT reintroduce `${CLAUDE_SKILL_DIR}` (Claude-Code-only) — it is unset on Codex/Cursor/agents and breaks every script call there. -- **No `commands/` wrapper.** `/watch` is derived from SKILL.md frontmatter (`name: watch` + `user-invocable: true`). A separate command file creates a duplicate slash command. +- **No `commands/` wrapper.** `/watch` comes from `name: watch` (skills are user-invocable by default). A separate command file creates a duplicate slash command. +- **SKILL.md frontmatter uses only Agent Skills keys:** `name`, `description`, `license`, `allowed-tools`, `metadata`, `compatibility`. Claude's skill upload rejects anything else; the version lives in `metadata.version`. ## Install surfaces @@ -45,6 +46,6 @@ bash skills/watch/scripts/build-skill.sh # → dist/watch.skill ## Rules -- Keep the version in sync across `skills/watch/SKILL.md` (frontmatter), `.claude-plugin/plugin.json`, and `.codex-plugin/plugin.json` when cutting a release. +- Keep the version in sync across `skills/watch/SKILL.md` (`metadata.version`), `.claude-plugin/plugin.json`, and `.codex-plugin/plugin.json` when cutting a release. - Releasing: tag `vX.Y.Z` and push the tag; `.github/workflows/release.yml` builds `dist/watch.skill` and attaches it to the GitHub release. - Never commit real API keys or `.env` contents; keys live in `~/.config/watch/.env` (mode `0600`) at runtime. diff --git a/CHANGELOG.md b/CHANGELOG.md index e4473dd..f4d18ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `/watch` are documented here. +## [0.3.1] — Unreleased + +### Fixed +- `SKILL.md` frontmatter uses only Agent Skills keys, so `watch.skill` uploads to Claude (the upload rejected `version`, `argument-hint`, `user-invocable`, and other extra keys). The version moves to `metadata.version`. + ## [0.3.0] — 2026-09-25 ### Added diff --git a/skills/watch/SKILL.md b/skills/watch/SKILL.md index e6ab3ae..a738bf2 100644 --- a/skills/watch/SKILL.md +++ b/skills/watch/SKILL.md @@ -1,14 +1,10 @@ --- name: watch -version: "0.3.0" description: Watch a video (URL or local path). Downloads with yt-dlp, extracts auto-scaled frames with ffmpeg, pulls the transcript from captions (or local WhisperX / cloud Whisper fallback), and hands the result to the agent so it can answer questions about what's in the video. With a Gemini API key, Google's agentic video model watches the full video instead. -argument-hint: " [question]" -allowed-tools: Bash, Read, AskUserQuestion -homepage: https://github.com/bradautomates/claude-video -repository: https://github.com/bradautomates/claude-video -author: bradautomates license: MIT -user-invocable: true +allowed-tools: Bash, Read, AskUserQuestion +metadata: + version: "0.3.1" --- # /watch @@ -17,7 +13,7 @@ Run the bundled Python script. With the **gemini** engine (a `GEMINI_API_KEY` is ## Resolve the skill and interpreter -Set `SKILL_DIR` to the **absolute directory containing this SKILL.md that you just read**. `scripts/` is its sibling in every host; do not use a Claude-specific environment variable. Check that `SKILL_DIR/scripts/watch.py` exists. The whole `skills/watch/` folder must be installed, with one canonical skill and no command wrapper. +`SKILL_DIR` is the absolute directory containing this SKILL.md; `scripts/` sits beside it. Commands below use `python3` for macOS/Linux. On Windows, verify a working Python 3.10+ with `python --version` or `py -3 --version` and use that interpreter. `python3` is not always a Store alias; inspect the actual result. In PowerShell use `$SKILL_DIR` rather than Bash variable syntax, for example: diff --git a/tests/test_packaging.py b/tests/test_packaging.py index e03376c..eaa0b6f 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -10,9 +10,17 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] +def test_frontmatter_uses_only_agent_skills_keys(): + # claude.ai uploads reject any other top-level key. + text = (ROOT / 'skills/watch/SKILL.md').read_text(encoding='utf-8') + frontmatter = text.split('---\n')[1] + keys = {line.split(':')[0] for line in frontmatter.splitlines() if line and not line.startswith(' ')} + assert keys <= {'name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility'} + + def test_release_versions_match_canonical_skill(): skill = ROOT / 'skills/watch/SKILL.md' - version = re.search(r'^version: "([^"]+)"', skill.read_text(encoding='utf-8'), re.M).group(1) + version = re.search(r'^ version: "([^"]+)"', skill.read_text(encoding='utf-8'), re.M).group(1) for relative in ('.claude-plugin/plugin.json', '.codex-plugin/plugin.json'): assert json.loads((ROOT / relative).read_text(encoding='utf-8'))['version'] == version assert json.loads((ROOT / '.codex-plugin/plugin.json').read_text(encoding='utf-8'))['skills'] == './skills/'