Newest phases at the bottom. Tick boxes when shipped.
- Initial
requests-basedapi.py+cli.py(list/add/edit/rm, collections, tags, highlights). No tests, no docs.
The current release. Made it correct, dependency-free, and conformant to the portfolio conventions.
- Port to stdlib
urllib; droprequestsandpython-dotenv(zero runtime deps). -
RaindropClientcore: single_request, request timeouts, boolean-param lowercasing, typed errors carrying the APIerrorMessage. - Rate-limit (
429) and5xxretry with bounded backoff. - Token/config resolution: env,
config.toml,.env;rd config. Removed the hard-coded personal.envpath. - Full API coverage: raindrops (single + batch + suggest + upload + export), collections (tree/merge/clean/empty-trash/reorder), tags (rename/merge/rm), highlights (list/add/edit/rm), user, stats, filters, import-dedup, backups.
- Pagination generators +
--all. - TTY-aware ANSI output (colour off when piped,
NO_COLOR/--no-color), tree and aligned-column renderers. -
--jsonworks in any position; consistent JSON contract. - Grouped command surface with hidden back-compat aliases.
- pytest suite over a fake urllib transport (no network); ruff configured.
- Docs:
CLAUDE.md(API + codebase),spec.md,README.md,logo.svg,VERSION, this roadmap, patchnotes. - Bulk/reorg commands (pulled forward after mining prior-art CLIs):
mv, multi-id/scoperm(--permanent),tag(add/remove/clear),add --file/--stdin,collections reorder. Grounded in an empirically verified batch-scope quirk (id-lists loop single-item endpoints; scope mode uses the batch endpoints; seeCLAUDE.md). -
--dry-run(log method + payload, skip the call) on every write. - Extra endpoints: raindrop/collection cover upload, icon/cover search,
HTML-file import (
rd import), and user-settings edit (rd user set).
- OAuth2 3-legged flow (
rd auth login): local redirect catcher, code exchange, token + refresh_token stored inconfig.toml. (RETIRED 2026-09-12 (Brandon): the auth trio retires and the spec's non-goal stands; the non-expiring token makes the refresh machinery moot.) -
Automatic token refresh on(RETIRED 2026-09-12 (Brandon): rides on the OAuth2 retirement; there will be no refresh token. The spec's non-goal now states this.)401when a refresh token is present. - Optional secret storage via the Secret Service (
oo7/keyring) instead of plaintextconfig.toml(ask before adding the dep). (DECLINED 2026-09-12 (Brandon): config.toml stays the hardened plaintext store; the zero-dependency stance holds.) -
rd open <id>to launch a raindrop (or its permanent copy) in the browser. Takes several ids,--cachefor the permanent copy (read off the307Location, PRO only), and--printto emit the URL instead of launching.
- CLI batch commands (
rd mv,rd tag, multi-idrm) mapped to the client batch methods, plus--dry-runas the safety guard. -
rd import <file>(Netscape/Pocket/Instapaper) viaPOST /import/file. - Collection cover upload / icon search commands.
- Interactive confirmation prompt for large destructive scope operations.
Gated on unbounded or irreversible blast radius, not on every write: any
scope mode (
rm/mv/tag --clear --from),rm --permanent,collections rm,collections empty-trash, andtags rm. Removing to Trash by id stays unprompted because it is recoverable.-y/--yesandRD_ASSUME_YESare the escape hatches,--dry-runbypasses (it writes nothing), and a non-interactive stdin refuses rather than hanging. -
--fieldsprojection for--jsonoutput;--formattemplates for human output. - A
rd context/--schemadump (whole tree + data-model schema) as an agent affordance (seen in kyoji2/raindrip).
- Shell completion (bash/zsh/fish) generated from the parser. (0.5.0 —
rd completion <shell>, walked offbuild_parser()so it cannot drift.) - Config profiles (multiple accounts/tokens).
- Optional interactive picker (fzf-style) behind a flag, still dependency-free.
Turned raindrop-cli from a Raindrop client into a two-service bookmark CLI, reusing the existing HTTP/output/config machinery.
-
PinboardClient: stdliburllib,auth_tokenquery-param auth,format=json, a minimum inter-request pacer for Pinboard's rate limit (~1 call / 3s),429/5xxbackoff, and the shared typed errors. -
rd pinboard(aliaspb) command group honest to Pinboard's flat model (URL as key, no collections):list,get,add,rm,edit,tag,suggest,tags list|rename|rm,notes list|view. - Read-modify-write
edit/tag(Pinboard has no update endpoint). - Pinboard token resolution +
rd config set-pinboard-token; both service tokens coexist inconfig.toml. -
--dry-runand--jsonacross the Pinboard surface; pytest coverage against the fake transport (auth params, pacing, retry, read-modify-write). - Cross-service sync landed in Phase 6 (below).
rd sync between Raindrop and Pinboard, built additive-first for safety.
- URL normalization as the match + dedup key (fold scheme/
www/fragment, strip tracking params, keep meaningful query). - Two-way additive sync (adds + merges, never deletes); the two libraries converge to their union.
- Reversible model-gap encoding in tags (collection <-> slug tag,
toread,important); notes merged idempotently; tags unioned on a shared URL. - Scoping:
--direction,--collection,--rd-tag,--pb-tag. Scope narrows what is written; matching uses the full sets (no re-import of an out-of-scope item that already exists on the other side). -
--dry-runplan preview; pure, unit-tested planner. - Delete propagation + conflict resolution via a persistent manifest (CONFIRMED DEFERRED 2026-09-12 (Brandon): additive-first sync is the standing design; Pinboard deletes are permanent.) (three-way diff). Deferred: it needs stored sync state and carries real data-loss risk (Pinboard deletes are permanent).
- A
--reconcile-dupespass that merges near-duplicate URLs within a single service, not just across the two.
- A TUI (would pull a dependency or a lot of stdlib curses; low value over the Raindrop web app).
- Sharing/collaborator commands (little personal value; the web UI covers it).
- Library extraction of
client.pyinto a standaloneraindroppackage (a post-1.0 call once the surface is stable and a second consumer exists).
Context: Identified CLI crashes, cross-service data loss, and documentation inaccuracies during codebase sweep.
- Context Count Crash: Fix
cmd_filtersraisingAttributeErrorwhen Raindrop API returns raw integer counts instead of dicts. (Fixed 0.6.0: int-or-dict handling.) - False Positives in Human Mode: Ensure human output mode checks the
okvariable and returns exit code1on failure, matching the JSON mode behavior. (Verified already-shipped 2026-09-04: failure paths return 1 throughout commands.py; box closed, no change needed. Corrective note 0.6.1: that verification missed the human-mode false-result paths intags rename/merge/collections merge/reorder, which exited 0; the_outchokepoint now enforces one exit-code rule for both modes.) - Timestamp Loss on Pinboard / Sync: Ensure
PinboardClient.edit_postand cross-service sync operations preserve the original bookmark creation time (dt=current.get("time")) instead of overwriting with the current date. (Fixed 0.6.0: edit_post preserves current time; sync carries the raindrop's created stamp as dt.) - Highlight ANSI Colors: Map Raindrop highlight colors (
yellow,blue, etc.) to standard ANSI color codes inoutput.pyso the▍marker isn't completely colorless. (Fixed 0.6.0: _HL_CODES maps Raindrop names onto the palette; unknown names fall back to muted.) -
toreadFlag Drop: Fixraindrop_to_pinboarddropping the unread status during cross-service pushes to Pinboard. (Fixed 0.6.0: raindrop_to_pinboard honors the "toread" tag that pinboard_to_raindrop already writes.) - KeyError on Malformed Raindrops: Use
.get("link")inplan_syncto avoid crashing on uploaded files/documents that lack a URL. (Fixed 0.6.0: plan_sync skips records with no URL on either side.) - Percent-Encoding Slashes: Pass
safe=""tourllib.parse.quoteinsearch_coversso slashes inside cover search terms don't break the URL path. (Fixed 0.6.0: safe="" in search_covers.) -
.envLoading Skip: Remove the earlyreturninload_env_filesto ensure both local and global configuration files are read. (Resolved 0.6.0 as documented behavior: load_env_files reads the first existing file on purpose; docstring and code agree, the roadmap's bug framing loses. Corrective note 0.6.1: the 0.6.0 "documented behavior" verdict was half wrong, since the env-over-config order itself was broken until 339baa9; the module-level injected-keys registry now keeps the documented order true across resolvers in one process.) - Browser Launch in Headless: Prevent
cmd_openfrom callingwebbrowser.open()when--jsonis specified to prevent headless system errors. (Fixed 0.6.0: --json never launches a browser.)
- Standardize Command Dispatch: Extract boilerplate
if args.json: emit(...) else: success(...)branching into a single helper method. (Shipped 0.6.1: two chokepoints in commands.py,_outfor write-shaped and_renderedfor list-shaped commands, replacing ~57 scattered branches; bespoke renders (user, stats, filters, sync, open, config show, pb get, exists) keep an explicit branch rather than bend through a helper. Both output modes are pinned per command by the StubClient tests.) - Safe File Writing: Use atomic
os.openand proper TOML escaping inconfig.pyto prevent temporary permission exposure of tokens. (Fixed 0.6.0: temp-file + os.replace, chmod 0600 before the swap, TOML quote/backslash escaping.) - Multi-Level Completion: Expand bash/zsh/fish generators to correctly autocomplete nested subcommands (e.g.,
rd pinboard tags list <TAB>). (Verified already-shipped 2026-09-04: completion.py recurses into nested subcommands, test-pinned.) - Docs Sync: Update
CLAUDE.mdandspec.mdto reflect thatrd sync,rd open, and interactive prompts are now implemented. (Shipped 0.6.1: spec.md re-synced (Status line, non-goals corrected,openin the verb list,--yes/RD_ASSUME_YESin the contract, the sync JSON shape); CLAUDE.md tree lists pinboard/sync/completion and no longer calls sync "not built"; OAuth2 retirement wording applied across spec/README/CLAUDE/roadmap.)
rd.json disposition: the file was deleted (never tracked) and rd.json now sits in .gitignore as insurance against recurrence. The landmine note closes.
- HIGH: the token-resolution fix (339baa9) breaks in dual-token
processes. load_env_files mutates os.environ and reports only keys
injected this call; in cmd_sync, resolve_token() runs first, so
resolve_pinboard_token() then sees the .env value as a real env var
and a stale ./.env token beats rd config set-pinboard-token - the
exact bug the commit claims to fix. Persist the injected-keys
registry across calls (module-level), and add a both-resolvers
regression test. (Shipped 0.6.1: module-level
_injectedregistry matched by exact value; regression test covers both resolution orders plus the "config lacks one key" case.) - HIGH: rd sync --json prints human plan lines before the JSON
document (only --json --dry-run is clean). Guard the plan lines on
not args.json; add cmd_sync CLI tests (none exist). (Shipped 0.6.1:
plan lines guarded on
not args.json; four cmd_sync CLI tests added via a run_sync fixture, plus a merge-date test.) - 0.6.1 lane (go-granted, now precise): the dispatch helper (57 if-args.json branches -> one emit chokepoint), the spec.md docs sync (Status 0.3.0; non-goals list shipped features; open missing from the verb list; the OAuth2 retirement wording x3 + the orphaned refresh box), the token fix + both HIGHs, the merge-branch dt fix (sync re-dates Pinboard posts today), then cut and publish. (Shipped 0.6.1, tag v0.6.1 published to PyPI by the workflow.)
- More code findings (shipped half): merge-branch re-dating and shared:true (both fixed in sync, see above); TimeoutError escapes the retry core on 3.11+ (both clients catch it now); explicit ids + --from silently ignores the ids (rejected as a usage error in rm/mv/tag); aliases visible in --help (investigated 0.6.1: not reproducible - unhelped subparsers are unlisted on 3.11 and 3.14, and help=argparse.SUPPRESS backfires on 3.14 by rendering the sentinel literally; a regression test pins the hidden behavior); config show --json masks tokens while README said raw (README corrected, masking kept); backups create gained --json.
- Remaining code findings (unchanged): iter_* truncate at perpage>50; batch add silently ignores six flags incl. --no-parse doing the opposite; NO_COLOR empty-string deviation; CJK width math; _toml_line corrupts nested config values; BrokenPipe stderr noise. Full list in the ledger.
- The rename broke Brandon's own config path:
~/.config/rd-cli/config.toml holds both tokens; config_dir() now
returns ~/.config/raindrop-cli (renamed Sep 3) - no migration ran, so
the installed rd has no token source. A one-time migration (or reading
the old path as fallback) belongs in 0.6.1. (Shipped 0.6.1: the old
config.toml is a read fallback, the old .env is the last .env candidate,
the first set- write repatriates every key,
config pathreports the file in effect. Verified live: ~/.config/rd-cli/config.toml exists on this machine and raindrop-cli/ did not.)* - Blitz candidates: highlights -c + source titles (get_collection_highlights: zero callers); Pinboard date filters + last_update() sync fast-path (built, unwired); backups download --latest; --reconcile-dupes stays deliberately deferred.
- GitHub presentation (workspace batch): description omits Pinboard + sync (the differentiators; replacement drafted); homepage 404s (codex renamed - /codex/raindrop-cli/ is 200); Releases for v0.6.0; topics add pinboard/sync, drop the python triplication; wiki off. (Applied 2026-09-13 with the v0.6.1 cut: description now names Pinboard + sync, homepage points at the renamed codex page (verified 200; the old /codex/rd-cli/ path confirmed 404), topics gained pinboard/sync and lost the python3/python-311 duplicates, the first GitHub Release (v0.6.1, verbatim patchnotes body) exists, and the empty wiki is off.)