Build a near-real-time indexing pipeline from Apple Notes into QMD, then expose retrieval to OpenClaw.
- No manual export workflow
- Incremental sync (changed notes only)
- Practical real-time UX (target: 5–15s indexing latency)
- macOS can read Apple Notes via
osascript - host machine has
qmdavailable in PATH - host API endpoint is reachable from macOS (LAN/Tailscale/public)
INGEST_TOKEN/SEARCH_TOKENare configured before startup
- Mac Watcher reads Apple Notes changes continuously
- Ingestion API receives normalized note batches
- QMD Indexer upserts markdown mirror into a QMD collection
- OpenClaw Tool (
notes_search) queries QMD and returns ranked snippets
See docs/ARCHITECTURE.md for details.
docs/ARCHITECTURE.md— components, data flow, runtime modeldocs/DESIGN_GRAPH.md— Mermaid architecture/sequence/entity graphsdocs/graphs/*.svg— rendered SVG diagramsdocs/DATA_MODEL.md— canonical schema + checkpoint schemadocs/INGEST_API.md— ingestion/retrieval API contractdocs/IMPLEMENTATION_PLAN.md— phased plan and milestonesdocs/SECURITY_PRIVACY.md— threat model + controlsdocs/OPERATIONS.md— runbook, health checks, troubleshootingdocs/INSTALL_FOR_OPENCLAW_USERS.md— copy-paste installer guide for other OpenClaw users
src/notes_sync/
api.py # FastAPI ingest/search bridge
server.py # uvicorn launcher
qmd_index.py # qmd collection/index/search adapter
watcher.py # macOS poller + delta sync loop
apple_notes.py # osascript/JXA extractor
state_db.py # watcher checkpoint sqlite
diff.py # changed/deleted detection
models.py # shared request/response schemas
utils.py # hash + filename helpers
cd <project-dir>
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'export INGEST_TOKEN='change-me'
export SEARCH_TOKEN='change-me-search'
export NOTES_DATA_DIR='./data/notes'
export QMD_COLLECTION='apple_notes'
./scripts/run_api.shAPI defaults to http://0.0.0.0:8787.
cd <project-dir>
source .venv/bin/activate
python3 -m notes_sync.watcher \
--api-url 'http://<linux-host>:8787' \
--token 'change-me' \
--oncecd <project-dir>
./scripts/setup_mac_watcher.sh \
--api-url 'http://<linux-host>:8787' \
--token 'change-me' \
--interval 10The installer will:
- pick Python >= 3.10
- create/update
.venv - install project dependencies
- install + start LaunchAgent
./scripts/watcherctl.sh status
./scripts/watcherctl.sh logs
./scripts/watcherctl.sh restart
./scripts/watcherctl.sh uninstallcurl -sS -X POST 'http://127.0.0.1:8787/search/apple-notes' \
-H 'Authorization: Bearer <SEARCH_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"query":"qmd indexing plan","top_k":5,"mode":"search"}'curl -sS -X POST 'http://127.0.0.1:8787/tool/notes_search' \
-H 'Authorization: Bearer change-me-search' \
-H 'Content-Type: application/json' \
-d '{"query":"这周会议纪要","top_k":5,"mode":"search"}'./scripts/notes_search.sh --query '这周会议纪要' --top-k 5 --prettyHelper behavior:
- auto-detects token from
NOTES_SEARCH_TOKEN/SEARCH_TOKEN - if env token is missing, tries reading token from running
notes_sync.serverprocess env - tries
/tool/notes_searchfirst, then falls back to/search/apple-notes
mode="search"is default and fast.mode="query"may trigger heavier model downloads in QMD.
pytest -qThis repository also includes the reusable agent skill package:
- source:
skill/SKILL.md+skill/references/commands.md - packaged:
skill/dist/apple-notes-sync.skill
- Apple Notes has no official webhook API; this project uses near-realtime polling (default 10s).
- This implementation mirrors notes into local markdown files on host side, then indexes with QMD.
- Delete events are handled via tombstones and mirrored file deletion.
Yes — this repo is designed to be reusable by other OpenClaw users in a self-hosted setup:
- each user runs their own Mac watcher locally
- watcher pushes to their own OpenClaw host ingest API
- tokens are per-user (
INGEST_TOKEN,SEARCH_TOKEN)
For production sharing, provide this repo + the one-command mac installer:
./scripts/setup_mac_watcher.sh --api-url 'http://<host>:8787' --token '<INGEST_TOKEN>'Expose this helper behind an MCP/custom tool so notes_search appears as a first-class callable tool in your OpenClaw runtime.