A semantic git CLI tool using zvec + fastembed
Project description
Town Elder
Town Elder is a local-first semantic memory CLI for AI coding agents.
Like a town elder, it helps your tools remember what your project has already learned and why past decisions were made.
It helps you recover project context when exact keywords are unknown:
- Index source files and notes (
index files,add) - Index commit messages and diffs (
index commits) - Search everything with natural language (
search)
Town Elder runs locally and stores project memory in .town_elder, so you can query context without relying on an external retrieval service. It uses zvec as an embedded vector database, which means there is no separate vector DB server to deploy or operate.
Core Value
Town Elder is useful when it reliably delivers these outcomes:
- Find likely code and notes quickly when you do not know exact names or paths.
- Recover intent from git history by searching commit messages and diffs semantically.
- Get local semantic search powered by embedded
zvec, with explicit data-dir control for predictable multi-repo use.
Quick Start
# Initialize Town Elder in your project (using uv)
uv run te init
# Index your code files (full file indexing)
uv run te index --all
# Search semantically
uv run te search "authentication logic"
Output example:
Search results for: authentication logic
1. Score: 0.912
src/auth/session.py
validate_token() checks token expiration and issuer before loading user context.
2. Score: 0.857
Commit: tighten auth retry flow for expired refresh tokens
Ad-hoc usage (no dependency setup in the current project, for example a Poetry repo):
uvx --from town-elder te init
uvx --from town-elder te search "authentication logic"
How To Use This Day-To-Day
Use Town Elder as your semantic first pass, then use exact tools to confirm and implement.
Decision Rule
- Start with Town Elder when you do not know exact keywords, symbol names, or commit hashes.
- Switch to
rgandgitafter Town Elder identifies likely files and commits.
Typical Workflow
- From your repo root, initialize once:
uv run te init
- Build memory from code and commit history:
uv run te index --all
uv run te index commits --limit 200
- Ask intent-level questions:
uv run te search "data-dir leakage across CLI invocations"
uv run te search "last indexed commit not found stale state"
- Confirm and implement with exact tools:
rg -n "data-dir|sentinel|last_indexed_commit" src tests
git log --oneline --grep="data-dir leakage"
git show <commit-hash>
- Save new project knowledge for future sessions:
uv run te add \
--text "Safety rule: never delete non-Town Elder hooks unless --force" \
--metadata '{"source":"engineering-note","topic":"hook-safety"}'
- Keep history memory fresh automatically:
uv run te hook install
Use --data-dir when you want storage outside the current repo (for example, a temporary isolated session or operations run from another working directory).
Typical questions for te search: "why was retry logic changed?", "what fixed the stale state bug?", "where is initialization failure handled?"
Installation
From PyPI
pip install town-elder
From Source
git clone https://github.com/KalleBylin/town-elder.git
cd town-elder
uv pip install -e .
Running with uv or uvx
This project uses uv for dependency management.
- Use
uv run te <command>when working from a local checkout/environment. - Use
uvx --from town-elder te <command>for one-off usage without changing project dependencies.
# Run Town Elder commands from the project root
uv run te <command>
# Run without modifying the current project's dependency config
uvx --from town-elder te <command>
# Operate on a different repository (using --data-dir)
uv run te --data-dir /path/to/target-repo/.town_elder <command>
uvx --from town-elder te --data-dir /path/to/target-repo/.town_elder <command>
For example, to index commits in another repository:
uv run te --data-dir /path/to/target-repo/.town_elder init --path /path/to/target-repo
uv run te --data-dir /path/to/target-repo/.town_elder index commits --repo /path/to/target-repo
Usage
A) Fresh Project (New Repository)
Start from scratch and build semantic memory as you develop.
# Initialize the database in your project
$ uv run te init
Initialized Town Elder database at /path/to/project/.town_elder
# Index your codebase (Python, Markdown, and reStructuredText files)
$ uv run te index --all
Indexing 42 files...
Indexed 42 files
# Add contextual notes for AI agents
$ uv run te add -t "Use FastAPI for REST APIs, Django for full-stack apps" \
-m '{"source": "architecture-guidelines"}'
# Search your semantic memory
$ uv run te search "API framework"
Search results for: API framework
1. Score: 0.892
Use FastAPI for REST APIs, Django for full-stack apps...
2. Score: 0.745
# API Documentation
This module provides REST endpoints for...
B) Existing Project (With History)
Unlock tribal knowledge from your git history.
# Navigate to your existing repository
$ cd /path/to/existing-project
# Initialize Town Elder
$ uv run te init
# Index all commits (last 100 by default)
$ uv run te index commits
Indexing 100 commits...
Indexed 100 commits
# Or limit to a specific number
$ uv run te index commits --limit 50
# Search git history semantically
$ uv run te search "payment retry bug"
Search results for: payment retry bug
1. Score: 0.923
Commit: Fixed race condition in retry logic by adding exponential backoff
diff --git a/payment.py b/payment.py
- retry_count = 3
+ retry_count = 5
+ sleep_factor = 2 # exponential backoff
# Install automatic indexing on every commit
$ uv run te hook install
Installed post-commit hook at /path/to/.git/hooks/post-commit
Commits will now be automatically indexed
Commands Reference
Commands are shown with uv run te. You can use uvx --from town-elder te equivalently.
| Command | Description |
|---|---|
uv run te init |
Initialize a Town Elder database in the current directory |
uv run te add |
Add a document with optional metadata |
uv run te index --all |
Index all .py, .md, and .rst files in current directory (full repository file indexing) |
uv run te index files [PATH] |
Index all .py, .md, and .rst files in a specific directory |
uv run te index commits |
Index git commits from a repository |
uv run te search |
Search indexed documents semantically |
uv run te stats |
Show document count and configuration |
uv run te hook install |
Install post-commit hook for automatic indexing |
uv run te hook uninstall |
Remove post-commit hook |
uv run te hook status |
Check if hook is installed |
Options
--data-dir,-d: Data directory (default: .town_elder in current directory)--path,-p: Specify directory path (for init)index files [PATH]: Positional directory path for file indexing (default: current directory)index --all: Index all files in current directory (equivalent tote index files .)--top-k,-k: Number of search results (default: 5)--limit,-n: Number of commits to index (default: 100)--repo,-r: Git repository path (for index commits, hook commands)--force,-f: Overwrite existing data--text,-t: Text content to add--metadata,-m: JSON metadata string
Configuration
Town Elder stores data in a .town_elder directory in your project:
your-project/
├── .town_elder/
│ ├── vectors/ # Vector database files
│ ├── index_state.json # Commit indexing state
│ └── file_index_state.json # File hash/chunk state for incremental file indexing
└── .git/
Default Settings
- Embedding model: Fastembed (BAAI/bge-small-en-v1.5, 384 dimensions)
- Data location:
.town_elder/in your project directory - Indexed file types:
.py,.md, and.rstfiles
Configuration is managed via environment variables or pyproject.toml. See town_elder.config for available options.
File Indexing Notes
.rst Semantic Metadata
When indexing .rst files (uv run te index --all or uv run te index files), Town Elder stores section-aware chunk metadata:
section_pathandsection_depthfor heading hierarchydirectivesandhas_directivesfor directive extraction (for example.. note::)temporal_tagsandhas_temporal_tagsfor temporal directives (for example.. deprecated::,.. versionchanged::)
First Run vs Rerun
- First run (
uv run te index --all): scans all supported files and writesfile_index_state.json. - Incremental rerun (
uv run te index --allagain): compares tracked git blob hashes and skips unchanged files automatically. - Use
uv run te index files . --no-incrementalif you need a forced full reindex.
60k-File Benchmark (2026-02-17)
Benchmark command:
PYTHONPATH=src python scripts/benchmark_indexing.py \
--files 60000 \
--workers 8 \
--output-json docs/benchmarks/indexing-60k-2026-02-17.json \
--output-md docs/benchmarks/indexing-60k-2026-02-17.md
Measured results from docs/benchmarks/indexing-60k-2026-02-17.json:
| Profile | Parser mode | Scan files/s | Parse files/s | Embed chunks/s | Total wall s |
|---|---|---|---|---|---|
| baseline_full | sequential | 36,502.6 | 21,248.2 | 126,454.1 | 5.04 |
| optimized_full | sequential_fallback | 39,189.8 | 20,613.8 | 2,088,038.4 | 4.48 |
| optimized_rerun_hash_skip | n/a | 158,919.2 | 0.0 | 0.0 | 1.47 |
Notes:
optimized_fullusedsequential_fallbackparser mode in this restricted environment (process pools unavailable), so parse speedup is conservative.- Embed throughput improved ~16.5x and total wall time improved ~1.13x.
- Incremental rerun (
optimized_rerun_hash_skip) skipped all unchanged files.
For full workflow details see docs/indexing.md.
Troubleshooting
First-Run Model Download
On first use, Town Elder downloads an embedding model from HuggingFace. This may take some time depending on your internet connection.
What to expect:
- Initial commands (
te index files,te search,te add,te index commits) may take 30-60 seconds on first run - The model (~100MB) is downloaded once and cached locally
- Subsequent runs will be fast
Common issues:
| Issue | Solution |
|---|---|
| Slow first run | Normal - model is being downloaded. Wait for completion. |
| "Failed to load embedding backend" | Install fastembed: pip install fastembed or uv pip install fastembed |
| Network error during download | Ensure internet access to HuggingFace (huggingface.co). Check proxy settings if behind a firewall. |
| Model not found | Ensure fastembed is installed: pip show fastembed |
Hook Prerequisites
The post-commit hook automatically indexes commits after each git commit. For hooks to work correctly:
Requirements:
- Git repository: Must run in a directory with a
.gitfolder - Python, uv, or uvx: The hook uses a fallback chain (
uv run te→uvx --from town-elder te→te→python -m town_elder) - Initialized database: Run
te initbefore installing hooks
Hook fallback chain:
# First tries uv
command -v uv >/dev/null 2>&1 && uv run te index commits ... && exit
# Then tries uvx
command -v uvx >/dev/null 2>&1 && uvx --from town-elder te index commits ... && exit
# Then tries te command
command -v te >/dev/null 2>&1 && te index commits ... && exit
# Finally falls back to python module
python -m town_elder index commits ...
Common issues:
| Issue | Solution |
|---|---|
| Commits not being indexed | Check hook status: te hook status |
| Hook not found | Install it: te hook install |
| "command not found" errors | Ensure uv, uvx, te, or Python is on your PATH |
| Hook runs but nothing happens | Ensure database is initialized: te init first |
| uv not found | Install uv, use uvx --from town-elder te ..., or ensure te is on your PATH |
Verifying hook installation:
# Check if hook is installed
te hook status
# View installed hook content
cat .git/hooks/post-commit
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file town_elder-0.2.1.tar.gz.
File metadata
- Download URL: town_elder-0.2.1.tar.gz
- Upload date:
- Size: 45.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
183706ed96c85de584f9b399eb21abc2f94dc2ac421f77632867669c64ce8cd6
|
|
| MD5 |
90aa7bfa21c33e1309214c41aa838ff5
|
|
| BLAKE2b-256 |
d1c771e6fbb0da025e1f3203a7feb71c60827c1c67fb386ceea11965e147e571
|
File details
Details for the file town_elder-0.2.1-py3-none-any.whl.
File metadata
- Download URL: town_elder-0.2.1-py3-none-any.whl
- Upload date:
- Size: 55.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.3 {"installer":{"name":"uv","version":"0.10.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
393b7aaf2b11837f7d5d7195a744e7dbb9644955b78e6f83ecda161b78adfb16
|
|
| MD5 |
88a72369e9e31628786b8dad2546b5a1
|
|
| BLAKE2b-256 |
238280481bd60b47ab6750715c0fce19a2bed95ab5ca536738366869d5f4f368
|