Denote knowledge base CLI for AI agents — search, read, analyze, and manage 3,000+ org-mode notes
Go stdlib only. Single binary. JSON output. Korean-native.
AI Agent Skill: 에이전트용 스킬 문서는 agent-config 리포의
skills/denotecli/SKILL.md에서 관리합니다.
CLI that gives AI agents structured access to a Denote/org-mode knowledge base. Not just search — semantic navigation, Korean↔English bridging, tag governance, and knowledge graph traversal.
./run.sh build # Build + install
./run.sh test # 135 tests
./run.sh showcase # Visual search pattern check
./run.sh cover # Coverage reportYYYYMMDDTHHMMSS[==SIGNATURE]--title-with-hyphens[__tag1_tag2].org
- ID = unique timestamp identifier (the key for everything)
- Signature = optional alphanumeric code (
==5a2,==0za), used for Denote signatures (e.g. syntopicon/propaedia ordering) - Frontmatter:
#+title:,#+date:,#+filetags:,#+identifier: - Links:
[[denote:YYYYMMDDTHHMMSS]]
Examples:
20251107T082610--제목-하이픈-구분__tag1_tag2_tag3.org # standard
20250904T075937==5a2--힣-ai-에이전트__agents_ai.org # with signature
20250421T125513==0--†-syntopicon-신토피콘__metameta.org # single-char signature
20251021T105353--simple-title.org # no tags
denotecli search "에릭 호퍼" --tags bib --max 5 # title/tag/ID search
denotecli search-headings "양자역학" --level 1 --max 10 # heading search across all files
denotecli search-content "LSP 설정" --tags emacs # full-text grep with tag filterdenotecli read 20250314T125213 --outline --level 2 # heading structure (TOC)
denotecli read 20250314T125213 --offset 41 --limit 20 # specific section by line rangedenotecli graph 20250314T125213 # outgoing + incoming links
denotecli keyword-map "이맥스" # Korean↔English tag mapping
denotecli keyword-map "emacs" # bidirectionaldenotecli day 2023-02-22 # 특정 날짜 저널/노트/datetree 통합 (생성+수정 노트)
denotecli day --years-ago 3 # N년 전 오늘
denotecli timeline-journal --month 2023-02 # 월간 저널 활동 개요 (count projection)
denotecli agenda # 오늘 1일 journal 본문 + agent stamps
denotecli agenda 2026-09-11 # 해당 1일
denotecli agenda 2026-09-11 --week # 그 날이 속한 월~일 7일
denotecli agenda --from 2026-09-07 --to 2026-09-13 # 양끝 포함 범위denotecli day는 다섯 축을 함께 반환합니다.
| Field | Source | Meaning |
|---|---|---|
journal |
journal/<date>__journal.org 또는 weekly |
그날 저널 시간 엔트리 |
datetree |
*--diary.org reverse datetree |
그날 datetree 엔트리 + CLOCK |
notes_created |
파일명 ID prefix == 그날 | 그날 만든 Denote 노트 |
notes_modified |
본문의 #+hugo_lastmod: == 그날 |
그날 수정된 노트. 생성일은 다른 날 |
years_ago |
system time | 같은 월일 N년 전인 경우 N |
notes_created와 notes_modified는 상보(complementary) 관계로 정의됩니다 — 같은 날 만들고 그날 hugo_lastmod까지 박은 파일은 notes_created에만 들어가고 notes_modified에서는 제외됩니다 (중복 방지). notes_modified는 명시적인 #+hugo_lastmod: 만 신뢰합니다 (mtime/#+date: fallback 없음). 다양한 org 타임스탬프 포맷 — [2025-06-10], [2025-03-29 Sat 02:06], <2024-01-03 Wed 16:54>, 2023-06-19, Time-stamp: <...> — 모두 첫 YYYY-MM-DD 패턴으로 정규화됩니다.
notes_modified 패스는 모든 노트의 frontmatter head(첫 50줄)만 읽기 때문에 ~3,300 노트 코퍼스에서 100ms 수준을 유지합니다.
journal.entries[]의 기존 time/text는 그대로다. 공유 파서가 TODO 헤딩을 읽게 되면서 todo / body / blocks가 추가 필드로만 붙는다 (todo는 "TODO" | "NEXT" | "DONE" | null). 빈 blocks는 []이다.
day.datetree는 legacy *diary*.org 한 source의 {source, entries} 객체이거나, 없으면 null이다. agent stamp를 여기로 밀어 넣지 않는다. 객체→배열 변경도, null→[] 변경도 없다. stamp는 아래 agenda 커맨드의 days[].stamps에만 산다. timeline-journal의 월간 count 계약도 바꾸지 않는다.
주간 본문을 day나 timeline-journal에 덧씌우지 않는다. 상세 어젠다의 public surface는 새 agenda 커맨드다.
범위
| 호출 | 의미 |
|---|---|
| 인자 없음 | 오늘 1일 |
DATE |
해당 1일 |
DATE --week (DATE 생략 가능) |
그 날이 속한 월요일~일요일 7일 |
--from YYYY-MM-DD --to YYYY-MM-DD |
둘을 함께 받으며 양끝 포함(inclusive) |
DATE / --week와 --from/--to를 같이 쓰면 오류다. --from과 --to는 짝이다. days는 빈 날도 포함해 날짜 오름차순이다. 빈 날은 journal: null, stamps: [].
소스
journal: 기존day와 같은 선택 —journal/daily 우선, 없으면 weekly fallback. 여러 journal 파일 충돌을 새로 풀지 않는다.stamps: 각--dirsroot 아래botlog/agenda/*__agenda_*.org를 파일 수 제한 없이 전부. timestamp 다음의from:을 분리하고, 여러 파일 결과는 시각→source 순으로 결정론 정렬한다.__agenda.org(디바이스 접미사 없음)는 이 glob에 안 걸린다.
제외 (이번 범위 밖)
- org-agenda의 schedule/deadline/반복 규칙 재현
- TODO/NEXT/DONE 이외의 임의 Org TODO keyword
- legacy diary와 agent stamp의 병합 또는 중복 제거
- Org 링크 렌더링, Markdown 변환, 본문 요약/절단
timeline-journalcount 계약 변경, agent-config 쪽 바이너리 직접 배포
스키마 (1일 예시)
{
"from": "2026-09-11",
"to": "2026-09-11",
"days": [
{
"date": "2026-09-11",
"day_of_week": "Friday",
"journal": {
"source": "/home/.../journal/20260907T000000--2026-09-07__journal_week36.org",
"format": "weekly",
"entries": [
{
"time": "05:25",
"text": "§entwurf ...",
"todo": "TODO",
"body": "이번브랜치 목표는 ...",
"blocks": [
{"kind": "user", "text": "이번브랜치 목표는 ..."},
{"kind": "assistant", "text": "GLG, GPT-6 ..."}
]
}
]
},
"stamps": [
{
"time": "14:15",
"text": "agent-config: feat(agenda): ...",
"from": "pi@oracle",
"body": "",
"tags": ["pi", "commit", "agentconfig"],
"source": "/home/.../botlog/agenda/...__agenda_oracle.org"
}
]
}
]
}body는 해당 시간 헤딩부터 다음 같은/상위 레벨 헤딩 전까지의 본문이다. planning timestamp와:PROPERTIES:drawer는 제외하고, 하위 헤딩의 내용은 보존한다.[!user]/[!assistant]quote는blocks[]로도 구조화한다.body에는 wrapper와 marker를 뺀 실제 텍스트를 유지하므로 callout 밖 문장도 잃지 않는다. callout이 없으면blocks는[]이다.- 따옴표·역슬래시·개행은 Go JSON encoder가 escape하며 임의 치환하지 않는다. Org/Denote 링크는 원문 유지한다.
journal이 없으면null,stamps가 없으면[]이다.
denotecli create --title "대화 기록" --tags llmlog,emacs --dir ~/org/llmlog
denotecli rename-tag --from llms --to llm --dry-run # batch tag rename (preview)
denotecli rename-tag --from llms --to llm # actual rename (filename + frontmatter)
denotecli tags --top 20 # tag statistics
denotecli tags --suggest # stem-based duplicate detection| Command | Scope | Time |
|---|---|---|
search |
3K files, filenames | ~16ms |
search-headings |
3K files, 60K headings | ~30ms |
search-content |
3K files, 14MB text | ~270ms |
keyword-map |
meta notes | ~24ms |
graph |
3K files, backlink scan | ~85ms |
tags --suggest |
2K+ tags, Porter stemmer | ~23ms |
agenda DATE |
1일 journal + stamps | ~8ms |
agenda DATE --week |
7일 envelope | ~40ms |
git clone https://github.com/junghan0611/denotecli.git
cd denotecli
./run.sh build # → ~/.local/bin/denotecliRequires Go 1.21+. No external dependencies (stdlib only).
All output is JSON. Optional fields are omitted when the note does not carry them
(signature, lastmod, hugo_lastmod, description, abstract).
{
"id": "20250904T075937",
"signature": "5a2",
"title": "힣-ai-에이전트-편재성-기억-연결",
"tags": ["agents", "ai"],
"date": "2025-09-04",
"lastmod": "2026-05-18",
"hugo_lastmod": "[2026-05-18 Mon 09:09]",
"description": "노트 한 줄 요약 (#+description:).",
"path": "/home/.../meta/20250904T075937==5a2--힣-ai-에이전트-편재성-기억-연결__agents_ai.org"
}date 는 만들어진 때(#+date:, 또는 Denote ID에서 유도)이고 수정 시각이 아니다.
수정은 #+hugo_lastmod: 하나뿐이며 두 모양으로 함께 나간다:
| 필드 | 값 | 쓰는 자리 |
|---|---|---|
lastmod |
2026-05-18 |
날짜 비교, day 커맨드와 같은 모양 |
hugo_lastmod |
[2026-05-18 Mon 09:09] |
원본 그대로 — HH:MM이 필요한 비교 |
시각을 남겨 두는 이유가 실측으로 섰다. 날짜만 보고 판정하면 같은 날 21:55에 찍힌 도장보다 이른 18:32 커밋이 "도장 이후 커밋"으로 잘못 세어진다. 호출자가 org 파일을 따로 정규식으로 파지 않도록, 유도 가능한 값은 이쪽에서 두 모양 다 준다.
search / list / day / read 모두 같은 필드를 싣는다 — "어느 노트가 낡았나"는
한 번의 호출로 답해져야 한다.
read (일반·--outline 둘 다)는 첫 헤딩 앞에 놓인 콜아웃 인용 블록을 구조화해서 준다.
#+begin_quote
[!abstract] 이 노트에 대하여
...본문...
#+end_quote
"abstract": { "kind": "abstract", "title": "이 노트에 대하여", "body": "...본문..." }- 콜아웃 표식(
[!xxx])이 없는 평범한 인용은 abstract가 아니다. - 첫 헤딩 아래의 콜아웃은 본문이지 노트의 abstract가 아니므로 잡지 않는다.
kind는abstract외의 표식(note,tip…)도 그대로 싣는다.
./run.sh test # All 135 tests
./run.sh showcase # Visual: all search patterns with input→output
./run.sh cover # Coverage report (logic functions: 85-100%)Showcase tests (go test -v -run TestShowcase) print every search pattern with actual results — makes edge cases visible at a glance.
denotecli/
├── run.sh # Build, test, showcase, cover
├── README.md
├── docs/
│ └── obsidian-cli-comparison.md
└── denotecli/ # Go source (single package, 16 modules + 19 test files)
├── main.go # CLI routing + flag parsing
├── parser.go # Denote filename (with signature) + frontmatter + link parser
├── search.go # Directory scanner + title/tag search
├── search_headings.go # Heading search across all files
├── search_content.go # Full-text content search
├── read.go # Read by ID + outline extraction
├── graph.go # Outgoing/incoming link traversal
├── keyword_map.go # Korean↔English keyword mapping
├── create.go # Note creation with Denote naming
├── rename_tag.go # Batch tag rename (filename + frontmatter)
├── tags.go # Tag statistics
├── tag_suggest.go # Stem-based duplicate detection
├── day.go # Day query (journal + datetree + notes)
├── agenda.go # Agenda range (journal body + agent stamps)
├── timeline_journal.go # Monthly journal timeline
├── stemmer.go # Porter stemmer (embedded, no deps)
└── *_test.go # 135 tests (unit + integration + showcase)
| Project | Description |
|---|---|
| agent-config | AI agent configuration — denotecli SKILL.md managed here |
| zotero-config | Headless Zotero + bibcli (sister CLI for 8K+ bibliography) |
Author: @junghanacs · License: Apache 2.0