A macOS desktop app that runs in the background, indexes your watched local git repositories (commit history + working-tree file changes) into a searchable "developer memory," and exposes that memory to a locally-run LLM via an MCP server.
Chat with the model to recall past work and generate summaries, and keep a library of code snippets. Nothing leaves your machine — capture, retrieval, the MCP server, and the LLM all run locally.
Status: all v1 functionality is implemented and the app is packaged into a self-contained macOS
.app. This repo is meant to be a solid starting point if you want to use it as a template for your own Blocks. The detailed engineering log lives indocs/PROGRESS.md— read it first when picking up the code.
Requires macOS on Apple Silicon.
- Download
Blocks.for.Developers.zipfrom the latest release and unzip it. - Drag
Blocks for Developers.appinto/Applications. - Remove the download quarantine flag so macOS will run it:
xattr -dr com.apple.quarantine "/Applications/Blocks for Developers.app" - Open it (double-click, or right-click → Open).
Why step 3? macOS tags downloaded files with a com.apple.quarantine flag, and
Gatekeeper blocks apps that aren't notarized by Apple. This build is ad-hoc signed but
not notarized (notarization needs a paid Apple Developer account), so removing the flag
tells macOS you trust the app and lets it launch — the same trust decision as
right-click → Open → Open, done in one reliable step.
On first launch the app onboards you, creates its data folder at
~/Library/Application Support/dev.blocks.app/, and lets you pick and download a local
model. The MCP server and llama.cpp runtime are bundled — no Homebrew needed.
To build from source instead, see Getting started.
- Memory capture — git commits (indexed incrementally) plus working-tree file-change snapshots (debounced, content-hash-deduped). Scope is git-tracked content only; no terminal/window/clipboard capture.
- Retrieval — local embeddings + vector search plus SQLite FTS5 keyword search,
exposed through an MCP server as
search_memory/get_activity/get_commits. - Chat — a local llama.cpp runtime with an OpenAI-compatible API on loopback. Chat
uses retrieval-augmented generation: the app queries
search_memoryfirst and injects the hits as context (not native tool-calling, which small models do unreliably). - Single-click summaries — Day Recap, What's Top of Mind, and Standup Update, each a
canned turn that pulls recent activity via
get_activityand asks the model to write it. - Materials — saved code snippets with a language tag, annotation, and text-expander note; searchable, sortable, filterable, copy-to-clipboard, and save-from-chat linking.
- Always-on — a menu-bar tray app; capture, the MCP server, and the UI live in one process.
- Local-first & private — your code memory and models stay in one folder on your Mac.
┌─────────────────────────────────────────────┐
│ Blocks app (Native SDK + Zig, one process) │
│ │
watched repos ──▶│ capture ──▶ SQLite (app.db) ◀── retrieval│
(git + files) │ (git log, events/snapshots/ │
│ file scan) embeddings/FTS5 │
│ ▲ │
│ chat UI ──────┐ │ reads (read-only) │
└───────────────┼─┼────┼────────────────────────┘
│ │ │
HTTP (loopback) │ │ spawns
▼ │ ▼
┌────────────────────┐ ┌──────────────────────┐
│ llama-server │ │ blocks-mcp (sidecar) │
│ (OpenAI-compatible)│ │ MCP over JSON-RPC/HTTP│
└────────────────────┘ └──────────────────────┘
The app spawns two child processes at runtime and reaches them over loopback HTTP:
blocks-mcp— a standalone MCP server that opensapp.dbread-only and serves the three memory tools. Built separately (mcp/), links only the systemlibsqlite3.llama-server— the llama.cpp chat runtime, serving an OpenAI-compatible API.
All data lives in one folder: ~/Library/Application Support/dev.blocks.app/
(app.db, config.json, models/). Settings shows this path so you can back it up.
- UI framework: Native SDK (native-rendered — GPU surface /
Metal — no WebView, no npm, no build files for the app). The
nativeCLI owns the build. - Language: Zig (0.16.0). Zero TypeScript.
- View layer: declarative
.nativemarkup (src/app.native) bound to a Model / Msg /updatecore insrc/main.zig(Elm-style architecture; the pure core requests side effects through an effects handle and receives results as messages). - Storage: SQLite via the SDK's relational store (STRICT tables) with FTS5 for
keyword search; schema is migration-driven (
src/schema/NNNN_*.sql). - MCP sidecar: a standalone Zig binary (
src/mcp_server.zig, built bymcp/build.zigoutside the SDK build graph) linking the systemlibsqlite3. - LLM runtime: llama.cpp
llama-server, spawned as a subprocess. - Embeddings: a deterministic in-process hashing embedder (
hash-v1, dim 256), so the index never needs re-embedding. A neural embedder can be added later under a new model id (the schema allows both to coexist).
- macOS on Apple Silicon (the app is arm64; llama.cpp uses Metal).
- Native SDK CLI —
npm install -g @native-sdk/cli(developed against 0.10.1). Thenativecommand drives everything. - Zig — auto-installed by the CLI on first build (at
~/.native/toolchains), so you don't install it yourself.native test/buildwill prompt once; pass--yesto auto-approve. - llama.cpp —
brew install llama.cpp, which providesllama-server. Needed for the chat runtime during development. (The packaged.appvendors it, so end users don't need Homebrew.)
The app spawns llama-server, so point it at your Homebrew binary via an env var:
BLOCKS_LLAMA_SERVER=/opt/homebrew/bin/llama-server native dev --yesnative dev builds a Debug binary and opens the app with hot reload for
src/app.native — edit the markup and the window updates in ~2s without losing model
state. (The Zig core is not hot-reloaded; changing it needs a rebuild.)
Env vars the app respects:
| Variable | Purpose | Default |
|---|---|---|
BLOCKS_LLAMA_SERVER |
absolute path to llama-server |
vendor/llama/bin/llama-server |
BLOCKS_MCP_SERVER |
absolute path to the blocks-mcp sidecar |
mcp/zig-out/bin/blocks-mcp |
Under native dev the working directory is the repo root, so the relative defaults
resolve. A packaged .app sets both env vars from a launcher script (see Packaging).
native check # validate src/*.native markup + app.json (run after UI/Model changes)
native test --yes # build + run the Zig test suite (also refreshes the model contract)
native build --yes # ReleaseFast binary into zig-out/bin/blocks
native dev --yes # build + run with markup hot reload
native db status # inspect the relational schema / dev database
native db new-migration <name> # scaffold src/schema/NNNN_<name>.sqlBuild the MCP sidecar separately (it lives outside the SDK build graph):
cd mcp && zig build # -> mcp/zig-out/bin/blocks-mcpThe app ships an automation server so you can drive and assert the GUI headlessly — handy for verifying flows end-to-end. Start the app with the flag, then drive it from another shell:
# terminal 1
BLOCKS_LLAMA_SERVER=/opt/homebrew/bin/llama-server native dev --yes -Dautomation=true
# terminal 2 (from the repo root)
native automate wait
native automate snapshot # widget tree with #ids, roles, names, actions
native automate widget-action main-canvas <id> set_text "hello"
native automate widget-click main-canvas <id>
native automate assert [--absent] [--timeout-ms N] "<regex>"The GPU view label is main-canvas. Widget ids are large numbers that change per
rebuild — snapshot to get the current ones. Note: buttons inside a <scroll> may not
receive synthesized clicks (a harness limitation) — prove that logic with unit tests.
packaging/package-macos.shThis builds the app + the MCP sidecar, vendors a relocatable llama-server (with its
dylib closure) into the bundle, assembles an ad-hoc-signed Blocks for Developers.app,
and zips it. See packaging/README.md for details (signing,
notarization, and the --skip-llama option).
src/
main.zig the app: Model/Msg/update, boot, effect firing, the Zig-built
secondary windows (Settings, material editor, delete-confirm)
app.native the main-window declarative view (markup binds ONE canvas)
config.zig data-dir/path resolution, username detection (pure)
git.zig git-history capture builders + parsers (pure)
snapshots.zig working-tree file-change capture (pure)
embeddings.zig / embed_core.zig hashing embedder + vector/FTS search
chat.zig chat data layer + LLM protocol shaping (pure)
models.zig local-model catalog + llama runtime argv/paths (pure)
repos.zig watched-repos data layer (pure)
snippets.zig materials data layer (pure)
tray.zig menu-bar menu definition (pure)
mcp/ pure MCP protocol + tools (JSON-RPC, SQL, shapers)
mcp_server.zig the standalone sidecar binary (no Native SDK dependency)
schema/ SQL migrations (NNNN_*.sql) + migrations.lock.json
tests.zig test root (imports every module + update-arm/markup tests)
mcp/ separate build graph for the sidecar (build.zig)
packaging/ package-macos.sh, vendor-llama.sh, README.md
docs/PROGRESS.md the authoritative engineering log — read first
Almost all logic lives in pure, unit-tested modules; only effect firing lives in
main.zig. That is the pattern to follow when extending: add pure builders/parsers in a
module with tests, then wire the effects in main.zig.
- Read
docs/PROGRESS.mdfor the per-feature "what was built" notes, the database schema, and hard-won Native SDK + Zig learnings. - After any change to markup or the Model/Msg, run
native test --yes(refreshes the model contract) thennative check. - Keep new logic pure and tested; wire effects in
main.zigwith a fresh, documented effect-key block.
- v2 — truly cross-platform. Native builds and installers for Windows and Linux (the Native SDK targets both), so Blocks isn't macOS-only. This means per-platform packaging of the app and its sidecars (the MCP server and the llama.cpp runtime) and platform equivalents for the macOS-specific bits.
- Text-expander notes with real expansion. Each material already carries a "text expander" note; the plan is to wire that into espanso (and similar tools) so a snippet's trigger actually expands system-wide, turning saved materials into live text-expansion shortcuts.
See LICENSE for details.
