| name | denotecli |
| description | Search, read, and analyze 3,000+ Denote/org-mode notes. Supports title/tag search, full-text search, heading search, outline extraction, and content reading. Use when working with ~/org/, Denote files, org-mode knowledge bases, or when user asks about notes, journal entries, or bibliography. |
denotecli — Denote Knowledge Base CLI
Binary: {baseDir}/denotecli. All output is JSON. Default --dirs: ~/org.
Commands
| Command | Args | Description |
|---|
search | QUERY [--tags T] [--title-only] [--max N] | Find notes by title/tag/ID. Multiple words = AND |
search-content | QUERY [--tags T] [--max N] [--matches M] | Full-text grep across all files (~300ms) |
search-headings | QUERY [--level N] [--tags T] [--max N] | Find org headings across all files |
read | ID [--offset N --limit N] | Read note content + frontmatter + links |
read --outline | ID [--level N] | Heading structure with line numbers → use for offset/limit |
day | DATE [--years-ago N] [--days-ago N] | Journal/diary/notes for a date (notes_created + notes_modified) |
timeline-journal | --month YYYY-MM | Monthly journal activity overview |
graph | ID | Outgoing + incoming links (backlinks) |
tags | [--top N] [--pattern PAT] [--suggest] | Tag stats, duplicate detection |
keyword-map | [QUERY] | Korean↔English keyword mapping |
create | --title T --tags T [--dir D] [--content C] | Create new Denote note |
rename-tag | --from T --to T [--dry-run] | Batch rename tag across all files |
Examples
{baseDir}/denotecli search "에릭 호퍼" --max 5
{baseDir}/denotecli search-content "양자역학 관찰자" --max 10
{baseDir}/denotecli search-headings "창조" --level 1 --tags bib
{baseDir}/denotecli read 20250314T152111 --outline --level 2
{baseDir}/denotecli read 20250314T152111 --offset 40 --limit 30
{baseDir}/denotecli day --years-ago 3
{baseDir}/denotecli graph 20250314T125213
{baseDir}/denotecli tags --suggest
{baseDir}/denotecli keyword-map "이맥스"
{baseDir}/denotecli create --title "새 노트" --tags llmlog,topic --dir ~/org/llmlog
{baseDir}/denotecli rename-tag --from llms --to llm --dry-run
Workflow
1. search or search-headings → find note ID
2. read ID --outline → see structure + line numbers
3. read ID --offset N --limit M → read specific section
4. graph ID → explore connections
For date queries: day + gitcli + lifetract = full daily view (see day-query skill).
Key Flags
| Flag | Commands | Description | Default |
|---|
--dirs D,... | most | Search directories | ~/org |
--max N | search* | Max result files | 20 |
--matches N | search-content | Max matches per file | 3 |
--tags T,... | search*, create | Filter/assign by tag (OR). Reads filename slots ∪ #+filetags: header (union) | all |
--level N | search-headings, read --outline | Max heading level (0=all) | 0 |
--offset N | read | Start line (1-indexed) | 0 |
--limit N | read | Lines to read (0=all) | 0 |
--title-only | search | Title field only | false |
Output Contract
- Empty result =
[] (JSON array), never null. Applies to all search-like commands (search, search-content, search-headings, tags, keyword-map, graph outgoing/incoming, read --outline, rename-tag, day entries). Safe to call len(json.load(...)) directly. New in e0a6c52 (2026-05-12).
- Unknown flag = fatal.
error: unknown flag: --X → exit 1. No silent ignore. Typos like --tag (vs --tags) or --limit (vs --max) are caught immediately. Applies to all 11 commands. New in e0a6c52.
- Header-aware indexing.
search and --tags index #+title: and #+filetags: headers (top 30 frontmatter lines) in union with the filename slots. Previously filename-only — 6.4% of corpus (192/3,505 notes) had header-only words that silently missed. Each result carries header_title field when present. Added 2026-05-12.
Modification time — date is not it
date is the note's creation stamp (from its Denote id), so comparing it against a commit
time answers the wrong question. The modification stamp is #+hugo_lastmod:, and it now ships
in the JSON of search / list / day / read:
lastmod — normalised YYYY-MM-DD, the shape day has always used.
hugo_lastmod — raw, with HH:MM intact. Compare times with this one. Normalising the
time away is what made sorge read an 18:32 commit as newer than a 21:55 stamp on the same day.
So "which notes have gone stale" is one call now, not one call plus opening every file.
date itself has two shapes by command, and they are not interchangeable as strings:
search/list give 2026-02-22 (derived from the id), read gives [2026-02-22 Sun 09:00]
(the #+date: line verbatim).
read --outline also carries description and abstract (the callout before the first
heading), so "what is this note about" no longer needs the body. All four fields are
omitempty — a note that lacks one simply has no key.
Full field-by-field contract lives in the denotecli repo (README.md ## Output, AGENTS.md);
that repo's caretaker document is denote id 20260222T090000. Copying the field list here would
make a second copy that ages on its own — v0.9.0, deployed 2026-09-04.
Notes
Denote filename format
YYYYMMDDTHHMMSS[==SIGNATURE]--title__tag1_tag2.org
Knowledge base: ~/org/
notes/ (800+), bib/ (900+), journal/ (700+), llmlog/ (300+), meta/, archives/
Why not rg/fd?
Structured JSON output (ID, tags, links parsed), heading-aware navigation, Korean↔English bridging, tag governance.