|
|
||
|---|---|---|
| src | ||
| test | ||
| .gitignore | ||
| bun.lock | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
linkwarden-cli
A command-line client for Linkwarden, the self-hosted bookmark manager. It runs on Bun, has no runtime dependencies, and covers the Linkwarden REST API: links, collections, tags, highlights, RSS subscriptions, access tokens, account settings, import and export.
It was written from the Linkwarden server source (API routes and validation schemas, v2.16) and checked against a real v2.16.3 server.
Install
Bun 1.2 or later is required. Install from npm:
bun install -g linkwarden-cli
Or install straight from the Git repository:
bun install -g git+https://git.kaki87.net/VibedByKaKi/linkwarden-cli.git
Both put the linkwarden and lw commands on your PATH.
To build a standalone binary that does not need Bun on the target machine:
bun run build # writes dist/linkwarden
Log in
linkwarden login --url https://links.example.com
The command asks for a username and password, creates a session on the server, and saves the token. For accounts that sign in with SSO, create an access token in Settings > Access Tokens and run:
linkwarden login --url https://links.example.com --token <token>
For scripts, use --password-stdin, or skip the config file and set LINKWARDEN_URL and LINKWARDEN_TOKEN.
The config file is ~/.config/linkwarden/config.json ($XDG_CONFIG_HOME and $LINKWARDEN_CONFIG are respected) and is created with mode 0600. Each login can use its own profile:
linkwarden login --profile work --url https://links.work.example --token <token>
linkwarden profile list
linkwarden profile use work
linkwarden --profile home ls
Credentials come from, in order, the --url and --token flags, the LINKWARDEN_URL and LINKWARDEN_TOKEN variables, and the selected profile (--profile, LINKWARDEN_PROFILE, or the profile set with profile use).
Everyday use
linkwarden add https://example.com -c Reading -t article,todo # save a link
linkwarden add -f paper.pdf -c Papers # upload a PDF or image
cat urls.txt | linkwarden add -c Inbox # bulk add from stdin
linkwarden ls # recent links
linkwarden search "docker compose" -c Dev # search
linkwarden ls -t todo --pinned # filter by tag and pin
linkwarden links get 42 # details and stored copies
linkwarden links update 42 -n "New title" --add-tag later -c Archive
linkwarden links pin 42
linkwarden open 42 # open in the browser
linkwarden links download 42 -f pdf # save the stored PDF
linkwarden links delete 42 -y
Run linkwarden --help for the command list and linkwarden <command> --help for options and examples.
Collections can be named or numbered, and nested ones use a path such as Dev/Rust. The CLI looks names up on the server, so add -c "Dev/Rust" never creates a collection by accident. Pass --create-collection to create one on purpose.
Search syntax
The query matches name, URL, description and tags. When the server runs Meilisearch it also accepts filters, and a leading ! negates one:
tag:<name> collection:<name> type:<url|pdf|image> url:<text> name:<text>
description:<text> pinned:true public:true before:<date> after:<date>
linkwarden search 'tag:rust !collection:Archive after:2025-01-01'
Commands
| Command | What it does |
|---|---|
login, logout, whoami, profile |
Authentication and saved profiles |
add, ls, search, open |
Shortcuts for the most common links commands |
links |
list, search, get, add, update, delete, pin, unpin, open, refresh, clear-archives, download |
collections |
list, get, create, update, delete, share, unshare |
tags |
list, create, rename, delete, merge |
highlights |
list, add, delete |
rss |
list, add, delete |
tokens |
list, create, revoke |
settings |
get, set for archive formats, duplicate prevention, link click behavior and AI tagging |
dashboard |
Pinned and recent links |
export, import |
Linkwarden JSON backup, browser HTML, Wallabag, Omnivore and Pocket files |
public |
Read public collections without a token |
info |
Server configuration |
api |
Call any endpoint directly, for example linkwarden api GET /search --query searchQueryString=rust |
completion |
Shell completion for bash, zsh and fish |
Scripting
Every command accepts --json and prints the API objects unchanged. With -q, commands print only ids, one per line:
linkwarden ls -t todo -q | xargs linkwarden links update --add-tag reviewed
linkwarden ls --all --json | jq -r '.links[].url'
Prompts and progress go to stderr, so stdout stays clean for pipes. Deleting commands refuse to run without --yes when stdin is not a terminal.
Exit codes: 0 success, 1 failure (including API errors), 2 usage error, 3 authentication or configuration error. Set NO_COLOR or pass --no-color to turn off colors.
Behavior worth knowing
- The server returns 50 links per page.
--limitstops early,--allfollows every page, and--cursorresumes from a previous page. The--jsonoutput includesnextCursor. - Linkwarden ORs the tag filter with the other conditions, so
--tagand--pinnedare applied again on the client. A tag filter on a large library can therefore fetch several pages. - Updating a link replaces its tag list on the server, so
updatereads the link first and sends the merged result. - Updating a collection recreates its member list, so
collections updatesends the existing members back unchanged. logoutremoves the profile locally. To invalidate the token, revoke it withlinkwarden tokens revoke <id>.
Development
git clone https://git.kaki87.net/VibedByKaKi/linkwarden-cli.git
cd linkwarden-cli
bun install
bun link # optional, installs the commands from the checkout
bun test # unit tests and tests against an in-memory mock server
bun run typecheck
The mock server in test/mock-server.ts follows the Linkwarden route handlers. To run the suite in test/e2e against a real server:
docker compose -f test/e2e/compose.yml -p lw-e2e up -d
LINKWARDEN_E2E_URL=http://localhost:13000 bun test test/e2e
docker compose -p lw-e2e down -v
The project uses Bun for everything: Bun.serve, Bun.file, Bun.stringWidth and bun:test replace the usual dependencies, and node:util parseArgs handles option parsing.
License
MIT