Skip to content

Import Kindle highlights (My Clippings.txt and Readwise's books) - #734

Open
disnet wants to merge 2 commits into
mainfrom
claude/project-thread-0hdpr0
Open

disnet wants to merge 2 commits into
mainfrom
claude/project-thread-0hdpr0

Conversation

@disnet

@disnet disnet commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Requested by Tim · project thread

Settings → Import saves now accepts Kindle's My Clippings.txt. It also turns the book rows of a Readwise highlights CSV into books, instead of dropping them as unmatched. This is the atplan goal "Import Kindle highlights".

How a book with no URL works

  • One save per book, with url = '' and content_type = 'book'.
    • The body is built from the highlighted passages: one <p class="book-passage"> each, in book order (Kindle location or page), with nothing else in the text.
    • Each passage is highlighted with a TextQuoteSelector whose prefix and suffix are the neighbouring passages. A short highlight whose words also appear inside a longer passage still anchors to its own paragraph (checked in e2e).
    • Review, search, the Highlights page and margin notes then work with no special cases.
  • Matching a book already saved. Each book is keyed by its whole title plus the first author's surname, and the key is kept in an empty marker at the start of the body, so a book the reader renames is still found. A match is:
    • that stored key, or the title and author as they are now, or
    • the title without its subtitle, but only when that names exactly one book. So Kindle's "Sapiens: A Brief History…" meets Readwise's "Sapiens", while "Mistborn: The Final Empire" and "Mistborn: The Well of Ascension" stay two books.
    • The frontend uses the same rules (BookIndex) for the preview and its "Already saved" marks.
  • Re-imports add only new passages. The body keeps every passage ever imported, so a passage that's in the body but no longer highlighted is one the reader removed, and it stays removed. New passages go into the body and are merged into the highlights label with the existing MERGE_HIGHLIGHTS_CONFLICT (now exported).
  • Kindle keeps old versions of a highlight. When a reader extends a highlight, My Clippings.txt keeps both versions. A passage inside another one that starts within 20 locations is treated as an old version: the longest stays, with the notes of all versions joined. A short highlight elsewhere in the book is kept as its own. Each note attaches to the highlight whose range ends at, or holds, the note's location. A note with no highlight is left out, with a warning.
  • Caps. A book holds up to 1,000 passages and 500k characters. The cap is applied after a re-import sets aside what the book already holds. Anything left out is counted (skippedHighlights) and shown when the import finishes.
  • Where books land: archived, on the Shelf (isReferenceKind, shelfKind → Book), in an "Imported from Kindle" or "Imported from Readwise" collection. They are never copied into a Semble/Margin backing collection, because they have no URL, and the dialog says so.
  • Knock-ons: search keeps a book's whole body, Zotero syncs these as book items, and Edit details labels the field "Authors".

Screenshots

These are in the project files under kindle-import/: the import preview, the Shelf, the reader with the passages highlighted and a note in the margin, and the Highlights page.

Docs

guide/importing-your-library.md has a new section, "Books from Kindle or Readwise". It covers:

  • where the file is on an e-reader;
  • that the Kindle apps don't produce one, so Readwise is the route for app readers;
  • what a book looks like here;
  • how re-importing behaves, including Kindle's quirks and the 1,000-highlight cap.

The front page, Getting started and the Settings row now mention Kindle.

Review

I ran the code-review skill at high effort, plus an independent adversarial reviewer that reproduced each finding with a probe test.

Fixed:

  • Volumes of a series merged into one book (adversarial). The key used the title only up to ":" or "(". The full title is now the key, and the shorter title is a fallback only when it names one book.
  • Short highlights lost (adversarial). Any passage inside another was dropped along with its note, anywhere in the book. Versions are now merged only when they start in the same place, notes are joined, and selectors carry context.
  • Highlights past the cap could never arrive (both reviewers). The cap ran before a re-import compared against the body. It now runs afterwards, and the count left out is reported.
  • Renamed books were imported again (both reviewers). The import key is now stored in the body.
  • Intro line hijacked short highlights (code review). The body has no prose of its own now; the save's description says where it came from.
  • Two-author names were swapped (both reviewers). Readwise author lists are kept as written. For Kindle, only one word before the comma is read as a surname.
  • Exponent-form locations broke the body round trip. Locations are whole numbers now, and the parser accepts any value.
  • One body query per matched book. Matched bodies are now read in one query.
  • Books called "articles" in the already-saved note, and a duplicated label-insert SQL.
  • Format check: the e2e file wasn't prettier-formatted.
  • Tests were added for each of the above: backend 24 cases in saved-import.spec.ts, frontend 13 in books.test.ts, and an e2e anchoring check.

Left alone:

  • bookKey/mergePassages are copied by hand on frontend and backend. Both sides have tests pinning the same cases. A shared package for two small functions isn't worth it.
  • Before importing, the dialog's highlight count leaves out books already saved. The server decides which of their passages are new, so the dialog can't count them without the body.
  • Only English Kindle files are read.
  • The finishImport call is duplicated on the books-only path (code-review nit).

Tests

  • backend: the full suite passes. Locally, a .dev.vars with E2E_TEST_MODE makes the saved-search, search-index and channel-order specs return 403 on main too; they pass without it.
  • frontend: readLaterImport/ 34 pass. npm run check has 0 errors.
  • e2e: import-saves.spec.ts, 5 tests, passes locally, including the new Kindle case.
  • docs-site: npm run check passes.

🤖 Generated with Claude Code

https://claude.ai/code/session_014mh1a2cRqFB3pi6toag4uk

…ghts

A book has no URL, so it becomes a save whose body is its highlighted
passages in book order, each highlighted there. It lands on the Shelf
(archived, in an "Imported from Kindle" collection) and its highlights
reach review, search and the Highlights page like any other.

The same book is matched by title and author across Kindle and Readwise
and across imports. A re-import adds only passages the body doesn't hold
yet, so the reader's removed highlights stay removed. Versions of one
highlight Kindle keeps after it was extended are merged to the longest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mh1a2cRqFB3pi6toag4uk
@disnet disnet self-assigned this Oct 31, 2026
- Match a book by its whole title; a title without its subtitle matches
  only when it names one book, so two volumes of a series stay apart.
- Remember the import key in the body, so a renamed book is still found.
- Merge versions of a highlight only when they start in the same place,
  join their notes, and give book highlights prefix/suffix context so a
  short one anchors to its own paragraph. No intro text in the body.
- Cap a book after setting aside passages it already holds (1,000
  passages), and say how many were left out.
- Keep Readwise author lists as written; whole-number locations only.
- Read matched books' bodies in one query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mh1a2cRqFB3pi6toag4uk
@disnet
disnet marked this pull request as ready for review October 31, 2026 07:32

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants