From db551bea9aff0c136b74e2807b1ebf8f90b6063b Mon Sep 17 00:00:00 2001 From: Agni Date: Mon, 23 Mar 2026 03:59:19 +0530 Subject: [PATCH 1/2] docs: rewrite README with badges, TOC, and comprehensive sections Also fix module path from github.com/agni/apple-notes-sync to github.com/PyAgni/apple-notes-syncer to match the actual GitHub repo. Co-Authored-By: Claude Opus 4.6 --- README.md | 183 +++++++++++++++++++++---- cmd/apple-notes-sync/main.go | 18 +-- go.mod | 2 +- internal/applescript/extractor.go | 4 +- internal/applescript/extractor_test.go | 4 +- internal/applescript/parser.go | 2 +- internal/filesystem/writer.go | 2 +- internal/filesystem/writer_test.go | 2 +- internal/gitops/git.go | 2 +- internal/gitops/git_test.go | 2 +- internal/rclone/sync.go | 2 +- internal/rclone/sync_test.go | 2 +- internal/syncer/syncer.go | 14 +- internal/syncer/syncer_test.go | 4 +- 14 files changed, 183 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index f11e566..11da775 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,121 @@ # apple-notes-sync -A macOS CLI tool that exports Apple Notes to a Git repository as Markdown files, with optional Google Drive sync via rclone. +[![Go](https://img.shields.io/badge/Go-1.26+-00ADD8?logo=go&logoColor=white)](https://go.dev) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Platform: macOS](https://img.shields.io/badge/platform-macOS-brightgreen)](#prerequisites) +[![CI](https://github.com/PyAgni/apple-notes-syncer/actions/workflows/ci.yml/badge.svg)](https://github.com/PyAgni/apple-notes-syncer/actions) + +**Seamless, automatic Git backup of your Apple Notes — with optional Google Drive sync.** +One command (or hourly launchd) turns your Notes app into a version-controlled Markdown repo. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Features](#features) +- [Why apple-notes-sync?](#why-apple-notes-sync) +- [In Action](#in-action) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Configuration](#configuration) +- [Running Manually](#running-manually) +- [Scheduling with launchd](#scheduling-with-launchd) +- [Google Drive Setup](#google-drive-setup) +- [How Renames and Deletions Are Handled](#how-renames-and-deletions-are-handled) +- [Limitations](#limitations) +- [Troubleshooting](#troubleshooting) +- [Alternatives](#alternatives) +- [Contributing](#contributing) +- [License](#license) + +## Quick Start + +```bash +# 1. Install +go install github.com/PyAgni/apple-notes-syncer/cmd/apple-notes-sync@latest + +# 2. Create a Git repo for your notes +mkdir ~/Notes && cd ~/Notes +git init +git remote add origin git@github.com:yourusername/my-notes.git +git commit --allow-empty -m "init" +git push -u origin main + +# 3. Configure +cp configs/config.example.yaml ~/.apple-notes-sync.yaml +# Edit repo_path in the config file + +# 4. Run +apple-notes-sync --repo-path ~/Notes + +# 5. (Optional) Schedule hourly syncs +make launchd +``` + +Done. Your notes now live in Git with full history. ## Features - Extracts all notes from the macOS Notes app via AppleScript - Converts HTML note bodies to clean Markdown - Mirrors the Notes folder hierarchy into the repository -- Adds YAML front matter (title, dates, account) to each note +- Adds a metadata table (ID, dates, account) to each note - Commits with timestamped messages and pushes to your remote - Optionally syncs to Google Drive via rclone - Cleans up notes that were deleted from Apple Notes - Configurable via CLI flags, environment variables, or YAML config file +## Why apple-notes-sync? + +Apple Notes is great on your Mac and iPhone, but terrible for backups, version history, or migrating to Obsidian/Logseq. + +Other exporters are one-shot scripts. This tool gives you **continuous sync**: + +- **Automatic Git commits + push** — full version history of every edit +- **Orphan cleanup** — deleted notes disappear from the repo automatically +- **Hourly launchd scheduling** — set it and forget it +- **rclone integration** — Google Drive as a bonus backup layer +- **Folder mirroring** — your Notes folder structure is preserved exactly + +> **Note**: This is a one-way export. Edits made to the Markdown files do not flow back to Apple Notes. + +## In Action + + + + +Example output for a single note: + +```markdown +# My Project Ideas + +Content of the note converted to clean Markdown... + +- Bullet points preserved +- [Links](https://example.com) converted properly +- **Bold** and *italic* formatting kept + +--- + +| ID | Created | Modified | Account | Shared | +|----|---------|----------|---------|--------| +| x-coredata://abc123 | 2026-03-18 16:00:00 | 2026-03-20 09:30:00 | iCloud | No | +``` + +Repository structure mirrors your Notes folders: + +``` +~/Notes/ +├── Work/ +│ ├── Meeting Notes.md +│ └── Project Ideas.md +├── Personal/ +│ ├── Travel Plans.md +│ └── Reading List.md +├── Recipes/ +│ └── Pasta Carbonara.md +└── ... +``` + ## Prerequisites - **macOS** (required — uses AppleScript to access Notes) @@ -33,20 +136,7 @@ make install ### Using `go install` ```bash -go install github.com/agni/apple-notes-sync/cmd/apple-notes-sync@latest -``` - -## One-time repo setup - -Create and initialize a Git repository for your notes: - -```bash -mkdir ~/Notes -cd ~/Notes -git init -git remote add origin git@github.com:yourusername/my-notes.git -git commit --allow-empty -m "init" -git push -u origin main +go install github.com/PyAgni/apple-notes-syncer/cmd/apple-notes-sync@latest ``` ## Configuration @@ -58,6 +148,18 @@ Configuration is loaded from (in order of precedence): 3. YAML config file (`~/.apple-notes-sync.yaml`) 4. Defaults +Minimal config to get started: + +```yaml +repo_path: ~/Notes +``` + +See [`configs/config.example.yaml`](configs/config.example.yaml) for all options. Copy it: + +```bash +cp configs/config.example.yaml ~/.apple-notes-sync.yaml +``` + ### CLI flags ``` @@ -82,15 +184,6 @@ export ANS_RCLONE_REMOTE_NAME=gdrive export ANS_RCLONE_REMOTE_PATH=AppleNotes ``` -### YAML config file - -See [`configs/config.example.yaml`](configs/config.example.yaml) for a complete reference. Copy it to get started: - -```bash -cp configs/config.example.yaml ~/.apple-notes-sync.yaml -# Edit with your settings -``` -
Full config reference @@ -119,7 +212,7 @@ cp configs/config.example.yaml ~/.apple-notes-sync.yaml | `attachments.max_size_mb` | int | `50` | Max attachment size in MB | | `attachments.dir` | string | `"_attachments"` | Attachment subdirectory | | `dry_run` | bool | `false` | Preview mode | -| `front_matter` | bool | `true` | Add YAML front matter | +| `front_matter` | bool | `true` | Add metadata table to notes | | `clean_orphans` | bool | `true` | Remove deleted notes | | `timeout` | duration | `120s` | AppleScript timeout | | `commit_template` | string | see below | Commit message Go template | @@ -130,7 +223,7 @@ Template fields: `.Timestamp`, `.Written`, `.Total`, `.Skipped`
-## Running manually +## Running Manually ```bash # Basic run @@ -169,7 +262,7 @@ Create the log directory: mkdir -p ~/Library/Logs/apple-notes-sync ``` -## Google Drive setup +## Google Drive Setup 1. Install rclone: `brew install rclone` @@ -198,14 +291,43 @@ rclone: rclone sync ~/Notes gdrive:AppleNotes --dry-run ``` -## How renames and deletions are handled +## How Renames and Deletions Are Handled - **Renamed notes**: A renamed note appears as a new file and the old filename is removed (if `clean_orphans: true`). This shows as a delete + add in git, which GitHub renders as a rename if content is similar. - **Deleted notes**: When a note is deleted from Apple Notes, the corresponding `.md` file is removed on the next sync (if `clean_orphans: true`). - **Moved notes**: Moving a note to a different folder creates the file in the new directory and removes it from the old one. +## Limitations + +- **macOS only** — relies on AppleScript to access the Notes app +- **One-way export** — edits to Markdown files do not sync back to Apple Notes +- **AppleScript permissions required** — on first run, macOS will prompt to allow automation access +- **Attachments >50 MB skipped** by default (configurable via `attachments.max_size_mb`) +- **Large note libraries** may take a few minutes on the first sync (AppleScript extraction is the bottleneck) + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| AppleScript timeout | Increase `timeout: 300s` in your config | +| Permission denied on Notes | System Settings > Privacy & Security > Automation > allow Terminal (or your terminal app) | +| rclone OAuth expired | Run `rclone config` again to re-authenticate | +| Unicode errors in dates | Already handled — the parser normalizes Unicode whitespace from macOS locales | +| "repo_path is required" | Set `repo_path` in your config file or pass `--repo-path` | + +## Alternatives + +| Tool | Git auto-commit | Scheduled | rclone | Bidirectional | Language | +|------|-----------------|-----------|--------|---------------|----------| +| **apple-notes-sync** | Yes | Yes (launchd) | Yes | No | Go | +| [notes2md](https://github.com/vacekj/notes2md) | No | No | No | No | Rust | +| [apple-cloud-notes-parser](https://github.com/threeplanetssoftware/apple_cloud_notes_parser) | No | No | No | No | Ruby | +| [Bear/Stash](https://github.com/andymatuschak/Bear-Markdown-Export) | No | No | No | No | Swift | + ## Contributing +Contributions are welcome! Please open an issue or submit a pull request. + 1. Fork the repository 2. Create a feature branch: `git checkout -b my-feature` 3. Make your changes @@ -217,10 +339,11 @@ rclone sync ~/Notes gdrive:AppleNotes --dry-run ```bash make build # Build the binary make test # Run tests with race detector -make check-coverage # Run tests and check coverage ≥80% +make check-coverage # Run tests and check coverage >= 80% make lint # Run go vet + staticcheck make fmt # Format code make tidy # Tidy go modules +make help # Show all available targets ``` ## License diff --git a/cmd/apple-notes-sync/main.go b/cmd/apple-notes-sync/main.go index 75b7982..fd4e51d 100644 --- a/cmd/apple-notes-sync/main.go +++ b/cmd/apple-notes-sync/main.go @@ -13,15 +13,15 @@ import ( "github.com/spf13/cobra" "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/applescript" - "github.com/agni/apple-notes-sync/internal/config" - "github.com/agni/apple-notes-sync/internal/converter" - "github.com/agni/apple-notes-sync/internal/filesystem" - "github.com/agni/apple-notes-sync/internal/gitops" - "github.com/agni/apple-notes-sync/internal/logging" - "github.com/agni/apple-notes-sync/internal/rclone" - "github.com/agni/apple-notes-sync/internal/shell" - "github.com/agni/apple-notes-sync/internal/syncer" + "github.com/PyAgni/apple-notes-syncer/internal/applescript" + "github.com/PyAgni/apple-notes-syncer/internal/config" + "github.com/PyAgni/apple-notes-syncer/internal/converter" + "github.com/PyAgni/apple-notes-syncer/internal/filesystem" + "github.com/PyAgni/apple-notes-syncer/internal/gitops" + "github.com/PyAgni/apple-notes-syncer/internal/logging" + "github.com/PyAgni/apple-notes-syncer/internal/rclone" + "github.com/PyAgni/apple-notes-syncer/internal/shell" + "github.com/PyAgni/apple-notes-syncer/internal/syncer" ) // Build-time variables set via -ldflags. diff --git a/go.mod b/go.mod index af909f7..8a0a544 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/agni/apple-notes-sync +module github.com/PyAgni/apple-notes-syncer go 1.26.1 diff --git a/internal/applescript/extractor.go b/internal/applescript/extractor.go index 779dd93..594e827 100644 --- a/internal/applescript/extractor.go +++ b/internal/applescript/extractor.go @@ -7,8 +7,8 @@ import ( "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/model" - "github.com/agni/apple-notes-sync/internal/shell" + "github.com/PyAgni/apple-notes-syncer/internal/model" + "github.com/PyAgni/apple-notes-syncer/internal/shell" ) //go:embed scripts diff --git a/internal/applescript/extractor_test.go b/internal/applescript/extractor_test.go index 49cceb3..7b0f821 100644 --- a/internal/applescript/extractor_test.go +++ b/internal/applescript/extractor_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/model" - "github.com/agni/apple-notes-sync/internal/shell" + "github.com/PyAgni/apple-notes-syncer/internal/model" + "github.com/PyAgni/apple-notes-syncer/internal/shell" ) // MockCommandExecutor is a testify mock for shell.CommandExecutor. diff --git a/internal/applescript/parser.go b/internal/applescript/parser.go index ea34483..5e0d02f 100644 --- a/internal/applescript/parser.go +++ b/internal/applescript/parser.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "github.com/agni/apple-notes-sync/internal/model" + "github.com/PyAgni/apple-notes-syncer/internal/model" ) const ( diff --git a/internal/filesystem/writer.go b/internal/filesystem/writer.go index e45df1e..aabff75 100644 --- a/internal/filesystem/writer.go +++ b/internal/filesystem/writer.go @@ -13,7 +13,7 @@ import ( "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/model" + "github.com/PyAgni/apple-notes-syncer/internal/model" ) // NoteWriter manages writing notes to the filesystem. diff --git a/internal/filesystem/writer_test.go b/internal/filesystem/writer_test.go index aee1951..2b589d9 100644 --- a/internal/filesystem/writer_test.go +++ b/internal/filesystem/writer_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/model" + "github.com/PyAgni/apple-notes-syncer/internal/model" ) func newTestWriter(t *testing.T, subdir string, frontMatter bool) (*FSNoteWriter, string) { diff --git a/internal/gitops/git.go b/internal/gitops/git.go index 7227568..83efe9a 100644 --- a/internal/gitops/git.go +++ b/internal/gitops/git.go @@ -9,7 +9,7 @@ import ( "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/shell" + "github.com/PyAgni/apple-notes-syncer/internal/shell" ) // GitClient performs git operations on the notes repository. diff --git a/internal/gitops/git_test.go b/internal/gitops/git_test.go index cbe427a..aa4c16f 100644 --- a/internal/gitops/git_test.go +++ b/internal/gitops/git_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/shell" + "github.com/PyAgni/apple-notes-syncer/internal/shell" ) // MockCommandExecutor is a testify mock for shell.CommandExecutor. diff --git a/internal/rclone/sync.go b/internal/rclone/sync.go index bfc460b..b57c0b3 100644 --- a/internal/rclone/sync.go +++ b/internal/rclone/sync.go @@ -9,7 +9,7 @@ import ( "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/shell" + "github.com/PyAgni/apple-notes-syncer/internal/shell" ) // Syncer syncs a local directory to a cloud remote via rclone. diff --git a/internal/rclone/sync_test.go b/internal/rclone/sync_test.go index 4a356ae..d60939c 100644 --- a/internal/rclone/sync_test.go +++ b/internal/rclone/sync_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/shell" + "github.com/PyAgni/apple-notes-syncer/internal/shell" ) // MockCommandExecutor is a testify mock for shell.CommandExecutor. diff --git a/internal/syncer/syncer.go b/internal/syncer/syncer.go index 00d4fae..4edf0c2 100644 --- a/internal/syncer/syncer.go +++ b/internal/syncer/syncer.go @@ -11,13 +11,13 @@ import ( "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/applescript" - "github.com/agni/apple-notes-sync/internal/config" - "github.com/agni/apple-notes-sync/internal/converter" - "github.com/agni/apple-notes-sync/internal/filesystem" - "github.com/agni/apple-notes-sync/internal/gitops" - "github.com/agni/apple-notes-sync/internal/model" - "github.com/agni/apple-notes-sync/internal/rclone" + "github.com/PyAgni/apple-notes-syncer/internal/applescript" + "github.com/PyAgni/apple-notes-syncer/internal/config" + "github.com/PyAgni/apple-notes-syncer/internal/converter" + "github.com/PyAgni/apple-notes-syncer/internal/filesystem" + "github.com/PyAgni/apple-notes-syncer/internal/gitops" + "github.com/PyAgni/apple-notes-syncer/internal/model" + "github.com/PyAgni/apple-notes-syncer/internal/rclone" ) // Syncer orchestrates the full Apple Notes sync pipeline. diff --git a/internal/syncer/syncer_test.go b/internal/syncer/syncer_test.go index dec437d..126da2e 100644 --- a/internal/syncer/syncer_test.go +++ b/internal/syncer/syncer_test.go @@ -10,8 +10,8 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" - "github.com/agni/apple-notes-sync/internal/config" - "github.com/agni/apple-notes-sync/internal/model" + "github.com/PyAgni/apple-notes-syncer/internal/config" + "github.com/PyAgni/apple-notes-syncer/internal/model" ) // --- Mock implementations --- From db7ed09bffcaf7c9f6b45115480f0975e01281be Mon Sep 17 00:00:00 2001 From: Agni Date: Mon, 23 Mar 2026 04:09:46 +0530 Subject: [PATCH 2/2] update README --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 11da775..98ac215 100644 --- a/README.md +++ b/README.md @@ -315,15 +315,6 @@ rclone sync ~/Notes gdrive:AppleNotes --dry-run | Unicode errors in dates | Already handled — the parser normalizes Unicode whitespace from macOS locales | | "repo_path is required" | Set `repo_path` in your config file or pass `--repo-path` | -## Alternatives - -| Tool | Git auto-commit | Scheduled | rclone | Bidirectional | Language | -|------|-----------------|-----------|--------|---------------|----------| -| **apple-notes-sync** | Yes | Yes (launchd) | Yes | No | Go | -| [notes2md](https://github.com/vacekj/notes2md) | No | No | No | No | Rust | -| [apple-cloud-notes-parser](https://github.com/threeplanetssoftware/apple_cloud_notes_parser) | No | No | No | No | Ruby | -| [Bear/Stash](https://github.com/andymatuschak/Bear-Markdown-Export) | No | No | No | No | Swift | - ## Contributing Contributions are welcome! Please open an issue or submit a pull request.