Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dara

A personal spaced-repetition app for macOS. Built to replace Anki after 6–7 years of use — keeping what works (FSRS scheduling, atomic cards) and fixing what doesn't (slow, bloated, and enough friction in card creation that cards don't get made).

Principles

  • Fast and resident. Lives in the menu bar (Docker-style), launches instantly, costs ~nothing at idle. Closing the window leaves Dara running behind its menu-bar icon rather than quitting. Dara does keep a Dock icon while it runs: macOS grants the menu bar only to a Regular application, and only at the moment that application is activated, so a Dock-less menu-bar process can never own the menu bar it is activated from.
  • Capture in five seconds. A global hotkey shows an ephemeral Quick Add window over the current workspace and puts the caret in the editor. Save or cancel and keyboard control returns immediately to the app and window you were using.
  • Keyboard-first everywhere. Reviews, editing, search, occlusion editing — time off the keyboard is the enemy.
  • Local-first. One relational SQLite database plus one blob-only media database. No server, sync, or required account. Capture, review, and lexical search work immediately offline; semantic search runs locally after a background model download. Optional R2 backup uses the network when enabled. Automatic update checks ask GitHub for the latest release shortly after launch and every six hours by default, and can be disabled in Settings without removing manual update checks.
  • AI stays advisory. If the deferred mistake-explainer is added later, it may explain why you got a card wrong. It will never write cards or touch scheduling.
  • No feature bloat. No decks (one interleaved pool). No tags (search is the organization). Three card types, nothing else.

What v1 is

  • FSRS scheduling (same modern algorithm Anki uses), desired-retention as the single user-facing knob
  • Card types: markdown front/back, cloze deletion, image occlusion (N masks → N sibling cards)
  • Ephemeral Quick Add window (global hotkey, restores the prior app/window on dismiss) + full app window for reviewing/editing/searching
  • Hybrid search on Enter: lexical FTS5 + local text embeddings fused with Reciprocal Rank Fusion
  • OCR on pasted images, so text inside screenshots is searchable
  • Edit / suspend / unsuspend / delete / undo-last-grade
  • Optional continuous, private off-site backup to Cloudflare R2, with restore drills and explicit recovery

What v1 is not

Mobile, sync, decks, tags, AI card generation, the AI mistake-explainer, or a hosted service. UUIDs, tombstones, and append-only history preserve sync-compatible foundations, but no merge protocol or sync guarantee exists. Anki import is specced but optional — a fresh start is the likely path.

Stack

Tauri v2. TypeScript UI (React + ProseMirror, with CodeMirror 6 nested in code blocks) + ts-fsrs; Rust layer for data (rusqlite + statically linked sqlite-vec + FTS5), the FSRS optimizer (fsrs-rs), a llama.cpp sidecar running Jina v5 nano retrieval embeddings, and macOS activation/focus-restoration glue around standard Tauri windows. State is two SQLite files (relational data + immutable media blobs) with an append-only review log as the scheduling source of truth.

Reproducing the v1 text-embedding pin

The v1 semantic index is defined by the model file and the settings that give its vectors meaning. Dara pins those inputs so it never mixes incompatible embeddings in one vec0 table. The llama.cpp sidecar is tested and recorded per Dara release, but it is not part of the index identity: a compatible sidecar upgrade does not require rebuilding every vector.

The v1 index inputs are:

Input Pinned value
GGUF repository jinaai/jina-embeddings-v5-text-nano-retrieval-GGUF
GGUF revision 59cfaceeeb7d738c404659435af4c0da74d06c96
GGUF file v5-nano-retrieval-Q8_0.gguf
GGUF size 232883776 bytes
GGUF SHA-256 86b6e6279e9b9e71389f02a082764a2ac2b15a50e37482c26f98d69092f12442
Vector configuration 768 dimensions, last-token pooling, L2 normalization
Retrieval prefixes queries: Query: ; documents: Document:

The official repository manifest can be checked with curl and jq:

curl -sS \
  'https://huggingface.co/api/models/jinaai/jina-embeddings-v5-text-nano-retrieval-GGUF?blobs=true' \
  | jq '{
      revision: .sha,
      q8: (.siblings[]
        | select(.rfilename == "v5-nano-retrieval-Q8_0.gguf")
        | {file: .rfilename, size, sha256: .lfs.sha256})
    }'

Download by immutable revision and verify the bytes before loading the model:

MODEL_DIR="$HOME/Library/Application Support/dara/models"
MODEL_FILE="$MODEL_DIR/v5-nano-retrieval-Q8_0.gguf"
MODEL_REVISION='59cfaceeeb7d738c404659435af4c0da74d06c96'
MODEL_SHA256='86b6e6279e9b9e71389f02a082764a2ac2b15a50e37482c26f98d69092f12442'

mkdir -p "$MODEL_DIR"
curl -fL --retry 3 \
  "https://huggingface.co/jinaai/jina-embeddings-v5-text-nano-retrieval-GGUF/resolve/$MODEL_REVISION/v5-nano-retrieval-Q8_0.gguf" \
  -o "$MODEL_FILE"
printf '%s  %s\n' "$MODEL_SHA256" "$MODEL_FILE" | shasum -a 256 -c -

Local development

Repository-local model files belong in the ignored models/ directory. The application normally downloads and verifies the pinned model beneath its Dara data directory; development can reuse a checked artifact and a local llama.cpp build explicitly:

cd app
DARA_EMBEDDING_MODEL_PATH="$PWD/../models/v5-nano-retrieval-Q8_0.gguf" \
DARA_LLAMA_SERVER_PATH=/opt/homebrew/bin/llama-server \
  pnpm tauri dev

The development command runs as Dara Local with bundle identifier com.silo77.dara.local and keeps its databases under app/.data/local/. Its macOS application identity, single-instance boundary, autostart entry, logs, and R2 Keychain service are separate from the packaged Dara application. A non-production build refuses to start without an explicit DARA_DATA_DIR; the repository command supplies the safe local path.

The model override changes only where model bytes are read from. Dara runs the manifest checksum and golden compatibility checks before first use and whenever the model, sidecar, inference settings, or verification contract changes. A successful check writes an atomic, machine-local derived receipt beneath the Dara data directory. An exact receipt match keeps the model unloaded until a hybrid query or stale document embedding actually needs inference; a runtime failure invalidates the receipt.

Reproducing the sidecar

Dara's arm64 production build bundles llama-server and downloads the larger GGUF separately. The canonical source revision, target, CMake flags, and license notice live in app/src-tauri/resources/sidecars/llama-server-v1.json. The v1 pin is upstream commit fdb1db877c526ec90f668eca1b858da5dba85560 (build 9860). Homebrew's formula moves over time and is never consulted by a release build.

The release staging script checks out that exact revision, builds a static arm64 executable with embedded Metal shaders, runs the Jina compatibility gate through CPU and Metal, verifies that the binary has no non-system dynamic dependencies, and writes an ignored staging directory containing the executable, MIT license, and a machine-readable release manifest:

./scripts/build-llama-sidecar.sh \
  ./models/v5-nano-retrieval-Q8_0.gguf

The staged files are explicit Tauri resources. Build the ad-hoc .app from app/ with:

pnpm release:build:app

After configuring the Apple credentials described in docs/RELEASE.md, build the Developer ID signed, hardened, notarized, and stapled public DMG with:

pnpm release:build:distribution

See docs/RELEASE.md for the complete versioning, build, installation, smoke-check, and tagging procedure.

See docs/OFFSITE_BACKUP.md for plain-language R2 setup, privacy, recovery, and decommissioning guidance.

Both release commands target arm64 macOS 14 or newer and intentionally rebuild and recheck the sidecars before packaging. The GGUF remains outside the .app and is downloaded and verified under Dara's data directory on first semantic use.

Compatibility check

Run a fixed input with the same pooling, normalization, and prefix Dara uses. The basic check confirms that the model loads and returns one normalized 768-dimensional vector:

LLAMA_EMBEDDING="$LLAMA_CPP_DIR/build/bin/llama-embedding"

"$LLAMA_EMBEDDING" \
  --model "$MODEL_FILE" \
  --pooling last \
  --embd-normalize 2 \
  --embd-output-format json \
  --device none \
  --n-gpu-layers 0 \
  --seed 0 \
  --prompt 'Query: Why does spaced repetition work?' \
  > /tmp/dara-jina-query.json

jq -e '.data[0].embedding | length == 768' /tmp/dara-jina-query.json
jq '[.data[0].embedding[] | . * .] | add | sqrt' /tmp/dara-jina-query.json

Dara keeps one or two small known-output fixtures—one query and one document—to catch mistakes such as dropping a prefix or changing the pooling mode. A sidecar upgrade must produce compatible results for those fixtures, within a small tolerance, and pass through the same llama-server --embedding --pooling last --embd-normalize 2 endpoint used by the application. The fixtures test Dara's integration with the official GGUF; Dara does not independently validate Jina's quantization against the original high-precision model.

Run the complete artifact, fixture, and sidecar-endpoint check with:

LLAMA_EMBEDDING="$LLAMA_CPP_DIR/build/bin/llama-embedding" \
LLAMA_SERVER="$LLAMA_CPP_DIR/build/bin/llama-server" \
  ./scripts/verify-jina-v1.sh "$MODEL_FILE"

The script defaults to the CPU backend for reproducibility. Before packaging a macOS release, run the same gate through Metal as well:

LLAMA_DEVICE=auto LLAMA_GPU_LAYERS=all \
LLAMA_EMBEDDING="$LLAMA_CPP_DIR/build/bin/llama-embedding" \
LLAMA_SERVER="$LLAMA_CPP_DIR/build/bin/llama-server" \
  ./scripts/verify-jina-v1.sh "$MODEL_FILE"

After this check passes, the V1 migration seeds the immutable TextEmbeddingIndex definition but leaves AppSettings.active_text_embedding_index_id null. Dara activates it only after the artifact is verified and every active SearchDocument has a current vector.

A new upstream GGUF or llama.cpp commit does not itself create a new index. Dara creates a new vec0 table only when it intentionally adopts model bytes or semantic settings that produce incompatible vectors. It then builds the replacement in the background and switches the active pointer when the new index is complete.

Status

Personal-v1 release-candidate work. The macOS windowing and SQLite foundations are implemented, together with all three card types, images/OCR, saved-card editing, suspension, tombstone deletion, FSRS review, local hybrid search, and optional off-site backup and recovery through Litestream and Cloudflare R2. Final release acceptance and distribution preparation remain in progress.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages