Skip to content
aswathasPublic

About

Clipper — a personal smart clipboard / second-brain for Windows. Capture URLs, screenshots, text via hotkey → AI-extract → Obsidian-shaped vault with a calm library dashboard.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

579 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Clipper

Smart clipboard / watch-later for Windows. A hotkey captures anything, Claude Code turns it into structured notes in your Obsidian vault, and a local dashboard shows it all.

One Tauri binary. No cloud, no account, no telemetry.


Dashboard v3: The Library

The Clipper dashboard has been redesigned from the ground up according to DESIGN.md under the Impeccable design specification (Operate mode). It embodies a calm, editorial, content-forward personal library aesthetic that feels like a quiet reading room:

  • Typography & Theme: Built with Spectral (display serif), Inter (body), and JetBrains Mono (code). Includes "Warm Paper" light mode and "Lamplit Room" dark mode with zero raw hex in components.
  • 8 Hand-Tuned Type Icons: Distinct inline SVG glyphs for articles, videos, books, repositories, code errors, photos, events, and papers.
  • Today Landing Page:
    • Time-aware personal greeting and localized date.
    • This Week metric cards (saved, read, focus time).
    • For You Today ranked cards with 6px top accent bar in the clip type's color and "Why" explanations.
    • Stale & Forgotten triage drawer for neglected items.
    • Pure SVG 90-day activity sparkline (720×60, 90 bars, zero chart libraries).
    • Topics You Lean On horizontal bars & Decay Distribution meters.
    • Contextual right rail with user focus and today's digest.
  • Library Page:
    • Horizontal chip rail filtering by all 8 types and custom tag pills.
    • Grid view (320px cards with decay indicators, reading time, and lift hover) and List view (56px dense rows with decay dots).
  • Inbox Page:
    • 64px triage rows with circular checkboxes, hover actions (Archive, Tag, Obsidian), and keyboard navigation (j/k, a, t, Enter).
    • Floating bulk action bar for multi-selection.
  • Detail Slide-Over:
    • 520px smooth slide-over from the right with backdrop blur.
    • Rendered markdown body with syntax highlighting, pull quotes, and code blocks.
    • Quick action toolbar, related clip recommendations, and expandable inline chat.
  • Ask Chat Page:
    • Left rail with conversation history grouped by date.
    • Essay-style assistant responses with collapsible source citation cards.
    • Streaming SSE output via ReadableStream.getReader() with subtle 1px accent cursor.
    • Sticky composer with auto-grow textarea and streaming abort control.
  • Skills Workbench:
    • Card list of extraction prompts and triggers with usage frequency stats.
    • Split-view slide-over editor: YAML & markdown form on the left, live sanitized preview (DOMPurify) on the right.
    • Interactive URL extraction test runner.
  • Settings Page:
    • 6 dedicated tabs: Soul (persona/voice), Memory (user.md/memory.md), Self (learned patterns), Providers (CLI health & config.toml), Pairing (LAN QR code & TTL countdown), and Devices (paired mobile access with revocation).
    • Sticky save button with dirty-state tracking.
  • Universal States & Accessibility:
    • Full-page and row-level skeleton pulse animations (1.5s ease-in-out).
    • prefers-reduced-motion: reduce overrides for instantaneous transitions.
    • Accessible focus rings (2px solid var(--accent)), skip links, and ARIA live regions.

What's new

v0.4.0 — content + calendar + colors + palette

  • /api/items/{id} now returns the full markdown body (content + body_md), parsed frontmatter (frontmatter map), and raw frontmatter (raw_frontmatter). The slide-over renders the real notes, not a stub.
  • 365-day contribution calendar on Today above the sparkline. Color buckets by save count, hover for date + count, click to filter the day.
  • Tag color map — deterministic 12-color palette (rust / amber / forest / sea / berry / plum / indigo / teal / olive / ochre / crimson / slate) with WCAG AA tokens. Applied to Library, Inbox, slideover, For-You.
  • Cmd+K command palette — fuzzy search across pages, recent items, tags; quick-save URL mode.
  • Inline hover preview on cards (600ms hover → floating panel with title + summary + hero image).
  • All 11 --<n>-smoke gates green, 22 smoke.ps1 checks green.

v0.3.0 — The Library

First Impeccable redesign. Headless Tauri daemon, 6 pages, 12 dashboard commits by Pam, 9 a11y fix commits, 0 raw hex outside token definitions.

Features

  • Clipboard URL capture — press Ctrl+Shift+C in any app. A URL is deduped (SHA-256 of the URL, first 16 hex chars, checked against archive.jsonl) and queued. An already-saved URL reports Already saved: <host> and is skipped.
  • Clipboard image capture — press Ctrl+Shift+X (use Win+Shift+S first). The image is written to ~/.clipper/staging/img_<unix_ts>.png and queued as type: image.
  • Smart classification via the Claude Code CLI — a background drainer batches queued items, builds a prompt from ~/.clipper/prompts/extract.md, and runs claude -p. It gets back one markdown path per item carrying type, title, a one-sentence summary, and up to five tags.
  • Your Obsidian vault is the source of truth — notes land as <vault>/Inbox/YYYY-MM-DD_HHMM_<source>_<id>.md with YAML frontmatter. Clipper auto-detects the vault; if you have none it creates ~/Documents/ClipperVault.
  • Local dashboard — http://127.0.0.1:7137/ with Today / Inbox / Library / Ask / Skills / Settings panes and full-text search, backed by a SQLite + FTS5 index that mirrors the vault.
  • Headless desktop process — the tray daemon has no window and no webview. Tray → Show dashboard opens the dashboard in your default browser.
  • Phone PWA pair over LAN — tray → Show QR, scan it, and the phone gets a no-build web app that posts links to the desktop.
  • Zero Build Step Frontend — Pure Vanilla JS + Alpine.js + Pico.css with standalone mock API fallback.
  • Local only — no cloud, no account, no telemetry. A JSONL queue, one SQLite file, and markdown in your vault.
  • MIT licensed.

Design Reference: See DESIGN.md for the complete design system specification.


Install

Prerequisites

  • Windows 10/11 with the WebView2 runtime. Included on Windows 11; on Windows 10 install it from Microsoft.

  • Rust 1.99+ with MinGW-w64 (GCC 16+) and the x86_64-pc-windows-gnu target. Clipper builds with the GNU toolchain, so no Visual Studio C++ build tools are needed:

    winget install --id Rustlang.Rustup -e
    winget install --id BrechtSanders.WinLibs.POSIX.UCRT -e   # MinGW-w64: gcc, ld, ar
    rustup toolchain install stable-x86_64-pc-windows-gnu
    rustup default stable-x86_64-pc-windows-gnu

    Then point cargo at the MinGW linker — ~/.cargo/config.toml:

    [target.x86_64-pc-windows-gnu]
    linker = "x86_64-w64-mingw32-gcc.exe"
    ar = "ar.exe"

    winget id note: BrechtSanders.WinLibs.POSIX.UCRT is the package that works. The niXman.mingw-w64-gcc id does not exist in the winget repository.

  • Node.js 20+ — only to run the Tauri CLI. Not a runtime dependency of the built app.

  • Claude Code CLI on PATH (claude --version works). This is what performs the extraction; without it the queue will deadletter.

Build

git clone <repo>
cd SmartClipboard
npm install
npx tauri build

Produces target\release\clipper.exe, plus installers in target\release\bundle\: an .msi and an NSIS -setup.exe.

First run

  1. Launch clipper.exe. The tray icon appears; no window opens (by design).
  2. Clipper auto-detects your Obsidian vault, or creates ~/Documents/ClipperVault. ~/.clipper/config.toml is written on this first run and then left alone.
  3. Tray → Show dashboard opens http://127.0.0.1:7137/ in your default browser. There is no app window: the desktop process is headless and the browser is the UI.

Usage

Hotkeys (system-wide — they work while any app is focused)

Hotkey Does
Ctrl+Shift+C queue the URL currently on the clipboard
Ctrl+Shift+X queue the image currently on the clipboard (Win+Shift+S first)

Tray menu

  • Show dashboard — opens the dashboard in your default browser
  • Show QR — opens the phone-pairing page in your browser
  • Quit

Launching Clipper a second time also opens the dashboard — that is what the single-instance guard does now that there is no window to focus.

Phone pair

Tray → Show QR. Scan the code with your phone; the browser opens the Clipper PWA. Type a URL, press Save. It takes exactly the same path as a desktop capture — same queue, same drainer, same vault. The PWA is plain HTML/JS with no build step, and keeps working if the CDN is unreachable (the form falls back to plain DOM).

The QR encodes http://<lan-ip>:<port>/pair?token=<session-token>. The token is 32 random characters and rotates every 24 hours; POST /save rejects anything without it.

Vault layout

<vault>/
└── Inbox/
    └── 2026-10-15_1225_clipboard_<uuid>.md    # one structured note per capture

Each note opens with YAML frontmatter:

---
type: article        # article | video | repo | book | product | note | other | image
source: example.com
url: https://example.com/post
saved_at: 2026-10-15T12:25:00Z
title: How sockets work
summary: One sentence, at most 25 words.
tags: [networking, tcp]
---

Editing a note is safe: the index is rebuilt from the vault, keyed by the id in the filename, and re-indexing never overwrites a status you set in the dashboard.


Config

~/.clipper/config.toml is written on first run and left alone afterwards. Every path accepts a leading ~.

Key Default Meaning
vault.path auto-detected your Obsidian vault
vault.inbox_subdir Inbox subfolder notes are filed into
provider.claude.command / .args claude / ["-p"] the extractor to spawn
provider.claude.stdin prompt-file prompt-file pipes the prompt into the child's stdin
provider.image.backend / .prompt_hint claude-via-mcp / MiniMax hint how images are described
queue.dir ~/.clipper/queue inbox.jsonl, archive.jsonl, deadletter.jsonl
queue.staging_dir ~/.clipper/staging batch prompts, generated notes, screenshots
queue.batch_size 5 items per provider run
queue.poll_interval_ms 5000 drainer poll interval (it also wakes on file change)
queue.timeout_s 120 per-batch provider timeout
server.bind / .port 127.0.0.1 / 7137 dashboard + API
prompts.extract ~/.clipper/prompts/extract.md the extraction prompt
skills.dir ~/.clipper/skills where skill markdown files live
memory.dir ~/.clipper/memory soul.md, user.md, memory.md

Memory files

Three markdown files are read at startup and sent ahead of every Claude prompt, so the persona and your context come before the task:

~/.clipper/memory/
├── soul.md      # persona and tone
├── user.md      # your expertise, style, dislikes
└── memory.md    # active work, recent focus, today's plan

They are created with defaults on first run (seeded from src-tauri/seed/). Edit them and restart Clipper for the change to take effect; a live editor is planned.

Each prompt is assembled as <soul><user><memory>, then a --- fenced copy of extract.md, then the batch instructions.

Setting CLIPPER_HOME replaces ~/.clipper entirely — that is how scripts/smoke.ps1 runs a whole session without touching your real state, and it also makes a portable install possible.


API

Bound to [server] bind/port (127.0.0.1:7137). GET / serves the dashboard.

Route Returns
GET /healthz 200 ok
GET /api/items?limit=50&offset=0&type=&tag=&q= {items, total}; q is an FTS5 phrase match, type/tag are exact
GET /api/items/{id} one item, or 404
POST /api/items/{id}/status {status} {ok:true}; status must be inbox/reading/done/archived/deleted
POST /api/items/{id}/tags {tags:[…]} {ok:true, tags}
POST /api/clips {url, type?} {id, status:"queued"}; appends to inbox.jsonl
GET /api/today {items}, newest 3 with status inbox
GET /api/stats {total, by_type, by_status, stale_30d}
GET /pair the phone PWA page
GET /pair.png QR code for the pairing URL, as image/png
GET /qr the small page shown by the tray's Show QR item
GET /pwa/app.js, GET /pwa/manifest.json PWA assets
POST /save {url} same as /api/clips; requires the pairing token in ?token= or X-Clipper-Token, else 401
GET /api/skills {skills, total} from ~/.clipper/skills/
GET /api/skills/{name} one skill plus html (markdown rendered, embedded raw HTML stripped)
PUT /api/skills/{name} {body_md, source?} writes the file and re-indexes it
DELETE /api/skills/{name} removes the file and its row
POST /api/skills/rescan rebuilds the skill index from the directory
GET /api/memory {parts} — each with bytes, mtime, etag
GET /api/memory/{part} {part, content, mtime, etag}; part is soul/user/memory
PUT /api/memory/{part} {content, if_match?} {ok, mtime, etag}; 412 when if_match is stale
GET /api/soul the soul plus settings parsed from its frontmatter
PUT /api/soul {content, if_match?} same rules as /api/memory/soul
GET /api/events Server-Sent Events stream (text/event-stream)
POST /api/events {kind, payload?} records a user_event
GET /api/self/summary {topics, patterns, recent_events, event_count}
POST /api/self/recompute rebuilds topics + patterns from the index
GET /api/providers the configured providers (read-only in this release)
POST /api/providers/{name}/test runs the provider once with a trivial prompt; reports exit code + first stdout line
GET /api/pairing {lan_ip, port, pair_url, qr_path, token_length} — never the token itself
POST /api/pairing/regenerate rotates the session token and emits token_rotated
POST /api/items/{id}/chat {message} SSE: event: chunk frames, then event: done
GET /api/chat/history?item_id=&limit= {turns, total} — session-scoped

Chat spawns the configured provider once per question with the full prompt template (soul + user + memory, skill context, the clip and its body, then the question) and normalises both plain-text and NDJSON stdout into chunk frames. A bounded channel between the reader thread and the stream is the backpressure: if the client stalls, the reader blocks instead of buffering the model's output without limit.

Provider stdout is parsed tolerantly. The strict shape (exactly one existing staging path per input) is tried first; if the count does not match, Clipper rescans for .md paths under the staging directory and recovers the batch, logging when it does. Only files under staging are ever accepted, so a provider cannot talk Clipper into moving a file from somewhere else.

The pairing token is never returned by the JSON API. It exists only inside the QR image served at /pair.png, so nothing but a scan of that image can read it.

Optimistic concurrency uses If-Match against the part's etag — a SHA-256 content hash, not the mtime. Filesystems that only tick mtime once per second would let two edits in the same tick silently clobber each other; a hash cannot. Send the etag you last read, or omit if_match to force the write.

items.saved_at is Unix seconds as a JSON number — the dashboard multiplies by 1000 before formatting.

Skill names are slugs ([A-Za-z0-9._-], no ..), so a name can never escape the skills directory. Skill markdown is rendered with pulldown-cmark and the embedded raw-HTML events are dropped server-side; the client runs DOMPurify as well.

CORS: only http://127.0.0.1:7137, http://localhost:7137 and the app's own webview origins get CORS headers; everything else is default-deny.

If the port is already in use Clipper logs the address it could not bind and exits non-zero — it never silently picks another port.

Architecture

One Tauri 2 binary: Rust backend, vanilla HTML/JS frontend, no framework and no bundler.

A capture appends a line to a JSONL queue. A background drainer batches those lines, writes a prompt file, runs the Claude Code CLI with a timeout, and files each returned markdown document into the Obsidian vault — which stays the source of truth. A SQLite index with an FTS5 virtual table mirrors the vault for search, kept in step by a file watcher. An axum server on 127.0.0.1:7137 serves the dashboard, the REST API, and the pairing endpoints; the dashboard is embedded in the binary and served from /, so its relative /api/... fetches stay same-origin.

Failures are loud and bounded: a bad provider exit, a timeout, or unparseable stdout moves the batch to deadletter.jsonl with a reason instead of retrying forever, and if the port is taken Clipper reports it and exits rather than silently relocating.

See SPEC.md for the data model and the build slices.


Development

Smoke test

pwsh -File scripts/smoke.ps1        # or: powershell -File scripts\smoke.ps1

Builds the release binary if needed, then runs it fully isolated under a temporary CLIPPER_HOME with a faked provider and asserts the whole pipeline: config + vault auto-detect, /healthz, the items/today API, /pair, /pair.png, /save returning 401 without a token, then queue → drainer → vault → index for three URLs. It ends with SMOKE OK (19 checks) and leaves the temp vault behind for inspection.

Per-slice self-checks

The release binary also carries flag-driven self-checks, one per slice:

clipper.exe --hag3-smoke   # vault, queue, drainer, writer
clipper.exe --hag4-smoke   # SQLite, FTS5, every REST route over real HTTP
clipper.exe --hag6-smoke   # image capture through the drainer
clipper.exe --hag7-smoke   # pairing: /pair, /pair.png, token-gated /save
clipper.exe --hag10-smoke  # soul/user/memory seeding + injection into the Claude prompt
clipper.exe --integration-smoke  # cross-cutting API contract gates (also tests/integration_api.rs)

These live behind flags rather than in tests/ because a Rust test-harness binary linking the Tauri stack cannot be loaded on the windows-gnu toolchain (STATUS_ENTRYPOINT_NOT_FOUND), while clipper.exe itself loads fine. This way the checks exercise the code that actually ships.


Troubleshooting

Symptom Fix
claude: command not found Install the Claude Code CLI and make sure it is on PATH.
Clipper: cannot bind 127.0.0.1:7137 … Something else owns 7137. Clipper exits rather than quietly moving to another port — kill the other process, or change [server] port.
no .obsidian folder found Expected if you have no vault yet: Clipper creates ~/Documents/ClipperVault. Move it wherever you like and update vault.path.
warning: .rsrc merge failure: multiple non-default manifests during the build Known and harmless — MinGW ld and tauri-winres both supply a manifest. Linking succeeds.
A hotkey does nothing Another app may already own Ctrl+Shift+C / Ctrl+Shift+X. Both are constants at the top of src-tauri/src/lib.rs.
Notes never appear in the vault Check ~/.clipper/queue/deadletter.jsonl — a note there carries the reason (bad-stdout, timeout, exit-<code>).
The dashboard shows sample clips that are not yours (e.g. "Tauri 2.0 Architecture…") That is the dashboard's built-in offline fallback: it could not reach the API, so it rendered mock fixtures. Make sure Clipper is running, and open the dashboard from the tray.

License

MIT — see LICENSE.

About

Clipper — a personal smart clipboard / second-brain for Windows. Capture URLs, screenshots, text via hotkey → AI-extract → Obsidian-shaped vault with a calm library dashboard.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages