167 lines
7.7 KiB
Markdown
167 lines
7.7 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 via:
|
|
|
|
```bash
|
|
/home/nanobot/.local/bin/uv run /home/nanobot/.nanobot/workspace/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
|
|
/home/nanobot/.local/bin/uv run /home/nanobot/.nanobot/workspace/skills/bookmark/scripts/html_to_markdown.py <<'HTML' \
|
|
| /home/nanobot/.local/bin/uv run /home/nanobot/.nanobot/workspace/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
|
|
/home/nanobot/.local/bin/uv run /home/nanobot/.nanobot/workspace/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
|