From 8e0d5d9290d96c3bc01bb5cb2843722a09c625ec Mon Sep 17 00:00:00 2001 From: Ryan Anderson Date: Sun, 19 Oct 2025 23:08:41 -0400 Subject: [PATCH 1/8] Working kindle sync --- .claude/commands/kindle-sync.md | 175 +++++ .gitignore | 5 +- .kindle/cache/all_books.json | 598 ++++++++++++++++++ .kindle/config.json | 13 + .scripts/kindle/auth.js | 166 +++++ .scripts/kindle/config.js | 148 +++++ .scripts/kindle/lib/config.js | 65 ++ .scripts/kindle/lib/errors.js | 50 ++ .scripts/kindle/lib/renderer.js | 130 ++++ .scripts/kindle/lib/scraper.js | 281 ++++++++ .scripts/kindle/lib/utils.js | 60 ++ .scripts/kindle/lib/validation.js | 94 +++ .scripts/kindle/sync.js | 363 +++++++++++ .scripts/kindle/templates/kindle-note.md.hbs | 26 + ...The Expanse Book 3) - James S. A. Corey.md | 29 + ...The Expanse Book 6) - James S. A. Corey.md | 34 + ...ht_ A King Oliver Novel - Walter Mosley.md | 17 + ...ntum Physics Is Different - Philip Ball.md | 181 ++++++ ...The Expanse Book 2) - James S. A. Corey.md | 33 + ...The Expanse Book 4) - James S. A. Corey.md | 34 + ...The Expanse Book 1) - James S. A. Corey.md | 37 ++ ...dition) - James Martin and John L Allen.md | 42 ++ ...The Expanse Book 5) - James S. A. Corey.md | 21 + ...The Expanse Book 7) - James S. A. Corey.md | 37 ++ README.md | 57 ++ package.json | 14 +- 26 files changed, 2707 insertions(+), 3 deletions(-) create mode 100644 .claude/commands/kindle-sync.md create mode 100644 .kindle/cache/all_books.json create mode 100644 .kindle/config.json create mode 100644 .scripts/kindle/auth.js create mode 100644 .scripts/kindle/config.js create mode 100644 .scripts/kindle/lib/config.js create mode 100644 .scripts/kindle/lib/errors.js create mode 100644 .scripts/kindle/lib/renderer.js create mode 100644 .scripts/kindle/lib/scraper.js create mode 100644 .scripts/kindle/lib/utils.js create mode 100644 .scripts/kindle/lib/validation.js create mode 100644 .scripts/kindle/sync.js create mode 100644 .scripts/kindle/templates/kindle-note.md.hbs create mode 100644 02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md create mode 100644 02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md create mode 100644 02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md create mode 100644 02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md create mode 100644 02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md create mode 100644 02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md create mode 100644 02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md create mode 100644 02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md create mode 100644 02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md create mode 100644 02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md diff --git a/.claude/commands/kindle-sync.md b/.claude/commands/kindle-sync.md new file mode 100644 index 00000000..5909a174 --- /dev/null +++ b/.claude/commands/kindle-sync.md @@ -0,0 +1,175 @@ +# Kindle Highlights Sync + +Sync your Kindle highlights to the vault with zero-config setup. + +## Important Instructions + +**Step 1: Check if this is first run:** + +Check if `.kindle/config.json` exists: +- If it EXISTS: Skip to Step 2b +- If it DOES NOT exist: This is first run, go to Step 1b + +**Step 1b (First run only): Ask for output folder:** + +Ask the user: +``` +This is your first time syncing Kindle highlights! Where would you like to save them? + +Press Enter for default: 03_Resources/Kindle Highlights +Or type a custom path (e.g., 02_Areas/Reading) + +Your choice: +``` + +Wait for their answer, then continue to Step 2a with the `--output` flag. + +**Step 2a: Ask how many books to sync:** + +Present this text directly: + +``` +How many books would you like to sync? This syncs your most recent books first. + +A. 10 books - Quick test sync of your setup +B. 25 books - Broader sync +C. 50 books - Extensive sync +D. All books - Complete library (10-30 min for large libraries) +E. Custom number - You specify how many + +Just reply with A, B, C, D, E, or a number. +``` + +Wait for their answer, then continue to Step 3. + +**Step 2b: Ask how many books to sync:** + +Present this text directly: + +``` +How many books would you like to sync? + +A. 10 books (recommended) - Quick sync of your 10 most-recently highlighted books +B. 25 books - Broader sync +C. 50 books - Extensive sync +D. All books - Complete library (10-30 min for large libraries -- only ) +E. Custom number - You specify how many + +Just reply with A, B, C, D, E, or a number. +``` + +**Step 3: Run the command immediately after their answer:** + +Based on their response and whether it's first run: + +**If first run (no config):** +- If they answer **A** or **10**: Run `node .scripts/kindle/sync.js --limit 10 --output "[their-folder-choice]"` +- If they answer **B** or **25**: Run `node .scripts/kindle/sync.js --limit 25 --output "[their-folder-choice]"` +- If they answer **C** or **50**: Run `node .scripts/kindle/sync.js --limit 50 --output "[their-folder-choice]"` +- If they answer **D** or **all**: Run `node .scripts/kindle/sync.js --all --output "[their-folder-choice]"` +- If they answer **E** or a number: Run `node .scripts/kindle/sync.js --limit [number] --output "[their-folder-choice]"` + +**If NOT first run (config exists):** +- If they answer **A** or **10**: Run `node .scripts/kindle/sync.js --limit 10` +- If they answer **B** or **25**: Run `node .scripts/kindle/sync.js --limit 25` +- If they answer **C** or **50**: Run `node .scripts/kindle/sync.js --limit 50` +- If they answer **D** or **all**: Run `node .scripts/kindle/sync.js --all` +- If they answer **E** or a number: Run `node .scripts/kindle/sync.js --limit [number]` + +Note: If user just presses Enter for default folder, use `03_Resources/Kindle Highlights` as the path. + +## What This Does + +Fetches your Kindle highlights from Amazon and creates beautifully formatted Markdown notes in your vault. + +**✨ First Run Magic**: +- First time? You'll choose where to save highlights (defaults to `03_Resources/Kindle Highlights`) +- Not authenticated? Browser opens automatically for Amazon login +- Everything just works! + +**Subsequent syncs:** +- Uses your saved folder location +- Uses cached authentication +- Much faster! + +## What Gets Created + +Each book creates a note with: +- **YAML frontmatter:** title, author, ASIN, tags, sync date +- **Metadata:** Total highlights, Kindle notebook link +- **All highlights:** Each with location and your notes +- **Kindle app links:** Click to open specific location in Kindle app +- **Notes section:** Space for your own thoughts + +## Changing Your Settings + +Want to change where highlights are saved or other settings? You have three options: + +### Option 1: Edit Config File Directly (Recommended) +Open `.kindle/config.json` in your vault and edit: +```json +{ + "outputFolder": "02_Areas/Reading", // Change this to your preferred folder + "templatePath": ".scripts/kindle/templates/kindle-note.md.hbs", + "overwrite": false, // Set to true to replace existing files + "addTags": ["kindle", "highlights", "books"], + "createIndex": true, + "indexPath": "03_Resources/Kindle Highlights Index.md", + "lastSync": "2025-10-19T22:30:00.000Z" +} +``` + +### Option 2: Reset to First-Run Prompt +Delete the config file to trigger the folder selection prompt again: +```bash +rm .kindle/config.json +# Next /kindle-sync will ask where to save highlights +``` + +### Option 3: One-Time Override +Use the `--output` flag to save to a different folder just once (without changing config): +```bash +node .scripts/kindle/sync.js --limit 10 --output "01_Projects/Current Reading" +``` + +## Tips + +### Expected Timing +- **10 books:** 1-3 minutes +- **50 books:** 5-10 minutes +- **100+ books (all):** 15-30 minutes + +Rate limiting delays are required to avoid Amazon detection. + +### Organization +- Notes are automatically named: ` - .md` +- All include `#kindle`, `#highlights`, `#books` tags +- Easy to find with Obsidian search or graph view + +### Advanced Configuration +All settings in `.kindle/config.json`: +- `outputFolder`: Where to save notes (default: `03_Resources/Kindle Highlights`) +- `templatePath`: Custom Handlebars template location +- `overwrite`: Whether to replace existing files (default: `false`) +- `addTags`: Tags to add to all notes (default: `["kindle", "highlights", "books"]`) +- `createIndex`: Whether to create an index file (not yet implemented) +- `indexPath`: Where to save the index file + +## Troubleshooting + +### "Not authenticated" error +Shouldn't happen anymore! The script auto-detects and runs authentication. But if you see this: +- Just run `/kindle-sync` again +- It will automatically handle authentication + +### No books found +- Check if your Kindle library is visible at read.amazon.com/notebook +- Verify you have highlights (not just books in library) + +### Duplicate files +- Set `overwrite: true` in config to replace existing files +- Or manually delete old files before syncing + +### Scraping seems slow +- Normal! Amazon requires delays between requests +- Use smaller number of books for faster syncs diff --git a/.gitignore b/.gitignore index b66ecfd4..43d5264c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,7 @@ pnpm-lock.yaml .tmp/ # Never commit real API keys -*_API_KEY \ No newline at end of file +*_API_KEY + +# Kindle Auth Directory +.kindle/auth/ \ No newline at end of file diff --git a/.kindle/cache/all_books.json b/.kindle/cache/all_books.json new file mode 100644 index 00000000..7845c7da --- /dev/null +++ b/.kindle/cache/all_books.json @@ -0,0 +1,598 @@ +[ + { + "bookId": "B06XKN9G27", + "title": "Persepolis Rising (The Expanse Book 7)", + "author": "James S. A. Corey", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:25.071Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B06XKN9G27", + "highlights": [ + { + "text": "there had been a time when riding the lift instead of climbing the ladder would have felt like admitting defeat, even if the ship was burning ten times that hard. But for the last couple years Bobbie’s joints had been giving her trouble, and proving to herself that she could make the climb had stopped mattering as much. It seemed to her that the real sign you were getting old was when you stopped needing to prove you weren’t getting old.", + "location": "426", + "color": "yellow", + "id": "6c348b9dac47ca50" + }, + { + "text": "“All right, then,” Bobbie said. “The way I see it, the next step is find someone who can get messages back to the union. Or Earth-Mars. See if there’s anyone out there with a plan, or if we’re going to have to make one up on our own.” “We can do that,” Amos said. “Shouldn’t be hard.” “You sure?” Alex said. “This is Medina Station under occupation by a bunch of splinter Martian military expats. It’s not Baltimore.” Amos’ smile was as placid as always. “Everywhere’s Baltimore.”", + "location": "3,003", + "color": "yellow", + "id": "d8f91bab073342a2" + }, + { + "text": "If they kill us all, Drummer thought, this will be why. Not their technology, not their strategy, not the invisible cycle of history. It’ll be our inability to do anything without five committee meetings to talk about it.", + "location": "3,771", + "color": "yellow", + "id": "d618536dde571e38" + }, + { + "text": "It was all becoming normal. He could see it in the way the clerk served up the terrible, terrible coffee. He could hear it in the conversations at the nearby tables. It showed on the screens and in the gaits of the people walking by. Panic and alarm were exhausting. He was exhausted by them, and Medina was exhausted too. It was already shifting into its new routine. Checkpoints, yes. Armed security, yes. All the theater of dominance and control and nothing to undercut that narrative.", + "location": "4,044", + "color": "yellow", + "id": "b7ba92c0027ed0ec" + }, + { + "text": "controlling don’t have a voice in how you control them. As long as everyone’s on the same page, things may be great, but when there’s a question, you win. Right?” “There has to be a way to come to a final decision.” “No, there doesn’t. Every time someone starts talking about final anythings in politics, that means the atrocities are warming up. Humanity has done amazing things by just muddling through, arguing and complaining and fighting and negotiating. It’s messy and undignified, but it’s when we’re at our best, because everyone gets to have a voice in it. Even if everyone else is trying to shout it down. Whenever there’s just one voice that matters, something terrible comes out of it.”", + "location": "5,949", + "color": "yellow", + "id": "692e86cc25bcb0a4" + }, + { + "text": "“For what it’s worth, I don’t disagree with you. These people are scum. They don’t deserve or understand what we’ve brought them. For me, I think they never will. But their children might. Their grandchildren or their great-grandchildren. The story of Medina will be that Governor Singh mismanaged the station, lost his ship to a band of malcontents, lost his perspective. And when he let his wounded pride exceed the mandates of the high consul’s directives, he was removed for the protection of the everyday citizens in his care. You see the difference? If you kill an insurgent, you’re the enemy of all their friends. All their family. And then there’s an expectation. Precedent. Enemies for generations. Forever. If you kill your own—even the highest among your own—to protect someone powerless, they remember that too. It sows gratitude. It sows trust. Generations from now your sacrifice will lead to the peace, prosperity, and fellow feeling among all humanity.”", + "location": "7,853", + "color": "yellow", + "id": "c3ef5243596a4c91" + } + ], + "meta": { + "totalHighlights": 6 + } + }, + { + "bookId": "B07H9HNDYL", + "title": "Beyond Weird: Why Everything You Thought You Knew about Quantum Physics Is Different", + "author": "Philip Ball", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:27.099Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B07H9HNDYL", + "highlights": [ + { + "text": "Rather than insisting on its difficulty, we might better regard it as a beguiling, maddening, even amusing gauntlet thrown down to challenge the imagination. For that is indeed what is challenged. I suspect we are, in the wider cultural context, finally beginning to appreciate this. Artists, writers, poets and playwrights have started to imbibe and deploy ideas from quantum physics: see, for instance, plays such as Tom Stoppard’s Hapgood and Michael Frayn’s Copenhagen, and novels such as Jeanette Winterson’s Gut Symmetries and Audrey Niffenegger’s The Time Traveler’s Wife. We can argue about how accurately or aptly these writers appropriate the scientific ideas, but it is right that there should be imaginative responses to quantum mechanics, because it is quite possible that only an imagination sufficiently broad and liberated will come close to articulating what it is about.", + "note": "Check out these works, especially Winterson", + "location": "102", + "color": "yellow", + "id": "0fba278d664afb20" + }, + { + "text": "Feynman seemed to feel that it was impossible and even pointless to attempt anything comparable for quantum mechanics: We can’t pretend to understand it since it affronts all our commonsense notions. The best we can do is to describe what happens in mathematics, in equations, and that’s very difficult. What is even harder is trying to decide what the equations mean. That’s the hardest thing of all.", + "location": "143", + "color": "yellow", + "id": "f0f8742d2806d44f" + }, + { + "text": "only a few decades ago, professing a serious interest in the topic could be tantamount to career suicide for a young physicist. Only a handful of scientists and philosophers, idiosyncratically if not plain crankily, insisted on caring about the answer. Many researchers would shrug or roll their eyes when the ‘meaning’ of quantum mechanics came up; some still do. ‘Ah, nobody understands it anyway!’ How different this is from the attitude of Albert Einstein, Niels Bohr and their contemporaries, for whom grappling with the apparent oddness of the theory became almost an obsession. For them, the meaning mattered intensely. In 1998 the American physicist John Wheeler, a pioneer of modern quantum theory, lamented the loss of the ‘desperate puzzlement’ that was in the air in the 1930s. ‘I want to recapture that feeling for all, even if it is my last act on Earth’, Wheeler said.", + "note": "\"desperate puzzlement\"", + "location": "153", + "color": "yellow", + "id": "b5080d08294caf98" + }, + { + "text": "You can see that I’m already struggling to find a language that works for talking about these things. That’s OK, and you’ll have to get used to it. That’s how it should be. When words come too easily, it’s because we haven’t delved deeply enough (you’ll see that scientists can be guilty of that too).", + "note": "I've been thinkibg lately about if (really how) the human need for language inhibits our ability to understand things. If your mother tongue does not have the words for something then how much harder is it to understand? What is the instance of bilingualism among Nobel-winning scientists?", + "location": "191", + "color": "yellow", + "id": "1dfa0d699cf08e8a" + }, + { + "text": "and there are plenty of accessible books on the subject.*2", + "location": "208", + "color": "yellow", + "id": "a0f0aacec2c2aaac" + }, + { + "text": "Best of all for its narrative value is the largely good-natured but trenchant dispute that rumbled on for decades between Einstein and Bohr about what it all meant – about the nature of reality. This is indeed a superb story, and if you haven’t heard it before then you should.*1", + "location": "256", + "color": "yellow", + "id": "0139c998fbad4493" + }, + { + "text": "The best illustration I know of that quantization is rather incidental to quantum theory is found in the book Quantum Mechanics: The Theoretical Minimum, based on a series of lectures that Leonard Susskind, a professor of theoretical physics at Stanford University, gave to undergraduates, which were written up with the help of the writer Art Friedman. The book is described as being ‘for anyone who ever regretted not taking physics at university, who knows a little but would like to know more’.", + "note": "I'm less than 10% into this book and already have 3 more to read!", + "location": "337", + "color": "yellow", + "id": "a2976a8f8215de07" + }, + { + "text": "Quantum objects are what they are, and we have no reason to suppose that ‘what they are’ changes in any meaningful way depending on how we try to look at them. Rather, all we can say is that what we measure sometimes looks like what we would expect to see if we were measuring discrete little ball-like entities, while in other experiments it looks like the behaviour expected of waves of the same kind as those of sound travelling in air, or that wrinkle and swell on the sea surface. So the phrase ‘wave–particle duality’ doesn’t really refer to quantum objects at all, but to the interpretation of experiments – which is to say, to our human-scale view of things.", + "location": "392", + "color": "yellow", + "id": "48a1418b0bbcba0a" + }, + { + "text": "What makes the quantum situation of radioactive decay any different from this classical (and, forgive me, rather unfortunate) analogy of childbirth? We have every reason to suppose that, if we were able to monitor the biology of each pregnant mother and baby sufficiently closely, we could understand exactly why the respective birth process started when it did – perhaps some hormonal threshold was reached, say. But for radioactive decay, there is nothing you can monitor to explain why a particular atom decayed when it did. There is nothing we can call a reason.", + "note": "At this point in knowledge, sure. But there has to be an explanation, a trigger. We can't just live in a world where things happen with no cause. If that was the case, then what is the source of the wave function?", + "location": "606", + "color": "yellow", + "id": "53615fc2f146b9ec" + }, + { + "text": "So, said Bohr, we had better just stop there. As he put it: There is no quantum world. There is only an abstract quantum physical description. It is wrong to think that the task of physics is to find out how nature is. Physics concerns what we can say about nature.", + "location": "777", + "color": "yellow", + "id": "d928e35c618eff5f" + }, + { + "text": "At face value that sounds crazy. Why make a mathematical theory like quantum mechanics if you don’t think it can tell you about the system it is supposed to describe? But Bohr argued that quantum theory tells you something more meaningful – indeed, the only thing that can be meaningful. It tells you what you will find when you try to investigate that system. It tells you about measurements.", + "location": "783", + "color": "yellow", + "id": "4e9549eb8a6e86e2" + }, + { + "text": "the early days of this new physics, the problem was often debated in terms of the ‘role of the observer’. That the observer had a role at all was deeply troubling, because it seemed to challenge the very concept of science. If what we see depends on what questions we ask, whither then the idea of an objective world, governed by rules that pertain independently of our attempts to figure them out? As Heisenberg put it, science had ceased to be a way of peeking unnoticed at the world, and instead had become ‘an actor in [the] interplay between man and nature’.", + "note": "Like how what we get from an LLM depends on not just what questions we ask it but how.", + "location": "813", + "color": "yellow", + "id": "f770db7238f7656c" + }, + { + "text": "But measurement does something else. It ‘collapses’ (Heisenberg’s original word was ‘reduces’) those possibilities, expressed in the wavefunction, to just one. Suppose that, before the measurement, the probabilities of finding some property of a quantum object with the values corresponding to states A, B and C are 10%, 70% and 20% respectively. When we make a single measurement on the object, we might find that we get the result C. What happened to A and B? We are forced to assert that the probabilities have now changed: that for C it is 100%, and for A and B it is zero. What’s more, we can’t get A and B back: if we repeat the measurement, we’ll keep getting C.*2", + "note": "We will keep getting C *unless and until* we reset the quantum object in its original state. How in tge world does that happen??", + "location": "1,043", + "color": "yellow", + "id": "ab3eebcc4be4b708" + }, + { + "text": "To Bohr, wavefunction collapse was virtually emblematic of the distinction between the unitary quantum world and the everyday reality in which we make observations and measurements. Measurements must be classical by definition: they require some big apparatus with which humans can interact. From our perspective the world is made up of phenomena – things happen – and a phenomenon only exists when it has been measured. Wavefunction collapse is simply a name we give to the process by which we turn quantum states into observed phenomena. Wavefunction collapse is then a generator of knowledge: it is not so much a process that gives us the answers, but is the process by which answers are created. The outcome of that process can’t, in general, be predicted with certainty, but quantum mechanics gives us a method for calculating the probabilities of particular outcomes. That’s all we can ask for.", + "location": "1,062", + "color": "yellow", + "id": "fd098f3250b8f339" + }, + { + "text": "Bohr could be stubborn and dogmatic as well as rather cryptic. But he deserves praise for defining the limits of what can be said with confidence. Some suspect that his intuition surpassed anything he could justify mathematically, or indeed semantically. As von Weizsäcker said, ‘Bohr was essentially right but he did not know why.’", + "location": "1,110", + "color": "yellow", + "id": "24522609b8a9dd3a" + }, + { + "text": "But nevertheless the notion of a universal wavefunction is popular with cosmologists, for the perfectly valid reason that in the earliest moments of the Big Bang the entire universe was smaller than an atom and surely needs to be considered, in that moment, a quantum-mechanical entity.", + "note": "Should we just accept that as fact?", + "location": "1,222", + "color": "yellow", + "id": "f2c8242cfa74ff76" + }, + { + "text": "Quantum mechanics generally assumes that quantum states exist in some meaningful sense, and that the math tells us what we can know about those states. But in QBism there are no objective states. Rather, according to Chris Fuchs, ‘quantum states represent observers’ personal information, expectations and degrees of belief’. This view, he says, ‘allows one to see all quantum measurement events as little “moments of creation”, rather than as revealing anything pre-existent’.", + "note": "What if every time we collapse a wave function we are setting off a big bang in another universe?", + "location": "1,303", + "color": "yellow", + "id": "4509cc16ba78b606" + }, + { + "text": "The main thing you need to know about entanglement is this: it tells us that a quantum object may have properties that are not entirely located on that object.", + "location": "1,704", + "color": "yellow", + "id": "365d454d2123cc0d" + }, + { + "text": "But this locality is just what quantum entanglement undermines – which is why ‘spooky action at a distance’ is precisely the wrong way to look at it. We can’t regard particle A and particle B in the EPR experiment as separate entities, even though they are separated in space. As far as quantum mechanics is concerned, entanglement makes them both parts of a single object. Or to put it another way, the spin of particle A is not located solely on A in the way that the redness of a cricket ball is located on the cricket ball. In quantum mechanics, properties can be non-local.", + "location": "1,931", + "color": "yellow", + "id": "5d56dff142cbc9f5" + }, + { + "text": "What in fact we’re dealing with here is another kind of quantum superposition. We’ve seen that superposition refers to a situation in which a measurement on a quantum object could produce two or more possible outcomes, but we don’t know which it will be, only their relative probabilities. Entanglement is that same idea applied to two or more particles: a superposition of the state in which particle A has spin up and B spin down, say, and the state with the opposite configuration. Although the particles are separated, they must be described by a single wavefunction. We can’t untangle that wavefunction into some combination of two single-particle wavefunctions.", + "note": "If we could change the spin of an entangled particle, would that change the spin of the second particle or would in disentangle the particles? Can you even disentangle particles? Can you have an entangled triad of particles?", + "location": "1,936", + "color": "yellow", + "id": "25bf6b0c01ba4b97" + }, + { + "text": "Kochen and Specker pointed out that the outcomes of quantum measurements may depend on their context. This means something subtly different from the fact that different types of experiment on what seems like the same system (a double-slit experiment with or without a ‘path detector’, say) give different results. It means that if you look at a quantum object through different windows, you see different things. If you want to count the number of black and white balls in a jar, it doesn’t matter if you count the black ones or the white ones first, or if you tot them up in rows of five, or if you separate the two colours into piles and weigh them. You’ll get the same answer. But in quantum mechanics, even when you ask the same question (‘How many black and white balls are there?’), the answer you get may depend on how the measurement is done.", + "note": "Here is this similarity to interfacing with LLMs again", + "location": "2,009", + "color": "yellow", + "id": "c3c498350e232dce" + }, + { + "text": "The Kochen–Specker theorem stipulates what follows from this dependence on context. In effect it is another corollary of the way that, in quantum mechanics, what we choose not to measure may leave an imprint on what we do measure.", + "location": "2,018", + "color": "yellow", + "id": "d417257cba5d6f9b" + }, + { + "text": "An Assyrian seer, unwilling to let his young daughter be wed to suitors who he considered unworthy, set them a task. He presented them with three closed boxes in a row, each of which might or might not have a gem inside it. Any prediction will identify at least two boxes that are guessed to be both empty, or two that are both filled. (Think about it for a moment and you’ll see that this must be so.) The suitors were asked to open those two, and if they were right then they would have the daughter’s hand. But they were never right! One of the opened boxes was always empty while the other contained a gem. How could that be? Surely chance alone should guarentee that someone would guess right some time? Finally the daughter, who was getting impatient to be wed, intervened by opening up the boxes for the rather dishy son of a prophet. But she didn’t open the two he’d predicted to be both full or both empty. Instead she opened one he’d guessed was filled, and one he’d guessed was empty. And both guesses proved correct. After some weak objection, the seer accepted that the suitor had made two correct guesses, and the daughter was married. The seer had foiled previous suitors because his boxes were quantum boxes, and he had entangled them to create correlations such that, if one opened box was filled, the other would be empty and vice versa. That made it impossible to satisfy the seer’s challenge and show you’d guessed right. But the daughter was able to reveal a correct guess by interrogating the same system with a different set of measurements. And that is what quantum contextuality is like.*", + "location": "2,021", + "color": "yellow", + "id": "30361820e37953d2" + }, + { + "text": "You can’t say, to make a macroscopic analogy, ‘the ball is red’, but only ‘the ball is red when looked at through the round window’. It really is red under these conditions (to the extent that we can ever say how something really is). But equally, it really is green through the square window. Yes, but what colour is it really? According to Kochen and Specker, you can’t improve on the answer I’ve just given.*", + "note": "Is this possible to create as an art piece?", + "location": "2,043", + "color": "yellow", + "id": "0ce94133b9500387" + }, + { + "text": "It is this waviness that gives rise to distinctly quantum phenomena like interference, superposition and entanglement. These behaviours become possible when there is a well-defined relationship between the quantum ‘waves’: in effect, when they are in step. This co-ordination is called coherence.", + "note": "coherence is a well-defined relationship between wave functions of 2 quantum objects", + "location": "2,129", + "color": "yellow", + "id": "04178c1ddb30d780" + }, + { + "text": "if the quantum wavefunctions of two states are not coherent, they cannot interfere, nor can they maintain a superposition. A loss of coherence (decoherence) therefore destroys these fundamentally quantum properties, and the states behave more like distinct classical systems.", + "location": "2,134", + "color": "yellow", + "id": "f9346992f3aa97a8" + }, + { + "text": "Quantum coherence is essentially what permits ‘quantumness’.", + "location": "2,140", + "color": "yellow", + "id": "7b168efa2be4e566" + }, + { + "text": "There is no reason (that we yet know of) why in principle objects cannot remain in coherent quantum states no matter how big they are – provided that no measurement is made on them. But it seems that measurement somehow does destroy quantum coherence, forcing us to speak of the wavefunction as having ‘collapsed’. If we can understand how measurement unravels coherence then we would be able to bring measurement itself within the scope of quantum theory, rather than making it a boundary where the theory stops.", + "note": "Measurement of quantum activity == Tower of Babel. Arrogance of Man, smited.", + "location": "2,141", + "color": "yellow", + "id": "2588eaa070e913ed" + }, + { + "text": "The truth is that they don’t. Quantum superpositions of states aren’t fragile. On the contrary, they are highly contagious and apt to spread out rapidly. And that is what seems to destroy them. If a quantum system in a superposed state interacts with another particle, the two become linked into a composite superposition. That, we saw earlier, is exactly what entanglement is: a superposed state of two particles, whose interaction has turned them into a single quantum entity.", + "location": "2,159", + "color": "yellow", + "id": "2891cbf02a805c22" + }, + { + "text": "In theory there is no end to this process. That entangled air molecule hits another, and the second molecule gets captured in an entangled state too. As time passes, the initial quantum system becomes more and more entangled with its environment. In effect, we then no longer have a well-defined quantum system embedded in an environment. Rather, system and environment have merged into a single superposition.", + "location": "2,166", + "color": "yellow", + "id": "3d488098bc360008" + }, + { + "text": "Decoherence is what destroys the possibility of observing macroscopic superpositions – including Schrödinger’s live/dead cat. And this has nothing to do with observation in the normal sense: we don’t need a conscious mind to ‘look’ in order to ‘collapse the wavefunction’. All we need is for the environment to disperse the quantum coherence. This happens with extraordinary efficiency – it’s probably the most efficient process known to science. And it is very clear why size matters here: there is simply more interaction with the environment, and therefore faster decoherence, for larger objects.", + "location": "2,218", + "color": "yellow", + "id": "9ebfb9db2ec8d4e9" + }, + { + "text": "Here, then, is a concrete way to look at that most puzzling of quantum mysteries, which is that what we measure depends on how we look – on the context of the measurement.", + "location": "2,412", + "color": "yellow", + "id": "f3ab22ef8599ca52" + }, + { + "text": "Charles Bennett and Gilles Brassard pointed out in the early 1980s that quantum correlations between entangled states can be exploited to send information encoded in qubits that is immune to eavesdropping. The signal could never be intercepted and read without the tampering being detected. This was the start of the technology known as quantum cryptography. The idea goes like this. Alice encodes her message in two entangled qubits – 1s and 0s represented by, say, the polarization states of pairs of photons. One photon from each entangled pair is then sent to Bob, who measures their states and so decodes the message.", + "location": "2,817", + "color": "yellow", + "id": "6fbf11cc7f78e906" + }, + { + "text": "I’ll say it upfront: no one fully understands how quantum computers work.", + "note": "Thank God!", + "location": "2,872", + "color": "yellow", + "id": "f77f5aca64862a45" + }, + { + "text": "But Bohr and colleagues didn’t bring wavefunction collapse into the picture just to make things difficult. They did it because that’s what seems to happen. When we make a measurement, we really do get just one result out of the many that quantum mechanics offers. Wavefunction collapse seemed to be demanded in order to connect the theory to reality. So what Everett was saying was that this isn’t, after all, what reality is. It’s not quantum mechanics that is at fault, but our concept of reality. We only think that there’s a single outcome of a measurement. But in fact all of them occur. We only see one of those realities, but the others have a separate physical existence too.", + "note": "If MWI is true then how do we deal with the law of conservation of matter? is anything other than possibilities \"lost\" when the wave function collapses?", + "location": "2,989", + "color": "yellow", + "id": "f435104e41a408b4" + }, + { + "text": "Luis Jorge Borges’ story ‘The Garden of Forking Paths’,", + "location": "3,035", + "color": "yellow", + "id": "63beceab91a143cf" + }, + { + "text": "The issue is whether, like Bohr, you believe that quantum mechanics provides a prescription for evaluating the possible outcomes we might observe when we look at the quantum world, or whether you regard the Schrödinger equation as an inviolable and universal law that describes – in some sense is – reality.", + "location": "3,154", + "color": "yellow", + "id": "dbd07712fc8b800a" + }, + { + "text": "Can it be right that the laws of Newtonian mechanics for our classical world, expressed in concise, prosaic words and sentences, must give way to something with the forbidding, abstract mathematical complexity of the quantum axioms? Or is this just because we don’t quite know what we’re talking about? When someone explains something in a complicated way, it’s often a sign that they don’t really understand it.", + "location": "3,334", + "color": "yellow", + "id": "4ad95cb872511658" + } + ], + "meta": { + "totalHighlights": 38 + } + }, + { + "bookId": "B018S2773Y", + "title": "Babylon's Ashes (The Expanse Book 6)", + "author": "James S. A. Corey", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:29.347Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B018S2773Y", + "highlights": [ + { + "text": "The John Galt and the Mark Watney, out of Mars.", + "location": "2,231", + "color": "yellow", + "id": "80d52cf15c16a655" + }, + { + "text": "Holden went on as if he hadn’t heard Alex at all. “I thought if you told people facts, they’d draw their conclusions, and because the facts were true, the conclusions mostly would be too. But we don’t run on facts. We run on stories about things. About people. Naomi told me that when the rocks fell, the people on Inaros’ ship cheered. They were happy about it.”", + "location": "2,491", + "color": "yellow", + "id": "f5962997eeef2e87" + }, + { + "text": "If someone killed Pallas, he’d be worried about what the drop in refining capacity would do before he thought about how many preschools there are on the station. Or if the station manager’s son liked writing poetry. Or that blowing the station meant that Annie down in Pallas central accounting wasn’t going to get to throw her big birthday party after all.”", + "location": "2,498", + "color": "yellow", + "id": "6aa557469ce19232" + }, + { + "text": "listening to the too-loud laughter of the soldiers, one moment came back to him vividly. His professor—an overweight, balding man with an alcoholic’s complexion and an air of intelligence so profound it seemed to bend space around him—lifting a hand and speaking a phrase: the terror of the normal.", + "location": "3,545", + "color": "yellow", + "id": "eb140b4855b3467c" + }, + { + "text": "Naomi smiled. Laughed. It was strange how it made the darkness better. Not less dark. Just better, even though it still was what it was.", + "note": "Trudie girl <3", + "location": "4,173", + "color": "yellow", + "id": "4739b1bce721bb57" + } + ], + "meta": { + "totalHighlights": 5 + } + }, + { + "bookId": "B00O7X626W", + "title": "Nemesis Games (The Expanse Book 5)", + "author": "James S. A. Corey", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:31.340Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B00O7X626W", + "highlights": [ + { + "text": "At least he knows there’s a problem. Realizing you’ve got shit on your fingers is the first step toward washing your hands.”", + "location": "2,763", + "color": "yellow", + "id": "ea53d4e84293a6d2" + }, + { + "text": "Thing is, we’re humans. We’re tribal. More settled things are, the bigger your tribe is. All the people in your gang, or all the people in your country. All the ones on your planet. Then the churn comes, and the tribe gets small again.”", + "location": "4,165", + "color": "yellow", + "id": "6ae6a173b7989140" + } + ], + "meta": { + "totalHighlights": 2 + } + }, + { + "bookId": "B00FPQA4F0", + "title": "Cibola Burn (The Expanse Book 4)", + "author": "James S. A. Corey", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:33.338Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B00FPQA4F0", + "highlights": [ + { + "text": "“I’m wondering what I did to deserve all this,” she said. “I’m about to see the first genuinely alien biosphere. I’m about to learn things about evolution that were literally impossible to know until now. I must have been a very, very good person in a past life.”", + "color": "yellow", + "id": "27d0814076dea78a" + }, + { + "text": "“A vast new frontier has opened up for us. We have the chance to create a new society, with untold riches beyond every gate. But this world has treasure, so instead of figuring out the right way to divide up the damned galaxy, we’ll fight over the first crumbs we find.”", + "color": "yellow", + "id": "4eb5389694b83cc3" + }, + { + "text": "“What the hell are you doing?” she asked. “What am I doing? You think that’s the question?” “You know that her husband—” “I don’t know a damned thing, Elvi. Neither do you. I’m rich in interpretation and poor in datasets, just the same as you.” “You think… you think it’s not…”", + "color": "yellow", + "id": "b1d2d13b585a4280" + }, + { + "text": "“No,” Alex said, a deep frown cutting into his forehead. “A person can fail the people they love just by being who they are. I’m who I am, and it wasn’t what my wife wanted me to be, and somethin’ had to break. You decided to do what you did down on that planet, and it put you up here with me instead of with your family.” Alex leaned forward, grabbing Basia’s hands in his own. “It’s still on you. I will never live down not being the person my wife needed after she spent twenty years waitin’ for me. I can never make that right. Don’t go feelin’ sorry for yourself. You fucked up. You failed the people you love. They’re payin’ the price for it right now and you demean them every second you don’t own that shit.”", + "color": "yellow", + "id": "d4c9c54daebf9692" + }, + { + "text": "Earth alone had managed five kingdoms of life. Six, if you agreed with the Fityani hypothesis.", + "note": "look that up", + "color": "yellow", + "id": "b11c24866c997d4f" + } + ], + "meta": { + "totalHighlights": 5 + } + }, + { + "bookId": "B00A2DZMYE", + "title": "Abaddon's Gate (The Expanse Book 3)", + "author": "James S. A. Corey", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:35.395Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B00A2DZMYE", + "highlights": [ + { + "text": "“What have we got?” Ashford said. “Short form.” “It’s fucking weird, sir,” Chan said, and everyone chuckled. “Our best analysis is that the Ring is an artificially sustained Einstein-Rosen bridge. You go through the Ring, you don’t come out the other side here.” “So it’s a gate,” Ashford said. “Yes, sir. It appears that the protomolecule or Phoebe bug or whatever you want to call it was launched at the solar system several billion years ago, aiming for Earth with the intention of hijacking primitive life to build a gateway. We’re positing that whoever created the protomolecule did it as a first step toward making travel to the solar system more convenient and practical later.”", + "color": "yellow", + "id": "a5dc98047e6c1861" + }, + { + "text": "Protogen had named the protomolecule and decided it was a tool that could redefine what it meant to be human. Jules-Pierre Mao had treated it like a weapon. It killed humans, therefore it was a weapon. But radiation killed humans, and a medical X-ray machine wasn’t intended as a weapon. Holden was starting to feel like they were all monkeys playing with a microwave. Push a button, a light comes on inside, so it’s a light. Push a different button and stick your hand inside, it burns you, so it’s a weapon. Learn to open and close the door, it’s a place to hide things. Never grasping what it actually did, and maybe not even having the framework necessary to figure it out. No monkey ever reheated a frozen burrito.", + "color": "yellow", + "id": "e806c0e6189b8868" + }, + { + "text": "The truth was, distance was always measured in time.", + "color": "yellow", + "id": "9beab4f86789032e" + }, + { + "text": "When the orbits were right, the journey from Luna to Mars could take as little as twelve days. The trek from Saturn to Ceres, a few months. And because they were out there with their primate brains, evolved on the plains of prehistoric Africa, everyone had a sense of how far it was. Saturn to Ceres was a few months. Luna to Mars was a few days. Distance was time, and so they didn’t get overwhelmed by it.", + "color": "yellow", + "id": "20a3a47972d17614" + } + ], + "meta": { + "totalHighlights": 4 + } + }, + { + "bookId": "B01LA4CEGM", + "title": "My Life with the Saints (10th Anniversary Edition)", + "author": "James Martin and John L Allen", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:37.330Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B01LA4CEGM", + "highlights": [ + { + "text": "As C. S. Lewis writes in Mere Christianity: “How monotonously alike all the great tyrants and conquerors have been: how gloriously different are the saints.”", + "color": "yellow", + "id": "83979c52df9fdb78" + }, + { + "text": "The best way to come to know St. Thomas More, I believe, is to watch A Man for All Seasons.", + "color": "yellow", + "id": "b61bbcbd42d06220" + }, + { + "text": "Enter the idea of studying business. At the time, I had a vague idea that a business major would enable me to land a job in the “business end” of any number of fields, such as the “business end” of English (publishing) or the “business end” of art (managing a museum or gallery). At the very least, a business degree would likely lead to a high-paying job after graduation, and this, I reasoned, would make me happy. On this point, everyone—my family, my friends, my high school guidance counselor—agreed. The problem was that whenever I considered “earning a living,” I thought mostly about the “earning” and nothing about the “living.”", + "color": "yellow", + "id": "16ee566e3c99233f" + }, + { + "text": "Wharton’s reputation in the corporate world made finding a job ridiculously easy. At the beginning of senior year, hundreds of corporate recruiters flocked to the school to conduct interviews: in a few months I had received a number of excellent offers. After narrowing them down to three or four, I settled on a corporate training program at General Electric in New York City. Once again, almost no reflection was involved in my decision. To use some accounting terminology, I simply went along with “generally accepted principles.” For example, what should you do at the end of business school? Interview for jobs. Which job offer should you accept? The one with the highest salary. The more important questions were the ones that no one asked me, or rather, that I failed to ask myself: What do you desire in life? And what does God desire for you?", + "color": "yellow", + "id": "c8a8210010acf5db" + }, + { + "text": "I looked down at the bright green grass and thought about Merton’s strange, complicated, and contradictory life. And about my favorite lines from Walt Whitman, which come near the end of “Song of Myself”: I bequeath myself to the dirt to grow from the grass I love. If you want me again look for me under your boot-soles.", + "color": "yellow", + "id": "3f6aa463c480bfd0" + }, + { + "text": "the vanities of the world,” particularly concerning his physical appearance. He seems also to have been a ladies’ man. At least that’s how he fancied himself. He was definitely a rake. It was rumored that he fathered an illegitimate child. (There is some evidence to support this, but it is inconclusive.) And he may be the only canonized saint with a notarized police record, for nighttime brawling with intent to inflict serious harm.", + "note": "Ignatious of Loyola", + "color": "yellow", + "id": "6fe13565f654950b" + }, + { + "text": "In Paris he also completed what later become known as The Spiritual Exercises, a handbook of practices on prayer, on the human condition, on God’s love, and on the life of Jesus, all designed to help people draw closer to God. Iñigo also led his new roommate, Francisco Javier, through these exercises. His friend would later become better known, of course, as St. Francis Xavier, one of the Church’s great missionaries.", + "color": "yellow", + "id": "3a13fb625c7fbcc0" + } + ], + "meta": { + "totalHighlights": 7 + } + }, + { + "bookId": "B005SCRR1A", + "title": "Caliban's War (The Expanse Book 2)", + "author": "James S. A. Corey", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:39.213Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B005SCRR1A", + "highlights": [ + { + "text": "“You live in your own world, dear one.” “I do,” Arjun agreed, running his thumb across the hand terminal’s screen. He looked up. “You don’t mind, do you?” “I love you for it. Stay here. Read about post-lyricism.” “What are you going to do?” “The same thing as always. Try to keep civilization from blowing up while the children are in it.”", + "color": "yellow", + "id": "618595cc362237fd" + }, + { + "text": "“This is what I’m talking about. The intensity of your feelings isn’t evidence.", + "color": "yellow", + "id": "3040bec9d4eca2b0" + }, + { + "text": "Prax looked at the mass of data being sent to Alex and said, “Why fly at all? Couldn’t the ship just use this data to do the docking itself?” “Why fly?” Alex repeated with a laugh. “ ’Cuz it’s fun, Doc. Because it’s fun.”", + "color": "yellow", + "id": "865de43fa4a21920" + }, + { + "text": "‘If life transcends death, then I will seek for you there. If not, then there too,’ ” she said. “It’s a fucking haiku. That man has a one-track mind and one train on it. Poetry. Save me from poetry.”", + "color": "yellow", + "id": "c27bc8cc64d50ea2" + }, + { + "text": "He also wasn’t being fair to Avasarala. It wasn’t about making him take her orders, not really. It was about being in a situation that she was utterly untrained for, where she was the least useful person in the room and trying to assert some control. Trying to reshape the space around her to fit with her mental image of herself.", + "color": "yellow", + "id": "21910fd8b621a73d" + } + ], + "meta": { + "totalHighlights": 5 + } + }, + { + "bookId": "B0047Y171G", + "title": "Leviathan Wakes (The Expanse Book 1)", + "author": "James S. A. Corey", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:41.109Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B0047Y171G", + "highlights": [ + { + "text": "The joke was that Ceres didn’t have laws—it had police.", + "color": "yellow", + "id": "3afa297363756f27" + }, + { + "text": "It was easy to make fun of the marines when they weren’t listening. In Holden’s navy days, making fun of jarheads was as natural as cussing. But four marines had died getting him off the Donnager, and three of them had made a conscious decision to do so. Holden promised himself that he’d never make fun of them again.", + "color": "yellow", + "id": "f2e42a14eb1bdc03" + }, + { + "text": "The beautiful thing about losing your illusions, he thought, was that you got to stop pretending. All the years he’d told himself that he was respected, that he was good at his job, that all his sacrifices had been made for a reason fell away and left him with the clear, unmuddied knowledge that he was a functional alcoholic who had pared away everything good in his own life to make room for anesthetic. Shaddid thought he was a joke. Muss thought he was the price she paid not to sleep with someone she didn’t like. The only one who might have any respect for him at all was Havelock, an Earther. It was peaceful, in its way.", + "color": "yellow", + "id": "9f989658f0a1bc62" + }, + { + "text": "“You can’t just throw information at people,” Miller said. “You have to know what it means. What it’s going to do.", + "color": "yellow", + "id": "360b35e2eb7ef222" + }, + { + "text": "“Let me get this straight,” he said. “You’ll give me all the kingdoms of the Earth if I just bow down and do one act of worship for you?” Dresden cocked his head. “I don’t know the reference.”", + "color": "yellow", + "id": "6dc4533f0b84c1c0" + }, + { + "text": "Mars would survive, for a while. Pockets of the Belt would hold out even longer, probably. They had a culture of making do, surviving on scraps, living on the bleeding edge of their resources. But in the end, without Earth, everything would eventually die. Humans had been out of the gravity well a long time. Long enough to have developed the technology to cut that umbilical cord, but they’d just never bothered to do it. Stagnant. Humanity, for all its desire to fling itself into every livable pocket it could reach, had become stagnant. Satisfied to fly around in ships built half a century before, using technology that hadn’t changed in longer than that.", + "color": "yellow", + "id": "0e1a7c9be5fd685c" + } + ], + "meta": { + "totalHighlights": 6 + } + }, + { + "bookId": "B0D3DS677F", + "title": "Been Wrong So Long It Feels Like Right: A King Oliver Novel", + "author": "Walter Mosley", + "source": "read.amazon.com", + "extractedAt": "2025-10-20T03:00:43.607Z", + "notebookUrl": "https://read.amazon.com/notebook?asin=B0D3DS677F", + "highlights": [ + { + "text": "“You know how you said that me and my friends were different?” “Yeah?” “And how I said that we were mostly the same as everybody else?” “Uh-huh.” “The only real difference is that we believe that we can create our own reality; hunting the hunters, expecting a woman to be able to match most men in combat, blow for blow.”", + "location": "1,393", + "color": "yellow", + "id": "2dc5adb4709eebcf" + } + ], + "meta": { + "totalHighlights": 1 + } + } +] \ No newline at end of file diff --git a/.kindle/config.json b/.kindle/config.json new file mode 100644 index 00000000..c866b40f --- /dev/null +++ b/.kindle/config.json @@ -0,0 +1,13 @@ +{ + "outputFolder": "02_Areas/Test", + "templatePath": ".scripts/kindle/templates/kindle-note.md.hbs", + "overwrite": false, + "addTags": [ + "kindle", + "highlights", + "books" + ], + "createIndex": true, + "indexPath": "03_Resources/Kindle Highlights Index.md", + "lastSync": "2025-10-20T03:00:43.737Z" +} \ No newline at end of file diff --git a/.scripts/kindle/auth.js b/.scripts/kindle/auth.js new file mode 100644 index 00000000..5019ea22 --- /dev/null +++ b/.scripts/kindle/auth.js @@ -0,0 +1,166 @@ +#!/usr/bin/env node +import { chromium } from 'playwright'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { NOTEBOOK_URL } from './lib/config.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth'); + +/** + * Check if authentication is successful by verifying page state + */ +async function checkAuthenticationSuccess(page) { + try { + const currentUrl = page.url(); + + // Check 1: Look for the book library container (most important - indicates successful load) + const hasLibrary = await page.locator('#kp-notebook-library, .kp-notebook-library').count() > 0; + + // Check 2: Verify auth cookies exist + const cookies = await page.context().cookies(); + const hasSessionToken = cookies.some((c) => c.name === 'session-token'); + + // Success if we have BOTH library and session token + // URL can still have signin params even after successful login, so we check it last + if (hasLibrary && hasSessionToken) { + return { success: true }; + } + + // Determine specific failure reason for helpful feedback + if (currentUrl.includes('signin') || currentUrl.includes('ap/signin')) { + if (!hasLibrary && !hasSessionToken) { + return { success: false, reason: 'Still on signin page' }; + } + } + + if (!hasLibrary) { + return { success: false, reason: 'Book library not loaded yet' }; + } + + if (!hasSessionToken) { + return { success: false, reason: 'No session token found' }; + } + + return { success: false, reason: 'Waiting for authentication' }; + } catch (error) { + return { success: false, reason: error.message }; + } +} + +/** + * Poll the page until authentication is successful or timeout + */ +async function waitForAuthenticationSuccess(page, timeoutMs = 300000) { + const startTime = Date.now(); + const pollInterval = 2000; // Check every 2 seconds + + console.log('⏳ Waiting for authentication to complete...'); + console.log(' (Polling page state automatically - this may take a few minutes)\n'); + + let lastReason = ''; + + while (Date.now() - startTime < timeoutMs) { + const result = await checkAuthenticationSuccess(page); + + if (result.success) { + return true; + } + + // Only log if reason changed (reduce spam) + if (result.reason !== lastReason) { + console.log(` 🔍 Status: ${result.reason}...`); + lastReason = result.reason; + } + + // Wait before next check + await page.waitForTimeout(pollInterval); + } + + throw new Error('Authentication timeout - please try again'); +} + +/** + * Interactive authentication - opens browser for user to log in + */ +async function main() { + console.log('🔐 Kindle Authentication\n'); + console.log('📁 Session will be saved to:', AUTH_DIR); + console.log('⏱️ Timeout: 5 minutes\n'); + + const ctx = await chromium.launchPersistentContext(AUTH_DIR, { + headless: false, + viewport: { height: 900, width: 1000 }, + // Position on right side of screen (works for most screen sizes) + args: ['--window-position=50,50'], + }); + + const page = await ctx.newPage(); + + try { + console.log('🌐 Opening Amazon Kindle Notebook...\n'); + await page.goto(NOTEBOOK_URL, { waitUntil: 'load' }); + + console.log('📖 INSTRUCTIONS:'); + console.log(' 1. Log in to Amazon in the browser window'); + console.log(' 2. Wait for your Kindle library/highlights to load'); + console.log(' 3. Authentication will be saved automatically!\n'); + console.log(' 💡 The script will detect when you\'re logged in.\n'); + + // Wait for successful authentication (auto-detected) + await waitForAuthenticationSuccess(page); + + console.log('\n✅ Authentication detected! Saving session...\n'); + + // Wait for network to settle + await page.waitForLoadState('networkidle').catch(() => { + console.log(' (Network still active, continuing anyway)'); + }); + + // Get final state + const finalUrl = page.url(); + console.log(` 📍 Final URL: ${finalUrl}`); + + // Clean up redirect parameters if needed + if (finalUrl.includes('openid') && !finalUrl.includes('signin')) { + console.log(' 🔄 Cleaning up redirect parameters...'); + await page.goto(NOTEBOOK_URL, { waitUntil: 'networkidle' }); + } + + // Verify cookies + const cookies = await ctx.cookies(); + const hasXMain = cookies.some((c) => c.name === 'x-main'); + const hasSessionToken = cookies.some((c) => c.name === 'session-token'); + + console.log( + ` 🍪 Auth cookies: x-main=${hasXMain}, session-token=${hasSessionToken}` + ); + + if (!hasSessionToken) { + console.log( + '\n⚠️ Warning: Session token not found. Authentication may be incomplete.' + ); + console.log(' Try running /kindle-auth again if sync fails.'); + } + + await ctx.close(); + + console.log('\n✅ Authentication saved successfully!\n'); + console.log('Next steps:'); + console.log(' • Run /kindle-sync to sync your highlights'); + console.log(' • Or run /kindle-sync-all for a full re-scrape\n'); + } catch (error) { + console.error('\n❌ Authentication failed:', error.message); + console.log('\nTroubleshooting:'); + console.log(' • Make sure you fully logged in to Amazon'); + console.log(' • Verify your book library is visible in the browser'); + console.log(' • Try running /kindle-auth again\n'); + await ctx.close(); + process.exit(1); + } +} + +main().catch((error) => { + console.error('❌ Authentication failed:', error); + process.exit(1); +}); diff --git a/.scripts/kindle/config.js b/.scripts/kindle/config.js new file mode 100644 index 00000000..4273170b --- /dev/null +++ b/.scripts/kindle/config.js @@ -0,0 +1,148 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CONFIG_DIR = path.resolve(__dirname, '../../.kindle'); +const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json'); + +/** + * Default configuration + */ +export const DEFAULT_CONFIG = { + outputFolder: '03_Resources/Kindle Highlights', + templatePath: '.scripts/kindle/templates/kindle-note.md.hbs', + overwrite: false, + addTags: ['kindle', 'highlights', 'books'], + createIndex: true, + indexPath: '03_Resources/Kindle Highlights Index.md', + lastSync: null, +}; + +/** + * Load configuration from file, or create with defaults if it doesn't exist + */ +export async function loadConfig() { + try { + const content = await fs.readFile(CONFIG_PATH, 'utf-8'); + const config = JSON.parse(content); + // Merge with defaults to ensure all fields are present + return { ...DEFAULT_CONFIG, ...config }; + } catch (error) { + // Config doesn't exist, create it with defaults + console.log('📝 Creating default configuration...'); + await saveConfig(DEFAULT_CONFIG); + return DEFAULT_CONFIG; + } +} + +/** + * Save configuration to file + */ +export async function saveConfig(config) { + await fs.mkdir(CONFIG_DIR, { recursive: true }); + await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8'); +} + +/** + * Update specific config fields + */ +export async function updateConfig(updates) { + const config = await loadConfig(); + const updatedConfig = { ...config, ...updates }; + await saveConfig(updatedConfig); + return updatedConfig; +} + +/** + * Interactive configuration setup + */ +async function interactiveConfig() { + console.log('🔧 Kindle Sync Configuration\n'); + + const currentConfig = await loadConfig(); + + console.log('Current configuration:'); + console.log(JSON.stringify(currentConfig, null, 2)); + console.log('\n'); + + // For now, just display the config and config file path + // In a full implementation, this would use a prompt library like 'prompts' or 'inquirer' + console.log(`📁 Config file: ${CONFIG_PATH}`); + console.log('\n📝 To modify configuration:'); + console.log(' 1. Edit the config file directly, or'); + console.log(' 2. Use the updateConfig() function in your scripts\n'); + + console.log('Available settings:'); + console.log(' • outputFolder: Where to save Kindle highlight notes'); + console.log(' • templatePath: Path to custom Handlebars template'); + console.log(' • overwrite: Whether to overwrite existing files'); + console.log(' • addTags: Array of tags to add to notes'); + console.log(' • createIndex: Whether to create a master index file'); + console.log(' • indexPath: Path to the master index file'); +} + +/** + * CLI interface + */ +async function main() { + const argv = await yargs(hideBin(process.argv)) + .command('show', 'Show current configuration') + .command('reset', 'Reset to default configuration') + .option('interactive', { + alias: 'i', + description: 'Run interactive configuration', + type: 'boolean', + }) + .option('set', { + alias: 's', + description: 'Set a config value (format: key=value)', + type: 'string', + }) + .help() + .alias('h', 'help') + .parseAsync(); + + if (argv.interactive) { + await interactiveConfig(); + } else if (argv.set) { + const [key, value] = argv.set.split('='); + if (!key || value === undefined) { + console.error('❌ Invalid format. Use: --set key=value'); + process.exit(1); + } + + // Parse value (try JSON parse for objects/arrays, otherwise string) + let parsedValue; + try { + parsedValue = JSON.parse(value); + } catch { + parsedValue = value; + } + + const config = await updateConfig({ [key]: parsedValue }); + console.log(`✅ Updated ${key} = ${JSON.stringify(parsedValue)}`); + console.log(`\n📁 Config saved to: ${CONFIG_PATH}`); + } else if (argv._[0] === 'reset') { + await saveConfig(DEFAULT_CONFIG); + console.log('✅ Configuration reset to defaults'); + console.log(JSON.stringify(DEFAULT_CONFIG, null, 2)); + } else { + // Default: show config + const config = await loadConfig(); + console.log('📋 Current Kindle Sync Configuration:\n'); + console.log(JSON.stringify(config, null, 2)); + console.log(`\n📁 Config file: ${CONFIG_PATH}`); + } +} + +// Run CLI if this file is executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + console.error('❌ Error:', error.message); + process.exit(1); + }); +} diff --git a/.scripts/kindle/lib/config.js b/.scripts/kindle/lib/config.js new file mode 100644 index 00000000..97ae255c --- /dev/null +++ b/.scripts/kindle/lib/config.js @@ -0,0 +1,65 @@ +/** + * Configuration for Kindle Notebook scraper + * Centralizes all selectors and scraping parameters + */ + +/** + * Default configuration for Kindle Notebook scraper + * These selectors were last verified: 2025-10-18 + * + * NOTE: Amazon's Kindle Notebook page structure may change. + * If scraping fails, inspect the page DOM and update these selectors. + */ +export const DEFAULT_SCRAPER_CONFIG = { + bookSelectors: { + // Sidebar container with list of books + container: ['#kp-notebook-library', '.kp-notebook-library'], + // Individual book items in sidebar (not actual links, but clickable divs) + links: '.kp-notebook-library-each-book', + }, + highlightSelectors: { + // Color is in the class name, e.g., .kp-notebook-highlight-yellow + colorAttribute: ['.kp-notebook-highlight'], + // Container for all highlights + item: ['#kp-notebook-annotations > .a-row.a-spacing-base', '.a-row.a-spacing-base'], + // Location is in the highlight header + location: ['#annotationHighlightHeader', '.kp-notebook-metadata'], + // Note text + note: ['.kp-notebook-note span#note', '.kp-notebook-note #note', 'span#note'], + // Highlight text + text: [ + '.kp-notebook-highlight span#highlight', + '.kp-notebook-highlight #highlight', + '#highlight', + ], + }, + metadataSelectors: { + // Author in sidebar + author: [ + '.kp-notebook-library-each-book p.kp-notebook-searchable', + 'p.kp-notebook-searchable', + ], + // Title in sidebar + title: [ + '.kp-notebook-library-each-book h2.kp-notebook-searchable', + 'h2.kp-notebook-searchable', + ], + // Copyright truncation warning + truncationWarning: [ + '.copyright-export-limit', + '.kp-notebook-annotations-limit-warning', + ], + }, + scraping: { + maxLazyLoadRounds: 30, + maxStableRounds: 3, + pageLoadTimeout: 30000, + rateLimitBaseDelay: 500, + rateLimitJitter: 400, + scrollBaseDelay: 600, + scrollDelayIncrease: 20, + scrollJitter: 400, + }, +}; + +export const NOTEBOOK_URL = 'https://read.amazon.com/notebook'; diff --git a/.scripts/kindle/lib/errors.js b/.scripts/kindle/lib/errors.js new file mode 100644 index 00000000..95a5cede --- /dev/null +++ b/.scripts/kindle/lib/errors.js @@ -0,0 +1,50 @@ +/** + * Custom error types for Kindle sync functionality + */ + +/** + * Base error class for all Kindle export errors + */ +export class KindleExportError extends Error { + constructor(message, context = {}) { + super(message); + this.name = this.constructor.name; + this.context = context; + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * Error thrown when authentication fails or is missing + */ +export class AuthenticationError extends KindleExportError {} + +/** + * Error thrown when data validation fails + */ +export class ValidationError extends KindleExportError { + constructor(message, errors, context = {}) { + super(message, context); + this.errors = errors; + } +} + +/** + * Error thrown when file operations fail + */ +export class FileSystemError extends KindleExportError { + constructor(message, path, context = {}) { + super(message, { ...context, path }); + this.path = path; + } +} + +/** + * Format error for user-friendly display + */ +export function formatError(error) { + if (error instanceof Error) { + return `${error.name}: ${error.message}`; + } + return String(error); +} diff --git a/.scripts/kindle/lib/renderer.js b/.scripts/kindle/lib/renderer.js new file mode 100644 index 00000000..d2131adc --- /dev/null +++ b/.scripts/kindle/lib/renderer.js @@ -0,0 +1,130 @@ +import Handlebars from 'handlebars'; +import { decode } from 'html-entities'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createBookFilename, formatDate } from './utils.js'; +import { parseBookExports } from './validation.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * Extract author's last name from full author name + */ +function extractLastName(author) { + if (!author) return 'Unknown'; + const parts = author.trim().split(/\s+/); + return parts[parts.length - 1] || 'Unknown'; +} + +/** + * Create a Kindle app link for a specific location + */ +function createKindleLink(bookId, location) { + if (!location) return ''; + // Remove commas from location number + const cleanLocation = location.replace(/,/g, ''); + return `kindle://book?action=open&asin=${bookId}&location=${cleanLocation}`; +} + +/** + * Register Handlebars helpers + */ +function registerHandlebarsHelpers() { + // Helper to decode HTML entities + Handlebars.registerHelper('decode', function (text) { + return new Handlebars.SafeString(decode(text || '')); + }); + + // Helper to create Kindle app links + Handlebars.registerHelper('kindleLink', function (bookId, location) { + return createKindleLink(bookId, location); + }); + + // Helper to format dates + Handlebars.registerHelper('formatDate', function (date) { + return formatDate(date); + }); +} + +/** + * Load and compile Handlebars template + */ +export async function loadTemplate(templatePath) { + // Default template location + const defaultTemplate = path.resolve(__dirname, '../templates/kindle-note.md.hbs'); + const resolvedPath = templatePath || defaultTemplate; + + const templateSrc = await fs.readFile(resolvedPath, 'utf-8'); + return Handlebars.compile(templateSrc); +} + +/** + * Render a single book to Markdown + */ +export function renderBook(book, template) { + registerHandlebarsHelpers(); + return template(book); +} + +/** + * Export books to Markdown format using Handlebars template + */ +export async function exportBooksToMarkdown(books, outputDir, options = {}) { + const { + templatePath, + overwrite = false, + template: precompiledTemplate, + } = options; + + // Use provided template or load from path + const template = precompiledTemplate || (await loadTemplate(templatePath)); + + // Ensure output directory exists + await fs.mkdir(outputDir, { recursive: true }); + + let written = 0; + let skipped = 0; + const files = []; + + for (const book of books) { + // Create filename + const filename = createBookFilename(book.title, book.author, book.bookId); + const destPath = path.join(outputDir, filename); + + // Check if file exists and handle overwrite logic + if (!overwrite) { + try { + await fs.access(destPath); + skipped++; + continue; // File exists and we don't want to overwrite + } catch { + // File doesn't exist, proceed + } + } + + const markdown = renderBook(book, template); + await fs.writeFile(destPath, markdown, 'utf-8'); + written++; + files.push({ book, path: destPath }); + } + + return { written, skipped, files }; +} + +/** + * Load books from JSON file + */ +export async function loadBooksFromJSON(jsonPath) { + const rawJson = await fs.readFile(jsonPath, 'utf-8'); + const parsedJson = JSON.parse(rawJson); + return parseBookExports(parsedJson); +} + +/** + * Save books to JSON file + */ +export async function saveBooksToJSON(books, jsonPath) { + await fs.mkdir(path.dirname(jsonPath), { recursive: true }); + await fs.writeFile(jsonPath, JSON.stringify(books, null, 2), 'utf-8'); +} diff --git a/.scripts/kindle/lib/scraper.js b/.scripts/kindle/lib/scraper.js new file mode 100644 index 00000000..1a3b781a --- /dev/null +++ b/.scripts/kindle/lib/scraper.js @@ -0,0 +1,281 @@ +import { chromium } from 'playwright'; +import { AuthenticationError } from './errors.js'; +import { generateHighlightId } from './utils.js'; +import { DEFAULT_SCRAPER_CONFIG, NOTEBOOK_URL } from './config.js'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const AUTH_DIR = path.resolve(__dirname, '../../../.kindle/auth'); + +/** + * Launch a persistent browser context using saved authentication + */ +export async function getContext(headless = false) { + return chromium.launchPersistentContext(AUTH_DIR, { + headless, + viewport: { height: 900, width: 1000 }, + // Keep browser in consistent position for both auth and scraping + args: headless ? [] : ['--window-position=50,50'], + }); +} + +/** + * Verify authentication by checking if we're redirected to signin + */ +export async function ensureAuth(page) { + console.log(`🔍 Navigating to: ${NOTEBOOK_URL}`); + await page.goto(NOTEBOOK_URL, { waitUntil: 'networkidle' }); + console.log(`📍 Current URL: ${page.url()}`); + + if (/signin|ap\/signin/i.test(page.url())) { + throw new AuthenticationError( + 'Not authenticated. Authentication is required to access Kindle highlights.', + { currentUrl: page.url(), expectedUrl: NOTEBOOK_URL } + ); + } + + console.log('✅ Authentication verified'); +} + +/** + * Discover all books from the sidebar + */ +export async function discoverBooks(page, config = DEFAULT_SCRAPER_CONFIG) { + console.log('🔍 Discovering books from sidebar...'); + + // Wait for notebook container to load + const containerSelector = config.bookSelectors.container.join(', '); + await page + .waitForSelector(containerSelector, { + timeout: 10000, + }) + .catch(() => { + console.warn('⚠️ Could not find notebook container, proceeding anyway...'); + }); + + // Extract all book items from sidebar + const bookSelector = config.bookSelectors.links; + const books = await page.$$eval( + bookSelector, + (elements, selectors) => { + return elements.map((el) => { + // ASIN is the ID of the book div + const asin = el.id || ''; + + // Extract title + let title = 'Unknown'; + for (const selector of selectors.title) { + const titleEl = el.querySelector(selector); + if (titleEl?.textContent.trim()) { + title = titleEl.textContent.trim(); + break; + } + } + + // Extract author + let author; + for (const selector of selectors.author) { + const authorEl = el.querySelector(selector); + if (authorEl?.textContent.trim()) { + author = authorEl.textContent.trim(); + // Remove "By: " prefix if present + author = author.replace(/^By:\s*/i, ''); + break; + } + } + + return { asin, author, title }; + }); + }, + { + author: config.metadataSelectors.author, + title: config.metadataSelectors.title, + } + ); + + console.log(`📚 Found ${books.length} book(s)`); + return books.filter((b) => b.asin); // Only return books with valid ASINs +} + +/** + * Click a book in the sidebar and wait for highlights to load + */ +export async function clickBook(page, asin) { + // Find the book div by ASIN + // The data-action is on a span, and the anchor is inside it + const bookDiv = page.locator(`#${asin}`); + const clickableLink = bookDiv.locator('span[data-action="get-annotations-for-asin"] a'); + + await clickableLink.click(); + + // Wait for highlights to load + await page.waitForSelector('#kp-notebook-annotations', { timeout: 5000 }).catch(() => { + console.warn('⚠️ Annotations container not found, continuing anyway...'); + }); + + // Additional wait for content to populate + await page.waitForTimeout(500); +} + +/** + * Scrape a single book's highlights from the current page (after clicking it) + */ +export async function scrapeBook(page, bookInfo, config = DEFAULT_SCRAPER_CONFIG) { + const { asin, author, title } = bookInfo; + + // Click the book to load its highlights + await clickBook(page, asin); + + // Check for copyright truncation warning + const truncationSelector = config.metadataSelectors.truncationWarning.join(', '); + const truncated = (await page.locator(truncationSelector).count()) > 0; + + // Extract highlights from the annotations container + const annotationsContainer = '#kp-notebook-annotations'; + const highlightItems = await page.$$( + `${annotationsContainer} > .a-row.a-spacing-base` + ); + + const highlights = await Promise.all( + highlightItems.map(async (item) => { + // Extract highlight text + let text = ''; + const highlightTextEl = await item.$('.kp-notebook-highlight #highlight'); + if (highlightTextEl) { + text = (await highlightTextEl.textContent())?.trim() || ''; + } + + // Extract note text (if present) + let note; + const noteTextEl = await item.$('.kp-notebook-note #note'); + if (noteTextEl) { + note = (await noteTextEl.textContent())?.trim() || undefined; + } + + // Extract location from metadata + let location; + const locationEl = await item.$('#annotationHighlightHeader'); + if (locationEl) { + const locationText = (await locationEl.textContent())?.trim() || ''; + // Parse "Yellow highlight | Location: 102" to extract location + const match = /Location:\s*(\S+)/i.exec(locationText); + if (match) { + location = match[1]; + } + } + + // Extract color from highlight class + let color; + const highlightDiv = await item.$('.kp-notebook-highlight'); + if (highlightDiv) { + const className = await highlightDiv.getAttribute('class'); + // Look for color in class name like "kp-notebook-highlight-yellow" + const colorMatch = className?.match(/kp-notebook-highlight-(\w+)/); + if (colorMatch) { + color = colorMatch[1]; + } + } + + return { + text, + note, + location, + color, + }; + }) + ); + + // Filter out empty highlights and generate stable IDs + const validHighlights = highlights + .filter((h) => h.text.length > 0) + .map((h) => ({ + ...h, + id: generateHighlightId(h.text, h.location), + })); + + const notebookUrl = `${NOTEBOOK_URL}?asin=${asin}`; + + console.log(`✅ Scraped: ${title} (${validHighlights.length} highlights)`); + + return { + bookId: asin, + title, + author: author || undefined, + source: 'read.amazon.com', + extractedAt: new Date().toISOString(), + notebookUrl, + highlights: validHighlights, + meta: { + totalHighlights: validHighlights.length, + truncatedByCopyright: truncated || undefined, + }, + }; +} + +/** + * Scrape all books from Kindle Notebook + */ +export async function scrapeAllBooks(options = {}) { + const { limit, headless = false, config = DEFAULT_SCRAPER_CONFIG } = options; + + console.log('📖 Starting Kindle Notebook scraper\n'); + + // Launch browser and authenticate + // Using headed mode by default because Amazon detects headless browsers + const ctx = await getContext(headless); + const page = await ctx.newPage(); + + try { + await ensureAuth(page); + + // Discover all books from the sidebar + const allBooks = await discoverBooks(page, config); + + if (allBooks.length === 0) { + console.warn('⚠️ No books found. Check selectors or notebook page structure.'); + await ctx.close(); + return { books: [], errors: [] }; + } + + // Apply limit if specified + const books = limit ? allBooks.slice(0, limit) : allBooks; + + if (limit && allBooks.length > limit) { + console.log(`📚 Found ${allBooks.length} books, scraping first ${limit}\n`); + } + + // Scrape each book + const results = []; + const errors = []; + + for (let i = 0; i < books.length; i++) { + const bookInfo = books[i]; + console.log(`\n📄 Processing book ${i + 1}/${books.length}: ${bookInfo.title}`); + + try { + const book = await scrapeBook(page, bookInfo, config); + results.push(book); + + // Rate limiting: random delay between books + if (i < books.length - 1) { + const delay = + config.scraping.rateLimitBaseDelay + + Math.random() * config.scraping.rateLimitJitter; + await page.waitForTimeout(delay); + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.error(`❌ Failed to scrape book: ${errorMsg}`); + errors.push({ asin: bookInfo.asin, title: bookInfo.title, error: errorMsg }); + } + } + + await ctx.close(); + + return { books: results, errors }; + } catch (error) { + await ctx.close(); + throw error; + } +} diff --git a/.scripts/kindle/lib/utils.js b/.scripts/kindle/lib/utils.js new file mode 100644 index 00000000..9dc647ab --- /dev/null +++ b/.scripts/kindle/lib/utils.js @@ -0,0 +1,60 @@ +import { createHash } from 'node:crypto'; + +/** + * Sanitize a string for use in filenames by replacing invalid characters + */ +export function fileSafe(input) { + return input.replace(/[\\/:*?"<>|]/g, '_').trim(); +} + +/** + * Generate a stable hash-based ID for a highlight + * Uses text + location to ensure stability across exports + */ +export function generateHighlightId(text, location) { + const content = location ? `${text}::${location}` : text; + return createHash('sha256').update(content).digest('hex').slice(0, 16); +} + +/** + * Extract ASIN from Amazon Kindle Notebook URL + * Falls back to slugified title if ASIN not found + */ +export function extractBookId(url, title) { + const asinMatch = /[?&]asin=([^&]+)/.exec(url); + if (asinMatch?.[1]) { + return asinMatch[1]; + } + + // Fallback: slugify title + return title + .toLowerCase() + .trim() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .slice(0, 50); // Reasonable length limit +} + +/** + * Format date for YAML frontmatter + */ +export function formatDate(date) { + if (typeof date === 'string') { + date = new Date(date); + } + return date.toISOString().split('T')[0]; +} + +/** + * Create a safe filename from book title and author + */ +export function createBookFilename(title, author, asin) { + const safeTitle = fileSafe(title); + const safeAuthor = author ? fileSafe(author) : ''; + + if (safeAuthor) { + return `${safeTitle} - ${safeAuthor}.md`; + } + return `${safeTitle}.md`; +} diff --git a/.scripts/kindle/lib/validation.js b/.scripts/kindle/lib/validation.js new file mode 100644 index 00000000..dad9d3bd --- /dev/null +++ b/.scripts/kindle/lib/validation.js @@ -0,0 +1,94 @@ +import { z } from 'zod'; +import { ValidationError } from './errors.js'; + +/** + * Zod schema for highlight colors + */ +export const highlightColorSchema = z.string(); + +/** + * Zod schema for a single highlight + */ +export const highlightSchema = z.object({ + id: z.string().min(1), + text: z.string().min(1), + note: z.string().optional(), + location: z.string().optional(), + color: highlightColorSchema.optional(), +}); + +/** + * Zod schema for book export metadata + */ +export const bookMetaSchema = z + .object({ + totalHighlights: z.number().int().nonnegative().optional(), + truncatedByCopyright: z.boolean().optional(), + }) + .optional(); + +/** + * Zod schema for a complete book export + */ +export const bookExportSchema = z.object({ + bookId: z.string().min(1), + title: z.string().min(1), + author: z.string().optional(), + source: z.literal('read.amazon.com'), + extractedAt: z.string().datetime(), + notebookUrl: z.string().url().optional(), + highlights: z.array(highlightSchema), + meta: bookMetaSchema, +}); + +/** + * Zod schema for an array of book exports + */ +export const bookExportsSchema = z.array(bookExportSchema); + +/** + * Format Zod error issues into readable error messages + */ +function formatZodError(error) { + return error.issues.map((issue) => { + const path = issue.path.join('.'); + return `${path}: ${issue.message}`; + }); +} + +/** + * Parse and validate an array of book exports + * @throws {ValidationError} if validation fails + */ +export function parseBookExports(data) { + try { + return bookExportsSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new ValidationError( + 'Failed to validate book exports', + formatZodError(error), + { data } + ); + } + throw error; + } +} + +/** + * Validate a single book export + */ +export function validateBookExport(data) { + try { + return bookExportSchema.parse(data); + } catch (error) { + if (error instanceof z.ZodError) { + throw new ValidationError( + 'Failed to validate book export', + formatZodError(error), + { data } + ); + } + throw error; + } +} diff --git a/.scripts/kindle/sync.js b/.scripts/kindle/sync.js new file mode 100644 index 00000000..93f9ba84 --- /dev/null +++ b/.scripts/kindle/sync.js @@ -0,0 +1,363 @@ +#!/usr/bin/env node +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import readline from 'node:readline'; +import { loadConfig, updateConfig } from './config.js'; +import { scrapeAllBooks } from './lib/scraper.js'; +import { exportBooksToMarkdown, saveBooksToJSON, loadTemplate } from './lib/renderer.js'; +import { formatError, AuthenticationError } from './lib/errors.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const CACHE_PATH = path.resolve(__dirname, '../../.kindle/cache/all_books.json'); +const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth'); +const AUTH_SCRIPT = path.resolve(__dirname, './auth.js'); +const CONFIG_PATH = path.resolve(__dirname, '../../.kindle/config.json'); + +/** + * Check if authentication exists + */ +async function checkAuthExists() { + try { + const stats = await fs.stat(AUTH_DIR); + if (!stats.isDirectory()) return false; + + // Check if auth directory has files + const files = await fs.readdir(AUTH_DIR); + return files.length > 0; + } catch { + return false; + } +} + +/** + * Check if this is the first run (no config file exists) + */ +async function isFirstRun() { + try { + await fs.access(CONFIG_PATH); + return false; + } catch { + return true; + } +} + +/** + * Prompt user for output folder on first run + */ +async function promptForOutputFolder() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + console.log('\n📁 Kindle Sync Setup\n'); + console.log('Where would you like to save your Kindle highlights?'); + console.log('(Press Enter to use default: 03_Resources/Kindle Highlights)\n'); + + rl.question('Output folder: ', (answer) => { + rl.close(); + const folder = answer.trim() || '03_Resources/Kindle Highlights'; + resolve(folder); + }); + }); +} + +/** + * Run authentication script + */ +async function runAuth() { + console.log('🔐 Authentication required. Starting auth flow...\n'); + + return new Promise((resolve, reject) => { + const authProcess = spawn('node', [AUTH_SCRIPT], { + stdio: 'inherit', // Pass through stdin/stdout/stderr + cwd: path.dirname(AUTH_SCRIPT), + }); + + authProcess.on('close', (code) => { + if (code === 0) { + console.log('\n✅ Authentication complete! Continuing with sync...\n'); + resolve(); + } else { + reject(new Error(`Authentication failed with exit code ${code}`)); + } + }); + + authProcess.on('error', (error) => { + reject(new Error(`Failed to start auth process: ${error.message}`)); + }); + }); +} + +/** + * Main sync function + */ +async function sync(options) { + const { + all = false, + limit, + last, + force = false, + books: specificBooks, + outputFolder, + headless = false, + } = options; + + console.log('📚 Kindle Highlights Sync\n'); + + // Check if first run and prompt for folder + const firstRun = await isFirstRun(); + if (firstRun && !outputFolder) { + const userFolder = await promptForOutputFolder(); + outputFolder = userFolder; + console.log(`\n✅ Will save highlights to: ${userFolder}\n`); + + // Save the user's choice to config + await updateConfig({ outputFolder: userFolder }); + } else if (firstRun && outputFolder) { + // First run with --output flag provided: save to config + console.log(`📁 First run: saving output folder to config...\n`); + await updateConfig({ outputFolder }); + } + + // Debug: Log options + console.log('Options:', { all, limit, last, force, books: specificBooks, outputFolder }); + console.log(); + + // Check authentication before proceeding with scraping + const needsScraping = force || all || specificBooks || !all; + if (needsScraping) { + const hasAuth = await checkAuthExists(); + if (!hasAuth) { + console.log('⚠️ No authentication found.\n'); + await runAuth(); + } + } + + // Load configuration + const config = await loadConfig(); + const resolvedOutputFolder = outputFolder || config.outputFolder; + + // Resolve output folder relative to vault root + const vaultRoot = path.resolve(__dirname, '../../'); + const outputDir = path.resolve(vaultRoot, resolvedOutputFolder); + + console.log(`📁 Output folder: ${resolvedOutputFolder}`); + console.log(`📄 Template: ${config.templatePath}\n`); + + // Check if we should use cached data or scrape fresh + let books; + const useCache = !force && !all && !specificBooks && !limit; + + if (useCache) { + try { + console.log('🔍 Checking for cached data...'); + const { loadBooksFromJSON } = await import('./lib/renderer.js'); + books = await loadBooksFromJSON(CACHE_PATH); + console.log(`✅ Loaded ${books.length} books from cache\n`); + } catch (error) { + console.log('📥 No cache found, will scrape fresh data\n'); + books = null; + } + } + + // Scrape if no cache or forced + if (!books) { + console.log('🌐 Scraping Kindle Notebook...\n'); + + // Determine scraping strategy + // --last requires scraping ALL books to know which are the last N + const scrapeLimit = all ? undefined : (last ? undefined : limit); + + if (last) { + console.log(`📋 Will scrape all books, then select last ${last}...\n`); + } else if (limit) { + console.log(`📋 Will scrape first ${limit} books...\n`); + } + + try { + const result = await scrapeAllBooks({ + limit: scrapeLimit, + headless, + }); + + books = result.books; + + // If using --last flag, get the last N books instead of first N + if (last) { + if (books.length > last) { + console.log(`\n📚 Selecting last ${last} books from ${books.length} total...\n`); + books = books.slice(-last); + } else { + console.log(`\n📚 Found ${books.length} books (less than ${last} requested)\n`); + } + } + + if (result.errors.length > 0) { + console.warn(`\n⚠️ Failed to scrape ${result.errors.length} book(s):`); + result.errors.forEach(({ title, error }) => { + console.warn(` - ${title}: ${error}`); + }); + } + + // Save to cache + console.log('\n💾 Saving to cache...'); + await saveBooksToJSON(books, CACHE_PATH); + console.log(` ✅ Cached ${books.length} books`); + } catch (error) { + // If authentication error, try to re-auth and retry once + if (error instanceof AuthenticationError) { + console.error('\n❌ Authentication expired or invalid.\n'); + await runAuth(); + console.log('\n🔄 Retrying scrape...\n'); + + try { + const result = await scrapeAllBooks({ + limit: scrapeLimit, + headless, + }); + books = result.books; + + if (last) { + if (books.length > last) { + books = books.slice(-last); + } + } + + if (result.errors.length > 0) { + console.warn(`\n⚠️ Failed to scrape ${result.errors.length} book(s):`); + result.errors.forEach(({ title, error }) => { + console.warn(` - ${title}: ${error}`); + }); + } + + // Save to cache + console.log('\n💾 Saving to cache...'); + await saveBooksToJSON(books, CACHE_PATH); + console.log(` ✅ Cached ${books.length} books`); + } catch (retryError) { + console.error('\n❌ Scraping failed after re-authentication:'); + console.error(formatError(retryError)); + throw retryError; + } + } else { + console.error('\n❌ Scraping failed:'); + console.error(formatError(error)); + throw error; + } + } + } + + if (books.length === 0) { + console.log('⚠️ No books to sync'); + return; + } + + // Filter for specific books if requested + let booksToExport = books; + if (specificBooks) { + const bookTitles = specificBooks.split(',').map((t) => t.trim().toLowerCase()); + booksToExport = books.filter((b) => + bookTitles.some((title) => b.title.toLowerCase().includes(title)) + ); + console.log(`\n🔍 Filtered to ${booksToExport.length} book(s) matching: ${specificBooks}`); + } + + // Load template + console.log('\n📝 Rendering Markdown...'); + const templatePath = path.resolve(vaultRoot, config.templatePath); + const template = await loadTemplate(templatePath); + + // Export to Markdown + const result = await exportBooksToMarkdown(booksToExport, outputDir, { + template, + overwrite: config.overwrite, + }); + + console.log('\n✅ Sync complete!'); + console.log(` 📝 Written: ${result.written} file(s)`); + if (result.skipped > 0) { + console.log(` ⏭️ Skipped: ${result.skipped} file(s) (already exist)`); + } + console.log(` 📁 Location: ${outputDir}`); + + // Update last sync time + await updateConfig({ lastSync: new Date().toISOString() }); + + // TODO: Optionally create index file if config.createIndex is true + if (config.createIndex) { + console.log('\n📋 Index file creation not yet implemented'); + console.log(' You can manually create an index by linking to the book notes'); + } +} + +/** + * CLI interface + */ +async function main() { + const argv = await yargs(hideBin(process.argv)) + .option('all', { + alias: 'a', + description: 'Sync all books (re-scrape everything)', + type: 'boolean', + default: false, + }) + .option('limit', { + alias: 'l', + description: 'Limit number of books to sync - takes FIRST N books', + type: 'number', + }) + .option('last', { + description: 'Sync last N books instead of first N', + type: 'number', + }) + .option('force', { + alias: 'f', + description: 'Force re-scrape even if cache exists', + type: 'boolean', + default: false, + }) + .option('books', { + alias: 'b', + description: 'Comma-separated list of book titles to sync (partial match)', + type: 'string', + }) + .option('output', { + alias: 'o', + description: 'Override output folder from config', + type: 'string', + }) + .option('headless', { + description: 'Run browser in headless mode (not recommended - may cause auth issues)', + type: 'boolean', + default: false, + }) + .example('$0', 'Sync using cached data') + .example('$0 --all', 'Re-scrape all books') + .example('$0 --limit 5', 'Scrape first 5 books only') + .example('$0 --last 5', 'Scrape last 5 books only') + .example('$0 --books "Atomic Habits,Deep Work"', 'Sync specific books') + .help() + .alias('h', 'help') + .parseAsync(); + + await sync({ + all: argv.all, + limit: argv.limit, + last: argv.last, + force: argv.force, + books: argv.books, + outputFolder: argv.output, + headless: argv.headless, + }); +} + +main().catch((error) => { + console.error('\n❌ Sync failed:', formatError(error)); + process.exit(1); +}); diff --git a/.scripts/kindle/templates/kindle-note.md.hbs b/.scripts/kindle/templates/kindle-note.md.hbs new file mode 100644 index 00000000..06cccad7 --- /dev/null +++ b/.scripts/kindle/templates/kindle-note.md.hbs @@ -0,0 +1,26 @@ +--- +title: "{{title}}" +author: "{{#if author}}{{author}}{{else}}Unknown{{/if}}" +asin: {{bookId}} +highlights: {{highlights.length}} +lastSynced: {{extractedAt}} +--- + +# {{title}} + +{{#if meta.truncatedByCopyright}} + +> ⚠️ Some highlights were truncated due to publisher export limits. +{{/if}} + +## Highlights + +{{#each highlights}} +{{text}}{{#if location}} - loc: [{{location}}]({{kindleLink ../bookId location}}){{/if}} + +{{#if note}} +***Note:*** {{{decode note}}} +{{/if}} + +--- +{{/each}} \ No newline at end of file diff --git a/02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md b/02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md new file mode 100644 index 00000000..e8392ce8 --- /dev/null +++ b/02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md @@ -0,0 +1,29 @@ +--- +title: "Abaddon's Gate (The Expanse Book 3)" +author: "James S. A. Corey" +asin: B00A2DZMYE +highlights: 4 +lastSynced: 2025-10-20T03:00:35.395Z +--- + +# Abaddon's Gate (The Expanse Book 3) + + +## Highlights + +“What have we got?” Ashford said. “Short form.” “It’s fucking weird, sir,” Chan said, and everyone chuckled. “Our best analysis is that the Ring is an artificially sustained Einstein-Rosen bridge. You go through the Ring, you don’t come out the other side here.” “So it’s a gate,” Ashford said. “Yes, sir. It appears that the protomolecule or Phoebe bug or whatever you want to call it was launched at the solar system several billion years ago, aiming for Earth with the intention of hijacking primitive life to build a gateway. We’re positing that whoever created the protomolecule did it as a first step toward making travel to the solar system more convenient and practical later.” + + +--- +Protogen had named the protomolecule and decided it was a tool that could redefine what it meant to be human. Jules-Pierre Mao had treated it like a weapon. It killed humans, therefore it was a weapon. But radiation killed humans, and a medical X-ray machine wasn’t intended as a weapon. Holden was starting to feel like they were all monkeys playing with a microwave. Push a button, a light comes on inside, so it’s a light. Push a different button and stick your hand inside, it burns you, so it’s a weapon. Learn to open and close the door, it’s a place to hide things. Never grasping what it actually did, and maybe not even having the framework necessary to figure it out. No monkey ever reheated a frozen burrito. + + +--- +The truth was, distance was always measured in time. + + +--- +When the orbits were right, the journey from Luna to Mars could take as little as twelve days. The trek from Saturn to Ceres, a few months. And because they were out there with their primate brains, evolved on the plains of prehistoric Africa, everyone had a sense of how far it was. Saturn to Ceres was a few months. Luna to Mars was a few days. Distance was time, and so they didn’t get overwhelmed by it. + + +--- diff --git a/02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md b/02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md new file mode 100644 index 00000000..89e7ea55 --- /dev/null +++ b/02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md @@ -0,0 +1,34 @@ +--- +title: "Babylon's Ashes (The Expanse Book 6)" +author: "James S. A. Corey" +asin: B018S2773Y +highlights: 5 +lastSynced: 2025-10-20T02:59:36.018Z +--- + +# Babylon's Ashes (The Expanse Book 6) + + +## Highlights + +The John Galt and the Mark Watney, out of Mars. - loc: [2,231](kindle://book?action=open&asin=B018S2773Y&location=2231) + + +--- +Holden went on as if he hadn’t heard Alex at all. “I thought if you told people facts, they’d draw their conclusions, and because the facts were true, the conclusions mostly would be too. But we don’t run on facts. We run on stories about things. About people. Naomi told me that when the rocks fell, the people on Inaros’ ship cheered. They were happy about it.” - loc: [2,491](kindle://book?action=open&asin=B018S2773Y&location=2491) + + +--- +If someone killed Pallas, he’d be worried about what the drop in refining capacity would do before he thought about how many preschools there are on the station. Or if the station manager’s son liked writing poetry. Or that blowing the station meant that Annie down in Pallas central accounting wasn’t going to get to throw her big birthday party after all.” - loc: [2,498](kindle://book?action=open&asin=B018S2773Y&location=2498) + + +--- +listening to the too-loud laughter of the soldiers, one moment came back to him vividly. His professor—an overweight, balding man with an alcoholic’s complexion and an air of intelligence so profound it seemed to bend space around him—lifting a hand and speaking a phrase: the terror of the normal. - loc: [3,545](kindle://book?action=open&asin=B018S2773Y&location=3545) + + +--- +Naomi smiled. Laughed. It was strange how it made the darkness better. Not less dark. Just better, even though it still was what it was. - loc: [4,173](kindle://book?action=open&asin=B018S2773Y&location=4173) + +***Note:*** Trudie girl <3 + +--- diff --git a/02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md b/02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md new file mode 100644 index 00000000..70fd81c0 --- /dev/null +++ b/02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md @@ -0,0 +1,17 @@ +--- +title: "Been Wrong So Long It Feels Like Right: A King Oliver Novel" +author: "Walter Mosley" +asin: B0D3DS677F +highlights: 1 +lastSynced: 2025-10-20T03:00:43.607Z +--- + +# Been Wrong So Long It Feels Like Right: A King Oliver Novel + + +## Highlights + +“You know how you said that me and my friends were different?” “Yeah?” “And how I said that we were mostly the same as everybody else?” “Uh-huh.” “The only real difference is that we believe that we can create our own reality; hunting the hunters, expecting a woman to be able to match most men in combat, blow for blow.” - loc: [1,393](kindle://book?action=open&asin=B0D3DS677F&location=1393) + + +--- diff --git a/02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md b/02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md new file mode 100644 index 00000000..51651546 --- /dev/null +++ b/02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md @@ -0,0 +1,181 @@ +--- +title: "Beyond Weird: Why Everything You Thought You Knew about Quantum Physics Is Different" +author: "Philip Ball" +asin: B07H9HNDYL +highlights: 38 +lastSynced: 2025-10-20T02:59:33.827Z +--- + +# Beyond Weird: Why Everything You Thought You Knew about Quantum Physics Is Different + + +## Highlights + +Rather than insisting on its difficulty, we might better regard it as a beguiling, maddening, even amusing gauntlet thrown down to challenge the imagination. For that is indeed what is challenged. I suspect we are, in the wider cultural context, finally beginning to appreciate this. Artists, writers, poets and playwrights have started to imbibe and deploy ideas from quantum physics: see, for instance, plays such as Tom Stoppard’s Hapgood and Michael Frayn’s Copenhagen, and novels such as Jeanette Winterson’s Gut Symmetries and Audrey Niffenegger’s The Time Traveler’s Wife. We can argue about how accurately or aptly these writers appropriate the scientific ideas, but it is right that there should be imaginative responses to quantum mechanics, because it is quite possible that only an imagination sufficiently broad and liberated will come close to articulating what it is about. - loc: [102](kindle://book?action=open&asin=B07H9HNDYL&location=102) + +***Note:*** Check out these works, especially Winterson + +--- +Feynman seemed to feel that it was impossible and even pointless to attempt anything comparable for quantum mechanics: We can’t pretend to understand it since it affronts all our commonsense notions. The best we can do is to describe what happens in mathematics, in equations, and that’s very difficult. What is even harder is trying to decide what the equations mean. That’s the hardest thing of all. - loc: [143](kindle://book?action=open&asin=B07H9HNDYL&location=143) + + +--- +only a few decades ago, professing a serious interest in the topic could be tantamount to career suicide for a young physicist. Only a handful of scientists and philosophers, idiosyncratically if not plain crankily, insisted on caring about the answer. Many researchers would shrug or roll their eyes when the ‘meaning’ of quantum mechanics came up; some still do. ‘Ah, nobody understands it anyway!’ How different this is from the attitude of Albert Einstein, Niels Bohr and their contemporaries, for whom grappling with the apparent oddness of the theory became almost an obsession. For them, the meaning mattered intensely. In 1998 the American physicist John Wheeler, a pioneer of modern quantum theory, lamented the loss of the ‘desperate puzzlement’ that was in the air in the 1930s. ‘I want to recapture that feeling for all, even if it is my last act on Earth’, Wheeler said. - loc: [153](kindle://book?action=open&asin=B07H9HNDYL&location=153) + +***Note:*** "desperate puzzlement" + +--- +You can see that I’m already struggling to find a language that works for talking about these things. That’s OK, and you’ll have to get used to it. That’s how it should be. When words come too easily, it’s because we haven’t delved deeply enough (you’ll see that scientists can be guilty of that too). - loc: [191](kindle://book?action=open&asin=B07H9HNDYL&location=191) + +***Note:*** I've been thinkibg lately about if (really how) the human need for language inhibits our ability to understand things. If your mother tongue does not have the words for something then how much harder is it to understand? What is the instance of bilingualism among Nobel-winning scientists? + +--- +and there are plenty of accessible books on the subject.*2 - loc: [208](kindle://book?action=open&asin=B07H9HNDYL&location=208) + + +--- +Best of all for its narrative value is the largely good-natured but trenchant dispute that rumbled on for decades between Einstein and Bohr about what it all meant – about the nature of reality. This is indeed a superb story, and if you haven’t heard it before then you should.*1 - loc: [256](kindle://book?action=open&asin=B07H9HNDYL&location=256) + + +--- +The best illustration I know of that quantization is rather incidental to quantum theory is found in the book Quantum Mechanics: The Theoretical Minimum, based on a series of lectures that Leonard Susskind, a professor of theoretical physics at Stanford University, gave to undergraduates, which were written up with the help of the writer Art Friedman. The book is described as being ‘for anyone who ever regretted not taking physics at university, who knows a little but would like to know more’. - loc: [337](kindle://book?action=open&asin=B07H9HNDYL&location=337) + +***Note:*** I'm less than 10% into this book and already have 3 more to read! + +--- +Quantum objects are what they are, and we have no reason to suppose that ‘what they are’ changes in any meaningful way depending on how we try to look at them. Rather, all we can say is that what we measure sometimes looks like what we would expect to see if we were measuring discrete little ball-like entities, while in other experiments it looks like the behaviour expected of waves of the same kind as those of sound travelling in air, or that wrinkle and swell on the sea surface. So the phrase ‘wave–particle duality’ doesn’t really refer to quantum objects at all, but to the interpretation of experiments – which is to say, to our human-scale view of things. - loc: [392](kindle://book?action=open&asin=B07H9HNDYL&location=392) + + +--- +What makes the quantum situation of radioactive decay any different from this classical (and, forgive me, rather unfortunate) analogy of childbirth? We have every reason to suppose that, if we were able to monitor the biology of each pregnant mother and baby sufficiently closely, we could understand exactly why the respective birth process started when it did – perhaps some hormonal threshold was reached, say. But for radioactive decay, there is nothing you can monitor to explain why a particular atom decayed when it did. There is nothing we can call a reason. - loc: [606](kindle://book?action=open&asin=B07H9HNDYL&location=606) + +***Note:*** At this point in knowledge, sure. But there has to be an explanation, a trigger. We can't just live in a world where things happen with no cause. If that was the case, then what is the source of the wave function? + +--- +So, said Bohr, we had better just stop there. As he put it: There is no quantum world. There is only an abstract quantum physical description. It is wrong to think that the task of physics is to find out how nature is. Physics concerns what we can say about nature. - loc: [777](kindle://book?action=open&asin=B07H9HNDYL&location=777) + + +--- +At face value that sounds crazy. Why make a mathematical theory like quantum mechanics if you don’t think it can tell you about the system it is supposed to describe? But Bohr argued that quantum theory tells you something more meaningful – indeed, the only thing that can be meaningful. It tells you what you will find when you try to investigate that system. It tells you about measurements. - loc: [783](kindle://book?action=open&asin=B07H9HNDYL&location=783) + + +--- +the early days of this new physics, the problem was often debated in terms of the ‘role of the observer’. That the observer had a role at all was deeply troubling, because it seemed to challenge the very concept of science. If what we see depends on what questions we ask, whither then the idea of an objective world, governed by rules that pertain independently of our attempts to figure them out? As Heisenberg put it, science had ceased to be a way of peeking unnoticed at the world, and instead had become ‘an actor in [the] interplay between man and nature’. - loc: [813](kindle://book?action=open&asin=B07H9HNDYL&location=813) + +***Note:*** Like how what we get from an LLM depends on not just what questions we ask it but how. + +--- +But measurement does something else. It ‘collapses’ (Heisenberg’s original word was ‘reduces’) those possibilities, expressed in the wavefunction, to just one. Suppose that, before the measurement, the probabilities of finding some property of a quantum object with the values corresponding to states A, B and C are 10%, 70% and 20% respectively. When we make a single measurement on the object, we might find that we get the result C. What happened to A and B? We are forced to assert that the probabilities have now changed: that for C it is 100%, and for A and B it is zero. What’s more, we can’t get A and B back: if we repeat the measurement, we’ll keep getting C.*2 - loc: [1,043](kindle://book?action=open&asin=B07H9HNDYL&location=1043) + +***Note:*** We will keep getting C *unless and until* we reset the quantum object in its original state. How in tge world does that happen?? + +--- +To Bohr, wavefunction collapse was virtually emblematic of the distinction between the unitary quantum world and the everyday reality in which we make observations and measurements. Measurements must be classical by definition: they require some big apparatus with which humans can interact. From our perspective the world is made up of phenomena – things happen – and a phenomenon only exists when it has been measured. Wavefunction collapse is simply a name we give to the process by which we turn quantum states into observed phenomena. Wavefunction collapse is then a generator of knowledge: it is not so much a process that gives us the answers, but is the process by which answers are created. The outcome of that process can’t, in general, be predicted with certainty, but quantum mechanics gives us a method for calculating the probabilities of particular outcomes. That’s all we can ask for. - loc: [1,062](kindle://book?action=open&asin=B07H9HNDYL&location=1062) + + +--- +Bohr could be stubborn and dogmatic as well as rather cryptic. But he deserves praise for defining the limits of what can be said with confidence. Some suspect that his intuition surpassed anything he could justify mathematically, or indeed semantically. As von Weizsäcker said, ‘Bohr was essentially right but he did not know why.’ - loc: [1,110](kindle://book?action=open&asin=B07H9HNDYL&location=1110) + + +--- +But nevertheless the notion of a universal wavefunction is popular with cosmologists, for the perfectly valid reason that in the earliest moments of the Big Bang the entire universe was smaller than an atom and surely needs to be considered, in that moment, a quantum-mechanical entity. - loc: [1,222](kindle://book?action=open&asin=B07H9HNDYL&location=1222) + +***Note:*** Should we just accept that as fact? + +--- +Quantum mechanics generally assumes that quantum states exist in some meaningful sense, and that the math tells us what we can know about those states. But in QBism there are no objective states. Rather, according to Chris Fuchs, ‘quantum states represent observers’ personal information, expectations and degrees of belief’. This view, he says, ‘allows one to see all quantum measurement events as little “moments of creation”, rather than as revealing anything pre-existent’. - loc: [1,303](kindle://book?action=open&asin=B07H9HNDYL&location=1303) + +***Note:*** What if every time we collapse a wave function we are setting off a big bang in another universe? + +--- +The main thing you need to know about entanglement is this: it tells us that a quantum object may have properties that are not entirely located on that object. - loc: [1,704](kindle://book?action=open&asin=B07H9HNDYL&location=1704) + + +--- +But this locality is just what quantum entanglement undermines – which is why ‘spooky action at a distance’ is precisely the wrong way to look at it. We can’t regard particle A and particle B in the EPR experiment as separate entities, even though they are separated in space. As far as quantum mechanics is concerned, entanglement makes them both parts of a single object. Or to put it another way, the spin of particle A is not located solely on A in the way that the redness of a cricket ball is located on the cricket ball. In quantum mechanics, properties can be non-local. - loc: [1,931](kindle://book?action=open&asin=B07H9HNDYL&location=1931) + + +--- +What in fact we’re dealing with here is another kind of quantum superposition. We’ve seen that superposition refers to a situation in which a measurement on a quantum object could produce two or more possible outcomes, but we don’t know which it will be, only their relative probabilities. Entanglement is that same idea applied to two or more particles: a superposition of the state in which particle A has spin up and B spin down, say, and the state with the opposite configuration. Although the particles are separated, they must be described by a single wavefunction. We can’t untangle that wavefunction into some combination of two single-particle wavefunctions. - loc: [1,936](kindle://book?action=open&asin=B07H9HNDYL&location=1936) + +***Note:*** If we could change the spin of an entangled particle, would that change the spin of the second particle or would in disentangle the particles? Can you even disentangle particles? Can you have an entangled triad of particles? + +--- +Kochen and Specker pointed out that the outcomes of quantum measurements may depend on their context. This means something subtly different from the fact that different types of experiment on what seems like the same system (a double-slit experiment with or without a ‘path detector’, say) give different results. It means that if you look at a quantum object through different windows, you see different things. If you want to count the number of black and white balls in a jar, it doesn’t matter if you count the black ones or the white ones first, or if you tot them up in rows of five, or if you separate the two colours into piles and weigh them. You’ll get the same answer. But in quantum mechanics, even when you ask the same question (‘How many black and white balls are there?’), the answer you get may depend on how the measurement is done. - loc: [2,009](kindle://book?action=open&asin=B07H9HNDYL&location=2009) + +***Note:*** Here is this similarity to interfacing with LLMs again + +--- +The Kochen–Specker theorem stipulates what follows from this dependence on context. In effect it is another corollary of the way that, in quantum mechanics, what we choose not to measure may leave an imprint on what we do measure. - loc: [2,018](kindle://book?action=open&asin=B07H9HNDYL&location=2018) + + +--- +An Assyrian seer, unwilling to let his young daughter be wed to suitors who he considered unworthy, set them a task. He presented them with three closed boxes in a row, each of which might or might not have a gem inside it. Any prediction will identify at least two boxes that are guessed to be both empty, or two that are both filled. (Think about it for a moment and you’ll see that this must be so.) The suitors were asked to open those two, and if they were right then they would have the daughter’s hand. But they were never right! One of the opened boxes was always empty while the other contained a gem. How could that be? Surely chance alone should guarentee that someone would guess right some time? Finally the daughter, who was getting impatient to be wed, intervened by opening up the boxes for the rather dishy son of a prophet. But she didn’t open the two he’d predicted to be both full or both empty. Instead she opened one he’d guessed was filled, and one he’d guessed was empty. And both guesses proved correct. After some weak objection, the seer accepted that the suitor had made two correct guesses, and the daughter was married. The seer had foiled previous suitors because his boxes were quantum boxes, and he had entangled them to create correlations such that, if one opened box was filled, the other would be empty and vice versa. That made it impossible to satisfy the seer’s challenge and show you’d guessed right. But the daughter was able to reveal a correct guess by interrogating the same system with a different set of measurements. And that is what quantum contextuality is like.* - loc: [2,021](kindle://book?action=open&asin=B07H9HNDYL&location=2021) + + +--- +You can’t say, to make a macroscopic analogy, ‘the ball is red’, but only ‘the ball is red when looked at through the round window’. It really is red under these conditions (to the extent that we can ever say how something really is). But equally, it really is green through the square window. Yes, but what colour is it really? According to Kochen and Specker, you can’t improve on the answer I’ve just given.* - loc: [2,043](kindle://book?action=open&asin=B07H9HNDYL&location=2043) + +***Note:*** Is this possible to create as an art piece? + +--- +It is this waviness that gives rise to distinctly quantum phenomena like interference, superposition and entanglement. These behaviours become possible when there is a well-defined relationship between the quantum ‘waves’: in effect, when they are in step. This co-ordination is called coherence. - loc: [2,129](kindle://book?action=open&asin=B07H9HNDYL&location=2129) + +***Note:*** coherence is a well-defined relationship between wave functions of 2 quantum objects + +--- +if the quantum wavefunctions of two states are not coherent, they cannot interfere, nor can they maintain a superposition. A loss of coherence (decoherence) therefore destroys these fundamentally quantum properties, and the states behave more like distinct classical systems. - loc: [2,134](kindle://book?action=open&asin=B07H9HNDYL&location=2134) + + +--- +Quantum coherence is essentially what permits ‘quantumness’. - loc: [2,140](kindle://book?action=open&asin=B07H9HNDYL&location=2140) + + +--- +There is no reason (that we yet know of) why in principle objects cannot remain in coherent quantum states no matter how big they are – provided that no measurement is made on them. But it seems that measurement somehow does destroy quantum coherence, forcing us to speak of the wavefunction as having ‘collapsed’. If we can understand how measurement unravels coherence then we would be able to bring measurement itself within the scope of quantum theory, rather than making it a boundary where the theory stops. - loc: [2,141](kindle://book?action=open&asin=B07H9HNDYL&location=2141) + +***Note:*** Measurement of quantum activity == Tower of Babel. Arrogance of Man, smited. + +--- +The truth is that they don’t. Quantum superpositions of states aren’t fragile. On the contrary, they are highly contagious and apt to spread out rapidly. And that is what seems to destroy them. If a quantum system in a superposed state interacts with another particle, the two become linked into a composite superposition. That, we saw earlier, is exactly what entanglement is: a superposed state of two particles, whose interaction has turned them into a single quantum entity. - loc: [2,159](kindle://book?action=open&asin=B07H9HNDYL&location=2159) + + +--- +In theory there is no end to this process. That entangled air molecule hits another, and the second molecule gets captured in an entangled state too. As time passes, the initial quantum system becomes more and more entangled with its environment. In effect, we then no longer have a well-defined quantum system embedded in an environment. Rather, system and environment have merged into a single superposition. - loc: [2,166](kindle://book?action=open&asin=B07H9HNDYL&location=2166) + + +--- +Decoherence is what destroys the possibility of observing macroscopic superpositions – including Schrödinger’s live/dead cat. And this has nothing to do with observation in the normal sense: we don’t need a conscious mind to ‘look’ in order to ‘collapse the wavefunction’. All we need is for the environment to disperse the quantum coherence. This happens with extraordinary efficiency – it’s probably the most efficient process known to science. And it is very clear why size matters here: there is simply more interaction with the environment, and therefore faster decoherence, for larger objects. - loc: [2,218](kindle://book?action=open&asin=B07H9HNDYL&location=2218) + + +--- +Here, then, is a concrete way to look at that most puzzling of quantum mysteries, which is that what we measure depends on how we look – on the context of the measurement. - loc: [2,412](kindle://book?action=open&asin=B07H9HNDYL&location=2412) + + +--- +Charles Bennett and Gilles Brassard pointed out in the early 1980s that quantum correlations between entangled states can be exploited to send information encoded in qubits that is immune to eavesdropping. The signal could never be intercepted and read without the tampering being detected. This was the start of the technology known as quantum cryptography. The idea goes like this. Alice encodes her message in two entangled qubits – 1s and 0s represented by, say, the polarization states of pairs of photons. One photon from each entangled pair is then sent to Bob, who measures their states and so decodes the message. - loc: [2,817](kindle://book?action=open&asin=B07H9HNDYL&location=2817) + + +--- +I’ll say it upfront: no one fully understands how quantum computers work. - loc: [2,872](kindle://book?action=open&asin=B07H9HNDYL&location=2872) + +***Note:*** Thank God! + +--- +But Bohr and colleagues didn’t bring wavefunction collapse into the picture just to make things difficult. They did it because that’s what seems to happen. When we make a measurement, we really do get just one result out of the many that quantum mechanics offers. Wavefunction collapse seemed to be demanded in order to connect the theory to reality. So what Everett was saying was that this isn’t, after all, what reality is. It’s not quantum mechanics that is at fault, but our concept of reality. We only think that there’s a single outcome of a measurement. But in fact all of them occur. We only see one of those realities, but the others have a separate physical existence too. - loc: [2,989](kindle://book?action=open&asin=B07H9HNDYL&location=2989) + +***Note:*** If MWI is true then how do we deal with the law of conservation of matter? is anything other than possibilities "lost" when the wave function collapses? + +--- +Luis Jorge Borges’ story ‘The Garden of Forking Paths’, - loc: [3,035](kindle://book?action=open&asin=B07H9HNDYL&location=3035) + + +--- +The issue is whether, like Bohr, you believe that quantum mechanics provides a prescription for evaluating the possible outcomes we might observe when we look at the quantum world, or whether you regard the Schrödinger equation as an inviolable and universal law that describes – in some sense is – reality. - loc: [3,154](kindle://book?action=open&asin=B07H9HNDYL&location=3154) + + +--- +Can it be right that the laws of Newtonian mechanics for our classical world, expressed in concise, prosaic words and sentences, must give way to something with the forbidding, abstract mathematical complexity of the quantum axioms? Or is this just because we don’t quite know what we’re talking about? When someone explains something in a complicated way, it’s often a sign that they don’t really understand it. - loc: [3,334](kindle://book?action=open&asin=B07H9HNDYL&location=3334) + + +--- diff --git a/02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md b/02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md new file mode 100644 index 00000000..4e2692a3 --- /dev/null +++ b/02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md @@ -0,0 +1,33 @@ +--- +title: "Caliban's War (The Expanse Book 2)" +author: "James S. A. Corey" +asin: B005SCRR1A +highlights: 5 +lastSynced: 2025-10-20T03:00:39.213Z +--- + +# Caliban's War (The Expanse Book 2) + + +## Highlights + +“You live in your own world, dear one.” “I do,” Arjun agreed, running his thumb across the hand terminal’s screen. He looked up. “You don’t mind, do you?” “I love you for it. Stay here. Read about post-lyricism.” “What are you going to do?” “The same thing as always. Try to keep civilization from blowing up while the children are in it.” + + +--- +“This is what I’m talking about. The intensity of your feelings isn’t evidence. + + +--- +Prax looked at the mass of data being sent to Alex and said, “Why fly at all? Couldn’t the ship just use this data to do the docking itself?” “Why fly?” Alex repeated with a laugh. “ ’Cuz it’s fun, Doc. Because it’s fun.” + + +--- +‘If life transcends death, then I will seek for you there. If not, then there too,’ ” she said. “It’s a fucking haiku. That man has a one-track mind and one train on it. Poetry. Save me from poetry.” + + +--- +He also wasn’t being fair to Avasarala. It wasn’t about making him take her orders, not really. It was about being in a situation that she was utterly untrained for, where she was the least useful person in the room and trying to assert some control. Trying to reshape the space around her to fit with her mental image of herself. + + +--- diff --git a/02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md b/02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md new file mode 100644 index 00000000..e8a3b617 --- /dev/null +++ b/02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md @@ -0,0 +1,34 @@ +--- +title: "Cibola Burn (The Expanse Book 4)" +author: "James S. A. Corey" +asin: B00FPQA4F0 +highlights: 5 +lastSynced: 2025-10-20T02:59:40.160Z +--- + +# Cibola Burn (The Expanse Book 4) + + +## Highlights + +“I’m wondering what I did to deserve all this,” she said. “I’m about to see the first genuinely alien biosphere. I’m about to learn things about evolution that were literally impossible to know until now. I must have been a very, very good person in a past life.” + + +--- +“A vast new frontier has opened up for us. We have the chance to create a new society, with untold riches beyond every gate. But this world has treasure, so instead of figuring out the right way to divide up the damned galaxy, we’ll fight over the first crumbs we find.” + + +--- +“What the hell are you doing?” she asked. “What am I doing? You think that’s the question?” “You know that her husband—” “I don’t know a damned thing, Elvi. Neither do you. I’m rich in interpretation and poor in datasets, just the same as you.” “You think… you think it’s not…” + + +--- +“No,” Alex said, a deep frown cutting into his forehead. “A person can fail the people they love just by being who they are. I’m who I am, and it wasn’t what my wife wanted me to be, and somethin’ had to break. You decided to do what you did down on that planet, and it put you up here with me instead of with your family.” Alex leaned forward, grabbing Basia’s hands in his own. “It’s still on you. I will never live down not being the person my wife needed after she spent twenty years waitin’ for me. I can never make that right. Don’t go feelin’ sorry for yourself. You fucked up. You failed the people you love. They’re payin’ the price for it right now and you demean them every second you don’t own that shit.” + + +--- +Earth alone had managed five kingdoms of life. Six, if you agreed with the Fityani hypothesis. + +***Note:*** look that up + +--- diff --git a/02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md b/02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md new file mode 100644 index 00000000..70b8a559 --- /dev/null +++ b/02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md @@ -0,0 +1,37 @@ +--- +title: "Leviathan Wakes (The Expanse Book 1)" +author: "James S. A. Corey" +asin: B0047Y171G +highlights: 6 +lastSynced: 2025-10-20T03:00:41.109Z +--- + +# Leviathan Wakes (The Expanse Book 1) + + +## Highlights + +The joke was that Ceres didn’t have laws—it had police. + + +--- +It was easy to make fun of the marines when they weren’t listening. In Holden’s navy days, making fun of jarheads was as natural as cussing. But four marines had died getting him off the Donnager, and three of them had made a conscious decision to do so. Holden promised himself that he’d never make fun of them again. + + +--- +The beautiful thing about losing your illusions, he thought, was that you got to stop pretending. All the years he’d told himself that he was respected, that he was good at his job, that all his sacrifices had been made for a reason fell away and left him with the clear, unmuddied knowledge that he was a functional alcoholic who had pared away everything good in his own life to make room for anesthetic. Shaddid thought he was a joke. Muss thought he was the price she paid not to sleep with someone she didn’t like. The only one who might have any respect for him at all was Havelock, an Earther. It was peaceful, in its way. + + +--- +“You can’t just throw information at people,” Miller said. “You have to know what it means. What it’s going to do. + + +--- +“Let me get this straight,” he said. “You’ll give me all the kingdoms of the Earth if I just bow down and do one act of worship for you?” Dresden cocked his head. “I don’t know the reference.” + + +--- +Mars would survive, for a while. Pockets of the Belt would hold out even longer, probably. They had a culture of making do, surviving on scraps, living on the bleeding edge of their resources. But in the end, without Earth, everything would eventually die. Humans had been out of the gravity well a long time. Long enough to have developed the technology to cut that umbilical cord, but they’d just never bothered to do it. Stagnant. Humanity, for all its desire to fling itself into every livable pocket it could reach, had become stagnant. Satisfied to fly around in ships built half a century before, using technology that hadn’t changed in longer than that. + + +--- diff --git a/02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md b/02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md new file mode 100644 index 00000000..bc565ef6 --- /dev/null +++ b/02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md @@ -0,0 +1,42 @@ +--- +title: "My Life with the Saints (10th Anniversary Edition)" +author: "James Martin and John L Allen" +asin: B01LA4CEGM +highlights: 7 +lastSynced: 2025-10-20T03:00:37.330Z +--- + +# My Life with the Saints (10th Anniversary Edition) + + +## Highlights + +As C. S. Lewis writes in Mere Christianity: “How monotonously alike all the great tyrants and conquerors have been: how gloriously different are the saints.” + + +--- +The best way to come to know St. Thomas More, I believe, is to watch A Man for All Seasons. + + +--- +Enter the idea of studying business. At the time, I had a vague idea that a business major would enable me to land a job in the “business end” of any number of fields, such as the “business end” of English (publishing) or the “business end” of art (managing a museum or gallery). At the very least, a business degree would likely lead to a high-paying job after graduation, and this, I reasoned, would make me happy. On this point, everyone—my family, my friends, my high school guidance counselor—agreed. The problem was that whenever I considered “earning a living,” I thought mostly about the “earning” and nothing about the “living.” + + +--- +Wharton’s reputation in the corporate world made finding a job ridiculously easy. At the beginning of senior year, hundreds of corporate recruiters flocked to the school to conduct interviews: in a few months I had received a number of excellent offers. After narrowing them down to three or four, I settled on a corporate training program at General Electric in New York City. Once again, almost no reflection was involved in my decision. To use some accounting terminology, I simply went along with “generally accepted principles.” For example, what should you do at the end of business school? Interview for jobs. Which job offer should you accept? The one with the highest salary. The more important questions were the ones that no one asked me, or rather, that I failed to ask myself: What do you desire in life? And what does God desire for you? + + +--- +I looked down at the bright green grass and thought about Merton’s strange, complicated, and contradictory life. And about my favorite lines from Walt Whitman, which come near the end of “Song of Myself”: I bequeath myself to the dirt to grow from the grass I love. If you want me again look for me under your boot-soles. + + +--- +the vanities of the world,” particularly concerning his physical appearance. He seems also to have been a ladies’ man. At least that’s how he fancied himself. He was definitely a rake. It was rumored that he fathered an illegitimate child. (There is some evidence to support this, but it is inconclusive.) And he may be the only canonized saint with a notarized police record, for nighttime brawling with intent to inflict serious harm. + +***Note:*** Ignatious of Loyola + +--- +In Paris he also completed what later become known as The Spiritual Exercises, a handbook of practices on prayer, on the human condition, on God’s love, and on the life of Jesus, all designed to help people draw closer to God. Iñigo also led his new roommate, Francisco Javier, through these exercises. His friend would later become better known, of course, as St. Francis Xavier, one of the Church’s great missionaries. + + +--- diff --git a/02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md b/02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md new file mode 100644 index 00000000..15846555 --- /dev/null +++ b/02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md @@ -0,0 +1,21 @@ +--- +title: "Nemesis Games (The Expanse Book 5)" +author: "James S. A. Corey" +asin: B00O7X626W +highlights: 2 +lastSynced: 2025-10-20T02:59:38.069Z +--- + +# Nemesis Games (The Expanse Book 5) + + +## Highlights + +At least he knows there’s a problem. Realizing you’ve got shit on your fingers is the first step toward washing your hands.” - loc: [2,763](kindle://book?action=open&asin=B00O7X626W&location=2763) + + +--- +Thing is, we’re humans. We’re tribal. More settled things are, the bigger your tribe is. All the people in your gang, or all the people in your country. All the ones on your planet. Then the churn comes, and the tribe gets small again.” - loc: [4,165](kindle://book?action=open&asin=B00O7X626W&location=4165) + + +--- diff --git a/02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md b/02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md new file mode 100644 index 00000000..3167282b --- /dev/null +++ b/02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md @@ -0,0 +1,37 @@ +--- +title: "Persepolis Rising (The Expanse Book 7)" +author: "James S. A. Corey" +asin: B06XKN9G27 +highlights: 6 +lastSynced: 2025-10-20T02:59:31.761Z +--- + +# Persepolis Rising (The Expanse Book 7) + + +## Highlights + +there had been a time when riding the lift instead of climbing the ladder would have felt like admitting defeat, even if the ship was burning ten times that hard. But for the last couple years Bobbie’s joints had been giving her trouble, and proving to herself that she could make the climb had stopped mattering as much. It seemed to her that the real sign you were getting old was when you stopped needing to prove you weren’t getting old. - loc: [426](kindle://book?action=open&asin=B06XKN9G27&location=426) + + +--- +“All right, then,” Bobbie said. “The way I see it, the next step is find someone who can get messages back to the union. Or Earth-Mars. See if there’s anyone out there with a plan, or if we’re going to have to make one up on our own.” “We can do that,” Amos said. “Shouldn’t be hard.” “You sure?” Alex said. “This is Medina Station under occupation by a bunch of splinter Martian military expats. It’s not Baltimore.” Amos’ smile was as placid as always. “Everywhere’s Baltimore.” - loc: [3,003](kindle://book?action=open&asin=B06XKN9G27&location=3003) + + +--- +If they kill us all, Drummer thought, this will be why. Not their technology, not their strategy, not the invisible cycle of history. It’ll be our inability to do anything without five committee meetings to talk about it. - loc: [3,771](kindle://book?action=open&asin=B06XKN9G27&location=3771) + + +--- +It was all becoming normal. He could see it in the way the clerk served up the terrible, terrible coffee. He could hear it in the conversations at the nearby tables. It showed on the screens and in the gaits of the people walking by. Panic and alarm were exhausting. He was exhausted by them, and Medina was exhausted too. It was already shifting into its new routine. Checkpoints, yes. Armed security, yes. All the theater of dominance and control and nothing to undercut that narrative. - loc: [4,044](kindle://book?action=open&asin=B06XKN9G27&location=4044) + + +--- +controlling don’t have a voice in how you control them. As long as everyone’s on the same page, things may be great, but when there’s a question, you win. Right?” “There has to be a way to come to a final decision.” “No, there doesn’t. Every time someone starts talking about final anythings in politics, that means the atrocities are warming up. Humanity has done amazing things by just muddling through, arguing and complaining and fighting and negotiating. It’s messy and undignified, but it’s when we’re at our best, because everyone gets to have a voice in it. Even if everyone else is trying to shout it down. Whenever there’s just one voice that matters, something terrible comes out of it.” - loc: [5,949](kindle://book?action=open&asin=B06XKN9G27&location=5949) + + +--- +“For what it’s worth, I don’t disagree with you. These people are scum. They don’t deserve or understand what we’ve brought them. For me, I think they never will. But their children might. Their grandchildren or their great-grandchildren. The story of Medina will be that Governor Singh mismanaged the station, lost his ship to a band of malcontents, lost his perspective. And when he let his wounded pride exceed the mandates of the high consul’s directives, he was removed for the protection of the everyday citizens in his care. You see the difference? If you kill an insurgent, you’re the enemy of all their friends. All their family. And then there’s an expectation. Precedent. Enemies for generations. Forever. If you kill your own—even the highest among your own—to protect someone powerless, they remember that too. It sows gratitude. It sows trust. Generations from now your sacrifice will lead to the peace, prosperity, and fellow feeling among all humanity.” - loc: [7,853](kindle://book?action=open&asin=B06XKN9G27&location=7853) + + +--- diff --git a/README.md b/README.md index 132db7a3..b53a1c2a 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,10 @@ Pre-configured AI assistants ready to use: - `install-claudesidian-command` - Install shell command to launch vault from anywhere +**Kindle Integration:** + +- `kindle-sync` - Sync your Kindle highlights (interactive: choose 10, 25, 50, or all books) + Run with: `/[command-name]` in Claude Code ### Staying Updated with `/upgrade` @@ -253,6 +257,59 @@ npm run firecrawl:batch -- urls.txt "03_Resources/Research" 4. Copy the key (format: `fc-xxxxx...`) 5. Set it in your environment: `export FIRECRAWL_API_KEY="fc-your-key-here"` +## Kindle Highlights Sync + +Automatically sync your Kindle highlights to your vault with zero configuration. +On first run, you'll choose where to save highlights (defaults to +`03_Resources/Kindle Highlights`), then the integration handles everything else: + +- **Interactive sync**: Choose how many books to sync (10, 25, 50, or all) +- **Smart authentication**: Browser opens automatically when needed +- **Formatted notes**: Each book becomes a Markdown file with YAML frontmatter, + tags, and Kindle app deep links +- **Auto-retry**: If authentication expires mid-sync, automatically re-authenticates + +**Quick Start:** + +```bash +# In Claude Code, run: +/kindle-sync + +# You'll be prompted to choose: +# - Default (10): Quick sync of 10 most recent books +# - Custom number: Any amount (e.g., 25, 50) +# - All: Complete library (takes 10-30 minutes for large libraries) + +# First time only: +# 1. Choose output folder (or press Enter for default) +# 2. Browser opens for Amazon login +# 3. Syncs your selected books +``` + +**What you get:** + +Each book creates a note with: + +- YAML frontmatter (title, author, ASIN, tags, sync date) +- All highlights with locations and your notes +- Kindle app deep links to open specific passages in Kindle app +- Formatted with your custom template (`.scripts/kindle/templates/`) + +**Changing settings:** + +All settings are saved to `.kindle/config.json`. To change where highlights are +saved or other options: + +1. **Edit directly**: Open `.kindle/config.json` and change `outputFolder` or + other settings +2. **Reset setup**: Delete `.kindle/config.json` to trigger the first-run folder + prompt again +3. **One-time override**: Run with `--output` flag to save to a different folder + just once + +See `/kindle-sync` command documentation for full details on all configuration +options. + ## Helper Scripts Run these with `pnpm`: diff --git a/package.json b/package.json index b6f19a14..dd63ade6 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,12 @@ "attachments:create-organized": "mkdir -p 05_Attachments/Organized", "transcript:extract": ".scripts/transcript-extract.sh", "vault:stats": ".scripts/vault-stats.sh", - "check-updates": "REMOTE=$(curl -s https://raw.githubusercontent.com/heyitsnoah/claudesidian/main/package.json | grep version | head -1 | sed 's/.*: \"\\(.*\\)\".*/\\1/') && LOCAL=$(grep version package.json | head -1 | sed 's/.*: \"\\(.*\\)\".*/\\1/') && if [ \"$LOCAL\" != \"$REMOTE\" ]; then echo -e \"📦 Update available! Latest: $REMOTE (you have: $LOCAL)\\n\\n⬇\\n/upgrade\\n⬆\\n\\n## What will this do\\n\\n✅ Update to the latest version of Claudesidian\\n✅ Get new features and improvements\\n✅ Preserve your vault content and settings\\n\\n\"; fi" + "check-updates": "REMOTE=$(curl -s https://raw.githubusercontent.com/heyitsnoah/claudesidian/main/package.json | grep version | head -1 | sed 's/.*: \"\\(.*\\)\".*/\\1/') && LOCAL=$(grep version package.json | head -1 | sed 's/.*: \"\\(.*\\)\".*/\\1/') && if [ \"$LOCAL\" != \"$REMOTE\" ]; then echo -e \"📦 Update available! Latest: $REMOTE (you have: $LOCAL)\\n\\n⬇\\n/upgrade\\n⬆\\n\\n## What will this do\\n\\n✅ Update to the latest version of Claudesidian\\n✅ Get new features and improvements\\n✅ Preserve your vault content and settings\\n\\n\"; fi", + "kindle:auth": "node .scripts/kindle/auth.js", + "kindle:sync": "node .scripts/kindle/sync.js", + "kindle:sync-all": "node .scripts/kindle/sync.js --all", + "kindle:config": "node .scripts/kindle/config.js --interactive", + "playwright:install": "pnpm exec playwright install chromium" }, "keywords": [ "obsidian", @@ -36,7 +41,12 @@ "license": "MIT", "dependencies": { "@google/generative-ai": "^0.21.0", - "@modelcontextprotocol/sdk": "^1.0.0" + "@modelcontextprotocol/sdk": "^1.0.0", + "handlebars": "^4.7.8", + "html-entities": "^2.6.0", + "playwright": "^1.48.0", + "yargs": "^17.7.2", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/compat": "^1.3.0", From db9b35853e67c0d0cb629b47b607317b734d3a1e Mon Sep 17 00:00:00 2001 From: Ryan Anderson <ryan@alephic.com> Date: Sun, 19 Oct 2025 23:50:17 -0400 Subject: [PATCH 2/8] Pragmatic review and lint --- .claude/commands/kindle-sync.md | 68 +- .kindle/cache/all_books.json | 598 ------------------ .kindle/config.json | 13 - .prettierignore | 5 +- .scripts/kindle/auth.js | 164 ++--- .scripts/kindle/config.js | 125 ++-- .scripts/kindle/lib/config.js | 35 +- .scripts/kindle/lib/errors.js | 25 +- .scripts/kindle/lib/renderer.js | 122 ++-- .scripts/kindle/lib/scraper.js | 253 ++++---- .scripts/kindle/lib/utils.js | 40 +- .scripts/kindle/lib/validation.js | 49 +- .scripts/kindle/sync.js | 430 ++++++------- ...The Expanse Book 3) - James S. A. Corey.md | 29 - ...The Expanse Book 6) - James S. A. Corey.md | 34 - ...ht_ A King Oliver Novel - Walter Mosley.md | 17 - ...ntum Physics Is Different - Philip Ball.md | 181 ------ ...The Expanse Book 2) - James S. A. Corey.md | 33 - ...The Expanse Book 4) - James S. A. Corey.md | 34 - ...The Expanse Book 1) - James S. A. Corey.md | 37 -- ...dition) - James Martin and John L Allen.md | 42 -- ...The Expanse Book 5) - James S. A. Corey.md | 21 - ...The Expanse Book 7) - James S. A. Corey.md | 37 -- README.md | 6 +- 24 files changed, 640 insertions(+), 1758 deletions(-) delete mode 100644 .kindle/cache/all_books.json delete mode 100644 .kindle/config.json delete mode 100644 02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md delete mode 100644 02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md delete mode 100644 02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md delete mode 100644 02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md delete mode 100644 02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md delete mode 100644 02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md delete mode 100644 02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md delete mode 100644 02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md delete mode 100644 02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md delete mode 100644 02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md diff --git a/.claude/commands/kindle-sync.md b/.claude/commands/kindle-sync.md index 5909a174..6ac5b3de 100644 --- a/.claude/commands/kindle-sync.md +++ b/.claude/commands/kindle-sync.md @@ -7,12 +7,14 @@ Sync your Kindle highlights to the vault with zero-config setup. **Step 1: Check if this is first run:** Check if `.kindle/config.json` exists: + - If it EXISTS: Skip to Step 2b - If it DOES NOT exist: This is first run, go to Step 1b **Step 1b (First run only): Ask for output folder:** Ask the user: + ``` This is your first time syncing Kindle highlights! Where would you like to save them? @@ -63,31 +65,44 @@ Just reply with A, B, C, D, E, or a number. Based on their response and whether it's first run: **If first run (no config):** -- If they answer **A** or **10**: Run `node .scripts/kindle/sync.js --limit 10 --output "[their-folder-choice]"` -- If they answer **B** or **25**: Run `node .scripts/kindle/sync.js --limit 25 --output "[their-folder-choice]"` -- If they answer **C** or **50**: Run `node .scripts/kindle/sync.js --limit 50 --output "[their-folder-choice]"` -- If they answer **D** or **all**: Run `node .scripts/kindle/sync.js --all --output "[their-folder-choice]"` -- If they answer **E** or a number: Run `node .scripts/kindle/sync.js --limit [number] --output "[their-folder-choice]"` + +- If they answer **A** or **10**: Run + `node .scripts/kindle/sync.js --limit 10 --output "[their-folder-choice]"` +- If they answer **B** or **25**: Run + `node .scripts/kindle/sync.js --limit 25 --output "[their-folder-choice]"` +- If they answer **C** or **50**: Run + `node .scripts/kindle/sync.js --limit 50 --output "[their-folder-choice]"` +- If they answer **D** or **all**: Run + `node .scripts/kindle/sync.js --all --output "[their-folder-choice]"` +- If they answer **E** or a number: Run + `node .scripts/kindle/sync.js --limit [number] --output "[their-folder-choice]"` **If NOT first run (config exists):** + - If they answer **A** or **10**: Run `node .scripts/kindle/sync.js --limit 10` - If they answer **B** or **25**: Run `node .scripts/kindle/sync.js --limit 25` - If they answer **C** or **50**: Run `node .scripts/kindle/sync.js --limit 50` - If they answer **D** or **all**: Run `node .scripts/kindle/sync.js --all` -- If they answer **E** or a number: Run `node .scripts/kindle/sync.js --limit [number]` +- If they answer **E** or a number: Run + `node .scripts/kindle/sync.js --limit [number]` -Note: If user just presses Enter for default folder, use `03_Resources/Kindle Highlights` as the path. +Note: If user just presses Enter for default folder, use +`03_Resources/Kindle Highlights` as the path. ## What This Does -Fetches your Kindle highlights from Amazon and creates beautifully formatted Markdown notes in your vault. +Fetches your Kindle highlights from Amazon and creates beautifully formatted +Markdown notes in your vault. **✨ First Run Magic**: -- First time? You'll choose where to save highlights (defaults to `03_Resources/Kindle Highlights`) + +- First time? You'll choose where to save highlights (defaults to + `03_Resources/Kindle Highlights`) - Not authenticated? Browser opens automatically for Amazon login - Everything just works! **Subsequent syncs:** + - Uses your saved folder location - Uses cached authentication - Much faster! @@ -95,6 +110,7 @@ Fetches your Kindle highlights from Amazon and creates beautifully formatted Mar ## What Gets Created Each book creates a note with: + - **YAML frontmatter:** title, author, ASIN, tags, sync date - **Metadata:** Total highlights, Kindle notebook link - **All highlights:** Each with location and your notes @@ -103,15 +119,18 @@ Each book creates a note with: ## Changing Your Settings -Want to change where highlights are saved or other settings? You have three options: +Want to change where highlights are saved or other settings? You have three +options: ### Option 1: Edit Config File Directly (Recommended) + Open `.kindle/config.json` in your vault and edit: + ```json { - "outputFolder": "02_Areas/Reading", // Change this to your preferred folder + "outputFolder": "02_Areas/Reading", // Change this to your preferred folder "templatePath": ".scripts/kindle/templates/kindle-note.md.hbs", - "overwrite": false, // Set to true to replace existing files + "overwrite": false, // Set to true to replace existing files "addTags": ["kindle", "highlights", "books"], "createIndex": true, "indexPath": "03_Resources/Kindle Highlights Index.md", @@ -120,14 +139,19 @@ Open `.kindle/config.json` in your vault and edit: ``` ### Option 2: Reset to First-Run Prompt + Delete the config file to trigger the folder selection prompt again: + ```bash rm .kindle/config.json # Next /kindle-sync will ask where to save highlights ``` ### Option 3: One-Time Override -Use the `--output` flag to save to a different folder just once (without changing config): + +Use the `--output` flag to save to a different folder just once (without +changing config): + ```bash node .scripts/kindle/sync.js --limit 10 --output "01_Projects/Current Reading" ``` @@ -135,6 +159,7 @@ node .scripts/kindle/sync.js --limit 10 --output "01_Projects/Current Reading" ## Tips ### Expected Timing + - **10 books:** 1-3 minutes - **50 books:** 5-10 minutes - **100+ books (all):** 15-30 minutes @@ -142,34 +167,45 @@ node .scripts/kindle/sync.js --limit 10 --output "01_Projects/Current Reading" Rate limiting delays are required to avoid Amazon detection. ### Organization + - Notes are automatically named: `<Author Last Name> - <Title>.md` - All include `#kindle`, `#highlights`, `#books` tags - Easy to find with Obsidian search or graph view ### Advanced Configuration + All settings in `.kindle/config.json`: -- `outputFolder`: Where to save notes (default: `03_Resources/Kindle Highlights`) + +- `outputFolder`: Where to save notes (default: + `03_Resources/Kindle Highlights`) - `templatePath`: Custom Handlebars template location - `overwrite`: Whether to replace existing files (default: `false`) -- `addTags`: Tags to add to all notes (default: `["kindle", "highlights", "books"]`) +- `addTags`: Tags to add to all notes (default: + `["kindle", "highlights", "books"]`) - `createIndex`: Whether to create an index file (not yet implemented) - `indexPath`: Where to save the index file ## Troubleshooting ### "Not authenticated" error -Shouldn't happen anymore! The script auto-detects and runs authentication. But if you see this: + +Shouldn't happen anymore! The script auto-detects and runs authentication. But +if you see this: + - Just run `/kindle-sync` again - It will automatically handle authentication ### No books found + - Check if your Kindle library is visible at read.amazon.com/notebook - Verify you have highlights (not just books in library) ### Duplicate files + - Set `overwrite: true` in config to replace existing files - Or manually delete old files before syncing ### Scraping seems slow + - Normal! Amazon requires delays between requests - Use smaller number of books for faster syncs diff --git a/.kindle/cache/all_books.json b/.kindle/cache/all_books.json deleted file mode 100644 index 7845c7da..00000000 --- a/.kindle/cache/all_books.json +++ /dev/null @@ -1,598 +0,0 @@ -[ - { - "bookId": "B06XKN9G27", - "title": "Persepolis Rising (The Expanse Book 7)", - "author": "James S. A. Corey", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:25.071Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B06XKN9G27", - "highlights": [ - { - "text": "there had been a time when riding the lift instead of climbing the ladder would have felt like admitting defeat, even if the ship was burning ten times that hard. But for the last couple years Bobbie’s joints had been giving her trouble, and proving to herself that she could make the climb had stopped mattering as much. It seemed to her that the real sign you were getting old was when you stopped needing to prove you weren’t getting old.", - "location": "426", - "color": "yellow", - "id": "6c348b9dac47ca50" - }, - { - "text": "“All right, then,” Bobbie said. “The way I see it, the next step is find someone who can get messages back to the union. Or Earth-Mars. See if there’s anyone out there with a plan, or if we’re going to have to make one up on our own.” “We can do that,” Amos said. “Shouldn’t be hard.” “You sure?” Alex said. “This is Medina Station under occupation by a bunch of splinter Martian military expats. It’s not Baltimore.” Amos’ smile was as placid as always. “Everywhere’s Baltimore.”", - "location": "3,003", - "color": "yellow", - "id": "d8f91bab073342a2" - }, - { - "text": "If they kill us all, Drummer thought, this will be why. Not their technology, not their strategy, not the invisible cycle of history. It’ll be our inability to do anything without five committee meetings to talk about it.", - "location": "3,771", - "color": "yellow", - "id": "d618536dde571e38" - }, - { - "text": "It was all becoming normal. He could see it in the way the clerk served up the terrible, terrible coffee. He could hear it in the conversations at the nearby tables. It showed on the screens and in the gaits of the people walking by. Panic and alarm were exhausting. He was exhausted by them, and Medina was exhausted too. It was already shifting into its new routine. Checkpoints, yes. Armed security, yes. All the theater of dominance and control and nothing to undercut that narrative.", - "location": "4,044", - "color": "yellow", - "id": "b7ba92c0027ed0ec" - }, - { - "text": "controlling don’t have a voice in how you control them. As long as everyone’s on the same page, things may be great, but when there’s a question, you win. Right?” “There has to be a way to come to a final decision.” “No, there doesn’t. Every time someone starts talking about final anythings in politics, that means the atrocities are warming up. Humanity has done amazing things by just muddling through, arguing and complaining and fighting and negotiating. It’s messy and undignified, but it’s when we’re at our best, because everyone gets to have a voice in it. Even if everyone else is trying to shout it down. Whenever there’s just one voice that matters, something terrible comes out of it.”", - "location": "5,949", - "color": "yellow", - "id": "692e86cc25bcb0a4" - }, - { - "text": "“For what it’s worth, I don’t disagree with you. These people are scum. They don’t deserve or understand what we’ve brought them. For me, I think they never will. But their children might. Their grandchildren or their great-grandchildren. The story of Medina will be that Governor Singh mismanaged the station, lost his ship to a band of malcontents, lost his perspective. And when he let his wounded pride exceed the mandates of the high consul’s directives, he was removed for the protection of the everyday citizens in his care. You see the difference? If you kill an insurgent, you’re the enemy of all their friends. All their family. And then there’s an expectation. Precedent. Enemies for generations. Forever. If you kill your own—even the highest among your own—to protect someone powerless, they remember that too. It sows gratitude. It sows trust. Generations from now your sacrifice will lead to the peace, prosperity, and fellow feeling among all humanity.”", - "location": "7,853", - "color": "yellow", - "id": "c3ef5243596a4c91" - } - ], - "meta": { - "totalHighlights": 6 - } - }, - { - "bookId": "B07H9HNDYL", - "title": "Beyond Weird: Why Everything You Thought You Knew about Quantum Physics Is Different", - "author": "Philip Ball", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:27.099Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B07H9HNDYL", - "highlights": [ - { - "text": "Rather than insisting on its difficulty, we might better regard it as a beguiling, maddening, even amusing gauntlet thrown down to challenge the imagination. For that is indeed what is challenged. I suspect we are, in the wider cultural context, finally beginning to appreciate this. Artists, writers, poets and playwrights have started to imbibe and deploy ideas from quantum physics: see, for instance, plays such as Tom Stoppard’s Hapgood and Michael Frayn’s Copenhagen, and novels such as Jeanette Winterson’s Gut Symmetries and Audrey Niffenegger’s The Time Traveler’s Wife. We can argue about how accurately or aptly these writers appropriate the scientific ideas, but it is right that there should be imaginative responses to quantum mechanics, because it is quite possible that only an imagination sufficiently broad and liberated will come close to articulating what it is about.", - "note": "Check out these works, especially Winterson", - "location": "102", - "color": "yellow", - "id": "0fba278d664afb20" - }, - { - "text": "Feynman seemed to feel that it was impossible and even pointless to attempt anything comparable for quantum mechanics: We can’t pretend to understand it since it affronts all our commonsense notions. The best we can do is to describe what happens in mathematics, in equations, and that’s very difficult. What is even harder is trying to decide what the equations mean. That’s the hardest thing of all.", - "location": "143", - "color": "yellow", - "id": "f0f8742d2806d44f" - }, - { - "text": "only a few decades ago, professing a serious interest in the topic could be tantamount to career suicide for a young physicist. Only a handful of scientists and philosophers, idiosyncratically if not plain crankily, insisted on caring about the answer. Many researchers would shrug or roll their eyes when the ‘meaning’ of quantum mechanics came up; some still do. ‘Ah, nobody understands it anyway!’ How different this is from the attitude of Albert Einstein, Niels Bohr and their contemporaries, for whom grappling with the apparent oddness of the theory became almost an obsession. For them, the meaning mattered intensely. In 1998 the American physicist John Wheeler, a pioneer of modern quantum theory, lamented the loss of the ‘desperate puzzlement’ that was in the air in the 1930s. ‘I want to recapture that feeling for all, even if it is my last act on Earth’, Wheeler said.", - "note": "\"desperate puzzlement\"", - "location": "153", - "color": "yellow", - "id": "b5080d08294caf98" - }, - { - "text": "You can see that I’m already struggling to find a language that works for talking about these things. That’s OK, and you’ll have to get used to it. That’s how it should be. When words come too easily, it’s because we haven’t delved deeply enough (you’ll see that scientists can be guilty of that too).", - "note": "I've been thinkibg lately about if (really how) the human need for language inhibits our ability to understand things. If your mother tongue does not have the words for something then how much harder is it to understand? What is the instance of bilingualism among Nobel-winning scientists?", - "location": "191", - "color": "yellow", - "id": "1dfa0d699cf08e8a" - }, - { - "text": "and there are plenty of accessible books on the subject.*2", - "location": "208", - "color": "yellow", - "id": "a0f0aacec2c2aaac" - }, - { - "text": "Best of all for its narrative value is the largely good-natured but trenchant dispute that rumbled on for decades between Einstein and Bohr about what it all meant – about the nature of reality. This is indeed a superb story, and if you haven’t heard it before then you should.*1", - "location": "256", - "color": "yellow", - "id": "0139c998fbad4493" - }, - { - "text": "The best illustration I know of that quantization is rather incidental to quantum theory is found in the book Quantum Mechanics: The Theoretical Minimum, based on a series of lectures that Leonard Susskind, a professor of theoretical physics at Stanford University, gave to undergraduates, which were written up with the help of the writer Art Friedman. The book is described as being ‘for anyone who ever regretted not taking physics at university, who knows a little but would like to know more’.", - "note": "I'm less than 10% into this book and already have 3 more to read!", - "location": "337", - "color": "yellow", - "id": "a2976a8f8215de07" - }, - { - "text": "Quantum objects are what they are, and we have no reason to suppose that ‘what they are’ changes in any meaningful way depending on how we try to look at them. Rather, all we can say is that what we measure sometimes looks like what we would expect to see if we were measuring discrete little ball-like entities, while in other experiments it looks like the behaviour expected of waves of the same kind as those of sound travelling in air, or that wrinkle and swell on the sea surface. So the phrase ‘wave–particle duality’ doesn’t really refer to quantum objects at all, but to the interpretation of experiments – which is to say, to our human-scale view of things.", - "location": "392", - "color": "yellow", - "id": "48a1418b0bbcba0a" - }, - { - "text": "What makes the quantum situation of radioactive decay any different from this classical (and, forgive me, rather unfortunate) analogy of childbirth? We have every reason to suppose that, if we were able to monitor the biology of each pregnant mother and baby sufficiently closely, we could understand exactly why the respective birth process started when it did – perhaps some hormonal threshold was reached, say. But for radioactive decay, there is nothing you can monitor to explain why a particular atom decayed when it did. There is nothing we can call a reason.", - "note": "At this point in knowledge, sure. But there has to be an explanation, a trigger. We can't just live in a world where things happen with no cause. If that was the case, then what is the source of the wave function?", - "location": "606", - "color": "yellow", - "id": "53615fc2f146b9ec" - }, - { - "text": "So, said Bohr, we had better just stop there. As he put it: There is no quantum world. There is only an abstract quantum physical description. It is wrong to think that the task of physics is to find out how nature is. Physics concerns what we can say about nature.", - "location": "777", - "color": "yellow", - "id": "d928e35c618eff5f" - }, - { - "text": "At face value that sounds crazy. Why make a mathematical theory like quantum mechanics if you don’t think it can tell you about the system it is supposed to describe? But Bohr argued that quantum theory tells you something more meaningful – indeed, the only thing that can be meaningful. It tells you what you will find when you try to investigate that system. It tells you about measurements.", - "location": "783", - "color": "yellow", - "id": "4e9549eb8a6e86e2" - }, - { - "text": "the early days of this new physics, the problem was often debated in terms of the ‘role of the observer’. That the observer had a role at all was deeply troubling, because it seemed to challenge the very concept of science. If what we see depends on what questions we ask, whither then the idea of an objective world, governed by rules that pertain independently of our attempts to figure them out? As Heisenberg put it, science had ceased to be a way of peeking unnoticed at the world, and instead had become ‘an actor in [the] interplay between man and nature’.", - "note": "Like how what we get from an LLM depends on not just what questions we ask it but how.", - "location": "813", - "color": "yellow", - "id": "f770db7238f7656c" - }, - { - "text": "But measurement does something else. It ‘collapses’ (Heisenberg’s original word was ‘reduces’) those possibilities, expressed in the wavefunction, to just one. Suppose that, before the measurement, the probabilities of finding some property of a quantum object with the values corresponding to states A, B and C are 10%, 70% and 20% respectively. When we make a single measurement on the object, we might find that we get the result C. What happened to A and B? We are forced to assert that the probabilities have now changed: that for C it is 100%, and for A and B it is zero. What’s more, we can’t get A and B back: if we repeat the measurement, we’ll keep getting C.*2", - "note": "We will keep getting C *unless and until* we reset the quantum object in its original state. How in tge world does that happen??", - "location": "1,043", - "color": "yellow", - "id": "ab3eebcc4be4b708" - }, - { - "text": "To Bohr, wavefunction collapse was virtually emblematic of the distinction between the unitary quantum world and the everyday reality in which we make observations and measurements. Measurements must be classical by definition: they require some big apparatus with which humans can interact. From our perspective the world is made up of phenomena – things happen – and a phenomenon only exists when it has been measured. Wavefunction collapse is simply a name we give to the process by which we turn quantum states into observed phenomena. Wavefunction collapse is then a generator of knowledge: it is not so much a process that gives us the answers, but is the process by which answers are created. The outcome of that process can’t, in general, be predicted with certainty, but quantum mechanics gives us a method for calculating the probabilities of particular outcomes. That’s all we can ask for.", - "location": "1,062", - "color": "yellow", - "id": "fd098f3250b8f339" - }, - { - "text": "Bohr could be stubborn and dogmatic as well as rather cryptic. But he deserves praise for defining the limits of what can be said with confidence. Some suspect that his intuition surpassed anything he could justify mathematically, or indeed semantically. As von Weizsäcker said, ‘Bohr was essentially right but he did not know why.’", - "location": "1,110", - "color": "yellow", - "id": "24522609b8a9dd3a" - }, - { - "text": "But nevertheless the notion of a universal wavefunction is popular with cosmologists, for the perfectly valid reason that in the earliest moments of the Big Bang the entire universe was smaller than an atom and surely needs to be considered, in that moment, a quantum-mechanical entity.", - "note": "Should we just accept that as fact?", - "location": "1,222", - "color": "yellow", - "id": "f2c8242cfa74ff76" - }, - { - "text": "Quantum mechanics generally assumes that quantum states exist in some meaningful sense, and that the math tells us what we can know about those states. But in QBism there are no objective states. Rather, according to Chris Fuchs, ‘quantum states represent observers’ personal information, expectations and degrees of belief’. This view, he says, ‘allows one to see all quantum measurement events as little “moments of creation”, rather than as revealing anything pre-existent’.", - "note": "What if every time we collapse a wave function we are setting off a big bang in another universe?", - "location": "1,303", - "color": "yellow", - "id": "4509cc16ba78b606" - }, - { - "text": "The main thing you need to know about entanglement is this: it tells us that a quantum object may have properties that are not entirely located on that object.", - "location": "1,704", - "color": "yellow", - "id": "365d454d2123cc0d" - }, - { - "text": "But this locality is just what quantum entanglement undermines – which is why ‘spooky action at a distance’ is precisely the wrong way to look at it. We can’t regard particle A and particle B in the EPR experiment as separate entities, even though they are separated in space. As far as quantum mechanics is concerned, entanglement makes them both parts of a single object. Or to put it another way, the spin of particle A is not located solely on A in the way that the redness of a cricket ball is located on the cricket ball. In quantum mechanics, properties can be non-local.", - "location": "1,931", - "color": "yellow", - "id": "5d56dff142cbc9f5" - }, - { - "text": "What in fact we’re dealing with here is another kind of quantum superposition. We’ve seen that superposition refers to a situation in which a measurement on a quantum object could produce two or more possible outcomes, but we don’t know which it will be, only their relative probabilities. Entanglement is that same idea applied to two or more particles: a superposition of the state in which particle A has spin up and B spin down, say, and the state with the opposite configuration. Although the particles are separated, they must be described by a single wavefunction. We can’t untangle that wavefunction into some combination of two single-particle wavefunctions.", - "note": "If we could change the spin of an entangled particle, would that change the spin of the second particle or would in disentangle the particles? Can you even disentangle particles? Can you have an entangled triad of particles?", - "location": "1,936", - "color": "yellow", - "id": "25bf6b0c01ba4b97" - }, - { - "text": "Kochen and Specker pointed out that the outcomes of quantum measurements may depend on their context. This means something subtly different from the fact that different types of experiment on what seems like the same system (a double-slit experiment with or without a ‘path detector’, say) give different results. It means that if you look at a quantum object through different windows, you see different things. If you want to count the number of black and white balls in a jar, it doesn’t matter if you count the black ones or the white ones first, or if you tot them up in rows of five, or if you separate the two colours into piles and weigh them. You’ll get the same answer. But in quantum mechanics, even when you ask the same question (‘How many black and white balls are there?’), the answer you get may depend on how the measurement is done.", - "note": "Here is this similarity to interfacing with LLMs again", - "location": "2,009", - "color": "yellow", - "id": "c3c498350e232dce" - }, - { - "text": "The Kochen–Specker theorem stipulates what follows from this dependence on context. In effect it is another corollary of the way that, in quantum mechanics, what we choose not to measure may leave an imprint on what we do measure.", - "location": "2,018", - "color": "yellow", - "id": "d417257cba5d6f9b" - }, - { - "text": "An Assyrian seer, unwilling to let his young daughter be wed to suitors who he considered unworthy, set them a task. He presented them with three closed boxes in a row, each of which might or might not have a gem inside it. Any prediction will identify at least two boxes that are guessed to be both empty, or two that are both filled. (Think about it for a moment and you’ll see that this must be so.) The suitors were asked to open those two, and if they were right then they would have the daughter’s hand. But they were never right! One of the opened boxes was always empty while the other contained a gem. How could that be? Surely chance alone should guarentee that someone would guess right some time? Finally the daughter, who was getting impatient to be wed, intervened by opening up the boxes for the rather dishy son of a prophet. But she didn’t open the two he’d predicted to be both full or both empty. Instead she opened one he’d guessed was filled, and one he’d guessed was empty. And both guesses proved correct. After some weak objection, the seer accepted that the suitor had made two correct guesses, and the daughter was married. The seer had foiled previous suitors because his boxes were quantum boxes, and he had entangled them to create correlations such that, if one opened box was filled, the other would be empty and vice versa. That made it impossible to satisfy the seer’s challenge and show you’d guessed right. But the daughter was able to reveal a correct guess by interrogating the same system with a different set of measurements. And that is what quantum contextuality is like.*", - "location": "2,021", - "color": "yellow", - "id": "30361820e37953d2" - }, - { - "text": "You can’t say, to make a macroscopic analogy, ‘the ball is red’, but only ‘the ball is red when looked at through the round window’. It really is red under these conditions (to the extent that we can ever say how something really is). But equally, it really is green through the square window. Yes, but what colour is it really? According to Kochen and Specker, you can’t improve on the answer I’ve just given.*", - "note": "Is this possible to create as an art piece?", - "location": "2,043", - "color": "yellow", - "id": "0ce94133b9500387" - }, - { - "text": "It is this waviness that gives rise to distinctly quantum phenomena like interference, superposition and entanglement. These behaviours become possible when there is a well-defined relationship between the quantum ‘waves’: in effect, when they are in step. This co-ordination is called coherence.", - "note": "coherence is a well-defined relationship between wave functions of 2 quantum objects", - "location": "2,129", - "color": "yellow", - "id": "04178c1ddb30d780" - }, - { - "text": "if the quantum wavefunctions of two states are not coherent, they cannot interfere, nor can they maintain a superposition. A loss of coherence (decoherence) therefore destroys these fundamentally quantum properties, and the states behave more like distinct classical systems.", - "location": "2,134", - "color": "yellow", - "id": "f9346992f3aa97a8" - }, - { - "text": "Quantum coherence is essentially what permits ‘quantumness’.", - "location": "2,140", - "color": "yellow", - "id": "7b168efa2be4e566" - }, - { - "text": "There is no reason (that we yet know of) why in principle objects cannot remain in coherent quantum states no matter how big they are – provided that no measurement is made on them. But it seems that measurement somehow does destroy quantum coherence, forcing us to speak of the wavefunction as having ‘collapsed’. If we can understand how measurement unravels coherence then we would be able to bring measurement itself within the scope of quantum theory, rather than making it a boundary where the theory stops.", - "note": "Measurement of quantum activity == Tower of Babel. Arrogance of Man, smited.", - "location": "2,141", - "color": "yellow", - "id": "2588eaa070e913ed" - }, - { - "text": "The truth is that they don’t. Quantum superpositions of states aren’t fragile. On the contrary, they are highly contagious and apt to spread out rapidly. And that is what seems to destroy them. If a quantum system in a superposed state interacts with another particle, the two become linked into a composite superposition. That, we saw earlier, is exactly what entanglement is: a superposed state of two particles, whose interaction has turned them into a single quantum entity.", - "location": "2,159", - "color": "yellow", - "id": "2891cbf02a805c22" - }, - { - "text": "In theory there is no end to this process. That entangled air molecule hits another, and the second molecule gets captured in an entangled state too. As time passes, the initial quantum system becomes more and more entangled with its environment. In effect, we then no longer have a well-defined quantum system embedded in an environment. Rather, system and environment have merged into a single superposition.", - "location": "2,166", - "color": "yellow", - "id": "3d488098bc360008" - }, - { - "text": "Decoherence is what destroys the possibility of observing macroscopic superpositions – including Schrödinger’s live/dead cat. And this has nothing to do with observation in the normal sense: we don’t need a conscious mind to ‘look’ in order to ‘collapse the wavefunction’. All we need is for the environment to disperse the quantum coherence. This happens with extraordinary efficiency – it’s probably the most efficient process known to science. And it is very clear why size matters here: there is simply more interaction with the environment, and therefore faster decoherence, for larger objects.", - "location": "2,218", - "color": "yellow", - "id": "9ebfb9db2ec8d4e9" - }, - { - "text": "Here, then, is a concrete way to look at that most puzzling of quantum mysteries, which is that what we measure depends on how we look – on the context of the measurement.", - "location": "2,412", - "color": "yellow", - "id": "f3ab22ef8599ca52" - }, - { - "text": "Charles Bennett and Gilles Brassard pointed out in the early 1980s that quantum correlations between entangled states can be exploited to send information encoded in qubits that is immune to eavesdropping. The signal could never be intercepted and read without the tampering being detected. This was the start of the technology known as quantum cryptography. The idea goes like this. Alice encodes her message in two entangled qubits – 1s and 0s represented by, say, the polarization states of pairs of photons. One photon from each entangled pair is then sent to Bob, who measures their states and so decodes the message.", - "location": "2,817", - "color": "yellow", - "id": "6fbf11cc7f78e906" - }, - { - "text": "I’ll say it upfront: no one fully understands how quantum computers work.", - "note": "Thank God!", - "location": "2,872", - "color": "yellow", - "id": "f77f5aca64862a45" - }, - { - "text": "But Bohr and colleagues didn’t bring wavefunction collapse into the picture just to make things difficult. They did it because that’s what seems to happen. When we make a measurement, we really do get just one result out of the many that quantum mechanics offers. Wavefunction collapse seemed to be demanded in order to connect the theory to reality. So what Everett was saying was that this isn’t, after all, what reality is. It’s not quantum mechanics that is at fault, but our concept of reality. We only think that there’s a single outcome of a measurement. But in fact all of them occur. We only see one of those realities, but the others have a separate physical existence too.", - "note": "If MWI is true then how do we deal with the law of conservation of matter? is anything other than possibilities \"lost\" when the wave function collapses?", - "location": "2,989", - "color": "yellow", - "id": "f435104e41a408b4" - }, - { - "text": "Luis Jorge Borges’ story ‘The Garden of Forking Paths’,", - "location": "3,035", - "color": "yellow", - "id": "63beceab91a143cf" - }, - { - "text": "The issue is whether, like Bohr, you believe that quantum mechanics provides a prescription for evaluating the possible outcomes we might observe when we look at the quantum world, or whether you regard the Schrödinger equation as an inviolable and universal law that describes – in some sense is – reality.", - "location": "3,154", - "color": "yellow", - "id": "dbd07712fc8b800a" - }, - { - "text": "Can it be right that the laws of Newtonian mechanics for our classical world, expressed in concise, prosaic words and sentences, must give way to something with the forbidding, abstract mathematical complexity of the quantum axioms? Or is this just because we don’t quite know what we’re talking about? When someone explains something in a complicated way, it’s often a sign that they don’t really understand it.", - "location": "3,334", - "color": "yellow", - "id": "4ad95cb872511658" - } - ], - "meta": { - "totalHighlights": 38 - } - }, - { - "bookId": "B018S2773Y", - "title": "Babylon's Ashes (The Expanse Book 6)", - "author": "James S. A. Corey", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:29.347Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B018S2773Y", - "highlights": [ - { - "text": "The John Galt and the Mark Watney, out of Mars.", - "location": "2,231", - "color": "yellow", - "id": "80d52cf15c16a655" - }, - { - "text": "Holden went on as if he hadn’t heard Alex at all. “I thought if you told people facts, they’d draw their conclusions, and because the facts were true, the conclusions mostly would be too. But we don’t run on facts. We run on stories about things. About people. Naomi told me that when the rocks fell, the people on Inaros’ ship cheered. They were happy about it.”", - "location": "2,491", - "color": "yellow", - "id": "f5962997eeef2e87" - }, - { - "text": "If someone killed Pallas, he’d be worried about what the drop in refining capacity would do before he thought about how many preschools there are on the station. Or if the station manager’s son liked writing poetry. Or that blowing the station meant that Annie down in Pallas central accounting wasn’t going to get to throw her big birthday party after all.”", - "location": "2,498", - "color": "yellow", - "id": "6aa557469ce19232" - }, - { - "text": "listening to the too-loud laughter of the soldiers, one moment came back to him vividly. His professor—an overweight, balding man with an alcoholic’s complexion and an air of intelligence so profound it seemed to bend space around him—lifting a hand and speaking a phrase: the terror of the normal.", - "location": "3,545", - "color": "yellow", - "id": "eb140b4855b3467c" - }, - { - "text": "Naomi smiled. Laughed. It was strange how it made the darkness better. Not less dark. Just better, even though it still was what it was.", - "note": "Trudie girl <3", - "location": "4,173", - "color": "yellow", - "id": "4739b1bce721bb57" - } - ], - "meta": { - "totalHighlights": 5 - } - }, - { - "bookId": "B00O7X626W", - "title": "Nemesis Games (The Expanse Book 5)", - "author": "James S. A. Corey", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:31.340Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B00O7X626W", - "highlights": [ - { - "text": "At least he knows there’s a problem. Realizing you’ve got shit on your fingers is the first step toward washing your hands.”", - "location": "2,763", - "color": "yellow", - "id": "ea53d4e84293a6d2" - }, - { - "text": "Thing is, we’re humans. We’re tribal. More settled things are, the bigger your tribe is. All the people in your gang, or all the people in your country. All the ones on your planet. Then the churn comes, and the tribe gets small again.”", - "location": "4,165", - "color": "yellow", - "id": "6ae6a173b7989140" - } - ], - "meta": { - "totalHighlights": 2 - } - }, - { - "bookId": "B00FPQA4F0", - "title": "Cibola Burn (The Expanse Book 4)", - "author": "James S. A. Corey", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:33.338Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B00FPQA4F0", - "highlights": [ - { - "text": "“I’m wondering what I did to deserve all this,” she said. “I’m about to see the first genuinely alien biosphere. I’m about to learn things about evolution that were literally impossible to know until now. I must have been a very, very good person in a past life.”", - "color": "yellow", - "id": "27d0814076dea78a" - }, - { - "text": "“A vast new frontier has opened up for us. We have the chance to create a new society, with untold riches beyond every gate. But this world has treasure, so instead of figuring out the right way to divide up the damned galaxy, we’ll fight over the first crumbs we find.”", - "color": "yellow", - "id": "4eb5389694b83cc3" - }, - { - "text": "“What the hell are you doing?” she asked. “What am I doing? You think that’s the question?” “You know that her husband—” “I don’t know a damned thing, Elvi. Neither do you. I’m rich in interpretation and poor in datasets, just the same as you.” “You think… you think it’s not…”", - "color": "yellow", - "id": "b1d2d13b585a4280" - }, - { - "text": "“No,” Alex said, a deep frown cutting into his forehead. “A person can fail the people they love just by being who they are. I’m who I am, and it wasn’t what my wife wanted me to be, and somethin’ had to break. You decided to do what you did down on that planet, and it put you up here with me instead of with your family.” Alex leaned forward, grabbing Basia’s hands in his own. “It’s still on you. I will never live down not being the person my wife needed after she spent twenty years waitin’ for me. I can never make that right. Don’t go feelin’ sorry for yourself. You fucked up. You failed the people you love. They’re payin’ the price for it right now and you demean them every second you don’t own that shit.”", - "color": "yellow", - "id": "d4c9c54daebf9692" - }, - { - "text": "Earth alone had managed five kingdoms of life. Six, if you agreed with the Fityani hypothesis.", - "note": "look that up", - "color": "yellow", - "id": "b11c24866c997d4f" - } - ], - "meta": { - "totalHighlights": 5 - } - }, - { - "bookId": "B00A2DZMYE", - "title": "Abaddon's Gate (The Expanse Book 3)", - "author": "James S. A. Corey", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:35.395Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B00A2DZMYE", - "highlights": [ - { - "text": "“What have we got?” Ashford said. “Short form.” “It’s fucking weird, sir,” Chan said, and everyone chuckled. “Our best analysis is that the Ring is an artificially sustained Einstein-Rosen bridge. You go through the Ring, you don’t come out the other side here.” “So it’s a gate,” Ashford said. “Yes, sir. It appears that the protomolecule or Phoebe bug or whatever you want to call it was launched at the solar system several billion years ago, aiming for Earth with the intention of hijacking primitive life to build a gateway. We’re positing that whoever created the protomolecule did it as a first step toward making travel to the solar system more convenient and practical later.”", - "color": "yellow", - "id": "a5dc98047e6c1861" - }, - { - "text": "Protogen had named the protomolecule and decided it was a tool that could redefine what it meant to be human. Jules-Pierre Mao had treated it like a weapon. It killed humans, therefore it was a weapon. But radiation killed humans, and a medical X-ray machine wasn’t intended as a weapon. Holden was starting to feel like they were all monkeys playing with a microwave. Push a button, a light comes on inside, so it’s a light. Push a different button and stick your hand inside, it burns you, so it’s a weapon. Learn to open and close the door, it’s a place to hide things. Never grasping what it actually did, and maybe not even having the framework necessary to figure it out. No monkey ever reheated a frozen burrito.", - "color": "yellow", - "id": "e806c0e6189b8868" - }, - { - "text": "The truth was, distance was always measured in time.", - "color": "yellow", - "id": "9beab4f86789032e" - }, - { - "text": "When the orbits were right, the journey from Luna to Mars could take as little as twelve days. The trek from Saturn to Ceres, a few months. And because they were out there with their primate brains, evolved on the plains of prehistoric Africa, everyone had a sense of how far it was. Saturn to Ceres was a few months. Luna to Mars was a few days. Distance was time, and so they didn’t get overwhelmed by it.", - "color": "yellow", - "id": "20a3a47972d17614" - } - ], - "meta": { - "totalHighlights": 4 - } - }, - { - "bookId": "B01LA4CEGM", - "title": "My Life with the Saints (10th Anniversary Edition)", - "author": "James Martin and John L Allen", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:37.330Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B01LA4CEGM", - "highlights": [ - { - "text": "As C. S. Lewis writes in Mere Christianity: “How monotonously alike all the great tyrants and conquerors have been: how gloriously different are the saints.”", - "color": "yellow", - "id": "83979c52df9fdb78" - }, - { - "text": "The best way to come to know St. Thomas More, I believe, is to watch A Man for All Seasons.", - "color": "yellow", - "id": "b61bbcbd42d06220" - }, - { - "text": "Enter the idea of studying business. At the time, I had a vague idea that a business major would enable me to land a job in the “business end” of any number of fields, such as the “business end” of English (publishing) or the “business end” of art (managing a museum or gallery). At the very least, a business degree would likely lead to a high-paying job after graduation, and this, I reasoned, would make me happy. On this point, everyone—my family, my friends, my high school guidance counselor—agreed. The problem was that whenever I considered “earning a living,” I thought mostly about the “earning” and nothing about the “living.”", - "color": "yellow", - "id": "16ee566e3c99233f" - }, - { - "text": "Wharton’s reputation in the corporate world made finding a job ridiculously easy. At the beginning of senior year, hundreds of corporate recruiters flocked to the school to conduct interviews: in a few months I had received a number of excellent offers. After narrowing them down to three or four, I settled on a corporate training program at General Electric in New York City. Once again, almost no reflection was involved in my decision. To use some accounting terminology, I simply went along with “generally accepted principles.” For example, what should you do at the end of business school? Interview for jobs. Which job offer should you accept? The one with the highest salary. The more important questions were the ones that no one asked me, or rather, that I failed to ask myself: What do you desire in life? And what does God desire for you?", - "color": "yellow", - "id": "c8a8210010acf5db" - }, - { - "text": "I looked down at the bright green grass and thought about Merton’s strange, complicated, and contradictory life. And about my favorite lines from Walt Whitman, which come near the end of “Song of Myself”: I bequeath myself to the dirt to grow from the grass I love. If you want me again look for me under your boot-soles.", - "color": "yellow", - "id": "3f6aa463c480bfd0" - }, - { - "text": "the vanities of the world,” particularly concerning his physical appearance. He seems also to have been a ladies’ man. At least that’s how he fancied himself. He was definitely a rake. It was rumored that he fathered an illegitimate child. (There is some evidence to support this, but it is inconclusive.) And he may be the only canonized saint with a notarized police record, for nighttime brawling with intent to inflict serious harm.", - "note": "Ignatious of Loyola", - "color": "yellow", - "id": "6fe13565f654950b" - }, - { - "text": "In Paris he also completed what later become known as The Spiritual Exercises, a handbook of practices on prayer, on the human condition, on God’s love, and on the life of Jesus, all designed to help people draw closer to God. Iñigo also led his new roommate, Francisco Javier, through these exercises. His friend would later become better known, of course, as St. Francis Xavier, one of the Church’s great missionaries.", - "color": "yellow", - "id": "3a13fb625c7fbcc0" - } - ], - "meta": { - "totalHighlights": 7 - } - }, - { - "bookId": "B005SCRR1A", - "title": "Caliban's War (The Expanse Book 2)", - "author": "James S. A. Corey", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:39.213Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B005SCRR1A", - "highlights": [ - { - "text": "“You live in your own world, dear one.” “I do,” Arjun agreed, running his thumb across the hand terminal’s screen. He looked up. “You don’t mind, do you?” “I love you for it. Stay here. Read about post-lyricism.” “What are you going to do?” “The same thing as always. Try to keep civilization from blowing up while the children are in it.”", - "color": "yellow", - "id": "618595cc362237fd" - }, - { - "text": "“This is what I’m talking about. The intensity of your feelings isn’t evidence.", - "color": "yellow", - "id": "3040bec9d4eca2b0" - }, - { - "text": "Prax looked at the mass of data being sent to Alex and said, “Why fly at all? Couldn’t the ship just use this data to do the docking itself?” “Why fly?” Alex repeated with a laugh. “ ’Cuz it’s fun, Doc. Because it’s fun.”", - "color": "yellow", - "id": "865de43fa4a21920" - }, - { - "text": "‘If life transcends death, then I will seek for you there. If not, then there too,’ ” she said. “It’s a fucking haiku. That man has a one-track mind and one train on it. Poetry. Save me from poetry.”", - "color": "yellow", - "id": "c27bc8cc64d50ea2" - }, - { - "text": "He also wasn’t being fair to Avasarala. It wasn’t about making him take her orders, not really. It was about being in a situation that she was utterly untrained for, where she was the least useful person in the room and trying to assert some control. Trying to reshape the space around her to fit with her mental image of herself.", - "color": "yellow", - "id": "21910fd8b621a73d" - } - ], - "meta": { - "totalHighlights": 5 - } - }, - { - "bookId": "B0047Y171G", - "title": "Leviathan Wakes (The Expanse Book 1)", - "author": "James S. A. Corey", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:41.109Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B0047Y171G", - "highlights": [ - { - "text": "The joke was that Ceres didn’t have laws—it had police.", - "color": "yellow", - "id": "3afa297363756f27" - }, - { - "text": "It was easy to make fun of the marines when they weren’t listening. In Holden’s navy days, making fun of jarheads was as natural as cussing. But four marines had died getting him off the Donnager, and three of them had made a conscious decision to do so. Holden promised himself that he’d never make fun of them again.", - "color": "yellow", - "id": "f2e42a14eb1bdc03" - }, - { - "text": "The beautiful thing about losing your illusions, he thought, was that you got to stop pretending. All the years he’d told himself that he was respected, that he was good at his job, that all his sacrifices had been made for a reason fell away and left him with the clear, unmuddied knowledge that he was a functional alcoholic who had pared away everything good in his own life to make room for anesthetic. Shaddid thought he was a joke. Muss thought he was the price she paid not to sleep with someone she didn’t like. The only one who might have any respect for him at all was Havelock, an Earther. It was peaceful, in its way.", - "color": "yellow", - "id": "9f989658f0a1bc62" - }, - { - "text": "“You can’t just throw information at people,” Miller said. “You have to know what it means. What it’s going to do.", - "color": "yellow", - "id": "360b35e2eb7ef222" - }, - { - "text": "“Let me get this straight,” he said. “You’ll give me all the kingdoms of the Earth if I just bow down and do one act of worship for you?” Dresden cocked his head. “I don’t know the reference.”", - "color": "yellow", - "id": "6dc4533f0b84c1c0" - }, - { - "text": "Mars would survive, for a while. Pockets of the Belt would hold out even longer, probably. They had a culture of making do, surviving on scraps, living on the bleeding edge of their resources. But in the end, without Earth, everything would eventually die. Humans had been out of the gravity well a long time. Long enough to have developed the technology to cut that umbilical cord, but they’d just never bothered to do it. Stagnant. Humanity, for all its desire to fling itself into every livable pocket it could reach, had become stagnant. Satisfied to fly around in ships built half a century before, using technology that hadn’t changed in longer than that.", - "color": "yellow", - "id": "0e1a7c9be5fd685c" - } - ], - "meta": { - "totalHighlights": 6 - } - }, - { - "bookId": "B0D3DS677F", - "title": "Been Wrong So Long It Feels Like Right: A King Oliver Novel", - "author": "Walter Mosley", - "source": "read.amazon.com", - "extractedAt": "2025-10-20T03:00:43.607Z", - "notebookUrl": "https://read.amazon.com/notebook?asin=B0D3DS677F", - "highlights": [ - { - "text": "“You know how you said that me and my friends were different?” “Yeah?” “And how I said that we were mostly the same as everybody else?” “Uh-huh.” “The only real difference is that we believe that we can create our own reality; hunting the hunters, expecting a woman to be able to match most men in combat, blow for blow.”", - "location": "1,393", - "color": "yellow", - "id": "2dc5adb4709eebcf" - } - ], - "meta": { - "totalHighlights": 1 - } - } -] \ No newline at end of file diff --git a/.kindle/config.json b/.kindle/config.json deleted file mode 100644 index c866b40f..00000000 --- a/.kindle/config.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "outputFolder": "02_Areas/Test", - "templatePath": ".scripts/kindle/templates/kindle-note.md.hbs", - "overwrite": false, - "addTags": [ - "kindle", - "highlights", - "books" - ], - "createIndex": true, - "indexPath": "03_Resources/Kindle Highlights Index.md", - "lastSync": "2025-10-20T03:00:43.737Z" -} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 8b5f7794..f43f6f9c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -23,4 +23,7 @@ build/ .backup/ # Ignore git -.git/ \ No newline at end of file +.git/ + +# Ignore Handlebars templates (use Handlebars-specific syntax) +.scripts/kindle/templates/*.hbs \ No newline at end of file diff --git a/.scripts/kindle/auth.js b/.scripts/kindle/auth.js index 5019ea22..6fabde0f 100644 --- a/.scripts/kindle/auth.js +++ b/.scripts/kindle/auth.js @@ -1,50 +1,23 @@ #!/usr/bin/env node -import { chromium } from 'playwright'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { NOTEBOOK_URL } from './lib/config.js'; +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium } from 'playwright' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth'); +import { NOTEBOOK_URL } from './lib/config.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth') /** * Check if authentication is successful by verifying page state */ async function checkAuthenticationSuccess(page) { try { - const currentUrl = page.url(); - - // Check 1: Look for the book library container (most important - indicates successful load) - const hasLibrary = await page.locator('#kp-notebook-library, .kp-notebook-library').count() > 0; - - // Check 2: Verify auth cookies exist - const cookies = await page.context().cookies(); - const hasSessionToken = cookies.some((c) => c.name === 'session-token'); - - // Success if we have BOTH library and session token - // URL can still have signin params even after successful login, so we check it last - if (hasLibrary && hasSessionToken) { - return { success: true }; - } - - // Determine specific failure reason for helpful feedback - if (currentUrl.includes('signin') || currentUrl.includes('ap/signin')) { - if (!hasLibrary && !hasSessionToken) { - return { success: false, reason: 'Still on signin page' }; - } - } - - if (!hasLibrary) { - return { success: false, reason: 'Book library not loaded yet' }; - } - - if (!hasSessionToken) { - return { success: false, reason: 'No session token found' }; - } - - return { success: false, reason: 'Waiting for authentication' }; - } catch (error) { - return { success: false, reason: error.message }; + // If library loads, we're authenticated + const count = await page.locator('#kp-notebook-library').count() + return count > 0 + } catch (_error) { + return false } } @@ -52,115 +25,108 @@ async function checkAuthenticationSuccess(page) { * Poll the page until authentication is successful or timeout */ async function waitForAuthenticationSuccess(page, timeoutMs = 300000) { - const startTime = Date.now(); - const pollInterval = 2000; // Check every 2 seconds - - console.log('⏳ Waiting for authentication to complete...'); - console.log(' (Polling page state automatically - this may take a few minutes)\n'); + const startTime = Date.now() + const pollInterval = 2000 // Check every 2 seconds - let lastReason = ''; + console.log('⏳ Waiting for authentication to complete...') + console.log( + ' (Polling page state automatically - this may take a few minutes)\n', + ) while (Date.now() - startTime < timeoutMs) { - const result = await checkAuthenticationSuccess(page); - - if (result.success) { - return true; - } + const isAuthenticated = await checkAuthenticationSuccess(page) - // Only log if reason changed (reduce spam) - if (result.reason !== lastReason) { - console.log(` 🔍 Status: ${result.reason}...`); - lastReason = result.reason; + if (isAuthenticated) { + return true } // Wait before next check - await page.waitForTimeout(pollInterval); + await page.waitForTimeout(pollInterval) } - throw new Error('Authentication timeout - please try again'); + throw new Error('Authentication timeout - please try again') } /** * Interactive authentication - opens browser for user to log in */ async function main() { - console.log('🔐 Kindle Authentication\n'); - console.log('📁 Session will be saved to:', AUTH_DIR); - console.log('⏱️ Timeout: 5 minutes\n'); + console.log('🔐 Kindle Authentication\n') + console.log('📁 Session will be saved to:', AUTH_DIR) + console.log('⏱️ Timeout: 5 minutes\n') const ctx = await chromium.launchPersistentContext(AUTH_DIR, { headless: false, viewport: { height: 900, width: 1000 }, // Position on right side of screen (works for most screen sizes) args: ['--window-position=50,50'], - }); + }) - const page = await ctx.newPage(); + const page = await ctx.newPage() try { - console.log('🌐 Opening Amazon Kindle Notebook...\n'); - await page.goto(NOTEBOOK_URL, { waitUntil: 'load' }); + console.log('🌐 Opening Amazon Kindle Notebook...\n') + await page.goto(NOTEBOOK_URL, { waitUntil: 'load' }) - console.log('📖 INSTRUCTIONS:'); - console.log(' 1. Log in to Amazon in the browser window'); - console.log(' 2. Wait for your Kindle library/highlights to load'); - console.log(' 3. Authentication will be saved automatically!\n'); - console.log(' 💡 The script will detect when you\'re logged in.\n'); + console.log('📖 INSTRUCTIONS:') + console.log(' 1. Log in to Amazon in the browser window') + console.log(' 2. Wait for your Kindle library/highlights to load') + console.log(' 3. Authentication will be saved automatically!\n') + console.log(" 💡 The script will detect when you're logged in.\n") // Wait for successful authentication (auto-detected) - await waitForAuthenticationSuccess(page); + await waitForAuthenticationSuccess(page) - console.log('\n✅ Authentication detected! Saving session...\n'); + console.log('\n✅ Authentication detected! Saving session...\n') // Wait for network to settle await page.waitForLoadState('networkidle').catch(() => { - console.log(' (Network still active, continuing anyway)'); - }); + console.log(' (Network still active, continuing anyway)') + }) // Get final state - const finalUrl = page.url(); - console.log(` 📍 Final URL: ${finalUrl}`); + const finalUrl = page.url() + console.log(` 📍 Final URL: ${finalUrl}`) // Clean up redirect parameters if needed if (finalUrl.includes('openid') && !finalUrl.includes('signin')) { - console.log(' 🔄 Cleaning up redirect parameters...'); - await page.goto(NOTEBOOK_URL, { waitUntil: 'networkidle' }); + console.log(' 🔄 Cleaning up redirect parameters...') + await page.goto(NOTEBOOK_URL, { waitUntil: 'networkidle' }) } // Verify cookies - const cookies = await ctx.cookies(); - const hasXMain = cookies.some((c) => c.name === 'x-main'); - const hasSessionToken = cookies.some((c) => c.name === 'session-token'); + const cookies = await ctx.cookies() + const hasXMain = cookies.some((c) => c.name === 'x-main') + const hasSessionToken = cookies.some((c) => c.name === 'session-token') console.log( - ` 🍪 Auth cookies: x-main=${hasXMain}, session-token=${hasSessionToken}` - ); + ` 🍪 Auth cookies: x-main=${hasXMain}, session-token=${hasSessionToken}`, + ) if (!hasSessionToken) { console.log( - '\n⚠️ Warning: Session token not found. Authentication may be incomplete.' - ); - console.log(' Try running /kindle-auth again if sync fails.'); + '\n⚠️ Warning: Session token not found. Authentication may be incomplete.', + ) + console.log(' Try running /kindle-sync again if sync fails.') } - await ctx.close(); + await ctx.close() - console.log('\n✅ Authentication saved successfully!\n'); - console.log('Next steps:'); - console.log(' • Run /kindle-sync to sync your highlights'); - console.log(' • Or run /kindle-sync-all for a full re-scrape\n'); + console.log('\n✅ Authentication saved successfully!\n') + console.log('Next steps:') + console.log(' • Run /kindle-sync to sync your highlights') } catch (error) { - console.error('\n❌ Authentication failed:', error.message); - console.log('\nTroubleshooting:'); - console.log(' • Make sure you fully logged in to Amazon'); - console.log(' • Verify your book library is visible in the browser'); - console.log(' • Try running /kindle-auth again\n'); - await ctx.close(); - process.exit(1); + console.error('\n❌ Authentication failed:', error.message) + console.log('\nTroubleshooting:') + console.log(' • Make sure you fully logged in to Amazon') + console.log(' • Verify your book library is visible in the browser') + console.log(' • Try running /kindle-sync again\n') + await ctx.close() + process.exit(1) } } main().catch((error) => { - console.error('❌ Authentication failed:', error); - process.exit(1); -}); + console.error('❌ Authentication failed:', error) + process.exit(1) +}) diff --git a/.scripts/kindle/config.js b/.scripts/kindle/config.js index 4273170b..12ab8503 100644 --- a/.scripts/kindle/config.js +++ b/.scripts/kindle/config.js @@ -1,41 +1,39 @@ #!/usr/bin/env node -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const CONFIG_DIR = path.resolve(__dirname, '../../.kindle'); -const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const CONFIG_DIR = path.resolve(__dirname, '../../.kindle') +const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json') /** * Default configuration */ export const DEFAULT_CONFIG = { - outputFolder: '03_Resources/Kindle Highlights', - templatePath: '.scripts/kindle/templates/kindle-note.md.hbs', - overwrite: false, addTags: ['kindle', 'highlights', 'books'], - createIndex: true, - indexPath: '03_Resources/Kindle Highlights Index.md', lastSync: null, -}; + outputFolder: '03_Resources/Kindle Highlights', + overwrite: false, + templatePath: '.scripts/kindle/templates/kindle-note.md.hbs', +} /** * Load configuration from file, or create with defaults if it doesn't exist */ export async function loadConfig() { try { - const content = await fs.readFile(CONFIG_PATH, 'utf-8'); - const config = JSON.parse(content); + const content = await fs.readFile(CONFIG_PATH, 'utf-8') + const config = JSON.parse(content) // Merge with defaults to ensure all fields are present - return { ...DEFAULT_CONFIG, ...config }; - } catch (error) { + return { ...DEFAULT_CONFIG, ...config } + } catch (_error) { // Config doesn't exist, create it with defaults - console.log('📝 Creating default configuration...'); - await saveConfig(DEFAULT_CONFIG); - return DEFAULT_CONFIG; + console.log('📝 Creating default configuration...') + await saveConfig(DEFAULT_CONFIG) + return DEFAULT_CONFIG } } @@ -43,46 +41,29 @@ export async function loadConfig() { * Save configuration to file */ export async function saveConfig(config) { - await fs.mkdir(CONFIG_DIR, { recursive: true }); - await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8'); + await fs.mkdir(CONFIG_DIR, { recursive: true }) + await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8') } /** * Update specific config fields */ export async function updateConfig(updates) { - const config = await loadConfig(); - const updatedConfig = { ...config, ...updates }; - await saveConfig(updatedConfig); - return updatedConfig; + const config = await loadConfig() + const updatedConfig = { ...config, ...updates } + await saveConfig(updatedConfig) + return updatedConfig } /** - * Interactive configuration setup + * Show current configuration */ -async function interactiveConfig() { - console.log('🔧 Kindle Sync Configuration\n'); - - const currentConfig = await loadConfig(); - - console.log('Current configuration:'); - console.log(JSON.stringify(currentConfig, null, 2)); - console.log('\n'); - - // For now, just display the config and config file path - // In a full implementation, this would use a prompt library like 'prompts' or 'inquirer' - console.log(`📁 Config file: ${CONFIG_PATH}`); - console.log('\n📝 To modify configuration:'); - console.log(' 1. Edit the config file directly, or'); - console.log(' 2. Use the updateConfig() function in your scripts\n'); - - console.log('Available settings:'); - console.log(' • outputFolder: Where to save Kindle highlight notes'); - console.log(' • templatePath: Path to custom Handlebars template'); - console.log(' • overwrite: Whether to overwrite existing files'); - console.log(' • addTags: Array of tags to add to notes'); - console.log(' • createIndex: Whether to create a master index file'); - console.log(' • indexPath: Path to the master index file'); +async function showConfig() { + const currentConfig = await loadConfig() + console.log('📋 Current Kindle Sync Configuration:\n') + console.log(JSON.stringify(currentConfig, null, 2)) + console.log(`\n📁 Config file: ${CONFIG_PATH}`) + console.log('📝 Edit this file directly to change settings\n') } /** @@ -104,45 +85,45 @@ async function main() { }) .help() .alias('h', 'help') - .parseAsync(); + .parseAsync() if (argv.interactive) { - await interactiveConfig(); + await showConfig() } else if (argv.set) { - const [key, value] = argv.set.split('='); + const [key, value] = argv.set.split('=') if (!key || value === undefined) { - console.error('❌ Invalid format. Use: --set key=value'); - process.exit(1); + console.error('❌ Invalid format. Use: --set key=value') + process.exit(1) } // Parse value (try JSON parse for objects/arrays, otherwise string) - let parsedValue; + let parsedValue try { - parsedValue = JSON.parse(value); + parsedValue = JSON.parse(value) } catch { - parsedValue = value; + parsedValue = value } - const config = await updateConfig({ [key]: parsedValue }); - console.log(`✅ Updated ${key} = ${JSON.stringify(parsedValue)}`); - console.log(`\n📁 Config saved to: ${CONFIG_PATH}`); + await updateConfig({ [key]: parsedValue }) + console.log(`✅ Updated ${key} = ${JSON.stringify(parsedValue)}`) + console.log(`\n📁 Config saved to: ${CONFIG_PATH}`) } else if (argv._[0] === 'reset') { - await saveConfig(DEFAULT_CONFIG); - console.log('✅ Configuration reset to defaults'); - console.log(JSON.stringify(DEFAULT_CONFIG, null, 2)); + await saveConfig(DEFAULT_CONFIG) + console.log('✅ Configuration reset to defaults') + console.log(JSON.stringify(DEFAULT_CONFIG, null, 2)) } else { // Default: show config - const config = await loadConfig(); - console.log('📋 Current Kindle Sync Configuration:\n'); - console.log(JSON.stringify(config, null, 2)); - console.log(`\n📁 Config file: ${CONFIG_PATH}`); + const config = await loadConfig() + console.log('📋 Current Kindle Sync Configuration:\n') + console.log(JSON.stringify(config, null, 2)) + console.log(`\n📁 Config file: ${CONFIG_PATH}`) } } // Run CLI if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { - console.error('❌ Error:', error.message); - process.exit(1); - }); + console.error('❌ Error:', error.message) + process.exit(1) + }) } diff --git a/.scripts/kindle/lib/config.js b/.scripts/kindle/lib/config.js index 97ae255c..bddd860e 100644 --- a/.scripts/kindle/lib/config.js +++ b/.scripts/kindle/lib/config.js @@ -13,42 +13,29 @@ export const DEFAULT_SCRAPER_CONFIG = { bookSelectors: { // Sidebar container with list of books - container: ['#kp-notebook-library', '.kp-notebook-library'], + container: '#kp-notebook-library', // Individual book items in sidebar (not actual links, but clickable divs) links: '.kp-notebook-library-each-book', }, highlightSelectors: { // Color is in the class name, e.g., .kp-notebook-highlight-yellow - colorAttribute: ['.kp-notebook-highlight'], + colorAttribute: '.kp-notebook-highlight', // Container for all highlights - item: ['#kp-notebook-annotations > .a-row.a-spacing-base', '.a-row.a-spacing-base'], + item: '#kp-notebook-annotations > .a-row.a-spacing-base', // Location is in the highlight header - location: ['#annotationHighlightHeader', '.kp-notebook-metadata'], + location: '#annotationHighlightHeader', // Note text - note: ['.kp-notebook-note span#note', '.kp-notebook-note #note', 'span#note'], + note: '.kp-notebook-note #note', // Highlight text - text: [ - '.kp-notebook-highlight span#highlight', - '.kp-notebook-highlight #highlight', - '#highlight', - ], + text: '.kp-notebook-highlight #highlight', }, metadataSelectors: { // Author in sidebar - author: [ - '.kp-notebook-library-each-book p.kp-notebook-searchable', - 'p.kp-notebook-searchable', - ], + author: '.kp-notebook-library-each-book p.kp-notebook-searchable', // Title in sidebar - title: [ - '.kp-notebook-library-each-book h2.kp-notebook-searchable', - 'h2.kp-notebook-searchable', - ], + title: '.kp-notebook-library-each-book h2.kp-notebook-searchable', // Copyright truncation warning - truncationWarning: [ - '.copyright-export-limit', - '.kp-notebook-annotations-limit-warning', - ], + truncationWarning: '.copyright-export-limit', }, scraping: { maxLazyLoadRounds: 30, @@ -60,6 +47,6 @@ export const DEFAULT_SCRAPER_CONFIG = { scrollDelayIncrease: 20, scrollJitter: 400, }, -}; +} -export const NOTEBOOK_URL = 'https://read.amazon.com/notebook'; +export const NOTEBOOK_URL = 'https://read.amazon.com/notebook' diff --git a/.scripts/kindle/lib/errors.js b/.scripts/kindle/lib/errors.js index 95a5cede..d0797240 100644 --- a/.scripts/kindle/lib/errors.js +++ b/.scripts/kindle/lib/errors.js @@ -6,11 +6,10 @@ * Base error class for all Kindle export errors */ export class KindleExportError extends Error { - constructor(message, context = {}) { - super(message); - this.name = this.constructor.name; - this.context = context; - Error.captureStackTrace(this, this.constructor); + constructor(message) { + super(message) + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) } } @@ -23,9 +22,9 @@ export class AuthenticationError extends KindleExportError {} * Error thrown when data validation fails */ export class ValidationError extends KindleExportError { - constructor(message, errors, context = {}) { - super(message, context); - this.errors = errors; + constructor(message, errors) { + super(message) + this.errors = errors } } @@ -33,9 +32,9 @@ export class ValidationError extends KindleExportError { * Error thrown when file operations fail */ export class FileSystemError extends KindleExportError { - constructor(message, path, context = {}) { - super(message, { ...context, path }); - this.path = path; + constructor(message, path) { + super(message) + this.path = path } } @@ -44,7 +43,7 @@ export class FileSystemError extends KindleExportError { */ export function formatError(error) { if (error instanceof Error) { - return `${error.name}: ${error.message}`; + return `${error.name}: ${error.message}` } - return String(error); + return String(error) } diff --git a/.scripts/kindle/lib/renderer.js b/.scripts/kindle/lib/renderer.js index d2131adc..4ed7b4f6 100644 --- a/.scripts/kindle/lib/renderer.js +++ b/.scripts/kindle/lib/renderer.js @@ -1,70 +1,62 @@ -import Handlebars from 'handlebars'; -import { decode } from 'html-entities'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { createBookFilename, formatDate } from './utils.js'; -import { parseBookExports } from './validation.js'; +import Handlebars from 'handlebars' +import { decode } from 'html-entities' +import fs from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +import { createBookFilename, formatDate } from './utils.js' +import { parseBookExports } from './validation.js' -/** - * Extract author's last name from full author name - */ -function extractLastName(author) { - if (!author) return 'Unknown'; - const parts = author.trim().split(/\s+/); - return parts[parts.length - 1] || 'Unknown'; -} +const __dirname = path.dirname(fileURLToPath(import.meta.url)) /** * Create a Kindle app link for a specific location */ function createKindleLink(bookId, location) { - if (!location) return ''; + if (!location) return '' // Remove commas from location number - const cleanLocation = location.replace(/,/g, ''); - return `kindle://book?action=open&asin=${bookId}&location=${cleanLocation}`; + const cleanLocation = location.replace(/,/g, '') + return `kindle://book?action=open&asin=${bookId}&location=${cleanLocation}` } /** - * Register Handlebars helpers + * Register Handlebars helpers (runs once at module initialization) */ -function registerHandlebarsHelpers() { - // Helper to decode HTML entities - Handlebars.registerHelper('decode', function (text) { - return new Handlebars.SafeString(decode(text || '')); - }); - - // Helper to create Kindle app links - Handlebars.registerHelper('kindleLink', function (bookId, location) { - return createKindleLink(bookId, location); - }); - - // Helper to format dates - Handlebars.registerHelper('formatDate', function (date) { - return formatDate(date); - }); -} +// Helper to decode HTML entities +Handlebars.registerHelper('decode', function (text) { + return new Handlebars.SafeString(decode(text || '')) +}) + +// Helper to create Kindle app links +Handlebars.registerHelper('kindleLink', function (bookId, location) { + return createKindleLink(bookId, location) +}) + +// Helper to format dates +Handlebars.registerHelper('formatDate', function (date) { + return formatDate(date) +}) /** * Load and compile Handlebars template */ export async function loadTemplate(templatePath) { // Default template location - const defaultTemplate = path.resolve(__dirname, '../templates/kindle-note.md.hbs'); - const resolvedPath = templatePath || defaultTemplate; - - const templateSrc = await fs.readFile(resolvedPath, 'utf-8'); - return Handlebars.compile(templateSrc); + const defaultTemplate = path.resolve( + __dirname, + '../templates/kindle-note.md.hbs', + ) + const resolvedPath = templatePath || defaultTemplate + + const templateSrc = await fs.readFile(resolvedPath, 'utf-8') + return Handlebars.compile(templateSrc) } /** * Render a single book to Markdown */ export function renderBook(book, template) { - registerHandlebarsHelpers(); - return template(book); + return template(book) } /** @@ -72,59 +64,59 @@ export function renderBook(book, template) { */ export async function exportBooksToMarkdown(books, outputDir, options = {}) { const { - templatePath, overwrite = false, template: precompiledTemplate, - } = options; + templatePath, + } = options // Use provided template or load from path - const template = precompiledTemplate || (await loadTemplate(templatePath)); + const template = precompiledTemplate || (await loadTemplate(templatePath)) // Ensure output directory exists - await fs.mkdir(outputDir, { recursive: true }); + await fs.mkdir(outputDir, { recursive: true }) - let written = 0; - let skipped = 0; - const files = []; + let written = 0 + let skipped = 0 + const files = [] for (const book of books) { // Create filename - const filename = createBookFilename(book.title, book.author, book.bookId); - const destPath = path.join(outputDir, filename); + const filename = createBookFilename(book.title, book.author, book.bookId) + const destPath = path.join(outputDir, filename) // Check if file exists and handle overwrite logic if (!overwrite) { try { - await fs.access(destPath); - skipped++; - continue; // File exists and we don't want to overwrite + await fs.access(destPath) + skipped++ + continue // File exists and we don't want to overwrite } catch { // File doesn't exist, proceed } } - const markdown = renderBook(book, template); - await fs.writeFile(destPath, markdown, 'utf-8'); - written++; - files.push({ book, path: destPath }); + const markdown = renderBook(book, template) + await fs.writeFile(destPath, markdown, 'utf-8') + written++ + files.push({ book, path: destPath }) } - return { written, skipped, files }; + return { files, skipped, written } } /** * Load books from JSON file */ export async function loadBooksFromJSON(jsonPath) { - const rawJson = await fs.readFile(jsonPath, 'utf-8'); - const parsedJson = JSON.parse(rawJson); - return parseBookExports(parsedJson); + const rawJson = await fs.readFile(jsonPath, 'utf-8') + const parsedJson = JSON.parse(rawJson) + return parseBookExports(parsedJson) } /** * Save books to JSON file */ export async function saveBooksToJSON(books, jsonPath) { - await fs.mkdir(path.dirname(jsonPath), { recursive: true }); - await fs.writeFile(jsonPath, JSON.stringify(books, null, 2), 'utf-8'); + await fs.mkdir(path.dirname(jsonPath), { recursive: true }) + await fs.writeFile(jsonPath, JSON.stringify(books, null, 2), 'utf-8') } diff --git a/.scripts/kindle/lib/scraper.js b/.scripts/kindle/lib/scraper.js index 1a3b781a..ec5e0cba 100644 --- a/.scripts/kindle/lib/scraper.js +++ b/.scripts/kindle/lib/scraper.js @@ -1,12 +1,21 @@ -import { chromium } from 'playwright'; -import { AuthenticationError } from './errors.js'; -import { generateHighlightId } from './utils.js'; -import { DEFAULT_SCRAPER_CONFIG, NOTEBOOK_URL } from './config.js'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { chromium } from 'playwright' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const AUTH_DIR = path.resolve(__dirname, '../../../.kindle/auth'); +import { DEFAULT_SCRAPER_CONFIG, NOTEBOOK_URL } from './config.js' +import { AuthenticationError } from './errors.js' +import { generateHighlightId } from './utils.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const AUTH_DIR = path.resolve(__dirname, '../../../.kindle/auth') + +/** + * Extract text content from an element + */ +async function extractText(item, selector) { + const el = await item.$(selector) + return el ? (await el.textContent())?.trim() : undefined +} /** * Launch a persistent browser context using saved authentication @@ -17,85 +26,75 @@ export async function getContext(headless = false) { viewport: { height: 900, width: 1000 }, // Keep browser in consistent position for both auth and scraping args: headless ? [] : ['--window-position=50,50'], - }); + }) } /** * Verify authentication by checking if we're redirected to signin */ export async function ensureAuth(page) { - console.log(`🔍 Navigating to: ${NOTEBOOK_URL}`); - await page.goto(NOTEBOOK_URL, { waitUntil: 'networkidle' }); - console.log(`📍 Current URL: ${page.url()}`); + console.log(`🔍 Navigating to: ${NOTEBOOK_URL}`) + await page.goto(NOTEBOOK_URL, { waitUntil: 'networkidle' }) + console.log(`📍 Current URL: ${page.url()}`) if (/signin|ap\/signin/i.test(page.url())) { throw new AuthenticationError( 'Not authenticated. Authentication is required to access Kindle highlights.', - { currentUrl: page.url(), expectedUrl: NOTEBOOK_URL } - ); + ) } - console.log('✅ Authentication verified'); + console.log('✅ Authentication verified') } /** * Discover all books from the sidebar */ export async function discoverBooks(page, config = DEFAULT_SCRAPER_CONFIG) { - console.log('🔍 Discovering books from sidebar...'); + console.log('🔍 Discovering books from sidebar...') // Wait for notebook container to load - const containerSelector = config.bookSelectors.container.join(', '); await page - .waitForSelector(containerSelector, { + .waitForSelector(config.bookSelectors.container, { timeout: 10000, }) .catch(() => { - console.warn('⚠️ Could not find notebook container, proceeding anyway...'); - }); + console.warn( + '⚠️ Could not find notebook container, proceeding anyway...', + ) + }) // Extract all book items from sidebar - const bookSelector = config.bookSelectors.links; + const bookSelector = config.bookSelectors.links const books = await page.$$eval( bookSelector, (elements, selectors) => { return elements.map((el) => { // ASIN is the ID of the book div - const asin = el.id || ''; + const asin = el.id || '' // Extract title - let title = 'Unknown'; - for (const selector of selectors.title) { - const titleEl = el.querySelector(selector); - if (titleEl?.textContent.trim()) { - title = titleEl.textContent.trim(); - break; - } - } + const titleEl = el.querySelector(selectors.title) + const title = titleEl?.textContent.trim() || 'Unknown' // Extract author - let author; - for (const selector of selectors.author) { - const authorEl = el.querySelector(selector); - if (authorEl?.textContent.trim()) { - author = authorEl.textContent.trim(); - // Remove "By: " prefix if present - author = author.replace(/^By:\s*/i, ''); - break; - } + const authorEl = el.querySelector(selectors.author) + let author = authorEl?.textContent.trim() + if (author) { + // Remove "By: " prefix if present + author = author.replace(/^By:\s*/i, '') } - return { asin, author, title }; - }); + return { asin, author, title } + }) }, { author: config.metadataSelectors.author, title: config.metadataSelectors.title, - } - ); + }, + ) - console.log(`📚 Found ${books.length} book(s)`); - return books.filter((b) => b.asin); // Only return books with valid ASINs + console.log(`📚 Found ${books.length} book(s)`) + return books.filter((b) => b.asin) // Only return books with valid ASINs } /** @@ -104,87 +103,67 @@ export async function discoverBooks(page, config = DEFAULT_SCRAPER_CONFIG) { export async function clickBook(page, asin) { // Find the book div by ASIN // The data-action is on a span, and the anchor is inside it - const bookDiv = page.locator(`#${asin}`); - const clickableLink = bookDiv.locator('span[data-action="get-annotations-for-asin"] a'); + const bookDiv = page.locator(`#${asin}`) + const clickableLink = bookDiv.locator( + 'span[data-action="get-annotations-for-asin"] a', + ) - await clickableLink.click(); + await clickableLink.click() // Wait for highlights to load - await page.waitForSelector('#kp-notebook-annotations', { timeout: 5000 }).catch(() => { - console.warn('⚠️ Annotations container not found, continuing anyway...'); - }); + await page + .waitForSelector('#kp-notebook-annotations', { timeout: 5000 }) + .catch(() => { + console.warn('⚠️ Annotations container not found, continuing anyway...') + }) // Additional wait for content to populate - await page.waitForTimeout(500); + await page.waitForTimeout(500) } /** * Scrape a single book's highlights from the current page (after clicking it) */ -export async function scrapeBook(page, bookInfo, config = DEFAULT_SCRAPER_CONFIG) { - const { asin, author, title } = bookInfo; +export async function scrapeBook( + page, + bookInfo, + config = DEFAULT_SCRAPER_CONFIG, +) { + const { asin, author, title } = bookInfo // Click the book to load its highlights - await clickBook(page, asin); + await clickBook(page, asin) // Check for copyright truncation warning - const truncationSelector = config.metadataSelectors.truncationWarning.join(', '); - const truncated = (await page.locator(truncationSelector).count()) > 0; + const truncated = + (await page.locator(config.metadataSelectors.truncationWarning).count()) > 0 // Extract highlights from the annotations container - const annotationsContainer = '#kp-notebook-annotations'; + const annotationsContainer = '#kp-notebook-annotations' const highlightItems = await page.$$( - `${annotationsContainer} > .a-row.a-spacing-base` - ); + `${annotationsContainer} > .a-row.a-spacing-base`, + ) const highlights = await Promise.all( highlightItems.map(async (item) => { // Extract highlight text - let text = ''; - const highlightTextEl = await item.$('.kp-notebook-highlight #highlight'); - if (highlightTextEl) { - text = (await highlightTextEl.textContent())?.trim() || ''; - } + const text = + (await extractText(item, '.kp-notebook-highlight #highlight')) || '' // Extract note text (if present) - let note; - const noteTextEl = await item.$('.kp-notebook-note #note'); - if (noteTextEl) { - note = (await noteTextEl.textContent())?.trim() || undefined; - } + const note = await extractText(item, '.kp-notebook-note #note') // Extract location from metadata - let location; - const locationEl = await item.$('#annotationHighlightHeader'); - if (locationEl) { - const locationText = (await locationEl.textContent())?.trim() || ''; - // Parse "Yellow highlight | Location: 102" to extract location - const match = /Location:\s*(\S+)/i.exec(locationText); - if (match) { - location = match[1]; - } - } - - // Extract color from highlight class - let color; - const highlightDiv = await item.$('.kp-notebook-highlight'); - if (highlightDiv) { - const className = await highlightDiv.getAttribute('class'); - // Look for color in class name like "kp-notebook-highlight-yellow" - const colorMatch = className?.match(/kp-notebook-highlight-(\w+)/); - if (colorMatch) { - color = colorMatch[1]; - } - } + const locationText = await extractText(item, '#annotationHighlightHeader') + const location = locationText?.match(/Location:\s*(\S+)/i)?.[1] return { - text, - note, location, - color, - }; - }) - ); + note, + text, + } + }), + ) // Filter out empty highlights and generate stable IDs const validHighlights = highlights @@ -192,90 +171,100 @@ export async function scrapeBook(page, bookInfo, config = DEFAULT_SCRAPER_CONFIG .map((h) => ({ ...h, id: generateHighlightId(h.text, h.location), - })); + })) - const notebookUrl = `${NOTEBOOK_URL}?asin=${asin}`; + const notebookUrl = `${NOTEBOOK_URL}?asin=${asin}` - console.log(`✅ Scraped: ${title} (${validHighlights.length} highlights)`); + console.log(`✅ Scraped: ${title} (${validHighlights.length} highlights)`) return { - bookId: asin, - title, author: author || undefined, - source: 'read.amazon.com', + bookId: asin, extractedAt: new Date().toISOString(), - notebookUrl, highlights: validHighlights, meta: { totalHighlights: validHighlights.length, truncatedByCopyright: truncated || undefined, }, - }; + notebookUrl, + source: 'read.amazon.com', + title, + } } /** * Scrape all books from Kindle Notebook */ export async function scrapeAllBooks(options = {}) { - const { limit, headless = false, config = DEFAULT_SCRAPER_CONFIG } = options; + const { config = DEFAULT_SCRAPER_CONFIG, headless = false, limit } = options - console.log('📖 Starting Kindle Notebook scraper\n'); + console.log('📖 Starting Kindle Notebook scraper\n') // Launch browser and authenticate // Using headed mode by default because Amazon detects headless browsers - const ctx = await getContext(headless); - const page = await ctx.newPage(); + const ctx = await getContext(headless) + const page = await ctx.newPage() try { - await ensureAuth(page); + await ensureAuth(page) // Discover all books from the sidebar - const allBooks = await discoverBooks(page, config); + const allBooks = await discoverBooks(page, config) if (allBooks.length === 0) { - console.warn('⚠️ No books found. Check selectors or notebook page structure.'); - await ctx.close(); - return { books: [], errors: [] }; + console.warn( + '⚠️ No books found. Check selectors or notebook page structure.', + ) + await ctx.close() + return { books: [], errors: [] } } // Apply limit if specified - const books = limit ? allBooks.slice(0, limit) : allBooks; + const books = limit ? allBooks.slice(0, limit) : allBooks if (limit && allBooks.length > limit) { - console.log(`📚 Found ${allBooks.length} books, scraping first ${limit}\n`); + console.log( + `📚 Found ${allBooks.length} books, scraping first ${limit}\n`, + ) } // Scrape each book - const results = []; - const errors = []; + const results = [] + const errors = [] for (let i = 0; i < books.length; i++) { - const bookInfo = books[i]; - console.log(`\n📄 Processing book ${i + 1}/${books.length}: ${bookInfo.title}`); + const bookInfo = books[i] + console.log( + `\n📄 Processing book ${i + 1}/${books.length}: ${bookInfo.title}`, + ) try { - const book = await scrapeBook(page, bookInfo, config); - results.push(book); + const book = await scrapeBook(page, bookInfo, config) + results.push(book) // Rate limiting: random delay between books if (i < books.length - 1) { const delay = config.scraping.rateLimitBaseDelay + - Math.random() * config.scraping.rateLimitJitter; - await page.waitForTimeout(delay); + Math.random() * config.scraping.rateLimitJitter + await page.waitForTimeout(delay) } } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - console.error(`❌ Failed to scrape book: ${errorMsg}`); - errors.push({ asin: bookInfo.asin, title: bookInfo.title, error: errorMsg }); + const errorMsg = error instanceof Error ? error.message : String(error) + console.error(`❌ Failed to scrape book: ${errorMsg}`) + errors.push({ + asin: bookInfo.asin, + error: errorMsg, + title: bookInfo.title, + }) } } - await ctx.close(); + await ctx.close() - return { books: results, errors }; + return { books: results, errors } } catch (error) { - await ctx.close(); - throw error; + await ctx.close() + throw error } } diff --git a/.scripts/kindle/lib/utils.js b/.scripts/kindle/lib/utils.js index 9dc647ab..5646221b 100644 --- a/.scripts/kindle/lib/utils.js +++ b/.scripts/kindle/lib/utils.js @@ -1,10 +1,10 @@ -import { createHash } from 'node:crypto'; +import { createHash } from 'node:crypto' /** * Sanitize a string for use in filenames by replacing invalid characters */ export function fileSafe(input) { - return input.replace(/[\\/:*?"<>|]/g, '_').trim(); + return input.replace(/[\\/:*?"<>|]/g, '_').trim() } /** @@ -12,8 +12,8 @@ export function fileSafe(input) { * Uses text + location to ensure stability across exports */ export function generateHighlightId(text, location) { - const content = location ? `${text}::${location}` : text; - return createHash('sha256').update(content).digest('hex').slice(0, 16); + const content = location ? `${text}::${location}` : text + return createHash('sha256').update(content).digest('hex').slice(0, 16) } /** @@ -21,9 +21,9 @@ export function generateHighlightId(text, location) { * Falls back to slugified title if ASIN not found */ export function extractBookId(url, title) { - const asinMatch = /[?&]asin=([^&]+)/.exec(url); + const asinMatch = /[?&]asin=([^&]+)/.exec(url) if (asinMatch?.[1]) { - return asinMatch[1]; + return asinMatch[1] } // Fallback: slugify title @@ -33,7 +33,7 @@ export function extractBookId(url, title) { .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') - .slice(0, 50); // Reasonable length limit + .slice(0, 50) // Reasonable length limit } /** @@ -41,20 +41,30 @@ export function extractBookId(url, title) { */ export function formatDate(date) { if (typeof date === 'string') { - date = new Date(date); + date = new Date(date) } - return date.toISOString().split('T')[0]; + return date.toISOString().split('T')[0] +} + +/** + * Extract author's last name from full author name + */ +function extractLastName(author) { + if (!author) return 'Unknown' + const parts = author.trim().split(/\s+/) + return parts[parts.length - 1] || 'Unknown' } /** * Create a safe filename from book title and author */ -export function createBookFilename(title, author, asin) { - const safeTitle = fileSafe(title); - const safeAuthor = author ? fileSafe(author) : ''; +export function createBookFilename(title, author) { + const safeTitle = fileSafe(title) - if (safeAuthor) { - return `${safeTitle} - ${safeAuthor}.md`; + if (author) { + const lastName = extractLastName(author) + const safeLastName = fileSafe(lastName) + return `${safeLastName} - ${safeTitle}.md` } - return `${safeTitle}.md`; + return `${safeTitle}.md` } diff --git a/.scripts/kindle/lib/validation.js b/.scripts/kindle/lib/validation.js index dad9d3bd..6a8a5c8b 100644 --- a/.scripts/kindle/lib/validation.js +++ b/.scripts/kindle/lib/validation.js @@ -1,21 +1,16 @@ -import { z } from 'zod'; -import { ValidationError } from './errors.js'; +import { z } from 'zod' -/** - * Zod schema for highlight colors - */ -export const highlightColorSchema = z.string(); +import { ValidationError } from './errors.js' /** * Zod schema for a single highlight */ export const highlightSchema = z.object({ id: z.string().min(1), - text: z.string().min(1), - note: z.string().optional(), location: z.string().optional(), - color: highlightColorSchema.optional(), -}); + note: z.string().optional(), + text: z.string().min(1), +}) /** * Zod schema for book export metadata @@ -25,35 +20,35 @@ export const bookMetaSchema = z totalHighlights: z.number().int().nonnegative().optional(), truncatedByCopyright: z.boolean().optional(), }) - .optional(); + .optional() /** * Zod schema for a complete book export */ export const bookExportSchema = z.object({ - bookId: z.string().min(1), - title: z.string().min(1), author: z.string().optional(), - source: z.literal('read.amazon.com'), + bookId: z.string().min(1), extractedAt: z.string().datetime(), - notebookUrl: z.string().url().optional(), highlights: z.array(highlightSchema), meta: bookMetaSchema, -}); + notebookUrl: z.string().url().optional(), + source: z.literal('read.amazon.com'), + title: z.string().min(1), +}) /** * Zod schema for an array of book exports */ -export const bookExportsSchema = z.array(bookExportSchema); +export const bookExportsSchema = z.array(bookExportSchema) /** * Format Zod error issues into readable error messages */ function formatZodError(error) { return error.issues.map((issue) => { - const path = issue.path.join('.'); - return `${path}: ${issue.message}`; - }); + const path = issue.path.join('.') + return `${path}: ${issue.message}` + }) } /** @@ -62,16 +57,15 @@ function formatZodError(error) { */ export function parseBookExports(data) { try { - return bookExportsSchema.parse(data); + return bookExportsSchema.parse(data) } catch (error) { if (error instanceof z.ZodError) { throw new ValidationError( 'Failed to validate book exports', formatZodError(error), - { data } - ); + ) } - throw error; + throw error } } @@ -80,15 +74,14 @@ export function parseBookExports(data) { */ export function validateBookExport(data) { try { - return bookExportSchema.parse(data); + return bookExportSchema.parse(data) } catch (error) { if (error instanceof z.ZodError) { throw new ValidationError( 'Failed to validate book export', formatZodError(error), - { data } - ); + ) } - throw error; + throw error } } diff --git a/.scripts/kindle/sync.js b/.scripts/kindle/sync.js index 93f9ba84..04c49811 100644 --- a/.scripts/kindle/sync.js +++ b/.scripts/kindle/sync.js @@ -1,35 +1,40 @@ #!/usr/bin/env node -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import fs from 'node:fs/promises'; -import { spawn } from 'node:child_process'; -import readline from 'node:readline'; -import { loadConfig, updateConfig } from './config.js'; -import { scrapeAllBooks } from './lib/scraper.js'; -import { exportBooksToMarkdown, saveBooksToJSON, loadTemplate } from './lib/renderer.js'; -import { formatError, AuthenticationError } from './lib/errors.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const CACHE_PATH = path.resolve(__dirname, '../../.kindle/cache/all_books.json'); -const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth'); -const AUTH_SCRIPT = path.resolve(__dirname, './auth.js'); -const CONFIG_PATH = path.resolve(__dirname, '../../.kindle/config.json'); +import { spawn } from 'node:child_process' +import fs from 'node:fs/promises' +import path from 'node:path' +import readline from 'node:readline' +import { fileURLToPath } from 'node:url' +import yargs from 'yargs' +import { hideBin } from 'yargs/helpers' + +import { loadConfig, updateConfig } from './config.js' +import { AuthenticationError, formatError } from './lib/errors.js' +import { + exportBooksToMarkdown, + loadTemplate, + saveBooksToJSON, +} from './lib/renderer.js' +import { scrapeAllBooks } from './lib/scraper.js' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const CACHE_PATH = path.resolve(__dirname, '../../.kindle/cache/all_books.json') +const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth') +const AUTH_SCRIPT = path.resolve(__dirname, './auth.js') +const CONFIG_PATH = path.resolve(__dirname, '../../.kindle/config.json') /** * Check if authentication exists */ async function checkAuthExists() { try { - const stats = await fs.stat(AUTH_DIR); - if (!stats.isDirectory()) return false; + const stats = await fs.stat(AUTH_DIR) + if (!stats.isDirectory()) return false // Check if auth directory has files - const files = await fs.readdir(AUTH_DIR); - return files.length > 0; + const files = await fs.readdir(AUTH_DIR) + return files.length > 0 } catch { - return false; + return false } } @@ -38,10 +43,10 @@ async function checkAuthExists() { */ async function isFirstRun() { try { - await fs.access(CONFIG_PATH); - return false; + await fs.access(CONFIG_PATH) + return false } catch { - return true; + return true } } @@ -52,248 +57,241 @@ async function promptForOutputFolder() { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, - }); + }) return new Promise((resolve) => { - console.log('\n📁 Kindle Sync Setup\n'); - console.log('Where would you like to save your Kindle highlights?'); - console.log('(Press Enter to use default: 03_Resources/Kindle Highlights)\n'); + console.log('\n📁 Kindle Sync Setup\n') + console.log('Where would you like to save your Kindle highlights?') + console.log( + '(Press Enter to use default: 03_Resources/Kindle Highlights)\n', + ) rl.question('Output folder: ', (answer) => { - rl.close(); - const folder = answer.trim() || '03_Resources/Kindle Highlights'; - resolve(folder); - }); - }); + rl.close() + const folder = answer.trim() || '03_Resources/Kindle Highlights' + resolve(folder) + }) + }) } /** * Run authentication script */ async function runAuth() { - console.log('🔐 Authentication required. Starting auth flow...\n'); + console.log('🔐 Authentication required. Starting auth flow...\n') return new Promise((resolve, reject) => { const authProcess = spawn('node', [AUTH_SCRIPT], { - stdio: 'inherit', // Pass through stdin/stdout/stderr cwd: path.dirname(AUTH_SCRIPT), - }); + stdio: 'inherit', // Pass through stdin/stdout/stderr + }) authProcess.on('close', (code) => { if (code === 0) { - console.log('\n✅ Authentication complete! Continuing with sync...\n'); - resolve(); + console.log('\n✅ Authentication complete! Continuing with sync...\n') + resolve() } else { - reject(new Error(`Authentication failed with exit code ${code}`)); + reject(new Error(`Authentication failed with exit code ${code}`)) } - }); + }) authProcess.on('error', (error) => { - reject(new Error(`Failed to start auth process: ${error.message}`)); - }); - }); + reject(new Error(`Failed to start auth process: ${error.message}`)) + }) + }) } /** - * Main sync function + * Handle first-run setup: prompt for output folder and save to config */ -async function sync(options) { - const { - all = false, - limit, - last, - force = false, - books: specificBooks, - outputFolder, - headless = false, - } = options; - - console.log('📚 Kindle Highlights Sync\n'); +async function ensureFirstRunSetup(outputFolder) { + const firstRun = await isFirstRun() - // Check if first run and prompt for folder - const firstRun = await isFirstRun(); if (firstRun && !outputFolder) { - const userFolder = await promptForOutputFolder(); - outputFolder = userFolder; - console.log(`\n✅ Will save highlights to: ${userFolder}\n`); - - // Save the user's choice to config - await updateConfig({ outputFolder: userFolder }); + const userFolder = await promptForOutputFolder() + console.log(`\n✅ Will save highlights to: ${userFolder}\n`) + await updateConfig({ outputFolder: userFolder }) + return userFolder } else if (firstRun && outputFolder) { - // First run with --output flag provided: save to config - console.log(`📁 First run: saving output folder to config...\n`); - await updateConfig({ outputFolder }); + console.log(`📁 First run: saving output folder to config...\n`) + await updateConfig({ outputFolder }) + return outputFolder } - // Debug: Log options - console.log('Options:', { all, limit, last, force, books: specificBooks, outputFolder }); - console.log(); + return outputFolder +} - // Check authentication before proceeding with scraping - const needsScraping = force || all || specificBooks || !all; - if (needsScraping) { - const hasAuth = await checkAuthExists(); - if (!hasAuth) { - console.log('⚠️ No authentication found.\n'); - await runAuth(); +/** + * Get books either from cache or by scraping, with auth retry logic + */ +async function getOrScrapeBooks(options) { + const { all, force, headless, last, limit, specificBooks } = options + + // Determine scraping strategy + const scrapeLimit = all ? undefined : last ? undefined : limit + const useCache = !force && !all && !specificBooks && !limit + + // Try to use cache first + if (useCache) { + try { + console.log('🔍 Checking for cached data...') + const { loadBooksFromJSON } = await import('./lib/renderer.js') + const books = await loadBooksFromJSON(CACHE_PATH) + console.log(`✅ Loaded ${books.length} books from cache\n`) + return books + } catch (_error) { + console.log('📥 No cache found, will scrape fresh data\n') } } - // Load configuration - const config = await loadConfig(); - const resolvedOutputFolder = outputFolder || config.outputFolder; - - // Resolve output folder relative to vault root - const vaultRoot = path.resolve(__dirname, '../../'); - const outputDir = path.resolve(vaultRoot, resolvedOutputFolder); + // Scrape fresh data + console.log('🌐 Scraping Kindle Notebook...\n') - console.log(`📁 Output folder: ${resolvedOutputFolder}`); - console.log(`📄 Template: ${config.templatePath}\n`); + if (last) { + console.log(`📋 Will scrape all books, then select last ${last}...\n`) + } else if (limit) { + console.log(`📋 Will scrape first ${limit} books...\n`) + } - // Check if we should use cached data or scrape fresh - let books; - const useCache = !force && !all && !specificBooks && !limit; + try { + return await attemptScrape(scrapeLimit, headless, last) + } catch (error) { + // If authentication error, try to re-auth and retry once + if (error instanceof AuthenticationError) { + console.error('\n❌ Authentication expired or invalid.\n') + await runAuth() + console.log('\n🔄 Retrying scrape...\n') + + try { + return await attemptScrape(scrapeLimit, headless, last) + } catch (retryError) { + console.error('\n❌ Scraping failed after re-authentication:') + console.error(formatError(retryError)) + throw retryError + } + } else { + console.error('\n❌ Scraping failed:') + console.error(formatError(error)) + throw error + } + } +} - if (useCache) { - try { - console.log('🔍 Checking for cached data...'); - const { loadBooksFromJSON } = await import('./lib/renderer.js'); - books = await loadBooksFromJSON(CACHE_PATH); - console.log(`✅ Loaded ${books.length} books from cache\n`); - } catch (error) { - console.log('📥 No cache found, will scrape fresh data\n'); - books = null; +/** + * Attempt to scrape books with proper error handling and caching + */ +async function attemptScrape(scrapeLimit, headless, last) { + const result = await scrapeAllBooks({ headless, limit: scrapeLimit }) + let books = result.books + + // If using --last flag, get the last N books instead of first N + if (last) { + if (books.length > last) { + console.log( + `\n📚 Selecting last ${last} books from ${books.length} total...\n`, + ) + books = books.slice(-last) + } else { + console.log( + `\n📚 Found ${books.length} books (less than ${last} requested)\n`, + ) } } - // Scrape if no cache or forced - if (!books) { - console.log('🌐 Scraping Kindle Notebook...\n'); + if (result.errors.length > 0) { + console.warn(`\n⚠️ Failed to scrape ${result.errors.length} book(s):`) + result.errors.forEach(({ error, title }) => { + console.warn(` - ${title}: ${error}`) + }) + } - // Determine scraping strategy - // --last requires scraping ALL books to know which are the last N - const scrapeLimit = all ? undefined : (last ? undefined : limit); + // Save to cache + console.log('\n💾 Saving to cache...') + await saveBooksToJSON(books, CACHE_PATH) + console.log(` ✅ Cached ${books.length} books`) - if (last) { - console.log(`📋 Will scrape all books, then select last ${last}...\n`); - } else if (limit) { - console.log(`📋 Will scrape first ${limit} books...\n`); - } + return books +} - try { - const result = await scrapeAllBooks({ - limit: scrapeLimit, - headless, - }); - - books = result.books; - - // If using --last flag, get the last N books instead of first N - if (last) { - if (books.length > last) { - console.log(`\n📚 Selecting last ${last} books from ${books.length} total...\n`); - books = books.slice(-last); - } else { - console.log(`\n📚 Found ${books.length} books (less than ${last} requested)\n`); - } - } +/** + * Main sync function + */ +async function sync(options) { + const { + all = false, + books: specificBooks, + force = false, + outputFolder: initialOutputFolder, + } = options - if (result.errors.length > 0) { - console.warn(`\n⚠️ Failed to scrape ${result.errors.length} book(s):`); - result.errors.forEach(({ title, error }) => { - console.warn(` - ${title}: ${error}`); - }); - } + console.log('📚 Kindle Highlights Sync\n') - // Save to cache - console.log('\n💾 Saving to cache...'); - await saveBooksToJSON(books, CACHE_PATH); - console.log(` ✅ Cached ${books.length} books`); - } catch (error) { - // If authentication error, try to re-auth and retry once - if (error instanceof AuthenticationError) { - console.error('\n❌ Authentication expired or invalid.\n'); - await runAuth(); - console.log('\n🔄 Retrying scrape...\n'); - - try { - const result = await scrapeAllBooks({ - limit: scrapeLimit, - headless, - }); - books = result.books; - - if (last) { - if (books.length > last) { - books = books.slice(-last); - } - } - - if (result.errors.length > 0) { - console.warn(`\n⚠️ Failed to scrape ${result.errors.length} book(s):`); - result.errors.forEach(({ title, error }) => { - console.warn(` - ${title}: ${error}`); - }); - } - - // Save to cache - console.log('\n💾 Saving to cache...'); - await saveBooksToJSON(books, CACHE_PATH); - console.log(` ✅ Cached ${books.length} books`); - } catch (retryError) { - console.error('\n❌ Scraping failed after re-authentication:'); - console.error(formatError(retryError)); - throw retryError; - } - } else { - console.error('\n❌ Scraping failed:'); - console.error(formatError(error)); - throw error; - } + // Handle first-run setup + const outputFolder = await ensureFirstRunSetup(initialOutputFolder) + + // Check authentication before proceeding with scraping + const needsScraping = force || all || specificBooks || !all + if (needsScraping) { + const hasAuth = await checkAuthExists() + if (!hasAuth) { + console.log('⚠️ No authentication found.\n') + await runAuth() } } + // Load configuration + const config = await loadConfig() + const resolvedOutputFolder = outputFolder || config.outputFolder + + // Resolve output folder relative to vault root + const vaultRoot = path.resolve(__dirname, '../../') + const outputDir = path.resolve(vaultRoot, resolvedOutputFolder) + + console.log(`📁 Output folder: ${resolvedOutputFolder}`) + console.log(`📄 Template: ${config.templatePath}\n`) + + // Get books from cache or by scraping + const books = await getOrScrapeBooks(options) + if (books.length === 0) { - console.log('⚠️ No books to sync'); - return; + console.log('⚠️ No books to sync') + return } // Filter for specific books if requested - let booksToExport = books; + let booksToExport = books if (specificBooks) { - const bookTitles = specificBooks.split(',').map((t) => t.trim().toLowerCase()); + const bookTitles = specificBooks + .split(',') + .map((t) => t.trim().toLowerCase()) booksToExport = books.filter((b) => - bookTitles.some((title) => b.title.toLowerCase().includes(title)) - ); - console.log(`\n🔍 Filtered to ${booksToExport.length} book(s) matching: ${specificBooks}`); + bookTitles.some((title) => b.title.toLowerCase().includes(title)), + ) + console.log( + `\n🔍 Filtered to ${booksToExport.length} book(s) matching: ${specificBooks}`, + ) } // Load template - console.log('\n📝 Rendering Markdown...'); - const templatePath = path.resolve(vaultRoot, config.templatePath); - const template = await loadTemplate(templatePath); + console.log('\n📝 Rendering Markdown...') + const templatePath = path.resolve(vaultRoot, config.templatePath) + const template = await loadTemplate(templatePath) // Export to Markdown const result = await exportBooksToMarkdown(booksToExport, outputDir, { - template, overwrite: config.overwrite, - }); + template, + }) - console.log('\n✅ Sync complete!'); - console.log(` 📝 Written: ${result.written} file(s)`); + console.log('\n✅ Sync complete!') + console.log(` 📝 Written: ${result.written} file(s)`) if (result.skipped > 0) { - console.log(` ⏭️ Skipped: ${result.skipped} file(s) (already exist)`); + console.log(` ⏭️ Skipped: ${result.skipped} file(s) (already exist)`) } - console.log(` 📁 Location: ${outputDir}`); + console.log(` 📁 Location: ${outputDir}`) // Update last sync time - await updateConfig({ lastSync: new Date().toISOString() }); - - // TODO: Optionally create index file if config.createIndex is true - if (config.createIndex) { - console.log('\n📋 Index file creation not yet implemented'); - console.log(' You can manually create an index by linking to the book notes'); - } + await updateConfig({ lastSync: new Date().toISOString() }) } /** @@ -303,9 +301,9 @@ async function main() { const argv = await yargs(hideBin(process.argv)) .option('all', { alias: 'a', + default: false, description: 'Sync all books (re-scrape everything)', type: 'boolean', - default: false, }) .option('limit', { alias: 'l', @@ -318,13 +316,14 @@ async function main() { }) .option('force', { alias: 'f', + default: false, description: 'Force re-scrape even if cache exists', type: 'boolean', - default: false, }) .option('books', { alias: 'b', - description: 'Comma-separated list of book titles to sync (partial match)', + description: + 'Comma-separated list of book titles to sync (partial match)', type: 'string', }) .option('output', { @@ -333,9 +332,10 @@ async function main() { type: 'string', }) .option('headless', { - description: 'Run browser in headless mode (not recommended - may cause auth issues)', - type: 'boolean', default: false, + description: + 'Run browser in headless mode (not recommended - may cause auth issues)', + type: 'boolean', }) .example('$0', 'Sync using cached data') .example('$0 --all', 'Re-scrape all books') @@ -344,20 +344,20 @@ async function main() { .example('$0 --books "Atomic Habits,Deep Work"', 'Sync specific books') .help() .alias('h', 'help') - .parseAsync(); + .parseAsync() await sync({ all: argv.all, - limit: argv.limit, - last: argv.last, - force: argv.force, books: argv.books, - outputFolder: argv.output, + force: argv.force, headless: argv.headless, - }); + last: argv.last, + limit: argv.limit, + outputFolder: argv.output, + }) } main().catch((error) => { - console.error('\n❌ Sync failed:', formatError(error)); - process.exit(1); -}); + console.error('\n❌ Sync failed:', formatError(error)) + process.exit(1) +}) diff --git a/02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md b/02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md deleted file mode 100644 index e8392ce8..00000000 --- a/02_Areas/Test/Abaddon's Gate (The Expanse Book 3) - James S. A. Corey.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: "Abaddon's Gate (The Expanse Book 3)" -author: "James S. A. Corey" -asin: B00A2DZMYE -highlights: 4 -lastSynced: 2025-10-20T03:00:35.395Z ---- - -# Abaddon's Gate (The Expanse Book 3) - - -## Highlights - -“What have we got?” Ashford said. “Short form.” “It’s fucking weird, sir,” Chan said, and everyone chuckled. “Our best analysis is that the Ring is an artificially sustained Einstein-Rosen bridge. You go through the Ring, you don’t come out the other side here.” “So it’s a gate,” Ashford said. “Yes, sir. It appears that the protomolecule or Phoebe bug or whatever you want to call it was launched at the solar system several billion years ago, aiming for Earth with the intention of hijacking primitive life to build a gateway. We’re positing that whoever created the protomolecule did it as a first step toward making travel to the solar system more convenient and practical later.” - - ---- -Protogen had named the protomolecule and decided it was a tool that could redefine what it meant to be human. Jules-Pierre Mao had treated it like a weapon. It killed humans, therefore it was a weapon. But radiation killed humans, and a medical X-ray machine wasn’t intended as a weapon. Holden was starting to feel like they were all monkeys playing with a microwave. Push a button, a light comes on inside, so it’s a light. Push a different button and stick your hand inside, it burns you, so it’s a weapon. Learn to open and close the door, it’s a place to hide things. Never grasping what it actually did, and maybe not even having the framework necessary to figure it out. No monkey ever reheated a frozen burrito. - - ---- -The truth was, distance was always measured in time. - - ---- -When the orbits were right, the journey from Luna to Mars could take as little as twelve days. The trek from Saturn to Ceres, a few months. And because they were out there with their primate brains, evolved on the plains of prehistoric Africa, everyone had a sense of how far it was. Saturn to Ceres was a few months. Luna to Mars was a few days. Distance was time, and so they didn’t get overwhelmed by it. - - ---- diff --git a/02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md b/02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md deleted file mode 100644 index 89e7ea55..00000000 --- a/02_Areas/Test/Babylon's Ashes (The Expanse Book 6) - James S. A. Corey.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "Babylon's Ashes (The Expanse Book 6)" -author: "James S. A. Corey" -asin: B018S2773Y -highlights: 5 -lastSynced: 2025-10-20T02:59:36.018Z ---- - -# Babylon's Ashes (The Expanse Book 6) - - -## Highlights - -The John Galt and the Mark Watney, out of Mars. - loc: [2,231](kindle://book?action=open&asin=B018S2773Y&location=2231) - - ---- -Holden went on as if he hadn’t heard Alex at all. “I thought if you told people facts, they’d draw their conclusions, and because the facts were true, the conclusions mostly would be too. But we don’t run on facts. We run on stories about things. About people. Naomi told me that when the rocks fell, the people on Inaros’ ship cheered. They were happy about it.” - loc: [2,491](kindle://book?action=open&asin=B018S2773Y&location=2491) - - ---- -If someone killed Pallas, he’d be worried about what the drop in refining capacity would do before he thought about how many preschools there are on the station. Or if the station manager’s son liked writing poetry. Or that blowing the station meant that Annie down in Pallas central accounting wasn’t going to get to throw her big birthday party after all.” - loc: [2,498](kindle://book?action=open&asin=B018S2773Y&location=2498) - - ---- -listening to the too-loud laughter of the soldiers, one moment came back to him vividly. His professor—an overweight, balding man with an alcoholic’s complexion and an air of intelligence so profound it seemed to bend space around him—lifting a hand and speaking a phrase: the terror of the normal. - loc: [3,545](kindle://book?action=open&asin=B018S2773Y&location=3545) - - ---- -Naomi smiled. Laughed. It was strange how it made the darkness better. Not less dark. Just better, even though it still was what it was. - loc: [4,173](kindle://book?action=open&asin=B018S2773Y&location=4173) - -***Note:*** Trudie girl <3 - ---- diff --git a/02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md b/02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md deleted file mode 100644 index 70fd81c0..00000000 --- a/02_Areas/Test/Been Wrong So Long It Feels Like Right_ A King Oliver Novel - Walter Mosley.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "Been Wrong So Long It Feels Like Right: A King Oliver Novel" -author: "Walter Mosley" -asin: B0D3DS677F -highlights: 1 -lastSynced: 2025-10-20T03:00:43.607Z ---- - -# Been Wrong So Long It Feels Like Right: A King Oliver Novel - - -## Highlights - -“You know how you said that me and my friends were different?” “Yeah?” “And how I said that we were mostly the same as everybody else?” “Uh-huh.” “The only real difference is that we believe that we can create our own reality; hunting the hunters, expecting a woman to be able to match most men in combat, blow for blow.” - loc: [1,393](kindle://book?action=open&asin=B0D3DS677F&location=1393) - - ---- diff --git a/02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md b/02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md deleted file mode 100644 index 51651546..00000000 --- a/02_Areas/Test/Beyond Weird_ Why Everything You Thought You Knew about Quantum Physics Is Different - Philip Ball.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -title: "Beyond Weird: Why Everything You Thought You Knew about Quantum Physics Is Different" -author: "Philip Ball" -asin: B07H9HNDYL -highlights: 38 -lastSynced: 2025-10-20T02:59:33.827Z ---- - -# Beyond Weird: Why Everything You Thought You Knew about Quantum Physics Is Different - - -## Highlights - -Rather than insisting on its difficulty, we might better regard it as a beguiling, maddening, even amusing gauntlet thrown down to challenge the imagination. For that is indeed what is challenged. I suspect we are, in the wider cultural context, finally beginning to appreciate this. Artists, writers, poets and playwrights have started to imbibe and deploy ideas from quantum physics: see, for instance, plays such as Tom Stoppard’s Hapgood and Michael Frayn’s Copenhagen, and novels such as Jeanette Winterson’s Gut Symmetries and Audrey Niffenegger’s The Time Traveler’s Wife. We can argue about how accurately or aptly these writers appropriate the scientific ideas, but it is right that there should be imaginative responses to quantum mechanics, because it is quite possible that only an imagination sufficiently broad and liberated will come close to articulating what it is about. - loc: [102](kindle://book?action=open&asin=B07H9HNDYL&location=102) - -***Note:*** Check out these works, especially Winterson - ---- -Feynman seemed to feel that it was impossible and even pointless to attempt anything comparable for quantum mechanics: We can’t pretend to understand it since it affronts all our commonsense notions. The best we can do is to describe what happens in mathematics, in equations, and that’s very difficult. What is even harder is trying to decide what the equations mean. That’s the hardest thing of all. - loc: [143](kindle://book?action=open&asin=B07H9HNDYL&location=143) - - ---- -only a few decades ago, professing a serious interest in the topic could be tantamount to career suicide for a young physicist. Only a handful of scientists and philosophers, idiosyncratically if not plain crankily, insisted on caring about the answer. Many researchers would shrug or roll their eyes when the ‘meaning’ of quantum mechanics came up; some still do. ‘Ah, nobody understands it anyway!’ How different this is from the attitude of Albert Einstein, Niels Bohr and their contemporaries, for whom grappling with the apparent oddness of the theory became almost an obsession. For them, the meaning mattered intensely. In 1998 the American physicist John Wheeler, a pioneer of modern quantum theory, lamented the loss of the ‘desperate puzzlement’ that was in the air in the 1930s. ‘I want to recapture that feeling for all, even if it is my last act on Earth’, Wheeler said. - loc: [153](kindle://book?action=open&asin=B07H9HNDYL&location=153) - -***Note:*** "desperate puzzlement" - ---- -You can see that I’m already struggling to find a language that works for talking about these things. That’s OK, and you’ll have to get used to it. That’s how it should be. When words come too easily, it’s because we haven’t delved deeply enough (you’ll see that scientists can be guilty of that too). - loc: [191](kindle://book?action=open&asin=B07H9HNDYL&location=191) - -***Note:*** I've been thinkibg lately about if (really how) the human need for language inhibits our ability to understand things. If your mother tongue does not have the words for something then how much harder is it to understand? What is the instance of bilingualism among Nobel-winning scientists? - ---- -and there are plenty of accessible books on the subject.*2 - loc: [208](kindle://book?action=open&asin=B07H9HNDYL&location=208) - - ---- -Best of all for its narrative value is the largely good-natured but trenchant dispute that rumbled on for decades between Einstein and Bohr about what it all meant – about the nature of reality. This is indeed a superb story, and if you haven’t heard it before then you should.*1 - loc: [256](kindle://book?action=open&asin=B07H9HNDYL&location=256) - - ---- -The best illustration I know of that quantization is rather incidental to quantum theory is found in the book Quantum Mechanics: The Theoretical Minimum, based on a series of lectures that Leonard Susskind, a professor of theoretical physics at Stanford University, gave to undergraduates, which were written up with the help of the writer Art Friedman. The book is described as being ‘for anyone who ever regretted not taking physics at university, who knows a little but would like to know more’. - loc: [337](kindle://book?action=open&asin=B07H9HNDYL&location=337) - -***Note:*** I'm less than 10% into this book and already have 3 more to read! - ---- -Quantum objects are what they are, and we have no reason to suppose that ‘what they are’ changes in any meaningful way depending on how we try to look at them. Rather, all we can say is that what we measure sometimes looks like what we would expect to see if we were measuring discrete little ball-like entities, while in other experiments it looks like the behaviour expected of waves of the same kind as those of sound travelling in air, or that wrinkle and swell on the sea surface. So the phrase ‘wave–particle duality’ doesn’t really refer to quantum objects at all, but to the interpretation of experiments – which is to say, to our human-scale view of things. - loc: [392](kindle://book?action=open&asin=B07H9HNDYL&location=392) - - ---- -What makes the quantum situation of radioactive decay any different from this classical (and, forgive me, rather unfortunate) analogy of childbirth? We have every reason to suppose that, if we were able to monitor the biology of each pregnant mother and baby sufficiently closely, we could understand exactly why the respective birth process started when it did – perhaps some hormonal threshold was reached, say. But for radioactive decay, there is nothing you can monitor to explain why a particular atom decayed when it did. There is nothing we can call a reason. - loc: [606](kindle://book?action=open&asin=B07H9HNDYL&location=606) - -***Note:*** At this point in knowledge, sure. But there has to be an explanation, a trigger. We can't just live in a world where things happen with no cause. If that was the case, then what is the source of the wave function? - ---- -So, said Bohr, we had better just stop there. As he put it: There is no quantum world. There is only an abstract quantum physical description. It is wrong to think that the task of physics is to find out how nature is. Physics concerns what we can say about nature. - loc: [777](kindle://book?action=open&asin=B07H9HNDYL&location=777) - - ---- -At face value that sounds crazy. Why make a mathematical theory like quantum mechanics if you don’t think it can tell you about the system it is supposed to describe? But Bohr argued that quantum theory tells you something more meaningful – indeed, the only thing that can be meaningful. It tells you what you will find when you try to investigate that system. It tells you about measurements. - loc: [783](kindle://book?action=open&asin=B07H9HNDYL&location=783) - - ---- -the early days of this new physics, the problem was often debated in terms of the ‘role of the observer’. That the observer had a role at all was deeply troubling, because it seemed to challenge the very concept of science. If what we see depends on what questions we ask, whither then the idea of an objective world, governed by rules that pertain independently of our attempts to figure them out? As Heisenberg put it, science had ceased to be a way of peeking unnoticed at the world, and instead had become ‘an actor in [the] interplay between man and nature’. - loc: [813](kindle://book?action=open&asin=B07H9HNDYL&location=813) - -***Note:*** Like how what we get from an LLM depends on not just what questions we ask it but how. - ---- -But measurement does something else. It ‘collapses’ (Heisenberg’s original word was ‘reduces’) those possibilities, expressed in the wavefunction, to just one. Suppose that, before the measurement, the probabilities of finding some property of a quantum object with the values corresponding to states A, B and C are 10%, 70% and 20% respectively. When we make a single measurement on the object, we might find that we get the result C. What happened to A and B? We are forced to assert that the probabilities have now changed: that for C it is 100%, and for A and B it is zero. What’s more, we can’t get A and B back: if we repeat the measurement, we’ll keep getting C.*2 - loc: [1,043](kindle://book?action=open&asin=B07H9HNDYL&location=1043) - -***Note:*** We will keep getting C *unless and until* we reset the quantum object in its original state. How in tge world does that happen?? - ---- -To Bohr, wavefunction collapse was virtually emblematic of the distinction between the unitary quantum world and the everyday reality in which we make observations and measurements. Measurements must be classical by definition: they require some big apparatus with which humans can interact. From our perspective the world is made up of phenomena – things happen – and a phenomenon only exists when it has been measured. Wavefunction collapse is simply a name we give to the process by which we turn quantum states into observed phenomena. Wavefunction collapse is then a generator of knowledge: it is not so much a process that gives us the answers, but is the process by which answers are created. The outcome of that process can’t, in general, be predicted with certainty, but quantum mechanics gives us a method for calculating the probabilities of particular outcomes. That’s all we can ask for. - loc: [1,062](kindle://book?action=open&asin=B07H9HNDYL&location=1062) - - ---- -Bohr could be stubborn and dogmatic as well as rather cryptic. But he deserves praise for defining the limits of what can be said with confidence. Some suspect that his intuition surpassed anything he could justify mathematically, or indeed semantically. As von Weizsäcker said, ‘Bohr was essentially right but he did not know why.’ - loc: [1,110](kindle://book?action=open&asin=B07H9HNDYL&location=1110) - - ---- -But nevertheless the notion of a universal wavefunction is popular with cosmologists, for the perfectly valid reason that in the earliest moments of the Big Bang the entire universe was smaller than an atom and surely needs to be considered, in that moment, a quantum-mechanical entity. - loc: [1,222](kindle://book?action=open&asin=B07H9HNDYL&location=1222) - -***Note:*** Should we just accept that as fact? - ---- -Quantum mechanics generally assumes that quantum states exist in some meaningful sense, and that the math tells us what we can know about those states. But in QBism there are no objective states. Rather, according to Chris Fuchs, ‘quantum states represent observers’ personal information, expectations and degrees of belief’. This view, he says, ‘allows one to see all quantum measurement events as little “moments of creation”, rather than as revealing anything pre-existent’. - loc: [1,303](kindle://book?action=open&asin=B07H9HNDYL&location=1303) - -***Note:*** What if every time we collapse a wave function we are setting off a big bang in another universe? - ---- -The main thing you need to know about entanglement is this: it tells us that a quantum object may have properties that are not entirely located on that object. - loc: [1,704](kindle://book?action=open&asin=B07H9HNDYL&location=1704) - - ---- -But this locality is just what quantum entanglement undermines – which is why ‘spooky action at a distance’ is precisely the wrong way to look at it. We can’t regard particle A and particle B in the EPR experiment as separate entities, even though they are separated in space. As far as quantum mechanics is concerned, entanglement makes them both parts of a single object. Or to put it another way, the spin of particle A is not located solely on A in the way that the redness of a cricket ball is located on the cricket ball. In quantum mechanics, properties can be non-local. - loc: [1,931](kindle://book?action=open&asin=B07H9HNDYL&location=1931) - - ---- -What in fact we’re dealing with here is another kind of quantum superposition. We’ve seen that superposition refers to a situation in which a measurement on a quantum object could produce two or more possible outcomes, but we don’t know which it will be, only their relative probabilities. Entanglement is that same idea applied to two or more particles: a superposition of the state in which particle A has spin up and B spin down, say, and the state with the opposite configuration. Although the particles are separated, they must be described by a single wavefunction. We can’t untangle that wavefunction into some combination of two single-particle wavefunctions. - loc: [1,936](kindle://book?action=open&asin=B07H9HNDYL&location=1936) - -***Note:*** If we could change the spin of an entangled particle, would that change the spin of the second particle or would in disentangle the particles? Can you even disentangle particles? Can you have an entangled triad of particles? - ---- -Kochen and Specker pointed out that the outcomes of quantum measurements may depend on their context. This means something subtly different from the fact that different types of experiment on what seems like the same system (a double-slit experiment with or without a ‘path detector’, say) give different results. It means that if you look at a quantum object through different windows, you see different things. If you want to count the number of black and white balls in a jar, it doesn’t matter if you count the black ones or the white ones first, or if you tot them up in rows of five, or if you separate the two colours into piles and weigh them. You’ll get the same answer. But in quantum mechanics, even when you ask the same question (‘How many black and white balls are there?’), the answer you get may depend on how the measurement is done. - loc: [2,009](kindle://book?action=open&asin=B07H9HNDYL&location=2009) - -***Note:*** Here is this similarity to interfacing with LLMs again - ---- -The Kochen–Specker theorem stipulates what follows from this dependence on context. In effect it is another corollary of the way that, in quantum mechanics, what we choose not to measure may leave an imprint on what we do measure. - loc: [2,018](kindle://book?action=open&asin=B07H9HNDYL&location=2018) - - ---- -An Assyrian seer, unwilling to let his young daughter be wed to suitors who he considered unworthy, set them a task. He presented them with three closed boxes in a row, each of which might or might not have a gem inside it. Any prediction will identify at least two boxes that are guessed to be both empty, or two that are both filled. (Think about it for a moment and you’ll see that this must be so.) The suitors were asked to open those two, and if they were right then they would have the daughter’s hand. But they were never right! One of the opened boxes was always empty while the other contained a gem. How could that be? Surely chance alone should guarentee that someone would guess right some time? Finally the daughter, who was getting impatient to be wed, intervened by opening up the boxes for the rather dishy son of a prophet. But she didn’t open the two he’d predicted to be both full or both empty. Instead she opened one he’d guessed was filled, and one he’d guessed was empty. And both guesses proved correct. After some weak objection, the seer accepted that the suitor had made two correct guesses, and the daughter was married. The seer had foiled previous suitors because his boxes were quantum boxes, and he had entangled them to create correlations such that, if one opened box was filled, the other would be empty and vice versa. That made it impossible to satisfy the seer’s challenge and show you’d guessed right. But the daughter was able to reveal a correct guess by interrogating the same system with a different set of measurements. And that is what quantum contextuality is like.* - loc: [2,021](kindle://book?action=open&asin=B07H9HNDYL&location=2021) - - ---- -You can’t say, to make a macroscopic analogy, ‘the ball is red’, but only ‘the ball is red when looked at through the round window’. It really is red under these conditions (to the extent that we can ever say how something really is). But equally, it really is green through the square window. Yes, but what colour is it really? According to Kochen and Specker, you can’t improve on the answer I’ve just given.* - loc: [2,043](kindle://book?action=open&asin=B07H9HNDYL&location=2043) - -***Note:*** Is this possible to create as an art piece? - ---- -It is this waviness that gives rise to distinctly quantum phenomena like interference, superposition and entanglement. These behaviours become possible when there is a well-defined relationship between the quantum ‘waves’: in effect, when they are in step. This co-ordination is called coherence. - loc: [2,129](kindle://book?action=open&asin=B07H9HNDYL&location=2129) - -***Note:*** coherence is a well-defined relationship between wave functions of 2 quantum objects - ---- -if the quantum wavefunctions of two states are not coherent, they cannot interfere, nor can they maintain a superposition. A loss of coherence (decoherence) therefore destroys these fundamentally quantum properties, and the states behave more like distinct classical systems. - loc: [2,134](kindle://book?action=open&asin=B07H9HNDYL&location=2134) - - ---- -Quantum coherence is essentially what permits ‘quantumness’. - loc: [2,140](kindle://book?action=open&asin=B07H9HNDYL&location=2140) - - ---- -There is no reason (that we yet know of) why in principle objects cannot remain in coherent quantum states no matter how big they are – provided that no measurement is made on them. But it seems that measurement somehow does destroy quantum coherence, forcing us to speak of the wavefunction as having ‘collapsed’. If we can understand how measurement unravels coherence then we would be able to bring measurement itself within the scope of quantum theory, rather than making it a boundary where the theory stops. - loc: [2,141](kindle://book?action=open&asin=B07H9HNDYL&location=2141) - -***Note:*** Measurement of quantum activity == Tower of Babel. Arrogance of Man, smited. - ---- -The truth is that they don’t. Quantum superpositions of states aren’t fragile. On the contrary, they are highly contagious and apt to spread out rapidly. And that is what seems to destroy them. If a quantum system in a superposed state interacts with another particle, the two become linked into a composite superposition. That, we saw earlier, is exactly what entanglement is: a superposed state of two particles, whose interaction has turned them into a single quantum entity. - loc: [2,159](kindle://book?action=open&asin=B07H9HNDYL&location=2159) - - ---- -In theory there is no end to this process. That entangled air molecule hits another, and the second molecule gets captured in an entangled state too. As time passes, the initial quantum system becomes more and more entangled with its environment. In effect, we then no longer have a well-defined quantum system embedded in an environment. Rather, system and environment have merged into a single superposition. - loc: [2,166](kindle://book?action=open&asin=B07H9HNDYL&location=2166) - - ---- -Decoherence is what destroys the possibility of observing macroscopic superpositions – including Schrödinger’s live/dead cat. And this has nothing to do with observation in the normal sense: we don’t need a conscious mind to ‘look’ in order to ‘collapse the wavefunction’. All we need is for the environment to disperse the quantum coherence. This happens with extraordinary efficiency – it’s probably the most efficient process known to science. And it is very clear why size matters here: there is simply more interaction with the environment, and therefore faster decoherence, for larger objects. - loc: [2,218](kindle://book?action=open&asin=B07H9HNDYL&location=2218) - - ---- -Here, then, is a concrete way to look at that most puzzling of quantum mysteries, which is that what we measure depends on how we look – on the context of the measurement. - loc: [2,412](kindle://book?action=open&asin=B07H9HNDYL&location=2412) - - ---- -Charles Bennett and Gilles Brassard pointed out in the early 1980s that quantum correlations between entangled states can be exploited to send information encoded in qubits that is immune to eavesdropping. The signal could never be intercepted and read without the tampering being detected. This was the start of the technology known as quantum cryptography. The idea goes like this. Alice encodes her message in two entangled qubits – 1s and 0s represented by, say, the polarization states of pairs of photons. One photon from each entangled pair is then sent to Bob, who measures their states and so decodes the message. - loc: [2,817](kindle://book?action=open&asin=B07H9HNDYL&location=2817) - - ---- -I’ll say it upfront: no one fully understands how quantum computers work. - loc: [2,872](kindle://book?action=open&asin=B07H9HNDYL&location=2872) - -***Note:*** Thank God! - ---- -But Bohr and colleagues didn’t bring wavefunction collapse into the picture just to make things difficult. They did it because that’s what seems to happen. When we make a measurement, we really do get just one result out of the many that quantum mechanics offers. Wavefunction collapse seemed to be demanded in order to connect the theory to reality. So what Everett was saying was that this isn’t, after all, what reality is. It’s not quantum mechanics that is at fault, but our concept of reality. We only think that there’s a single outcome of a measurement. But in fact all of them occur. We only see one of those realities, but the others have a separate physical existence too. - loc: [2,989](kindle://book?action=open&asin=B07H9HNDYL&location=2989) - -***Note:*** If MWI is true then how do we deal with the law of conservation of matter? is anything other than possibilities "lost" when the wave function collapses? - ---- -Luis Jorge Borges’ story ‘The Garden of Forking Paths’, - loc: [3,035](kindle://book?action=open&asin=B07H9HNDYL&location=3035) - - ---- -The issue is whether, like Bohr, you believe that quantum mechanics provides a prescription for evaluating the possible outcomes we might observe when we look at the quantum world, or whether you regard the Schrödinger equation as an inviolable and universal law that describes – in some sense is – reality. - loc: [3,154](kindle://book?action=open&asin=B07H9HNDYL&location=3154) - - ---- -Can it be right that the laws of Newtonian mechanics for our classical world, expressed in concise, prosaic words and sentences, must give way to something with the forbidding, abstract mathematical complexity of the quantum axioms? Or is this just because we don’t quite know what we’re talking about? When someone explains something in a complicated way, it’s often a sign that they don’t really understand it. - loc: [3,334](kindle://book?action=open&asin=B07H9HNDYL&location=3334) - - ---- diff --git a/02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md b/02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md deleted file mode 100644 index 4e2692a3..00000000 --- a/02_Areas/Test/Caliban's War (The Expanse Book 2) - James S. A. Corey.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: "Caliban's War (The Expanse Book 2)" -author: "James S. A. Corey" -asin: B005SCRR1A -highlights: 5 -lastSynced: 2025-10-20T03:00:39.213Z ---- - -# Caliban's War (The Expanse Book 2) - - -## Highlights - -“You live in your own world, dear one.” “I do,” Arjun agreed, running his thumb across the hand terminal’s screen. He looked up. “You don’t mind, do you?” “I love you for it. Stay here. Read about post-lyricism.” “What are you going to do?” “The same thing as always. Try to keep civilization from blowing up while the children are in it.” - - ---- -“This is what I’m talking about. The intensity of your feelings isn’t evidence. - - ---- -Prax looked at the mass of data being sent to Alex and said, “Why fly at all? Couldn’t the ship just use this data to do the docking itself?” “Why fly?” Alex repeated with a laugh. “ ’Cuz it’s fun, Doc. Because it’s fun.” - - ---- -‘If life transcends death, then I will seek for you there. If not, then there too,’ ” she said. “It’s a fucking haiku. That man has a one-track mind and one train on it. Poetry. Save me from poetry.” - - ---- -He also wasn’t being fair to Avasarala. It wasn’t about making him take her orders, not really. It was about being in a situation that she was utterly untrained for, where she was the least useful person in the room and trying to assert some control. Trying to reshape the space around her to fit with her mental image of herself. - - ---- diff --git a/02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md b/02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md deleted file mode 100644 index e8a3b617..00000000 --- a/02_Areas/Test/Cibola Burn (The Expanse Book 4) - James S. A. Corey.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "Cibola Burn (The Expanse Book 4)" -author: "James S. A. Corey" -asin: B00FPQA4F0 -highlights: 5 -lastSynced: 2025-10-20T02:59:40.160Z ---- - -# Cibola Burn (The Expanse Book 4) - - -## Highlights - -“I’m wondering what I did to deserve all this,” she said. “I’m about to see the first genuinely alien biosphere. I’m about to learn things about evolution that were literally impossible to know until now. I must have been a very, very good person in a past life.” - - ---- -“A vast new frontier has opened up for us. We have the chance to create a new society, with untold riches beyond every gate. But this world has treasure, so instead of figuring out the right way to divide up the damned galaxy, we’ll fight over the first crumbs we find.” - - ---- -“What the hell are you doing?” she asked. “What am I doing? You think that’s the question?” “You know that her husband—” “I don’t know a damned thing, Elvi. Neither do you. I’m rich in interpretation and poor in datasets, just the same as you.” “You think… you think it’s not…” - - ---- -“No,” Alex said, a deep frown cutting into his forehead. “A person can fail the people they love just by being who they are. I’m who I am, and it wasn’t what my wife wanted me to be, and somethin’ had to break. You decided to do what you did down on that planet, and it put you up here with me instead of with your family.” Alex leaned forward, grabbing Basia’s hands in his own. “It’s still on you. I will never live down not being the person my wife needed after she spent twenty years waitin’ for me. I can never make that right. Don’t go feelin’ sorry for yourself. You fucked up. You failed the people you love. They’re payin’ the price for it right now and you demean them every second you don’t own that shit.” - - ---- -Earth alone had managed five kingdoms of life. Six, if you agreed with the Fityani hypothesis. - -***Note:*** look that up - ---- diff --git a/02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md b/02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md deleted file mode 100644 index 70b8a559..00000000 --- a/02_Areas/Test/Leviathan Wakes (The Expanse Book 1) - James S. A. Corey.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "Leviathan Wakes (The Expanse Book 1)" -author: "James S. A. Corey" -asin: B0047Y171G -highlights: 6 -lastSynced: 2025-10-20T03:00:41.109Z ---- - -# Leviathan Wakes (The Expanse Book 1) - - -## Highlights - -The joke was that Ceres didn’t have laws—it had police. - - ---- -It was easy to make fun of the marines when they weren’t listening. In Holden’s navy days, making fun of jarheads was as natural as cussing. But four marines had died getting him off the Donnager, and three of them had made a conscious decision to do so. Holden promised himself that he’d never make fun of them again. - - ---- -The beautiful thing about losing your illusions, he thought, was that you got to stop pretending. All the years he’d told himself that he was respected, that he was good at his job, that all his sacrifices had been made for a reason fell away and left him with the clear, unmuddied knowledge that he was a functional alcoholic who had pared away everything good in his own life to make room for anesthetic. Shaddid thought he was a joke. Muss thought he was the price she paid not to sleep with someone she didn’t like. The only one who might have any respect for him at all was Havelock, an Earther. It was peaceful, in its way. - - ---- -“You can’t just throw information at people,” Miller said. “You have to know what it means. What it’s going to do. - - ---- -“Let me get this straight,” he said. “You’ll give me all the kingdoms of the Earth if I just bow down and do one act of worship for you?” Dresden cocked his head. “I don’t know the reference.” - - ---- -Mars would survive, for a while. Pockets of the Belt would hold out even longer, probably. They had a culture of making do, surviving on scraps, living on the bleeding edge of their resources. But in the end, without Earth, everything would eventually die. Humans had been out of the gravity well a long time. Long enough to have developed the technology to cut that umbilical cord, but they’d just never bothered to do it. Stagnant. Humanity, for all its desire to fling itself into every livable pocket it could reach, had become stagnant. Satisfied to fly around in ships built half a century before, using technology that hadn’t changed in longer than that. - - ---- diff --git a/02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md b/02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md deleted file mode 100644 index bc565ef6..00000000 --- a/02_Areas/Test/My Life with the Saints (10th Anniversary Edition) - James Martin and John L Allen.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: "My Life with the Saints (10th Anniversary Edition)" -author: "James Martin and John L Allen" -asin: B01LA4CEGM -highlights: 7 -lastSynced: 2025-10-20T03:00:37.330Z ---- - -# My Life with the Saints (10th Anniversary Edition) - - -## Highlights - -As C. S. Lewis writes in Mere Christianity: “How monotonously alike all the great tyrants and conquerors have been: how gloriously different are the saints.” - - ---- -The best way to come to know St. Thomas More, I believe, is to watch A Man for All Seasons. - - ---- -Enter the idea of studying business. At the time, I had a vague idea that a business major would enable me to land a job in the “business end” of any number of fields, such as the “business end” of English (publishing) or the “business end” of art (managing a museum or gallery). At the very least, a business degree would likely lead to a high-paying job after graduation, and this, I reasoned, would make me happy. On this point, everyone—my family, my friends, my high school guidance counselor—agreed. The problem was that whenever I considered “earning a living,” I thought mostly about the “earning” and nothing about the “living.” - - ---- -Wharton’s reputation in the corporate world made finding a job ridiculously easy. At the beginning of senior year, hundreds of corporate recruiters flocked to the school to conduct interviews: in a few months I had received a number of excellent offers. After narrowing them down to three or four, I settled on a corporate training program at General Electric in New York City. Once again, almost no reflection was involved in my decision. To use some accounting terminology, I simply went along with “generally accepted principles.” For example, what should you do at the end of business school? Interview for jobs. Which job offer should you accept? The one with the highest salary. The more important questions were the ones that no one asked me, or rather, that I failed to ask myself: What do you desire in life? And what does God desire for you? - - ---- -I looked down at the bright green grass and thought about Merton’s strange, complicated, and contradictory life. And about my favorite lines from Walt Whitman, which come near the end of “Song of Myself”: I bequeath myself to the dirt to grow from the grass I love. If you want me again look for me under your boot-soles. - - ---- -the vanities of the world,” particularly concerning his physical appearance. He seems also to have been a ladies’ man. At least that’s how he fancied himself. He was definitely a rake. It was rumored that he fathered an illegitimate child. (There is some evidence to support this, but it is inconclusive.) And he may be the only canonized saint with a notarized police record, for nighttime brawling with intent to inflict serious harm. - -***Note:*** Ignatious of Loyola - ---- -In Paris he also completed what later become known as The Spiritual Exercises, a handbook of practices on prayer, on the human condition, on God’s love, and on the life of Jesus, all designed to help people draw closer to God. Iñigo also led his new roommate, Francisco Javier, through these exercises. His friend would later become better known, of course, as St. Francis Xavier, one of the Church’s great missionaries. - - ---- diff --git a/02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md b/02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md deleted file mode 100644 index 15846555..00000000 --- a/02_Areas/Test/Nemesis Games (The Expanse Book 5) - James S. A. Corey.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: "Nemesis Games (The Expanse Book 5)" -author: "James S. A. Corey" -asin: B00O7X626W -highlights: 2 -lastSynced: 2025-10-20T02:59:38.069Z ---- - -# Nemesis Games (The Expanse Book 5) - - -## Highlights - -At least he knows there’s a problem. Realizing you’ve got shit on your fingers is the first step toward washing your hands.” - loc: [2,763](kindle://book?action=open&asin=B00O7X626W&location=2763) - - ---- -Thing is, we’re humans. We’re tribal. More settled things are, the bigger your tribe is. All the people in your gang, or all the people in your country. All the ones on your planet. Then the churn comes, and the tribe gets small again.” - loc: [4,165](kindle://book?action=open&asin=B00O7X626W&location=4165) - - ---- diff --git a/02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md b/02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md deleted file mode 100644 index 3167282b..00000000 --- a/02_Areas/Test/Persepolis Rising (The Expanse Book 7) - James S. A. Corey.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: "Persepolis Rising (The Expanse Book 7)" -author: "James S. A. Corey" -asin: B06XKN9G27 -highlights: 6 -lastSynced: 2025-10-20T02:59:31.761Z ---- - -# Persepolis Rising (The Expanse Book 7) - - -## Highlights - -there had been a time when riding the lift instead of climbing the ladder would have felt like admitting defeat, even if the ship was burning ten times that hard. But for the last couple years Bobbie’s joints had been giving her trouble, and proving to herself that she could make the climb had stopped mattering as much. It seemed to her that the real sign you were getting old was when you stopped needing to prove you weren’t getting old. - loc: [426](kindle://book?action=open&asin=B06XKN9G27&location=426) - - ---- -“All right, then,” Bobbie said. “The way I see it, the next step is find someone who can get messages back to the union. Or Earth-Mars. See if there’s anyone out there with a plan, or if we’re going to have to make one up on our own.” “We can do that,” Amos said. “Shouldn’t be hard.” “You sure?” Alex said. “This is Medina Station under occupation by a bunch of splinter Martian military expats. It’s not Baltimore.” Amos’ smile was as placid as always. “Everywhere’s Baltimore.” - loc: [3,003](kindle://book?action=open&asin=B06XKN9G27&location=3003) - - ---- -If they kill us all, Drummer thought, this will be why. Not their technology, not their strategy, not the invisible cycle of history. It’ll be our inability to do anything without five committee meetings to talk about it. - loc: [3,771](kindle://book?action=open&asin=B06XKN9G27&location=3771) - - ---- -It was all becoming normal. He could see it in the way the clerk served up the terrible, terrible coffee. He could hear it in the conversations at the nearby tables. It showed on the screens and in the gaits of the people walking by. Panic and alarm were exhausting. He was exhausted by them, and Medina was exhausted too. It was already shifting into its new routine. Checkpoints, yes. Armed security, yes. All the theater of dominance and control and nothing to undercut that narrative. - loc: [4,044](kindle://book?action=open&asin=B06XKN9G27&location=4044) - - ---- -controlling don’t have a voice in how you control them. As long as everyone’s on the same page, things may be great, but when there’s a question, you win. Right?” “There has to be a way to come to a final decision.” “No, there doesn’t. Every time someone starts talking about final anythings in politics, that means the atrocities are warming up. Humanity has done amazing things by just muddling through, arguing and complaining and fighting and negotiating. It’s messy and undignified, but it’s when we’re at our best, because everyone gets to have a voice in it. Even if everyone else is trying to shout it down. Whenever there’s just one voice that matters, something terrible comes out of it.” - loc: [5,949](kindle://book?action=open&asin=B06XKN9G27&location=5949) - - ---- -“For what it’s worth, I don’t disagree with you. These people are scum. They don’t deserve or understand what we’ve brought them. For me, I think they never will. But their children might. Their grandchildren or their great-grandchildren. The story of Medina will be that Governor Singh mismanaged the station, lost his ship to a band of malcontents, lost his perspective. And when he let his wounded pride exceed the mandates of the high consul’s directives, he was removed for the protection of the everyday citizens in his care. You see the difference? If you kill an insurgent, you’re the enemy of all their friends. All their family. And then there’s an expectation. Precedent. Enemies for generations. Forever. If you kill your own—even the highest among your own—to protect someone powerless, they remember that too. It sows gratitude. It sows trust. Generations from now your sacrifice will lead to the peace, prosperity, and fellow feeling among all humanity.” - loc: [7,853](kindle://book?action=open&asin=B06XKN9G27&location=7853) - - ---- diff --git a/README.md b/README.md index b53a1c2a..07003fb3 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ Pre-configured AI assistants ready to use: **Kindle Integration:** -- `kindle-sync` - Sync your Kindle highlights (interactive: choose 10, 25, 50, or all books) +- `kindle-sync` - Sync your Kindle highlights (interactive: choose 10, 25, 50, + or all books) Run with: `/[command-name]` in Claude Code @@ -267,7 +268,8 @@ On first run, you'll choose where to save highlights (defaults to - **Smart authentication**: Browser opens automatically when needed - **Formatted notes**: Each book becomes a Markdown file with YAML frontmatter, tags, and Kindle app deep links -- **Auto-retry**: If authentication expires mid-sync, automatically re-authenticates +- **Auto-retry**: If authentication expires mid-sync, automatically + re-authenticates **Quick Start:** From 443560d609268e1d8f311dbfc78c3f531d4e682b Mon Sep 17 00:00:00 2001 From: Ryan Anderson <ryan@alephic.com> Date: Sun, 19 Oct 2025 23:56:54 -0400 Subject: [PATCH 3/8] updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c589490e..8f221fae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +### Added + +- New `kindle-sync` command for syncing a user's kindle highlights and notes to their vault + ## [0.13.1] - 2025-10-13 ### Fixed From a73b281b5713577e08fb3ae6370a61c82d16c33b Mon Sep 17 00:00:00 2001 From: Ryan Anderson <ryan@alephic.com> Date: Wed, 22 Oct 2025 23:02:44 -0400 Subject: [PATCH 4/8] Updated kindle-sync feature to be TypeScript --- .claude/commands/kindle-sync.md | 26 +- .scripts/kindle/{auth.js => auth.ts} | 18 +- .scripts/kindle/{config.js => config.ts} | 47 +- .scripts/kindle/lib/config.js | 52 - .scripts/kindle/lib/config.ts | 79 + .scripts/kindle/lib/{errors.js => errors.ts} | 12 +- .../kindle/lib/{renderer.js => renderer.ts} | 70 +- .../kindle/lib/{scraper.js => scraper.ts} | 87 +- .scripts/kindle/lib/{utils.js => utils.ts} | 18 +- .../lib/{validation.js => validation.ts} | 14 +- .scripts/kindle/{sync.js => sync.ts} | 96 +- CHANGELOG.md | 3 +- package-lock.json | 1956 +++++++++++------ package.json | 12 +- tsconfig.json | 21 + 15 files changed, 1713 insertions(+), 798 deletions(-) rename .scripts/kindle/{auth.js => auth.ts} (87%) rename .scripts/kindle/{config.js => config.ts} (73%) delete mode 100644 .scripts/kindle/lib/config.js create mode 100644 .scripts/kindle/lib/config.ts rename .scripts/kindle/lib/{errors.js => errors.ts} (78%) rename .scripts/kindle/lib/{renderer.js => renderer.ts} (60%) rename .scripts/kindle/lib/{scraper.js => scraper.ts} (78%) rename .scripts/kindle/lib/{utils.js => utils.ts} (77%) rename .scripts/kindle/lib/{validation.js => validation.ts} (81%) rename .scripts/kindle/{sync.js => sync.ts} (81%) create mode 100644 tsconfig.json diff --git a/.claude/commands/kindle-sync.md b/.claude/commands/kindle-sync.md index 6ac5b3de..d15f13f8 100644 --- a/.claude/commands/kindle-sync.md +++ b/.claude/commands/kindle-sync.md @@ -18,8 +18,8 @@ Ask the user: ``` This is your first time syncing Kindle highlights! Where would you like to save them? -Press Enter for default: 03_Resources/Kindle Highlights -Or type a custom path (e.g., 02_Areas/Reading) +1. 03_Resources/Kindle Highlights (default) +2. A custom path (e.g., 02_Areas/Reading) Your choice: ``` @@ -67,24 +67,24 @@ Based on their response and whether it's first run: **If first run (no config):** - If they answer **A** or **10**: Run - `node .scripts/kindle/sync.js --limit 10 --output "[their-folder-choice]"` + `pnpm kindle:sync --limit 10 --output "[their-folder-choice]"` - If they answer **B** or **25**: Run - `node .scripts/kindle/sync.js --limit 25 --output "[their-folder-choice]"` + `pnpm kindle:sync --limit 25 --output "[their-folder-choice]"` - If they answer **C** or **50**: Run - `node .scripts/kindle/sync.js --limit 50 --output "[their-folder-choice]"` + `pnpm kindle:sync --limit 50 --output "[their-folder-choice]"` - If they answer **D** or **all**: Run - `node .scripts/kindle/sync.js --all --output "[their-folder-choice]"` + `pnpm kindle:sync --all --output "[their-folder-choice]"` - If they answer **E** or a number: Run - `node .scripts/kindle/sync.js --limit [number] --output "[their-folder-choice]"` + `pnpm kindle:sync --limit [number] --output "[their-folder-choice]"` **If NOT first run (config exists):** -- If they answer **A** or **10**: Run `node .scripts/kindle/sync.js --limit 10` -- If they answer **B** or **25**: Run `node .scripts/kindle/sync.js --limit 25` -- If they answer **C** or **50**: Run `node .scripts/kindle/sync.js --limit 50` -- If they answer **D** or **all**: Run `node .scripts/kindle/sync.js --all` +- If they answer **A** or **10**: Run `pnpm kindle:sync --limit 10` +- If they answer **B** or **25**: Run `pnpm kindle:sync --limit 25` +- If they answer **C** or **50**: Run `pnpm kindle:sync --limit 50` +- If they answer **D** or **all**: Run `pnpm kindle:sync --all` - If they answer **E** or a number: Run - `node .scripts/kindle/sync.js --limit [number]` + `pnpm kindle:sync --limit [number]` Note: If user just presses Enter for default folder, use `03_Resources/Kindle Highlights` as the path. @@ -153,7 +153,7 @@ Use the `--output` flag to save to a different folder just once (without changing config): ```bash -node .scripts/kindle/sync.js --limit 10 --output "01_Projects/Current Reading" +pnpm kindle:sync --limit 10 --output "01_Projects/Current Reading" ``` ## Tips diff --git a/.scripts/kindle/auth.js b/.scripts/kindle/auth.ts similarity index 87% rename from .scripts/kindle/auth.js rename to .scripts/kindle/auth.ts index 6fabde0f..d003ecab 100644 --- a/.scripts/kindle/auth.js +++ b/.scripts/kindle/auth.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import path from 'node:path' import { fileURLToPath } from 'node:url' -import { chromium } from 'playwright' +import { type BrowserContext, chromium, type Page } from 'playwright' import { NOTEBOOK_URL } from './lib/config.js' @@ -11,7 +11,7 @@ const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth') /** * Check if authentication is successful by verifying page state */ -async function checkAuthenticationSuccess(page) { +async function checkAuthenticationSuccess(page: Page): Promise<boolean> { try { // If library loads, we're authenticated const count = await page.locator('#kp-notebook-library').count() @@ -24,7 +24,10 @@ async function checkAuthenticationSuccess(page) { /** * Poll the page until authentication is successful or timeout */ -async function waitForAuthenticationSuccess(page, timeoutMs = 300000) { +async function waitForAuthenticationSuccess( + page: Page, + timeoutMs = 300000, +): Promise<boolean> { const startTime = Date.now() const pollInterval = 2000 // Check every 2 seconds @@ -50,12 +53,12 @@ async function waitForAuthenticationSuccess(page, timeoutMs = 300000) { /** * Interactive authentication - opens browser for user to log in */ -async function main() { +async function main(): Promise<void> { console.log('🔐 Kindle Authentication\n') console.log('📁 Session will be saved to:', AUTH_DIR) console.log('⏱️ Timeout: 5 minutes\n') - const ctx = await chromium.launchPersistentContext(AUTH_DIR, { + const ctx: BrowserContext = await chromium.launchPersistentContext(AUTH_DIR, { headless: false, viewport: { height: 900, width: 1000 }, // Position on right side of screen (works for most screen sizes) @@ -116,7 +119,8 @@ async function main() { console.log('Next steps:') console.log(' • Run /kindle-sync to sync your highlights') } catch (error) { - console.error('\n❌ Authentication failed:', error.message) + const errorMessage = error instanceof Error ? error.message : String(error) + console.error('\n❌ Authentication failed:', errorMessage) console.log('\nTroubleshooting:') console.log(' • Make sure you fully logged in to Amazon') console.log(' • Verify your book library is visible in the browser') @@ -126,7 +130,7 @@ async function main() { } } -main().catch((error) => { +main().catch((error: unknown) => { console.error('❌ Authentication failed:', error) process.exit(1) }) diff --git a/.scripts/kindle/config.js b/.scripts/kindle/config.ts similarity index 73% rename from .scripts/kindle/config.js rename to .scripts/kindle/config.ts index 12ab8503..6c1e9d16 100644 --- a/.scripts/kindle/config.js +++ b/.scripts/kindle/config.ts @@ -9,27 +9,39 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) const CONFIG_DIR = path.resolve(__dirname, '../../.kindle') const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json') +export interface KindleConfig { + addTags: string[] + lastSync: null | string + outputFolder: string + overwrite: boolean + templatePath: string +} + /** * Default configuration */ -export const DEFAULT_CONFIG = { +export const DEFAULT_CONFIG: KindleConfig = { addTags: ['kindle', 'highlights', 'books'], lastSync: null, outputFolder: '03_Resources/Kindle Highlights', - overwrite: false, + overwrite: true, templatePath: '.scripts/kindle/templates/kindle-note.md.hbs', } /** * Load configuration from file, or create with defaults if it doesn't exist */ -export async function loadConfig() { +export async function loadConfig(): Promise<KindleConfig> { try { const content = await fs.readFile(CONFIG_PATH, 'utf-8') - const config = JSON.parse(content) + const parsed: unknown = JSON.parse(content) + // Safe to cast as we merge with defaults, ensuring all required fields are present + const config = ( + parsed && typeof parsed === 'object' ? parsed : {} + ) as Partial<KindleConfig> // Merge with defaults to ensure all fields are present return { ...DEFAULT_CONFIG, ...config } - } catch (_error) { + } catch (_error: unknown) { // Config doesn't exist, create it with defaults console.log('📝 Creating default configuration...') await saveConfig(DEFAULT_CONFIG) @@ -40,7 +52,7 @@ export async function loadConfig() { /** * Save configuration to file */ -export async function saveConfig(config) { +export async function saveConfig(config: KindleConfig): Promise<void> { await fs.mkdir(CONFIG_DIR, { recursive: true }) await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8') } @@ -48,7 +60,9 @@ export async function saveConfig(config) { /** * Update specific config fields */ -export async function updateConfig(updates) { +export async function updateConfig( + updates: Partial<KindleConfig>, +): Promise<KindleConfig> { const config = await loadConfig() const updatedConfig = { ...config, ...updates } await saveConfig(updatedConfig) @@ -58,7 +72,7 @@ export async function updateConfig(updates) { /** * Show current configuration */ -async function showConfig() { +async function showConfig(): Promise<void> { const currentConfig = await loadConfig() console.log('📋 Current Kindle Sync Configuration:\n') console.log(JSON.stringify(currentConfig, null, 2)) @@ -69,7 +83,7 @@ async function showConfig() { /** * CLI interface */ -async function main() { +async function main(): Promise<void> { const argv = await yargs(hideBin(process.argv)) .command('show', 'Show current configuration') .command('reset', 'Reset to default configuration') @@ -90,21 +104,23 @@ async function main() { if (argv.interactive) { await showConfig() } else if (argv.set) { - const [key, value] = argv.set.split('=') - if (!key || value === undefined) { + const parts = argv.set.split('=') + const key = parts[0] + const value = parts.slice(1).join('=') + if (!key || !value) { console.error('❌ Invalid format. Use: --set key=value') process.exit(1) } // Parse value (try JSON parse for objects/arrays, otherwise string) - let parsedValue + let parsedValue: unknown try { parsedValue = JSON.parse(value) } catch { parsedValue = value } - await updateConfig({ [key]: parsedValue }) + await updateConfig({ [key]: parsedValue } as Partial<KindleConfig>) console.log(`✅ Updated ${key} = ${JSON.stringify(parsedValue)}`) console.log(`\n📁 Config saved to: ${CONFIG_PATH}`) } else if (argv._[0] === 'reset') { @@ -122,8 +138,9 @@ async function main() { // Run CLI if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { - main().catch((error) => { - console.error('❌ Error:', error.message) + main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error) + console.error('❌ Error:', message) process.exit(1) }) } diff --git a/.scripts/kindle/lib/config.js b/.scripts/kindle/lib/config.js deleted file mode 100644 index bddd860e..00000000 --- a/.scripts/kindle/lib/config.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Configuration for Kindle Notebook scraper - * Centralizes all selectors and scraping parameters - */ - -/** - * Default configuration for Kindle Notebook scraper - * These selectors were last verified: 2025-10-18 - * - * NOTE: Amazon's Kindle Notebook page structure may change. - * If scraping fails, inspect the page DOM and update these selectors. - */ -export const DEFAULT_SCRAPER_CONFIG = { - bookSelectors: { - // Sidebar container with list of books - container: '#kp-notebook-library', - // Individual book items in sidebar (not actual links, but clickable divs) - links: '.kp-notebook-library-each-book', - }, - highlightSelectors: { - // Color is in the class name, e.g., .kp-notebook-highlight-yellow - colorAttribute: '.kp-notebook-highlight', - // Container for all highlights - item: '#kp-notebook-annotations > .a-row.a-spacing-base', - // Location is in the highlight header - location: '#annotationHighlightHeader', - // Note text - note: '.kp-notebook-note #note', - // Highlight text - text: '.kp-notebook-highlight #highlight', - }, - metadataSelectors: { - // Author in sidebar - author: '.kp-notebook-library-each-book p.kp-notebook-searchable', - // Title in sidebar - title: '.kp-notebook-library-each-book h2.kp-notebook-searchable', - // Copyright truncation warning - truncationWarning: '.copyright-export-limit', - }, - scraping: { - maxLazyLoadRounds: 30, - maxStableRounds: 3, - pageLoadTimeout: 30000, - rateLimitBaseDelay: 500, - rateLimitJitter: 400, - scrollBaseDelay: 600, - scrollDelayIncrease: 20, - scrollJitter: 400, - }, -} - -export const NOTEBOOK_URL = 'https://read.amazon.com/notebook' diff --git a/.scripts/kindle/lib/config.ts b/.scripts/kindle/lib/config.ts new file mode 100644 index 00000000..a51587d3 --- /dev/null +++ b/.scripts/kindle/lib/config.ts @@ -0,0 +1,79 @@ +/** + * Configuration for Kindle Notebook scraper + * Centralizes all selectors and scraping parameters + */ + +export interface BookSelectors { + container: string // Sidebar container with list of books + links: string // Individual book items in sidebar (not actual links, but clickable divs) +} + +export interface HighlightSelectors { + colorAttribute: string // Color is in the class name, e.g., .kp-notebook-highlight-yellow + item: string // Container for all highlights + location: string // Location is in the highlight header + note: string // Note text + text: string // Highlight text +} + +export interface MetadataSelectors { + author: string // Author in sidebar + title: string // Title in sidebar + truncationWarning: string // Copyright truncation warning +} + +export interface ScraperConfig { + bookSelectors: BookSelectors + highlightSelectors: HighlightSelectors + metadataSelectors: MetadataSelectors + scraping: ScrapingConfig +} + +export interface ScrapingConfig { + maxLazyLoadRounds: number + maxStableRounds: number + pageLoadTimeout: number + rateLimitBaseDelay: number + rateLimitJitter: number + scrollBaseDelay: number + scrollDelayIncrease: number + scrollJitter: number +} + +/** + * Default configuration for Kindle Notebook scraper + * These selectors were last verified: 2025-10-18 + * + * NOTE: Amazon's Kindle Notebook page structure may change. + * If scraping fails, inspect the page DOM and update these selectors. + */ +export const DEFAULT_SCRAPER_CONFIG: ScraperConfig = { + bookSelectors: { + container: '#kp-notebook-library', // Sidebar container with list of books + links: '.kp-notebook-library-each-book', // Individual book items in sidebar (not actual links, but clickable divs) + }, + highlightSelectors: { + colorAttribute: '.kp-notebook-highlight', // Color is in the class name, e.g., .kp-notebook-highlight-yellow + item: '#kp-notebook-annotations > .a-row.a-spacing-base', // Container for all highlights + location: '#annotationHighlightHeader', // Location is in the highlight header + note: '.kp-notebook-note #note', // Note text + text: '.kp-notebook-highlight #highlight', // Highlight text + }, + metadataSelectors: { + author: '.kp-notebook-library-each-book p.kp-notebook-searchable', // Author in sidebar + title: '.kp-notebook-library-each-book h2.kp-notebook-searchable', // Title in sidebar + truncationWarning: '.copyright-export-limit', // Copyright truncation warning + }, + scraping: { + maxLazyLoadRounds: 30, + maxStableRounds: 3, + pageLoadTimeout: 30000, + rateLimitBaseDelay: 500, + rateLimitJitter: 400, + scrollBaseDelay: 600, + scrollDelayIncrease: 20, + scrollJitter: 400, + }, +} + +export const NOTEBOOK_URL = 'https://read.amazon.com/notebook' diff --git a/.scripts/kindle/lib/errors.js b/.scripts/kindle/lib/errors.ts similarity index 78% rename from .scripts/kindle/lib/errors.js rename to .scripts/kindle/lib/errors.ts index d0797240..ce4cd607 100644 --- a/.scripts/kindle/lib/errors.js +++ b/.scripts/kindle/lib/errors.ts @@ -6,7 +6,7 @@ * Base error class for all Kindle export errors */ export class KindleExportError extends Error { - constructor(message) { + constructor(message: string) { super(message) this.name = this.constructor.name Error.captureStackTrace(this, this.constructor) @@ -22,7 +22,9 @@ export class AuthenticationError extends KindleExportError {} * Error thrown when data validation fails */ export class ValidationError extends KindleExportError { - constructor(message, errors) { + public errors: string[] + + constructor(message: string, errors: string[]) { super(message) this.errors = errors } @@ -32,7 +34,9 @@ export class ValidationError extends KindleExportError { * Error thrown when file operations fail */ export class FileSystemError extends KindleExportError { - constructor(message, path) { + public path: string + + constructor(message: string, path: string) { super(message) this.path = path } @@ -41,7 +45,7 @@ export class FileSystemError extends KindleExportError { /** * Format error for user-friendly display */ -export function formatError(error) { +export function formatError(error: unknown): string { if (error instanceof Error) { return `${error.name}: ${error.message}` } diff --git a/.scripts/kindle/lib/renderer.js b/.scripts/kindle/lib/renderer.ts similarity index 60% rename from .scripts/kindle/lib/renderer.js rename to .scripts/kindle/lib/renderer.ts index 4ed7b4f6..9d08091e 100644 --- a/.scripts/kindle/lib/renderer.js +++ b/.scripts/kindle/lib/renderer.ts @@ -5,14 +5,21 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { createBookFilename, formatDate } from './utils.js' -import { parseBookExports } from './validation.js' +import { + type BookExport, + type BookExports, + parseBookExports, +} from './validation.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) /** * Create a Kindle app link for a specific location */ -function createKindleLink(bookId, location) { +function createKindleLink( + bookId: string, + location: string | undefined, +): string { if (!location) return '' // Remove commas from location number const cleanLocation = location.replace(/,/g, '') @@ -23,24 +30,41 @@ function createKindleLink(bookId, location) { * Register Handlebars helpers (runs once at module initialization) */ // Helper to decode HTML entities -Handlebars.registerHelper('decode', function (text) { - return new Handlebars.SafeString(decode(text || '')) +Handlebars.registerHelper('decode', function (text?: string) { + return new Handlebars.SafeString(decode(text ?? '')) }) // Helper to create Kindle app links -Handlebars.registerHelper('kindleLink', function (bookId, location) { - return createKindleLink(bookId, location) -}) +Handlebars.registerHelper( + 'kindleLink', + function (bookId: string, location?: string) { + return createKindleLink(bookId, location) + }, +) // Helper to format dates -Handlebars.registerHelper('formatDate', function (date) { +Handlebars.registerHelper('formatDate', function (date: Date | string) { return formatDate(date) }) +export interface ExportOptions { + overwrite?: boolean + template?: Handlebars.TemplateDelegate<BookExport> + templatePath?: string +} + +export interface ExportResult { + files: { book: BookExport; path: string }[] + skipped: number + written: number +} + /** * Load and compile Handlebars template */ -export async function loadTemplate(templatePath) { +export async function loadTemplate( + templatePath?: string, +): Promise<Handlebars.TemplateDelegate<BookExport>> { // Default template location const defaultTemplate = path.resolve( __dirname, @@ -55,14 +79,21 @@ export async function loadTemplate(templatePath) { /** * Render a single book to Markdown */ -export function renderBook(book, template) { +export function renderBook( + book: BookExport, + template: Handlebars.TemplateDelegate<BookExport>, +): string { return template(book) } /** * Export books to Markdown format using Handlebars template */ -export async function exportBooksToMarkdown(books, outputDir, options = {}) { +export async function exportBooksToMarkdown( + books: BookExports, + outputDir: string, + options: ExportOptions = {}, +): Promise<ExportResult> { const { overwrite = false, template: precompiledTemplate, @@ -70,18 +101,18 @@ export async function exportBooksToMarkdown(books, outputDir, options = {}) { } = options // Use provided template or load from path - const template = precompiledTemplate || (await loadTemplate(templatePath)) + const template = precompiledTemplate ?? (await loadTemplate(templatePath)) // Ensure output directory exists await fs.mkdir(outputDir, { recursive: true }) let written = 0 let skipped = 0 - const files = [] + const files: { book: BookExport; path: string }[] = [] for (const book of books) { // Create filename - const filename = createBookFilename(book.title, book.author, book.bookId) + const filename = createBookFilename(book.title, book.author) const destPath = path.join(outputDir, filename) // Check if file exists and handle overwrite logic @@ -107,16 +138,21 @@ export async function exportBooksToMarkdown(books, outputDir, options = {}) { /** * Load books from JSON file */ -export async function loadBooksFromJSON(jsonPath) { +export async function loadBooksFromJSON( + jsonPath: string, +): Promise<BookExports> { const rawJson = await fs.readFile(jsonPath, 'utf-8') - const parsedJson = JSON.parse(rawJson) + const parsedJson: unknown = JSON.parse(rawJson) return parseBookExports(parsedJson) } /** * Save books to JSON file */ -export async function saveBooksToJSON(books, jsonPath) { +export async function saveBooksToJSON( + books: BookExports, + jsonPath: string, +): Promise<void> { await fs.mkdir(path.dirname(jsonPath), { recursive: true }) await fs.writeFile(jsonPath, JSON.stringify(books, null, 2), 'utf-8') } diff --git a/.scripts/kindle/lib/scraper.js b/.scripts/kindle/lib/scraper.ts similarity index 78% rename from .scripts/kindle/lib/scraper.js rename to .scripts/kindle/lib/scraper.ts index ec5e0cba..b98d8e59 100644 --- a/.scripts/kindle/lib/scraper.js +++ b/.scripts/kindle/lib/scraper.ts @@ -1,26 +1,59 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' -import { chromium } from 'playwright' - -import { DEFAULT_SCRAPER_CONFIG, NOTEBOOK_URL } from './config.js' +import { + type BrowserContext, + chromium, + type ElementHandle, + type Page, +} from 'playwright' + +import type { BookExport, Highlight } from './validation.js' + +import { + DEFAULT_SCRAPER_CONFIG, + NOTEBOOK_URL, + type ScraperConfig, +} from './config.js' import { AuthenticationError } from './errors.js' import { generateHighlightId } from './utils.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const AUTH_DIR = path.resolve(__dirname, '../../../.kindle/auth') +export interface BookInfo { + asin: string + author?: string + title: string +} + +export interface ScrapeOptions { + config?: ScraperConfig + headless?: boolean + limit?: number +} + +export interface ScrapeResult { + books: BookExport[] + errors: { asin: string; error: string; title: string }[] +} + /** * Extract text content from an element */ -async function extractText(item, selector) { +async function extractText( + item: ElementHandle, + selector: string, +): Promise<string | undefined> { const el = await item.$(selector) - return el ? (await el.textContent())?.trim() : undefined + if (!el) return undefined + const textContent = await el.textContent() + return textContent?.trim() } /** * Launch a persistent browser context using saved authentication */ -export async function getContext(headless = false) { +export async function getContext(headless = false): Promise<BrowserContext> { return chromium.launchPersistentContext(AUTH_DIR, { headless, viewport: { height: 900, width: 1000 }, @@ -32,7 +65,7 @@ export async function getContext(headless = false) { /** * Verify authentication by checking if we're redirected to signin */ -export async function ensureAuth(page) { +export async function ensureAuth(page: Page): Promise<void> { console.log(`🔍 Navigating to: ${NOTEBOOK_URL}`) await page.goto(NOTEBOOK_URL, { waitUntil: 'networkidle' }) console.log(`📍 Current URL: ${page.url()}`) @@ -49,7 +82,10 @@ export async function ensureAuth(page) { /** * Discover all books from the sidebar */ -export async function discoverBooks(page, config = DEFAULT_SCRAPER_CONFIG) { +export async function discoverBooks( + page: Page, + config: ScraperConfig = DEFAULT_SCRAPER_CONFIG, +): Promise<BookInfo[]> { console.log('🔍 Discovering books from sidebar...') // Wait for notebook container to load @@ -74,17 +110,26 @@ export async function discoverBooks(page, config = DEFAULT_SCRAPER_CONFIG) { // Extract title const titleEl = el.querySelector(selectors.title) - const title = titleEl?.textContent.trim() || 'Unknown' + const title = (titleEl?.textContent ?? '').trim() || 'Unknown' // Extract author const authorEl = el.querySelector(selectors.author) - let author = authorEl?.textContent.trim() + let author = (authorEl?.textContent ?? '').trim() || '' if (author) { // Remove "By: " prefix if present author = author.replace(/^By:\s*/i, '') } - return { asin, author, title } + // Build result object, only including author if it exists + const result: { asin: string; author?: string; title: string } = { + asin, + title, + } + if (author) { + result.author = author + } + + return result }) }, { @@ -100,7 +145,7 @@ export async function discoverBooks(page, config = DEFAULT_SCRAPER_CONFIG) { /** * Click a book in the sidebar and wait for highlights to load */ -export async function clickBook(page, asin) { +export async function clickBook(page: Page, asin: string): Promise<void> { // Find the book div by ASIN // The data-action is on a span, and the anchor is inside it const bookDiv = page.locator(`#${asin}`) @@ -125,10 +170,10 @@ export async function clickBook(page, asin) { * Scrape a single book's highlights from the current page (after clicking it) */ export async function scrapeBook( - page, - bookInfo, - config = DEFAULT_SCRAPER_CONFIG, -) { + page: Page, + bookInfo: BookInfo, + config: ScraperConfig = DEFAULT_SCRAPER_CONFIG, +): Promise<BookExport> { const { asin, author, title } = bookInfo // Click the book to load its highlights @@ -166,7 +211,7 @@ export async function scrapeBook( ) // Filter out empty highlights and generate stable IDs - const validHighlights = highlights + const validHighlights: Highlight[] = highlights .filter((h) => h.text.length > 0) .map((h) => ({ ...h, @@ -195,7 +240,9 @@ export async function scrapeBook( /** * Scrape all books from Kindle Notebook */ -export async function scrapeAllBooks(options = {}) { +export async function scrapeAllBooks( + options: ScrapeOptions = {}, +): Promise<ScrapeResult> { const { config = DEFAULT_SCRAPER_CONFIG, headless = false, limit } = options console.log('📖 Starting Kindle Notebook scraper\n') @@ -229,8 +276,8 @@ export async function scrapeAllBooks(options = {}) { } // Scrape each book - const results = [] - const errors = [] + const results: BookExport[] = [] + const errors: { asin: string; error: string; title: string }[] = [] for (let i = 0; i < books.length; i++) { const bookInfo = books[i] diff --git a/.scripts/kindle/lib/utils.js b/.scripts/kindle/lib/utils.ts similarity index 77% rename from .scripts/kindle/lib/utils.js rename to .scripts/kindle/lib/utils.ts index 5646221b..184e2087 100644 --- a/.scripts/kindle/lib/utils.js +++ b/.scripts/kindle/lib/utils.ts @@ -3,7 +3,7 @@ import { createHash } from 'node:crypto' /** * Sanitize a string for use in filenames by replacing invalid characters */ -export function fileSafe(input) { +export function fileSafe(input: string): string { return input.replace(/[\\/:*?"<>|]/g, '_').trim() } @@ -11,7 +11,10 @@ export function fileSafe(input) { * Generate a stable hash-based ID for a highlight * Uses text + location to ensure stability across exports */ -export function generateHighlightId(text, location) { +export function generateHighlightId( + text: string, + location: string | undefined, +): string { const content = location ? `${text}::${location}` : text return createHash('sha256').update(content).digest('hex').slice(0, 16) } @@ -20,7 +23,7 @@ export function generateHighlightId(text, location) { * Extract ASIN from Amazon Kindle Notebook URL * Falls back to slugified title if ASIN not found */ -export function extractBookId(url, title) { +export function extractBookId(url: string, title: string): string { const asinMatch = /[?&]asin=([^&]+)/.exec(url) if (asinMatch?.[1]) { return asinMatch[1] @@ -39,7 +42,7 @@ export function extractBookId(url, title) { /** * Format date for YAML frontmatter */ -export function formatDate(date) { +export function formatDate(date: Date | string): string { if (typeof date === 'string') { date = new Date(date) } @@ -49,7 +52,7 @@ export function formatDate(date) { /** * Extract author's last name from full author name */ -function extractLastName(author) { +function extractLastName(author: string): string { if (!author) return 'Unknown' const parts = author.trim().split(/\s+/) return parts[parts.length - 1] || 'Unknown' @@ -58,7 +61,10 @@ function extractLastName(author) { /** * Create a safe filename from book title and author */ -export function createBookFilename(title, author) { +export function createBookFilename( + title: string, + author: string | undefined, +): string { const safeTitle = fileSafe(title) if (author) { diff --git a/.scripts/kindle/lib/validation.js b/.scripts/kindle/lib/validation.ts similarity index 81% rename from .scripts/kindle/lib/validation.js rename to .scripts/kindle/lib/validation.ts index 6a8a5c8b..f2475fce 100644 --- a/.scripts/kindle/lib/validation.js +++ b/.scripts/kindle/lib/validation.ts @@ -12,6 +12,8 @@ export const highlightSchema = z.object({ text: z.string().min(1), }) +export type Highlight = z.infer<typeof highlightSchema> + /** * Zod schema for book export metadata */ @@ -22,6 +24,8 @@ export const bookMetaSchema = z }) .optional() +export type BookMeta = z.infer<typeof bookMetaSchema> + /** * Zod schema for a complete book export */ @@ -36,15 +40,19 @@ export const bookExportSchema = z.object({ title: z.string().min(1), }) +export type BookExport = z.infer<typeof bookExportSchema> + /** * Zod schema for an array of book exports */ export const bookExportsSchema = z.array(bookExportSchema) +export type BookExports = z.infer<typeof bookExportsSchema> + /** * Format Zod error issues into readable error messages */ -function formatZodError(error) { +function formatZodError(error: z.ZodError): string[] { return error.issues.map((issue) => { const path = issue.path.join('.') return `${path}: ${issue.message}` @@ -55,7 +63,7 @@ function formatZodError(error) { * Parse and validate an array of book exports * @throws {ValidationError} if validation fails */ -export function parseBookExports(data) { +export function parseBookExports(data: unknown): BookExports { try { return bookExportsSchema.parse(data) } catch (error) { @@ -72,7 +80,7 @@ export function parseBookExports(data) { /** * Validate a single book export */ -export function validateBookExport(data) { +export function validateBookExport(data: unknown): BookExport { try { return bookExportSchema.parse(data) } catch (error) { diff --git a/.scripts/kindle/sync.js b/.scripts/kindle/sync.ts similarity index 81% rename from .scripts/kindle/sync.js rename to .scripts/kindle/sync.ts index 04c49811..89a47288 100644 --- a/.scripts/kindle/sync.js +++ b/.scripts/kindle/sync.ts @@ -7,6 +7,8 @@ import { fileURLToPath } from 'node:url' import yargs from 'yargs' import { hideBin } from 'yargs/helpers' +import type { BookExports } from './lib/validation.js' + import { loadConfig, updateConfig } from './config.js' import { AuthenticationError, formatError } from './lib/errors.js' import { @@ -19,13 +21,32 @@ import { scrapeAllBooks } from './lib/scraper.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const CACHE_PATH = path.resolve(__dirname, '../../.kindle/cache/all_books.json') const AUTH_DIR = path.resolve(__dirname, '../../.kindle/auth') -const AUTH_SCRIPT = path.resolve(__dirname, './auth.js') +const AUTH_SCRIPT = path.resolve(__dirname, './auth.ts') const CONFIG_PATH = path.resolve(__dirname, '../../.kindle/config.json') +export interface SyncOptions { + all?: boolean + books?: string + force?: boolean + headless?: boolean + last?: number + limit?: number + outputFolder?: string +} + +interface GetOrScrapeOptions { + all: boolean + force: boolean + headless: boolean + last?: number + limit?: number + specificBooks?: string +} + /** * Check if authentication exists */ -async function checkAuthExists() { +async function checkAuthExists(): Promise<boolean> { try { const stats = await fs.stat(AUTH_DIR) if (!stats.isDirectory()) return false @@ -41,7 +62,7 @@ async function checkAuthExists() { /** * Check if this is the first run (no config file exists) */ -async function isFirstRun() { +async function isFirstRun(): Promise<boolean> { try { await fs.access(CONFIG_PATH) return false @@ -53,7 +74,7 @@ async function isFirstRun() { /** * Prompt user for output folder on first run */ -async function promptForOutputFolder() { +async function promptForOutputFolder(): Promise<string> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, @@ -77,11 +98,11 @@ async function promptForOutputFolder() { /** * Run authentication script */ -async function runAuth() { +async function runAuth(): Promise<void> { console.log('🔐 Authentication required. Starting auth flow...\n') return new Promise((resolve, reject) => { - const authProcess = spawn('node', [AUTH_SCRIPT], { + const authProcess = spawn('tsx', [AUTH_SCRIPT], { cwd: path.dirname(AUTH_SCRIPT), stdio: 'inherit', // Pass through stdin/stdout/stderr }) @@ -104,7 +125,9 @@ async function runAuth() { /** * Handle first-run setup: prompt for output folder and save to config */ -async function ensureFirstRunSetup(outputFolder) { +async function ensureFirstRunSetup( + outputFolder?: string, +): Promise<string | undefined> { const firstRun = await isFirstRun() if (firstRun && !outputFolder) { @@ -124,7 +147,9 @@ async function ensureFirstRunSetup(outputFolder) { /** * Get books either from cache or by scraping, with auth retry logic */ -async function getOrScrapeBooks(options) { +async function getOrScrapeBooks( + options: GetOrScrapeOptions, +): Promise<BookExports> { const { all, force, headless, last, limit, specificBooks } = options // Determine scraping strategy @@ -180,8 +205,16 @@ async function getOrScrapeBooks(options) { /** * Attempt to scrape books with proper error handling and caching */ -async function attemptScrape(scrapeLimit, headless, last) { - const result = await scrapeAllBooks({ headless, limit: scrapeLimit }) +async function attemptScrape( + scrapeLimit: number | undefined, + headless: boolean, + last: number | undefined, +): Promise<BookExports> { + const scrapeOptions: { headless: boolean; limit?: number } = { headless } + if (scrapeLimit !== undefined) { + scrapeOptions.limit = scrapeLimit + } + const result = await scrapeAllBooks(scrapeOptions) let books = result.books // If using --last flag, get the last N books instead of first N @@ -216,7 +249,7 @@ async function attemptScrape(scrapeLimit, headless, last) { /** * Main sync function */ -async function sync(options) { +async function sync(options: SyncOptions): Promise<void> { const { all = false, books: specificBooks, @@ -251,7 +284,21 @@ async function sync(options) { console.log(`📄 Template: ${config.templatePath}\n`) // Get books from cache or by scraping - const books = await getOrScrapeBooks(options) + const scrapeOptions: GetOrScrapeOptions = { + all, + force, + headless: options.headless ?? false, + } + if (options.last !== undefined) { + scrapeOptions.last = options.last + } + if (options.limit !== undefined) { + scrapeOptions.limit = options.limit + } + if (specificBooks !== undefined) { + scrapeOptions.specificBooks = specificBooks + } + const books = await getOrScrapeBooks(scrapeOptions) if (books.length === 0) { console.log('⚠️ No books to sync') @@ -297,7 +344,7 @@ async function sync(options) { /** * CLI interface */ -async function main() { +async function main(): Promise<void> { const argv = await yargs(hideBin(process.argv)) .option('all', { alias: 'a', @@ -346,18 +393,27 @@ async function main() { .alias('h', 'help') .parseAsync() - await sync({ + const syncOptions: SyncOptions = { all: argv.all, - books: argv.books, force: argv.force, headless: argv.headless, - last: argv.last, - limit: argv.limit, - outputFolder: argv.output, - }) + } + if (argv.books !== undefined) { + syncOptions.books = argv.books + } + if (argv.last !== undefined) { + syncOptions.last = argv.last + } + if (argv.limit !== undefined) { + syncOptions.limit = argv.limit + } + if (argv.output !== undefined) { + syncOptions.outputFolder = argv.output + } + await sync(syncOptions) } -main().catch((error) => { +main().catch((error: unknown) => { console.error('\n❌ Sync failed:', formatError(error)) process.exit(1) }) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f221fae..3fc2d1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to ### Added -- New `kindle-sync` command for syncing a user's kindle highlights and notes to their vault +- New `kindle-sync` command for syncing a user's kindle highlights and notes to + their vault. ## [0.13.1] - 2025-10-13 diff --git a/package-lock.json b/package-lock.json index 0a4021f7..c696c3fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,37 +1,483 @@ { "name": "claudesidian", - "version": "0.9.2", + "version": "0.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claudesidian", - "version": "0.9.2", + "version": "0.13.1", "license": "MIT", "dependencies": { "@google/generative-ai": "^0.21.0", - "@modelcontextprotocol/sdk": "^1.0.0" + "@modelcontextprotocol/sdk": "^1.0.0", + "handlebars": "^4.7.8", + "html-entities": "^2.6.0", + "playwright": "^1.48.0", + "yargs": "^17.7.2", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/compat": "^1.3.0", - "@next/eslint-plugin-next": "^15.3.4", + "@types/node": "^24.9.1", + "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.35.0", "@typescript-eslint/parser": "^8.35.0", "eslint": "^9.29.0", "eslint-plugin-check-file": "^3.3.0", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-perfectionist": "^4.15.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", "globals": "^16.2.0", "prettier": "^3.6.0", "prettier-plugin-packagejson": "^2.5.15", "prettier-plugin-sort-json": "^4.1.1", - "prettier-plugin-tailwindcss": "^0.6.13", + "tsx": "^4.20.6", + "typescript": "^5.9.3", "typescript-eslint": "^8.35.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -346,16 +792,6 @@ "node": ">=18" } }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.4.tgz", - "integrity": "sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -435,18 +871,45 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", - "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/type-utils": "8.35.0", - "@typescript-eslint/utils": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -460,22 +923,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.35.0", + "@typescript-eslint/parser": "^8.46.2", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", - "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/typescript-estree": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4" }, "engines": { @@ -487,18 +950,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", - "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.35.0", - "@typescript-eslint/types": "^8.35.0", + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", "debug": "^4.3.4" }, "engines": { @@ -509,18 +972,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", - "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0" + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -531,9 +994,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", - "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", "dev": true, "license": "MIT", "engines": { @@ -544,18 +1007,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", - "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.35.0", - "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -568,13 +1032,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", - "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", "dev": true, "license": "MIT", "engines": { @@ -586,16 +1050,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", - "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.35.0", - "@typescript-eslint/tsconfig-utils": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/visitor-keys": "8.35.0", + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -611,7 +1075,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { @@ -645,16 +1109,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", - "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.35.0", - "@typescript-eslint/types": "8.35.0", - "@typescript-eslint/typescript-estree": "8.35.0" + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -665,17 +1129,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.35.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", - "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/types": "8.46.2", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -751,11 +1215,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -774,16 +1246,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -824,27 +1286,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.findlastindex": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", @@ -905,23 +1346,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", @@ -944,13 +1368,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -977,26 +1394,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "dev": true, - "license": "MPL-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1131,11 +1528,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1148,7 +1558,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -1224,13 +1633,6 @@ "node": ">= 8" } }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -1410,13 +1812,6 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1513,34 +1908,6 @@ "node": ">= 0.4" } }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1600,6 +1967,57 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1838,60 +2256,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, - "engines": { - "node": ">=4.0" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-perfectionist": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-4.15.0.tgz", @@ -1910,104 +2274,6 @@ "eslint": ">=8.45.0" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -2255,46 +2521,16 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, @@ -2423,6 +2659,20 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -2463,6 +2713,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2518,6 +2777,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/git-hooks-list": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-4.1.1.tgz", @@ -2590,6 +2862,27 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -2682,6 +2975,22 @@ "node": ">= 0.4" } }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2947,6 +3256,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -3209,31 +3527,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3280,22 +3573,6 @@ "json5": "lib/cli.js" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3306,26 +3583,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/language-subtag-registry": { - "version": "0.3.23", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", - "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/language-tags": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", - "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", - "dev": true, - "license": "MIT", - "dependencies": { - "language-subtag-registry": "^0.3.20" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3363,19 +3620,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3471,7 +3715,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3509,6 +3752,12 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3561,22 +3810,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/object.fromentries": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", @@ -3799,6 +4032,36 @@ "node": ">=16.20.0" } }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -3867,97 +4130,6 @@ "prettier": "^3.0.0" } }, - "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", - "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "@ianvs/prettier-plugin-sort-imports": "*", - "@prettier/plugin-pug": "*", - "@shopify/prettier-plugin-liquid": "*", - "@trivago/prettier-plugin-sort-imports": "*", - "@zackad/prettier-plugin-twig": "*", - "prettier": "^3.0", - "prettier-plugin-astro": "*", - "prettier-plugin-css-order": "*", - "prettier-plugin-import-sort": "*", - "prettier-plugin-jsdoc": "*", - "prettier-plugin-marko": "*", - "prettier-plugin-multiline-arrays": "*", - "prettier-plugin-organize-attributes": "*", - "prettier-plugin-organize-imports": "*", - "prettier-plugin-sort-imports": "*", - "prettier-plugin-style-order": "*", - "prettier-plugin-svelte": "*" - }, - "peerDependenciesMeta": { - "@ianvs/prettier-plugin-sort-imports": { - "optional": true - }, - "@prettier/plugin-pug": { - "optional": true - }, - "@shopify/prettier-plugin-liquid": { - "optional": true - }, - "@trivago/prettier-plugin-sort-imports": { - "optional": true - }, - "@zackad/prettier-plugin-twig": { - "optional": true - }, - "prettier-plugin-astro": { - "optional": true - }, - "prettier-plugin-css-order": { - "optional": true - }, - "prettier-plugin-import-sort": { - "optional": true - }, - "prettier-plugin-jsdoc": { - "optional": true - }, - "prettier-plugin-marko": { - "optional": true - }, - "prettier-plugin-multiline-arrays": { - "optional": true - }, - "prettier-plugin-organize-attributes": { - "optional": true - }, - "prettier-plugin-organize-imports": { - "optional": true - }, - "prettier-plugin-sort-imports": { - "optional": true - }, - "prettier-plugin-style-order": { - "optional": true - }, - "prettier-plugin-svelte": { - "optional": true - } - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4056,13 +4228,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -4107,6 +4272,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4138,6 +4312,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4494,6 +4678,15 @@ "sort-package-json": "cli.js" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4517,59 +4710,25 @@ "node": ">= 0.4" } }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } + "node_modules/string-width/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/string.prototype.trim": { "version": "1.2.10", @@ -4630,13 +4789,25 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "engines": { + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { "node": ">=4" } }, @@ -4791,6 +4962,41 @@ "strip-bom": "^3.0.0" } }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4897,12 +5103,11 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4934,6 +5139,419 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.0.tgz", + "integrity": "sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/type-utils": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.35.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.35.0.tgz", + "integrity": "sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/utils": "8.35.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.35.0.tgz", + "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.35.0.tgz", + "integrity": "sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.35.0.tgz", + "integrity": "sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.35.0.tgz", + "integrity": "sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/typescript-estree": "8.35.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.35.0.tgz", + "integrity": "sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.35.0", + "@typescript-eslint/tsconfig-utils": "8.35.0", + "@typescript-eslint/types": "8.35.0", + "@typescript-eslint/visitor-keys": "8.35.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.35.0.tgz", + "integrity": "sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.35.0", + "@typescript-eslint/types": "^8.35.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.35.0.tgz", + "integrity": "sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.35.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.35.0.tgz", + "integrity": "sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.35.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/typescript-eslint/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/typescript-eslint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -4953,6 +5571,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5094,12 +5719,71 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index dd63ade6..eacd8dcb 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "transcript:extract": ".scripts/transcript-extract.sh", "vault:stats": ".scripts/vault-stats.sh", "check-updates": "REMOTE=$(curl -s https://raw.githubusercontent.com/heyitsnoah/claudesidian/main/package.json | grep version | head -1 | sed 's/.*: \"\\(.*\\)\".*/\\1/') && LOCAL=$(grep version package.json | head -1 | sed 's/.*: \"\\(.*\\)\".*/\\1/') && if [ \"$LOCAL\" != \"$REMOTE\" ]; then echo -e \"📦 Update available! Latest: $REMOTE (you have: $LOCAL)\\n\\n⬇\\n/upgrade\\n⬆\\n\\n## What will this do\\n\\n✅ Update to the latest version of Claudesidian\\n✅ Get new features and improvements\\n✅ Preserve your vault content and settings\\n\\n\"; fi", - "kindle:auth": "node .scripts/kindle/auth.js", - "kindle:sync": "node .scripts/kindle/sync.js", - "kindle:sync-all": "node .scripts/kindle/sync.js --all", - "kindle:config": "node .scripts/kindle/config.js --interactive", + "kindle:auth": "tsx .scripts/kindle/auth.ts", + "kindle:sync": "tsx .scripts/kindle/sync.ts", + "kindle:sync-all": "tsx .scripts/kindle/sync.ts --all", + "kindle:config": "tsx .scripts/kindle/config.ts --interactive", "playwright:install": "pnpm exec playwright install chromium" }, "keywords": [ @@ -50,6 +50,8 @@ }, "devDependencies": { "@eslint/compat": "^1.3.0", + "@types/node": "^24.9.1", + "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.35.0", "@typescript-eslint/parser": "^8.35.0", "eslint": "^9.29.0", @@ -60,6 +62,8 @@ "prettier": "^3.6.0", "prettier-plugin-packagejson": "^2.5.15", "prettier-plugin-sort-json": "^4.1.1", + "tsx": "^4.20.6", + "typescript": "^5.9.3", "typescript-eslint": "^8.35.0" }, "packageManager": "pnpm@9.15.4" diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..53e972ed --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "exactOptionalPropertyTypes": true, + "incremental": true, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noImplicitReturns": true, // preferred over https://typescript-eslint.io/rules/consistent-return/ + "noUnusedLocals": false, // prefer using https://typescript-eslint.io/rules/no-unused-vars/ + "noUnusedParameters": false, // prefer using https://typescript-eslint.io/rules/no-unused-vars/ + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2020", + "verbatimModuleSyntax": false // prefer using https://typescript-eslint.io/rules/consistent-type-imports + }, + "exclude": ["node_modules", "dist", ".user-config", ".kindle"], + "include": [".scripts/**/*", ".claude/**/*"] +} From 1542d3d50b9ec435d09be1dc494ea2ce5e93426f Mon Sep 17 00:00:00 2001 From: Ryan Anderson <ryan@alephic.com> Date: Mon, 3 Nov 2025 19:35:11 -0500 Subject: [PATCH 5/8] Better handling of last name for multiple authors --- .claude/commands/kindle-sync.md | 3 +-- .scripts/kindle/lib/utils.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.claude/commands/kindle-sync.md b/.claude/commands/kindle-sync.md index d15f13f8..8d6fd603 100644 --- a/.claude/commands/kindle-sync.md +++ b/.claude/commands/kindle-sync.md @@ -83,8 +83,7 @@ Based on their response and whether it's first run: - If they answer **B** or **25**: Run `pnpm kindle:sync --limit 25` - If they answer **C** or **50**: Run `pnpm kindle:sync --limit 50` - If they answer **D** or **all**: Run `pnpm kindle:sync --all` -- If they answer **E** or a number: Run - `pnpm kindle:sync --limit [number]` +- If they answer **E** or a number: Run `pnpm kindle:sync --limit [number]` Note: If user just presses Enter for default folder, use `03_Resources/Kindle Highlights` as the path. diff --git a/.scripts/kindle/lib/utils.ts b/.scripts/kindle/lib/utils.ts index 184e2087..469b4387 100644 --- a/.scripts/kindle/lib/utils.ts +++ b/.scripts/kindle/lib/utils.ts @@ -51,10 +51,27 @@ export function formatDate(date: Date | string): string { /** * Extract author's last name from full author name + * For multiple authors, extracts the last name of the first author: + * - 3+ authors (with comma): "Juan Rulfo, Gabriel García Márquez, and Douglas J. Weatherford" + * - 2 authors (with "and"): "Juan Rulfo and Gabriel García Márquez" + * - Single author: "Juan Rulfo" */ function extractLastName(author: string): string { if (!author) return 'Unknown' - const parts = author.trim().split(/\s+/) + + let firstAuthor = author.trim() + + // If there's a comma, take everything before the first comma (handles 3+ authors) + if (firstAuthor.includes(',')) { + firstAuthor = firstAuthor.split(',')[0].trim() + } + // Otherwise, if there's "and", take everything before it (handles 2 authors) + else if (/\band\b/i.test(firstAuthor)) { + firstAuthor = firstAuthor.split(/\band\b/i)[0].trim() + } + + // Extract the last name (last word) from the first author + const parts = firstAuthor.split(/\s+/) return parts[parts.length - 1] || 'Unknown' } From 9a1a02d6e89ffce3897131ebfa7efffd46ff28a6 Mon Sep 17 00:00:00 2001 From: Ryan Anderson <ryan@alephic.com> Date: Tue, 4 Nov 2025 10:20:08 -0500 Subject: [PATCH 6/8] Added page number capture to highlights --- .claude/commands/kindle-sync.md | 56 +++++++++++--------- .scripts/kindle/lib/scraper.ts | 6 ++- .scripts/kindle/lib/utils.ts | 6 ++- .scripts/kindle/lib/validation.ts | 1 + .scripts/kindle/templates/kindle-note.md.hbs | 2 +- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/.claude/commands/kindle-sync.md b/.claude/commands/kindle-sync.md index 8d6fd603..e5208236 100644 --- a/.claude/commands/kindle-sync.md +++ b/.claude/commands/kindle-sync.md @@ -54,7 +54,7 @@ How many books would you like to sync? A. 10 books (recommended) - Quick sync of your 10 most-recently highlighted books B. 25 books - Broader sync C. 50 books - Extensive sync -D. All books - Complete library (10-30 min for large libraries -- only ) +D. All books - Resyncs your complete library (10-30 min for large libraries -- only ) E. Custom number - You specify how many Just reply with A, B, C, D, E, or a number. @@ -118,26 +118,40 @@ Each book creates a note with: ## Changing Your Settings -Want to change where highlights are saved or other settings? You have three +Want to change where highlights are saved or other settings? You have four options: -### Option 1: Edit Config File Directly (Recommended) +### Option 1: View Current Settings + +See what your current configuration is: + +```bash +pnpm kindle:config +``` + +### Option 2: Edit Config File Directly (Recommended for Multiple Changes) Open `.kindle/config.json` in your vault and edit: ```json { - "outputFolder": "02_Areas/Reading", // Change this to your preferred folder + "outputFolder": "03_Resources/Kindle Highlights", "templatePath": ".scripts/kindle/templates/kindle-note.md.hbs", - "overwrite": false, // Set to true to replace existing files + "overwrite": true, "addTags": ["kindle", "highlights", "books"], - "createIndex": true, - "indexPath": "03_Resources/Kindle Highlights Index.md", "lastSync": "2025-10-19T22:30:00.000Z" } ``` -### Option 2: Reset to First-Run Prompt +**Available settings:** + +- `outputFolder`: Where to save notes (default: `03_Resources/Kindle Highlights`) +- `templatePath`: Custom Handlebars template location +- `overwrite`: Whether to replace existing files on re-sync (default: `true`) +- `addTags`: Tags to add to all notes (default: `["kindle", "highlights", "books"]`) +- `lastSync`: Timestamp of last sync (auto-updated) + +### Option 3: Reset to First-Run Prompt Delete the config file to trigger the folder selection prompt again: @@ -146,7 +160,7 @@ rm .kindle/config.json # Next /kindle-sync will ask where to save highlights ``` -### Option 3: One-Time Override +### Option 4: One-Time Override Use the `--output` flag to save to a different folder just once (without changing config): @@ -168,22 +182,9 @@ Rate limiting delays are required to avoid Amazon detection. ### Organization - Notes are automatically named: `<Author Last Name> - <Title>.md` -- All include `#kindle`, `#highlights`, `#books` tags +- All include `#kindle`, `#highlights`, `#books` tags by default - Easy to find with Obsidian search or graph view -### Advanced Configuration - -All settings in `.kindle/config.json`: - -- `outputFolder`: Where to save notes (default: - `03_Resources/Kindle Highlights`) -- `templatePath`: Custom Handlebars template location -- `overwrite`: Whether to replace existing files (default: `false`) -- `addTags`: Tags to add to all notes (default: - `["kindle", "highlights", "books"]`) -- `createIndex`: Whether to create an index file (not yet implemented) -- `indexPath`: Where to save the index file - ## Troubleshooting ### "Not authenticated" error @@ -199,10 +200,13 @@ if you see this: - Check if your Kindle library is visible at read.amazon.com/notebook - Verify you have highlights (not just books in library) -### Duplicate files +### Preventing file overwrites + +By default, kindle-sync will overwrite existing files on re-sync. To preserve +your edits: -- Set `overwrite: true` in config to replace existing files -- Or manually delete old files before syncing +- Set `overwrite: false` in `.kindle/config.json` +- Or rename files you've edited so they won't match the sync pattern ### Scraping seems slow diff --git a/.scripts/kindle/lib/scraper.ts b/.scripts/kindle/lib/scraper.ts index b98d8e59..a28f4909 100644 --- a/.scripts/kindle/lib/scraper.ts +++ b/.scripts/kindle/lib/scraper.ts @@ -198,13 +198,15 @@ export async function scrapeBook( // Extract note text (if present) const note = await extractText(item, '.kp-notebook-note #note') - // Extract location from metadata + // Extract location and/or page from metadata const locationText = await extractText(item, '#annotationHighlightHeader') const location = locationText?.match(/Location:\s*(\S+)/i)?.[1] + const page = locationText?.match(/Page:\s*(\S+)/i)?.[1] return { location, note, + page, text, } }), @@ -215,7 +217,7 @@ export async function scrapeBook( .filter((h) => h.text.length > 0) .map((h) => ({ ...h, - id: generateHighlightId(h.text, h.location), + id: generateHighlightId(h.text, h.location, h.page), })) const notebookUrl = `${NOTEBOOK_URL}?asin=${asin}` diff --git a/.scripts/kindle/lib/utils.ts b/.scripts/kindle/lib/utils.ts index 469b4387..ad629ffd 100644 --- a/.scripts/kindle/lib/utils.ts +++ b/.scripts/kindle/lib/utils.ts @@ -9,13 +9,15 @@ export function fileSafe(input: string): string { /** * Generate a stable hash-based ID for a highlight - * Uses text + location to ensure stability across exports + * Uses text + location/page to ensure stability across exports */ export function generateHighlightId( text: string, location: string | undefined, + page?: string | undefined, ): string { - const content = location ? `${text}::${location}` : text + const positionMarker = location || page + const content = positionMarker ? `${text}::${positionMarker}` : text return createHash('sha256').update(content).digest('hex').slice(0, 16) } diff --git a/.scripts/kindle/lib/validation.ts b/.scripts/kindle/lib/validation.ts index f2475fce..9ae50538 100644 --- a/.scripts/kindle/lib/validation.ts +++ b/.scripts/kindle/lib/validation.ts @@ -9,6 +9,7 @@ export const highlightSchema = z.object({ id: z.string().min(1), location: z.string().optional(), note: z.string().optional(), + page: z.string().optional(), text: z.string().min(1), }) diff --git a/.scripts/kindle/templates/kindle-note.md.hbs b/.scripts/kindle/templates/kindle-note.md.hbs index 06cccad7..fac46e59 100644 --- a/.scripts/kindle/templates/kindle-note.md.hbs +++ b/.scripts/kindle/templates/kindle-note.md.hbs @@ -16,7 +16,7 @@ lastSynced: {{extractedAt}} ## Highlights {{#each highlights}} -{{text}}{{#if location}} - loc: [{{location}}]({{kindleLink ../bookId location}}){{/if}} +{{text}}{{#if location}} - loc: [{{location}}]({{kindleLink ../bookId location}}){{else}}{{#if page}} - page: {{page}}{{/if}}{{/if}} {{#if note}} ***Note:*** {{{decode note}}} From e1405bccb41a5c944fcde004f94b33b66460b8a2 Mon Sep 17 00:00:00 2001 From: Ryan Anderson <ryan@alephic.com> Date: Wed, 14 Jan 2026 13:52:45 -0500 Subject: [PATCH 7/8] fix: simplify auth logic --- .scripts/kindle/sync.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.scripts/kindle/sync.ts b/.scripts/kindle/sync.ts index 89a47288..a2cc13f8 100644 --- a/.scripts/kindle/sync.ts +++ b/.scripts/kindle/sync.ts @@ -262,14 +262,11 @@ async function sync(options: SyncOptions): Promise<void> { // Handle first-run setup const outputFolder = await ensureFirstRunSetup(initialOutputFolder) - // Check authentication before proceeding with scraping - const needsScraping = force || all || specificBooks || !all - if (needsScraping) { - const hasAuth = await checkAuthExists() - if (!hasAuth) { - console.log('⚠️ No authentication found.\n') - await runAuth() - } + // Check authentication before proceeding + const hasAuth = await checkAuthExists() + if (!hasAuth) { + console.log('⚠️ No authentication found.\n') + await runAuth() } // Load configuration From af717ae97d94138c727eb27d81771fd2f71498fb Mon Sep 17 00:00:00 2001 From: Ryan Anderson <ryan@alephic.com> Date: Wed, 14 Jan 2026 13:59:13 -0500 Subject: [PATCH 8/8] clean: remove dead code --- .claude/commands/kindle-sync.md | 6 ++++-- .scripts/kindle/lib/config.ts | 14 -------------- .scripts/kindle/lib/errors.ts | 12 ------------ .scripts/kindle/lib/utils.ts | 22 +--------------------- .scripts/kindle/lib/validation.ts | 17 ----------------- 5 files changed, 5 insertions(+), 66 deletions(-) diff --git a/.claude/commands/kindle-sync.md b/.claude/commands/kindle-sync.md index e5208236..8e944c69 100644 --- a/.claude/commands/kindle-sync.md +++ b/.claude/commands/kindle-sync.md @@ -145,10 +145,12 @@ Open `.kindle/config.json` in your vault and edit: **Available settings:** -- `outputFolder`: Where to save notes (default: `03_Resources/Kindle Highlights`) +- `outputFolder`: Where to save notes (default: + `03_Resources/Kindle Highlights`) - `templatePath`: Custom Handlebars template location - `overwrite`: Whether to replace existing files on re-sync (default: `true`) -- `addTags`: Tags to add to all notes (default: `["kindle", "highlights", "books"]`) +- `addTags`: Tags to add to all notes (default: + `["kindle", "highlights", "books"]`) - `lastSync`: Timestamp of last sync (auto-updated) ### Option 3: Reset to First-Run Prompt diff --git a/.scripts/kindle/lib/config.ts b/.scripts/kindle/lib/config.ts index a51587d3..594b1cc2 100644 --- a/.scripts/kindle/lib/config.ts +++ b/.scripts/kindle/lib/config.ts @@ -9,7 +9,6 @@ export interface BookSelectors { } export interface HighlightSelectors { - colorAttribute: string // Color is in the class name, e.g., .kp-notebook-highlight-yellow item: string // Container for all highlights location: string // Location is in the highlight header note: string // Note text @@ -30,14 +29,8 @@ export interface ScraperConfig { } export interface ScrapingConfig { - maxLazyLoadRounds: number - maxStableRounds: number - pageLoadTimeout: number rateLimitBaseDelay: number rateLimitJitter: number - scrollBaseDelay: number - scrollDelayIncrease: number - scrollJitter: number } /** @@ -53,7 +46,6 @@ export const DEFAULT_SCRAPER_CONFIG: ScraperConfig = { links: '.kp-notebook-library-each-book', // Individual book items in sidebar (not actual links, but clickable divs) }, highlightSelectors: { - colorAttribute: '.kp-notebook-highlight', // Color is in the class name, e.g., .kp-notebook-highlight-yellow item: '#kp-notebook-annotations > .a-row.a-spacing-base', // Container for all highlights location: '#annotationHighlightHeader', // Location is in the highlight header note: '.kp-notebook-note #note', // Note text @@ -65,14 +57,8 @@ export const DEFAULT_SCRAPER_CONFIG: ScraperConfig = { truncationWarning: '.copyright-export-limit', // Copyright truncation warning }, scraping: { - maxLazyLoadRounds: 30, - maxStableRounds: 3, - pageLoadTimeout: 30000, rateLimitBaseDelay: 500, rateLimitJitter: 400, - scrollBaseDelay: 600, - scrollDelayIncrease: 20, - scrollJitter: 400, }, } diff --git a/.scripts/kindle/lib/errors.ts b/.scripts/kindle/lib/errors.ts index ce4cd607..b67233bc 100644 --- a/.scripts/kindle/lib/errors.ts +++ b/.scripts/kindle/lib/errors.ts @@ -30,18 +30,6 @@ export class ValidationError extends KindleExportError { } } -/** - * Error thrown when file operations fail - */ -export class FileSystemError extends KindleExportError { - public path: string - - constructor(message: string, path: string) { - super(message) - this.path = path - } -} - /** * Format error for user-friendly display */ diff --git a/.scripts/kindle/lib/utils.ts b/.scripts/kindle/lib/utils.ts index ad629ffd..fa7cc693 100644 --- a/.scripts/kindle/lib/utils.ts +++ b/.scripts/kindle/lib/utils.ts @@ -14,33 +14,13 @@ export function fileSafe(input: string): string { export function generateHighlightId( text: string, location: string | undefined, - page?: string | undefined, + page?: string, ): string { const positionMarker = location || page const content = positionMarker ? `${text}::${positionMarker}` : text return createHash('sha256').update(content).digest('hex').slice(0, 16) } -/** - * Extract ASIN from Amazon Kindle Notebook URL - * Falls back to slugified title if ASIN not found - */ -export function extractBookId(url: string, title: string): string { - const asinMatch = /[?&]asin=([^&]+)/.exec(url) - if (asinMatch?.[1]) { - return asinMatch[1] - } - - // Fallback: slugify title - return title - .toLowerCase() - .trim() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .slice(0, 50) // Reasonable length limit -} - /** * Format date for YAML frontmatter */ diff --git a/.scripts/kindle/lib/validation.ts b/.scripts/kindle/lib/validation.ts index 9ae50538..d4326a30 100644 --- a/.scripts/kindle/lib/validation.ts +++ b/.scripts/kindle/lib/validation.ts @@ -77,20 +77,3 @@ export function parseBookExports(data: unknown): BookExports { throw error } } - -/** - * Validate a single book export - */ -export function validateBookExport(data: unknown): BookExport { - try { - return bookExportSchema.parse(data) - } catch (error) { - if (error instanceof z.ZodError) { - throw new ValidationError( - 'Failed to validate book export', - formatZodError(error), - ) - } - throw error - } -}