Pirate Claw is a local CLI for pulling media candidates from RSS feeds, matching them against your rules, and queueing approved downloads in Transmission.
Phases 01–11 of the current product roadmap are implemented on main (including Phase 11 TMDB metadata enrichment). The documented engineering epics through Epic 04 are also on main. For future stacked delivery phases, merge reviewed slices with bun run closeout-stack --plan <plan-path> rather than ad hoc cherry-picks. Further product-surface or delivery-tooling expansion beyond the current roadmap still requires a new planning pass and new approved phase/epic docs when the work is not a small bounded change.
It currently supports:
- RSS feeds for TV and movies
- title normalization into media metadata
- TV matching with per-title rules
- compact TV config through
tv.defaults + tv.showswith per-show overrides - movie matching with global year, resolution, and codec preferences
- local dedupe and run history in SQLite
- queueing through Transmission RPC
- status inspection and retry of failed submissions
- effective config inspection through
pirate-claw config show - env-backed Transmission credentials via process env or
.env - read-only daemon HTTP API for external consumers when
runtime.apiPortis configured - optional TMDB-backed posters, ratings, and metadata on API and dashboard when a
tmdbAPI key is configured (seepirate-claw.config.example.json) - read-only browser dashboard (SvelteKit app in
web/) that talks to the daemon HTTP API (Phase 10)
pirate-claw runpirate-claw daemonpirate-claw statuspirate-claw retry-failedpirate-claw reconcilepirate-claw config show
- Install dependencies with
bun install. - Copy
pirate-claw.config.example.jsonto./pirate-claw.config.json. - Edit your feeds, TV/movie matching rules, and Transmission credentials.
- Make sure the Transmission app is running and local RPC access is enabled.
- Run:
./bin/pirate-claw run --config ./pirate-claw.config.jsonInspect the current state with:
./bin/pirate-claw statusWhen a torrent has been reconciled from Transmission, status shows the latest known lifecycle and brief downloader detail alongside the stored candidate state.
If a tracked torrent later disappears from Transmission before completion, status surfaces it as missing_from_transmission; once a torrent has been observed completed, that completed state stays sticky locally.
Retry failed submissions with:
./bin/pirate-claw retry-failed --config ./pirate-claw.config.jsonReconcile tracked torrents from Transmission with:
./bin/pirate-claw reconcile --config ./pirate-claw.config.jsonInspect the fully normalized effective config with:
./bin/pirate-claw config show --config ./pirate-claw.config.jsonPirate Claw reads a local config file at pirate-claw.config.json by default.
The repo includes a checked-in example at pirate-claw.config.example.json. Your real local config stays untracked.
High-level config shape:
feeds: RSS sources to inspect (optionalpollIntervalMinutesper feed)tv: either the legacy per-show rule array or a compactdefaults + showsobjectmovies: global movie intake policytransmission: local Transmission RPC settings (optionaldownloadDirsfor per-media-type download directories)runtime: daemon scheduling and artifact settings (optional, all fields have defaults;apiPortenables the HTTP API;tmdbRefreshIntervalMinutescontrols background TMDB cache refresh, default 360 minutes,0disables)tmdb: optional TMDB API key (apiKeyor envPIRATE_CLAW_TMDB_API_KEY) and optional cache TTL overrides
Example:
{
"feeds": [
{
"name": "EZTV",
"url": "https://myrss.org/eztv",
"mediaType": "tv"
},
{
"name": "Atlas Movies",
"url": "https://atlas.rssly.org/feed",
"mediaType": "movie"
}
],
"tv": {
"defaults": {
"resolutions": ["720p"],
"codecs": ["x265"]
},
"shows": [
"Beyond the Gates",
{
"name": "The Daily Show",
"matchPattern": "daily show",
"resolutions": ["1080p"]
}
]
},
"movies": {
"years": [2026],
"resolutions": ["1080p"],
"codecs": ["x265"],
"codecPolicy": "prefer"
},
"transmission": {
"url": "http://localhost:9091/transmission/rpc",
"downloadDirs": {
"movie": "/data/movies",
"tv": "/data/tv"
}
},
"runtime": {
"runIntervalMinutes": 30,
"reconcileIntervalMinutes": 1,
"artifactDir": ".pirate-claw/runtime",
"artifactRetentionDays": 7,
"apiPort": 3000
}
}The compact TV form reduces repetition when most tracked shows share one quality policy:
tv.defaultsdefines the sharedresolutionsandcodecstv.showsmay contain plain show names that inherit those defaultstv.showsmay also contain objects with localmatchPattern,resolutions, orcodecsoverrides- the older
tv: [{ ... }]array shape still works unchanged
Pirate Claw expects a reachable local Transmission RPC endpoint.
Before running:
- Open the Transmission app.
- Enable remote access in Transmission settings.
- Confirm the listening port matches your config. The default example uses
9091. - If authentication is enabled, either put the username/password inline in
pirate-claw.config.jsonor setPIRATE_CLAW_TRANSMISSION_USERNAME/PIRATE_CLAW_TRANSMISSION_PASSWORDin a local.env. - If Transmission restricts allowed addresses, keep
127.0.0.1orlocalhostallowed.
Transmission credential precedence is:
- inline
transmission.username/transmission.passwordwin when present - otherwise Pirate Claw reads
PIRATE_CLAW_TRANSMISSION_USERNAME/PIRATE_CLAW_TRANSMISSION_PASSWORD - Pirate Claw loads those env vars from the process environment and from a
.envfile next to your config file
At queue time, Pirate Claw attempts to send Transmission labels based on media type:
moviefor movie feedstvfor TV feeds
If the configured Transmission instance rejects label arguments, Pirate Claw logs a warning and retries the same submission without labels.
The current build is tuned to work against:
https://myrss.org/eztvhttps://atlas.rssly.org/feed
Current behavior:
- queueable torrent URLs come from RSS
enclosure.urlwhen present <link>remains a fallback when no enclosure URL exists- movie items default to
movies.codecPolicy: "prefer", so they can still match when year and resolution fit policy even if codec is missing - explicit preferred codecs still outrank otherwise equivalent unknown-codec movie releases
movies.codecPolicy accepts "prefer" or "require".
Use "require" to reject movie releases that do not expose an allowed codec in the title.
Pirate Claw keeps local operator state out of git:
pirate-claw.config.jsonpirate-claw.db.pirate-claw/runtime/poll-state.json-- persisted feed poll timestamps used by the daemon to resume due-feed scheduling across restarts.pirate-claw/runtime/cycles/-- JSON and Markdown artifacts for daemon cycle results (completed, failed, or skipped for run/reconcile), pruned to 7 days by default
Run the daemon for continuous scheduled operation:
./bin/pirate-claw daemon --config ./pirate-claw.config.jsonThe daemon runs in the foreground, executing run cycles every 30 minutes and reconcile cycles every 1 minute. Stop with Ctrl+C.
When runtime.apiPort is set in the config, the daemon starts a read-only HTTP JSON API alongside the normal scheduling loop:
{
"runtime": {
"apiPort": 3000
}
}When runtime.apiPort is omitted, no HTTP listener starts.
| Endpoint | Description |
|---|---|
GET /api/health |
Uptime, start time, and last run/reconcile cycle snapshots |
GET /api/status |
Recent run summaries from the local database |
GET /api/candidates |
All tracked candidate state records |
GET /api/shows |
TV candidates grouped by show → season → episode |
GET /api/movies |
Movie candidates sorted by title |
GET /api/feeds |
Feed config with poll state and isDue status |
GET /api/config |
Effective config with Transmission credentials redacted |
curl http://localhost:3000/api/health{
"uptime": 3600000,
"startedAt": "2026-04-08T12:00:00.000Z",
"lastRunCycle": {
"status": "completed",
"startedAt": "...",
"completedAt": "...",
"durationMs": 1234
},
"lastReconcileCycle": null
}All endpoints are read-only. No endpoint mutates daemon state. There is no authentication in this version — it is designed for private NAS networks.
Candidate, show, and movie payloads include TMDB fields when a match exists in the local cache; otherwise they fall back to Phase-10-style local data.
The dashboard is a SvelteKit app under web/. Pages load data through server-side requests to the daemon JSON API (the browser never talks to Transmission or SQLite directly). There is no login in this version—use it only on networks you trust, same as the daemon API.
- Daemon HTTP API enabled — set
runtime.apiPortin your config (for example3000) and run the daemon (pirate-claw daemonor./bin/pirate-claw daemon --config …). See Daemon HTTP API above. - API base URL for the web app — copy
web/.env.exampletoweb/.envand setPIRATE_CLAW_API_URLto the daemon’s base URL (no trailing slash), e.g.http://localhost:3000. The SvelteKit server reads this at runtime; if it is missing, API-backed routes error until you set it.
From the repo root (after bun install at the root for the CLI):
bun install --cwd web
bun run --cwd web devThis starts the Vite-powered SvelteKit dev server (by default http://localhost:5173; the terminal shows the exact URL). Open it in a browser to browse candidates, shows, movies, and effective config. When a TMDB API key is configured, posters and ratings show where cached.
To serve a built app instead of dev:
bun run --cwd web build
cd web && PIRATE_CLAW_API_URL=http://localhost:3000 PORT=5174 node build/index.jsThe Node adapter defaults to port 3000 and host 0.0.0.0 if you omit PORT and HOST. If your daemon already uses 3000 for its API, set PORT to another value for the dashboard (for example 5174 as above). The process prints Listening on http://… on startup. Keep PIRATE_CLAW_API_URL pointed at the daemon; the dashboard URL and the daemon API URL are different ports.
Pirate Claw is intentionally still a local operator tool.
Not in scope yet:
- remote feed capture
- hosted persistence
- automatic post-completion file handling
- download renaming or organization rules
- Synology archiving
- broader ingestion redesign
Useful local commands:
bun testbun run test:coveragebun run verifybun run cibun run deliver restackto restack the current delivery ticket after its parent PR was squash-merged tomainbun run closeout-stack --plan <plan-path>to squash-merge a completed stacked delivery phase ontomainin ticket order using forwardgit merge --squash(no rebase)bun run deliver --plan <plan-path> poll-reviewto run the orchestrator's 2/4/6/8-minuteai-code-reviewpolling loop for the active PR and persist reviewed-SHA provenance plus vendor-attributed review artifactsbun run deliver --plan <plan-path> reconcile-late-review <ticket-id>to re-run fetch + triage + artifact persistence + PR body refresh for a done ticket when late review feedback appears (seedocs/03-engineering/delivery-orchestrator.md)bun run deliver --plan <plan-path> record-review <ticket-id> patched ...to record patched follow-up and make a best-effort attempt to resolve mapped native GitHub inline review threadsbun run deliver ai-reviewto run the same converged post-PR external AI-review lifecycle for a standalone non-ticket PR
The delivery orchestrator applies reviewer-facing guards when opening or editing PR bodies: it rejects escaped-newline sequences, bans auto-generated sections like Summary by ... / Validation / Verification, and rejects basic malformed markdown (mismatched fenced code blocks, bad headings). Literal \\n inside inline code spans is allowed.
The review hooks and triage logic live in ./.agents/skills/ai-code-review/SKILL.md. Ticket-linked delivery PRs and standalone ai-review runs share the same post-PR lifecycle core: polling, outcome accumulation, reviewer-facing metadata refresh, and final persistence. Supported external review agents are CodeRabbit, Qodo, Greptile, and SonarQube. SonarQube uses GitHub check annotations rather than native PR review comments; the fetcher keeps only failed-check annotations so triage stays focused on meaningful static-analysis findings rather than the full warning stream. Repo-level SonarQube scope lives in ./.sonarcloud.properties.
If you are working on the repo rather than just using the CLI, start with docs/00-overview/start-here.md.
Licensed under the GNU General Public License v3.0 or later. See LICENSE.
This project is intended to be free as in freedom, not merely free of charge.