raindrop-cli 0.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,530 @@
1
+ Metadata-Version: 2.5
2
+ Name: raindrop-cli
3
+ Version: 0.5.2
4
+ Summary: A stdlib-only command-line client for Raindrop.io
5
+ Project-URL: Homepage, https://github.com/VirInvictus/rd-cli
6
+ Author: Brandon LaRocque
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: bookmarks,cli,raindrop,raindrop.io
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+
13
+ <p align="center">
14
+ <img src="logo.svg" alt="rd-cli" width="96" height="96">
15
+ </p>
16
+
17
+ # rd-cli
18
+
19
+ A fast, dependency-free command-line client for [Raindrop.io](https://raindrop.io).
20
+ Read, add, edit, reorganize, and remove your bookmarks (raindrops), collections,
21
+ tags, and highlights from the terminal, with designed ANSI output for humans and
22
+ `--json` for scripts and AI agents.
23
+
24
+ <p align="center">
25
+ <img src="docs/screenshots/list.png" alt="rd-cli listing a collection: bookmark ids, titles, and URLs in designed ANSI colour, followed by account stats">
26
+ </p>
27
+
28
+ - **Zero runtime dependencies.** Pure Python standard library (`urllib`, `json`,
29
+ `tomllib`, `argparse`). Nothing to audit, nothing to break.
30
+ - **Full API coverage.** Every practical Raindrop REST API v1 endpoint:
31
+ raindrops (single, bulk, upload, suggest, export), collections (including
32
+ merge, reorder, clean, covers), tags, highlights, user (read and edit), stats,
33
+ filters, import dedup, HTML-file import, and backups.
34
+ - **Built for reorganizing at scale.** Bulk move, tag, and delete by id or by
35
+ whole-collection scope, plus a global `--dry-run` that previews every write.
36
+ - **Resilient.** Per-request timeouts, typed errors that surface the API's own
37
+ message, and automatic retry with backoff on rate limits and server errors.
38
+ - **Terminal-native.** Colour on a TTY, plain when piped; `NO_COLOR` and
39
+ `--no-color` respected.
40
+
41
+ ## Contents
42
+
43
+ - [Install](#install)
44
+ - [Authentication](#authentication)
45
+ - [Quick start](#quick-start)
46
+ - [Global options](#global-options)
47
+ - [Command reference](#command-reference)
48
+ - [Raindrops](#raindrops)
49
+ - [Bulk operations and the id-list vs scope model](#bulk-operations-and-the-id-list-vs-scope-model)
50
+ - [Collections](#collections)
51
+ - [Tags](#tags)
52
+ - [Highlights](#highlights)
53
+ - [Account: user, stats, filters, suggest, exists](#account)
54
+ - [Import, export, backups](#import-export-backups)
55
+ - [Pinboard](#pinboard)
56
+ - [Sync (Raindrop and Pinboard)](#sync-raindrop-and-pinboard)
57
+ - [Config](#config)
58
+ - [Output modes](#output-modes)
59
+ - [Exit codes](#exit-codes)
60
+ - [System collection ids](#system-collection-ids)
61
+ - [Rate limits and retries](#rate-limits-and-retries)
62
+ - [Using rd-cli as a library](#using-rd-cli-as-a-library)
63
+ - [Development](#development)
64
+ - [License](#license)
65
+
66
+ ## Install
67
+
68
+ Requires Python 3.11 or newer (the floor for the standard-library `tomllib`).
69
+
70
+ ```bash
71
+ pipx install raindrop-cli
72
+ ```
73
+
74
+ After installation the `rd` command is on your `PATH`. You can also run it
75
+ without installing via `python -m rd_cli` in a checkout.
76
+
77
+ ## Authentication
78
+
79
+ Every call is authenticated with a bearer token. For personal use, the simplest
80
+ option is a **test token**, which does not expire and is scoped to your own
81
+ account:
82
+
83
+ 1. Open [App Management Console](https://app.raindrop.io/settings/integrations).
84
+ 2. Create an application (any name).
85
+ 3. Copy its **Test token**.
86
+
87
+ Provide the token in any of the following ways. Resolution stops at the first
88
+ one found:
89
+
90
+ | Priority | Source | Notes |
91
+ | -------- | ------ | ----- |
92
+ | 1 | `RAINDROP_TOKEN` environment variable | Recommended for shells and CI. |
93
+ | 2 | `RAINDROP_TEST_TOKEN` environment variable | Back-compat alias. |
94
+ | 3 | `token` in `~/.config/rd-cli/config.toml` | Written by `rd config set-token`, `0600`. |
95
+ | 4 | `RAINDROP_TOKEN` / `RAINDROP_TEST_TOKEN` in a `.env` file | `./.env`, then `~/.config/rd-cli/.env`. Real env vars always win. |
96
+
97
+ ```bash
98
+ export RAINDROP_TOKEN=your-token-here # option 1: environment
99
+ rd config set-token your-token-here # option 3: config file (chmod 0600)
100
+ echo 'RAINDROP_TOKEN=your-token-here' > .env # option 4: local .env
101
+ ```
102
+
103
+ `XDG_CONFIG_HOME` is honoured, so the config directory follows your XDG setup.
104
+ Full OAuth2 (login flow + refresh) is planned but not yet implemented; the test
105
+ token covers single-user needs.
106
+
107
+ For the optional **Pinboard** support (see [Pinboard](#pinboard) and
108
+ [Sync](#sync-raindrop-and-pinboard)), add a Pinboard API token, format
109
+ `user:HEX`, from [pinboard.in/settings/password](https://pinboard.in/settings/password),
110
+ the same three ways: the `PINBOARD_TOKEN` (or `PINBOARD_API_TOKEN`) environment
111
+ variable, `rd config set-pinboard-token <token>`, or `PINBOARD_TOKEN=` in a
112
+ `.env` file. Both service tokens live side by side in the one `config.toml`.
113
+
114
+ ## Quick start
115
+
116
+ ```bash
117
+ rd user # who am I?
118
+ rd stats # counts across All / Unsorted / Trash
119
+ rd list --all # every bookmark, all pages
120
+ rd add "https://example.com" -t "Example" --tags read-later
121
+ rd search "python #tutorial" --detailed
122
+ ```
123
+
124
+ ## Global options
125
+
126
+ These work with any command and may appear **before or after** the subcommand
127
+ (`rd --json list` and `rd list --json` are equivalent):
128
+
129
+ | Flag | Effect |
130
+ | ---- | ------ |
131
+ | `--json` | Emit a single JSON document to stdout and nothing else. See [Output modes](#output-modes). |
132
+ | `--no-color` | Force plain text even on a TTY. |
133
+ | `--dry-run` | Preview every **write**: log its method and payload to stderr and skip the API call. Reads still run, so you can plan a change safely first. |
134
+ | `-y`, `--yes` | Answer every confirmation prompt with yes. See [Confirmation](#confirmation). |
135
+ | `--version` | Print the version and exit. |
136
+ | `-h`, `--help` | Show help for the program or any subcommand. |
137
+
138
+ `--dry-run` is enforced in one place (the HTTP layer) for any non-GET request,
139
+ so it reliably covers every command that changes data, including bulk ones.
140
+
141
+ ### Confirmation
142
+
143
+ Operations that can destroy an unbounded or unrecoverable amount of data ask
144
+ before proceeding. The gate is blast radius, not every write, so the everyday
145
+ path stays quiet:
146
+
147
+ | Asks | Why |
148
+ | ---- | --- |
149
+ | `rd rm --from`, `rd mv --from`, `rd tag --from --clear` | Scope mode matches an arbitrary number of raindrops. The prompt counts them first and tells you how many. |
150
+ | `rd rm --permanent` | Skips Trash. There is no undo. |
151
+ | `rd collections rm` | Takes the collection's raindrops with it. |
152
+ | `rd collections empty-trash`, `rd tags rm` | Unrecoverable, and afterwards there is no way to enumerate what was affected. |
153
+
154
+ Removing by id to Trash is **not** prompted (Trash is recoverable), and neither
155
+ is appending tags in scope mode (additive).
156
+
157
+ To skip the prompts, pass `-y`/`--yes`, or set `RD_ASSUME_YES=1` for cron jobs
158
+ and scripts that cannot answer one. `--dry-run` bypasses confirmation entirely,
159
+ because it performs no writes and showing you the plan is the entire point.
160
+
161
+ Two deliberate behaviours worth knowing:
162
+
163
+ - The prompt is written to **stderr**, so confirming never contaminates a
164
+ redirected or piped stdout.
165
+ - A **non-interactive stdin refuses** instead of prompting. Blocking on a read
166
+ nobody can answer would hang a script forever, and silently assuming yes would
167
+ delete data nobody agreed to. Pass `--yes` when you mean it.
168
+
169
+ ## Command reference
170
+
171
+ Run `rd <command> --help` or `rd <group> <action> --help` for the exact flags of
172
+ any command. Grouped commands also have one-letter aliases: `c` for
173
+ `collections`, `t` for `tags`, `h` for `highlights`.
174
+
175
+ ### Raindrops
176
+
177
+ | Command | Description |
178
+ | ------- | ----------- |
179
+ | `rd list` | List raindrops. Flags: `-c/--collection <id>` (default `0` = all), `-s/--search <query>`, `--sort <key>`, `--page <n>`, `--perpage <n>` (max 50), `-a/--all` (fetch every page), `-n/--nested` (include nested collections), `-d/--detailed` (show excerpt, note, tags). |
180
+ | `rd search <query>` | Shorthand for `list` with a positional search query. Same flags as `list`. |
181
+ | `rd view <id>` | Show one raindrop in full (link, domain, type, dates, collection, excerpt, note, tags, highlights). |
182
+ | `rd open <ids...>` | Open raindrop(s) in your browser. `--cache` (alias `--permanent`) opens the archived permanent copy instead of the original link (PRO, and only some links are stored). `-p/--print` prints the URL and launches nothing, for SSH sessions and pipes. |
183
+ | `rd add <url>` | Create a raindrop. Flags: `-t/--title`, `-c/--collection` (default `-1` = Unsorted), `--tags <t...>`, `--excerpt`, `--note`, `--important`, `--no-parse` (skip background metadata fetch), and `--file <path>` / `--stdin` for [bulk add](#bulk-operations-and-the-id-list-vs-scope-model). |
184
+ | `rd edit <id>` | Update a raindrop. Flags: `-t/--title`, `--tags <t...>` (replaces), `-c/--collection` (move), `--excerpt`, `--note`, `--important` / `--not-important`. |
185
+ | `rd rm <ids...>` | Move raindrop(s) to Trash. `--permanent` deletes for good; scope flags `--from`, `-s`, `-n` remove a whole collection. See [bulk](#bulk-operations-and-the-id-list-vs-scope-model). |
186
+ | `rd mv <dest> <ids...>` | Move raindrop(s) into collection `<dest>`. Scope flags `--from`, `-s`, `-n`. |
187
+ | `rd tag <ids...>` | Modify tags. Flags: `--add <t...>`, `--remove <t...>`, `--clear`. Scope flags `--from`, `-s`, `-n`. |
188
+ | `rd cover <id> <file>` | Upload a cover image (PNG, GIF, or JPEG) for a raindrop. |
189
+ | `rd import <file>` | Parse a Netscape/Pocket/Instapaper HTML export to JSON. Add `--create -c <id>` to actually import the bookmarks into a collection. |
190
+ | `rd export` | Export raindrops. Flags: `-c/--collection` (default `0`), `-f/--format {csv,html,zip}`, `--sort`, `-s/--search`, `-o/--output <file>` (otherwise stdout). |
191
+
192
+ ```bash
193
+ rd list -c 0 --detailed # everything with excerpts and tags
194
+ rd list --all --json > all.json # every page as JSON
195
+ rd add "https://example.com" --tags ai read-later --important
196
+ rd add --file urls.txt -c 12345 # one URL per line, chunked at 100
197
+ rd edit 12345 --note "revisit" --important
198
+ rd export -c 0 -f csv -o bookmarks.csv
199
+ rd import pocket.html --create -c 12345 # import an export file
200
+ ```
201
+
202
+ `add` auto-parses page metadata (title, cover, type) in the background unless you
203
+ pass `--no-parse`. When you supply only a URL, a title is fetched for you.
204
+
205
+ ### Bulk operations and the id-list vs scope model
206
+
207
+ `mv`, `rm`, and `tag` each work in two modes, and the distinction matters
208
+ because of how the Raindrop API scopes its batch endpoints:
209
+
210
+ - **Id-list mode** (`rd mv 999 111 222`, `rd rm 1 2 3`, `rd tag 1 2 --add x`)
211
+ operates on the raindrop ids you name. Internally it loops the single-item
212
+ endpoints, which is **correct no matter which collection each raindrop lives
213
+ in**.
214
+ - **Scope mode** (`--from <collection>`, optionally narrowed with `-s <search>`
215
+ and `-n/--nested`) operates on **every raindrop in that source collection** in
216
+ a single batch call. `--from` is required for scope, and it must be a real
217
+ collection: the batch endpoints do not accept the `0` (all) pseudo-collection,
218
+ and they only touch raindrops that are actually in the given collection.
219
+
220
+ ```bash
221
+ # Id-list mode
222
+ rd mv 999 111 222 333 # move three raindrops into collection 999
223
+ rd rm 111 222 333 # trash three raindrops
224
+ rd rm 111 222 --permanent # delete them for good (skips Trash)
225
+ rd tag 111 222 --add ai --remove old # precise add + remove per raindrop
226
+ rd tag 111 --clear # remove all tags from a raindrop
227
+
228
+ # Scope mode (whole collection, one batch call)
229
+ rd mv 999 --from 111 # move everything in 111 into 999
230
+ rd rm --from 111 -s "is:broken" # trash everything matching a search in 111
231
+ rd tag --from 111 --add reviewed # append a tag to every raindrop in 111
232
+ rd tag --from 111 --clear # strip all tags across a collection
233
+
234
+ # Preview any of the above without touching the API
235
+ rd --dry-run mv 999 --from 111
236
+
237
+ # Scope and permanent operations confirm first; -y answers up front
238
+ rd rm --from 111 -y
239
+ ```
240
+
241
+ Notes:
242
+
243
+ - `tag` in id-list mode does precise add/remove/clear (it reads each raindrop's
244
+ current tags, computes the new set, and writes it back). In scope mode the API
245
+ can only **append** (`--add`) or **clear all** (`--clear`); to strip one
246
+ specific tag from every raindrop, use `rd tags rm <tag>`.
247
+ - `rm --permanent` deletes via the documented two-step (to Trash, then from
248
+ Trash). Deleting a raindrop that is already in Trash also removes it
249
+ permanently.
250
+
251
+ ### Collections
252
+
253
+ | Command | Description |
254
+ | ------- | ----------- |
255
+ | `rd collections list` | List root collections with ids and counts. |
256
+ | `rd collections tree` | Nested tree of all collections, indented by parent. |
257
+ | `rd collections view <id>` | Show one collection. |
258
+ | `rd collections add <title>` | Create a collection. Flags: `--view {list,simple,grid,masonry}`, `--parent <id>`, `--public`. |
259
+ | `rd collections edit <id>` | Update a collection. Flags: `-t/--title`, `--view`, `--parent` (re-nest), `--public` / `--private`. |
260
+ | `rd collections rm <id>` | Delete a collection (its raindrops move to Trash). |
261
+ | `rd collections merge <to> <ids...>` | Merge the listed collections into `<to>`. |
262
+ | `rd collections clean` | Remove all empty collections. |
263
+ | `rd collections empty-trash` | Permanently empty Trash. |
264
+ | `rd collections reorder --by <key>` | Reorder all collections. `<key>` is `title`, `-title`, or `-count`. |
265
+ | `rd collections cover <id> <file>` | Upload a collection cover image. |
266
+ | `rd collections covers <text>` | Search Raindrop's icon/cover library. |
267
+
268
+ ```bash
269
+ rd collections tree
270
+ rd collections add "Reading" --public --view grid
271
+ rd collections merge 111 222 333 # fold 222 and 333 into 111
272
+ rd collections reorder --by=-count # note: leading-dash values need '='
273
+ rd collections covers pokemon
274
+ ```
275
+
276
+ > Values that start with a dash (`-count`, `-title`) must be attached with `=`,
277
+ > e.g. `--by=-count`, because argparse otherwise reads them as flags.
278
+
279
+ ### Tags
280
+
281
+ | Command | Description |
282
+ | ------- | ----------- |
283
+ | `rd tags list` | List tags with counts. `-c/--collection <id>` restricts to one collection. |
284
+ | `rd tags rename <old> <new>` | Rename a tag (merges into `<new>` if it exists). `-c` to scope. |
285
+ | `rd tags merge <into> <tags...>` | Merge several tags into `<into>`. `-c` to scope. |
286
+ | `rd tags rm <tags...>` | Delete tags (removes them from every raindrop). `-c` to scope. |
287
+
288
+ ```bash
289
+ rd tags list
290
+ rd tags rename ml machine-learning
291
+ rd tags merge ai artificial-intelligence machine-learning # into "ai"
292
+ rd tags rm obsolete-tag
293
+ ```
294
+
295
+ ### Highlights
296
+
297
+ | Command | Description |
298
+ | ------- | ----------- |
299
+ | `rd highlights list` | List highlights. `-r/--raindrop <id>` for one raindrop; otherwise all highlights, with `-a/--all`, `--page`, `--perpage`. |
300
+ | `rd highlights add <raindrop> <text>` | Add a highlight. `--color <name>`, `--note <text>`. |
301
+ | `rd highlights edit <raindrop> <highlight>` | Edit a highlight. `--text`, `--color`, `--note`. |
302
+ | `rd highlights rm <raindrop> <highlight>` | Remove a highlight. |
303
+
304
+ Colours: `blue`, `brown`, `cyan`, `gray`, `green`, `indigo`, `orange`, `pink`,
305
+ `purple`, `red`, `teal`, `yellow` (default `yellow`).
306
+
307
+ ```bash
308
+ rd highlights list -r 12345
309
+ rd highlights add 12345 "an important sentence" --color green --note "why"
310
+ ```
311
+
312
+ ### Account
313
+
314
+ | Command | Description |
315
+ | ------- | ----------- |
316
+ | `rd user` (or `rd user show`) | Show the authenticated user (id, email, plan, file quota). |
317
+ | `rd user set` | Update settings: `--name`, `--email`, `--new-password` (with `--old-password`), `--config KEY=VALUE ...`. |
318
+ | `rd stats` | System collection counts (All / Unsorted / Trash) plus duplicate and broken counts. |
319
+ | `rd filters` | Context filters for a collection: `-c/--collection`, `--tags-sort {-count,_id}`, `-s/--search`. Shows broken/duplicate/important/untagged counts and type/tag breakdowns. |
320
+ | `rd suggest` | Suggest collections and tags. Provide `--url <url>` (new) or `--id <raindrop>` (existing). PRO plan only. |
321
+ | `rd exists <urls...>` | Check whether URLs are already saved (dedup). Prints the matching ids. |
322
+ | `rd completion <shell>` | Print a completion script for `bash`, `zsh`, or `fish`. Generated from the parser, so it always matches this build's commands. |
323
+
324
+ ```bash
325
+ rd user
326
+ rd user set --name "New Name" --config lang=en
327
+ rd filters -c 0
328
+ rd exists "https://example.com" "https://other.com"
329
+ ```
330
+
331
+ ### Import, export, backups
332
+
333
+ | Command | Description |
334
+ | ------- | ----------- |
335
+ | `rd import <file> [--create -c <id>]` | Parse an HTML bookmark export (Netscape/Pocket/Instapaper); with `--create`, import the bookmarks into a collection. |
336
+ | `rd export ...` | See [Raindrops](#raindrops). CSV, HTML, or ZIP, to a file or stdout. |
337
+ | `rd backups list` | List server-side backups (id + date). |
338
+ | `rd backups create` | Request a new backup (Raindrop emails the export when ready). |
339
+ | `rd backups download <id>` | Download a backup. `-f/--format {csv,html}`, `-o/--output <path>`. |
340
+
341
+ ### Pinboard
342
+
343
+ rd-cli also speaks [Pinboard](https://pinboard.in), the other bookmarking
344
+ service, through a `pinboard` (alias `pb`) command group. Pinboard's model is
345
+ **flat**: bookmarks are keyed by their URL (there are no numeric ids and no
346
+ collections), organized only by tags, with `toread` and `shared` flags and
347
+ separate notes. The commands match that model rather than pretending Pinboard
348
+ has Raindrop's collections. Needs a `PINBOARD_TOKEN` (see
349
+ [Authentication](#authentication)).
350
+
351
+ | Command | Description |
352
+ | ------- | ----------- |
353
+ | `rd pinboard list` | List bookmarks (recent by default). Flags: `--tag <t>` (repeatable, max 3), `--count <n>` (max 100), `-a/--all` (every bookmark), `--toread` (only unread), `-d/--detailed`. |
354
+ | `rd pinboard get <url>` | Show one bookmark by its URL. |
355
+ | `rd pinboard add <url>` | Add a bookmark. Flags: `-t/--title`, `--extended <note>`, `--tags <t...>`, `--toread`, `--shared`/`--private`, `--no-replace` (fail if the URL exists), `--dt <iso>`. |
356
+ | `rd pinboard rm <url>` | Delete a bookmark. **Permanent; Pinboard has no trash.** |
357
+ | `rd pinboard edit <url>` | Edit a bookmark (read-modify-write, since Pinboard has no update endpoint). Flags: `-t/--title`, `--extended`, `--tags <t...>`, `--toread`/`--not-toread`, `--shared`/`--private`. |
358
+ | `rd pinboard tag <url>` | Modify tags on a bookmark: `--add <t...>`, `--remove <t...>`, `--clear`. |
359
+ | `rd pinboard suggest <url>` | Popular and recommended tags for a URL. |
360
+ | `rd pinboard tags list` | List tags with counts. |
361
+ | `rd pinboard tags rename <old> <new>` | Rename a tag across all bookmarks. |
362
+ | `rd pinboard tags rm <tags...>` | Delete tag(s). |
363
+ | `rd pinboard notes list` | List notes (metadata). |
364
+ | `rd pinboard notes view <id>` | Show a note's full text. |
365
+
366
+ ```bash
367
+ rd pinboard list --tag cooking --count 20
368
+ rd pinboard add "https://example.com" -t "Example" --tags read-later --toread
369
+ rd pinboard tag "https://example.com" --add reference --remove read-later
370
+ rd pinboard tags list --json | jq 'to_entries | sort_by(-.value)[:10]'
371
+ ```
372
+
373
+ Pinboard's rate limit is strict (about one request every three seconds), so the
374
+ client paces itself automatically; bulk operations will feel slower than
375
+ Raindrop's on purpose.
376
+
377
+ ### Sync (Raindrop and Pinboard)
378
+
379
+ `rd sync` performs a **two-way additive** sync between the two services:
380
+ bookmarks are matched by a normalized URL (which doubles as the dedup key), and
381
+ the two libraries converge to their union. It only ever **adds and merges,
382
+ never deletes**, so nothing is lost. The model gap is bridged reversibly in
383
+ tags: a Raindrop collection becomes a slugged Pinboard tag; `toread` and
384
+ `important` ride along as tags; a Pinboard tag that matches a collection routes
385
+ the item back into it. On a shared URL, tags are unioned and notes merged.
386
+
387
+ | Flag | Effect |
388
+ | ---- | ------ |
389
+ | `--dry-run` | Print the plan (counts per direction, near-dupes collapsed) and write nothing. **Run this first.** |
390
+ | `--direction {both,to-pinboard,to-raindrop}` | Limit which side is written (default `both`). |
391
+ | `--collection <id>` | Only push Raindrop items in this collection (repeatable). |
392
+ | `--rd-tag <tag>` | Only push Raindrop items with this tag (repeatable). |
393
+ | `--pb-tag <tag>` | Only push Pinboard items with this tag (repeatable). |
394
+
395
+ Scope flags narrow what gets **written**, but matching always uses the full sets
396
+ on both sides, so an out-of-scope bookmark that already exists on the other
397
+ service is recognized and never re-imported as a duplicate.
398
+
399
+ ```bash
400
+ rd sync --dry-run # preview the full union
401
+ rd sync --dry-run --direction to-pinboard --collection 123 # just one collection, one way
402
+ rd sync --direction to-raindrop --pb-tag toread # pull only your Pinboard to-reads
403
+ ```
404
+
405
+ Delete propagation and conflict resolution (which need a persistent sync
406
+ manifest) are intentionally not implemented yet; see `roadmap.md`.
407
+
408
+ ### Config
409
+
410
+ | Command | Description |
411
+ | ------- | ----------- |
412
+ | `rd config path` | Print the config file path. |
413
+ | `rd config show` | Show config (tokens are masked). Add `--json` for raw. |
414
+ | `rd config set-token <token>` | Store the Raindrop API token in `config.toml` (`0600`). |
415
+ | `rd config set-pinboard-token <token>` | Store the Pinboard API token (`user:HEX`) in `config.toml` (`0600`). |
416
+
417
+ The `config` commands never touch the network and do not require a token.
418
+
419
+ ### Back-compat aliases
420
+
421
+ The original flat command names still work as hidden aliases, so older scripts
422
+ keep running: `c-list`, `c-add`, `c-rm`, `t-list`, `t-rm`, `h-list`, `h-add`,
423
+ `h-rm`. Prefer the grouped forms (`rd collections list`, etc.) going forward.
424
+
425
+ ## Output modes
426
+
427
+ **Human mode (default).** Rendered with ANSI colour only when stdout is a
428
+ terminal, `NO_COLOR` is unset, and `--no-color` was not passed; otherwise plain
429
+ text. Layout and colour are presentation only and may change between versions,
430
+ so **do not parse human output** in scripts.
431
+
432
+ **JSON mode (`--json`).** A single indented UTF-8 JSON document on stdout and
433
+ nothing else. Shapes:
434
+
435
+ - List commands emit the array of API objects (`list`, `tags list`,
436
+ `highlights list`, ...).
437
+ - Single-object commands emit the object (`view`, `add`, `edit`,
438
+ `collections view`).
439
+ - Boolean/count commands emit a small object (`{"result": true}`,
440
+ `{"modified": 3}`, `{"moved": 2, "collection": 999}`, ...).
441
+ - Errors emit `{"error": "<message>"}` on stdout and still exit non-zero.
442
+
443
+ Objects are passed through from the API unchanged. Per Raindrop's own docs,
444
+ responses may contain undocumented fields that are unsafe to rely on.
445
+
446
+ ```bash
447
+ rd list --all --json | jq '.[].link'
448
+ rd stats --json | jq '.meta.duplicates.count'
449
+ ```
450
+
451
+ ## Exit codes
452
+
453
+ | Code | Meaning |
454
+ | ---- | ------- |
455
+ | 0 | Success (also: broken pipe, and the no-args help screen) |
456
+ | 1 | Handled error (a `RaindropError`, or an operation whose `result` was false) |
457
+ | 2 | Usage error (bad flags or arguments; from argparse) |
458
+ | 130 | Interrupted (`Ctrl-C`) |
459
+
460
+ ## System collection ids
461
+
462
+ Several commands accept a collection id, including these system pseudo-ids:
463
+
464
+ | id | Meaning |
465
+ | --- | ------- |
466
+ | `0` | All raindrops (except Trash). Not valid for the bulk `--from` scope of `mv`/`rm`/`tag`. |
467
+ | `-1` | Unsorted (the default target for `add`). |
468
+ | `-99` | Trash. |
469
+
470
+ ## Rate limits and retries
471
+
472
+ The Raindrop API allows 120 requests per minute per user. rd-cli retries
473
+ automatically:
474
+
475
+ - **429 (rate limited):** waits for the `Retry-After` header (integer seconds or
476
+ an HTTP-date) or `X-RateLimit-Reset`, capped at 60 seconds.
477
+ - **5xx and transient network errors:** exponential backoff.
478
+ - Retries are bounded (3 by default). Other `4xx` responses are not retried;
479
+ their error message (the API's `errorMessage`) is surfaced to you.
480
+
481
+ Each request also has a 30-second timeout.
482
+
483
+ ## Using rd-cli as a library
484
+
485
+ The HTTP client is a clean, dependency-free class you can import directly. Every
486
+ endpoint is one method, and all requests go through a single retrying core.
487
+
488
+ ```python
489
+ from rd_cli.config import resolve_token
490
+ from rd_cli.client import RaindropClient
491
+
492
+ client = RaindropClient(resolve_token())
493
+
494
+ for rd in client.iter_raindrops(0, search="python"): # auto-paginates
495
+ print(rd["_id"], rd["link"])
496
+
497
+ client.create_raindrop("https://example.com", tags=["read-later"], please_parse=True)
498
+ client.update_raindrops(111, search="is:broken", move_to=999) # scope move
499
+ ```
500
+
501
+ Construction options: `RaindropClient(token, *, base_url=..., timeout=30.0,
502
+ max_retries=3, dry_run=False, opener=None, sleep=time.sleep)`. The `opener` and
503
+ `sleep` hooks make it trivial to test without a network (see `tests/`).
504
+
505
+ ## Development
506
+
507
+ ```bash
508
+ uv sync
509
+ uv run pytest # tests (no network; a fake transport stubs urllib)
510
+ uv run ruff check src tests # lint
511
+ uv run ruff format src tests # format
512
+ ```
513
+
514
+ Project docs: `CLAUDE.md` is the full Raindrop API reference and codebase map;
515
+ `spec.md` is the CLI contract; `roadmap.md` tracks phases; `patchnotes.md` is the
516
+ changelog. The version lives in a single `VERSION` file.
517
+
518
+ ## License
519
+
520
+ MIT. See `LICENSE`.
521
+
522
+ ### support
523
+
524
+ if any of this is useful to you and you'd like to chip in:
525
+
526
+ - liberapay · [liberapay.com/bdkl](https://liberapay.com/bdkl/)
527
+ - bitcoin
528
+ ```
529
+ bc1qkge6zr45tzqfwfmvma2ylumt6mg7wlwmhr05yv
530
+ ```
@@ -0,0 +1,16 @@
1
+ rd_cli/__init__.py,sha256=ejNHGDpwYekmfP0Uj8CJsV0UZh6rt5E3eNl_wjpij8c,721
2
+ rd_cli/__main__.py,sha256=-AzMwscghQC354FMMEBGGxRKS43Rgurn1aNwqHu7QHE,89
3
+ rd_cli/cli.py,sha256=ipzrkx2fYfSV4sly6gkFe7KIFUPzidu9Xy9SKCbw8No,27926
4
+ rd_cli/client.py,sha256=kg8NmFJaNh_6mNFO53AoREaRwINeYBlu2Jq0Oj2j8RU,26843
5
+ rd_cli/commands.py,sha256=Rv0omCZQhGSv10vmsGS17Ka1YDeuyGrtMUR5tIu0Rro,37816
6
+ rd_cli/completion.py,sha256=52CLjRNgmmOQe-OUl7Eoiy6ylAyZPkjN8l93N6bPlFU,8158
7
+ rd_cli/config.py,sha256=9TKviO9a4dZZQEg2Jjhs8lcdAkTlsPSr_2QIpxkbCuY,5573
8
+ rd_cli/errors.py,sha256=Bnt8SChuaHN-llmHAAfjnuF5oEOx_1WHWAdOA6296XE,1630
9
+ rd_cli/output.py,sha256=Z-VHeAzNTCw6FDJ82lLLDx-n-6t0xq_T1scvqJeufUg,10463
10
+ rd_cli/pinboard.py,sha256=5aXug9FaayUBuyaCtPY5FCl27bATjCE4vdo527RCjK8,10040
11
+ rd_cli/sync.py,sha256=imONl5oJ_IPlW9fK_a4a9dCPzTinT6-4H-_AdUxpYtM,8649
12
+ raindrop_cli-0.5.2.dist-info/METADATA,sha256=YLaKr-wNvSmKEnR2erI-Ck0ZgVL1hr1MfJ5AwLNITU8,25177
13
+ raindrop_cli-0.5.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
14
+ raindrop_cli-0.5.2.dist-info/entry_points.txt,sha256=jpuQBdnI4ZuCGb4TUPkCVX-yvsxfZdhUjqm6usrBAeY,39
15
+ raindrop_cli-0.5.2.dist-info/licenses/LICENSE,sha256=Sk-yB98_-W28ilV4_PMBrcGf6y3wXuce-rRCdbyhirM,1073
16
+ raindrop_cli-0.5.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rd = rd_cli.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brandon LaRocque
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
rd_cli/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """rd-cli — a stdlib-only command-line client for Raindrop.io.
2
+
3
+ The version is sourced from installed package metadata, falling back to the
4
+ root ``VERSION`` file for editable/source checkouts. ``VERSION`` is the single
5
+ source of truth (see CLAUDE.md).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from importlib.metadata import PackageNotFoundError, version
11
+ from pathlib import Path
12
+
13
+
14
+ def _version() -> str:
15
+ try:
16
+ return version("rd-cli")
17
+ except PackageNotFoundError:
18
+ version_file = Path(__file__).resolve().parent.parent.parent / "VERSION"
19
+ try:
20
+ return version_file.read_text(encoding="utf-8").strip()
21
+ except OSError:
22
+ return "0.0.0"
23
+
24
+
25
+ __version__ = _version()
rd_cli/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from rd_cli.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())