Every R package we’ve released over the past two weeks was built with the same set of tools. Today we’re making those tools public.
The sidequest era
CLI coding tools like Claude Code (and even llamaR!) have changed the economics of development. We’re entering an era of bespoke software. Things that sat, or would’ve sat, on a “someday” list for years… a documentation generator, an R Core–style formatter, an LLM-focused package introspection tool, all with no dependencies… suddenly became afternoon projects (at least to get 90% of the functionality, making it pretty always takes some extra time). Not because the problems are trivial, but because the iteration speed has changed.
Nobody knows where all of this is going. But for now, I’m happy to be completing sidequests that have long intrigued me. These packages started as internal tools to support the cornball.ai packages we’ve been building. They scratched an itch. Now that they’re working reliably, it seems wrong not to share them.
Before we get into these new packages, I wanted to touch on littler.
littler: why it matters for AI CLI tools
I’ve instructed my AIs to use the littler package’s r -e instead of Rscript. Both ultimately start an R interpreter, and in real-world use (outside of running an extremely large number of small R scripts; e.g. high-frequency trading) the startup time difference is likely small enough to be irrelevant. But speed is not the main reason to prefer littler.
Where littler does stand out is CLI ergonomics and predictability, which are especially important when an AI agent is orchestrating commands.
littler’s r is a native binary that embeds R directly and behaves like a purpose-built Unix CLI tool. In practice, that gives you:
- cleaner argument handling
- predictable stdout and stderr behavior
- reliable exit codes
- minimal startup noise
Those properties make it easier for AIs to parse output, detect failures, and decide what to do next. While shaving a few tens of milliseconds per call is a nice-to-have in an agent workflow, standard CLI ergonomics and clear failure signaling is invaluable.
To be clear, this absolutely does not replace interactive or stateful workflows. If you are doing exploratory data analysis (EDA), you need a persistent session; i.e. an MCP-style server like mcptools or llamaR allows you and your AI to work similar to how you would inside RStudio or the like.
But for stateless CLI execution (i.e. package development) driven by Claude Code, Codex, or other AI coding tools, littler provides a cleaner AI-friendly interface to R, and that is where it earns its place.
Why a toolchain?
The tinyverse philosophy is simple: dependencies have real costs. Each one is an invitation to break your project. We take that seriously enough that it always seemed slightly off to not apply it to our own development workflow.
The standard R development stack (devtools, roxygen2, testthat, usethis, styler) is excellent software. But it pulls in over one hundred dependencies, and for our purposes, we don’t need everything it provides. So we built alternatives that do less with less.
If you’ve read our previous posts on speech-to-text and text-to-speech in R, all 6 of those packages were documented, tested, formatted, and are in the process of being shipped to CRAN using the four tools we’re releasing today:
- tinyrox – Documentation and NAMESPACE generation (replaces roxygen2)
- tinypkgr – Package development utilities: install, check, build (replaces devtools)
- fyi – R package introspection for LLMs (inspired by, but different use case from, btw)
- rformat – Token-based R code formatter following R Core style (replaces styler)
We’re also releasing one more tool that doesn’t fit neatly into the “development toolchain” but fits squarely into the tinyverse:
- rapt – R + apt: Python-free alternative to bspm for r2u binary installs
Together with tinytest, simplermarkdown, and r-ci, these form a complete R package development toolchain with minimal dependencies.
tinyrox: documentation without the magic
tinyrox generates .Rd files and NAMESPACE from the same #' comments you’re used to. It supports a limited subset of Roxygen2 tags using only base R.
1#' Add Two Numbers
2#'
3#' @param x First number
4#' @param y Second number
5#' @return The sum
6#' @export
7add <- function(x, y) x + y
1tinyrox::document()
It also includes CRAN compliance checking – catching things like T/F instead of TRUE/FALSE, print() where message() should be, or unquoted package names in your DESCRIPTION. These are the kinds of things that get your submission bounced, and catching them early saves a lot of back-and-forth.
tinypkgr: the devtools replacement
tinypkgr wraps R CMD INSTALL, R CMD build, and R CMD check with functions that do what you’d expect:
1tinypkgr::load_all() # Source R/ files for interactive development
2tinypkgr::install() # R CMD INSTALL
3tinypkgr::reload() # Unload, install, reload -- like devtools::load_all()
4tinypkgr::check() # R CMD build + R CMD check --as-cran
5tinypkgr::build() # R CMD build
For CRAN submission:
1tinypkgr::check_win_devel() # Upload to win-builder
2tinypkgr::submit_cran() # Submit to CRAN
The only dependency is curl, used for the CRAN and win-builder upload functions. Everything else is base R.
The difference from devtools is scope. devtools does a lot of things. tinypkgr does the things we use, and nothing else.
fyi: package introspection for LLMs
This one is different from the others – it’s not a traditional development tool. fyi generates structured markdown about any R package, optimized for LLM consumption. If you’re using an AI coding assistant to work on R packages, fyi gives it the context it needs.
1fyi::fyi("stt.api")
This prints a complete summary: exported functions with signatures, internal functions, option names extracted from source code, and documentation topics. It’s 19-29% more token-efficient than Rd2txt output.
For persistent context, you can generate files:
1fyi::fyi_cache("dplyr")
2# Creates:
3# ~/.fyi/dplyr/fyi.md - Summary
4# ~/.fyi/dplyr/man-md/*.md - Individual help pages as markdown
fyi was inspired by Posit’s btw package. The difference is mechanism: btw uses MCP tools that the LLM calls at runtime. fyi generates static files that a file-reading agent (like Claude Code) can consume directly. Basically, I don’t always want to be running an MCP server for my CLI coding agent. So I generate these files and save them.
For large packages, filtering keeps things manageable:
1fyi::fyi_cache("torch", pattern = "^nn_") # Just neural network modules
rformat: R Core style, automated
rformat formats R code following the conventions found in actual R Core source code:
1rformat::rformat("x<-1+2")
2#> x <- 1 + 2
It’s token-based – it uses utils::getParseData() for tokenization rather than regex – so it handles edge cases correctly. Spaces around operators, space after function, no space before ( in function calls, and proper continuation indentation for long function signatures.
1rformat::rformat_dir("R/") # Format all R files in a directory
The AI runs this before every commit. It’s opinionated in the same way that R Core is opinionated, which means the output looks like the code you find in base R itself.
rapt: R + apt, no Python
If you’re an Ubuntu R user who is tired of getting notifications that your install.packages() didn’t work because you don’t have some system dependency like sf, curl, or magick… Or if you’ve had to go get second breakfast because you’re waiting on the tidyverse to install, you should try r2u. r2u intercepts your install.packages() command and routes it through your system package manager, apt, where dependencies are automatically resolved and packages are downloaded as binaries (courtesy of illinios.edu and Internet2). If you want to get your beach bod ready for summer by skipping second breakfast and/or you just don’t want to wait hours to install your packages when Ubuntu 26.04 drops, you should give r2u a try.
Right now, bspm is the standard way to route install.packages() through your system package manager. It works well. But it depends on Python, D-Bus, and PackageKit – which is a lot of machinery for something that boils down to “run apt install r-cran-dplyr.”
rapt replaces that entire stack with a minimal C daemon (~300 lines) and a pure R package. The daemon (raptd) listens on a Unix socket, validates package names, and calls apt directly. No Python needed.
1# After installing the rapt .deb, this just works:
2install.packages("dplyr")
3#> Installing via apt: dplyr
It ships as a .deb with systemd socket activation, so the daemon only starts when something actually needs to install a package.
Alternatively, if you don’t want the C daemon, you can install just the R package:
1remotes::install_github("cornball-ai/rapt", subdir = "r-pkg")
2rapt::enable()
3install.packages("dplyr") # routes through sudo apt
Dirk Eddelbuettel humorously called this mode “tinyrapt.” It falls back to sudo apt instead of the daemon (unless you apply the permanent sudo fix in the README). It’s somehow both more and less elegant, but it works and the R package itself has zero dependencies.
The workflow
Here’s what I’ve told the AI to use as development cycle using these tools, using littler as the CLI frontend:
1# Format, document, install, and test
2r -e 'rformat::rformat_dir("R"); tinyrox::document(); tinypkgr::install(); tinytest::test_package("mypkg")'
3
4# Full R CMD check
5r -e 'tinypkgr::check()'
6
7# Regenerate LLM context after API changes
8r -e 'fyi::use_fyi_md("mypkg", docs = TRUE)'
Or interactively in R:
1tinypkgr::reload() # See changes immediately
2tinytest::test_package("mypkg") # Run tests
3tinyrox::check_cran() # CRAN compliance check
rformat also supports the vintage R Core style if that’s your thing. The opening brace on its own line is called Allman style (as opposed to K&R where { stays on the same line), and the 8-space fixed continuation indent is how you’ll see long function signatures wrapped in base R source code:
1# Allman braces + fixed 8-space continuation (vintage R Core)
2rformat(code, brace_style = "allman", wrap = "fixed")
3
4# Tab indentation
5rformat(code, indent = "\t")
6
7# Expand inline if-else to multi-line blocks
8rformat(code, expand_if = TRUE)
Dependency counts
Here’s what these packages pull in:
| Package | Dependencies |
|---|---|
| tinyrox | 0 |
| tinypkgr | 1 (curl) |
| fyi | 0 |
| rformat | 0 |
| rapt | 0 (C daemon: libc only) |
For comparison, devtools + roxygen2 + styler together bring in 105 recursive dependencies. That’s not a criticism of those tools – they do more, and they do it well. But if you don’t need the extra features, you no longer need the extra dependencies.
Getting started
1remotes::install_github("cornball-ai/tinyrox")
2remotes::install_github("cornball-ai/tinypkgr")
3remotes::install_github("cornball-ai/fyi")
4remotes::install_github("cornball-ai/rformat")
Then pick what you need:
tinyrox::document()to generate docstinypkgr::check()to run a full checkfyi::fyi_cache("pkg")to generate LLM contextrformat::rformat_dir("R/")to format your code
For rapt, see the installation instructions, it’s not a typical R install and requires r2u to be set up.
All five packages are open source. Contributions welcome.