文件预览

SKILL.md

查看 Sync Notes 技能包中的文件内容。

文件内容

SKILL.md

---
name: sync-notes
description: Sync the local Obsidian vault with a Cloudflare R2 bucket using rclone (S3 + crypt). Triggered by `/sync-notes`. Defaults to bidirectional sync; understands natural-language flags for partial download/upload by glob.
version: 1.0.1
metadata:
  {"openclaw":{"emoji":"☁️","requires":{"bins":["rclone","bash"],"env":["CLOUD_NOTES_PATH"]}}}
---

# sync-notes Skill

Sync the local notes directory (`$CLOUD_NOTES_PATH`) with a **Cloudflare R2** bucket through an
end-to-end encrypted **rclone crypt** remote. The skill is a thin orchestrator over a single bash
entry point at `scripts/sync.sh`; the LLM's job is to translate the user's natural-language intent
into the right subcommand.

## Trigger

Activate **only** when the user message starts with `/sync-notes` (with or without trailing
arguments). Examples:

| User message | Intent | Action |
|---|---|---|
| `/sync-notes` | full bidirectional sync | `sync.sh bisync` |
| `/sync-notes setup` | run the configuration wizard | `sync.sh setup` |
| `/sync-notes init` | establish the bisync baseline (first run only) | `sync.sh init` |
| `/sync-notes status` | show config + last-sync info | `sync.sh status` |
| `/sync-notes 下载 welcome` | download files matching `welcome` | `sync.sh download '**welcome**'` |
| `/sync-notes upload daily/*.md` | upload glob | `sync.sh upload 'daily/*.md'` |
| `/sync-notes 把 ideas 目录拉下来` | download a directory | `sync.sh download 'ideas/**'` |

## How to interpret natural language

After stripping the `/sync-notes` prefix, classify the remainder:

1. **Empty** → run `bisync` (default bidirectional sync).
2. Contains a recognised keyword:
   - `setup`, `配置`, `wizard` → `setup`
   - `init`, `初始化`, `baseline` → `init`
   - `status`, `状态`, `信息` → `status`
3. Mentions **download** intent (`下载`, `拉`, `pull`, `download`, `get`, `取回`):
   - Extract the target token(s) and convert to a glob (see "Glob rules" below).
   - Run `sync.sh download '<glob>'`.
4. Mentions **upload** intent (`上传`, `推`, `push`, `upload`, `send`):
   - Same as above with `sync.sh upload '<glob>'`.
5. Otherwise: ask the user to clarify; do not guess.

### Glob rules

Globs are passed verbatim to rclone as `--include` filters and apply to *plain* (decrypted) paths.

- A bare keyword such as `welcome` becomes `**welcome**` (matches anywhere in path).
- A directory name like `ideas` becomes `ideas/**`.
- Paths already containing wildcards (`*`, `?`, `[`) are passed through unchanged.
- Filenames without extension default to matching `<name>` and `<name>.md`; combine with `**` if
  unsure. When in doubt, run `sync.sh download '<glob>' --dry-run` first.

## Workflow contract

1. **First run ever:** the script refuses `bisync` until a baseline exists. Tell the user to run
   `/sync-notes init` after confirming both sides are in the desired starting state.
2. **Backups:** before any write-side operation (`bisync`, `download`, `init`), the local notes
   directory is mirrored to `backups/latest/` (single rolling copy, previous one is replaced).
3. **Conflicts:** bisync runs with conflict markers. After every run the script scans for
   `*.conflict*` files; if any are found the script exits non-zero and lists them. **Surface this
   list to the user verbatim and ask which side to keep**—do not auto-resolve.
4. **Logs:** every invocation appends to `logs/sync.log` and emits a per-run file
   `logs/<UTC-timestamp>.log` for the rclone output.

## Filesystem layout

```
skills/sync-notes/
├── SKILL.md                  # this file
├── README.md                 # short user-facing notes (optional)
├── config/
│   ├── rclone.conf           # active rclone config (created by setup, gitignored)
│   ├── rclone.conf.example   # template
│   ├── .env                  # non-rclone settings (created by setup, gitignored)
│   └── .env.example          # template
├── scripts/
│   ├── sync.sh               # main entry point (dispatcher)
│   └── setup.sh              # interactive configuration wizard
├── state/
│   └── bisync/               # rclone bisync workdir (listings + lockfiles)
├── logs/                     # rolling logs
├── backups/
│   └── latest/               # most recent local snapshot (single copy)
└── .gitignore
```

`scripts/sync.sh` derives every path from its own location (`$(dirname "$0")/..`); no absolute
paths are hard-coded.

## Required environment

| Variable | Purpose | Required |
|---|---|---|
| `CLOUD_NOTES_PATH` | absolute path of the local notes directory | ✅ |

The script aborts early with a clear message if `CLOUD_NOTES_PATH` is unset, missing, or not a
directory.

## Configuration files

### `config/rclone.conf`

Native rclone format with **two remotes**:

```ini
[r2-raw]
type = s3
provider = Cloudflare
access_key_id = <ACCESS_KEY_ID>
secret_access_key = <SECRET_ACCESS_KEY>
endpoint = https://<ACCOUNT_ID>.r2.cloudflarestorage.com
acl = private

[notes]
type = crypt
remote = r2-raw:<BUCKET>/<OPTIONAL_PREFIX>
filename_encryption = standard
directory_name_encryption = true
password = <OBSCURED>
password2 = <OBSCURED>
```

### `config/.env`

Non-rclone settings consumed by `sync.sh`:

```sh
# Name of the crypt remote in rclone.conf
RCLONE_REMOTE=notes
# Number of local backup snapshots to retain
BACKUP_KEEP=1
# Extra rclone flags (optional)
RCLONE_EXTRA_FLAGS="--transfers=4 --checkers=8"
```

`.example` versions ship with the skill; the wizard fills in the real values.

## Subcommands implemented by `sync.sh`

| Subcommand | Behaviour |
|---|---|
| `setup` | Run `setup.sh`; collects R2 creds + crypt password(s); writes `rclone.conf` and `.env` with `chmod 600`. Idempotent: confirms before overwriting. |
| `init` | First-time baseline: `rclone bisync --resync`. Refuses to run if a baseline already exists unless `--force` is passed. |
| `bisync` (default) | `rclone bisync $LOCAL notes:` with conflict reporting. Backs up first. |
| `download <glob>` | `rclone copy notes: $LOCAL --include '<glob>'`. Backs up first. Defaults to `--dry-run` only when the user said "试" / "dry"; otherwise runs for real. |
| `upload <glob>` | `rclone copy $LOCAL notes: --include '<glob>'`. No local backup needed; remote is authoritative. |
| `status` | Prints config validity, last-sync timestamp, conflict files (if any), backup state. |

## Safety rules for the LLM

- **Never** write secrets into chat. If the user pastes secrets, immediately move them into
  `config/rclone.conf` / `config/.env` and remove from any visible context.
- **Never** invoke `rclone` with `--config` pointing somewhere outside this skill.
- **Never** run `bisync` if `status` reports unresolved conflicts; ask first.
- When `download`/`upload` glob match count is ambiguous, run with `--dry-run` first and show the
  list of matches.
- Backups are single-copy; if you are about to do something risky (large delete, schema change),
  copy `backups/latest/` to `backups/manual-<UTC>/` manually before proceeding and tell the user.

## First-time onboarding (what to tell the user)

```
1. /sync-notes setup           # fill in R2 + crypt creds
2. export CLOUD_NOTES_PATH=...  # point at the local notes dir
3. /sync-notes init             # establish the bisync baseline
4. /sync-notes                  # routine bidirectional sync
```