Update projektu

This commit is contained in:
lachtan
2026-07-22 12:32:02 +02:00
parent 19014ed3d9
commit 8e66d6b92a
22 changed files with 1995 additions and 503 deletions

View File

@@ -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.