From 271ab861f17baa39ea479899e93a60ffd3211ef4 Mon Sep 17 00:00:00 2001 From: Jelmer de Wit <1598297+jdwit@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:15:16 +0200 Subject: [PATCH 1/2] feat: add harness-agnostic ytstudio agent skill Author a model- and harness-agnostic SKILL.md (open SKILL standard) at skills/ytstudio/ that teaches any skill-aware agent to drive the CLI: prerequisites, the dry-run/--execute and -o json rules, and per-group usage with pointers rather than a full dump. Bundle the full command reference at skills/ytstudio/references/reference.md, generated from the live typer app via scripts/build_skill_reference.py and guarded against drift by tests/test_skill_reference.py. Add an example upload sidecar asset, a docs page, and README/docs links. Closes #29 --- README.md | 11 + docs/agent-skill.md | 51 + mkdocs.yml | 1 + scripts/build_skill_reference.py | 62 ++ skills/ytstudio/SKILL.md | 229 +++++ .../assets/upload-sidecar.example.yaml | 30 + skills/ytstudio/references/reference.md | 951 ++++++++++++++++++ tests/test_skill_reference.py | 31 + 8 files changed, 1366 insertions(+) create mode 100644 docs/agent-skill.md create mode 100644 scripts/build_skill_reference.py create mode 100644 skills/ytstudio/SKILL.md create mode 100644 skills/ytstudio/assets/upload-sidecar.example.yaml create mode 100644 skills/ytstudio/references/reference.md create mode 100644 tests/test_skill_reference.py diff --git a/README.md b/README.md index ba20b2c..0232f97 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,17 @@ command line. See the [full documentation](https://jdwit.github.io/ytstudio-cli/) for installation, OAuth setup, and the command reference. +## Agent skill + +ytstudio ships a harness-agnostic [agent skill](skills/ytstudio/SKILL.md) that +teaches any skill-aware AI agent to operate a channel through this CLI, following +the open [SKILL standard](https://agentskills.io). A public repo with a +`SKILL.md` is already a published skill, so point your agent runtime (or a +registry like [skills.sh](https://skills.sh)) at the `skills/ytstudio/` subpath. +The bundled command reference is generated from the CLI; see +[Agent skill](https://jdwit.github.io/ytstudio-cli/agent-skill/) for how to keep +it in sync. + ## Development Clone the repo, sync dev dependencies, and install the pre-commit hook so diff --git a/docs/agent-skill.md b/docs/agent-skill.md new file mode 100644 index 0000000..312f137 --- /dev/null +++ b/docs/agent-skill.md @@ -0,0 +1,51 @@ +# Agent skill + +ytstudio ships a model- and harness-agnostic **agent skill** so any skill-aware +AI agent can operate a YouTube channel through the CLI. It follows the open +[SKILL standard](https://agentskills.io): a folder with a `SKILL.md` (YAML +frontmatter plus instructions) and optional bundled resources. + +The skill lives in this repository at +[`skills/ytstudio/`](https://github.com/jdwit/ytstudio-cli/tree/main/skills/ytstudio): + +``` +skills/ytstudio/ +├── SKILL.md # frontmatter + instructions +├── references/ +│ └── reference.md # full command reference (generated) +└── assets/ + └── upload-sidecar.example.yaml # example upload sidecar +``` + +## Using it + +A public GitHub repo containing a `SKILL.md` is already a published skill, so no +separate package or repo is needed. Point your agent runtime (or a registry such +as [skills.sh](https://skills.sh)) at the `skills/ytstudio/` subpath; monorepo +skills in a subfolder are supported by the spec. + +The skill itself is vendor-neutral: every instruction is a shell command, with +no MCP server and nothing tied to a specific agent platform. It stresses the two +rules that matter most when an agent drives the CLI: pass `-o json` for parseable +output, and that mutating commands are dry-run by default until re-run with +`--execute`. + +## Keeping the reference in sync + +`skills/ytstudio/references/reference.md` is the full command reference. Unlike +the site's [command reference](reference.md) (built on the fly by mkdocs and +git-ignored), the skill copy is checked into git because it travels with the +skill when installed from the repo, so it can drift from the CLI. + +Two things keep it honest, both driven from the same typer app as the rest of +the docs: + +- Regenerate it after any change to the CLI surface: + + ```bash + uv run python scripts/build_skill_reference.py + ``` + +- CI guards against drift: `tests/test_skill_reference.py` re-renders the + reference and fails if the committed copy is stale, pointing at the command + above. diff --git a/mkdocs.yml b/mkdocs.yml index e3b3e8c..504dc20 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -102,6 +102,7 @@ nav: - Reference: - Commands: reference.md - API quota: api-quota.md + - Agent skill: agent-skill.md extra: social: diff --git a/scripts/build_skill_reference.py b/scripts/build_skill_reference.py new file mode 100644 index 0000000..7e8fd61 --- /dev/null +++ b/scripts/build_skill_reference.py @@ -0,0 +1,62 @@ +"""Regenerate the agent skill's bundled command reference from the live typer app. + +The skill at ``skills/ytstudio/`` ships a trimmed command reference so an agent +can read the full flag surface on demand without running ``--help`` for every +command. Unlike ``docs/reference.md`` (built on the fly by mkdocs and +git-ignored), this file is checked into git because it travels with the skill +when it is installed from the repo. It must therefore be kept in sync with the +CLI: ``tests/test_skill_reference.py`` fails if the committed file drifts. + +Run after changing the CLI surface: + + uv run python scripts/build_skill_reference.py +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +REFERENCE = REPO_ROOT / "skills" / "ytstudio" / "references" / "reference.md" + +BANNER = ( + "\n\n" +) + + +def render() -> str: + """Return the command reference markdown rendered from the typer app.""" + # typer ships its own CLI; `python -m typer` avoids depending on a + # console-script being on PATH, mirroring scripts/build_docs_reference.py. + cmd = [ + sys.executable, + "-m", + "typer", + "ytstudio.main", + "utils", + "docs", + "--name", + "ytstudio", + ] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + text = result.stdout + # Lift the top-level heading so it reads as a reference, not a command name. + if text.lstrip().startswith("# `ytstudio`"): + text = text.replace("# `ytstudio`", "# Command reference", 1) + # Exactly one trailing newline, matching the end-of-file-fixer pre-commit hook + # so the committed file and a fresh render stay byte-identical. + return BANNER + text.strip() + "\n" + + +def main() -> None: + REFERENCE.parent.mkdir(parents=True, exist_ok=True) + REFERENCE.write_text(render()) + print(f"wrote {REFERENCE.relative_to(REPO_ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/skills/ytstudio/SKILL.md b/skills/ytstudio/SKILL.md new file mode 100644 index 0000000..94d119b --- /dev/null +++ b/skills/ytstudio/SKILL.md @@ -0,0 +1,229 @@ +--- +name: ytstudio +description: Manage and automate a YouTube channel from the terminal with the ytstudio CLI - list and bulk-edit video metadata (search-replace titles/descriptions/tags), upload videos from YAML sidecars, query channel and per-video analytics, moderate comments, control live broadcasts, and run bulk playlist operations. Use when a task involves administering, scripting, or reporting on a YouTube channel rather than just watching or searching public videos. +license: MIT +metadata: + project: ytstudio-cli + repository: https://github.com/jdwit/ytstudio-cli +--- + +# ytstudio + +`ytstudio` is a command-line tool over the official YouTube Data and Analytics +APIs. It exists to do at scale what YouTube Studio's web UI makes you click +through one item at a time: bulk metadata edits, batch uploads, scripted +analytics, comment moderation, live broadcast control, and playlist operations. + +This skill teaches you to drive it. It is harness-agnostic: every instruction is +a shell command. + +## Two things to know first + +These two rules apply to almost every command and prevent the most common +mistakes: + +1. **Ask for JSON.** Read commands print a human table by default. Pass + `-o json` (alias for `--output json`) to get parseable output. Some commands + also support `-o csv`. The auth/setup commands (`init`, `login`, `status`) + have no JSON mode. +2. **Mutations are dry-run by default.** Every command that changes the channel + (`update`, `search-replace`, `upload`, comment moderation, livestream and + playlist writes) previews what it *would* do and changes nothing until you + re-run the exact command with `--execute`. Always preview first, show the + user the preview when consequential, then re-run with `--execute`. + +Treat write operations as costly and irreversible-ish: they consume API quota +(see [Quota](#quota-awareness)) and act on a real, public channel. + +## Prerequisites + +### Install + +`ytstudio` is on PyPI and needs Python 3.12+. Prefer an isolated install: + +```bash +uv tool install ytstudio-cli # recommended +# or: pipx install ytstudio-cli +# or: pip install --user ytstudio-cli +``` + +The CLI installs as `ytstudio`; `yts` is a short alias for the same entry point. +Verify with `ytstudio --version`. + +### One-time OAuth setup + +The user brings their own Google OAuth client (a "Desktop app" client created in +the Google Cloud Console with the YouTube Data API v3 and YouTube Analytics API +enabled). With the downloaded client-secrets JSON: + +```bash +ytstudio init --client-secrets path/to/client_secret.json +ytstudio login # opens a browser to authorize +ytstudio status # confirms the authenticated channel +``` + +On a headless box, use `ytstudio login --headless`: it prints a URL to open in +any browser, then you paste the failed `127.0.0.1` redirect URL back in. + +Credentials live owner-only under `~/.config/ytstudio-cli/`. This step is +one-shot per channel; do not re-run it unless auth is actually broken. + +If `ytstudio status` reports no authenticated channel, stop and ask the user to +complete OAuth setup; you cannot do the browser consent for them. + +### Multiple channels (profiles) + +One install can hold several channels, each a named profile. Commands act on the +**active** profile unless overridden: + +```bash +ytstudio profile list # active profile is marked +ytstudio profile use work # switch active profile +YTSTUDIO_PROFILE=work ytstudio videos list # override for one command (scripting) +``` + +Use the `YTSTUDIO_PROFILE=` env override when scripting so you never mutate +the wrong channel by relying on global state. + +## Command groups + +Below is the minimum to operate each area. For the complete flag surface of any +command, read [references/reference.md](references/reference.md) on demand +rather than guessing - or run `ytstudio --help`. + +### videos - the core use case + +```bash +ytstudio videos list -n 100 -o json # recent uploads, parseable +ytstudio videos list --scheduled # only future-dated publishes +ytstudio videos show -o json # full metadata for one video +ytstudio videos categories # category ids assignable on upload + +# Single-video edit (dry-run, then --execute) +ytstudio videos update --title "New title" +ytstudio videos update --tags one,two,three --execute + +# Bulk search-replace across the channel (dry-run, then --execute) +ytstudio videos search-replace -s "2024" -r "2025" -f title +ytstudio videos search-replace -s 'season \d' -r 'season X' -f title --regex --execute +``` + +`search-replace` requires `-s/--search`, `-r/--replace`, and `-f/--field` +(`title` or `description`); `--limit` caps how many matches it acts on (default +10). Preview the dry-run, confirm the match set is what the user intended, then +add `--execute`. + +### videos upload - batch upload from YAML sidecars + +`ytstudio videos upload ` pairs each video file with a sibling YAML +sidecar of the same basename, validates everything, and uploads. Dry-run by +default. See [assets/upload-sidecar.example.yaml](assets/upload-sidecar.example.yaml) +for the sidecar schema (`title`, `description`, `privacy`, `tags`, +`category_id` (required), languages, `made_for_kids`, optional `publish_at`). + +```bash +ytstudio videos upload ./outbox # validate + preview +ytstudio videos upload ./outbox --execute --max 3 # upload, capped for quota +``` + +Uploads are resumable: after each success the sidecar is patched with `video_id` +and `uploaded_at`, so re-running only retries sidecars that lack a `video_id`. +Use `--max` to bound a run because uploads are quota-heavy (~1600 units each). + +### analytics - reporting (read-only) + +```bash +ytstudio analytics overview -d 28 -o json # channel overview, last 28 days +ytstudio analytics video -o json # per-video analytics +ytstudio analytics metrics # discoverable metric names +ytstudio analytics dimensions # discoverable dimension names + +# Custom query straight against the Analytics API reports.query endpoint: +ytstudio analytics query -m views,likes -d day --days 7 -o json +ytstudio analytics query -m views -d country --sort -views -n 10 -o json +ytstudio analytics query -m views -d insightTrafficSourceType -f video== -o json +``` + +`analytics query` needs `-m/--metrics`; `-d/--dimensions`, `-f/--filter` +(`key==value`, repeatable), `--sort` (prefix `-` for descending), `-n/--limit`, +and date range (`--days` or `-s/-e` start/end) are optional. When unsure which +metric or dimension exists, list them first with `analytics metrics` / +`analytics dimensions` instead of guessing names. + +### comments - moderation + +```bash +ytstudio comments list --status held -o json # the moderation queue +ytstudio comments list -v -n 50 -o json +ytstudio comments publish [ ...] # approve held +ytstudio comments reject --ban # reject (+ optional ban) +``` + +`publish`/`reject` take one or more comment ids. `--ban` on `reject` also bans +the author - only use it when the user explicitly asks to ban. + +### livestreams - broadcast lifecycle + +```bash +ytstudio livestreams list -s upcoming -o json +ytstudio livestreams show --ingest -o json # ingest URL; key redacted +ytstudio livestreams schedule -t "Title" --scheduled-start 2026-07-01T19:00:00+02:00 --execute +ytstudio livestreams start --to testing # or --to live +ytstudio livestreams stop +ytstudio livestreams update --privacy unlisted --execute +``` + +`schedule`/`update` are dry-run until `--execute`. `livestreams show --show-key` +reveals the stream key - treat any such output as a secret and never echo it +into logs or chat. `start --to live` publishes to viewers; prefer `--to testing` +unless the user wants to go live immediately. + +### playlists - bulk operations + +```bash +ytstudio playlists list -o json +ytstudio playlists items -o json +ytstudio playlists create -t "Title" --privacy unlisted --execute +ytstudio playlists add --from-search "topic" -n 20 --execute # search costs 100 units/call +ytstudio playlists add -v -v --execute +ytstudio playlists reorder --by views --order desc --execute +ytstudio playlists remove -v --execute +ytstudio playlists delete --execute -y # -y skips the prompt +``` + +All playlist writes are dry-run until `--execute`. `delete` also prompts for +confirmation unless `-y/--yes` is passed; only add `-y` when running +non-interactively and the user has confirmed the deletion. + +## Quota awareness + +The YouTube Data API has a default budget of 10,000 units/day per project, +resetting at midnight Pacific. Rough costs: + +| Operation | Cost | +|---|---| +| Read (list/show videos, comments, playlists; analytics) | ~1 unit | +| Write (update video, moderate comment, playlist insert/reorder, schedule broadcast) | ~50 units | +| Search (`playlists add --from-search`) | ~100 units/call | +| Upload (`videos upload`) | ~1600 units | + +Before kicking off a large bulk run (e.g. `search-replace` over hundreds of +videos, or several uploads), estimate the cost and warn the user if it could +exhaust the daily quota. A `quotaExceeded` response (HTTP 403) means the budget +is spent until the next reset; long-running jobs (`videos upload`) stop cleanly +and report how many succeeded so they can be resumed later. + +## Recommended workflow for an agent + +1. Confirm setup once with `ytstudio status` (and `profile list` if multiple + channels may be in play). Select the channel with `YTSTUDIO_PROFILE=` when + scripting. +2. Gather state with read commands using `-o json` and parse the result. +3. For any change, run the command without `--execute` first, inspect the + preview, and surface it to the user when the change is consequential or bulk. +4. Re-run the identical command with `--execute` to apply. +5. Mind the quota for bulk and upload operations. + +When a flag or behavior is unclear, consult +[references/reference.md](references/reference.md) or `--help` rather than +assuming. diff --git a/skills/ytstudio/assets/upload-sidecar.example.yaml b/skills/ytstudio/assets/upload-sidecar.example.yaml new file mode 100644 index 0000000..a9e1932 --- /dev/null +++ b/skills/ytstudio/assets/upload-sidecar.example.yaml @@ -0,0 +1,30 @@ +# Example upload sidecar for `ytstudio videos upload`. +# +# Place this next to a video file with the SAME basename, e.g. +# outbox/holiday.mp4 +# outbox/holiday.yaml <- this file +# outbox/holiday.jpg <- optional thumbnail (.jpg/.png, max 2 MB), same basename +# +# Then preview, then upload: +# ytstudio videos upload ./outbox # dry-run +# ytstudio videos upload ./outbox --execute # actually upload + +title: Holiday recap 2026 +description: | + Multi-line description. + Blank lines and Markdown-ish text are fine. +privacy: private # private | unlisted | public +tags: [travel, vlog] +category_id: "22" # Required. Run `ytstudio videos categories` for the list. +default_language: en +default_audio_language: en +made_for_kids: false + +# Optional. Must be timezone-aware and in the future; setting it forces +# privacy=private, the only state YouTube accepts for scheduled releases. +# publish_at: 2026-07-01T10:00:00+02:00 + +# After a successful upload the CLI patches these back into the sidecar so a +# re-run skips already-uploaded videos. Leave them out for a fresh upload. +# video_id: dQw4w9WgXcQ +# uploaded_at: 2026-07-01T08:00:05+00:00 diff --git a/skills/ytstudio/references/reference.md b/skills/ytstudio/references/reference.md new file mode 100644 index 0000000..e5c4baa --- /dev/null +++ b/skills/ytstudio/references/reference.md @@ -0,0 +1,951 @@ + + +# Command reference + +Manage your YouTube channel from the terminal + +**Usage**: + +```console +$ ytstudio [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `-v, --version`: Show version +* `--install-completion`: Install completion for the current shell. +* `--show-completion`: Show completion for the current shell, to copy it or customize the installation. +* `--help`: Show this message and exit. + +**Commands**: + +* `init`: Initialize with Google OAuth credentials +* `login`: Authenticate with YouTube via OAuth +* `status`: Show current authentication status +* `videos`: Video management commands +* `analytics`: Analytics commands +* `comments`: Comment commands +* `livestreams`: Live broadcast management (schedule,... +* `playlists`: Playlist management commands +* `profile`: Manage credential profiles (one per... + +## `ytstudio init` + +Initialize with Google OAuth credentials + +**Usage**: + +```console +$ ytstudio init [OPTIONS] +``` + +**Options**: + +* `-c, --client-secrets TEXT`: Path to Google OAuth client secrets JSON file +* `--help`: Show this message and exit. + +## `ytstudio login` + +Authenticate with YouTube via OAuth + +**Usage**: + +```console +$ ytstudio login [OPTIONS] +``` + +**Options**: + +* `--headless`: Authenticate by pasting a redirect URL from another browser +* `--help`: Show this message and exit. + +## `ytstudio status` + +Show current authentication status + +**Usage**: + +```console +$ ytstudio status [OPTIONS] +``` + +**Options**: + +* `--help`: Show this message and exit. + +## `ytstudio videos` + +Video management commands + +**Usage**: + +```console +$ ytstudio videos [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--help`: Show this message and exit. + +**Commands**: + +* `list`: List your YouTube videos +* `show`: Show details for a specific video +* `update`: Update a video's metadata +* `search-replace`: Bulk update videos using search and replace +* `categories`: List YouTube video categories assignable... +* `upload`: Upload one or more videos described by... + +### `ytstudio videos list` + +List your YouTube videos + +**Usage**: + +```console +$ ytstudio videos list [OPTIONS] +``` + +**Options**: + +* `-n, --limit INTEGER`: Number of videos to list [default: 20] +* `-p, --page-token TEXT`: Page token for pagination +* `-s, --sort TEXT`: Sort by: date, views, likes [default: date] +* `-o, --output TEXT`: Output format: table, json, csv [default: table] +* `--audio-lang TEXT`: Filter by audio language (e.g., en, nl) +* `--meta-lang TEXT`: Filter by metadata language (e.g., en, nl) +* `--has-localization TEXT`: Filter by available translation (e.g., en, nl) +* `--scheduled`: Only show videos scheduled for future publish +* `--help`: Show this message and exit. + +### `ytstudio videos show` + +Show details for a specific video + +**Usage**: + +```console +$ ytstudio videos show [OPTIONS] VIDEO_ID +``` + +**Arguments**: + +* `VIDEO_ID`: Video ID [required] + +**Options**: + +* `-o, --output TEXT`: Output format: table, json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio videos update` + +Update a video's metadata + +**Usage**: + +```console +$ ytstudio videos update [OPTIONS] VIDEO_ID +``` + +**Arguments**: + +* `VIDEO_ID`: Video ID [required] + +**Options**: + +* `-t, --title TEXT`: New title +* `-d, --description TEXT`: New description +* `--tags TEXT`: Comma-separated tags +* `--execute`: Apply changes (default is dry-run) +* `--help`: Show this message and exit. + +### `ytstudio videos search-replace` + +Bulk update videos using search and replace + +**Usage**: + +```console +$ ytstudio videos search-replace [OPTIONS] +``` + +**Options**: + +* `-s, --search TEXT`: Text to search for [required] +* `-r, --replace TEXT`: Text to replace with [required] +* `-f, --field TEXT`: Field to update: title, description [required] +* `--regex`: Treat search as regex +* `-n, --limit INTEGER`: Max matches to find [default: 10] +* `--execute`: Apply changes (default is dry-run) +* `--help`: Show this message and exit. + +### `ytstudio videos categories` + +List YouTube video categories assignable to uploads + +**Usage**: + +```console +$ ytstudio videos categories [OPTIONS] +``` + +**Options**: + +* `-r, --region TEXT`: Region code (ISO 3166-1 alpha-2) [default: US] +* `-o, --output TEXT`: Output format: table, json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio videos upload` + +Upload one or more videos described by yaml sidecars. + +**Usage**: + +```console +$ ytstudio videos upload [OPTIONS] PATH +``` + +**Arguments**: + +* `PATH`: Video file or directory of videos with yaml sidecars [required] + +**Options**: + +* `--execute`: Actually upload (default is dry-run) +* `-m, --max INTEGER`: Maximum number of uploads in this run (0 = no limit) [default: 0] +* `--help`: Show this message and exit. + +## `ytstudio analytics` + +Analytics commands + +**Usage**: + +```console +$ ytstudio analytics [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--help`: Show this message and exit. + +**Commands**: + +* `overview`: Get channel overview analytics +* `video`: Get analytics for a specific video +* `query`: Run a custom analytics query with any... +* `metrics`: List available analytics metrics. +* `dimensions`: List available analytics dimensions. + +### `ytstudio analytics overview` + +Get channel overview analytics + +**Usage**: + +```console +$ ytstudio analytics overview [OPTIONS] +``` + +**Options**: + +* `-d, --days INTEGER`: Number of days to analyze [default: 28] +* `--compare / --no-compare`: Compare each metric to the previous equal-length window [default: compare] +* `-o, --output TEXT`: Output format: table, json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio analytics video` + +Get analytics for a specific video + +**Usage**: + +```console +$ ytstudio analytics video [OPTIONS] VIDEO_ID +``` + +**Arguments**: + +* `VIDEO_ID`: Video ID [required] + +**Options**: + +* `-d, --days INTEGER`: Number of days to analyze [default: 28] +* `-o, --output TEXT`: Output format: table, json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio analytics query` + +Run a custom analytics query with any metrics and dimensions. + +Direct access to the YouTube Analytics API reports.query endpoint. +Supports all available metrics and dimensions. + +Examples: + + ytstudio analytics query -m views,likes --dimensions day --days 7 + + ytstudio analytics query -m views,shares -d country --sort -views -n 10 + + ytstudio analytics query -m views,estimatedMinutesWatched -d video \ + --sort -views -n 5 -o json + + ytstudio analytics query -m videoThumbnailImpressions,videoThumbnailImpressionsClickRate \ + -d video --sort -videoThumbnailImpressions -n 10 + + ytstudio analytics query -m views -d insightTrafficSourceType \ + -f video==dMH0bHeiRNg --sort -views + +**Usage**: + +```console +$ ytstudio analytics query [OPTIONS] +``` + +**Options**: + +* `-m, --metrics TEXT`: Comma-separated metrics (e.g. views,likes,shares) [required] +* `-d, --dimensions TEXT`: Comma-separated dimensions (e.g. day,country) +* `-f, --filter TEXT`: Filter in key==value format (repeatable, e.g. -f video==ID -f country==NL) +* `-s, --start TEXT`: Start date (YYYY-MM-DD). Defaults to --days ago +* `-e, --end TEXT`: End date (YYYY-MM-DD). Defaults to today +* `--days INTEGER`: Number of days (used if --start not set) [default: 28] +* `--sort TEXT`: Sort field (prefix with - for descending) +* `-n, --limit INTEGER`: Maximum number of rows +* `--currency TEXT`: Currency code for revenue (e.g. EUR) +* `-o, --output TEXT`: Output format: table, json, csv [default: table] +* `--raw`: Show raw numbers instead of human-readable +* `--help`: Show this message and exit. + +### `ytstudio analytics metrics` + +List available analytics metrics. + +Examples: + + ytstudio analytics metrics + + ytstudio analytics metrics --group engagement + +**Usage**: + +```console +$ ytstudio analytics metrics [OPTIONS] +``` + +**Options**: + +* `-g, --group TEXT`: Filter by group +* `-o, --output TEXT`: Output format: table, json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio analytics dimensions` + +List available analytics dimensions. + +Examples: + + ytstudio analytics dimensions + + ytstudio analytics dimensions --group geographic + + ytstudio analytics dimensions country + +**Usage**: + +```console +$ ytstudio analytics dimensions [OPTIONS] [NAME] +``` + +**Arguments**: + +* `[NAME]`: Show details for a specific dimension + +**Options**: + +* `-g, --group TEXT`: Filter by group +* `-o, --output TEXT`: Output format: table, json [default: table] +* `--help`: Show this message and exit. + +## `ytstudio comments` + +Comment commands + +**Usage**: + +```console +$ ytstudio comments [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--help`: Show this message and exit. + +**Commands**: + +* `list`: List comments across channel or for a... +* `publish`: Publish held comments (approve for public... +* `reject`: Reject comments (hide from public display) +* `reply`: Reply to a top-level comment. + +### `ytstudio comments list` + +List comments across channel or for a specific video + +**Usage**: + +```console +$ ytstudio comments list [OPTIONS] +``` + +**Options**: + +* `-v, --video TEXT`: Filter by video ID +* `--status [published|held|spam]`: Moderation status: published, held, spam [default: published] +* `-n, --limit INTEGER`: Number of comments [default: 20] +* `-s, --sort [relevance|time]`: Sort order [default: time] +* `-o, --output TEXT`: Output format: table, json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio comments publish` + +Publish held comments (approve for public display) + +**Usage**: + +```console +$ ytstudio comments publish [OPTIONS] COMMENT_IDS... +``` + +**Arguments**: + +* `COMMENT_IDS...`: Comment IDs to publish [required] + +**Options**: + +* `--help`: Show this message and exit. + +### `ytstudio comments reject` + +Reject comments (hide from public display) + +**Usage**: + +```console +$ ytstudio comments reject [OPTIONS] COMMENT_IDS... +``` + +**Arguments**: + +* `COMMENT_IDS...`: Comment IDs to reject [required] + +**Options**: + +* `--ban`: Also ban the comment author +* `--help`: Show this message and exit. + +### `ytstudio comments reply` + +Reply to a top-level comment. + +Replies are flat on YouTube: COMMENT_ID must be a top-level comment id +(the id shown by 'comments list'), not the id of another reply. + +**Usage**: + +```console +$ ytstudio comments reply [OPTIONS] COMMENT_ID +``` + +**Arguments**: + +* `COMMENT_ID`: Top-level comment ID to reply to (from 'comments list') [required] + +**Options**: + +* `-t, --text TEXT`: Reply text [required] +* `--help`: Show this message and exit. + +## `ytstudio livestreams` + +Live broadcast management (schedule, start, stop, update) + +**Usage**: + +```console +$ ytstudio livestreams [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--help`: Show this message and exit. + +**Commands**: + +* `list`: List your YouTube live broadcasts. +* `show`: Show details for a specific broadcast. +* `start`: Transition a broadcast to testing or live. +* `stop`: Stop a live broadcast (transition to... +* `schedule`: Schedule a new live broadcast. +* `update`: Update a broadcast's metadata or settings... + +### `ytstudio livestreams list` + +List your YouTube live broadcasts. + +**Usage**: + +```console +$ ytstudio livestreams list [OPTIONS] +``` + +**Options**: + +* `-s, --status [all|active|completed|upcoming]`: Filter: all, upcoming, active, completed [default: upcoming] +* `-n, --limit INTEGER RANGE`: Number of broadcasts [default: 20; 1<=x<=50] +* `-p, --page-token TEXT`: Page token for pagination +* `-o, --output [table|json]`: Output format: table or json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio livestreams show` + +Show details for a specific broadcast. + +**Usage**: + +```console +$ ytstudio livestreams show [OPTIONS] BROADCAST_ID +``` + +**Arguments**: + +* `BROADCAST_ID`: Broadcast ID [required] + +**Options**: + +* `--ingest`: Also fetch and display the bound stream's ingest URL (key is redacted by default) +* `--show-key`: Reveal the bound stream key (implies --ingest). Treat output as a secret. +* `-o, --output [table|json]`: Output format: table or json [default: table] +* `--help`: Show this message and exit. + +### `ytstudio livestreams start` + +Transition a broadcast to testing or live. + +**Usage**: + +```console +$ ytstudio livestreams start [OPTIONS] BROADCAST_ID +``` + +**Arguments**: + +* `BROADCAST_ID`: Broadcast ID [required] + +**Options**: + +* `--to [testing|live]`: Target state: testing (monitor only) or live (publish to viewers). [default: live] +* `--help`: Show this message and exit. + +### `ytstudio livestreams stop` + +Stop a live broadcast (transition to complete). + +**Usage**: + +```console +$ ytstudio livestreams stop [OPTIONS] BROADCAST_ID +``` + +**Arguments**: + +* `BROADCAST_ID`: Broadcast ID [required] + +**Options**: + +* `--help`: Show this message and exit. + +### `ytstudio livestreams schedule` + +Schedule a new live broadcast. + +**Usage**: + +```console +$ ytstudio livestreams schedule [OPTIONS] +``` + +**Options**: + +* `-t, --title TEXT`: Broadcast title [required] +* `--scheduled-start TEXT`: Scheduled start time, ISO 8601 (e.g. 2026-06-01T19:00:00+02:00) [required] +* `--scheduled-end TEXT`: Scheduled end time, ISO 8601 +* `-d, --description TEXT`: Broadcast description +* `--privacy [public|private|unlisted]`: public, private, or unlisted [default: public] +* `--made-for-kids / --not-made-for-kids`: COPPA self-declaration; required by YouTube on every broadcast. [default: not-made-for-kids] +* `--execute`: Create the broadcast (default is dry-run preview) +* `--help`: Show this message and exit. + +### `ytstudio livestreams update` + +Update a broadcast's metadata or settings (partial update). + +Note: liveBroadcasts.update only accepts privacyStatus under status; the +made-for-kids designation is set at schedule time and managed on the +resulting video resource afterwards. + +**Usage**: + +```console +$ ytstudio livestreams update [OPTIONS] BROADCAST_ID +``` + +**Arguments**: + +* `BROADCAST_ID`: Broadcast ID [required] + +**Options**: + +* `-t, --title TEXT`: New title +* `-d, --description TEXT`: New description +* `--privacy [public|private|unlisted]`: New privacy status +* `--scheduled-start TEXT`: New scheduled start, ISO 8601 +* `--scheduled-end TEXT`: New scheduled end, ISO 8601 +* `--auto-start / --no-auto-start`: Auto-start when stream begins +* `--auto-stop / --no-auto-stop`: Auto-stop when stream ends +* `--dvr / --no-dvr`: Enable DVR controls +* `--embed / --no-embed`: Allow embedding +* `--record-from-start / --no-record-from-start`: Record broadcast for archive +* `--closed-captions [closedCaptionsDisabled|closedCaptionsHttpPost|closedCaptionsEmbedded]`: Closed-caption mode +* `--latency [normal|low|ultraLow]`: Latency: normal, low, ultraLow +* `--projection [rectangular|360]`: Projection: rectangular or 360 +* `--execute`: Apply changes (default is dry-run) +* `--help`: Show this message and exit. + +## `ytstudio playlists` + +Playlist management commands + +**Usage**: + +```console +$ ytstudio playlists [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--help`: Show this message and exit. + +**Commands**: + +* `list`: List your playlists. +* `show`: Show details for a specific playlist. +* `create`: Create a new playlist. +* `update`: Update a playlist's metadata. +* `delete`: Delete a playlist. +* `items`: List the items in a playlist. +* `add`: Add videos to a playlist by ID or search... +* `remove`: Remove items from a playlist by item id or... +* `reorder`: Reorder a playlist by one of views, likes,... + +### `ytstudio playlists list` + +List your playlists. + +**Usage**: + +```console +$ ytstudio playlists list [OPTIONS] +``` + +**Options**: + +* `-n, --limit INTEGER`: Number of playlists to list [default: 50] +* `-p, --page-token TEXT`: Page token for pagination +* `-s, --sort TEXT`: Sort by: date, title, count [default: date] +* `-o, --output TEXT`: Output format: table, json, csv [default: table] +* `--help`: Show this message and exit. + +### `ytstudio playlists show` + +Show details for a specific playlist. + +**Usage**: + +```console +$ ytstudio playlists show [OPTIONS] PLAYLIST_ID +``` + +**Arguments**: + +* `PLAYLIST_ID`: Playlist ID [required] + +**Options**: + +* `-o, --output TEXT`: Output format: table, json [default: table] +* `-i, --items`: Also fetch and render the first 50 items +* `--help`: Show this message and exit. + +### `ytstudio playlists create` + +Create a new playlist. + +**Usage**: + +```console +$ ytstudio playlists create [OPTIONS] +``` + +**Options**: + +* `-t, --title TEXT`: Playlist title [required] +* `-d, --description TEXT`: Playlist description +* `--privacy TEXT`: Privacy: private, public, unlisted [default: private] +* `--language TEXT`: Default language tag (e.g. en, nl) +* `--execute`: Create the playlist (default dry-run) +* `--help`: Show this message and exit. + +### `ytstudio playlists update` + +Update a playlist's metadata. + +**Usage**: + +```console +$ ytstudio playlists update [OPTIONS] PLAYLIST_ID +``` + +**Arguments**: + +* `PLAYLIST_ID`: Playlist ID [required] + +**Options**: + +* `-t, --title TEXT`: New title +* `-d, --description TEXT`: New description +* `--privacy TEXT`: New privacy: private, public, unlisted +* `--language TEXT`: New default language +* `--execute`: Apply changes (default is dry-run) +* `--help`: Show this message and exit. + +### `ytstudio playlists delete` + +Delete a playlist. + +**Usage**: + +```console +$ ytstudio playlists delete [OPTIONS] PLAYLIST_ID +``` + +**Arguments**: + +* `PLAYLIST_ID`: Playlist ID [required] + +**Options**: + +* `--execute`: Apply deletion (default is dry-run) +* `-y, --yes`: Skip confirmation prompt +* `--help`: Show this message and exit. + +### `ytstudio playlists items` + +List the items in a playlist. + +**Usage**: + +```console +$ ytstudio playlists items [OPTIONS] PLAYLIST_ID +``` + +**Arguments**: + +* `PLAYLIST_ID`: Playlist ID [required] + +**Options**: + +* `-n, --limit INTEGER`: Number of items to list [default: 50] +* `-p, --page-token TEXT`: Page token for pagination +* `-o, --output TEXT`: Output format: table, json, csv [default: table] +* `--help`: Show this message and exit. + +### `ytstudio playlists add` + +Add videos to a playlist by ID or search query. + +If --video is passed more than once, every ID is added. --from-search runs +search().list(forMine=True, type=video, q=...) and adds up to --limit hits. +Quota is 50 units per inserted video; a running counter is printed. + +**Usage**: + +```console +$ ytstudio playlists add [OPTIONS] PLAYLIST_ID +``` + +**Arguments**: + +* `PLAYLIST_ID`: Playlist ID [required] + +**Options**: + +* `-v, --video TEXT`: Video ID to add (repeatable) +* `--from-search TEXT`: Add the top results from a search of your videos +* `-n, --limit INTEGER`: Max videos to add per invocation (search mode) [default: 50] +* `--position INTEGER`: Insert at this position +* `--note TEXT`: Set contentDetails.note on each item +* `--execute`: Apply changes (default is dry-run) +* `--help`: Show this message and exit. + +### `ytstudio playlists remove` + +Remove items from a playlist by item id or video id. + +**Usage**: + +```console +$ ytstudio playlists remove [OPTIONS] PLAYLIST_ID +``` + +**Arguments**: + +* `PLAYLIST_ID`: Playlist ID [required] + +**Options**: + +* `-i, --item TEXT`: Playlist item ID (PLPLI...) to remove (repeatable) +* `-v, --video TEXT`: Video ID to remove (repeatable). Resolves to all playlist items for that video; duplicates are all removed. +* `--execute`: Apply changes (default is dry-run) +* `--help`: Show this message and exit. + +### `ytstudio playlists reorder` + +Reorder a playlist by one of views, likes, published, title. + +**Usage**: + +```console +$ ytstudio playlists reorder [OPTIONS] PLAYLIST_ID +``` + +**Arguments**: + +* `PLAYLIST_ID`: Playlist ID [required] + +**Options**: + +* `--by TEXT`: Sort by: views, likes, published, title [default: views] +* `--order TEXT`: Order: asc, desc [default: desc] +* `--execute`: Apply changes (default is dry-run) +* `--help`: Show this message and exit. + +## `ytstudio profile` + +Manage credential profiles (one per YouTube channel) + +**Usage**: + +```console +$ ytstudio profile [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `--help`: Show this message and exit. + +**Commands**: + +* `add`: Authenticate a new profile and switch to it +* `list`: List configured profiles +* `use`: Switch the active profile +* `status`: Show authentication status for a profile +* `remove`: Remove a profile and its stored credentials + +### `ytstudio profile add` + +Authenticate a new profile and switch to it + +**Usage**: + +```console +$ ytstudio profile add [OPTIONS] NAME +``` + +**Arguments**: + +* `NAME`: Name for the new profile [required] + +**Options**: + +* `--headless`: Authenticate by pasting a redirect URL from another browser +* `--help`: Show this message and exit. + +### `ytstudio profile list` + +List configured profiles + +**Usage**: + +```console +$ ytstudio profile list [OPTIONS] +``` + +**Options**: + +* `--help`: Show this message and exit. + +### `ytstudio profile use` + +Switch the active profile + +**Usage**: + +```console +$ ytstudio profile use [OPTIONS] NAME +``` + +**Arguments**: + +* `NAME`: Profile to make active [required] + +**Options**: + +* `--help`: Show this message and exit. + +### `ytstudio profile status` + +Show authentication status for a profile + +**Usage**: + +```console +$ ytstudio profile status [OPTIONS] [NAME] +``` + +**Arguments**: + +* `[NAME]`: Profile name (default: active) + +**Options**: + +* `--help`: Show this message and exit. + +### `ytstudio profile remove` + +Remove a profile and its stored credentials + +**Usage**: + +```console +$ ytstudio profile remove [OPTIONS] NAME +``` + +**Arguments**: + +* `NAME`: Profile to remove [required] + +**Options**: + +* `-f, --force`: Skip confirmation +* `--help`: Show this message and exit. diff --git a/tests/test_skill_reference.py b/tests/test_skill_reference.py new file mode 100644 index 0000000..c90b3a1 --- /dev/null +++ b/tests/test_skill_reference.py @@ -0,0 +1,31 @@ +"""The skill's bundled command reference must track the CLI surface. + +``skills/ytstudio/references/reference.md`` is checked into git (it ships with +the skill), so it can silently drift when commands or flags change. This test +re-renders it from the live typer app and fails if the committed copy is stale, +pointing at the regeneration command. +""" + +import importlib.util +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPT = REPO_ROOT / "scripts" / "build_skill_reference.py" +REFERENCE = REPO_ROOT / "skills" / "ytstudio" / "references" / "reference.md" + + +def _load_builder(): + spec = importlib.util.spec_from_file_location("build_skill_reference", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_skill_reference_is_in_sync(): + builder = _load_builder() + expected = builder.render() + actual = REFERENCE.read_text() + assert actual == expected, ( + "skills/ytstudio/references/reference.md is out of date.\n" + "Regenerate it with: uv run python scripts/build_skill_reference.py" + ) From dcba3127907c2f7cd05a8b967c50858481c46470 Mon Sep 17 00:00:00 2001 From: Jelmer de Wit <1598297+jdwit@users.noreply.github.com> Date: Wed, 17 Jun 2026 17:35:01 +0200 Subject: [PATCH 2/2] docs: clarify immediate ytstudio mutations --- docs/agent-skill.md | 9 +++++---- skills/ytstudio/SKILL.md | 35 ++++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/docs/agent-skill.md b/docs/agent-skill.md index 312f137..b0a9ea6 100644 --- a/docs/agent-skill.md +++ b/docs/agent-skill.md @@ -25,10 +25,11 @@ as [skills.sh](https://skills.sh)) at the `skills/ytstudio/` subpath; monorepo skills in a subfolder are supported by the spec. The skill itself is vendor-neutral: every instruction is a shell command, with -no MCP server and nothing tied to a specific agent platform. It stresses the two -rules that matter most when an agent drives the CLI: pass `-o json` for parseable -output, and that mutating commands are dry-run by default until re-run with -`--execute`. +no MCP server and nothing tied to a specific agent platform. It stresses the +rules that matter most when an agent drives the CLI: pass `-o json` for +parseable output, preview mutations that support `--execute`, and explicitly +confirm before immediate writes such as comment moderation/replies and +livestream start/stop. ## Keeping the reference in sync diff --git a/skills/ytstudio/SKILL.md b/skills/ytstudio/SKILL.md index 94d119b..5ab4750 100644 --- a/skills/ytstudio/SKILL.md +++ b/skills/ytstudio/SKILL.md @@ -26,12 +26,17 @@ mistakes: `-o json` (alias for `--output json`) to get parseable output. Some commands also support `-o csv`. The auth/setup commands (`init`, `login`, `status`) have no JSON mode. -2. **Mutations are dry-run by default.** Every command that changes the channel - (`update`, `search-replace`, `upload`, comment moderation, livestream and - playlist writes) previews what it *would* do and changes nothing until you +2. **Most mutations are dry-run by default.** Commands with `--execute` + (`videos update/search-replace/upload`, `livestreams schedule/update`, and + playlist writes) preview what they *would* do and change nothing until you re-run the exact command with `--execute`. Always preview first, show the user the preview when consequential, then re-run with `--execute`. +Some writes execute immediately because they have no `--execute`: comment +moderation/replies (`comments publish`, `comments reject`, `comments reply`) and +livestream transitions (`livestreams start`, `livestreams stop`). Treat these as +high-risk: confirm intent before running them. + Treat write operations as costly and irreversible-ish: they consume API quota (see [Quota](#quota-awareness)) and act on a real, public channel. @@ -155,12 +160,15 @@ metric or dimension exists, list them first with `analytics metrics` / ```bash ytstudio comments list --status held -o json # the moderation queue ytstudio comments list -v -n 50 -o json -ytstudio comments publish [ ...] # approve held -ytstudio comments reject --ban # reject (+ optional ban) +ytstudio comments publish [ ...] # approve held; executes immediately +ytstudio comments reject --ban # reject (+ optional ban); executes immediately +ytstudio comments reply -t "Thanks!" # executes immediately ``` -`publish`/`reject` take one or more comment ids. `--ban` on `reject` also bans -the author - only use it when the user explicitly asks to ban. +`publish`/`reject` take one or more comment ids and execute immediately (no +`--execute` dry-run). `reply` also posts immediately. Confirm the exact comment +ids/text first. `--ban` on `reject` also bans the author - only use it when the +user explicitly asks to ban. ### livestreams - broadcast lifecycle @@ -168,15 +176,16 @@ the author - only use it when the user explicitly asks to ban. ytstudio livestreams list -s upcoming -o json ytstudio livestreams show --ingest -o json # ingest URL; key redacted ytstudio livestreams schedule -t "Title" --scheduled-start 2026-07-01T19:00:00+02:00 --execute -ytstudio livestreams start --to testing # or --to live -ytstudio livestreams stop +ytstudio livestreams start --to testing # executes immediately; or --to live +ytstudio livestreams stop # executes immediately ytstudio livestreams update --privacy unlisted --execute ``` -`schedule`/`update` are dry-run until `--execute`. `livestreams show --show-key` -reveals the stream key - treat any such output as a secret and never echo it -into logs or chat. `start --to live` publishes to viewers; prefer `--to testing` -unless the user wants to go live immediately. +`schedule`/`update` are dry-run until `--execute`, but `start`/`stop` execute +immediately. `livestreams show --show-key` reveals the stream key - treat any +such output as a secret and never echo it into logs or chat. `start --to live` +publishes to viewers; prefer `--to testing` unless the user wants to go live +immediately. ### playlists - bulk operations