11 KiB
name, description
| name | description |
|---|---|
| note | 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.pywrites the raw input verbatim intonotes/inbox/(atomic) plus one line tolog/note.log. No reformulation, no reading ofnotes.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):
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.
- Read
Channel/Chat IDfrom the runtime context if present. - Capture, in two steps —
write_filethe raw input totmp/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. - Run the Compile workflow (below) inline: acquire the lock, process
notes/inbox/, file intonotes/notes.md, move the source tonotes/done/(ornotes/hard/). - Commit the change (see Versioning below): via
execrungit add notes/ && git commit -m "note: <short summary of what landed>". - 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.
- Read
Channel/Chat IDfrom the runtime context if present. write_filethe raw input totmp/note-capture.md, thenuv 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)- 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:
- 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 editnotes.mdat once. - 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
webtool (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/incompletemarker. Whole articles: summarize the key points. - File it under the right thematic
##section ofnotes/notes.md. Create a new section if none fits. Use a surgicalstr_replace/append — never rewrite the whole document. - Move the source out of
inbox/immediately: tonotes/done/if anything usable was filed (paywall markers count as filed — they are the breadcrumb); tonotes/hard/if nothing usable could be extracted. Move right after each file so a crash mid-batch re-processes at most one.
- 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 …".
- Read the whole
notes/notes.md— that single file is the knowledge base. Do not readinbox/,done/, orhard/(those are the raw pipeline, not the KB). - Answer from it. If the topic is not covered, say so plainly — do not confabulate.
- 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):
- Read
notes/notes.mdto find the matching note(s). That file is the only place a note lives — do not searchdb/, do not runsqlite3, do not readinbox//done//hard/. If nothing matches, say so. If the target is ambiguous or several entries match, list the candidates and ask which one. - 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"…):
- Remove/change it in
notes/notes.mdwith a surgicalstr_replace(never rewrite the whole document). Touchnotes/notes.mdonly — do NOT delete or move anything innotes/done/; those raw-capture breadcrumbs are internal plumbing, not a second copy of the note. - Commit (see Versioning): via
execrungit 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 viaexec(git add notes/ && git commit -m "note: …"). One commit per operation. - Cron drain:
note_compile.pycommits deterministically after the batch — you do not commit inside the cronDRAIN_GOALrun. - Never
git add -A(Dream owns the rest of the workspace); stage onlynotes/. The.compile.lockis gitignored, sogit add notes/never stages it.
Edge cases
/notewith no content → ask what to note.- Empty / whitespace-only input →
note_capture.pyexits non-zero; ask for real content. No such filefrom capture → thewrite_filestep was skipped or the path is wrong; writetmp/note-capture.mdfirst, 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.mdduring acroncapture, stop.