clipmem 0.1.0

macOS clipboard memory backed by SQLite and searchable from OpenClaw
Documentation
# clipmem

<p align="center">
  <img src="docs/clipmem-screenshot.png" alt="clipmem screenshot" width="600">
</p>

`clipmem` is a small Rust CLI for macOS that archives observed clipboard states into SQLite, builds a searchable local clipboard history, and gives you a command that OpenClaw can call whenever it needs to recall something you copied.

It captures the current `NSPasteboard` contents, stores every representation it can read for every pasteboard item, deduplicates identical clipboard snapshots by SHA-256 fingerprint, and records each observed state as its own event.

## What it stores

For each observed clipboard state, `clipmem` stores:

- the whole clipboard snapshot
- each pasteboard item inside that snapshot
- every representation type exposed by that item
- raw bytes for every representation
- decoded text when the representation is text-like, plus strict UTF-8/UTF-16 recovery for some byte-only payloads
- a searchable text projection for recognized text-like representations that powers automatic FTS-or-literal lookup
- the frontmost application at capture time as a best-effort source hint

The database schema separates deduplicated `snapshots` from per-observation `capture_events`, so the same content copied ten times does not create ten full blob copies.

The local archive is treated as sensitive state: the tool and installer tighten archive, log, and LaunchAgent file permissions to the current user on platforms that support POSIX modes.

## Current behaviour

- Text, HTML, URLs, file URLs, RTF, JSON and XML are indexed when a reasonable text form is available.
- Images, PDFs and opaque binary types are fully stored as blobs but are not OCR’d.
- Default search is automatic: it uses SQLite FTS5 when that fits the query and falls back to literal substring matching for wildcard-like input, invalid FTS syntax, or zero FTS hits.
- The watcher polls `NSPasteboard.changeCount` on a short interval.
- The watcher is best-effort: if the clipboard changes multiple times between polls, intermediate states can be missed.
- The frontmost app is recorded as a practical hint, not a guaranteed pasteboard owner.

## Project layout

- `src/` – Rust source
- `extras/launchd/` – LaunchAgent template
- `extras/openclaw/clipboard_memory/` – OpenClaw skill stub
- `scripts/install_launchagent.sh` – install and load the watcher as a user LaunchAgent
- `scripts/uninstall_launchagent.sh` – remove the LaunchAgent
- `scripts/install_openclaw_skill.sh` – copy the skill into `~/.openclaw/skills`

## Install

Published installs:

```bash
brew install tristanmanchester/tap/clipmem
```

Or, if you already have Rust installed:

```bash
cargo install clipmem
```

The Homebrew package is intended for Apple Silicon Macs. On Intel Macs, prefer:

```bash
cargo install clipmem
```

Build from source:

```bash
cargo build --release
```

Or install the current checkout into `~/.local/bin`:

```bash
cargo install --path . --root ~/.local --force --locked
```

That gives you:

```bash
~/.local/bin/clipmem
```

## Quick start

Capture the current clipboard once:

```bash
clipmem capture-once
```

Start the watcher in the foreground:

```bash
clipmem watch --interval-ms 350
```

Skip storing the clipboard state that already exists when a watcher starts:

```bash
clipmem watch --interval-ms 350 --skip-initial
```

Search the archive:

```bash
clipmem search "launchctl bootstrap" --limit 5
clipmem search "that shell one-liner with rsync" --json
clipmem search --mode literal "50%"
clipmem search --mode fts "\"launchctl\" AND bootstrap"
```

Show recent unique clipboard states from the last 24 hours:

```bash
clipmem recent --hours 24
```

Inspect one stored snapshot:

```bash
clipmem get 42
clipmem get 42 --json
```

Export a stored raw representation:

```bash
clipmem export 42 --item 0 --uti public.png --out ./clipboard.png
```

Check SQLite / FTS5 diagnostics:

```bash
clipmem doctor
```

## LaunchAgent install

The easiest route is:

```bash
./scripts/install_launchagent.sh
```

By default that will:

- install the Rust binary into `~/.local/bin`
- create `~/Library/Application Support/clipmem`
- write `~/Library/LaunchAgents/io.openclaw.clipmem.watch.plist`
- configure the LaunchAgent to start with `--skip-initial`
- load and kickstart the user LaunchAgent

Useful environment variables for the script:

- `CLIPMEM_INSTALL_ROOT` – defaults to `~/.local`
- `CLIPMEM_DB_PATH` – defaults to `~/Library/Application Support/clipmem/clipmem.sqlite3`
- `CLIPMEM_INTERVAL_MS` – defaults to `350`

To remove the LaunchAgent and plist:

```bash
./scripts/uninstall_launchagent.sh
```

## OpenClaw

Install the bundled skill stub:

```bash
./scripts/install_openclaw_skill.sh
```

That copies:

```text
extras/openclaw/clipboard_memory/SKILL.md
```

into:

```text
~/.openclaw/skills/clipboard_memory
```

The skill tells OpenClaw to use:

- `clipmem search "<query>" --json`
- `clipmem recent --hours 24 --json`
- `clipmem get <snapshot-id> --json`

OpenClaw must be able to run `clipmem` from its own environment, not just from your interactive shell. If you installed `clipmem` into `~/.local/bin`, make sure that directory is on the PATH seen by OpenClaw.

After copying the skill, reload skills or restart OpenClaw if it does not appear immediately.

## Schema notes

The key tables are:

- `snapshots` – deduplicated clipboard states
- `snapshot_items` – items inside a snapshot
- `item_representations` – one row per item/type pair with raw blob storage
- `capture_events` – each time a snapshot was observed
- `snapshots_fts` – FTS5 external-content index over `snapshots.search_text`

## Limitations worth knowing

- Binary payloads are stored exactly, but only recognized text-like payloads are indexed.
- `clipmem get --json` intentionally omits raw blob bytes. Use `clipmem export` to recover stored binary payloads.
- RTF and HTML text extraction is intentionally lightweight and best-effort.
- Search is great for commands, code, URLs, notes, logs and copied prose. Use `--mode auto` for the default FTS-or-literal behavior, `--mode fts` for strict FTS5 queries, or `--mode literal` for exact substring matching. It is not semantic search.
- Explicit `--mode fts` keeps SQLite FTS5 semantics. In automatic mode, punctuation-heavy inputs may fall back to literal search.
- This project is written to be easy to extend: adding export commands, embeddings, OCR, source-app heuristics or richer HTML parsing is straightforward.

## Example OpenClaw prompts once installed

- “Find that ffmpeg command I copied yesterday.”
- “Search my clipboard history for the SQL migration with WAL mode.”
- “What was the URL I copied from Safari about objc2 NSPasteboard?”
- “Show me the full clipboard entry for snapshot 128.”

## Development notes

The code is split so the database, search and tests compile cross-platform, while the actual capture implementation is behind `cfg(target_os = "macos")`.

There are a couple of unit tests for the database layer and text extraction helpers. On a Mac with Rust installed, run:

```bash
cargo test
```

## Release automation

Releases are tag-driven. Pushing a semver tag like `v0.1.0` triggers:

- validation that the git tag matches `Cargo.toml`
- test and package verification
- crates.io publish
- GitHub Release asset generation via `cargo-dist`
- Homebrew tap updates for `tristanmanchester/homebrew-tap`

Repo-side release files:

- `.github/workflows/ci.yml` – push and pull request validation
- `.github/workflows/release.yml` – generated by `cargo-dist` for tag-driven releases
- `.github/workflows/publish-crate.yml` – reusable bootstrap publish job for crates.io
- `dist-workspace.toml``cargo-dist` release configuration

Bootstrap for `0.1.0`:

1. Create the tap repo `tristanmanchester/homebrew-tap`.
2. Add the GitHub Actions secret `HOMEBREW_TAP_TOKEN` with write access to that repo.
3. Add a temporary `CARGO_REGISTRY_TOKEN` secret for the first crates.io publish.
4. Push tag `v0.1.0`.

After `0.1.0` is live on crates.io, migrate the crates publish step from the temporary `CARGO_REGISTRY_TOKEN` secret to crates.io Trusted Publishing with `rust-lang/crates-io-auth-action@v1`, then remove the long-lived secret.