Ben's Bites cookbook
← Home

Bookmark sync for newsletter curation

Twitter bookmarks → Slack channel with AI auto-classification + web UI

2026-02-03 Tools: Droid, bird, curl, llm, bun, hono

I bookmark everything on Twitter. Interesting threads, product launches, research papers, hot takes. Hundreds of bookmarks. They're my content bank for the newsletter.

But they just... sit there. I'd forget what I bookmarked. I'd open Twitter, scroll through bookmarks, copy links manually, paste them somewhere. It was friction. And friction kills content pipelines.

I wanted:

  • **Bookmarks saved locally** — A file I can Grep, reference, own
  • **Posted to Slack** — So I see them during the day, they're in my face
  • **Auto-classified** — Is this news? A product? Research? Thread?
  • **No manual work** — Runs on a schedule, I just keep bookmarking

---

The first prompt

I already have a bird CLI for Twitter. It can pull bookmarks as JSON. So I told my agent what I wanted.

Session
2026-02-03T06:07:34.882Z
i want to create a system for saving links for the newsletter. i bookmark everything on twitter. so how can we get those links sent into a slack channel when i save them or on a cront
2026-02-03T06:07:51.973Z
Thinking... (click to expand)
The user wants to create a system to save links for their newsletter. They bookmark things on Twitter and want those bookmarks to be sent to a Slack channel - either when they save them or on a cron schedule. Let me think about the approaches: 1. **Real-time (when saved)**: Twitter doesn't have webhooks for bookmarks. There's no native way to get notified when you bookmark something. So this would require polling. 2. **Cron-based**: Poll Twitter bookmarks periodically and send new ones to Slack. The user has a `bird` CLI tool for Twitter (bookmarks, view tweets). Let me think about the architecture: **Option A: Cron job that polls bookmarks** - Run `bird bookmarks` periodically - Track which bookmarks have been seen (store IDs somewhere) - Send new ones to Slack via webhook or Slack API **Option B: Use a service like Zapier/Make/IFTTT** - These might have Twitter bookmark triggers, but I'm not sure they do since bookmarks are relatively private **Option C: Browser extension that sends to Slack on bookmark** - More complex, requires browser extension development Given the user has `bird` CLI already, Option A (cron) is the most practical. Let me explain this approach before jumping into implementation.
Good question. Let me outline the practical approaches: ## Options **1. Cron-based (recommended)** - Run a script every X minutes that fetches your Twitter bookmarks via `bird` - Track seen bookmark IDs in a file to avoid duplicates - Post new ones to Slack via webhook **2. Real-time isn't really possible** - Twitter has no webhook for bookmarks - Would still require polling ## Proposed Architecture ``` Cron (every 15-30 min) → bird bookmarks --json → Compare against seen IDs (stored in ~/bites/links/seen-bookmarks.txt) → Post new links to Slack webhook → Update seen IDs file ``` ## What I'd need to set up: 1. **Slack webhook URL** - create an incoming webhook in Slack for the target channel 2. **Script** - bash or bun script that does the fetch/diff/post 3. **Cron entry** - schedule it (you mentioned you have cron support via `/cron`) ## Questions: 1. Do you already have a Slack webhook set up, or should I walk you through creating one? 2. How often do you want it to check? (e.g., every 15 min, hourly, daily digest) 3. Should it post each link individually, or batch them into one message? Want me to build this? If so, let me know the Slack webhook URL (or I can help you create one) and your preferred frequency.

The agent laid out the architecture immediately — cron job that polls bookmarks, tracks seen IDs, posts to Slack via webhook.

It asked the right questions: Do I have a Slack webhook? How often? Batched or individual messages?

---

Setting up the Slack webhook

I didn't have one set up yet. The agent walked me through it.

Session
2026-02-03T06:10:01.312Z
do a cron every at midnight, and at 9am dont have slack webhook setup send them in one message we should have them all save to a file in bites tho too
2026-02-03T06:10:11.472Z
Thinking... (click to expand)
The user wants: 1. Cron at midnight and 9am 2. No Slack webhook set up yet - need to help them create one 3. Send all new bookmarks in one message (batched) 4. Save them to a file in ~/bites as well Let me create a todo list and also help them set up the Slack webhook. I should: 1. Create the todo list 2. Help them set up a Slack webhook (provide instructions) 3. Create the script that: - Fetches bookmarks via bird - Tracks seen IDs - Saves new links to a file in ~/bites/links/ - Posts to Slack 4. Set up the cron Let me first check if the links directory exists and what the bird command looks like.
TodoWrite
Result
TODO List Updated
LS
{
  "directory_path": "/Users/mini/bites/links"
}
Result
total 8
drwxr-xr-x   8 mini  staff  256 30 Jan 23:12 .
drwxr-xr-x  22 mini  staff  704  2 Feb 15:28 ..
drwxr-xr-x   2 mini  staff   64 30 Jan 23:12 content
-rw-r--r--   1 mini  staff   14 30 Jan 23:12 inbox.md
drwxr-xr-x   2 mini  staff   64 30 Jan 23:12 newsletter
drwxr-xr-x   2 mini  staff   64 30 Jan 23:12 personal
drwxr-xr-x   2 mini  staff   64 30 Jan 23:12 research
drwxr-xr-x   2 mini  staff   64 30 Jan 23:12 tools
Execute
Result
2026-02-03T06:10:18.881Z
Good - `bird bookmarks --json` will work. Now for Slack: ## Setting up Slack Webhook 1. Go to: **https://api.slack.com/apps** 2. Click **"Create New App"** → **"From scratch"** 3. Name it something like "Bookmark Bot", pick your workspace 4. In the left sidebar, click **"Incoming Webhooks"** 5. Toggle **"Activate Incoming Webhooks"** to ON 6. Click **"Add New Webhook to Workspace"** 7. Select the channel you want bookmarks posted to 8. Copy the webhook URL (looks like `https://hooks.slack.com/services/T.../B.../xxx`) Once you have that URL, paste it here and I'll: - Create the sync script at `~/bites/links/sync-bookmarks.sh` - Store bookmarks in `~/bites/links/bookmarks.md` - Set up crons for midnight and 9am UK time

Simple enough. Create an app on api.slack.com, enable incoming webhooks, pick a channel, copy the URL.

Once I pasted it, the agent started building.

---

Building the sync script

This is where it gets real. The agent created a bash script that:

1. Fetches bookmarks via `bird bookmarks --json`

2. Tracks seen tweet IDs in a dotfile (no duplicates)

3. Saves new bookmarks to a markdown file

4. Posts batched messages to Slack

Session
2026-02-03T08:17:53.322Z
https://hooks.slack.com/services/[REDACTED]
2026-02-03T08:18:13.048Z
Thinking... (click to expand)
The user provided the Slack webhook URL. Now I need to: 1. Create the sync script 2. Set up the cron jobs Let me create a script that: - Fetches bookmarks via `bird bookmarks --json` - Tracks seen IDs in a file - Saves new bookmarks to a markdown file - Posts new ones to Slack in one batched message I'll also need to check how crons work in this system - the user mentioned `/cron list` so there's likely a cron system.
TodoWrite
Result
TODO List Updated
Create
Result
File created successfully
2026-02-03T08:18:17.741Z
Execute
{
  "command": "chmod +x /Users/mini/bites/links/sync-bookmarks.sh",
  "riskLevel": "low",
  "riskLevelReason": "Making a script executable"
}
Result
Command completed successfully

[Process exited with code 0]
Execute
{
  "command": "crontab -l 2>/dev/null || echo \"No existing crontab\"",
  "riskLevel": "low",
  "riskLevelReason": "Just reading existing crontab"
}
Result
No existing crontab


[Process exited with code 0]
2026-02-03T08:18:26.197Z
TodoWrite
Result
TODO List Updated
Execute
Result
Error: Command failed (exit code: 1)
crontab: tmp/tmp.3694: Operation not permitted


[Process exited with code 1]
2026-02-03T08:18:54.548Z
Crontab doesn't have permission. Let me check your system's cron setup:
Execute
{
  "command": "ls -la ~/Library/LaunchAgents/ 2>/dev/null | head -20 || echo \"No LaunchAgents\"",
  "riskLevel": "low",
  "riskLevelReason": "Just listing files"
}
Result
Grep
{
  "pattern": "cron",
  "path": "/Users/mini/repos/bitesbot",
  "output_mode": "file_paths"
}
Result
2026-02-03T08:18:58.304Z
I see bitesbot has its own cron system. Let me check how to use it:
Read
{
  "file_path": "/Users/mini/repos/bitesbot/src/cron/AGENTS.md"
}
Result
# Cron notes

> Inherits from ../../AGENTS.md

## Scope

- Scheduled job service used by `/cron` commands in the bridge.

## Entry points

- `src/cron/service.ts` - job management API
- `src/cron/scheduler.ts` - timer and scheduling loop
- `src/cron/store.ts` - persistence
- `src/cron/run-history.ts` - run tracking
- `src/cron/types.ts` - shared types
2026-02-03T08:19:01.619Z
Read
{
  "file_path": "/Users/mini/repos/bitesbot/src/cron/service.ts"
}
Result
2026-02-03T08:19:10.835Z
I see - the bitesbot gateway has a `/cron` command system. Let me create a LaunchAgent plist instead since crontab is restricted:
Create
Result
File created successfully
2026-02-03T08:19:16.818Z
Execute
{
  "command": "launchctl load /Users/mini/Library/LaunchAgents/com.bites.bookmark-sync.plist 2>&1",
  "riskLevel": "medium",
  "riskLevelReason": "Loading a LaunchAgent to schedule the sync script"
}
Result
Command completed successfully

[Process exited with code 0]
2026-02-03T08:19:25.999Z
Now let me test the script:
TodoWrite
Result
TODO List Updated
Execute
{
  "command": "/Users/mini/bites/links/sync-bookmarks.sh 2>&1",
  "riskLevel": "medium",
  "riskLevelReason": "Running the sync script which will post to Slack",
  "timeout": 120
}
Result
Posted 20 new bookmarks to Slack


[Process exited with code 0]
2026-02-03T08:19:43.146Z
Check Slack - it should have posted. Let me verify the files were created:
Execute
{
  "command": "wc -l /Users/mini/bites/links/.seen-bookmark-ids && head -50 /Users/mini/bites/links/bookmarks.md",
  "riskLevel": "low",
  "riskLevelReason": "Just reading files"
}
Result
TodoWrite
Result
TODO List Updated
2026-02-03T08:19:49.444Z
Done! Here's what's set up: **Created:** - `/Users/mini/bites/links/sync-bookmarks.sh` - fetches bookmarks, dedupes, saves locally, posts to Slack - `/Users/mini/bites/links/bookmarks.md` - local archive of all bookmarked tweets - `/Users/mini/bites/links/.seen-bookmark-ids` - tracks already-processed IDs - LaunchAgent running at **midnight** and **9am UK time** **Just ran:** Posted 20 bookmarks to Slack and saved them locally. Check your Slack channel! **Future runs:** Will only post *new* bookmarks since the last sync.

The script handles edge cases — what if the bird command fails? What if there are no new bookmarks? It skips seen IDs, extracts the tweet text and URLs, and formats everything for Slack.

---

Adding AI classification

I wanted categories. "News", "Product", "Research", "Opinion/Thread". So the agent added an LLM call to classify each bookmark.

Session
2026-02-03T08:20:17.603Z
spin up a ui on tailscale with json-renderer so i can manage how this works
2026-02-03T08:20:28.868Z
Thinking... (click to expand)
The user wants a web UI on Tailscale to manage the bookmark sync system. They mentioned json-renderer (from the AGENTS.md - it's a framework for AI → JSON → UI). Let me think about what this UI should do: 1. View recent bookmarks 2. Trigger a manual sync 3. View/edit settings (schedule, Slack webhook) 4. View sync logs 5. Maybe filter/search bookmarks I should: 1. Create a simple web app using json-render 2. Expose it via Tailscale 3. Let them manage the bookmark sync Let me first check what json-render looks like and set up a basic Next.js or simple server.
TodoWrite
{
  "todos": "1. [in_progress] Create bookmark manager UI with json-render\n2. [pending] Expose on Tailscale\n3. [pending] Test the UI"
}
Result
TODO List Updated
Execute
{
  "command": "mkdir -p /Users/mini/bites/links/ui && cd /Users/mini/bites/links/ui && ls",
  "riskLevel": "low",
  "riskLevelReason": "Creating a directory"
}
Result
Command completed successfully

[Process exited with code 0]
WebSearch
{
  "query": "json-render vercel-labs npm package usage 2025",
  "numResults": 5
}
Result
2026-02-03T08:20:50.163Z
FetchUrl
{
  "url": "https://github.com/vercel-labs/json-render"
}
Result
2026-02-03T08:21:02.076Z
Got it. I'll create a simpler Bun/Hono server with a React frontend that uses json-render. Let me set it up:
Execute
Result
2026-02-03T08:21:39.611Z
Create
Result
File created successfully
2026-02-03T08:21:45.534Z
Execute
Result
2026-02-03T08:21:52.479Z
Template literal issue. Let me fix it:
Edit
Result
2026-02-03T08:21:57.475Z
Execute
Result
Bookmark Sync Manager running on http://localhost:3847
Started development server: http://localhost:3847
{
  "totalBookmarks": 20,
  "seenIds": 20
}


[Process exited with code 0]
2026-02-03T08:22:05.471Z
Server is running. Now let me create a LaunchAgent to keep it running and expose it via Tailscale:
Execute
{
  "command": "tailscale status 2>/dev/null | head -5",
  "riskLevel": "low",
  "riskLevelReason": "Checking tailscale status"
}
Result
2026-02-03T08:22:13.026Z
Create
Result
File created successfully
2026-02-03T08:22:18.777Z
Execute
Result
{
  "totalBookmarks": 20,
  "seenIds": 20
}


[Process exited with code 0]
2026-02-03T08:22:25.657Z
TodoWrite
{
  "todos": "1. [completed] Create bookmark manager UI with json-render\n2. [completed] Expose on Tailscale\n3. [completed] Test the UI"
}
Result
TODO List Updated
2026-02-03T08:22:33.487Z
Done! Your bookmark manager UI is now running and accessible via Tailscale: **Local:** http://localhost:3847 **Tailscale:** http://bens-mac-mini:3847 (or http://100.106.254.46:3847) **Features:** - View stats (total bookmarks, seen IDs) - **Sync Now** button - trigger manual sync - **Clear Seen IDs** - reset to re-sync all bookmarks - View recent bookmarks (clickable links to tweets) - View sync logs **Running as:** - LaunchAgent `com.bites.bookmark-ui` - auto-starts, stays alive - Port 3847 The cron still runs at midnight and 9am UK, but now you can also trigger syncs manually from the UI.

Now each Slack message shows bookmarks grouped by type:


🔖 5 new bookmarks

📰 News
• @OpenAI: GPT-5 announcement...

🔧 Product  
• @verlorendb: Just shipped...

📚 Research
• @anthropic: New paper on...

Scannable. Actually useful.

---

Setting up the crons

Two runs per day:

  • **Midnight UK** — Catches evening bookmarks
  • **9am UK** — Morning batch, fresh in Slack when I start work
Session
2026-02-03T08:27:17.749Z
that port is used by something else - use a new port
2026-02-03T08:27:24.196Z
Thinking... (click to expand)
User wants a different port. Let me change it to something less common.
Edit
{
  "file_path": "/Users/mini/bites/links/ui/server.ts",
  "old_str": "const port = 3847",
  "new_str": "const port = 4892"
}
Result
Execute
Result
Command completed successfully

[Process exited with code 0]
2026-02-03T08:27:29.743Z
Execute
Result
{
  "totalBookmarks": 20,
  "seenIds": 20
}


[Process exited with code 0]
2026-02-03T08:27:36.324Z
Done. Now running on **port 4892**: - **Local:** http://localhost:4892 - **Tailscale:** http://bens-mac-mini:4892

The agent registered both crons. Done.

---

Testing it

Had to make sure it actually worked.

Session
2026-02-03T08:28:53.340Z
we need to populate any images/videos from the tweets to be in our ui. i also want a way to categorise whether they'd be in the newsletter or not, this should be used as examples in our prompt to then make classification automatic
2026-02-03T08:29:04.270Z
Thinking... (click to expand)
The user wants to: 1. Show images/videos from tweets in the UI 2. Add a way to categorize bookmarks as "newsletter" or "not newsletter" 3. Use these categorizations as examples to train automatic classification This means: 1. Update the sync script to capture media URLs from tweets 2. Update the bookmarks storage format to include media 3. Add UI for categorizing bookmarks 4. Store categorizations 5. Eventually use these as few-shot examples for AI classification Let me update the system to: 1. Modify sync-bookmarks.sh to capture media URLs (or better, rewrite in TypeScript/Bun for easier JSON handling) 2. Store bookmarks as JSON instead of markdown for better structure 3. Add category field and UI controls 4. Show images/videos in the UI Actually, the bash script is getting complex. Let me rewrite the sync logic in TypeScript as part of the server, which will make it much easier to handle the JSON from bird bookmarks.
Execute
{
  "command": "bird bookmarks --json --plain 2>/dev/null | jq '.[0]' | head -100",
  "riskLevel": "low",
  "riskLevelReason": "Checking tweet structure for media fields"
}
Result
{
  "id": "2018059495335764273",
  "text": "https://t.co/NKX4oIDbMR",
  "createdAt": "Sun Feb 01 20:30:52 +0000 2026",
  "replyCount": 28,
  "retweetCount": 144,
  "likeCount": 1747,
  "conversationId": "2018059495335764273",
  "author": {
    "username": "ashpreetbedi",
    "name": "Ashpreet Bedi"
  },
  "authorId": "10350082"
}


[Process exited with code 0]
2026-02-03T08:29:10.568Z
Execute
Result
2026-02-03T08:29:17.903Z
Execute
Result
Command completed successfully

[Process exited with code 0]
2026-02-03T08:29:23.879Z
Execute
{
  "command": "bird bookmarks --json-full --plain 2>/dev/null | jq '.[0]' 2>/dev/null | head -100",
  "riskLevel": "low",
  "riskLevelReason": "Checking raw tweet structure"
}
Result
Command completed successfully

[Process exited with code 0]
2026-02-03T08:29:29.246Z
Execute
{
  "command": "bird bookmarks --help 2>&1 | grep -A2 \"json\"",
  "riskLevel": "low",
  "riskLevelReason": "Checking bird help"
}
Result
2026-02-03T08:29:33.893Z
Execute
{
  "command": "bird bookmarks --json-full 2>/dev/null | jq '.[5] | keys' 2>/dev/null",
  "riskLevel": "low",
  "riskLevelReason": "Checking tweet keys"
}
Result
Error: Command failed (exit code: 5)

[Process exited with code 5]
2026-02-03T08:29:40.220Z
Execute
{
  "command": "bird bookmarks --json-full 2>&1 | head -200",
  "riskLevel": "low",
  "riskLevelReason": "Checking bird output"
}
Result
2026-02-03T08:29:47.421Z
I can see the structure. Let me find a tweet with media:
Execute
Result
Command completed successfully

[Process exited with code 0]
2026-02-03T08:29:53.494Z
Execute
Result
Error: Command failed (exit code: 5)

[Process exited with code 5]
2026-02-03T08:29:59.977Z
Execute
{
  "command": "bird bookmarks --json-full 2>/dev/null | jq '.[10]._raw.legacy' 2>/dev/null",
  "riskLevel": "low",
  "riskLevelReason": "Checking tweet structure"
}
Result
Error: Command failed (exit code: 5)

[Process exited with code 5]
2026-02-03T08:30:05.057Z
Execute
{
  "command": "bird bookmarks --json-full 2>/dev/null | jq 'length'",
  "riskLevel": "low",
  "riskLevelReason": "Checking bookmarks count"
}
Result
Error: Command failed (exit code: 5)
jq: parse error: Unfinished string at EOF at line 2368, column 23


[Process exited with code 5]
2026-02-03T08:31:08.508Z
There's a JSON parsing issue with the full output. Let me rewrite the server to handle this properly and add the categorization feature:
Create
Result
File created successfully
2026-02-03T08:31:15.011Z
Execute
Result
{
  "stats": {
    "totalBookmarks": 0,
    "seenIds": 20,
    "categorized": 0,
    "newsletter": 0,
    "skipped": 0
  },
  "examples": {
    "newsletter": [],
    "skip": []
  },
  "logs": []
}


[Process exited with code 0]
2026-02-03T08:31:24.021Z
The DB is empty since I switched to JSON format. Let me sync to populate it:
Execute
Result
{
  "success": true,
  "added": 0,
  "total": 0
}


[Process exited with code 0]
2026-02-03T08:31:28.682Z
Execute
{
  "command": "cat /Users/mini/bites/links/ui/server.log 2>/dev/null | tail -20",
  "riskLevel": "low",
  "riskLevelReason": "Checking logs"
}
Result
2026-02-03T08:31:33.500Z
The LaunchAgent doesn't have access to the cookies. Let me check how bird is configured:
Execute
{
  "command": "cat ~/.config/bird/config.json5 2>/dev/null || echo \"No config\"",
  "riskLevel": "low",
  "riskLevelReason": "Reading config"
}
Result
No config


[Process exited with code 0]
2026-02-03T08:31:39.385Z
Execute
{
  "command": "bird whoami 2>&1",
  "riskLevel": "low",
  "riskLevelReason": "Checking bird auth"
}
Result
📍 env AUTH_TOKEN
** @bentossell (Ben Tossell)
🪪 53175441
⚙️ graphql
🔑 env AUTH_TOKEN


[Process exited with code 0]
2026-02-03T08:31:46.322Z
It uses env vars. I need to pass them to the LaunchAgent:
Execute
{
  "command": "echo $AUTH_TOKEN | head -c 20 && echo \"...\"\necho $CT0 | head -c 20 && echo \"...\"",
  "riskLevel": "low",
  "riskLevelReason": "Checking if env vars exist"
}
Result
b8a7e2961a3da03dcdc7...
2c1cb9a0e86e7880b734...


[Process exited with code 0]
2026-02-03T08:31:51.456Z
Execute
{
  "command": "grep -E \"AUTH_TOKEN|CT0\" ~/.secrets 2>/dev/null | head -2",
  "riskLevel": "low",
  "riskLevelReason": "Checking secrets file"
}
Result
export AUTH_TOKEN="****************************************"
export CT0="2c1cb9a0e86e7880b734fc79f527c8d0826d30a6f0ebf4c8938160033994957bb49f459edf5965fba409e371b67d29b24f23de474ddcf69883ae7b90fd3679ea6b559e28afea855b2961b6980dc84fa8"


[Process exited with code 0]
2026-02-03T08:31:58.182Z
I need to update the server to source the secrets file, or update the LaunchAgent. Let me modify the server to source secrets:
Edit
Result
Error: Error: File "/Users/mini/bites/links/ui/server.ts" has been modified externally since it was last read. Please use the Read tool to get the latest version before editing.
2026-02-03T08:32:02.575Z
Read
{
  "file_path": "/Users/mini/bites/links/ui/server.ts",
  "limit": 80
}
Result
2026-02-03T08:32:07.538Z
Edit
Result
2026-02-03T08:32:12.877Z
Execute
Result
{
  "success": true,
  "added": 0,
  "total": 0
}


[Process exited with code 0]
2026-02-03T08:32:20.025Z
Execute
{
  "command": "cat /Users/mini/bites/links/ui/server.log 2>/dev/null | tail -30",
  "riskLevel": "low",
  "riskLevelReason": "Checking logs"
}
Result
2026-02-03T08:32:27.171Z
Bun shell doesn't support `source`. Let me use a different approach - read the secrets file and set env vars:
Read
{
  "file_path": "/Users/mini/bites/links/ui/server.ts",
  "limit": 20
}
Result
2026-02-03T08:32:37.299Z
Edit
Result
2026-02-03T08:32:42.527Z
Edit
Result
2026-02-03T08:32:50.038Z
Execute
Result
{
  "success": true,
  "added": 20,
  "total": 20
}


[Process exited with code 0]
2026-02-03T08:33:01.254Z
Now let's check if media is being captured:
Execute
Result
{
  "username": "ashpreetbedi",
  "media_count": 0,
  "links_count": 0
}
{
  "username": "ai",
  "media_count": 0,
  "links_count": 0
}
{
  "username": "Shpigford",
  "media_count": 0,
  "links_count": 0
}
{
  "username": "krzyzanowskim",
  "media_count": 2,
  "links_count": 1
}
{
  "username": "chamath",
  "media_count": 1,
  "links_count": 0
}


[Process exited with code 0]
2026-02-03T08:33:06.631Z
Media is being captured. Let me verify the UI works:
Execute
{
  "command": "curl -s http://localhost:4892/ | head -50",
  "riskLevel": "low",
  "riskLevelReason": "Testing UI endpoint"
}
Result
2026-02-03T08:33:11.853Z
TodoWrite
Result
TODO List Updated
2026-02-03T08:33:21.849Z
Done! The UI is now running at: **http://bens-mac-mini:4892** (or http://100.106.254.46:4892) **Features:** - **Media display**: Images and videos from tweets are shown inline - **Categorization**: Click 📰 to mark as "Newsletter", "Skip" to exclude - **Filters**: View All / Uncategorized / Newsletter / Skipped - **Stats dashboard**: See counts at a glance - **Examples stored**: Your categorizations are saved to `/Users/mini/bites/links/bookmarks.json` - the `examples` object tracks newsletter vs skip IDs for training future auto-classification **API endpoints:** - `GET /api/examples` - returns your labeled examples for use in a classification prompt - `POST /api/categorize/:id` - set category for a bookmark As you categorize more bookmarks, the examples build up. You can then use `/api/examples` to feed them into an LLM prompt for automatic classification.

The script ran, found my bookmarks, classified them, and posted to Slack. The local file started filling up with my curated links.

---

The result

Now I just bookmark. That's it. The pipeline handles everything else:

  • **Local archive** — `~/bites/links/bookmarks.md` grows over time, searchable
  • **Slack visibility** — Links show up organized, I don't forget them
  • **AI sorting** — News vs products vs research, no manual tagging
  • **Zero friction** — Bookmark on phone, see it categorized in Slack

---

But wait, I want a UI

The script works. But I wanted to see my bookmarks, manage the config, trigger syncs manually. So I asked for a web UI.

Session
2026-02-03T08:35:47.827Z
when clicking 'skip' i dont see any state change. but it shows in 'skipped' tab. we also need to figure out a good way to display articles ie this is an article https://x.com/ashpreetbedi/status/2018059495335764273

The agent scaffolded a bun + hono server with a full dashboard — stats, recent bookmarks, config editor, manual sync button.

---

Categorization for the newsletter

I wanted to categorize bookmarks — is this newsletter material or skip? And use my categorizations as training examples for auto-classification.

Session
2026-02-03T08:35:50.377Z
Error: Request was aborted.

Now I can mark bookmarks as "newsletter" or "skip". Those become examples for the LLM to learn my preferences.

---

The rules system

I wanted the system to learn from my categorizations and eventually do it automatically.

Session
2026-02-03T08:46:31.949Z
ok so i've categorised them. what i want is all my bookmarks to get pull into this ui. then when i categorise them, thats like my RLHF for the categorisation rules, so they should be updated somewhere (in agents.md? or elsewhere?) then at some point i'll have my agent do the categorisation for me, and ask me for review, when i review - any changes i make should update the rules. the plan is to get to a fully-autonomous system that pulls my bookmarks and categorises them to be included in the newsletter.
2026-02-03T08:46:51.941Z
Thinking... (click to expand)
The user wants to build a system where: 1. All bookmarks are pulled into the UI 2. When they categorize, it acts as training data (RLHF-style) 3. Rules should be updated somewhere based on their categorizations 4. Eventually an agent auto-categorizes, user reviews, and corrections update rules 5. End goal: fully autonomous bookmark → newsletter pipeline This is a great use case. Let me think about the architecture: 1. **Training data collection** - already happening with `category` field and `examples` in bookmarks.json 2. **Rule extraction** - we need to analyze the categorized bookmarks and extract patterns/rules. This could be: - A prompt that gets updated with examples - A rules file that describes what makes something newsletter-worthy - Both 3. **Auto-classification** - when new bookmarks come in, run them through the LLM with the rules/examples 4. **Review flow** - show auto-categorized items for review, corrections update the rules Let me propose creating: 1. A `classification-rules.md` file that contains the rules/prompt for categorization 2. Update the sync to auto-classify new bookmarks using these rules 3. Add a "pending review" state for auto-classified items 4. When user confirms or changes, update the rules/examples I should ask the user what they prefer for the rules storage and how they want this to work.
Great vision. Here's how I'd structure it: ## Proposed System ``` New Bookmark → Auto-classify → Pending Review → User confirms/corrects → Rules update ``` ### Storage 1. **`bookmarks.json`** - raw data + categories (already exists) 2. **`classification-rules.md`** - human-readable rules the agent follows 3. **`examples.json`** - curated few-shot examples for the prompt (extracted from your categorizations) ### Bookmark States - `null` - uncategorized (needs manual review) - `newsletter` / `skip` - manually categorized (training data) - `pending_newsletter` / `pending_skip` - auto-classified, awaiting review - When you confirm → stays as is - When you change → updates rules ### Rule Learning When you categorize, the system: 1. Looks at patterns (author, topic, engagement, links, keywords) 2. Updates `classification-rules.md` with observations 3. Keeps best examples in `examples.json` --- **Questions:** 1. **Rule format** - should rules be in markdown (human-readable, you can edit) or just emergent from examples (pure few-shot)? 2. **Review UX** - separate "Pending Review" tab, or inline badges on auto-classified items? 3. **When to auto-classify** - on sync (midnight/9am), or also when you click "Sync Now"? Want me to build this out? I'd start with the rules file and auto-classification on sync.

The agent built a rules system:

  • Store categorization examples in markdown
  • Show a "Rules" tab in the UI
  • Run auto-classification on sync
  • Let me review and override

---

The final system

Now I have:

  • **CLI script** — runs on cron, pulls bookmarks, posts to Slack
  • **Web dashboard** — view bookmarks, categorize them, edit config
  • **Auto-classification** — learns from my examples, suggests categories
  • **Review flow** — AI suggests, I approve or override, system learns
Session
2026-02-03T09:08:00.002Z
ok i just changed the settings - what did i save to do? will it work as intended?
2026-02-03T09:08:07.796Z
Thinking... (click to expand)
The user wants to verify what settings they saved and if it will work correctly. Let me check the config file to see what they saved.
Execute
{
  "command": "cat /Users/mini/bites/links/.config.json | jq",
  "riskLevel": "low",
  "riskLevelReason": "Reading config file"
}
Result
{
  "slackWebhook": "*********************************************************************************",
  "slackMaxBookmarks": 1,
  "slackTextLength": 150,
  "slackTemplate": "{{#bookmarks}}\n<{{url}}>\n{{/bookmarks}}"
}


[Process exited with code 0]
2026-02-03T09:08:16.505Z
You saved: - **Max bookmarks**: 1 (only sends 1 bookmark per message) - **Template**: Just the URL, no header or text So your Slack message will look like: ``` <https://x.com/username/status/123> ``` That's a bare URL with Slack's `<url>` format which will auto-unfurl/preview. **Will it work?** Yes, but note: - Only 1 bookmark per sync notification (even if you bookmark 5 new tweets, Slack only shows 1) - No count header, just the link Was that intentional? If you want all new bookmarks but minimal format, change `slackMaxBookmarks` back to 10+ and use: ``` {{#bookmarks}} <{{url}}> {{/bookmarks}} ```

The whole thing runs on localhost. I categorize a few, the system learns, and over time it handles more automatically.

*Total build time: ~30 minutes of conversation. Full system with web UI and learning categorization.*

Related Posts