Inspiration

My knowledge was scattered across browser tabs, Obsidian notes, uTools, screenshots, and random links. Capture tools were good at saving things; note tools were good at storing them; almost nothing helped me decide what deserved to stay. I wanted a small private place where capture, review, and writing were one loop rather than three disconnected apps.

The name comes from that feeling: 半亩 is a small plot of land, and Bothy is a simple shelter in the Scottish hills. Not a platform, not a second brain with a dashboard — just enough room to keep a few things and return to them.

What it does

半亩 is a private personal knowledge portal with capture surfaces on three places I already work:

  • A Chrome MV3 extension for selections, links, and images.
  • An Obsidian plugin for bringing media and notes into the vault workflow.
  • A uTools plugin for fast capture from the desktop.

Everything lands in a private portal with /inbox, /drafts, /media, and /organize.

The important part is the loop after capture:

  • Inbox keeps raw fragments, source links, notes, tags, and media.
  • Organize gives each fragment a summary, suggested tags, duplicate candidates, and a weekly review.
  • Triage lets me archive a fragment, turn it into a draft, merge a duplicate, or select several fragments and archive them together.
  • Drafts are real writing surfaces with Markdown editing, citations, and export back into a file I can keep.
  • Media is content-addressed, deduplicated, and served separately from the portal.

AI is deliberately an assistant, not an editor. It suggests; I apply. When AI is disabled or unavailable, deterministic rules still provide summaries, tags, and duplicate checks. The private pages are server-rendered, no-store, noindex, and protected by a session cookie or Bearer token.

How we built it

The portal is Astro 7 + Starlight + TypeScript, running on Node 24. Data lives in SQLite through node:sqlite; media is stored by SHA-256 key and served from its own domain. The UI is server-rendered Astro with small vanilla-JS interactions rather than a client-side framework.

AI runs through a Cloudflare Worker reverse proxy. The browser only talks to the portal; the portal talks to the Worker with a private proxy token; the Worker injects the Gemini key and forwards an OpenAI-compatible chat completion. No model key reaches the client.

The organizers use structured JSON output for summaries, tags, and duplicate IDs, with a rules engine as the baseline. Weekly reviews are generated from scoped statistics — new fragments, active days, top tags, kinds, sources, inbox count, archived count, media count, and drafts — so the model does not have to guess numbers.

Deployment uses release directories, a current symlink, systemd, and a Cloudflare Tunnel. Backups use SQLite VACUUM INTO plus a media.tar.gz snapshot, an integrity check, a daily systemd timer, 14-snapshot retention, and a restore script that preserves the previous database and media before overwriting them. There is also a documented way to pull snapshots to a Mac as an off-site copy.

The three capture clients are intentionally thin: they collect context, normalize it, and post to the same private API. The Chrome extension, Obsidian plugin, and uTools plugin each have their own packaging constraints, but they share the same fragment model.

Challenges we ran into

AI latency is product design, not just infrastructure. A single organization request can take 15–25 seconds, and a weekly review can take longer. Buttons need honest progress states, retries, and eventually client-side timeouts. A request that eventually succeeds but looks stuck is still a bad experience.

The model needed constraints, not more prompt. Early review output counted media as part of the week, and reasoning settings caused truncated or rejected responses. Scoping the input keys, labeling “media library” separately, and pinning the reasoning effort fixed the output more reliably than adding more instructions.

Duplicate detection has more than one meaning. Exact re-pastes should refresh an existing fragment; near-duplicates should be suggested for merging; AI duplicates and rules duplicates should not silently disagree. The merge action also had to preserve notes, tags, and source context instead of just deleting one row.

Private data changes the backup problem. A database snapshot alone is not a backup when the images live on disk. The first backup script only handled SQLite; the real version needed media, a manifest, an integrity check, retention, and a restore path that could put the media back without destroying the current library.

Three capture surfaces, three sets of constraints. MV3 service workers, Obsidian’s vault filesystem, and uTools’ sandbox all fail in different ways. The shared API contract was the only thing that kept the clients from becoming three separate products.

Testing with real content exposes different bugs. Synthetic fixtures do not reveal awkward source titles, mixed Chinese/English text, long quotes, or the emotional question of whether a fragment is worth keeping. I tested the triage actions against temporary real fragments, then removed them and verified that the original three captures were untouched.

Accomplishments that we're proud of

  • A complete private loop from capture → triage → draft → export, not a collection of disconnected demos.
  • AI that suggests without taking control; tags are appended only when I apply them, and every AI action has a rules-based baseline.
  • A triage surface that feels like a workbench: archive, convert to draft, merge duplicates, bulk select, and immediate count updates.
  • A backup system that treats media as part of the product, with a real restore drill and an off-site Mac copy.
  • Real browser testing on the live portal, including the new actions, with test fragments cleaned up afterward.
  • 93 tests passing, plus a clean type check and production build.
  • A coherent identity: 半亩 / Bothy, with Josh kept as the author and assistant persona rather than the product name.

What we learned

  • Rules are not an embarrassing fallback. They define the baseline, make the product useful without a model, and give AI something concrete to improve on.
  • The hard part of a knowledge tool is state transition, not capture. Deciding what moves from inbox to archive, archive to draft, or duplicate to merged is where the product actually lives.
  • AI features need a latency budget. A good answer that arrives after the user has given up is not a good feature.
  • Backups must be tested, not just scheduled. The restore drill found assumptions that a green timer would never reveal.
  • A private tool can be small on purpose. No social graph, no engagement loop, no fake numbers. Restraint is a feature.
  • Real content is the best test suite. The three live fragments exposed wording, spacing, and duplicate behavior that unit tests alone would not.

What's next for 半亩

  • Add a client-side timeout, retry, and visible fallback for AI organization and review requests.
  • Cache AI suggestions by content hash and update time so repeated views do not re-run the same request.
  • Turn the weekly review into actions: archive suggestions, draft ideas, and a small “keep / let go” queue.
  • Add orphan-media cleanup, thumbnails/WebP variants, and signed media URLs for safer sharing.
  • Automate the Mac off-site pull with a LaunchAgent and add a second destination for a real 3-2-1 backup.
  • Finish the Chrome extension production configuration and the page-explosion acceptance pass.
  • Split the growing inbox page and private styles into smaller modules.
  • Keep semantic search deferred until the capture and triage loop is boringly reliable. The next milestone is not more intelligence; it is fewer moments where the tool makes me wait or guess.

Built With

Share this project:

Updates

Submission history