Vibecode Resurf
track this build4 phases, 12 steps, beginner friendly0%The core mechanic is unglamorous and very buildable: capture the text of pages you spend real time on, embed it, store it locally, then search and resurface it. An agent can get a Chrome extension doing that in a weekend, and because the corpus is your own browsing, there is no data moat to lose. Where the DIY version cracks is everything around the loop: cross-device sync, mobile and Safari, capture quality on SPAs and paywalled or auth-gated pages, and the taste of deciding what deserves to come back. Also the honest failure mode of every personal recall tool, which is that you stop opening it after week three. Buildable, yes; pleasant enough that you keep it, less certain.
You are building a lean indie version of Resurf. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Resurf · indie build A reading capture tool whose real problem is the return path: saving is easy and everyone does it, but a bookmark folder nobody opens is not a memory aid, so the engineering is in surfacing the right thing at a moment when it is useful rather than at a scheduled time nobody wants. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension and a hotkey, both storing the readable text | a link rots and a page changes, so the text is what you actually saved | | Storage | Local SQLite with full text search | reading history is a record of what you were thinking about | | Extraction | Readable content extraction at capture time, kept verbatim | extracting later means extracting from a page that has changed or gone | | Surfacing | Context-triggered rather than scheduled | a daily digest of old links is unread within a week | | Runtime | A local application with no account and no sync by default | the value does not require a service and the privacy cost of one is high | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application and builds the extension. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A readable content extraction library** · free - Why: Capturing the whole page markup stores navigation and advertisements, which pollutes search and wastes space. - Get it: A maintained readability extraction library, tested against pages you actually read. - Verify: A news article, a documentation page and a long blog post all extract cleanly - [ ] **A month of your own real reading** · free, and it takes a month - Why: Resurfacing quality cannot be evaluated on a synthetic set, since the whole question is whether an old item is genuinely relevant to a new one, and only you can judge that. - Get it: Use the capture for a month before building phase 4, so there is something real to surface. - Verify: At least two hundred captures with some highlights exist - [ ] **What counts as a useful moment** · free - Why: Phase 4 is the whole product and a wrong answer produces an annoying tool, so decide what triggers you will support before implementing any of them. - Get it: Note the last five times you wished you could find something you had read, and what you were doing at the time. Those are your triggers. - Verify: A written note lists at least three trigger situations from your own experience ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. - Sync across devices and browsers, so your phone reading never enters the index - Capture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers - Robust extraction on SPAs, infinite scrolls, PDFs, and anything behind a login wall - Curation judgment: your version will resurface plenty of pricing pages and airline confirmations - Someone else maintaining it against Chrome manifest changes that break extensions on a schedule If one of those is essential to you, that is the reason to keep paying for Resurf, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Resurf The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local capture tool that brings back what you read when it becomes relevant again to replace Resurf. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A browser extension and a hotkey, both storing the readable text · a link rots and a page changes, so the text is what you actually saved - Storage: Local SQLite with full text search · reading history is a record of what you were thinking about - Extraction: Readable content extraction at capture time, kept verbatim · extracting later means extracting from a page that has changed or gone - Surfacing: Context-triggered rather than scheduled · a daily digest of old links is unread within a week - Runtime: A local application with no account and no sync by default · the value does not require a service and the privacy cost of one is high ### Data model (create this before Phase 1) - captures(id, url, title, text, excerpt, captured_at, source, reading_time_ms) · text stored at capture, not fetched later - highlights(id, capture_id, text, note, at) · what you actually marked, which is a much stronger signal than the capture - topics(id, label) and capture_topics(capture_id, topic_id, confidence) · derived, never asked for at capture - surfaces(id, capture_id, trigger_kind, triggered_at, acted_on, dismissed) · what was shown, why, and what you did - dismissals(capture_id, count, last_at) · dismissing the same thing three times means stop - Everything findable is derived after capture, because any field asked for at save time is a decision that slows saving down until saving stops. ### Phase 1 · Capturing without deciding anything Build: A hotkey and an extension button that capture the page's readable text, title and URL with no prompt for tags, folders or notes. Store the extracted text at capture time. Measure how long capture takes. Trap: Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. Done when: Capture completes in one action with no prompt of any kind, timed by hand. The readable text is stored at capture rather than fetched later. A page behind a login or a paywall captures what was on screen rather than failing. Capture works with no network available. Do not build yet: No highlights, topics or surfacing. ### Phase 2 · Highlights, which mean far more than captures Build: Capture highlighted text with optional notes, anchored to the stored text. Treat a highlight as a much stronger relevance signal than a capture. Handle highlighting on a page whose text differs slightly from what was captured. Trap: Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. Done when: Highlights are stored with their surrounding context and anchored to the capture's text. A highlight on a slightly changed page still anchors or is recorded as unanchored rather than lost. Highlights are counted and weighted separately from captures. Adding a note to a highlight does not require leaving the page. Do not build yet: No topics or surfacing yet. ### Phase 3 · Finding it when you go looking Build: Full text search over captured text, highlights and notes with date and domain filters. Derive topics after capture. Make search fast enough at ten thousand captures to be the default way in. Trap: Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. Done when: Searching a phrase from the body of an article finds it, tested against a real capture set. Search across ten thousand captures returns as you type. Topics are derived rather than entered. Date and domain filters combine with text search. Do not build yet: No surfacing or resurfacing yet. ### Phase 4 · Bringing things back at a useful moment Build: Surface captures triggered by context: what you are currently reading, what you searched for, or a topic you have returned to. Record every surface and whether it was acted on. Stop surfacing something dismissed repeatedly. Trap: A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. Done when: Reading a page related to an earlier capture surfaces it, verified against a real capture set. Every surface records its trigger and whether it was acted on. An item dismissed three times stops appearing. There is no daily or weekly digest, verified by looking for one. Do not build yet: No export or backup yet. ### Phase 5 · Keeping it, and being able to leave Build: Export everything as readable files with highlights and notes preserved. Handle the database growing over years. Optional sync through storage you control, off by default. Trap: Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal. Done when: Export produces readable files with highlights and notes preserved and re-imports identically. The database remains manageable after years of captures, verified against a large synthetic set. Sync is off by default and goes through storage you control. Deleting a capture removes its highlights, topics and surface history. Do not build yet: Nothing after this. ### Out of scope (and why) - Any account or hosted service, which is what turns a private reading record into a product with a privacy policy. - Mobile capture, which is where a lot of reading happens and would need a different architecture. - Social features and shared collections, which change what people are willing to save. - Archiving full page appearance, which is a much larger storage problem than text. ### README must contain - That capture asks for nothing, because a decision at capture time is what kills the habit - That the readable text is stored at capture rather than fetched later from a changed page - That highlights are weighted far above captures because they are the stronger signal - That resurfacing is triggered by context and there is no scheduled digest - That old captures are compressed rather than deleted, since deleting removes what resurfacing runs on ===== AGENTS.md ===== # Agent instructions · Resurf indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A browser extension and a hotkey, both storing the readable text, Local SQLite with full text search, Readable content extraction at capture time, kept verbatim, Context-triggered rather than scheduled, A local application with no account and no sync by default. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. ===== BUILD_PLAN.md ===== # Build plan · Resurf A reading capture tool whose real problem is the return path: saving is easy and everyone does it, but a bookmark folder nobody opens is not a memory aid, so the engineering is in surfacing the right thing at a moment when it is useful rather than at a scheduled time nobody wants. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capturing without deciding anything Saving something takes one action and no thought. ### Steps 1. Capture with no prompt No tags, no folder, no note. The absence is the design. Files: `extension/capture.ts` 2. Store the extracted text now Later means a page that changed or disappeared. Files: `src/extract.ts` 3. Work offline The text is on screen. Capture should not need a network. Files: `src/capture.ts` ### Done when - [ ] Capture is one action with no prompt - [ ] Readable text is stored at capture time - [ ] A logged-in page captures what was on screen - [ ] Capture works with no network ### Watch out - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. ## Phase 2 · Highlights, which mean far more than captures What you marked is distinguished from what you merely saved. ### Steps 1. Anchor highlights to the stored text With surrounding context, so a small change does not orphan them. Files: `src/highlights.ts` 2. Weight highlights far above captures People save everything and highlight only what landed. Files: `src/signals.ts` 3. Keep note-taking inline Leaving the page to add a note is a decision, and decisions stop habits. Files: `extension/highlight.ts` ### Done when - [ ] Highlights anchor with context - [ ] A slightly changed page anchors or records unanchored - [ ] Highlights are weighted above captures - [ ] Notes can be added without leaving the page ### Watch out - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. ## Phase 3 · Finding it when you go looking Full text search across everything you read finds it instantly. ### Steps 1. Index the full captured text Titles and URLs find nothing. You remember phrases. Files: `src/search.ts` 2. Derive topics after capture Never ask at capture. That was phase one's whole point. Files: `src/topics.ts` 3. Keep search instant at scale Ten thousand captures. Slower than typing means it is not the default way in. Files: `src/search.ts` ### Done when - [ ] A phrase from an article body is found - [ ] Ten thousand captures search as you type - [ ] Topics are derived, not entered - [ ] Date and domain filters combine with text ### Watch out - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. ## Phase 4 · Bringing things back at a useful moment Something you saved reappears when it is relevant, not on a schedule. ### Steps 1. Trigger on what you are doing now The related page is the moment the old one becomes interesting. Files: `src/surface.ts` 2. Record surfaces and outcomes So you can tell whether resurfacing works rather than assuming. Files: `src/surfaces.ts` 3. Stop after repeated dismissal Three dismissals is an answer. Files: `src/dismissals.ts` ### Done when - [ ] A related page surfaces an earlier capture - [ ] Surfaces record trigger and outcome - [ ] Repeated dismissal stops resurfacing - [ ] No scheduled digest exists ### Watch out - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. ## Not in this build - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. ## After v1, if you want it - Surfacing while you write rather than only while you read, which is the harder half - Grouping captures into a thread when you return to a topic repeatedly - A weekly view of what you actually read, which is different from what you saved ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything you have read. A record of what you were thinking about. DB_PATH=~/.local/share/resurf/captures.db # Required. Present as a constant so the answer is visible. A prompt is a decision and decisions end habits. CAPTURE_PROMPT=never # Required. How many dismissals before an item stops resurfacing. Three is an answer. DISMISS_LIMIT=3 # Required. Present as a constant. A scheduled digest of old links is unread within a week. SCHEDULED_DIGEST=never # Required. Old text is compressed rather than deleted, since deleting removes what resurfacing runs on. COMPRESS_AFTER_DAYS=180 # Optional. Optional and off by default. Absent means one machine, which is a fine way to run. SYNC_REMOTE=s3://my-reading/archive
You are building a lean indie version of Resurf. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Resurf · indie build A reading capture tool whose real problem is the return path: saving is easy and everyone does it, but a bookmark folder nobody opens is not a memory aid, so the engineering is in surfacing the right thing at a moment when it is useful rather than at a scheduled time nobody wants. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension and a hotkey, both storing the readable text | a link rots and a page changes, so the text is what you actually saved | | Storage | Local SQLite with full text search | reading history is a record of what you were thinking about | | Extraction | Readable content extraction at capture time, kept verbatim | extracting later means extracting from a page that has changed or gone | | Surfacing | Context-triggered rather than scheduled | a daily digest of old links is unread within a week | | Runtime | A local application with no account and no sync by default | the value does not require a service and the privacy cost of one is high | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application and builds the extension. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A readable content extraction library** · free - Why: Capturing the whole page markup stores navigation and advertisements, which pollutes search and wastes space. - Get it: A maintained readability extraction library, tested against pages you actually read. - Verify: A news article, a documentation page and a long blog post all extract cleanly - [ ] **A month of your own real reading** · free, and it takes a month - Why: Resurfacing quality cannot be evaluated on a synthetic set, since the whole question is whether an old item is genuinely relevant to a new one, and only you can judge that. - Get it: Use the capture for a month before building phase 4, so there is something real to surface. - Verify: At least two hundred captures with some highlights exist - [ ] **What counts as a useful moment** · free - Why: Phase 4 is the whole product and a wrong answer produces an annoying tool, so decide what triggers you will support before implementing any of them. - Get it: Note the last five times you wished you could find something you had read, and what you were doing at the time. Those are your triggers. - Verify: A written note lists at least three trigger situations from your own experience ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. - Sync across devices and browsers, so your phone reading never enters the index - Capture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers - Robust extraction on SPAs, infinite scrolls, PDFs, and anything behind a login wall - Curation judgment: your version will resurface plenty of pricing pages and airline confirmations - Someone else maintaining it against Chrome manifest changes that break extensions on a schedule If one of those is essential to you, that is the reason to keep paying for Resurf, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Resurf The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local capture tool that brings back what you read when it becomes relevant again to replace Resurf. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A browser extension and a hotkey, both storing the readable text · a link rots and a page changes, so the text is what you actually saved - Storage: Local SQLite with full text search · reading history is a record of what you were thinking about - Extraction: Readable content extraction at capture time, kept verbatim · extracting later means extracting from a page that has changed or gone - Surfacing: Context-triggered rather than scheduled · a daily digest of old links is unread within a week - Runtime: A local application with no account and no sync by default · the value does not require a service and the privacy cost of one is high ### Data model (create this before Phase 1) - captures(id, url, title, text, excerpt, captured_at, source, reading_time_ms) · text stored at capture, not fetched later - highlights(id, capture_id, text, note, at) · what you actually marked, which is a much stronger signal than the capture - topics(id, label) and capture_topics(capture_id, topic_id, confidence) · derived, never asked for at capture - surfaces(id, capture_id, trigger_kind, triggered_at, acted_on, dismissed) · what was shown, why, and what you did - dismissals(capture_id, count, last_at) · dismissing the same thing three times means stop - Everything findable is derived after capture, because any field asked for at save time is a decision that slows saving down until saving stops. ### Phase 1 · Capturing without deciding anything Build: A hotkey and an extension button that capture the page's readable text, title and URL with no prompt for tags, folders or notes. Store the extracted text at capture time. Measure how long capture takes. Trap: Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. Done when: Capture completes in one action with no prompt of any kind, timed by hand. The readable text is stored at capture rather than fetched later. A page behind a login or a paywall captures what was on screen rather than failing. Capture works with no network available. Do not build yet: No highlights, topics or surfacing. ### Phase 2 · Highlights, which mean far more than captures Build: Capture highlighted text with optional notes, anchored to the stored text. Treat a highlight as a much stronger relevance signal than a capture. Handle highlighting on a page whose text differs slightly from what was captured. Trap: Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. Done when: Highlights are stored with their surrounding context and anchored to the capture's text. A highlight on a slightly changed page still anchors or is recorded as unanchored rather than lost. Highlights are counted and weighted separately from captures. Adding a note to a highlight does not require leaving the page. Do not build yet: No topics or surfacing yet. ### Phase 3 · Finding it when you go looking Build: Full text search over captured text, highlights and notes with date and domain filters. Derive topics after capture. Make search fast enough at ten thousand captures to be the default way in. Trap: Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. Done when: Searching a phrase from the body of an article finds it, tested against a real capture set. Search across ten thousand captures returns as you type. Topics are derived rather than entered. Date and domain filters combine with text search. Do not build yet: No surfacing or resurfacing yet. ### Phase 4 · Bringing things back at a useful moment Build: Surface captures triggered by context: what you are currently reading, what you searched for, or a topic you have returned to. Record every surface and whether it was acted on. Stop surfacing something dismissed repeatedly. Trap: A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. Done when: Reading a page related to an earlier capture surfaces it, verified against a real capture set. Every surface records its trigger and whether it was acted on. An item dismissed three times stops appearing. There is no daily or weekly digest, verified by looking for one. Do not build yet: No export or backup yet. ### Phase 5 · Keeping it, and being able to leave Build: Export everything as readable files with highlights and notes preserved. Handle the database growing over years. Optional sync through storage you control, off by default. Trap: Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal. Done when: Export produces readable files with highlights and notes preserved and re-imports identically. The database remains manageable after years of captures, verified against a large synthetic set. Sync is off by default and goes through storage you control. Deleting a capture removes its highlights, topics and surface history. Do not build yet: Nothing after this. ### Out of scope (and why) - Any account or hosted service, which is what turns a private reading record into a product with a privacy policy. - Mobile capture, which is where a lot of reading happens and would need a different architecture. - Social features and shared collections, which change what people are willing to save. - Archiving full page appearance, which is a much larger storage problem than text. ### README must contain - That capture asks for nothing, because a decision at capture time is what kills the habit - That the readable text is stored at capture rather than fetched later from a changed page - That highlights are weighted far above captures because they are the stronger signal - That resurfacing is triggered by context and there is no scheduled digest - That old captures are compressed rather than deleted, since deleting removes what resurfacing runs on ===== AGENTS.md ===== # Agent instructions · Resurf indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A browser extension and a hotkey, both storing the readable text, Local SQLite with full text search, Readable content extraction at capture time, kept verbatim, Context-triggered rather than scheduled, A local application with no account and no sync by default. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. ===== BUILD_PLAN.md ===== # Build plan · Resurf A reading capture tool whose real problem is the return path: saving is easy and everyone does it, but a bookmark folder nobody opens is not a memory aid, so the engineering is in surfacing the right thing at a moment when it is useful rather than at a scheduled time nobody wants. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capturing without deciding anything Saving something takes one action and no thought. ### Steps 1. Capture with no prompt No tags, no folder, no note. The absence is the design. Files: `extension/capture.ts` 2. Store the extracted text now Later means a page that changed or disappeared. Files: `src/extract.ts` 3. Work offline The text is on screen. Capture should not need a network. Files: `src/capture.ts` ### Done when - [ ] Capture is one action with no prompt - [ ] Readable text is stored at capture time - [ ] A logged-in page captures what was on screen - [ ] Capture works with no network ### Watch out - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. ## Phase 2 · Highlights, which mean far more than captures What you marked is distinguished from what you merely saved. ### Steps 1. Anchor highlights to the stored text With surrounding context, so a small change does not orphan them. Files: `src/highlights.ts` 2. Weight highlights far above captures People save everything and highlight only what landed. Files: `src/signals.ts` 3. Keep note-taking inline Leaving the page to add a note is a decision, and decisions stop habits. Files: `extension/highlight.ts` ### Done when - [ ] Highlights anchor with context - [ ] A slightly changed page anchors or records unanchored - [ ] Highlights are weighted above captures - [ ] Notes can be added without leaving the page ### Watch out - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. ## Phase 3 · Finding it when you go looking Full text search across everything you read finds it instantly. ### Steps 1. Index the full captured text Titles and URLs find nothing. You remember phrases. Files: `src/search.ts` 2. Derive topics after capture Never ask at capture. That was phase one's whole point. Files: `src/topics.ts` 3. Keep search instant at scale Ten thousand captures. Slower than typing means it is not the default way in. Files: `src/search.ts` ### Done when - [ ] A phrase from an article body is found - [ ] Ten thousand captures search as you type - [ ] Topics are derived, not entered - [ ] Date and domain filters combine with text ### Watch out - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. ## Phase 4 · Bringing things back at a useful moment Something you saved reappears when it is relevant, not on a schedule. ### Steps 1. Trigger on what you are doing now The related page is the moment the old one becomes interesting. Files: `src/surface.ts` 2. Record surfaces and outcomes So you can tell whether resurfacing works rather than assuming. Files: `src/surfaces.ts` 3. Stop after repeated dismissal Three dismissals is an answer. Files: `src/dismissals.ts` ### Done when - [ ] A related page surfaces an earlier capture - [ ] Surfaces record trigger and outcome - [ ] Repeated dismissal stops resurfacing - [ ] No scheduled digest exists ### Watch out - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. ## Not in this build - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. ## After v1, if you want it - Surfacing while you write rather than only while you read, which is the harder half - Grouping captures into a thread when you return to a topic repeatedly - A weekly view of what you actually read, which is different from what you saved ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything you have read. A record of what you were thinking about. DB_PATH=~/.local/share/resurf/captures.db # Required. Present as a constant so the answer is visible. A prompt is a decision and decisions end habits. CAPTURE_PROMPT=never # Required. How many dismissals before an item stops resurfacing. Three is an answer. DISMISS_LIMIT=3 # Required. Present as a constant. A scheduled digest of old links is unread within a week. SCHEDULED_DIGEST=never # Required. Old text is compressed rather than deleted, since deleting removes what resurfacing runs on. COMPRESS_AFTER_DAYS=180 # Optional. Optional and off by default. Absent means one machine, which is a fine way to run. SYNC_REMOTE=s3://my-reading/archive
You are building a production product version of Resurf. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Resurf · product brief ## Problem The core mechanic is unglamorous and very buildable: capture the text of pages you spend real time on, embed it, store it locally, then search and resurface it. An agent can get a Chrome extension doing that in a weekend, and because the corpus is your own browsing, there is no data moat to lose. Where the DIY version cracks is everything around the loop: cross-device sync, mobile and Safari, capture quality on SPAs and paywalled or auth-gated pages, and the taste of deciding what deserves to come back. Also the honest failure mode of every personal recall tool, which is that you stop opening it after week three. Buildable, yes; pleasant enough that you keep it, less certain. ## Product outcome A reading record that costs nothing to add to, finds a phrase from the middle of an article instantly, and brings something back when you are reading about it rather than on a schedule you ignore. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Chrome or any Chromium browser you are willing to load an unpacked extension into - Node and pnpm for the build - Disk space for a few hundred MB of page text and vectors - Optional OpenAI key if you want better embeddings than the on-device model ## Explicit non-goals for v1 - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. - Sync across devices and browsers, so your phone reading never enters the index - Capture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers - Robust extraction on SPAs, infinite scrolls, PDFs, and anything behind a login wall - Curation judgment: your version will resurface plenty of pricing pages and airline confirmations - Someone else maintaining it against Chrome manifest changes that break extensions on a schedule ## Success criteria - Capture is confirmed to be one action with no prompt - Readable text is confirmed stored at capture time - A phrase from an article body is confirmed findable - Repeated dismissal is confirmed to stop resurfacing - No scheduled digest is confirmed to exist ===== BRIEF.md ===== # Build brief · Resurf The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local capture tool that brings back what you read when it becomes relevant again to replace Resurf. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A browser extension and a hotkey, both storing the readable text · a link rots and a page changes, so the text is what you actually saved - Storage: Local SQLite with full text search · reading history is a record of what you were thinking about - Extraction: Readable content extraction at capture time, kept verbatim · extracting later means extracting from a page that has changed or gone - Surfacing: Context-triggered rather than scheduled · a daily digest of old links is unread within a week - Runtime: A local application with no account and no sync by default · the value does not require a service and the privacy cost of one is high ### Data model (create this before Phase 1) - captures(id, url, title, text, excerpt, captured_at, source, reading_time_ms) · text stored at capture, not fetched later - highlights(id, capture_id, text, note, at) · what you actually marked, which is a much stronger signal than the capture - topics(id, label) and capture_topics(capture_id, topic_id, confidence) · derived, never asked for at capture - surfaces(id, capture_id, trigger_kind, triggered_at, acted_on, dismissed) · what was shown, why, and what you did - dismissals(capture_id, count, last_at) · dismissing the same thing three times means stop - Everything findable is derived after capture, because any field asked for at save time is a decision that slows saving down until saving stops. ### Phase 1 · Capturing without deciding anything Build: A hotkey and an extension button that capture the page's readable text, title and URL with no prompt for tags, folders or notes. Store the extracted text at capture time. Measure how long capture takes. Trap: Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. Done when: Capture completes in one action with no prompt of any kind, timed by hand. The readable text is stored at capture rather than fetched later. A page behind a login or a paywall captures what was on screen rather than failing. Capture works with no network available. Do not build yet: No highlights, topics or surfacing. ### Phase 2 · Highlights, which mean far more than captures Build: Capture highlighted text with optional notes, anchored to the stored text. Treat a highlight as a much stronger relevance signal than a capture. Handle highlighting on a page whose text differs slightly from what was captured. Trap: Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. Done when: Highlights are stored with their surrounding context and anchored to the capture's text. A highlight on a slightly changed page still anchors or is recorded as unanchored rather than lost. Highlights are counted and weighted separately from captures. Adding a note to a highlight does not require leaving the page. Do not build yet: No topics or surfacing yet. ### Phase 3 · Finding it when you go looking Build: Full text search over captured text, highlights and notes with date and domain filters. Derive topics after capture. Make search fast enough at ten thousand captures to be the default way in. Trap: Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. Done when: Searching a phrase from the body of an article finds it, tested against a real capture set. Search across ten thousand captures returns as you type. Topics are derived rather than entered. Date and domain filters combine with text search. Do not build yet: No surfacing or resurfacing yet. ### Phase 4 · Bringing things back at a useful moment Build: Surface captures triggered by context: what you are currently reading, what you searched for, or a topic you have returned to. Record every surface and whether it was acted on. Stop surfacing something dismissed repeatedly. Trap: A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. Done when: Reading a page related to an earlier capture surfaces it, verified against a real capture set. Every surface records its trigger and whether it was acted on. An item dismissed three times stops appearing. There is no daily or weekly digest, verified by looking for one. Do not build yet: No export or backup yet. ### Phase 5 · Keeping it, and being able to leave Build: Export everything as readable files with highlights and notes preserved. Handle the database growing over years. Optional sync through storage you control, off by default. Trap: Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal. Done when: Export produces readable files with highlights and notes preserved and re-imports identically. The database remains manageable after years of captures, verified against a large synthetic set. Sync is off by default and goes through storage you control. Deleting a capture removes its highlights, topics and surface history. Do not build yet: Nothing after this. ### Out of scope (and why) - Any account or hosted service, which is what turns a private reading record into a product with a privacy policy. - Mobile capture, which is where a lot of reading happens and would need a different architecture. - Social features and shared collections, which change what people are willing to save. - Archiving full page appearance, which is a much larger storage problem than text. ### README must contain - That capture asks for nothing, because a decision at capture time is what kills the habit - That the readable text is stored at capture rather than fetched later from a changed page - That highlights are weighted far above captures because they are the stronger signal - That resurfacing is triggered by context and there is no scheduled digest - That old captures are compressed rather than deleted, since deleting removes what resurfacing runs on ===== ARCHITECTURE.md ===== # Architecture · Resurf ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension and a hotkey, both storing the readable text | a link rots and a page changes, so the text is what you actually saved | | Storage | Local SQLite with full text search | reading history is a record of what you were thinking about | | Extraction | Readable content extraction at capture time, kept verbatim | extracting later means extracting from a page that has changed or gone | | Surfacing | Context-triggered rather than scheduled | a daily digest of old links is unread within a week | | Runtime | A local application with no account and no sync by default | the value does not require a service and the privacy cost of one is high | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | One-action saving with no prompt. | Any implementation, provided nothing is asked at capture time. | | Highlights | Anchored marks with heavy weighting. | Any anchoring, provided highlights outweigh captures downstream. | | Search | Full text over captured content. | Any index, provided it covers body text rather than titles. | | Surfacing | Context triggers and dismissal limits. | Any trigger set, provided it is not a schedule. | | Retention | Compression rather than deletion. | Any policy, provided old captures survive since they are what resurfacing uses. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Everything you have read. A record of what you were thinking about. - `CAPTURE_PROMPT` · required · Present as a constant so the answer is visible. A prompt is a decision and decisions end habits. - `DISMISS_LIMIT` · required · How many dismissals before an item stops resurfacing. Three is an answer. - `SCHEDULED_DIGEST` · required · Present as a constant. A scheduled digest of old links is unread within a week. - `COMPRESS_AFTER_DAYS` · required · Old text is compressed rather than deleted, since deleting removes what resurfacing runs on. - `SYNC_REMOTE` · optional · Optional and off by default. Absent means one machine, which is a fine way to run. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Resurf product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A browser extension and a hotkey, both storing the readable text, Local SQLite with full text search, Readable content extraction at capture time, kept verbatim, Context-triggered rather than scheduled, A local application with no account and no sync by default. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. - Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal. ===== MILESTONES.md ===== # Delivery milestones · Resurf Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capturing without deciding anything Saving something takes one action and no thought. ### Steps 1. Capture with no prompt No tags, no folder, no note. The absence is the design. Files: `extension/capture.ts` 2. Store the extracted text now Later means a page that changed or disappeared. Files: `src/extract.ts` 3. Work offline The text is on screen. Capture should not need a network. Files: `src/capture.ts` ### Done when - [ ] Capture is one action with no prompt - [ ] Readable text is stored at capture time - [ ] A logged-in page captures what was on screen - [ ] Capture works with no network ### Watch out - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. ## M2 · Highlights, which mean far more than captures What you marked is distinguished from what you merely saved. ### Steps 1. Anchor highlights to the stored text With surrounding context, so a small change does not orphan them. Files: `src/highlights.ts` 2. Weight highlights far above captures People save everything and highlight only what landed. Files: `src/signals.ts` 3. Keep note-taking inline Leaving the page to add a note is a decision, and decisions stop habits. Files: `extension/highlight.ts` ### Done when - [ ] Highlights anchor with context - [ ] A slightly changed page anchors or records unanchored - [ ] Highlights are weighted above captures - [ ] Notes can be added without leaving the page ### Watch out - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. ## M3 · Finding it when you go looking Full text search across everything you read finds it instantly. ### Steps 1. Index the full captured text Titles and URLs find nothing. You remember phrases. Files: `src/search.ts` 2. Derive topics after capture Never ask at capture. That was phase one's whole point. Files: `src/topics.ts` 3. Keep search instant at scale Ten thousand captures. Slower than typing means it is not the default way in. Files: `src/search.ts` ### Done when - [ ] A phrase from an article body is found - [ ] Ten thousand captures search as you type - [ ] Topics are derived, not entered - [ ] Date and domain filters combine with text ### Watch out - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. ## M4 · Bringing things back at a useful moment Something you saved reappears when it is relevant, not on a schedule. ### Steps 1. Trigger on what you are doing now The related page is the moment the old one becomes interesting. Files: `src/surface.ts` 2. Record surfaces and outcomes So you can tell whether resurfacing works rather than assuming. Files: `src/surfaces.ts` 3. Stop after repeated dismissal Three dismissals is an answer. Files: `src/dismissals.ts` ### Done when - [ ] A related page surfaces an earlier capture - [ ] Surfaces record trigger and outcome - [ ] Repeated dismissal stops resurfacing - [ ] No scheduled digest exists ### Watch out - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. ## M5 · Keeping it, and being able to leave (production only) Years of reading survive and can move somewhere else. ### Steps 1. Export to readable files Round trip tested. A reading archive should not be trapped. Files: `src/export.ts`, `tests/roundtrip.test.ts` 2. Compress old text, never delete it Deleting old captures removes what resurfacing runs on. Files: `src/compact.ts` 3. Make sync optional and yours Off by default, through storage you control if on. Files: `src/sync.ts` ### Done when - [ ] Export round trips with highlights and notes - [ ] The database stays manageable over years - [ ] Sync is off by default and self-hosted - [ ] Deleting removes everything attached ### Watch out - Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal. ===== OPERATIONS.md ===== # Operations · Resurf ## Backup A reading archive accumulates value over years and is not reconstructible. Back up the database, and run periodic exports as a format-independent copy since the archive should outlive this application. ## Restore Restore and confirm highlights and surface history survived, since the history is what prevents already-dismissed items from reappearing and its loss makes the tool immediately annoying. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a personal tool the number worth watching is the proportion of surfaced items acted on. A falling rate means the triggers have stopped matching what you actually need. ## Incident checklist Capture becoming slow is the failure that ends the habit, and it is usually extraction running synchronously in the capture path. Store first and extract afterwards if it comes to that. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Capture is confirmed to be one action with no prompt - [ ] Readable text is confirmed stored at capture time - [ ] A phrase from an article body is confirmed findable - [ ] Repeated dismissal is confirmed to stop resurfacing - [ ] No scheduled digest is confirmed to exist ## Launch constraint Do not market omitted Resurf capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything you have read. A record of what you were thinking about. DB_PATH=~/.local/share/resurf/captures.db # Required. Present as a constant so the answer is visible. A prompt is a decision and decisions end habits. CAPTURE_PROMPT=never # Required. How many dismissals before an item stops resurfacing. Three is an answer. DISMISS_LIMIT=3 # Required. Present as a constant. A scheduled digest of old links is unread within a week. SCHEDULED_DIGEST=never # Required. Old text is compressed rather than deleted, since deleting removes what resurfacing runs on. COMPRESS_AFTER_DAYS=180 # Optional. Optional and off by default. Absent means one machine, which is a fine way to run. SYNC_REMOTE=s3://my-reading/archive
# Resurf · indie build A reading capture tool whose real problem is the return path: saving is easy and everyone does it, but a bookmark folder nobody opens is not a memory aid, so the engineering is in surfacing the right thing at a moment when it is useful rather than at a scheduled time nobody wants. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension and a hotkey, both storing the readable text | a link rots and a page changes, so the text is what you actually saved | | Storage | Local SQLite with full text search | reading history is a record of what you were thinking about | | Extraction | Readable content extraction at capture time, kept verbatim | extracting later means extracting from a page that has changed or gone | | Surfacing | Context-triggered rather than scheduled | a daily digest of old links is unread within a week | | Runtime | A local application with no account and no sync by default | the value does not require a service and the privacy cost of one is high | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the local application and builds the extension. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **A readable content extraction library** · free - Why: Capturing the whole page markup stores navigation and advertisements, which pollutes search and wastes space. - Get it: A maintained readability extraction library, tested against pages you actually read. - Verify: A news article, a documentation page and a long blog post all extract cleanly - [ ] **A month of your own real reading** · free, and it takes a month - Why: Resurfacing quality cannot be evaluated on a synthetic set, since the whole question is whether an old item is genuinely relevant to a new one, and only you can judge that. - Get it: Use the capture for a month before building phase 4, so there is something real to surface. - Verify: At least two hundred captures with some highlights exist - [ ] **What counts as a useful moment** · free - Why: Phase 4 is the whole product and a wrong answer produces an annoying tool, so decide what triggers you will support before implementing any of them. - Get it: Note the last five times you wished you could find something you had read, and what you were doing at the time. Those are your triggers. - Verify: A written note lists at least three trigger situations from your own experience ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. - Sync across devices and browsers, so your phone reading never enters the index - Capture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers - Robust extraction on SPAs, infinite scrolls, PDFs, and anything behind a login wall - Curation judgment: your version will resurface plenty of pricing pages and airline confirmations - Someone else maintaining it against Chrome manifest changes that break extensions on a schedule If one of those is essential to you, that is the reason to keep paying for Resurf, and the README should say so rather than pretend.
# Build brief · Resurf The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local capture tool that brings back what you read when it becomes relevant again to replace Resurf. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A browser extension and a hotkey, both storing the readable text · a link rots and a page changes, so the text is what you actually saved - Storage: Local SQLite with full text search · reading history is a record of what you were thinking about - Extraction: Readable content extraction at capture time, kept verbatim · extracting later means extracting from a page that has changed or gone - Surfacing: Context-triggered rather than scheduled · a daily digest of old links is unread within a week - Runtime: A local application with no account and no sync by default · the value does not require a service and the privacy cost of one is high ### Data model (create this before Phase 1) - captures(id, url, title, text, excerpt, captured_at, source, reading_time_ms) · text stored at capture, not fetched later - highlights(id, capture_id, text, note, at) · what you actually marked, which is a much stronger signal than the capture - topics(id, label) and capture_topics(capture_id, topic_id, confidence) · derived, never asked for at capture - surfaces(id, capture_id, trigger_kind, triggered_at, acted_on, dismissed) · what was shown, why, and what you did - dismissals(capture_id, count, last_at) · dismissing the same thing three times means stop - Everything findable is derived after capture, because any field asked for at save time is a decision that slows saving down until saving stops. ### Phase 1 · Capturing without deciding anything Build: A hotkey and an extension button that capture the page's readable text, title and URL with no prompt for tags, folders or notes. Store the extracted text at capture time. Measure how long capture takes. Trap: Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. Done when: Capture completes in one action with no prompt of any kind, timed by hand. The readable text is stored at capture rather than fetched later. A page behind a login or a paywall captures what was on screen rather than failing. Capture works with no network available. Do not build yet: No highlights, topics or surfacing. ### Phase 2 · Highlights, which mean far more than captures Build: Capture highlighted text with optional notes, anchored to the stored text. Treat a highlight as a much stronger relevance signal than a capture. Handle highlighting on a page whose text differs slightly from what was captured. Trap: Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. Done when: Highlights are stored with their surrounding context and anchored to the capture's text. A highlight on a slightly changed page still anchors or is recorded as unanchored rather than lost. Highlights are counted and weighted separately from captures. Adding a note to a highlight does not require leaving the page. Do not build yet: No topics or surfacing yet. ### Phase 3 · Finding it when you go looking Build: Full text search over captured text, highlights and notes with date and domain filters. Derive topics after capture. Make search fast enough at ten thousand captures to be the default way in. Trap: Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. Done when: Searching a phrase from the body of an article finds it, tested against a real capture set. Search across ten thousand captures returns as you type. Topics are derived rather than entered. Date and domain filters combine with text search. Do not build yet: No surfacing or resurfacing yet. ### Phase 4 · Bringing things back at a useful moment Build: Surface captures triggered by context: what you are currently reading, what you searched for, or a topic you have returned to. Record every surface and whether it was acted on. Stop surfacing something dismissed repeatedly. Trap: A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. Done when: Reading a page related to an earlier capture surfaces it, verified against a real capture set. Every surface records its trigger and whether it was acted on. An item dismissed three times stops appearing. There is no daily or weekly digest, verified by looking for one. Do not build yet: No export or backup yet. ### Phase 5 · Keeping it, and being able to leave Build: Export everything as readable files with highlights and notes preserved. Handle the database growing over years. Optional sync through storage you control, off by default. Trap: Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal. Done when: Export produces readable files with highlights and notes preserved and re-imports identically. The database remains manageable after years of captures, verified against a large synthetic set. Sync is off by default and goes through storage you control. Deleting a capture removes its highlights, topics and surface history. Do not build yet: Nothing after this. ### Out of scope (and why) - Any account or hosted service, which is what turns a private reading record into a product with a privacy policy. - Mobile capture, which is where a lot of reading happens and would need a different architecture. - Social features and shared collections, which change what people are willing to save. - Archiving full page appearance, which is a much larger storage problem than text. ### README must contain - That capture asks for nothing, because a decision at capture time is what kills the habit - That the readable text is stored at capture rather than fetched later from a changed page - That highlights are weighted far above captures because they are the stronger signal - That resurfacing is triggered by context and there is no scheduled digest - That old captures are compressed rather than deleted, since deleting removes what resurfacing runs on
# Agent instructions · Resurf indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A browser extension and a hotkey, both storing the readable text, Local SQLite with full text search, Readable content extraction at capture time, kept verbatim, Context-triggered rather than scheduled, A local application with no account and no sync by default. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing.
# Build plan · Resurf A reading capture tool whose real problem is the return path: saving is easy and everyone does it, but a bookmark folder nobody opens is not a memory aid, so the engineering is in surfacing the right thing at a moment when it is useful rather than at a scheduled time nobody wants. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capturing without deciding anything Saving something takes one action and no thought. ### Steps 1. Capture with no prompt No tags, no folder, no note. The absence is the design. Files: `extension/capture.ts` 2. Store the extracted text now Later means a page that changed or disappeared. Files: `src/extract.ts` 3. Work offline The text is on screen. Capture should not need a network. Files: `src/capture.ts` ### Done when - [ ] Capture is one action with no prompt - [ ] Readable text is stored at capture time - [ ] A logged-in page captures what was on screen - [ ] Capture works with no network ### Watch out - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. ## Phase 2 · Highlights, which mean far more than captures What you marked is distinguished from what you merely saved. ### Steps 1. Anchor highlights to the stored text With surrounding context, so a small change does not orphan them. Files: `src/highlights.ts` 2. Weight highlights far above captures People save everything and highlight only what landed. Files: `src/signals.ts` 3. Keep note-taking inline Leaving the page to add a note is a decision, and decisions stop habits. Files: `extension/highlight.ts` ### Done when - [ ] Highlights anchor with context - [ ] A slightly changed page anchors or records unanchored - [ ] Highlights are weighted above captures - [ ] Notes can be added without leaving the page ### Watch out - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. ## Phase 3 · Finding it when you go looking Full text search across everything you read finds it instantly. ### Steps 1. Index the full captured text Titles and URLs find nothing. You remember phrases. Files: `src/search.ts` 2. Derive topics after capture Never ask at capture. That was phase one's whole point. Files: `src/topics.ts` 3. Keep search instant at scale Ten thousand captures. Slower than typing means it is not the default way in. Files: `src/search.ts` ### Done when - [ ] A phrase from an article body is found - [ ] Ten thousand captures search as you type - [ ] Topics are derived, not entered - [ ] Date and domain filters combine with text ### Watch out - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. ## Phase 4 · Bringing things back at a useful moment Something you saved reappears when it is relevant, not on a schedule. ### Steps 1. Trigger on what you are doing now The related page is the moment the old one becomes interesting. Files: `src/surface.ts` 2. Record surfaces and outcomes So you can tell whether resurfacing works rather than assuming. Files: `src/surfaces.ts` 3. Stop after repeated dismissal Three dismissals is an answer. Files: `src/dismissals.ts` ### Done when - [ ] A related page surfaces an earlier capture - [ ] Surfaces record trigger and outcome - [ ] Repeated dismissal stops resurfacing - [ ] No scheduled digest exists ### Watch out - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. ## Not in this build - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. ## After v1, if you want it - Surfacing while you write rather than only while you read, which is the harder half - Grouping captures into a thread when you return to a topic repeatedly - A weekly view of what you actually read, which is different from what you saved
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything you have read. A record of what you were thinking about. DB_PATH=~/.local/share/resurf/captures.db # Required. Present as a constant so the answer is visible. A prompt is a decision and decisions end habits. CAPTURE_PROMPT=never # Required. How many dismissals before an item stops resurfacing. Three is an answer. DISMISS_LIMIT=3 # Required. Present as a constant. A scheduled digest of old links is unread within a week. SCHEDULED_DIGEST=never # Required. Old text is compressed rather than deleted, since deleting removes what resurfacing runs on. COMPRESS_AFTER_DAYS=180 # Optional. Optional and off by default. Absent means one machine, which is a fine way to run. SYNC_REMOTE=s3://my-reading/archive
# Resurf · product brief ## Problem The core mechanic is unglamorous and very buildable: capture the text of pages you spend real time on, embed it, store it locally, then search and resurface it. An agent can get a Chrome extension doing that in a weekend, and because the corpus is your own browsing, there is no data moat to lose. Where the DIY version cracks is everything around the loop: cross-device sync, mobile and Safari, capture quality on SPAs and paywalled or auth-gated pages, and the taste of deciding what deserves to come back. Also the honest failure mode of every personal recall tool, which is that you stop opening it after week three. Buildable, yes; pleasant enough that you keep it, less certain. ## Product outcome A reading record that costs nothing to add to, finds a phrase from the middle of an article instantly, and brings something back when you are reading about it rather than on a schedule you ignore. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Chrome or any Chromium browser you are willing to load an unpacked extension into - Node and pnpm for the build - Disk space for a few hundred MB of page text and vectors - Optional OpenAI key if you want better embeddings than the on-device model ## Explicit non-goals for v1 - Any account or hosted service. - Mobile capture. - Social features and shared collections. - Full page appearance archiving. - Sync across devices and browsers, so your phone reading never enters the index - Capture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers - Robust extraction on SPAs, infinite scrolls, PDFs, and anything behind a login wall - Curation judgment: your version will resurface plenty of pricing pages and airline confirmations - Someone else maintaining it against Chrome manifest changes that break extensions on a schedule ## Success criteria - Capture is confirmed to be one action with no prompt - Readable text is confirmed stored at capture time - A phrase from an article body is confirmed findable - Repeated dismissal is confirmed to stop resurfacing - No scheduled digest is confirmed to exist
# Build brief · Resurf The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local capture tool that brings back what you read when it becomes relevant again to replace Resurf. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Capture: A browser extension and a hotkey, both storing the readable text · a link rots and a page changes, so the text is what you actually saved - Storage: Local SQLite with full text search · reading history is a record of what you were thinking about - Extraction: Readable content extraction at capture time, kept verbatim · extracting later means extracting from a page that has changed or gone - Surfacing: Context-triggered rather than scheduled · a daily digest of old links is unread within a week - Runtime: A local application with no account and no sync by default · the value does not require a service and the privacy cost of one is high ### Data model (create this before Phase 1) - captures(id, url, title, text, excerpt, captured_at, source, reading_time_ms) · text stored at capture, not fetched later - highlights(id, capture_id, text, note, at) · what you actually marked, which is a much stronger signal than the capture - topics(id, label) and capture_topics(capture_id, topic_id, confidence) · derived, never asked for at capture - surfaces(id, capture_id, trigger_kind, triggered_at, acted_on, dismissed) · what was shown, why, and what you did - dismissals(capture_id, count, last_at) · dismissing the same thing three times means stop - Everything findable is derived after capture, because any field asked for at save time is a decision that slows saving down until saving stops. ### Phase 1 · Capturing without deciding anything Build: A hotkey and an extension button that capture the page's readable text, title and URL with no prompt for tags, folders or notes. Store the extracted text at capture time. Measure how long capture takes. Trap: Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. Done when: Capture completes in one action with no prompt of any kind, timed by hand. The readable text is stored at capture rather than fetched later. A page behind a login or a paywall captures what was on screen rather than failing. Capture works with no network available. Do not build yet: No highlights, topics or surfacing. ### Phase 2 · Highlights, which mean far more than captures Build: Capture highlighted text with optional notes, anchored to the stored text. Treat a highlight as a much stronger relevance signal than a capture. Handle highlighting on a page whose text differs slightly from what was captured. Trap: Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. Done when: Highlights are stored with their surrounding context and anchored to the capture's text. A highlight on a slightly changed page still anchors or is recorded as unanchored rather than lost. Highlights are counted and weighted separately from captures. Adding a note to a highlight does not require leaving the page. Do not build yet: No topics or surfacing yet. ### Phase 3 · Finding it when you go looking Build: Full text search over captured text, highlights and notes with date and domain filters. Derive topics after capture. Make search fast enough at ten thousand captures to be the default way in. Trap: Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. Done when: Searching a phrase from the body of an article finds it, tested against a real capture set. Search across ten thousand captures returns as you type. Topics are derived rather than entered. Date and domain filters combine with text search. Do not build yet: No surfacing or resurfacing yet. ### Phase 4 · Bringing things back at a useful moment Build: Surface captures triggered by context: what you are currently reading, what you searched for, or a topic you have returned to. Record every surface and whether it was acted on. Stop surfacing something dismissed repeatedly. Trap: A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. Done when: Reading a page related to an earlier capture surfaces it, verified against a real capture set. Every surface records its trigger and whether it was acted on. An item dismissed three times stops appearing. There is no daily or weekly digest, verified by looking for one. Do not build yet: No export or backup yet. ### Phase 5 · Keeping it, and being able to leave Build: Export everything as readable files with highlights and notes preserved. Handle the database growing over years. Optional sync through storage you control, off by default. Trap: Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal. Done when: Export produces readable files with highlights and notes preserved and re-imports identically. The database remains manageable after years of captures, verified against a large synthetic set. Sync is off by default and goes through storage you control. Deleting a capture removes its highlights, topics and surface history. Do not build yet: Nothing after this. ### Out of scope (and why) - Any account or hosted service, which is what turns a private reading record into a product with a privacy policy. - Mobile capture, which is where a lot of reading happens and would need a different architecture. - Social features and shared collections, which change what people are willing to save. - Archiving full page appearance, which is a much larger storage problem than text. ### README must contain - That capture asks for nothing, because a decision at capture time is what kills the habit - That the readable text is stored at capture rather than fetched later from a changed page - That highlights are weighted far above captures because they are the stronger signal - That resurfacing is triggered by context and there is no scheduled digest - That old captures are compressed rather than deleted, since deleting removes what resurfacing runs on
# Architecture · Resurf ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A browser extension and a hotkey, both storing the readable text | a link rots and a page changes, so the text is what you actually saved | | Storage | Local SQLite with full text search | reading history is a record of what you were thinking about | | Extraction | Readable content extraction at capture time, kept verbatim | extracting later means extracting from a page that has changed or gone | | Surfacing | Context-triggered rather than scheduled | a daily digest of old links is unread within a week | | Runtime | A local application with no account and no sync by default | the value does not require a service and the privacy cost of one is high | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | One-action saving with no prompt. | Any implementation, provided nothing is asked at capture time. | | Highlights | Anchored marks with heavy weighting. | Any anchoring, provided highlights outweigh captures downstream. | | Search | Full text over captured content. | Any index, provided it covers body text rather than titles. | | Surfacing | Context triggers and dismissal limits. | Any trigger set, provided it is not a schedule. | | Retention | Compression rather than deletion. | Any policy, provided old captures survive since they are what resurfacing uses. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DB_PATH` · required · Everything you have read. A record of what you were thinking about. - `CAPTURE_PROMPT` · required · Present as a constant so the answer is visible. A prompt is a decision and decisions end habits. - `DISMISS_LIMIT` · required · How many dismissals before an item stops resurfacing. Three is an answer. - `SCHEDULED_DIGEST` · required · Present as a constant. A scheduled digest of old links is unread within a week. - `COMPRESS_AFTER_DAYS` · required · Old text is compressed rather than deleted, since deleting removes what resurfacing runs on. - `SYNC_REMOTE` · optional · Optional and off by default. Absent means one machine, which is a fine way to run. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Resurf product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A browser extension and a hotkey, both storing the readable text, Local SQLite with full text search, Readable content extraction at capture time, kept verbatim, Context-triggered rather than scheduled, A local application with no account and no sync by default. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. - Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal.
# Delivery milestones · Resurf Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capturing without deciding anything Saving something takes one action and no thought. ### Steps 1. Capture with no prompt No tags, no folder, no note. The absence is the design. Files: `extension/capture.ts` 2. Store the extracted text now Later means a page that changed or disappeared. Files: `src/extract.ts` 3. Work offline The text is on screen. Capture should not need a network. Files: `src/capture.ts` ### Done when - [ ] Capture is one action with no prompt - [ ] Readable text is stored at capture time - [ ] A logged-in page captures what was on screen - [ ] Capture works with no network ### Watch out - Any field offered at capture, even an optional one, is a decision, and a decision at capture time is what turns a fast habit into a slow one that stops within a month. Capture with no prompt at all, and derive everything findable afterwards. ## M2 · Highlights, which mean far more than captures What you marked is distinguished from what you merely saved. ### Steps 1. Anchor highlights to the stored text With surrounding context, so a small change does not orphan them. Files: `src/highlights.ts` 2. Weight highlights far above captures People save everything and highlight only what landed. Files: `src/signals.ts` 3. Keep note-taking inline Leaving the page to add a note is a decision, and decisions stop habits. Files: `extension/highlight.ts` ### Done when - [ ] Highlights anchor with context - [ ] A slightly changed page anchors or records unanchored - [ ] Highlights are weighted above captures - [ ] Notes can be added without leaving the page ### Watch out - Treating a highlight the same as a capture wastes the strongest signal in the system, since people save far more than they read and highlight only what actually landed. Weight highlights heavily in everything downstream and count them separately. ## M3 · Finding it when you go looking Full text search across everything you read finds it instantly. ### Steps 1. Index the full captured text Titles and URLs find nothing. You remember phrases. Files: `src/search.ts` 2. Derive topics after capture Never ask at capture. That was phase one's whole point. Files: `src/topics.ts` 3. Keep search instant at scale Ten thousand captures. Slower than typing means it is not the default way in. Files: `src/search.ts` ### Done when - [ ] A phrase from an article body is found - [ ] Ten thousand captures search as you type - [ ] Topics are derived, not entered - [ ] Date and domain filters combine with text ### Watch out - Searching only titles and URLs finds almost nothing, because you remember a phrase from the middle of an article rather than its headline. Index the full captured text, which you have precisely because it was stored at capture time. ## M4 · Bringing things back at a useful moment Something you saved reappears when it is relevant, not on a schedule. ### Steps 1. Trigger on what you are doing now The related page is the moment the old one becomes interesting. Files: `src/surface.ts` 2. Record surfaces and outcomes So you can tell whether resurfacing works rather than assuming. Files: `src/surfaces.ts` 3. Stop after repeated dismissal Three dismissals is an answer. Files: `src/dismissals.ts` ### Done when - [ ] A related page surfaces an earlier capture - [ ] Surfaces record trigger and outcome - [ ] Repeated dismissal stops resurfacing - [ ] No scheduled digest exists ### Watch out - A daily digest of old saves is unread within a week, because a scheduled reminder arrives when you are doing something else and old links are not interesting out of context. Trigger on context: reading a related page is the moment the old one is worth seeing. ## M5 · Keeping it, and being able to leave (production only) Years of reading survive and can move somewhere else. ### Steps 1. Export to readable files Round trip tested. A reading archive should not be trapped. Files: `src/export.ts`, `tests/roundtrip.test.ts` 2. Compress old text, never delete it Deleting old captures removes what resurfacing runs on. Files: `src/compact.ts` 3. Make sync optional and yours Off by default, through storage you control if on. Files: `src/sync.ts` ### Done when - [ ] Export round trips with highlights and notes - [ ] The database stays manageable over years - [ ] Sync is off by default and self-hosted - [ ] Deleting removes everything attached ### Watch out - Storing the full text of everything you read grows without bound, and the naive response is to delete old captures, which removes exactly the material that resurfacing depends on. Compress old text rather than deleting it, and keep highlights uncompressed since they are small and are the strongest signal.
# Operations · Resurf ## Backup A reading archive accumulates value over years and is not reconstructible. Back up the database, and run periodic exports as a format-independent copy since the archive should outlive this application. ## Restore Restore and confirm highlights and surface history survived, since the history is what prevents already-dismissed items from reappearing and its loss makes the tool immediately annoying. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a personal tool the number worth watching is the proportion of surfaced items acted on. A falling rate means the triggers have stopped matching what you actually need. ## Incident checklist Capture becoming slow is the failure that ends the habit, and it is usually extraction running synchronously in the capture path. Store first and extract afterwards if it comes to that. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Capture is confirmed to be one action with no prompt - [ ] Readable text is confirmed stored at capture time - [ ] A phrase from an article body is confirmed findable - [ ] Repeated dismissal is confirmed to stop resurfacing - [ ] No scheduled digest is confirmed to exist ## Launch constraint Do not market omitted Resurf capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Everything you have read. A record of what you were thinking about. DB_PATH=~/.local/share/resurf/captures.db # Required. Present as a constant so the answer is visible. A prompt is a decision and decisions end habits. CAPTURE_PROMPT=never # Required. How many dismissals before an item stops resurfacing. Three is an answer. DISMISS_LIMIT=3 # Required. Present as a constant. A scheduled digest of old links is unread within a week. SCHEDULED_DIGEST=never # Required. Old text is compressed rather than deleted, since deleting removes what resurfacing runs on. COMPRESS_AFTER_DAYS=180 # Optional. Optional and off by default. Absent means one machine, which is a fine way to run. SYNC_REMOTE=s3://my-reading/archive
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because the value only shows up after months of quiet, correct capture, and a homegrown extension that silently stops recording on a Chrome update fails invisibly. Paying for it means the capture layer is somebody's job, the mobile gap is at least on a roadmap, and the ranking has been tuned against more browsing than yours. Personal recall tools are also the classic case where the build is fun and the upkeep is not.
xSync across devices and browsers, so your phone reading never enters the index
xCapture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers
xRobust extraction on SPAs, infinite scrolls, PDFs, and anything behind a login wall
xCuration judgment: your version will resurface plenty of pricing pages and airline confirmations
xSomeone else maintaining it against Chrome manifest changes that break extensions on a schedule
Nothing worth pointing at. That's why the prompt exists.
Vibecode Resurf
Kinda. The core of Resurf is buildable in a weekend with the prompt on this page, but there are real gaps: Sync across devices and browsers, so your phone reading never enters the index, Capture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers. Read the honest list above before committing.
How much does Resurf cost?
Resurf's pricing is usage-based or varies by plan · Sold as a one-time licence: $49 (launch promo $39), or $99 lifetime; free on iPhone and iPad. Canonical monthly amount is null..
What do I lose by replacing Resurf?
Honestly: Sync across devices and browsers, so your phone reading never enters the index; Capture on anything that is not desktop Chrome: Safari, mobile apps, in-app browsers; Robust extraction on SPAs, infinite scrolls, PDFs, and anything behind a login wall; Curation judgment: your version will resurface plenty of pricing pages and airline confirmations; Someone else maintaining it against Chrome manifest changes that break extensions on a schedule. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Resurf?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.