libredb is a small command-line tool for inspecting and editing .libredb
files — no code required. It is built on the public API, ships with the package,
and has zero dependencies (it uses Node/Bun's built-in parseArgs).
Because LibreDB is an embedded database, the CLI is a local tool — like
sqlite3 or cat: it opens the file path you give it, on your machine. There is
no server and no daemon.
Three ways to run the exact same CLI: via the package (
npx libredb, below), a standalone binary (no Node/Bun needed), or a Docker image. The command set in this document applies to all three.
# one-off, via the published package (no install)
npx libredb <command> <path> [args] # npm
bunx libredb <command> <path> [args] # bun
# or install it
npm install -g @libredb/libredb # then: libredb <command> ...Run with no arguments (or --help) to print usage:
$ libredb
libredb - inspect and edit .libredb files
Usage:
libredb inspect <path> List each namespace, its kind, and table schemas
libredb stats <path> Summarize the file: size and namespace counts
libredb get <path> <key> Print the value stored at a key
libredb scan <path> <prefix> Print key=value for every key under a prefix
libredb export <path> <file.json> Dump every key to a JSON object (the shape import reads)
libredb set <path> <key> <value> Set a key to a value
libredb delete <path> <key> Remove a key
libredb import <path> <file.json> Bulk-set keys from a JSON object (one atomic commit)
Options:
--force Remove a write lock whose holder is no longer alive
--raw Print values verbatim (default escapes control characters)These open the file read-only and never modify it (see Safety).
Lists the file size and each catalogued namespace with its kind, plus the schema for relational tables.
$ libredb inspect app.libredb
app.libredb 412 bytes
logs document
people relational {"primaryKey":"id","columns":{"id":"string","name":"string"}}Note: only
documentandrelationalnamespaces are catalogued; plain key-value pairs are the raw layer and are not listed here. A file holding only kv data prints(no catalogued namespaces)— its keys are still readable withget/scan.
$ libredb stats app.libredb
app.libredb 412 bytes 2 namespaces
kv: 0 document: 1 relational: 1$ libredb get app.libredb user:1
AdaExits non-zero (1) with key not found: <key> if the key is absent.
Prints key=value, one per line, in the kernel's ascending byte order.
$ libredb scan app.libredb user:
user:1=Ada
user:2=GraceThe counterpart of import below: it writes the same JSON shape import
reads — one object of string values — so a dump round-trips back into a database.
$ libredb export app.libredb backup.json
export 4 keys
$ cat backup.json
{
"color": "teal",
"logs:l1": "{\"message\":\"hi\"}",
"user:1": "Ada",
"user:2": "Grace"
}
$ libredb import restored.libredb backup.json # restore into a fresh file
import 4 keysWhat a dump covers, exactly:
- The key-value layer — the raw layer, so
documentandrelationaldata appears as the internal prefixed entries those lenses store (a documentl1in collectionlogsis the keylogs:l1holding its JSON). There is no per-lens export in v1: one dump, one flat object. - Not the reserved namespace. LibreDB's
\x00-prefixed catalog space is left out, becauseimportrefuses to write reserved keys (see Safety). A restored file therefore holds every row but no catalog entry, soinspectlists nothing until a lens registers a namespace again. For a byte-exact copy, copy the file — see Backup and restore. - UTF-8 text only. Every lens and every CLI command writes well-formed
UTF-8. If a database holds raw non-UTF-8 bytes — only reachable by writing
through the kernel API directly —
exportrefuses rather than emit replacement characters that would import back as different data.
Escaping is JSON.stringify's (quotes, backslashes, control characters,
Unicode), so values are stored verbatim rather than terminal-escaped the way
get/scan print them; --raw does not apply. The output file is overwritten
if it exists, like a shell redirect, and its parent directory must already exist.
import merges into its target, so restore into a fresh path unless you mean
to overlay.
These take an advisory lock (see Safety) and commit through the WAL.
$ libredb set app.libredb user:1 Ada
set user:1 (1 changed)$ libredb delete app.libredb user:1
delete user:1 (1 removed) # "(0 removed)" if the key did not existReads a JSON object of string values and sets every pair in a single transaction — the whole load lands, or (on a crash mid-write) none of it does.
$ cat seed.json
{ "user:1": "Ada", "user:2": "Grace", "color": "teal" }
$ libredb import app.libredb seed.json
import 3 keysThe JSON must be an object whose values are all strings; anything else is a usage error.
The CLI touches real database files, so it is deliberately careful:
- Reads never mutate the file. Opening a database runs crash recovery, which
would normally truncate a torn tail — a write. Read commands
(
inspect/stats/get/scan/export) open through a read-only filesystem adapter: recovery drops a torn tail in memory only; the bytes on disk are left exactly as found. That adapter also has no lock at all, so a read never creates a<path>.lock—exportcan dump a file a live writer holds open. - A wrong path cannot destroy a file. Opening a file that is not a LibreDB
database (a typo, a text file) fails with a clear error and leaves the file
byte-for-byte untouched — the on-disk
LRDBheader is checked before anything is written. - Writes hold the exclusive open lock. The library itself locks the database
on open (
<path>.lock, recording the holder's pid and host), so a second writer — this CLI against a live app, or two CLI invocations — fails loudly instead of silently corrupting the file. A lock whose holder is verifiably dead is reclaimed automatically;--forceadditionally removes a lock that cannot be verified (for example one from another machine), but refuses a verifiably live holder and refuses to delete a file that is not a libredb lock. - Output is escaped by default.
get/scanprint stored values with control characters escaped (\x1b,\x07, ...), so a value containing terminal escape sequences cannot clear your screen, retitle your terminal, or write your clipboard when you inspect an untrusted file. Pass--rawfor the exact bytes. - Reserved keys are refused. Writes reject keys in LibreDB's reserved
namespace (the
\x00-prefixed catalog space), so the CLI cannot corrupt the catalog or another lens's layout. - Bulk imports are atomic.
importcommits all keys in one transaction.
The WAL is the database: one .libredb file holds everything, so backup is a
file copy — with one rule.
-
Backup: copy the file while no writer has it open (no
<path>.lockpresent, or only your own closed session). A copy taken mid-write can split a record in half; the copy would then open only up to the split.cp app.libredb backup/app-$(date -u +%Y%m%d).libredb -
Restore: copy the file back and open it — recovery replays it like any reopen. Nothing else to do.
-
Export as JSON:
libredb export <path> <file.json>dumps the key-value layer as an import-compatible object — seeexportfor exactly what it covers. Restore it withlibredb import. This is a logical dump and not a replacement for the file copy above: the copy is byte-exact (it carries the catalog and the log itself), while a dump carries only the keysimportcan write back.
| Code | Meaning | Examples |
|---|---|---|
0 |
Success | a read/write completed |
1 |
Runtime error | file not found, get on a missing key, lock held by another writer |
2 |
Usage error | unknown command/option, missing argument, malformed import JSON, reserved key |
This makes the CLI scriptable — e.g. in CI:
libredb get app.libredb migration:done >/dev/null 2>&1 || libredb set app.libredb migration:done "$(date -u +%FT%TZ)"get/scan/export/set/delete/importoperate on the key-value layer (UTF-8 string keys and values).inspect/statsread the catalog for the richer document/relational view.- There is no interactive
repl(it was intentionally left out for now). - The CLI is one of three identical front-ends — see the standalone binary and Docker image for the same commands without a Node/Bun install.
- For the programmatic API behind these commands, see the lens guides.