- Build: Replaced unittest with pytest in the CI workflow, restoring test coverage execution.
- Build: build: add GitHub Actions CI workflow
Newest at the top.
-
rd completion bash|zsh|fishprints a shell completion script. It is generated by walkingbuild_parser(), so the parser stays the single source of truth and the completion cannot drift from the commands a build actually has. Completes commands, nested subcommands, flags, and fixed positional choices (rd completion <TAB>offers the three shells).Install:
rd completion bash > ~/.local/share/bash-completion/completions/rd rd completion zsh > "${fpath[1]}/_rd" rd completion fish > ~/.config/fish/completions/rd.fish
Still zero dependencies: argcomplete would have been one, so the emitters are hand-rolled. The cost is reading argparse's private structures, which is contained by a guard test that names exactly what is relied on and fails with a pointed message if a Python release moves any of it.
The generated bash and zsh scripts are checked in CI by the shells' own parsers, and the bash one is additionally sourced and driven to confirm it really completes. The fish check skips where fish is not installed rather than pretending to pass.
rd open <id>...opens raindrops in your browser. Takes any number of ids.--cache(alias--permanent) opens the archived permanent copy instead of the original link: that endpoint answers307with the storage URL in a header, so the client asks for it with redirects suppressed and readsLocationrather than following it and downloading the copy. The archive is a PRO feature and only some links are stored, so a missing copy reports that plainly instead of opening the wrong thing.--printemits the URL and launches nothing, which is what you want over SSH or in a pipe.- Confirmation prompts on destructive operations. The guard used to be
--dry-runand nothing else, which only helps if you remember to type it first. Prompts are gated on blast radius rather than on every write, so the common path stays quiet:- Unbounded: scope mode on
rd rm,rd mv, andrd tag --clear, where--fromcan match any number of raindrops. The prompt counts them first and names the number. - Irreversible:
rd rm --permanent,rd collections empty-trash, andrd tags rm.rd collections rmalso asks, because deleting a collection takes its contents along with it. - Not prompted: removing by id to Trash (recoverable), and appending tags in scope mode (additive).
- Unbounded: scope mode on
-y/--yesskips the prompts, andRD_ASSUME_YES=1does the same for cron and scripts that cannot answer one.--dry-runbypasses confirmation entirely, since it performs no writes and its whole purpose is to show you what would happen.
- A non-interactive stdin refuses rather than prompting. Blocking on a read no one can answer would hang a script forever, and assuming yes would delete things nobody agreed to.
- The prompt is written to stderr, so confirming does not contaminate a redirected stdout.
- The affected-item count is read opportunistically. The list endpoint is not documented to return a total, so a missing count produces "every raindrop in collection X" rather than a confidently wrong number.
rd sync: two-way additive sync between Raindrop and Pinboard. Matches bookmarks across the two services by a normalized URL (scheme/www/fragment folded, tracking params likeutm_*/fbclidstripped, meaningful query kept), which doubles as the cross-service dedup key. It only ever adds and merges, never deletes, so the two libraries converge to their union with no data loss.- The model gap is bridged reversibly in tags: a Raindrop collection becomes a
slugged Pinboard tag, Pinboard's
toreadand Raindrop'simportantride along as tags, and a Pinboard tag that matches a collection routes the item back into that collection. Highlights stay Raindrop-only. On a URL that exists on both sides, tags are unioned and notes are merged idempotently (no duplication on repeat runs). - Scoping so you never have to union everything at once:
--direction both|to-pinboard|to-raindrop, and--collection/--rd-tag/--pb-tagto restrict which items are pushed. Scope narrows what is written, but matching always uses the full sets, so an out-of-scope item that already exists on the other side is never re-imported as a duplicate. --dry-runprints the plan (counts per direction, near-dupes collapsed) and writes nothing. The planning half (sync.plan_syncand the mapping helpers) is pure and covered by unit tests independent of the network.
- Pinboard as a second bookmarking backend, alongside Raindrop. A new
rd pinboard(aliaspb) command group speaks Pinboard's flat model natively (bookmarks keyed by URL, tags, notes, and thetoread/sharedflags) instead of pretending it has Raindrop's collections:pinboard list|get|add|rm|edit| tag|suggest,pinboard tags list|rename|rm, andpinboard notes list|view. PinboardClient, a stdlib sibling ofRaindropClient: auth through theauth_tokenquery param,format=jsonon every call, a minimum inter-request pacer for Pinboard's strict rate limit (about one call every three seconds) on top of the usual429backoff, and the shared typed-error family plus--dry-runand--jsonbehavior. Pinboard writes are all GETs, so they are flagged explicitly rather than inferred from the HTTP method.- Pinboard token resolution mirrors Raindrop:
PINBOARD_TOKEN(orPINBOARD_API_TOKEN) env var,pinboard_tokeninconfig.toml, or a.envfile;rd config set-pinboard-token <token>writes it (0600). Both service tokens coexist in the one config file without clobbering each other. - Pinboard has no update endpoint, so
editandtagare a read-modify-write: fetch the bookmark, merge the change, and re-save withreplace=yes, leaving untouched fields intact.
--dry-runno longer mislabels a bodyless request as<multipart>. A plain DELETE or PUT with no body now previews as<no body>, JSON writes preview as their JSON (unchanged), and multipart uploads preview as<multipart ... files=[...]>without dumping the raw file bytes. Extracted the logic into_dry_run_previewwith direct unit coverage.
The framework rebuild. The barebones prototype became a dependency-free, tested, fully documented CLI.
- Complete API coverage: raindrops (single, batch, suggest, file/cover upload,
export), collections (list, tree, view, add, edit, remove, merge, clean,
empty-trash, reorder, cover, cover search), tags (list, rename, merge,
remove), highlights (list, add, edit, remove), plus
user(show +set),stats,filters,suggest,exists(import dedup), HTML-file import, andbackups(list, create, download). - Bulk/reorganization commands:
rd mv, multi-id and scoperd rm(--permanent),rd tag(add/remove/clear), andrd add --file/--stdinfor batch create. Explicit ids loop the single-item endpoints;--from <collection>uses the batch endpoints for whole-collection scope. (Grounded in an empirically verified quirk: the batch endpoints only touch raindrops actually in the path collection, so a naive id-based batch move silently no-ops. Two other CLIs surveyed carry exactly that latent bug.) --dry-run: previews every write (logs method + payload to stderr) without calling the API; reads still run so a plan can be built first.- Grouped command surface (
rd collections tree,rd tags rename,rd highlights add, ...) withc/t/hshort aliases, keeping the original flat commands (c-list,t-list,h-list, ...) working as hidden aliases. --allto auto-paginate list and highlight reads.rd config path|show|set-token; token also resolvable from~/.config/rd-cli/config.tomland.env, withRAINDROP_TOKENas the primary env var (RAINDROP_TEST_TOKENstill honored).- TTY-aware ANSI output (Kanagawa-ish palette): colour on a terminal, plain when
piped,
NO_COLORand--no-colorrespected. Nested collection tree view and aligned tag/collection columns. --version, and--jsonnow works before or after the subcommand.- pytest suite exercising the client, config, output, and CLI against a fake urllib transport (no network); ruff lint/format configured.
- Framework docs: comprehensive
CLAUDE.md(Raindrop API + codebase),spec.md,roadmap.md,logo.svg, single-sourceVERSION.
- Ported the whole client from
requeststo stdliburllib; removed therequestsandpython-dotenvdependencies (zero runtime deps now). - Split the two-file prototype into a package:
errors,config,client,output,commands,cli. addnow auto-parses page metadata by default (title, cover, type) unless--no-parse; default target collection is Unsorted.
- Requests now use a timeout (previously could hang forever).
- Boolean query params are sent lowercase (
nested=true); the API rejected the previousTrue/False. - API error messages surface to the user (the
errorMessagefrom the response) instead of a bare HTTP status. - Rate-limit (
429) and transient5xxresponses retry with backoff instead of failing immediately (Retry-Afterparsed as seconds or an HTTP-date). rm --permanentuses the documented two-step delete; the undocumented?permanent=truequery param was tested and does not one-shot a live raindrop.- Removed a hard-coded personal
.envpath that leaked into the repo.