- Python 100%
New-repo commit gap fix, idempotent `zettel repo new`, and a CHANGELOG.md flagging breaking changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|---|---|---|
| .vscode | ||
| docs | ||
| src/zettel | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| pyproject.toml | ||
| README.md | ||
zettel
A shared, searchable collection of notes for a team, built entirely on git.
Notes are ordinary markdown files in ordinary git repositories. You
write them in your editor, commit them, and push them like any other
file. zettel reads whatever repositories you can clone and gives you
one search across all of them, the web of links between the notes, an
answer-my-question command, editor support, and an interface for AI
assistants.
Three consequences of "it's just git" are worth knowing up front:
- There is no server to run. No database to back up, no service to keep alive, no accounts to manage. Everything is your git remotes plus a small local program.
- Permissions are git's permissions, and only git's. If you cannot clone a repository, its notes do not appear in your search — not because something filters them out, but because they were never there. Sharing a note means granting access to a repo; nothing else can.
- The search index is disposable. It is built from your repos and can be deleted and rebuilt at any time. Your notes are never inside it.
New here? → docs/getting-started.md.
It explains every term it uses and walks from an empty machine to a
working setup.
Configuring an installation? → docs/configuration.md.
It is a complete, standalone reference for zettel.toml,
zettel_secrets.toml, and environment-variable overrides.
The technical summary (for readers who want the shape rather than the tour)
Git-native, organization-wide Zettelkasten: notes are markdown files with YAML frontmatter, git repositories are the unit of access control and distribution, and there is deliberately no server component — everything a backend would normally do is handled by git plus a thin local client.
A repo you own declares itself one by carrying a top-level
zettelmesh-repository.md (zettel repo init) — frontmatter naming
its profile and layout, prose telling a human what is in it. Its
registry entry then needs little more than a URL. A repo you don't
own carries nothing and is registered as mode: adopted, read-only,
with its embeddings and synthesized ids held in a separate cache repo.
Search is hybrid: dense vectors (sqlite-vec, from embeddings stored
as sidecar files in git) fused with sparse BM25 (SQLite FTS5, computed
locally, no API needed) via reciprocal rank fusion. Access control is
git's own repo ACL — no clone means no access means no index entry,
correct by construction rather than by enforcement. Because the trust
model is delegated to git and to how you deploy the MCP server, read
docs/extensions/security-and-deployment.md before an org-wide or
agent-facing deployment.
Getting started
→ docs/getting-started.md is the full
setup guide, written to assume no prior knowledge: what each term means,
prerequisites per platform, installing as a user or as a developer,
creating your own note collection, and setting up each piece in turn —
the terminal command, the terminal browser, meaning-based search,
question answering, editor support, VS Code, AI assistants, and the
website export. The complete configuration reference is
docs/configuration.md.
The short version, if you already know the shape:
uv tool install "zettel[openai,tui] @ git+https://code.hans-aschauer.de/hans/zettelmesh.git"
uv tool update-shell
export ZETTEL_REGISTRY_PATH=/path/to/registry.yaml # which repos are in the mesh
export ZETTEL_ROOT=/path/to/clones # where sync clones them
export ZETTEL_INDEX_DB=/path/to/index.db # the derived index
zettel init && zettel sync && zettel search "..."
There is a ready-made example workspace wiring up three real repositories to look at first — see the guide.
Status
Implemented and tested end to end. All 15 originally planned packages
plus linting/ and lsp/ have real implementations; all six
user-story integration tests (US1–US6) run against real local git
repos, real SQLite/FTS5/sqlite-vec, and the real installed MCP and
LSP SDKs — no mocks anywhere in the suite.
Machine-readable output: the --json shapes of list / search /
repos / graph / taxonomy / ask are a documented stable contract
— see docs/json-output.md.
See docs/getting-started.md to set it up, AGENTS.md for the
architecture, working conventions, and the design decisions made during
implementation, and docs/extensions/ for one document per component
in depth (plus proposals that are documented but not yet built).
CHANGELOG.md lists user-visible changes, flagging breaking ones.
Development
uv sync --dev
uv run pytest
uv run lint-imports # layering contracts (see pyproject.toml)
uv run zettel --help
To get a zettel on PATH that tracks your working tree:
uv tool install --editable ".[openai,tui,mkdocs]".
Commands
zettel init # create local config (this machine)
zettel repo init [DIR] # declare a checkout a zettelmesh repo (writes the marker)
zettel repo check [DIR] [--fix] # flag/repair contract deviations (additive fixes only)
zettel repo add URL # clone + register a repo, inheriting its own declaration
zettel repo new URL [--name N] [--dry-run] # create a NEW notes repo end to end; safe to re-run
zettel config show [--json] # effective local config; safe for editor/tool integrations
zettel repo show SHORTNAME # one repo's effective config, field by field, with provenance
zettel sync [SHORTNAME] [--force] [--non-interactive] # clone/pull registered repos, reindex if needed
zettel search QUERY [--tags ...] [--type ...] [--org-field NAME=VALUE ...] [--author ...] [--since ...]
zettel list [--tags ...] [--type ...] [--org-field NAME=VALUE ...] [--author ...] [--since ...] [--json]
zettel show NOTE_ID
zettel edit NOTE_ID # open in $EDITOR at its checkout path (native/colocated)
zettel links NOTE_ID # links: + inline zettel: links, each way (--no-backlinks / --no-inline)
zettel graph [filters] [--include-inline] [--format dot|mermaid|json] # the link graph, or a filtered subgraph
zettel new REPO TITLE # writes notes/YYYY-MM-DD_slug.md; validates tags first
zettel repo commit [SHORTNAME] [--no-push] [--author "Name <email>"] # git-add + commit (+ push by default)
zettel repo annotate SHORTNAME [-f title=heading] [-f tags=llm] # per-note metadata for an adopted repo
zettel repo embed [SHORTNAME] # compute .emb sidecars (or cache entries for adopted repos)
zettel ask "QUESTION" [--json] # grounded answer with citations (needs $ZETTEL_CHAT_MODEL)
zettel chat # REPL over the same retrieval
zettel repo list | taxonomy [--json] # what's registered / the tag vocabulary
zettel lint [SHORTNAME] [--strict] [--stale DAYS]
zettel tui # full-screen terminal browser (needs the [tui] extra)
zettel serve mcp | lsp
Browse it as a website
The zettel[mkdocs] extra ships a MkDocs plugin (zettelmesh) that
renders your local index — every repo you have cloned, nothing more —
as a static site. A published site is just this, run by an identity
that can only clone the repos its audience may see. See
docs/getting-started.md
and docs/extensions/export-and-publish.md.
Layout
domain/ has no I/O dependencies; services/ is the only package
holding business logic; cli/, mcp/, and lsp/ are thin adapters
over services/. Full package table in AGENTS.md.