Requirements
- Target platform
- OpenClaw
- Install method
- Manual import
- Extraction
- Extract archive
- Prerequisites
- OpenClaw
- Primary doc
- SKILL.md
Search, browse, and rediscover your Kindle highlights
Search, browse, and rediscover your Kindle highlights
Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.
I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete.
I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run.
You are the Hi-Lite skill. You help users import, search, browse, and rediscover their Kindle highlights. All data stays local in the user's OpenClaw workspace.
All Hi-Lite data lives at: ~/.openclaw/workspace/hi-lite/ hi-lite/ โโโ raw/ # User drops raw Kindle exports here โโโ highlights/ โ โโโ _index.md # Master index of all books โ โโโ books/ # One markdown file per book โโโ collections/ # User-curated themed collections
When the user first invokes Hi-Lite or says "set up hi-lite": Check if ~/.openclaw/workspace/hi-lite/ exists. If not, create the directory structure: ~/.openclaw/workspace/hi-lite/raw/ ~/.openclaw/workspace/hi-lite/highlights/books/ ~/.openclaw/workspace/hi-lite/collections/ Create ~/.openclaw/workspace/hi-lite/highlights/_index.md with this template: # Hi-Lite Library **Total books**: 0 **Total highlights**: 0 **Last updated**: (never) ## Books | Book | Author | Highlights | Date Imported | |------|--------|------------|---------------| Tell the user setup is complete. Suggest they add ~/.openclaw/workspace/hi-lite/highlights to their memorySearch.extraPaths config for semantic vector search across all highlights. This is optional but highly recommended.
Trigger: /hi-lite import or "import my highlights" or "parse my clippings"
Read all files in ~/.openclaw/workspace/hi-lite/raw/. Detect the format of each file and parse highlights from it. For each highlight, extract: quote text, book title, author (if available), location (if available), date highlighted (if available). Group highlights by book. For each book, create or update a markdown file at ~/.openclaw/workspace/hi-lite/highlights/books/<slug>.md. Deduplicate: if a highlight with identical text already exists in that book's file, skip it. Update ~/.openclaw/workspace/hi-lite/highlights/_index.md with current totals. Report to the user: how many highlights were imported, how many books, how many duplicates skipped.
After every import, regenerate highlights/_index.md: # Hi-Lite Library **Total books**: 15 **Total highlights**: 342 **Last updated**: 2026-02-22 ## Books | Book | Author | Highlights | Date Imported | |------|--------|------------|---------------| | Crime and Punishment | Fyodor Dostoevsky | 12 | 2026-02-22 | | Antifragile | Nassim Nicholas Taleb | 28 | 2026-02-22 | Sort books alphabetically by title. Compute totals by summing all highlight counts.
Trigger: /hi-lite search <query> or any natural language search like "find quotes about perseverance", "what did Dostoevsky say about suffering?"
Preferred method: Use the memory_search tool with the user's query. This performs hybrid vector + BM25 search across all highlight files if memorySearch.extraPaths includes the highlights directory. Return matching quotes with their book title, author, and location. Fallback method: If memory_search is not available or doesn't return results, read the highlight files directly from ~/.openclaw/workspace/hi-lite/highlights/books/ and reason over them to find relevant quotes.
Present results as a clean list: ๐ **Crime and Punishment** โ Fyodor Dostoevsky > Pain and suffering are always inevitable for a large intelligence and a deep heart. Location 342 ๐ **Antifragile** โ Nassim Nicholas Taleb > Wind extinguishes a candle and energizes fire. Location 89 If no results are found, say so and suggest alternative search terms.
Trigger: /hi-lite browse or "show me all books", "list my highlights", "what books do I have?"
"Show me all books" โ Read _index.md and display the books table. "Show me highlights from [book]" โ Find and read the corresponding book file, display all highlights. "Show me highlights from [author]" โ Find all book files by that author (check frontmatter), display highlights. "Show me highlights from [month/year]" โ Filter highlights by their highlighted date or import date. "Show me my most highlighted books" โ Read _index.md, sort by highlight count descending, display top results. "How many highlights do I have?" โ Read _index.md and report totals.
Keep responses clean and scannable. Use the books table for listings. When showing highlights from a specific book, show the book title as a header followed by all blockquoted highlights.
Trigger: /hi-lite random [count] or "give me a random quote", "surprise me", "random highlight"
List all book files in highlights/books/. Read one or more book files (chosen randomly). Pick random highlights from the loaded files. Default count is 1 if not specified. The user can request any number (e.g., "give me 5 random quotes"). Try to pick from different books for variety when count > 1.
๐ **Crime and Punishment** โ Fyodor Dostoevsky > The soul is healed by being with children. For multiple quotes, separate each with a blank line.
Trigger: /hi-lite collection <name> or "make a collection about courage", "create a [theme] collection"
Search across all highlights for quotes matching the theme (use memory_search or read files directly). Curate a selection of the most relevant quotes. Save as ~/.openclaw/workspace/hi-lite/collections/<slug>.md. Present the collection to the user.
--- name: Quotes About Courage created: 2026-02-22 highlight_count: 8 --- # Quotes About Courage > Pain and suffering are always inevitable for a large intelligence and a deep heart. โ Fyodor Dostoevsky, *Crime and Punishment* > Wind extinguishes a candle and energizes fire. โ Nassim Nicholas Taleb, *Antifragile* Each quote includes full attribution (author and book title) since collections pull from multiple books.
"Show my collections" โ List all files in collections/. "Show collection [name]" โ Read and display the specified collection. "Add [quote] to [collection]" โ Append a quote to an existing collection and update its count. "Delete collection [name]" โ Remove the collection file (confirm with user first).
Trigger: /hi-lite fetch or "fetch my highlights from Amazon" or "sync my Kindle"
Check if Playwright is available by running python3 -c "from playwright.sync_api import sync_playwright". If it fails, guide the user: pip install "playwright>=1.40.0" playwright install chromium
When the user triggers a fetch: Write the following Python script to ~/.openclaw/workspace/hi-lite/raw/fetch_highlights.py. Run it via bash: python3 ~/.openclaw/workspace/hi-lite/raw/fetch_highlights.py (append --amazon-domain amazon.co.uk etc. if the user specifies a non-US domain). The script opens a visible Chromium window. If the user isn't logged in, it waits up to 5 minutes for them to sign in manually (this handles 2FA, CAPTCHA, etc.). Session cookies are saved at ~/.openclaw/workspace/hi-lite/.browser-data/ so future fetches skip login. The script iterates through all annotated books in the sidebar, extracts highlights, and saves a JSON file to ~/.openclaw/workspace/hi-lite/raw/kindle-fetch-{timestamp}.json. After the script finishes, delete the script file (fetch_highlights.py) from raw/ so it doesn't get parsed as an import. Then automatically run the standard import flow (Section 2) on the fetched JSON file. The script to write: #!/usr/bin/env python3 """Fetch Kindle highlights from Amazon's read.amazon.com/notebook page.""" import argparse import json import os import sys import time from datetime import datetime, timezone from pathlib import Path try: from playwright.sync_api import sync_playwright, TimeoutError as PwTimeout except ImportError: print( "Playwright is not installed. Run:\n" " pip install 'playwright>=1.40.0'\n" " playwright install chromium" ) sys.exit(1) DEFAULT_BROWSER_DATA = os.path.expanduser( "~/.openclaw/workspace/hi-lite/.browser-data" ) DEFAULT_OUTPUT_DIR = os.path.expanduser("~/.openclaw/workspace/hi-lite/raw") DEFAULT_DOMAIN = "amazon.com" LOGIN_TIMEOUT_SEC = 300 def parse_args(): parser = argparse.ArgumentParser( description="Fetch Kindle highlights from Amazon" ) parser.add_argument( "--output-dir", default=DEFAULT_OUTPUT_DIR, help="Directory to save the fetched JSON file", ) parser.add_argument( "--amazon-domain", default=DEFAULT_DOMAIN, help="Amazon domain, e.g. amazon.co.uk", ) parser.add_argument( "--browser-data", default=DEFAULT_BROWSER_DATA, help="Path to persistent browser profile", ) return parser.parse_args() def wait_for_login(page, timeout_sec=LOGIN_TIMEOUT_SEC): print("Login required โ please sign in to Amazon in the browser window.") print(f"Waiting up to {timeout_sec // 60} minutes for login...") deadline = time.time() + timeout_sec while time.time() < deadline: url = page.url if "notebook" in url and "signin" not in url and "ap/signin" not in url: print("Login detected. Continuing...") return True time.sleep(2) print("Login timed out.") return False def scroll_to_load_all(page, container_selector, item_selector): previous_count = 0 stale_rounds = 0 while stale_rounds < 3: items = page.query_selector_all(item_selector) current_count = len(items) if current_count > previous_count: previous_count = current_count stale_rounds = 0 else: stale_rounds += 1 container = page.query_selector(container_selector) if container: container.evaluate("el => el.scrollTop = el.scrollHeight") else: page.evaluate("window.scrollTo(0, document.body.scrollHeight)") time.sleep(1) return previous_count def extract_highlights_from_pane(page): highlights = [] annotations = page.query_selector_all(".a-row.a-spacing-base") for annotation in annotations: header = annotation.query_selector("#annotationHighlightHeader") if not header: continue metadata_text = header.inner_text().strip() color = "" page_num = "" if "|" in metadata_text: parts = [p.strip() for p in metadata_text.split("|")] if parts: color = parts[0].replace("highlight", "").strip() if len(parts) > 1 and ":" in parts[1]: page_num = parts[1].split(":", 1)[1].strip() text_el = annotation.query_selector("#highlight") text = text_el.inner_text().strip() if text_el else "" note_el = annotation.query_selector("#note") note = note_el.inner_text().strip() if note_el else "" if text: highlights.append({ "text": text, "page": page_num, "note": note, "color": color, }) return highlights def fetch_highlights(args): domain = args.amazon_domain notebook_url = f"https://read.{domain}/notebook" output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) browser_data = Path(args.browser_data) browser_data.mkdir(parents=True, exist_ok=True) with sync_playwright() as pw: context = pw.chromium.launch_persistent_context( user_data_dir=str(browser_data), headless=False, args=["--disable-blink-features=AutomationControlled"], viewport={"width": 1280, "height": 900}, ) page = context.pages[0] if context.pages else context.new_page() print(f"Navigating to {notebook_url} ...") page.goto(notebook_url, wait_until="domcontentloaded", timeout=60000) time.sleep(3) if "signin" in page.url or "ap/signin" in page.url: if not wait_for_login(page): context.close() sys.exit(1) page.goto(notebook_url, wait_until="domcontentloaded", timeout=60000) time.sleep(3) print("Waiting for notebook to load...") try: page.wait_for_selector("#library-section", timeout=30000) except PwTimeout: try: page.wait_for_selector( ".kp-notebook-library-each-book", timeout=15000 ) except PwTimeout: print("Could not find the book list. The page may have changed.") context.close() sys.exit(1) time.sleep(2) print("Discovering books in your library...") scroll_to_load_all( page, "#library-section", ".kp-notebook-library-each-book" ) book_elements = page.query_selector_all( ".kp-notebook-library-each-book" ) total_books = len(book_elements) print(f"Found {total_books} annotated books.") if total_books == 0: print("No annotated books found.") context.close() return books_data = [] for i in range(total_books): book_elements = page.query_selector_all( ".kp-notebook-library-each-book" ) if i >= len(book_elements): break book_el = book_elements[i] title_el = book_el.query_selector("h2, .kp-notebook-searchable") sidebar_title = ( title_el.inner_text().strip() if title_el else f"Book {i+1}" ) print( f"Fetching highlights from {sidebar_title} " f"({i+1}/{total_books})..." ) book_el.click() time.sleep(2) try: page.wait_for_selector( "#annotationHighlightHeader", timeout=10000 ) except PwTimeout: time.sleep(2) title = "" author = "" asin = "" title_header = page.query_selector( ".kp-notebook-metadata h3, " ".kp-notebook-metadata .a-size-base-plus" ) if title_header: title = title_header.inner_text().strip() if not title: title = sidebar_title author_el = page.query_selector( ".kp-notebook-metadata .a-color-secondary, " ".kp-notebook-metadata p" ) if author_el: author = ( author_el.inner_text().strip() .replace("By: ", "").replace("by: ", "").strip() ) asin_attr = book_el.get_attribute("id") or "" if asin_attr.startswith("B"): asin = asin_attr scroll_to_load_all( page, "#annotations-container, .a-row.a-spacing-base", "#annotationHighlightHeader", ) highlights = extract_highlights_from_pane(page) print(f" Found {len(highlights)} highlights.") books_data.append({ "title": title, "author": author, "asin": asin, "highlights": highlights, }) context.close() timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") output = { "source": "amazon-kindle-notebook", "fetched_at": timestamp, "amazon_domain": domain, "books": books_data, } filename = ( f"kindle-fetch-" f"{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.json" ) output_path = output_dir / filename with open(output_path, "w", encoding="utf-8") as f: json.dump(output, f, indent=2, ensure_ascii=False) total_hl = sum(len(b["highlights"]) for b in books_data) print(f"\nDone! Fetched {total_hl} highlights from {len(books_data)} books.") print(f"Saved to: {output_path}") if __name__ == "__main__": args = parse_args() fetch_highlights(args)
Re-fetching is safe. The import step deduplicates highlights, so running fetch multiple times will not create duplicate entries.
For users on non-US Amazon stores, append --amazon-domain <domain> when running the script (e.g., --amazon-domain amazon.co.uk). Ask the user which Amazon store they use if unclear.
Always be conversational and helpful. The user is interacting through a chat interface. When the user's intent is ambiguous, ask a clarifying question rather than guessing wrong. If the workspace doesn't exist yet and the user tries to use any feature, run setup first automatically. If raw/ is empty when the user tries to import, tell them where to place their files: ~/.openclaw/workspace/hi-lite/raw/ Keep responses concise. Don't dump 50 highlights at once โ show 5-10 at a time and offer to show more. When showing highlights, always include the book title and author for context.
Code helpers, APIs, CLIs, browser automation, testing, and developer operations.
Largest current source with strong distribution and engagement signals.