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.
- raindrop_cli-0.5.2.dist-info/METADATA +530 -0
- raindrop_cli-0.5.2.dist-info/RECORD +16 -0
- raindrop_cli-0.5.2.dist-info/WHEEL +4 -0
- raindrop_cli-0.5.2.dist-info/entry_points.txt +2 -0
- raindrop_cli-0.5.2.dist-info/licenses/LICENSE +21 -0
- rd_cli/__init__.py +25 -0
- rd_cli/__main__.py +6 -0
- rd_cli/cli.py +757 -0
- rd_cli/client.py +727 -0
- rd_cli/commands.py +1180 -0
- rd_cli/completion.py +225 -0
- rd_cli/config.py +162 -0
- rd_cli/errors.py +56 -0
- rd_cli/output.py +305 -0
- rd_cli/pinboard.py +275 -0
- rd_cli/sync.py +252 -0
|
@@ -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,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()
|