Update projektu
This commit is contained in:
@@ -1,151 +1,178 @@
|
||||
---
|
||||
name: note
|
||||
description: >
|
||||
Explicit notes.
|
||||
Use when user says "note X", "note it".
|
||||
Capture notes, texts, URLs, or whole articles into a personal knowledge base and
|
||||
answer questions against it. Triggers on "note X", "/note cron X", "search my
|
||||
notes for X", "delete/edit the note about X", "forget X". For filing reference
|
||||
material to search later — not a short durable fact/preference to just
|
||||
remember, and not a bare URL saved only to read later with no filing.
|
||||
---
|
||||
|
||||
# Note
|
||||
|
||||
Explicit note store backed by SQLite. User says "note X" → take only
|
||||
explicitly-typed tags, reformulate content, store via `note.py add`. Delete only
|
||||
on explicit user request. Notes are stored to sqlite db.
|
||||
A personal capture-to-knowledge-base skill. The user throws in notes, texts, URLs, or
|
||||
whole articles from any channel; each input is captured raw, then reformulated and filed
|
||||
into one structured markdown document (`notes/notes.md`) organized into thematic sections
|
||||
that the LLM owns and grows. Search = load the whole document and answer from it.
|
||||
|
||||
## Backend
|
||||
## Architecture — read this first
|
||||
|
||||
`skills/note/scripts/note.py` — CLI wrapper around `db/note.sqlite`.
|
||||
Operation log: `log/note.log` (append-only, all write operations).
|
||||
Two-stage pipeline, one shared compile step:
|
||||
|
||||
## Tag protocol
|
||||
- **Capture (always, instant, dumb).** `note_capture.py` writes the raw input verbatim
|
||||
into `notes/inbox/` (atomic) plus one line to `log/note.log`. No reformulation, no
|
||||
reading of `notes.md`, no fetching. This is all capture ever does.
|
||||
- **Compile (reformulate + file into `notes/notes.md`).** Runs either **inline** in the
|
||||
immediate mode, or in the **background** cron drain. Same workflow either way.
|
||||
|
||||
Tags are the **first token** right after the trigger — comma-separated, no spaces:
|
||||
Storage layout under the workspace root (fixed locations):
|
||||
|
||||
```
|
||||
/note arch explanation of the architecture decision → tags: [arch]
|
||||
/note hw,linux interesting article about kernel → tags: [hw, linux]
|
||||
/note this is a note without tags → tags: []
|
||||
```text
|
||||
notes/
|
||||
├── notes.md ← THE structured doc (thematic ## sections, LLM-owned)
|
||||
├── inbox/ ← pending captures (one file each); compile drains this
|
||||
├── done/ ← successfully compiled captures (sibling of inbox/)
|
||||
├── hard/ ← held back: paywalled / unreadable / ambiguous — for manual review
|
||||
└── .compile.lock ← concurrency lock shared by inline compile and cron drain
|
||||
log/note.log ← append-only audit of every capture
|
||||
```
|
||||
|
||||
Rules:
|
||||
- **Tags come *only* from the first token the user actually typed. Never
|
||||
derive, infer, or invent tags from the note's content, topic, or meaning.**
|
||||
If the user did not type a tag, the note has no tags — full stop.
|
||||
- Lowercase only; multi-word tags use `-`: `cli`, `soft-delete`, `task-queue`
|
||||
- If user writes `#tag`, strip `#` before passing to the script
|
||||
- If no tag is given — that is fine, use no tags; never force tags
|
||||
**No database — ever.** Notes live *only* in `notes/notes.md` (prose) plus the `notes/`
|
||||
pipeline dirs above. There is **no** SQLite/DB backend. To find a note, `grep` or read
|
||||
`notes/notes.md` — **never search `db/`, never run `sqlite3`, never create or open any
|
||||
`.sqlite`/`.db` file.** (The AGENTS.md "store SQLite under `db/`" convention does **not**
|
||||
apply to notes — that is for other skills.) An older version of this skill used a
|
||||
database; it is gone. If you catch yourself opening a DB, stop — the answer is in
|
||||
`notes/notes.md`.
|
||||
|
||||
Tags must be **registered before use**. There is no auto-creation: the database
|
||||
holds a registry of known tags, and `add` rejects any tag that is not in it (exit
|
||||
2). A new tag is born only via the explicit `tag-add` command (see Tag management).
|
||||
Still only pass tags the user typed — registration does not license inventing them.
|
||||
**Separate store.** `notes/` is not agent memory: keep it distinct from `keep`,
|
||||
`MEMORY.md`, and the llm-wiki store (`cml/`). Never cross-read or cross-write between
|
||||
them. The Dream processor must not touch `notes/`.
|
||||
|
||||
## Write protocol
|
||||
**Run scripts with `uv run`, workspace-relative paths** (exec runs from the workspace
|
||||
root, not the skill dir): `uv run skills/note/scripts/<script>.py …`.
|
||||
|
||||
1. Take inline tags from the first token only (see Tag protocol above). If that
|
||||
token is not a tag the user typed, the tags field stays empty — never fill it
|
||||
from the content.
|
||||
2. Reformulate the remaining text into a terse fact. One concept per entry —
|
||||
split if too complex; omit context that is not itself a fact. Preserve
|
||||
input language; never translate. Drop filler.
|
||||
- Input: "poznamenej si, glow zobrazuje markdown v terminálu #cli"
|
||||
- Run: `uv run skills/note/scripts/note.py add "glow displays markdown in terminal" --tags cli`
|
||||
3. **Unknown tag (`add` exits 2, prints `Unknown tag(s): …`):** the note was NOT
|
||||
stored. For each unknown tag, ask the user (in their language): "Tag #X
|
||||
doesn't exist — create it?"
|
||||
- **Yes** → `uv run skills/note/scripts/note.py tag-add X`, then re-run `add`
|
||||
with the original tags.
|
||||
- **No** → re-run `add` without that tag (keep the known ones). If nothing
|
||||
remains, store with no tags.
|
||||
4. Echo: `Noted [#1]: <content> [#tag1 #tag2]` (tags omitted if none).
|
||||
`#1` is the display ID of the new note — use it to delete immediately if needed.
|
||||
**Language.** This skill body is English; always reply to the user in the user's own
|
||||
language.
|
||||
|
||||
No dedup. No MEMORY.md lookup. Blind append.
|
||||
## `/note <text>` — capture and file NOW (default)
|
||||
|
||||
## Tag management
|
||||
The default: file the note into the knowledge base immediately, in this turn.
|
||||
|
||||
Tags are created and listed explicitly — never as a side effect of adding a note.
|
||||
1. Read `Channel` / `Chat ID` from the runtime context if present.
|
||||
2. Capture:
|
||||
`uv run skills/note/scripts/note_capture.py --text "<raw input>" [--channel <ch>] [--chat-id <id>]`
|
||||
Pass the input **as-is** — do not reformulate or strip URLs here.
|
||||
3. Run the **Compile workflow** (below) inline: acquire the lock, process `notes/inbox/`,
|
||||
file into `notes/notes.md`, move the source to `notes/done/` (or `notes/hard/`).
|
||||
4. **Commit** the change (see *Versioning* below): via `exec` run
|
||||
`git add notes/ && git commit -m "note: <short summary of what landed>"`.
|
||||
5. Confirm to the user **which section** it landed under, **and quote the exact
|
||||
text that was filed** (the reformulated fact(s), verbatim as written into
|
||||
`notes.md` — not a re-summary of it), in their language.
|
||||
|
||||
Trigger (create): `/note tag add X`, "create tag X", "register tag X".
|
||||
This blocks the turn for a while (reads the whole doc; a URL/article adds a fetch). If
|
||||
the user is firing off many notes quickly, suggest `/note cron`.
|
||||
|
||||
1. Run: `uv run skills/note/scripts/note.py tag-add X`
|
||||
2. Echo the result. Already-existing tag → script reports it and exits 0 (no error).
|
||||
3. No tag name given → ask which tag to create; do not guess.
|
||||
## `/note cron <text>` — deferred capture
|
||||
|
||||
Trigger (list): `/note tags`, "what tags are there?", "list tags".
|
||||
Capture only; let the background cron file it later. Fast, non-blocking.
|
||||
|
||||
1. Run: `uv run skills/note/scripts/note.py tag-list`
|
||||
2. Echo output. Empty → "No tags."
|
||||
1. Read `Channel` / `Chat ID` from the runtime context if present.
|
||||
2. `uv run skills/note/scripts/note_capture.py --text "<raw input>" [--channel <ch>] [--chat-id <id>]`
|
||||
3. Confirm in **one short line** (e.g. "captured — I'll file it in the background") and **STOP the
|
||||
turn**. Forbidden here: reformulating, reading `notes/notes.md`, running any compile
|
||||
step, taking the lock. If you catch yourself about to read the doc, you are compiling
|
||||
inline — stop and just capture.
|
||||
|
||||
Tags are referenced by name everywhere (no display ID). There is no tag deletion.
|
||||
## Compile workflow (shared: inline immediate mode + cron drain)
|
||||
|
||||
## List protocol
|
||||
The cron (`note_compile.py`) invokes this via a drain goal; immediate mode runs it inline.
|
||||
Either way:
|
||||
|
||||
Trigger: `/note list`, `show notes`, `what notes do you have?`
|
||||
1. **Take the lock.** Create `notes/.compile.lock` (skip if a live one exists — another
|
||||
compile is running; try again later). The cron script handles this itself; inline mode
|
||||
must respect it so an inline merge and a cron drain never edit `notes.md` at once.
|
||||
2. **For each file in `notes/inbox/`:**
|
||||
- **Reformulate** the body into a terse fact (or a few). One concept per entry; drop
|
||||
filler; **preserve the input language** — never translate. Split if too complex.
|
||||
If the input is Czech typed without diacritics (e.g. "kdyz uz to psal bez hacku"),
|
||||
restore correct diacritics as part of reformulation. Leave already-accented text
|
||||
and non-Czech text untouched — never add diacritics where none belong.
|
||||
- **URLs:** extract **every** URL from the body (0..N). Fetch each with the `web` tool
|
||||
(Jina Reader — returns clean markdown, handles JS and soft paywalls). If a URL is
|
||||
**paywalled / login-gated / truncated / unreadable** (login/subscribe/metered
|
||||
content, very short output, HTTP 401/403): **do not fabricate a summary** — write
|
||||
just the URL + any available title + a `⚠ paywall/incomplete` marker. Whole articles:
|
||||
summarize the key points.
|
||||
- **File it** under the right thematic `##` section of `notes/notes.md`. Create a new
|
||||
section if none fits. Use a surgical `str_replace`/append — never rewrite the whole
|
||||
document.
|
||||
- **Move the source out of `inbox/` immediately:** to `notes/done/` if anything usable
|
||||
was filed (paywall markers count as filed — they are the breadcrumb); to `notes/hard/`
|
||||
if nothing usable could be extracted. Move right after each file so a crash mid-batch
|
||||
re-processes at most one.
|
||||
3. **Release the lock** (the cron script does this in `finally`).
|
||||
|
||||
1. Run: `uv run skills/note/scripts/note.py list [--limit N] [--tag TAG [TAG ...]]`
|
||||
2. Echo output. If empty → respond "No notes."
|
||||
Never assert content you could not read. When unsure, hedge or mark it.
|
||||
|
||||
`--tag` accepts one or more tags; OR logic (notes with at least one matching tag).
|
||||
## `/note search <query>` / `/note find <query>` — query
|
||||
|
||||
The number before each note (`1.`, `2.`, …) is the **display ID** — sequential
|
||||
among active notes, newest first. Renumbers after every deletion. Never change,
|
||||
renumber, or drop it.
|
||||
Also triggered by "what do I have on …?", "find in my notes …".
|
||||
|
||||
### URLs in a note
|
||||
1. Read the whole `notes/notes.md` — that single file **is** the knowledge base. Do
|
||||
not read `inbox/`, `done/`, or `hard/` (those are the raw pipeline, not the KB).
|
||||
2. Answer from it. If the topic is not covered, say so plainly — do not confabulate.
|
||||
3. Read-only: never modify the document in a search turn.
|
||||
|
||||
The script already lays out each URL (with its inline label, if any) on its own
|
||||
indented bullet line. **Echo the output verbatim** — keep the bullets and line
|
||||
breaks, keep URLs bare. Never collapse the bullets back onto one line and never
|
||||
wrap a URL in `[text](url)`: this chat UI merges two adjacent inline links into
|
||||
one block, hides the second URL, and overlays the list number. Bare URLs on their
|
||||
own lines autolink correctly and stay separate.
|
||||
## `/note delete <query>` / `/note edit <query>` — remove or change a note
|
||||
|
||||
## Show protocol
|
||||
Also triggered by "delete/remove the note about X", "forget X", "edit/update the note
|
||||
about X". Notes are prose in `notes/notes.md` — **no IDs, no DB.** The user
|
||||
names a note by describing it; you find it by reading the document.
|
||||
|
||||
Trigger: `/note show <id>`, `show note N`, `read note N`, `what does note N say`.
|
||||
**This is a hard-gated TWO-TURN flow. NEVER delete or edit in the same turn as the
|
||||
request — showing is not doing.**
|
||||
|
||||
1. Display IDs are the same as in `list`/`delete` — sequential among active
|
||||
notes, newest first, renumbered after every deletion. If unsure, run `list`
|
||||
first.
|
||||
2. Run: `uv run skills/note/scripts/note.py show <display-id>`
|
||||
- Exit 0 → **output the script's stdout verbatim — print every line exactly
|
||||
as emitted.** Do not summarize, shorten, rewrap, or drop any part of the
|
||||
`content` field, including URLs and links. The `show` command exists
|
||||
precisely to surface the note in full; brevity directives do not apply here.
|
||||
- Exit 1 → display ID out of range; respond accordingly.
|
||||
3. `show` is read-only — it never deletes or modifies anything.
|
||||
**Turn 1 — locate and confirm (absolutely NO mutation):**
|
||||
|
||||
The block contains every stored field: display ID, internal DB id, creation
|
||||
timestamp, tags, and full untruncated content.
|
||||
1. Read `notes/notes.md` to find the matching note(s). That file is the only place a note
|
||||
lives — **do not search `db/`, do not run `sqlite3`, do not read `inbox/`/`done/`/
|
||||
`hard/`.** If nothing matches, say so. If the target is ambiguous or several entries
|
||||
match, list the candidates and ask which one.
|
||||
2. Show the user the **exact verbatim line(s)/section** you would remove (for an edit: the
|
||||
`before` → `after`), and ask them to confirm in plain words. Then **STOP the turn.**
|
||||
Forbidden this turn: `str_replace`/`edit_file`, `rm`, `git`, or any other mutation.
|
||||
|
||||
## Delete protocol
|
||||
**Turn 2 — only after the user explicitly confirms ("yes", "confirmed", "delete it"…):**
|
||||
|
||||
Trigger: `/note delete`, `delete a note`, `remove a note`.
|
||||
1. Remove/change it in `notes/notes.md` with a surgical `str_replace` (never rewrite the
|
||||
whole document). **Touch `notes/notes.md` only** — do NOT delete or move anything in
|
||||
`notes/done/`; those raw-capture breadcrumbs are internal plumbing, not a second copy
|
||||
of the note.
|
||||
2. Commit (see *Versioning*): via `exec` run
|
||||
`git add notes/ && git commit -m "note: delete <short desc>"` (edit → `note: edit …`).
|
||||
|
||||
1. If the user has not specified an ID, run `list` first to show current notes.
|
||||
2. Run: `uv run skills/note/scripts/note.py delete <display-id>`
|
||||
- Exit 0 → confirm deletion.
|
||||
- Exit 1 → display ID out of range; respond accordingly.
|
||||
3. Nothing is deleted automatically. Only this explicit protocol deletes.
|
||||
**Exception:** a capture still **pending** (not yet compiled, a file in `notes/inbox/`)
|
||||
never reached the KB — you may cancel it directly with `exec: rm notes/inbox/<file>`
|
||||
(commit only if it was already tracked).
|
||||
|
||||
Display IDs renumber after every deletion (e.g., after deleting #3, the old #4
|
||||
becomes #3). Always run `list` first if unsure of current IDs.
|
||||
## Versioning (git)
|
||||
|
||||
`notes/` lives inside the workspace git repo. The Dream processor never touches it, so
|
||||
**this skill is the only thing that commits `notes/`** — do it after every change to
|
||||
`notes/notes.md`:
|
||||
|
||||
- **Inline `/note <text>` and delete/edit:** commit in the same turn via `exec`
|
||||
(`git add notes/ && git commit -m "note: …"`). One commit per operation.
|
||||
- **Cron drain:** `note_compile.py` commits deterministically after the batch — you do
|
||||
not commit inside the cron `DRAIN_GOAL` run.
|
||||
- Never `git add -A` (Dream owns the rest of the workspace); stage only `notes/`. The
|
||||
`.compile.lock` is gitignored, so `git add notes/` never stages it.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- `/note` with no content → ask "What should I note?"
|
||||
- Vague input → ask for the concrete fact; do not store a placeholder.
|
||||
- `/note tag add` with no name → ask which tag to create; never guess.
|
||||
- `/note show` with no ID → run `list` first, then ask which display ID.
|
||||
- `/note delete` with no ID → run `list` first, then ask which display ID.
|
||||
- Multi-line input → collapse to one line; one entry = one row.
|
||||
|
||||
## Rules
|
||||
|
||||
- Never store verbatim input. Always reformulate. Preserve input language.
|
||||
- Do not store smalltalk or meta-commentary about the note skill itself.
|
||||
- **No auto-load:** `note.sqlite` is never referenced in bootstrap files.
|
||||
- **No auto-delete / no compaction.** Only explicit delete marks an entry.
|
||||
- **Delete is soft** — the entry is marked with a timestamp, not removed from
|
||||
the database. The operation log (`log/note.log`) is the primary audit trail.
|
||||
- Separate from `/keep`, `MEMORY.md`, Dream — never cross-write or cross-read.
|
||||
- `/note` with no content → ask what to note.
|
||||
- Empty / whitespace-only input → `note_capture.py` exits non-zero; ask for real content.
|
||||
- The compile step, not capture, decides sections and does all fetching. If you ever find
|
||||
yourself reformulating or reading `notes.md` during a `cron` capture, stop.
|
||||
|
||||
Reference in New Issue
Block a user