Installation
One command, then a wizard. karani runs end to end with no external services at all and upgrades piecewise from there.
uv tool install karani # or: pip install karani
karani init # interactive: writes karani.toml
karani config check # see what it resolved toThat is enough to run a hunt. Everything else on this page is an upgrade.
What you need
Python 3.11 or newer. No compiler, no Rust toolchain, no system libraries. karani is pure Python and the wheel carries everything the core pipeline needs.
A resume, as markdown. This is the one file that is genuinely required, because it is what every qualification and every draft is written against:
cp data/resume.md.example data/resume.md # then edit itKeep it in markdown rather than exporting a PDF. karani reads it as text, hashes it, and re-judges every stored posting when the hash changes — so editing your resume is how you re-run the whole shortlist.
Nothing else. With no DATABASE_URL set, karani uses an in-memory
store, which is a real code path rather than a test shim (ADR
0006): it is how you try the
pipeline before deciding you want it. State does not survive the process,
which is the only catch.
Secrets
Every credential comes from the environment, never from karani.toml
(CLAUDE.md
4.8 ):
cp .env.example .envNothing in .env is required to run a pass. Set what you actually want:
| Variable | When you need it |
|---|---|
DATABASE_URL | To keep state between runs. Any Postgres. |
OPENROUTER_API_KEY | To qualify or draft on a hosted model. Not needed if you run local. |
ANTHROPIC_API_KEY | Only with provider = "anthropic". |
SLACK_BOT_TOKEN, SLACK_APP_TOKEN | For the Slack surface. |
NOTION_TOKEN | For the Notion mirror. |
GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN | For the inbox watcher. |
The full list is in Environment variables.
Optional extras
Each one is a pip extra, and each is genuinely optional: karani degrades
rather than failing when a feature’s dependency is absent.
uv sync --extra slack # the two-way Slack listener (Socket Mode)
uv sync --extra orchestrator # LangGraph, for the scheduled hourly pass
uv sync --extra artifacts # MinIO/S3 object storage for the documents
uv sync --extra memory # mem0 + pgvector semantic recall
uv sync --extra anthropic # the Anthropic SDK, for provider = "anthropic"
uv sync --extra gmail # only the one-time OAuth consent flow
uv sync --extra dev # pytest and ruffInstalling everything at once, which is what the scheduler wants:
uv tool install --editable ".[slack,orchestrator,artifacts,memory,anthropic]"Infrastructure, if you want it
Postgres with pgvector, and optionally a local model server, as disposable local containers:
karani infra up # Postgres + pgvector on :5433
karani infra up --profile local-llm # and Ollama on :11434
karani infra down # stop; volumes persistThen point the DSN at it:
DATABASE_URL=postgresql://karani:karani@localhost:5433/karaniAny Postgres works — a managed one is fine, and the DSN is the only switch. See Infrastructure for the shape of it, and the important caveat about not running Ollama in Docker on a Mac.
Check it
karani config check # resolved config, including which model runs each task
karani stats # database counts; proves the DSN works
karani run # one free pass over every sourcekarani run calls no model, so it is the safe thing to run first. If it
fetches a few hundred postings and drops most of them, everything works.
From source
git clone https://github.com/cod3smith/karani
cd karani
uv sync --extra dev
cp .env.example .env
cp data/resume.md.example data/resume.md
uv run pytest tests -qThe default suite is deterministic — no network, no clock, no model calls —
and runs in about forty seconds. The Postgres-backed SQL has its own suite,
make test-pg, which skips when the compose database is down.