Files
nanobot-runtime/skills/bookmark/SKILL.md

167 lines
7.6 KiB
Markdown

---
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 <command> [args]
```
### Add a bookmark
```bash
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:
```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 `<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:
```bash
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:
```bash
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
```bash
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
```bash
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)
```bash
bookmark.py unread <display-id>
```
`<display-id>` is the number from `history`.
### Show bookmark details
```bash
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
```bash
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
```bash
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)
```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: `#<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