<h1 align="center">AgentP</h1>
<p align="center"><strong>A fast, polished podcast downloader for your terminal.</strong></p>
<p align="center">Browse feeds, select episodes, and write clean tags (ID3 for MP3, MP4 metadata for M4A) from an interactive TUI—or automate everything with the CLI.</p>
<p align="center">
<a href="https://github.com/ByteMagicInc/AgentP/actions/workflows/release.yml"><img src="https://github.com/ByteMagicInc/AgentP/actions/workflows/release.yml/badge.svg" alt="Release status"></a>
<a href="https://crates.io/crates/agent-p"><img src="https://img.shields.io/crates/v/agent-p.svg" alt="Crates.io version"></a>
<a href="https://github.com/ByteMagicInc/AgentP/releases"><img src="https://img.shields.io/github/downloads/ByteMagicInc/AgentP/total.svg" alt="GitHub downloads"></a>
<a href="https://www.gnu.org/licenses/gpl-3.0"><img src="https://img.shields.io/badge/License-GPLv3-blue.svg" alt="GPL v3 license"></a>
</p>
<p align="center">
<a href="#installation">Installation</a> ·
<a href="#quick-start">Quick start</a> ·
<a href="#keyboard-reference">Keyboard reference</a> ·
<a href="#cli-reference">CLI reference</a> ·
<a href="#configuration">Configuration</a> ·
<a href="#troubleshooting">Troubleshooting</a>
</p>
<p align="center">
<img src="docs/assets/browse-and-select.gif" alt="AgentP browsing podcasts and selecting episodes from The AI Daily Brief, Merge Conflict, and Committing High Reason" width="1572">
</p>
## Why AgentP?
| | |
|---|---|
| **Interactive or automated**<br>Choose episodes in the TUI or script repeatable downloads with the CLI. | **Library-ready metadata**<br>Control album, artist, title, artwork, filenames, and numeric prefixes. |
| **Any RSS podcast**<br>Add a feed manually or let AgentP prepopulate its channel metadata. | **Flexible selection**<br>Download specific episodes, the latest or oldest N, or the complete feed. |
| **Fast command palette**<br>Press `Ctrl+K` to find actions without memorizing every shortcut. | **Runs everywhere**<br>One Rust binary for macOS, Linux, and Windows with a built-in Dracula theme. |
## Installation
| Method | Install |
|---|---|
| **Homebrew** · macOS and Linux | `brew install ByteMagicInc/tap/agent-p` |
| **Scoop** · Windows | `scoop bucket add bytemagicinc https://github.com/ByteMagicInc/scoop-bucket`<br>`scoop install agent-p` |
| **Cargo** · Rust 1.88+ | `cargo install agent-p --locked` |
| **Pre-built binary** | Download an archive from [GitHub Releases](https://github.com/ByteMagicInc/AgentP/releases) and place `agentp` on your `PATH`. |
> [!TIP]
> Alpine and other minimal Linux distributions can use the fully static `x86_64-unknown-linux-musl` build.
Installing [ffmpeg](https://ffmpeg.org/download.html) is recommended but optional (`brew install ffmpeg`, `scoop install ffmpeg`, or your distribution's package). AgentP needs it only for **Convert To MP3**, an off-by-default setting that re-encodes episodes a feed serves as M4A or other non-MP3 audio. Homebrew and Scoop mention ffmpeg after installing AgentP but do not install it. Without ffmpeg, everything else works and those episodes keep their original format.
## Quick start
### Interactive TUI
```sh
agentp
```
The first launch creates your configuration, seeds a set of demo podcasts, and points downloads at `~/Downloads/Podcasts` so you can explore immediately:
1. Choose a podcast and press `Enter`.
2. Mark episodes with `Space` or select everything with `a`.
3. Press `Enter` to download.
Press `c` to add your own feeds, change the download folder, and set the defaults for new podcasts.
AgentP comes with my favorite podcasts so you can see how different per-podcast configs are applied.
### Scriptable CLI
```sh
agentp podcast add --feed-url <RSS_URL> --from-feed
agentp podcast list
agentp download --podcast <NAME_OR_INDEX> --latest 5
```
Podcast selectors accept either a 1-based index or a case-insensitive name. A subcommand always runs headlessly, and the mode flags do not change that. With no subcommand, `agentp` opens the TUI unless `config.json` sets `"default_mode": "cli"`, and `--tui` or `--cli` (aliases `--user-mode` and `--agent-mode`) override that default for a single run, where `--cli` prints this help. `--help` works on every command and `agentp --version` prints the version.
### From a coding agent
```sh
npx skills add ByteMagicInc/AgentP --skill agentp -g
```
This installs the `agentp` [agent skill](https://skills.sh) for Claude Code, Codex, Cursor and other coding agents, so you can ask one to download an episode, find one by topic, date or number, or add a podcast, and it drives the CLI for you. Leave out `-g` to install it into the current project only. It needs Node.js 22.20 or later. The skill tracks the `master` branch, so it can be ahead of the latest release: it offers to upgrade an older AgentP, and stops when no newer release is out yet.
Without Node, `agentp skill` (in releases after 0.3.0) prints the copy bundled with your installed version. Save it as `SKILL.md` in your agent's skill folder:
```sh
mkdir -p ~/.claude/skills/agentp && agentp skill > ~/.claude/skills/agentp/SKILL.md
```
On Windows, from PowerShell, go through `cmd`: Windows PowerShell 5.1's `>` writes UTF-16, which agents cannot read.
```powershell
New-Item -ItemType Directory -Force "$HOME\.claude\skills\agentp" | Out-Null
cmd /c 'agentp skill > "%USERPROFILE%\.claude\skills\agentp\SKILL.md"'
```
## See AgentP in action
### Search episodes and jump to one by number
<p align="center">
<img src="docs/assets/search-and-jump.gif" alt="Searching Merge Conflict's episodes for copilot since 2025 without SDK episodes, with the matches underlined, selecting one, clearing the filter, jumping to episode 500, and finding both actions in the command palette" width="1572">
</p>
### Add a podcast from its RSS feed
<p align="center">
<img src="docs/assets/add-from-feed.gif" alt="Prepopulate explaining that a non-feed URL returned a web page, then adding The Pragmatic Engineer from its RSS feed and prepopulating its name, album, and artist" width="1572">
</p>
### Find actions and update configuration
<p align="center">
<img src="docs/assets/command-palette-and-config.gif" alt="Filtering AgentP's command palette and navigating its configuration screen" width="1572">
</p>
### Screenshots
| Podcast library | Episode selection |
|---|---|
| [](docs/assets/podcast-list.png) | [](docs/assets/episode-selection.png) |
| Command palette | Configuration |
|---|---|
| [](docs/assets/command-palette.png) | [](docs/assets/configuration.png) |
## Keyboard reference
Every screen lists its own keys in the bar at the bottom. Press `?` to expand it, or `Ctrl+K` to search every action by name.
### Global
| Key | Action |
|---|---|
| `Ctrl+K` | Toggle the command palette |
| `Ctrl+C` | Quit from anywhere, including during a download (asks first if the podcast editor has unsaved changes) |
| `q` | Quit when not typing in a text field; ignored while a download is running |
| `Ctrl+V` | Paste into the active text field or the palette filter |
| `?` or `.` | Expand or collapse the key-hint bar |
### Any list
Available on the podcast list, episode select, podcast picker, config menu, and the podcast editor.
| Key | Action |
|---|---|
| `Home` / `g` | Jump to the first row |
| `End` / `G` | Jump to the last row |
| `PageUp` / `PageDown` | Move ten rows |
### Podcast list
| Key | Action |
|---|---|
| `j` / `↓`, `k` / `↑` | Move selection |
| `Enter` | Open episode list |
| `Shift+↑` / `Shift+↓` | Reorder podcasts |
| `r` | Refresh feeds |
| `e` | Edit selected podcast |
| `o` | Open selected podcast folder |
| `c` | Config menu |
| `D` | Delete selected podcast; confirm with `y` or `D`, cancel with `Esc` |
Deleting is the one confirmation that does not take `Enter`. `Enter` opens a podcast on this screen, and removing one cannot be undone.
### Episode select
| Key | Action |
|---|---|
| `j` / `↓`, `k` / `↑` | Move selection |
| `Space` | Toggle episode |
| `a` | Toggle all (only the episodes a search leaves showing) |
| `s` | Sort (newest/oldest) |
| `/` | Search episodes by title, description, date, or date range |
| `#` or any digit | Go to an episode by its number |
| `o` | Open podcast folder |
| `Enter` | Download selected |
| `Esc` | Clear the search, or go back when none is active |
Every row starts with its episode number, the same numbering `--episodes` uses: numbers count up from the feed's last item, so in a feed that lists newest first, `1` is the oldest episode and the highest number is the newest.
**Search.** Press `/` and type; the list narrows as you type, and the matching words are underlined in each title. Several terms must all match, in any order, ignoring case:
| Term | Matches | Examples |
|---|---|---|
| A word | The title, the description, or the date as the list shows it, including the full month name | `copilot`, `sep`, `september`, `2025` |
| A `"phrase"` | Those words together, in that order; it narrows while you type it, before the closing quote | `"github copilot"` |
| `-` before a term | Episodes that do **not** match it | `-intro`, `-"live show"`, `-11/2026` |
| A date | Episodes published that day. With slashes or dashes the month comes first (`11/2/2026` is November 2); a four-digit year at the front reads year, month, day (ISO). Leading zeros are optional | `11/2/2026`, `11-02-2026`, `2026-11-02` |
| A month | Episodes published that month, month first or year first | `11/2026`, `2026-11` |
| A month and day | That day in any year | `9/10` |
| `#` and a number | That episode number, or an inclusive range | `#500`, `#100-120` |
| `since:` / `until:` | On or after / on or before a date, month, year, or relative day | `since:2026-01`, `until:2025`, `since:30d` |
| `after:` / `before:` | Strictly after / before the same | `after:9/10/2025`, `before:2026`, `before:1y` |
A relative day counts back from today: `today`, `yesterday`, or a number of days, weeks, months, or years such as `30d`, `2w`, `6m`, `1y`. A date written in a title, such as `9/11`, still matches that title, and a hashtag such as `#MacBookNeo` is a plain word. While a range term is unfinished, such as `after:20`, it is left out and the search bar says what it needs. An episode whose feed date AgentP cannot read shows no date in the list; its words still match the date as the feed wrote it, but it never falls inside a range.
`Enter` keeps the filter and returns to the list; `Esc` puts back the filter the search opened with. On the list, `Esc` clears an active filter. Pasting into the search turns line breaks into spaces. Selections are kept while the list is narrowed, and `Download` fetches every selected episode, including any the search is currently hiding.
**Go to a number.** Type a number such as `128` (or press `#` first) and press `Enter`. If a search is hiding that episode, the search is cleared. A number the feed does not have leaves the prompt open with a message. A pasted number may start with `#`; anything else that is not a plain number, such as `-2`, is reported rather than changed.
<details>
<summary>Keys for the command palette, download screen, config menu, wizard, and editor</summary>
### Command palette
| Key | Action |
|---|---|
| Type | Filter actions |
| `↑` / `↓`, `Shift+Tab` / `Tab` | Move selection |
| `Enter` | Run the selected action |
| `Esc` | Close |
### Downloading
| Key | Action |
|---|---|
| `Esc` | Back to the podcast list once the run has finished |
| `q` | Ignored while the transfer runs, so a stray keypress cannot leave a partial file |
### Config menu
The menu has eleven rows: **Add New Podcast**, **Download Folder**, **Default Mode**, **Banner Style**, **New Podcast Defaults**, **Edit Existing Podcasts**, **Open Config File**, **Open Podcasts Config File**, **Open Download Folder**, **About AgentP**, and **Report Issue**.
| Key | Action |
|---|---|
| `j` / `↓`, `k` / `↑` | Move selection |
| `Enter` | Activate the row (edit the folder path, toggle `tui`/`cli`, open an editor, or open a file or folder) |
| `r` on **Download Folder** | Reset the download folder to the built-in default |
| `Enter` / `Esc` while editing the path | Confirm or cancel the new folder path |
| `y` / `Enter`, `Esc` | Answer a confirmation dialog |
| `Esc` | Back to the podcast list |
### Add podcast wizard
| Key | Action |
|---|---|
| `Enter` on Feed URL | Open the mode selector |
| `1` / `m` | Manual — fill every field yourself |
| `2` / `p` / `Enter` | Prepopulate from feed — fetches the channel title, album, and artist |
| `Enter` | Next step (the last step saves the podcast) |
| `Tab` / `↓` / `j` on a yes/no or numeric step | Next step |
| `↑` / `k` / `Backspace` on a yes/no or numeric step | Previous step |
| `Backspace` on an empty text step | Previous step |
| `Space` | Toggle a yes/no step |
| `←` / `-`, `→` / `+` | Adjust the leading-zeros count |
| `Ctrl+V` | Paste into a text step |
| `Esc` | Cancel the wizard, or cancel an in-flight feed fetch |
### Edit podcast
| Key | Action |
|---|---|
| `j` / `↓`, `k` / `↑` | Move between fields |
| `Enter` | Edit a text field or toggle a yes/no field |
| `Enter` / `Esc` while editing text | Commit or cancel the edit |
| `Space` | Toggle a yes/no field |
| `←` / `-`, `→` / `+` | Adjust the leading-zeros count |
| `r` | Restore the current field to its built-in default |
| `R` | Reset every yes/no and numeric field to built-in defaults (template editor only) |
| `s` | Save |
| `Esc` | Back; asks `s` save / `d` discard / `Esc` cancel if there are unsaved changes |
| `Ctrl+V` | Paste into the text field being edited |
</details>
## CLI reference
Commands that select a podcast accept either its 1-based index or a case-insensitive name. Run `agentp <command> --help` for the full flag list.
### Podcasts
```sh
agentp podcast list
agentp podcast list --no-fetch
agentp podcast show --podcast "Darknet Diaries"
agentp podcast episodes --podcast 1 --limit 20
agentp podcast episodes --podcast 1 --search 'copilot since:30d -intro'
agentp podcast add --name "My Show" --feed-url https://example.com/feed.xml
agentp podcast add --feed-url https://example.com/feed.xml --from-feed
agentp podcast add --feed-url https://example.com/feed.xml --from-feed --artist "Jane Doe" --overwrite-file-name true
agentp podcast edit --podcast 2 --name "New Name" --feed-url https://example.com/new.xml
agentp podcast edit --podcast 2 --remove-images true
agentp podcast delete --podcast "Old Show"
```
`podcast episodes --search` takes the same terms as the episode-list search described above, and `--limit` then caps the matches. A range term that is not a date, such as `since:soon`, is an error.
New podcasts start from the `default_podcast` template in `config.json`. `--from-feed` fills in the name, album, and artist from the channel metadata; `--name` still wins if you pass both. Every per-podcast option in the [table below](#per-podcast-options-podcastsjson) is also a flag on `podcast add` and `podcast edit`, and yes/no flags take an explicit `true` or `false`.
### Downloads
```sh
agentp download --podcast 1 --latest 5
agentp download --podcast 1 --oldest 5
agentp download --podcast 1 --episodes 3,7,12
agentp download --podcast 1 --all
```
`--episodes`, `--latest`, `--oldest`, and `--all` are mutually exclusive and exactly one is required. Indices follow the feed's item order: index `1` is the feed's last item and the highest index is its first. Most feeds list newest first, which makes `1` the oldest episode. `--latest` and `--oldest` take items from the top and bottom of the feed, and `agentp podcast episodes` prints the indices.
### Configuration and folders
```sh
agentp config show
agentp config set --download-dir ~/Podcasts
agentp config set --default-mode cli
agentp config set --banner-style ascii
agentp config path
agentp open downloads
agentp open downloads --create
agentp open podcast --podcast 1 --create
agentp skill
```
`open` refuses to open a folder that does not exist yet; `--create` makes it first. `agentp skill` prints the agent skill for this version (see [From a coding agent](#from-a-coding-agent)).
## Configuration
Config lives under `$HOME/.config/AgentP/` (or `%USERPROFILE%\.config\AgentP\` on Windows). `agentp config path` prints the directory.
- `config.json` — download folder, the `default_podcast` template, `default_mode` (`tui` or `cli`), and `banner_style` (`joined` or `ascii`)
- `podcasts.json` — your podcast list, as `{ "podcasts": [ ... ] }`
`banner_style` picks the TUI banner wordmark: `joined` (the default) draws the letters with box-drawing characters and needs a terminal that draws those itself, such as Windows Terminal or kitty; `ascii` is the plain fallback for terminals that leave gaps between the strokes, Warp among them.
### Per-podcast options (`podcasts.json`)
Each entry in `podcasts.json` has the same shape as `default_podcast` plus its identity fields. The **CLI flag** column applies to `podcast add` and `podcast edit`.
| Field | CLI flag | Purpose |
|---|---|---|
| `name` | `--name` | Display name in lists |
| `feed_url` | `--feed-url` | RSS feed URL |
| `album_name` | `--album-name` | Subfolder name and album tag; falls back to `name` when empty |
| `artist` | `--artist` | Artist and album-artist tag |
| `user_agent` | `--user-agent` | Custom `User-Agent` for this podcast's audio downloads; empty uses AgentP's default |
| `overwrite_tags` | `--overwrite-tags` | Master switch for writing text tags: ID3 on MP3, MP4 metadata on M4A |
| `remove_existing_tags` | `--remove-existing-tags` | Drop the source file's existing tag (ID3 or MP4 metadata) and write a fresh minimal one (only when `overwrite_tags` is on) |
| `remove_images` | `--remove-images` | Strip embedded cover art; works even when `overwrite_tags` is off |
| `overwrite_album_name` | `--overwrite-album-name` | Write the album tag |
| `overwrite_artist` | `--overwrite-artist` | Write the artist and album-artist tags |
| `overwrite_title` | `--overwrite-title` | Write the title tag |
| `overwrite_file_name` | `--overwrite-file-name` | Rename the file to the episode title |
| `convert_to_mp3` | `--convert-to-mp3` | Re-encode episodes served as M4A, AAC, Ogg, FLAC, or WAV to MP3 with ffmpeg; off by default, which keeps the feed's original format. Needs ffmpeg on `PATH`, and the re-encode is lossy |
| `append_number_to_title` | `--append-number-to-title` | Prefix the title tag with the episode number |
| `leading_zeros_amount` | `--leading-zeros-amount` | Zero-padding for that number; `3` pads to four digits (`0001`), `0` disables |
> [!NOTE]
> Older configs that use `override_*`, `leading_zeros_to_title`, `remove_image`, or `create_new_tags` still load. AgentP rewrites them with the current key names the next time it saves.
### New-podcast defaults
The `default_podcast` block in `config.json` is copied into every podcast you create, whether through the TUI wizard or `agentp podcast add`. Edit it from **New Podcast Defaults** in the TUI config menu or directly in the file. Changing the template does not touch podcasts that already exist.
### Download layout
Downloads are organized by album name, which falls back to the podcast name when empty:
```text
<download_dir>/
└── <album_name>/
├── <first episode>.mp3
└── <second episode>.mp3
```
By default an episode keeps the format the feed serves, so a feed that mixes formats, as Latent Space does, gives you some `<episode>.m4a` files. Turn on `convert_to_mp3` and every episode lands as `.mp3` once its conversion succeeds. It still keeps its original format when ffmpeg is missing, when the conversion fails, or when AgentP cannot identify the file's format.
Without `overwrite_file_name`, the filename is the last path segment of the episode's enclosure URL (hosts that wrap the real file URL inside a redirect link, such as Anchor, are unwrapped), falling back to the final response URL and then `default.<ext>`. With it, the file is renamed to `<sanitized episode title>.<ext>`. Downloading an episode again overwrites the existing file.
AgentP checks the first bytes of each download to find its real format rather than trusting the URL or the server's `Content-Type`. A URL's extension is kept only when it is an audio extension matching that format (`mp3` for MP3; `m4a`, `m4b`, or `mp4` for MP4 audio; `aac` for raw AAC; `ogg`, `oga`, or `opus` for Ogg; `flac`; `wav`). Otherwise `<ext>` is the format's default (`mp3`, `m4a`, `aac`, `ogg`, `flac`, or `wav`; `mp3` for an unrecognised format), and a kept URL file name with any other extension gets that default appended, so a feed cannot make AgentP save an episode as, say, an `.exe`.
With `convert_to_mp3` on, AgentP saves an M4A, AAC, Ogg, FLAC, or WAV download under that name, re-encodes it with ffmpeg to the same name ending in `.mp3` (LAME VBR quality 4, about 130–165 kbps, keeping cover art and the feed's metadata; a video episode keeps only its audio), and then deletes the original. A download AgentP cannot identify as one of those formats is never handed to ffmpeg; it is saved as downloaded, with a notice. AgentP also tells ffmpeg the input format rather than letting it guess, and limits it to local files, so a feed cannot pass off a playlist as an episode to make ffmpeg fetch URLs or read other files. That narrows what an untrusted download can do but does not sandbox ffmpeg. A missing ffmpeg is reported once per download run and a failed conversion once per episode, both as warnings: the original file is kept and tagged as below, and `agentp download` still exits successfully. Turning the setting on in the TUI shows a warning beside it while ffmpeg is missing, and `podcast add` and `podcast edit` print one; neither stops you saving. `agentp config show` reports whether ffmpeg was found. Raw AAC or FLAC that starts with an ID3 tag is recognised as AAC or FLAC, not MP3, so it is converted too. A conversion still running after 20 minutes is stopped and the original kept. Conversion takes several seconds per episode; stopping AgentP mid-conversion can leave a `<episode>.mp3.part` file beside the original, which the next download of that episode replaces.
When `overwrite_tags` is on, AgentP writes the title, album, and artist plus album-artist tags that you have enabled and leaves every other tag alone. MP3 files, converted ones included, get ID3 frames. With `convert_to_mp3` off, or when conversion could not run, M4A files get MP4 metadata atoms, and raw AAC and other formats are saved untagged, with a notice in the download log when tagging or image removal is on. Turn on `remove_existing_tags` to discard the source file's tag first, which helps when a host embeds large metadata blocks that confuse players.
> [!NOTE]
> Episode indices are **1-based, counted up from the feed's last item**, so in a feed that lists newest first, 1 is the oldest episode and the newest has the highest index. AgentP does not re-sort by publication date. This applies to `--episodes` and numeric title prefixes. With `leading_zeros_amount: 3`, episode 1 becomes `0001`, episode 10 becomes `0010`, and episode 100 becomes `0100`.
## Troubleshooting
- **`... (internet-filter/proxy) blocked URL ...`** while downloading — the host answered with an HTML or text page instead of audio. This usually means a proxy or content filter intercepted the request, or the host rejected AgentP's `User-Agent`. Check the filter, or set `user_agent` on that podcast (`agentp podcast edit --podcast N --user-agent "..."`).
- **`... (internet-filter/proxy) blocked URL ...`** when adding a podcast or loading its episodes — the feed request was redirected to another host that answered with a web page instead of RSS, the signature of a content filter. Ask for the feed's host to be allowed; `user_agent` applies only to audio downloads and does not change feed requests.
- **A feed adds fine but every download fails** — some hosts (Buzzsprout's CDN, for example) refuse anonymous clients. AgentP sends `AgentP/<version>` by default; a browser-style `user_agent` on the podcast usually resolves it.
- **`ffmpeg was not found on PATH`** — **Convert To MP3** is on for that podcast but ffmpeg is not installed, so episodes keep their original format. Install ffmpeg, or turn the setting off (`agentp podcast edit --podcast N --convert-to-mp3 false`) to stop the warning.
- **`Episode index N is out of range`** — indices follow the feed's item order, counting up from its last item, so `1` is the oldest episode only when the feed lists newest first, as most do. Run `agentp podcast episodes --podcast N` to see the current numbering.
- **Start over** — delete `config.json` and `podcasts.json` from the directory printed by `agentp config path`. The next launch recreates both, including the demo podcasts.
## Building from source
Requires Rust 1.88+ (edition 2024). The binary lands at `target/release/agentp`.
```sh
git clone https://github.com/ByteMagicInc/AgentP.git
cd AgentP
cargo build --release --locked
```
CI runs automatically only when a release is tagged, so before opening a pull request run the same checks locally:
```sh
cargo fmt --all -- --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --all --locked
```
## Contributing
Issues and PRs welcome at <https://github.com/ByteMagicInc/AgentP>. Read [CONTRIBUTING.md](CONTRIBUTING.md) before you contribute and [AGENTS.md](AGENTS.md) for architecture details. Start commit messages with `Fix`, `Add`, `Refactor`, or `Update` so the changelog groups them correctly.
Maintainers: see the short [release guide](docs/releasing.md).
## License
[GPL-3.0-only](LICENSE) © 2026 ByteMagic Inc.