192 lines
11 KiB
Markdown
192 lines
11 KiB
Markdown
---
|
|
name: note
|
|
description: >
|
|
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
|
|
|
|
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.
|
|
|
|
## Architecture — read this first
|
|
|
|
Two-stage pipeline, one shared compile step:
|
|
|
|
- **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.
|
|
|
|
Storage layout under the workspace root (fixed locations):
|
|
|
|
```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
|
|
```
|
|
|
|
**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`.
|
|
|
|
**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/`.
|
|
|
|
**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 …`.
|
|
|
|
**Never pass the text on the command line.** Not as an argument, not in a heredoc, not
|
|
through a pipe — always `write_file` it to `tmp/` and pass the path. The exec safety
|
|
guard scans the raw command string and has no shell parser, so quoting does not help: a
|
|
colon right after a letter that follows a diacritic parses as a Windows drive path, and
|
|
a note like `Cíl: koupit mléko` blocks the whole command with *path outside working dir*.
|
|
Since capture takes the user's input verbatim, this would hit real notes, not edge cases.
|
|
|
|
**Language.** This skill body is English; always reply to the user in the user's own
|
|
language.
|
|
|
|
## `/note <text>` — capture and file NOW (default)
|
|
|
|
The default: file the note into the knowledge base immediately, in this turn.
|
|
|
|
1. Read `Channel` / `Chat ID` from the runtime context if present.
|
|
2. Capture, in two steps — `write_file` the raw input to `tmp/note-capture.md`, then:
|
|
`uv run skills/note/scripts/note_capture.py --file tmp/note-capture.md [--channel <ch>] [--chat-id <id>]`
|
|
Pass the input **as-is** — do not reformulate or strip URLs here. See
|
|
*Never pass the text on the command line* below for why it goes through a file.
|
|
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.
|
|
|
|
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`.
|
|
|
|
## `/note cron <text>` — deferred capture
|
|
|
|
Capture only; let the background cron file it later. Fast, non-blocking.
|
|
|
|
1. Read `Channel` / `Chat ID` from the runtime context if present.
|
|
2. `write_file` the raw input to `tmp/note-capture.md`, then
|
|
`uv run skills/note/scripts/note_capture.py --file tmp/note-capture.md [--channel <ch>] [--chat-id <id>]`
|
|
(the extra write is what keeps the capture from being blocked — see below; it is still
|
|
one fast turn, so this mode stays non-blocking)
|
|
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.
|
|
|
|
## Compile workflow (shared: inline immediate mode + cron drain)
|
|
|
|
The cron (`note_compile.py`) invokes this via a drain goal; immediate mode runs it inline.
|
|
Either way:
|
|
|
|
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`).
|
|
|
|
Never assert content you could not read. When unsure, hedge or mark it.
|
|
|
|
## `/note search <query>` / `/note find <query>` — query
|
|
|
|
Also triggered by "what do I have on …?", "find in my notes …".
|
|
|
|
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.
|
|
|
|
## `/note delete <query>` / `/note edit <query>` — remove or change a note
|
|
|
|
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.
|
|
|
|
**This is a hard-gated TWO-TURN flow. NEVER delete or edit in the same turn as the
|
|
request — showing is not doing.**
|
|
|
|
**Turn 1 — locate and confirm (absolutely NO mutation):**
|
|
|
|
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.
|
|
|
|
**Turn 2 — only after the user explicitly confirms ("yes", "confirmed", "delete it"…):**
|
|
|
|
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 …`).
|
|
|
|
**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).
|
|
|
|
## 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 to note.
|
|
- Empty / whitespace-only input → `note_capture.py` exits non-zero; ask for real content.
|
|
- `No such file` from capture → the `write_file` step was skipped or the path is wrong;
|
|
write `tmp/note-capture.md` first, never fall back to passing the text as an argument.
|
|
- 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.
|