Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Eyes and ears for your AI agent. watch-cli composes `yt-dlp` + `ffmpeg` + a Whis
watch https://twitter.com/anyone/status/12345
```

Works on YouTube, X, LinkedIn, TikTok, Reddit, Vimeo, and Facebook. Login-walled posts (LinkedIn, private X, FB) fall back to your browser cookies automatically.
Works on YouTube, X, LinkedIn, TikTok, Reddit, Vimeo, and Facebook. Login-walled posts (LinkedIn, private X, FB) work with `WATCH_BROWSER=auto`, which reads cookies from a browser you are signed in to.

## What you can build

Expand Down Expand Up @@ -118,8 +118,8 @@ The agent then picks up `watch <url>` as a first-class command.
Pin a specific version:

```bash
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.3/install.sh \
| WATCH_CLI_VERSION=0.3.3 bash
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.4/install.sh \
| WATCH_CLI_VERSION=0.3.4 bash
```

Or from a clone:
Expand Down Expand Up @@ -257,11 +257,16 @@ to see what's behind the alias today.
Most YouTube / TikTok / Reddit / Vimeo / public X work without setup.
LinkedIn, private X posts, and Facebook need a session.

watch-cli tries every URL anonymously first. On a login wall it
retries with cookies from a signed-in browser on this machine
(Chrome → Firefox → Safari → Edge → Brave → Chromium). Just sign in
normally and re-run. `WATCH_BROWSER=none` turns the browser step off;
cookies are read locally by yt-dlp and never stored or uploaded.
watch-cli fetches every URL anonymously and never reads a browser
session on its own. For a login-walled URL, opt in per run:

```bash
WATCH_BROWSER=auto watch <url> # any signed-in browser: Chrome → Firefox → Safari → Edge → Brave → Chromium
WATCH_BROWSER=firefox watch <url> # one browser
```

Cookies are read from the local browser profile by yt-dlp, sent only to
that platform, and never stored or uploaded.

For servers / CI without browsers, pass a manual cookies file:

Expand Down
10 changes: 5 additions & 5 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ metadata:
bins:
- watch
label: "Install watch-cli (Homebrew)"
- id: release-0.3.3
- id: release-0.3.4
kind: shell
command: "curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.3/install.sh | WATCH_CLI_VERSION=0.3.3 bash"
command: "curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.4/install.sh | WATCH_CLI_VERSION=0.3.4 bash"
bins:
- watch
label: "Install watch-cli v0.3.3 (pinned release, SHA256-verified tarball)"
label: "Install watch-cli v0.3.4 (pinned release, SHA256-verified tarball)"
---

Watch any social video → get an architecture diagram, working component, runnable notebook, or step-by-step cheat sheet — automatically.
Expand All @@ -36,7 +36,7 @@ Reach for `watch` whenever the user gives you a video URL and wants you to do so
- The user asks to "implement", "clone", "build", or "replicate" what is on screen in a video.
- The user asks to "extract architecture from", "diagram", or "turn this paper talk into code" at a video URL.

Supported platforms: YouTube, X / Twitter, LinkedIn, TikTok, Vimeo, Reddit, Facebook. Every URL is fetched anonymously first. Only when the platform answers with a login wall does `watch` retry with the cookies of a browser the user is already signed in to on this machine (Chrome, Firefox, Safari, Edge, Brave, Chromium, in that order); `WATCH_BROWSER=none` turns that step off and `--cookies <file>` replaces it with an exported cookie file. Cookies are read from the local browser profile by yt-dlp and sent only to the platform that issued them; watch-cli never stores or uploads them.
Supported platforms: YouTube, X / Twitter, LinkedIn, TikTok, Vimeo, Reddit, Facebook. Every URL is fetched anonymously. A login-walled URL fails with `tag=download-auth`; watch-cli does not touch browser sessions on its own. If the user wants to use one, they opt in per run with `WATCH_BROWSER=auto` (or a browser name), which lets yt-dlp read cookies from the local browser profile and send them only to that platform; `--cookies <file>` uses an exported cookie file instead. Ask the user before setting either; never set them silently.

## What you get back

Expand Down Expand Up @@ -99,7 +99,7 @@ Pass `--no-cache` only when the source itself has changed. A failed transcriptio

- The video download goes to the platform hosting it, through yt-dlp.
- The extracted audio track is uploaded to Kyma API for transcription; frames and the video file stay on disk under `~/.watch-cli/archive`. With `--with-local` installed, transcription runs offline through whisper.cpp instead and nothing is uploaded.
- Browser cookies, when used, go only to the platform that set them (see above).
- Browser cookies are never read unless the user sets `WATCH_BROWSER` or passes `--cookies`; when they are, they go only to the platform that set them.
- Installation is pinned: the release installer downloads a tagged tarball and verifies its SHA256 against the checksum published on the same GitHub Release. Homebrew does the same through the formula's `sha256`.

Transcription runs through Kyma API. Get a key at https://kymaapi.com/?src=skill:watch and set `KYMA_API_KEY`; a one-hour video costs about $0.05.
36 changes: 19 additions & 17 deletions bin/dl-video
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
#
# Cookie fallback (4 tiers, tried in order):
# 1. Anonymous fetch — works for all public videos.
# 2. Auto-detect signed-in browser (Chrome → Firefox → Safari → Edge).
# Override via WATCH_BROWSER=chrome|firefox|safari|edge|brave|chromium.
# 2. Cookies from a signed-in browser, only when the user opts in with
# WATCH_BROWSER=auto (Chrome → Firefox → Safari → Edge → Brave →
# Chromium) or WATCH_BROWSER=<one of those>. Unset or "none": skipped.
# 3. Manual cookies file via --cookies <file> (Netscape format).
# 4. Graceful error with guidance — no silent failure.
#
Expand Down Expand Up @@ -147,19 +148,18 @@ if [[ -n "$COOKIES_FILE" ]]; then
exit 3
fi

# Tier 2: auto-detect signed-in browser.
# Order: Chrome → Firefox → Safari → Edge → Brave → Chromium.
# Override via env: WATCH_BROWSER=firefox. WATCH_BROWSER=none skips
# this tier entirely, so a login wall fails with tag=download-auth
# instead of touching any browser profile.
# Tier 2: cookies from a signed-in browser, opt-in only. A browser
# session is the user's identity on that platform, so watch-cli never
# reads one unless asked: WATCH_BROWSER=auto tries Chrome → Firefox →
# Safari → Edge → Brave → Chromium, WATCH_BROWSER=firefox tries that
# one. Unset (the default) or "none" skips this tier, and a login wall
# fails with tag=download-auth and a hint instead.
declare -a BROWSERS
if [[ "${WATCH_BROWSER:-}" == "none" ]]; then
BROWSERS=()
elif [[ -n "${WATCH_BROWSER:-}" ]]; then
BROWSERS=("$WATCH_BROWSER")
else
BROWSERS=(chrome firefox safari edge brave chromium)
fi
case "${WATCH_BROWSER:-}" in
""|none) BROWSERS=() ;;
auto) BROWSERS=(chrome firefox safari edge brave chromium) ;;
*) BROWSERS=("$WATCH_BROWSER") ;;
esac

# ${arr[@]+"${arr[@]}"} keeps `set -u` happy on an empty array (bash 3.2).
for BROWSER in ${BROWSERS[@]+"${BROWSERS[@]}"}; do
Expand All @@ -177,11 +177,13 @@ TAG="$(classify_failure)"
case "$TAG" in
download-auth)
cat >&2 <<ERR
[dl-video] error: download failed for $URL tag=download-auth — sign in to the platform in your browser and re-run, or pass --cookies <file>
[dl-video] error: download failed for $URL tag=download-auth — re-run with WATCH_BROWSER=auto to use a signed-in browser on this machine, or pass --cookies <file>

Three ways to fix:
1. Sign in to the platform in Chrome (or Firefox/Safari/Edge), then re-run.
watch-cli auto-reads cookies from any signed-in browser.
1. Sign in to the platform in Chrome (or Firefox/Safari/Edge), then re-run with
WATCH_BROWSER=auto watch <url>
Cookies are read from the local browser profile and sent only to that
platform. watch-cli never does this unless you set WATCH_BROWSER.

2. Export cookies.txt (Netscape format) and pass:
watch-cli <url> --cookies ~/path/to/cookies.txt
Expand Down
22 changes: 10 additions & 12 deletions docs/cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ For these, watch-cli needs to act as a signed-in user.
## Option 1 — sign in to a browser (easiest)

**You don't need to copy cookies by hand.** If you're already signed in
to the platform in **any** of these browsers, watch-cli reads the live
session directly. No F12, no DevTools, no extensions:
to the platform in **any** of these browsers, `WATCH_BROWSER=auto`
lets watch-cli read the live session directly. No F12, no DevTools, no
extensions. Nothing is read unless you set the variable:

- Chrome (default first try)
- Firefox
Expand All @@ -26,23 +27,20 @@ session directly. No F12, no DevTools, no extensions:
- Brave
- Chromium

No setup needed — just sign in normally and re-run `watch <url>`.

To force a specific browser:

```bash
WATCH_BROWSER=firefox watch <url>
WATCH_BROWSER=auto watch <url>
```

To never touch a browser profile (servers, shared machines, or when you
would rather see a login wall fail than have a session reused):
To use a specific browser:

```bash
WATCH_BROWSER=none watch <url>
WATCH_BROWSER=firefox watch <url>
```

The download then stops at `tag=download-auth` for login-walled URLs;
pass `--cookies <file>` if you still need them.
With `WATCH_BROWSER` unset (the default, and what `none` also means),
a login-walled URL stops at `tag=download-auth` and the error tells you
how to opt in; pass `--cookies <file>` if you would rather not touch a
browser profile at all.

### Why this works

Expand Down
2 changes: 1 addition & 1 deletion docs/exit-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ parser around its stderr. Documented exit codes are the first step:
| `0` | success | The pipeline completed without errors. In `watch`, both frame extraction and transcribe succeeded. | (no error output) | Consume the stdout payload. |
| `1` | general error | An uncategorized error that doesn't fit one of the more specific codes below. Should be rare; if you see it often, the case probably deserves its own row. | `[watch] error: <message>` | Log the stderr, treat as a real failure. |
| `2` | missing dependency | A required binary (`yt-dlp`, `ffmpeg`, `ffprobe`, `jq`, `curl`, `python3`) is not on `PATH`. | `[watch] error: ffmpeg not found on PATH. Install via 'brew install ffmpeg' or 'apt install ffmpeg'.` | Install the missing tool, then retry. Do not auto-retry. |
| `3` | download failed | `yt-dlp` returned a non-zero exit. The pipeline cannot continue without a video file. The stderr line includes a `tag=…` token so callers can grep without needing extra exit codes (see below). | `[watch] error: download failed for <url> tag=download-auth — sign in to the platform in your browser and re-run, or pass --cookies <file>` | Inspect the `tag=…` value. Auth → retry with cookies. Region → try VPN. Network → wait and retry. Other → file an issue. |
| `3` | download failed | `yt-dlp` returned a non-zero exit. The pipeline cannot continue without a video file. The stderr line includes a `tag=…` token so callers can grep without needing extra exit codes (see below). | `[watch] error: download failed for <url> tag=download-auth — re-run with WATCH_BROWSER=auto to use a signed-in browser on this machine, or pass --cookies <file>` | Inspect the `tag=…` value. Auth → retry with cookies. Region → try VPN. Network → wait and retry. Other → file an issue. |
| `4` | transcribe failed | Frame extraction succeeded but the transcribe step failed. Output is partial: `frame_paths` is populated, `transcript` is `null`. See *Partial success* below. The stderr line includes a `tag=…` token. | `[watch] error: transcribe failed tag=transcribe-quota — top up Kyma credit at https://kymaapi.com/billing?src=cli:watch or set GROQ_API_KEY for BYOK` | Read the partial output and decide. Quota → top up. Timeout → retry with shorter audio. Silent-audio → expected, fall through to frames only. |
| `64` | usage error | Bad invocation: missing required argument, unknown flag, malformed URL, or `-h` / `--help` was passed in a context where the caller wants a non-zero. Mirrors `sysexits.h` `EX_USAGE`. | `usage: watch <url> [frame-count] [--cookies <file>] [--format text\|json] [--pipe] [--no-cache] tag=usage-error` | Do not retry. Fix the invocation. |

Expand Down
1 change: 1 addition & 0 deletions docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

`watch-cli` works on any platform `yt-dlp` understands. The table below
covers the ones we have explicitly tested and the cookie tier they need.
"Auto browser" means `WATCH_BROWSER=auto` (opt-in; never read by default).

| Platform | No cookie | Auto browser | Manual `--cookies` |
|---|---|---|---|
Expand Down
6 changes: 3 additions & 3 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ tagged release tarball.
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/latest/download/install.sh | bash

# pin a specific version
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.3/install.sh \
| WATCH_CLI_VERSION=0.3.3 bash
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.4/install.sh \
| WATCH_CLI_VERSION=0.3.4 bash
```

`releases/latest/download/<asset>` is a GitHub-managed redirect that
Expand Down Expand Up @@ -350,7 +350,7 @@ The implementer must verify end to end before declaring done:
against `watch-cli.tar.gz.sha256`, unpack and run
`./watch-cli-0.3.0/install.sh` on a clean container. `watch
--version` prints `watch-cli v0.3.0`.
3. **Pinned install.** Run the `WATCH_CLI_VERSION=0.3.3` curl form (the variable goes on the `bash` side of the pipe)
3. **Pinned install.** Run the `WATCH_CLI_VERSION=0.3.4` curl form (the variable goes on the `bash` side of the pipe)
on a second clean container. Same result.
4. **Latest install.** Run the `releases/latest/download/install.sh`
form. Resolves to v0.3.0.
Expand Down
10 changes: 5 additions & 5 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# Usage:
# curl -fsSL https://github.com/sonpiaz/watch-cli/releases/latest/download/install.sh | bash
# or pin a version:
# curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.3/install.sh \
# | WATCH_CLI_VERSION=0.3.3 bash
# curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.4/install.sh \
# | WATCH_CLI_VERSION=0.3.4 bash
# or, from a clone:
# ./install.sh
#
Expand Down Expand Up @@ -52,12 +52,12 @@ watch-cli installer

Usage:
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/latest/download/install.sh | bash
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.3/install.sh \
| WATCH_CLI_VERSION=0.3.3 bash
curl -fsSL https://github.com/sonpiaz/watch-cli/releases/download/v0.3.4/install.sh \
| WATCH_CLI_VERSION=0.3.4 bash
./install.sh [--with-skill] [--with-mcp] [--with-local]

Env:
WATCH_CLI_VERSION Pin to a specific release (e.g. 0.3.3). Set it on
WATCH_CLI_VERSION Pin to a specific release (e.g. 0.3.4). Set it on
the bash side of the pipe, not on curl.
Unset → install the latest release.

Expand Down
2 changes: 1 addition & 1 deletion lib/version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
# titled `release: vX.Y.Z`, merge, then tag the merge commit `vX.Y.Z`
# and push. The tag fires the release workflow.

export WATCH_CLI_VERSION="0.3.3"
export WATCH_CLI_VERSION="0.3.4"
export WATCH_CLI_VERSION_STRING="watch-cli v${WATCH_CLI_VERSION}"
Loading