<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Robbe Verhelst</title>
    <description>The latest articles on Forem by Robbe Verhelst (@robbeverhelst).</description>
    <link>https://forem.com/robbeverhelst</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F838968%2Fcfc1403a-ff1a-46d1-b12f-2bac3ca443d8.jpeg</url>
      <title>Forem: Robbe Verhelst</title>
      <link>https://forem.com/robbeverhelst</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/robbeverhelst"/>
    <language>en</language>
    <item>
      <title>AI-Powered Media Server Management with OpenClaw and Tsarr</title>
      <dc:creator>Robbe Verhelst</dc:creator>
      <pubDate>Mon, 30 Mar 2026 08:39:30 +0000</pubDate>
      <link>https://forem.com/robbeverhelst/ai-powered-media-server-management-with-openclaw-and-tsarr-494h</link>
      <guid>https://forem.com/robbeverhelst/ai-powered-media-server-management-with-openclaw-and-tsarr-494h</guid>
      <description>&lt;p&gt;Last weekend I wanted to know which TV series were eating the most storage on my media server. Simple question.&lt;/p&gt;

&lt;p&gt;My AI assistant (running on &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;) tried to answer it. It took multiple approaches, five attempts, and several minutes of fumbling before it found the data. Top Gear at 494 GB. Pokémon at 362 GB. South Park at 344 GB. The answer was there, but getting to it was painful.&lt;/p&gt;

&lt;p&gt;The thing is, I'd already built a tool that does this in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr sonarr series list &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/robbeverhelst/Tsarr" rel="noopener noreferrer"&gt;Tsarr&lt;/a&gt; is a type-safe CLI I built for managing Radarr, Sonarr, and the rest of the *arr stack. My AI assistant just didn't know it existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;AI assistants are great generalists but terrible specialists. They'll try raw API calls, shell scripts, whatever they can think of. Anything except the purpose-built tool sitting right there. They don't read your README. They don't know your CLI flags. They improvise, and improvisation with infrastructure is how things break.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: AgentSkills
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://clawhub.ai" rel="noopener noreferrer"&gt;ClawHub&lt;/a&gt; is a skill registry for &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt; agents. A "skill" is a SKILL.md file that teaches an AI how to use a specific tool: what commands exist, when to use them, safety rules, and common workflows.&lt;/p&gt;

&lt;p&gt;I wrote one for Tsarr. It's structured like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skill/
├── SKILL.md                          # Entry point: routing + safety rules
└── references/
    ├── setup.md                      # Installation, config, connectivity
    ├── common-workflows.md           # Health checks, search, add, queue, history
    └── service-cheatsheet.md         # Every command mapped per service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SKILL.md tells the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start&lt;/strong&gt; with &lt;code&gt;tsarr doctor&lt;/code&gt; if anything seems off&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;--json&lt;/code&gt;&lt;/strong&gt; when extracting data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect before mutating&lt;/strong&gt;: always fetch before delete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask which service&lt;/strong&gt; if the user just says "library" without specifying Radarr or Sonarr&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The references give it a cheatsheet of every command pattern so it doesn't have to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;Same question, "what series are the largest?", now gets answered instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr sonarr series list &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="c"&gt;# sorted, filtered, exact byte counts, all metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No fumbling. No five attempts. The AI knows the tool because I taught it.&lt;/p&gt;

&lt;p&gt;Installing the skill on any OpenClaw instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx clawhub &lt;span class="nb"&gt;install &lt;/span&gt;tsarr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. The agent immediately knows how to manage your entire *arr stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Can Do Now
&lt;/h2&gt;

&lt;p&gt;With the skill installed, I just ask in plain English and the agent handles it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What's eating my storage?"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr sonarr series list &lt;span class="nt"&gt;--json&lt;/span&gt;  &lt;span class="c"&gt;# sorted by size, instant answer&lt;/span&gt;
tsarr radarr movie list &lt;span class="nt"&gt;--json&lt;/span&gt;   &lt;span class="c"&gt;# same for movies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Are there any download issues?"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr sonarr queue list          &lt;span class="c"&gt;# stuck downloads, warnings, errors&lt;/span&gt;
tsarr radarr queue list          &lt;span class="c"&gt;# same for movies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Check if everything is healthy"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr doctor                     &lt;span class="c"&gt;# tests all configured services at once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Find me that new show everyone's talking about"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr sonarr series search &lt;span class="s2"&gt;"Shogun"&lt;/span&gt;
tsarr sonarr series add &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--quality-profile&lt;/span&gt; HD &lt;span class="nt"&gt;--root-folder&lt;/span&gt; /media/shows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Are subtitles missing for anything?"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr bazarr movie list &lt;span class="nt"&gt;--json&lt;/span&gt;   &lt;span class="c"&gt;# check subtitle status across your library&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"What got added recently?"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tsarr sonarr &lt;span class="nb"&gt;history &lt;/span&gt;list        &lt;span class="c"&gt;# recent grabs, imports, upgrades&lt;/span&gt;
tsarr radarr &lt;span class="nb"&gt;history &lt;/span&gt;list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tsarr supports six services: &lt;strong&gt;Radarr&lt;/strong&gt; (movies), &lt;strong&gt;Sonarr&lt;/strong&gt; (TV), &lt;strong&gt;Lidarr&lt;/strong&gt; (music), &lt;strong&gt;Readarr&lt;/strong&gt; (books), &lt;strong&gt;Prowlarr&lt;/strong&gt; (indexers), and &lt;strong&gt;Bazarr&lt;/strong&gt; (subtitles). Every service follows the same CLI pattern, so the agent learns one and knows them all.&lt;/p&gt;

&lt;p&gt;The skill doesn't just list commands. It teaches the agent &lt;em&gt;when&lt;/em&gt; to use each one, what flags matter, and when to ask clarifying questions before doing anything destructive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Every homelab runner, every DevOps engineer, every developer has tools their AI doesn't know about. Custom CLIs, internal scripts, niche utilities. The knowledge gap between "this tool exists" and "the AI can use it effectively" is just a SKILL.md file.&lt;/p&gt;

&lt;p&gt;If you've built a CLI that you use regularly, consider writing a skill for it. Your future self (and your AI) will thank you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tsarr:&lt;/strong&gt; &lt;a href="https://github.com/robbeverhelst/Tsarr" rel="noopener noreferrer"&gt;github.com/robbeverhelst/Tsarr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;ClawHub skill:&lt;/strong&gt; &lt;a href="https://clawhub.ai/robbeverhelst/tsarr" rel="noopener noreferrer"&gt;clawhub.ai/robbeverhelst/tsarr&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;OpenClaw:&lt;/strong&gt; &lt;a href="https://openclaw.ai" rel="noopener noreferrer"&gt;openclaw.ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>homelab</category>
      <category>openclaw</category>
    </item>
    <item>
      <title>Guard Wolves: a Minecraft plugin I shouldn't have been able to build</title>
      <dc:creator>Robbe Verhelst</dc:creator>
      <pubDate>Fri, 13 Mar 2026 16:16:57 +0000</pubDate>
      <link>https://forem.com/robbeverhelst/guard-wolves-a-minecraft-plugin-i-shouldnt-have-been-able-to-build-poc</link>
      <guid>https://forem.com/robbeverhelst/guard-wolves-a-minecraft-plugin-i-shouldnt-have-been-able-to-build-poc</guid>
      <description>&lt;p&gt;I run a small Minecraft server for friends. We had a problem: every time we'd go exploring, mobs would wreck our base. The obvious solution? Guard dogs. Minecraft has wolves, they can be tamed, but tamed wolves are useless as guards. They either sit there doing nothing or follow you around like lost puppies.&lt;/p&gt;

&lt;p&gt;I wanted wolves that would stay at a location, patrol a radius, and attack hostile mobs. Basically guard dogs. Minecraft doesn't have that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The language I'd never seen
&lt;/h2&gt;

&lt;p&gt;The way to script custom behavior in Minecraft (without writing a full Java plugin) is &lt;a href="https://denizenscript.com/" rel="noopener noreferrer"&gt;DenizenScript&lt;/a&gt;. It's a YAML-like scripting language specific to Minecraft servers. It has its own syntax, its own commands, its own way of handling entities, events, and flags.&lt;/p&gt;

&lt;p&gt;I'd never written a line of it. My day job is TypeScript. DenizenScript looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;if &amp;lt;[wolf].location.distance[&amp;lt;[guard_center]&amp;gt;]&amp;gt; &amp;gt; 15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;walk &amp;lt;[wolf]&amp;gt; &amp;lt;[safe_center]&amp;gt; speed:0.3&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;flag &amp;lt;[wolf]&amp;gt; return_attempts:++&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not YAML. That's not any language I know. The angle brackets, the dot-chained methods, the flag system. It's its own thing entirely.&lt;/p&gt;

&lt;p&gt;Learning DenizenScript properly for one project didn't make sense. I'd use it once and forget it. So I described what I wanted in plain English and let AI translate it into DenizenScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I asked for
&lt;/h2&gt;

&lt;p&gt;The concept was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Right-click a tamed wolf with a stick to toggle guard mode&lt;/li&gt;
&lt;li&gt;Guard wolf stays at that location and patrols a 15-block radius&lt;/li&gt;
&lt;li&gt;It attacks hostile mobs but never players, never creepers (explosions near the base = bad), and never passive animals&lt;/li&gt;
&lt;li&gt;If it gets stuck or wanders too far, teleport it back&lt;/li&gt;
&lt;li&gt;When the owner comes back, right-click with a stick to get your wolf back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Simple on paper. 463 lines of DenizenScript in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy part
&lt;/h2&gt;

&lt;p&gt;AI handled the basic structure well. The event handlers, the combat scanning, the flag system for tracking guard state. Within an hour I had a script that could toggle guard mode and make wolves attack nearby zombies.&lt;/p&gt;

&lt;p&gt;The owner removal trick was clever: when a wolf enters guard mode, you remove the owner (so it stops following you and won't teleport to you), but save the original owner in a flag. When you disable guard mode, restore the owner. AI figured that out without me asking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Enter guard mode&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;flag &amp;lt;[wolf]&amp;gt; original_owner:&amp;lt;player&amp;gt;&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;adjust &amp;lt;[wolf]&amp;gt; owner:!&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;flag &amp;lt;[wolf]&amp;gt; guard_mode&lt;/span&gt;

&lt;span class="c1"&gt;# Exit guard mode&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;adjust &amp;lt;[wolf]&amp;gt; owner:&amp;lt;[wolf].flag[original_owner]&amp;gt;&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;flag &amp;lt;[wolf]&amp;gt; guard_mode:!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The hard part
&lt;/h2&gt;

&lt;p&gt;Then reality hit. The script worked in theory. In practice, wolves are dumb.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pathfinding failures.&lt;/strong&gt; Wolves would try to walk back to their guard point and get stuck on a fence. Or a half-slab. Or literally nothing visible. The AI-generated "walk back" command wasn't enough. I needed stuck detection: if a wolf hasn't moved in 60 seconds despite being told to walk, teleport it home.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;if &amp;lt;[time_stuck]&amp;gt; &amp;gt; 60&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;teleport &amp;lt;[wolf]&amp;gt; &amp;lt;[guard_center]&amp;gt;&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;flag &amp;lt;[wolf]&amp;gt; invulnerable_until:&amp;lt;util.time_now.add[3s]&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Teleport safety.&lt;/strong&gt; Teleporting a wolf sounds simple. But if you teleport it into a block, it suffocates. If you teleport it mid-air, fall damage. If you teleport it while a mob is hitting it, it dies during the invulnerability gap. Every teleport needed a 3-second invulnerability window with protection from multiple damage types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The creeper problem.&lt;/strong&gt; First version: wolves attacked everything hostile. I realized pretty quickly that creepers would be a disaster. Wolf attacks creeper, creeper explodes, half your base is gone. Worse than no guard wolf at all. Had to explicitly exclude creepers from the target list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server restarts.&lt;/strong&gt; Guard wolves need to survive server restarts. Their guard points, health, and state all stored in Denizen flags. But on restart, max health resets to vanilla values. Needed a startup script that re-discovers all guard wolves and restores their stats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health management.&lt;/strong&gt; Tamed wolves in Minecraft can be healed by feeding them meat. But guard wolves have their owner removed (that's how they stay put). Without an owner, the healing mechanic doesn't work the same way. I had to add a warning message: "Disable guard mode first, then heal, then re-enable."&lt;/p&gt;

&lt;p&gt;Each of these took multiple deploy-test-fix cycles. Change the script, reload on the server, spawn some zombies, watch the wolf, see what breaks, repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the result looks like
&lt;/h2&gt;

&lt;p&gt;After all the iteration, the plugin is actually solid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wolves patrol a 15-block radius and return to their post&lt;/li&gt;
&lt;li&gt;They attack zombies, skeletons, spiders, and other hostiles on sight&lt;/li&gt;
&lt;li&gt;They ignore creepers, players, and passive mobs&lt;/li&gt;
&lt;li&gt;They survive server restarts with all their stats intact&lt;/li&gt;
&lt;li&gt;Stuck wolves teleport home with invulnerability&lt;/li&gt;
&lt;li&gt;You get death notifications when a guard wolf dies&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;/gw ls&lt;/code&gt; command shows all your wolves with health, location, and armor status
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;========== Your Wolves (5) ==========
• Fang (Pale) ❤ 32/40 GUARDING at -487,78,-18,world
• Rex (Ashen) ❤ 20/20 Following at 123,65,456,world
• Scout (Woods) ❤ 8/8 Sitting at -200,70,300,world
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The unexpected payoff
&lt;/h2&gt;

&lt;p&gt;Once the guard wolves script was solid, something clicked. I had 463 lines of working DenizenScript that covered events, flags, entity management, combat, persistence. It was basically a reference implementation.&lt;/p&gt;

&lt;p&gt;When I wanted other scripts afterward, like keeping dolphins alive (they drown when you wander too far from them), I could point AI at the guard wolves script and say "make something like this but for dolphins." It already understood the patterns: how Denizen handles entities, how flags work, how to hook into server events.&lt;/p&gt;

&lt;p&gt;The first script took a day. The dolphin script took maybe 20 minutes. Having a working example in the same language made AI dramatically better at generating new scripts. It's like giving it a style guide instead of asking it to figure everything out from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest breakdown
&lt;/h2&gt;

&lt;p&gt;AI got me maybe 60-70% of the way there. The basic structure, the event system, the flag management. Things that are well-documented and pattern-based.&lt;/p&gt;

&lt;p&gt;The remaining 30-40% was all edge cases that only surface when you actually run the thing. Pathfinding quirks, teleport safety, combat targeting rules, persistence across restarts. AI can't predict that wolves get stuck on half-slabs or that creepers near your base are a disaster. That knowledge comes from testing.&lt;/p&gt;

&lt;p&gt;Total time: about a day. Without AI, I probably wouldn't have built it at all. DenizenScript is too niche to justify learning for one project. With AI, I could focus on &lt;em&gt;what&lt;/em&gt; I wanted instead of &lt;em&gt;how&lt;/em&gt; DenizenScript works.&lt;/p&gt;

&lt;p&gt;The plugin is open source if you want to use it or build on it: &lt;a href="https://github.com/robbeverhelst/guard-wolves" rel="noopener noreferrer"&gt;guard-wolves on GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anyone else building weird Minecraft plugins? I'd love to see what people are doing with DenizenScript.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>minecraft</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Tsarr: a type-safe TypeScript SDK &amp; CLI for the Servarr ecosystem</title>
      <dc:creator>Robbe Verhelst</dc:creator>
      <pubDate>Fri, 13 Mar 2026 15:00:13 +0000</pubDate>
      <link>https://forem.com/robbeverhelst/tsarr-a-type-safe-typescript-sdk-cli-for-the-servarr-ecosystem-55n4</link>
      <guid>https://forem.com/robbeverhelst/tsarr-a-type-safe-typescript-sdk-cli-for-the-servarr-ecosystem-55n4</guid>
      <description>&lt;p&gt;If you run Radarr, Sonarr, or any of the *arr apps, you've probably written a quick script to automate something. Bulk imports, library cleanup, monitoring. And you've probably hit the same wall: the APIs are big, undocumented in practice, and every app has slightly different conventions.&lt;/p&gt;

&lt;p&gt;I got tired of writing raw fetch calls and guessing at response shapes, so I built &lt;strong&gt;Tsarr&lt;/strong&gt;: a fully type-safe TypeScript client and CLI that covers the entire Servarr ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Tsarr provides auto-generated TypeScript clients for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Radarr&lt;/strong&gt; (movies)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sonarr&lt;/strong&gt; (TV series)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lidarr&lt;/strong&gt; (music)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readarr&lt;/strong&gt; (books)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prowlarr&lt;/strong&gt; (indexers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bazarr&lt;/strong&gt; (subtitles)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every client is generated directly from the official Swagger/OpenAPI specs, so types are always accurate and up-to-date. When the *arr devs update their API, Tsarr follows automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;Each *arr app ships an OpenAPI/Swagger spec that describes their full API. Tsarr uses &lt;a href="https://github.com/hey-api/openapi-ts" rel="noopener noreferrer"&gt;@hey-api/openapi-ts&lt;/a&gt; to generate typed TypeScript clients from those specs.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the latest OpenAPI specs from each *arr app's GitHub repo&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;@hey-api/openapi-ts&lt;/code&gt; to generate fully typed clients (request types, response types, enums, everything)&lt;/li&gt;
&lt;li&gt;Bundle it all into one package with modular imports per app&lt;/li&gt;
&lt;li&gt;The CLI layer wraps those same clients with &lt;a href="https://github.com/unjs/citty" rel="noopener noreferrer"&gt;citty&lt;/a&gt; for the command-line interface&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the generation is automated, keeping up with API changes is just a matter of re-running the generator. Renovate handles dependency bumps, and semantic-release cuts new versions automatically when PRs merge.&lt;/p&gt;

&lt;p&gt;No hand-written API types. No manual maintenance. If the official spec changes, the next build catches it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it as an SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RadarrClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SonarrClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tsarr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;radarr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RadarrClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:7878&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your-api-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Full type safety: autocomplete, type checking, the works&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;movies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMovies&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;radarr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSystemStatus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get proper TypeScript types for every request and response. No more guessing what fields a movie object has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use it as a CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install globally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tsarr

&lt;span class="c"&gt;# Setup&lt;/span&gt;
tsarr config init

&lt;span class="c"&gt;# Manage your library from the terminal&lt;/span&gt;
tsarr radarr movie list
tsarr radarr movie search &lt;span class="nt"&gt;--term&lt;/span&gt; &lt;span class="s2"&gt;"Interstellar"&lt;/span&gt;
tsarr sonarr series list
tsarr prowlarr indexer list

&lt;span class="c"&gt;# Check all connections at once&lt;/span&gt;
tsarr doctor

&lt;span class="c"&gt;# JSON output for scripting&lt;/span&gt;
tsarr radarr movie list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.[] | .title'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI supports table output for humans and JSON for scripts. Shell completions included for bash, zsh, and fish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built this
&lt;/h2&gt;

&lt;p&gt;I self-host everything on a Kubernetes cluster at home. When you manage multiple *arr instances, automation isn't optional. I also run an AI assistant that manages my homelab, and it works way better with CLIs than raw APIs. Being able to say "add Interstellar to Radarr" and have it run &lt;code&gt;tsarr radarr movie add&lt;/code&gt; behind the scenes is a game changer.&lt;/p&gt;

&lt;p&gt;So I needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Type safety&lt;/strong&gt; so I catch API changes at compile time, not at 3 AM when my automation breaks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One package&lt;/strong&gt; instead of six different libraries for six different apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI access&lt;/strong&gt; because sometimes you just want to check something from the terminal without opening a browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing like this existed for TypeScript. &lt;a href="https://github.com/Dark-Alex-17/managarr" rel="noopener noreferrer"&gt;Managarr&lt;/a&gt; is a great Rust TUI for interactive management, but if your stack is Node.js/TypeScript and you want to build automation, you were on your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Available everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;tsarr

&lt;span class="c"&gt;# Homebrew&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;robbeverhelst/tsarr/tsarr

&lt;span class="c"&gt;# Docker&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; ghcr.io/robbeverhelst/tsarr doctor

&lt;span class="c"&gt;# AUR (Arch Linux)&lt;/span&gt;
yay &lt;span class="nt"&gt;-S&lt;/span&gt; tsarr-bin

&lt;span class="c"&gt;# Nix&lt;/span&gt;
nix profile &lt;span class="nb"&gt;install &lt;/span&gt;github:robbeverhelst/tsarr?dir&lt;span class="o"&gt;=&lt;/span&gt;packaging/nix

&lt;span class="c"&gt;# Or grab a standalone binary, no runtime needed&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://github.com/robbeverhelst/tsarr/releases/latest/download/tsarr-linux-x64 &lt;span class="nt"&gt;-o&lt;/span&gt; tsarr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you can build with it
&lt;/h2&gt;

&lt;p&gt;Some things I use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bulk movie imports&lt;/strong&gt;: feed it a list, let it add everything with proper quality profiles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Library audits&lt;/strong&gt;: find movies without subtitles, series with missing episodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring scripts&lt;/strong&gt;: check queue status, disk space, indexer health in cron jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-app automation&lt;/strong&gt;: when Sonarr grabs a series, trigger Bazarr to fetch subtitles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI is also great for quick checks without leaving the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What's in my download queue right now?&lt;/span&gt;
tsarr radarr queue list &lt;span class="nt"&gt;--table&lt;/span&gt;

&lt;span class="c"&gt;# Any indexer issues?&lt;/span&gt;
tsarr prowlarr indexer list &lt;span class="nt"&gt;--table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  It's open source
&lt;/h2&gt;

&lt;p&gt;MIT licensed. Contributions welcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/robbeverhelst/Tsarr" rel="noopener noreferrer"&gt;robbeverhelst/Tsarr&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/tsarr" rel="noopener noreferrer"&gt;tsarr&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://robbeverhelst.github.io/Tsarr/" rel="noopener noreferrer"&gt;robbeverhelst.github.io/Tsarr&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you run any *arr apps and write TypeScript, give it a try. Issues and PRs welcome, especially if you find edge cases in specific API endpoints.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's your *arr automation setup like? I'd love to hear what people are building.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>cli</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
