modelshelf 0.1.0

A shared local LLM model registry: discover, deduplicate, download, and update models across desktop apps.
Documentation
# modelshelf

**A shared local LLM model registry for desktop apps.** Discover every model already on the machine, share one physical copy between apps, keep them up to date — no daemon required.

[![CI](https://github.com/koiyal/modelshelf/actions/workflows/ci.yml/badge.svg)](https://github.com/koiyal/modelshelf/actions/workflows/ci.yml)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

## The problem

When several desktop apps each embed a local LLM workflow:

- **Duplicate downloads.** App A and App B both pull the same 5 GB model into their own private directory.
- **Stale models.** App A silently keeps using a model downloaded a year ago while App B uses a recent one — the user never notices.
- **No visibility.** Models are scattered across Ollama's blob store, LM Studio's tree, the Hugging Face cache, and ad-hoc folders. Nothing sees the whole picture.

## What modelshelf does

modelshelf is a Rust SDK + CLI built around a simple **on-disk convention** (`~/.modelshelf`): a JSON registry, a content-addressed shared model store, and advisory file locks. Any app that links the SDK (or shells out to the CLI) cooperates through that convention — there is no background service to install.

- **Discover** — scan Ollama, LM Studio, the Hugging Face hub cache, Jan, GPT4All, and custom directories; list every local model in one unified view, with duplicates detected by content hash.
- **Share**`ensure()` returns an existing local copy (adopted into the shared store via hardlink, zero extra disk) instead of downloading again.
- **Recommend** — detect RAM and GPU VRAM, then pick the best model for *this* machine from a curated, remotely updated catalog — per use case: chat, code, reasoning, embedding (RAG), speech-to-text, text-to-speech, vision. One call provisions it (companion files like TTS decoders and vision projectors included), and users are told when a better model appears.
- **Update** — check Hugging Face Hub for newer revisions without downloading a byte; pull updates with resumable downloads.
- **Deduplicate** — replace redundant copies with hardlinks to reclaim disk space. Dry-run by default, journaled, undoable.

Desktop only (Windows / macOS / Linux). Mobile is out of scope.

## Quick start (CLI)

```console
$ modelshelf scan
$ modelshelf list --duplicates
$ modelshelf recommend        # which curated chat models fit this machine's RAM/GPU?
$ modelshelf recommend --task stt   # …or code, reasoning, embedding, tts, vision
$ modelshelf recommend --pull # download the best one (asks before removing an old pick)
$ modelshelf pull hf:bartowski/Meta-Llama-3.1-8B-Instruct-GGUF --quant Q4_K_M
$ modelshelf update           # newer revisions? a better model for this machine?
$ modelshelf catalog update   # refresh the recommendation catalog now
$ modelshelf dedup            # dry run: shows reclaimable space
$ modelshelf dedup --apply
```

## Quick start (SDK)

```rust
use modelshelf::{Shelf, OpenOptions, ScanOptions, ModelSpec, PullOptions};

let shelf = Shelf::open(OpenOptions::default().app_id("com.example.myapp"))?;
shelf.scan(&ScanOptions::default())?;

// Reuse an existing local copy if one exists anywhere; download otherwise.
let spec = ModelSpec::parse("hf:bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf")?;
let entry = shelf.ensure(&spec, &PullOptions::default())?;

// Feed this path to llama.cpp or any GGUF-capable runtime.
let path = shelf.blob_path(&entry.id)?;
```

Or let modelshelf pick a model for the machine it is running on — the
local-first "just works" setup for a desktop app's first run:

```rust
use modelshelf::{Hardware, RecommendOptions, PullOptions};

// Detects RAM / NVIDIA VRAM / Apple Silicon; picks from the curated catalog;
// downloads only when no local copy can be reused.
let hw = Hardware::detect();
let outcome = shelf.provision_recommended(&hw, &RecommendOptions::default(), &PullOptions::default())?;
let path = shelf.blob_path(&outcome.entry.id)?;

// Later runs: tell the user when the catalog has something better.
if let Some(upgrade) = shelf.recommend(&hw)?.upgrade {
    println!("A better model is available: {}", upgrade.to.name);
    // On consent: provision_recommended() again, then optionally
    // shelf.remove_if_unreferenced(&upgrade.from) — it refuses to touch
    // anything still referenced or living outside the shared store.
}
```

The catalog is versioned data in [crates/modelshelf/catalog/](crates/modelshelf/catalog/), refreshed from this repository at most weekly with an embedded fallback — see its README for the curation policy. A runnable version of the above lives at `cargo run -p modelshelf --example recommend`.

modelshelf manages models; it does not run them. Inference stays in your app's runtime of choice.

## How it works

```
~/.modelshelf/
├── registry.json    # which models exist, where, which apps use them
├── registry.lock    # advisory lock for safe concurrent access
├── blobs/           # canonical content-addressed store (sha256-<hex>)
├── downloads/       # resumable download state
├── journal/         # dedup operation records (undo)
└── catalog.json     # cached recommendation catalog (client-side)
```

The convention is specified in [docs/SPEC.md](docs/SPEC.md) so that other tools and languages can implement it independently. Safety rules — including which ecosystem directories modelshelf will never write into — are part of the spec.

## AI-assisted development

- **Integrating modelshelf into your app with Claude Code / Codex / Cursor?** Point your assistant at [docs/AI-INTEGRATION.md]docs/AI-INTEGRATION.md — a self-contained guide with the JSON CLI, exit codes, SDK recipes, and a ready-made prompt.
- **Contributing to modelshelf itself with an agent?** Agents read [AGENTS.md]AGENTS.md (Claude Code picks it up via `CLAUDE.md`): build gates, hard safety rules, and the catalog update runbook.

## Status

Early development, pre-0.1. The on-disk format may still change until 0.1.0 is tagged.

## Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md). All commits, issues, and pull requests are in English.

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.