--- name: bookmark description: > Use when the user wants to save a URL (with or without additional content) to read later or search in. Triggers on "bookmark", "save URL". --- # Bookmark Manage a personal reading list stored in SQLite (`db/bookmark.sqlite`). ## Commands All commands run from the workspace root (`working_dir`), with relative script paths — the exec safety guard blocks absolute paths: ```bash uv run skills/bookmark/scripts/bookmark.py [args] ``` ### Add a bookmark ```bash bookmark.py add "" [--tags tag1,tag2] ``` - `url` — the article URL - `description` — short human-readable description (required). If the user did not give one, generate a short (~1 sentence) description yourself — from the article text if you have it, otherwise from the URL. - `--tags` — optional comma-separated tags - `--content-file ` — optional; path to the cleaned article markdown to archive. `-` reads it from stdin. See "Saving an article's full text" below. Example: ```bash bookmark.py add "https://example.com/rust-async" "Async Rust patterns" --tags rust,async ``` ### Saving an article's full text When the user pastes a **large block of text** together with a URL (typically a whole page selected with Ctrl+A/Ctrl+C), treat that text as the **full article to archive**, not as the description. Pick the path by what the pasted content looks like: **If the pasted content is raw HTML** (you see ``, `
`, `

` tags, etc.), do **not** convert it yourself — pipe it through the `html_to_markdown.py` helper (it uses trafilatura to strip boilerplate and emit clean markdown) straight into `add`, so the converted text never passes through your context: ```bash uv run skills/bookmark/scripts/html_to_markdown.py <<'HTML' \ | uv run skills/bookmark/scripts/bookmark.py add "" "" [--tags a,b] --content-file - HTML ``` If `add` does **not** report "article content stored" (trafilatura extracted nothing → empty content), fall back to cleaning the text yourself and storing it as below. **If the pasted content is plain text or already markdown**, store it directly with `--content-file -` via a heredoc (one call, no shell-escaping of the body). Only clean it yourself **if you see obvious boilerplate** (copied menus, "Share"/"Tweet", cookie banners, footers) — otherwise store it as-is: ```bash uv run skills/bookmark/scripts/bookmark.py \ add "" "" [--tags a,b] --content-file - <<'ARTICLE'

ARTICLE ``` In both cases: - **Generate a description** (~1 sentence) from the article, unless the user gave one. - **Confirm and echo.** After saving, tell the user it was stored and quote a short **verbatim** slice of what was archived (the title and first line or two, exactly as written) — not a re-summary — so they can see it worked. **URL without pasted text:** try to fetch the article yourself via the `web` tool (Jina Reader returns clean markdown), then store it the same way. If the page is behind a **paywall** or the fetched text is **garbage/incomplete**, do **not** store that text (never fabricate the article body) — instead **ask the user to copy the whole article (Ctrl+A/Ctrl+C) and paste it**, briefly saying why (paywall / the fetched text looks incomplete). If they paste it, follow the paths above. If they decline or don't reply, save the bookmark without content and mark it `⚠ paywall/incomplete`. ### List unread bookmarks ```bash bookmark.py list [--tag ] ``` Shows display ID, URL, tags, description, and date added for each unread bookmark. Use `--tag` to filter (display IDs stay global, so a filtered list may show gaps). ### Display IDs The `#1`, `#2`, … shown by `list` and `history` are **display IDs** — sequential positions, computed on the fly, never the internal DB id. They renumber whenever the set changes, so run `list`/`history` first if unsure. - `read `, `show `, `content `, and `delete ` take the display ID from **`list`** (the unread set). - `unread ` takes the display ID from **`history`** (the read set). A freshly added bookmark is always display `#1` in `list` (newest first). ### Mark as read ```bash bookmark.py read ``` `` is the number from `list`. Marks bookmark as read (stores `read_at` timestamp). Does **not** delete — entry stays in DB. ### Unmark (mark as unread again) ```bash bookmark.py unread ``` `` is the number from `history`. ### Show bookmark details ```bash bookmark.py show ``` `` is the number from `list`. Shows full URL, description, tags, status, and dates. A `📄` marker means an archived article body is stored (read it with `content`). Does **not** change any state. ### Read stored article content ```bash bookmark.py content ``` `` is the number from `list`. Prints the archived article markdown to stdout — render it for the user. If no full text was stored for that bookmark, it says so. Does **not** change any state. ### Delete a bookmark ```bash bookmark.py delete ``` `` is the number from `list` (the unread set). Soft delete: the bookmark drops out of `list`/`history` but the row stays in the DB (recoverable by hand if ever needed). There is no `restore` command. **Always confirm before deleting.** Display IDs renumber whenever the set changes, so a stale number can point at the wrong bookmark. First run `list`/`show`, tell the user exactly which bookmark you are about to delete (URL + description), and **wait for their explicit confirmation** — only then run `delete`. To delete a bookmark that is already read, `unread` it first (delete works on the unread set only). ### List read bookmarks (history) ```bash bookmark.py history ``` Shows all bookmarks marked as read, with both `added` and `read` dates, numbered with their own display IDs. ## Output formatting When presenting bookmark lists or details to the user, **always use markdown links** so URLs are clickable in WebUI and Telegram: ``` #3 [hackaday.com](https://hackaday.com/2026/06/02/linux-fu-taming-strace/) [linux, strace] — lepší strace ``` Format: `# []() — []` - Domain is clickable, pointing to the full URL - Description after em-dash (most important, always shown) - Tags in brackets, comma-separated (secondary, after description) - A `📄` in `list`/`history`/`show` marks a bookmark with an archived article body — offer to open it with `content ` - **Never** strip URLs from the output or replace them with plain-text summaries ## Workflow 1. User shares a URL → `add` with description and optional tags 2. User pastes a URL **and the full article text** → clean it to markdown and `add … --content-file -` (see "Saving an article's full text") 3. User wants to see what to read → `list` 4. User wants to see details of a bookmark → `show ` (from `list`) 5. User wants to read an archived article → `content ` (from `list`) 6. User finishes an article → `read ` (from `list`) 7. User wants to revisit → `unread ` (from `history`) or `history` 8. User wants to remove one (e.g. accidental duplicate) → confirm, then `delete ` (from `list`) ## Known limitations - No `edit`/`update` command — to change a description or tags, delete and re-add - `delete` is soft-delete only (row stays in DB); no `restore` command - Sites behind Cloudflare bot protection (PCTuning.cz, vtm.zive.cz, zive.cz) cannot be auto-fetched; ask user to paste full HTML manually