Server (Express + better-sqlite3 + FTS5, mirroring keep's stack): /api/capture (upsert-on-url, per PROPOSAL.md's dedup decision), /api/articles (list/search/mark-read/delete), plus a minimal server-rendered reading list and reader view at / and /read/:id (token passed as ?t= there since browser navigation can't set an Authorization header). Bearer-token auth generated via `npm run token` — no login flow, matching keep's one-time identity registration pattern. Extension (Manifest V3, sideloaded, no store distribution): toolbar click or context menu "Send to stash" runs @mozilla/readability (vendored into extension/lib/) against the current page, falling back to raw rendered HTML when extraction comes back too thin. Options page for one-time server URL + token entry. Verified end-to-end with the real extension loaded in a live Chromium instance (not just unit-tested extraction logic): capture against a real page, dedup-on-recapture, mark-read, FTS5 search, 401 on missing auth, and both server-rendered views all confirmed working. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
54 lines
1.9 KiB
Markdown
54 lines
1.9 KiB
Markdown
# stash
|
|
|
|
A self-hosted read-later tool. Click "Send to stash" in the browser, the
|
|
current page's article content is extracted and stored on your own
|
|
server — full-text searchable, with a plain reader view. No third party
|
|
ever sees your reading list.
|
|
|
|
Two pieces, neither useful alone:
|
|
|
|
- **Server** (`src/server`) — Express + SQLite (via `better-sqlite3`),
|
|
FTS5 full-text search, a minimal server-rendered reading list + reader
|
|
view, and the `/api/*` endpoints the extension talks to.
|
|
- **Extension** (`extension/`) — Manifest V3, sideloaded (not published to
|
|
any store). Runs [`@mozilla/readability`](https://github.com/mozilla/readability)
|
|
against the current page to strip nav/ads/chrome and keep just the
|
|
article, falling back to the raw rendered page if extraction comes back
|
|
too thin to be useful.
|
|
|
|
Full design and the reasoning behind the scope boundaries: [PROPOSAL.md](./PROPOSAL.md).
|
|
|
|
## Server setup
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
npm install
|
|
npm run token -- my-laptop # prints a bearer token — paste it into the extension's options
|
|
npm run dev:server
|
|
```
|
|
|
|
## Extension setup
|
|
|
|
1. `chrome://extensions` → enable Developer mode → "Load unpacked" → select `extension/`
|
|
2. Click the extension icon once, or use "Options" from its context menu, to open the options page
|
|
3. Enter your server's URL and the token from `npm run token`
|
|
|
|
Then either click the toolbar icon or right-click a page → "Send to stash".
|
|
|
|
## Reading
|
|
|
|
Visit `http://your-server:3070/?t=<token>` — a plain list of what's been
|
|
captured, with search and an unread filter. Click through to `/read/:id`
|
|
for the cleaned reader view.
|
|
|
|
## Status
|
|
|
|
Built and verified end-to-end: capture → server storage → full-text
|
|
search → mark-read → reader view, driven through the actual extension
|
|
(loaded in a real Chromium instance, not just unit-tested logic) against
|
|
a live server. Not yet deployed anywhere.
|
|
|
|
## License
|
|
|
|
MIT.
|