Files

7.6 KiB

name, description
name description
bookmark 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:

uv run skills/bookmark/scripts/bookmark.py <command> [args]

Add a bookmark

bookmark.py add <url> "<description>" [--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 <path> — optional; path to the cleaned article markdown to archive. - reads it from stdin. See "Saving an article's full text" below.

Example:

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 <html>, <div>, <p> 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:

uv run skills/bookmark/scripts/html_to_markdown.py <<'HTML' \
  | uv run skills/bookmark/scripts/bookmark.py add "<url>" "<description>" [--tags a,b] --content-file -
<raw html here>
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:

uv run skills/bookmark/scripts/bookmark.py \
    add "<url>" "<description>" [--tags a,b] --content-file - <<'ARTICLE'
<article text / markdown here>
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

bookmark.py list [--tag <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 <n>, show <n>, content <n>, and delete <n> take the display ID from list (the unread set).
  • unread <n> takes the display ID from history (the read set).

A freshly added bookmark is always display #1 in list (newest first).

Mark as read

bookmark.py read <display-id>

<display-id> 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)

bookmark.py unread <display-id>

<display-id> is the number from history.

Show bookmark details

bookmark.py show <display-id>

<display-id> 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

bookmark.py content <display-id>

<display-id> 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

bookmark.py delete <display-id>

<display-id> 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)

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: #<display-id> [<domain>](<url>) — <description> [<tags>]

  • 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 <display-id>
  • 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 <display-id> (from list)
  5. User wants to read an archived article → content <display-id> (from list)
  6. User finishes an article → read <display-id> (from list)
  7. User wants to revisit → unread <display-id> (from history) or history
  8. User wants to remove one (e.g. accidental duplicate) → confirm, then delete <display-id> (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