lydia-cli
Health Pass
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 36 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Lydia is a local AI coding agent for the terminal — reads and edits your code, runs commands, and drives git through a local Ollama model. No API keys, no cloud, nothing leaves your machine.
Lydia
A local AI coding agent for your terminal — no API keys, no subscriptions,
no cloud. Lydia reads your code, answers questions about it, edits files,
runs commands, drives git, and checks its own work by running your tests,
all through a local Ollama model running on your own
machine. It can also work like a personal assistant — checking email,
Canvas assignments, the stock market, and AI news on a schedule.
It's a personal alternative to tools like Claude Code, Cursor's agent, or
GitHub Copilot Workspace, built for anyone who wants that workflow without
paying for API usage or sending code to a third party.
██ ██╗ ██╗ ██╗██████╗ ██╗ █████╗ ██████╗██╗ ██╗ ██
████ ██║ ╚██╗ ██╔╝██╔══██╗██║██╔══██╗ ██╔════╝██║ ██║ ████
██████ ██║ ╚████╔╝ ██║ ██║██║███████║ ██║ ██║ ██║ ██████
██████ ██║ ╚██╔╝ ██║ ██║██║██╔══██║ ██║ ██║ ██║ ██████
████ ███████╗██║ ██████╔╝██║██║ ██║ ╚██████╗███████╗██║ ████
██ ╚══════╝╚═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ██
╭─────────────────╮
│ model qwen3.5 │
│ project Python │
╰─────────────────╯
Type your request, or /help for commands. Ctrl-D to exit.
Lydia (auto) > add input validation to the login handler and run the tests
The wordmark is rendered live, not stored as art: cli/ui.py builds it with
pyfiglet's ansi_shadow font, frames it with the block-diamond mark on both
sides, and paints it in a blue → violet → pink gradient sampled across its
width. In a terminal too narrow for the framed version it drops the icons,
and below that falls back to plain text.
Why
Claude Code and similar tools are genuinely useful, but they require an API
key and send your code to a hosted model. Lydia is the same workflow —
an agent that reads your project, proposes changes, and asks before doing
anything risky — running entirely against models you've already pulled with
Ollama. Nothing leaves your machine. Nothing costs anything per token.
The tradeoff is real: local models on consumer hardware are smaller and
slower than frontier hosted models, so Lydia won't be as capable. It's built
for personal projects, learning, and situations where "good enough and free"
beats "best available and metered."
Features
- Interactive chat with full streaming output, Markdown rendering, and
live-updating "thinking" previews for reasoning models like Qwen3. - A real coding agent, not just chat. Lydia reads files, searches your
codebase by content (search_code) or by filename pattern (find_files),
makes targeted edits (edit_file, ormulti_edit_filefor several
changes to one file in a single diff), writes new files, runs shell
commands, and drives git — by calling tools the model itself decides to
use, via Ollama's native function-calling support. - Closes the loop on its own work. Set a
verify_command(e.g.pytest -q) and Lydia runs it after making changes, reading the output
and fixing failures before calling the task done — instead of just hoping
the edit was right. - A visible checklist for bigger jobs. For multi-step tasks, Lydia
tracks progress with a live-updating todo list, especially useful in auto
mode where there's no confirmation prompt at every step to anchor you. - Three session modes —
plan(research only, no tool can touch
anything),ask(confirm every change, the default),auto(skip
confirmation for routine edits, still confirm anything destructive) —
switchable with/modeor a Shift-Tab press, always visible in the prompt. - Nothing happens to your files or repo without a diff and a yes/no
prompt (outside auto mode). Writes and deletes always show what's about
to change and keep a timestamped backup; commits and pushes always show
the message/target first. Anything matching a destructive shell pattern
(rm -rf,git push --force,sudo, piping a remote script into a
shell, ...) is always confirmed, in every mode. - Also works as a personal assistant, additively — check Gmail/Outlook
email, Canvas assignments, the stock market, and AI news, composed into a
daily briefing that can run on a schedule with a macOS notification. See
Personal assistant. - Project-aware from the first message. A repository scanner detects
the language mix, project type, and key manifest files, and feeds that
into the system prompt automatically. - Per-project and global configuration, so you can pin a smaller/faster
model for one repo and a larger one for another. - Path-sandboxed by construction. Every filesystem tool resolves paths
relative to the project root and refuses anything that tries to escape it.
Requirements
Python 3.11+
Ollama, running locally, with at least one model
pulled — a model with tool-calling support is needed for the agent
features (Qwen3.5, Qwen2.5, Llama 3.1+ all work):ollama pull qwen3.5
Install
git clone https://github.com/levimackay/lydia-cli.git && cd lydia-cli
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
ln -s "$PWD/.venv/bin/lydia" /opt/homebrew/bin/lydia # or anywhere on your PATH
Usage
| Command | What it does |
|---|---|
lydia |
Interactive agent chat in the current project |
lydia ask "why is this failing?" |
One-shot question, no tools, good for scripts |
lydia ask "..." --yes |
Same, but with full tool access — auto-approves everything except dangerous commands, for scripts/CI with no one to answer a y/n prompt |
lydia analyze |
Project summary: languages, size, key files |
lydia models |
List installed Ollama models |
lydia index |
Build/refresh the semantic search index (for search_semantic) |
lydia restore list / apply <n> |
List/restore file backups from .lydia/backups/ |
lydia init |
Create .lydia/ project config |
lydia config show |
Show effective (merged) configuration |
lydia config set model qwen3.5:9b |
Set a config value (--project for per-repo) |
lydia memory list / add <fact> / forget <n> |
View/manage facts Lydia remembers about this project |
lydia auth login gmail/outlook/canvas |
Connect a personal-assistant data source |
lydia auth status / logout <provider> |
Check or disconnect a connected source |
lydia briefing run / show |
Generate/print the daily personal briefing |
lydia briefing schedule enable --time 08:00 / disable |
Turn the daily scheduled briefing on/off |
lydia automate "..." |
Create an automation from a plain-English description |
lydia automations list / show <name> |
List all automations or show one in detail |
lydia automations run <name> |
Execute one automation immediately (ignores its trigger) |
lydia automations enable/disable <name> |
Enable or disable an automation |
lydia automations remove <name> |
Delete an automation recipe |
lydia automations schedule enable / disable |
Turn the automations heartbeat (launchd) on/off |
Inside chat: /help, /mode [plan\|ask\|auto] (or Shift-Tab), /model <name>,/models, /new (fresh conversation), /remember <fact>, /memory,/forget <n>, /exit.
A typical session
With verify_command set to pytest -q (see Configuration):
Lydia (auto) > fix the bug where login accepts an empty password
› read_file(path='src/auth/login.py')
read src/auth/login.py
› search_code(pattern='def login')
searched for 'def login'
Found it — login() never checks that password is non-empty before
comparing the hash. Here's the fix:
› edit_file(path='src/auth/login.py', old_string='...', new_string='...')
Updated src/auth/login.py
› run_command(command='pytest -q')
ran `pytest -q` (exit 0)
I added a check that rejects an empty password before it ever reaches the
hash comparison, and ran the test suite to confirm — all passing.
In ask mode (the default), edit_file shows a diff and asks first:
╭──────────────── Edit src/auth/login.py ───────────────╮
│ --- a/src/auth/login.py │
│ +++ b/src/auth/login.py │
│ @@ -12,6 +12,8 @@ │
│ def login(username, password): │
│ + if not password: │
│ + raise ValueError("password required") │
│ user = find_user(username) │
╰──────────────────────────────────────────────────────────╯
Proceed? [y/n] (y): y
Configuration
Layered JSON config — project overrides global:
~/.lydia/config.json— global defaults<project>/.lydia/config.json— per-repository (created bylydia init)
| Key | Default | Meaning |
|---|---|---|
model |
auto | Ollama model name; auto-picks the best installed coder model if unset |
temperature |
0.7 |
Sampling temperature |
num_ctx |
8192 |
Context window size passed to Ollama |
ollama_host |
http://localhost:11434 |
Where the Ollama daemon is listening |
think |
auto |
auto/on/off — reasoning for thinking models (Qwen3, DeepSeek-R1); off is much faster |
mode |
ask |
ask/auto/plan — the session's permission mode; see Agent tools and the safety model |
verify_command |
not set | Shell command Lydia runs after code changes and self-corrects on failure, e.g. pytest -q; see Auto-verify |
keep_alive |
30m |
How long Ollama keeps the model loaded after a request; avoids a multi-second reload on your next message. Ollama duration string, or -1 to never unload |
server_url |
not set | If set, talk to a remote Lydia Server instead of a local Ollama daemon — see Running Lydia Server |
api_key |
not set | Bearer token for server_url |
canvas_base_url |
not set | Your school's Canvas URL, e.g. https://school.instructure.com; see Personal assistant |
briefing_schedule_enabled / briefing_schedule_time |
false / 08:00 |
Managed by lydia briefing schedule enable/disable, not usually set directly |
Performance and model choice
Local models are the whole point of Lydia, but they're not free-riding on a
frontier hosted model's scale — a few things make a real difference on
consumer hardware:
- Use a coding-specific model, not a generic chat model, if your
hardware allows it.qwen2.5-coder/qwen3.5-coder/deepseek-coder
are trained specifically on code and noticeably outperform a generic
model of the same size on programming tasks.llm/models.pyalready
prefers these automatically if you have one installed — you just need toollama pullone. - Match model size to your RAM. As a rough guide on Apple Silicon: 16GB
comfortably handles up to ~7-9B models; going bigger risks swapping, which
is far slower than a smaller model outright.qwen2.5-coder:7bis a
solid default on a 16GB machine. think: offif you're on a reasoning model (Qwen3, DeepSeek-R1) and
want speed over the model showing its work — reasoning tokens can easily
add 10-30s to a reply before the actual answer starts.keep_alive(above) avoids paying Ollama's model-load cost
(multi-second) on every single message in a session.
None of this closes the gap with a large hosted model — it narrows it as
much as the "runs entirely on your machine" constraint allows.
Running Lydia Server (remote/GPU inference)
If you have a second machine with more RAM or a real GPU — a gaming PC,
say — you can run inference there instead and keep using lydia normally
from a laptop. Tool execution (file edits, git, shell commands) always
stays on whichever machine runs the CLI — only chat/tool-call inference
and embeddings go over the network. This means lydia "fix this bug"
works exactly the same, from any directory, whether it's talking to a
local Ollama or a remote one.
# On the server machine (needs Ollama already running):
pip install -e . -e server/ # from the repo root, one shared venv
LYDIA_SERVER_TOKEN=<a-long-random-token> lydia-server
# On the client machine:
lydia config set server_url https://<server-host>:<port>
lydia config set api_key <the-same-token>
lydia # works exactly as before
Full server configuration (env vars), API design, and the reasoning behind
the client/server split live in server/README.md.
Agent tools and the safety model
Inside chat, Lydia can call tools against your project. Every tool is
classified into a risk tier that decides whether it needs your approval:
| Tool | Risk | Behavior |
|---|---|---|
read_file, list_dir, search_code, find_files |
safe | Runs immediately |
git_status, git_diff, git_add |
safe | Runs immediately |
write_file, edit_file, multi_edit_file, delete_file |
confirm | Shows a diff, asks y/n, keeps a backup in .lydia/backups/ |
git_commit, git_push |
confirm | Shows the message/target, asks y/n |
run_command |
policy | Safe-looking commands follow the session mode below; anything matching a destructive pattern always asks, regardless of mode |
remember |
safe | Saves a fact to .lydia/memory.json so it's known in future sessions |
update_todos |
safe | Renders a live checklist for multi-step work; ephemeral, not persisted |
search_semantic |
safe | Meaning-based search over an embedding index (lydia index first); falls back to literal search_code if not indexed |
check_email, check_canvas, check_stocks, check_news |
safe | Personal-assistant sources — see Personal assistant |
edit_file replaces one exact snippet of text within an existing file (like
Claude Code's own edit tool) — the model doesn't have to reproduce the whole
file to make a small change. multi_edit_file does the same for several
distinct changes to one file in a single call, applied in order, with one
diff and one approval instead of several. write_file is for new files or a
genuine full-file rewrite. find_files matches file names/paths (e.g.*.py) rather than contents — use search_code for that.
Auto-verify
Set verify_command (e.g. pytest -q, npm test, cargo test) and Lydia
is told to run it via run_command after making code changes, read the
result, and fix any failures before calling the task done — instead of just
assuming an edit was correct. lydia init scans the project's manifest
files and suggests one automatically if it recognizes exactly one project
type; otherwise it's up to you:
lydia config set verify_command "pytest -q" --project
Session modes
The current mode governs every confirm/command-tier tool above, and is
always visible in the prompt (Lydia (ask) > ):
| Mode | Behavior |
|---|---|
ask (default) |
Every confirm/command-tier action asks first. |
auto |
Routine actions (edits, commits, safe shell commands) run without asking; anything flagged dangerous (deletes, git push, a destructive shell command) still asks. |
plan |
Research only — write_file/edit_file/multi_edit_file/delete_file/run_command/git_add/git_commit/git_push aren't even offered to the model (update_todos still is, for tracking the plan's own steps). Ask Lydia to plan something, review what it proposes, then switch modes to let it actually make the changes. |
Switch modes with /mode plan//mode auto//mode ask (or just /mode to
see the current one), or press Shift-Tab to cycle through them without
typing. lydia config set mode auto changes the default for future sessions.
All file paths are resolved relative to the project root and refused if they
try to escape it (.., absolute paths outside the project) — a confused or
adversarial model can't touch files elsewhere on your machine.
Personal assistant
Additive to the coding agent, not a replacement for it — the same lydia
session can do both. Four read-only sources, all running through the same
local Ollama model as everything else:
| Source | Setup |
|---|---|
| Gmail | lydia auth login gmail (one-time Google OAuth; needs your own Google Cloud OAuth client — see the command's output for the exact steps) |
| Outlook / Microsoft 365 | lydia auth login outlook --client-id <id> (one-time device-code sign-in; needs your own Azure app registration) |
| Canvas (school LMS) | lydia auth login canvas (base URL + a personal access token from your Canvas settings) |
| Stock market (general indices) | No setup — via yfinance |
| AI news | No setup — via a curated RSS list |
lydia briefing run # generate today's briefing now
lydia briefing show # print the last one
lydia briefing schedule enable --time 08:00 # run automatically every day
lydia briefing schedule disable
briefing run fetches every connected source deterministically first, then
uses the model only to synthesize a prioritized checklist from that real
data — it never decides on its own whether to check a source, so it can't
skip one and improvise plausible-looking content instead. The full checklist
is always saved for lydia briefing show.
briefing schedule enable works on macOS (via launchd) and Linux (via asystemd --user service + timer); it isn't supported on Windows yet. On a
scheduled run, --notify (on by default when scheduled) fires a short
desktop notification on macOS via osascript — Linux scheduling works, but
the notification itself is macOS-only for now.
Automations
Create scheduled tasks in plain English and let the model run them on a timer.
Automations are recipes stored as JSON under .lydia/automations/, each with a
trigger (time-of-day or an event like new email), a step the model takes (like
"check my email and Canvas and send me a briefing"), and a notification style
(always, if_important, or never).
lydia automate "every morning at 8, check my email and canvas and send me a briefing"
# Lydia echoes the automation it parsed, you confirm, it saves the recipe.
lydia automations run morning-briefing # execute immediately (testing)
lydia automations schedule enable # turn on the launchd heartbeat
lydia automations list # see all recipes and their last run time
The model runs in a stripped-down mode for automations — only the tasks
you've defined, no interactive chat — so executions are deterministic and
(mostly) fast. Notifications go to macOS via ntfy (requires lydia auth login ntfy
first), or can be sent to a webhook endpoint if configured. A heartbeat process
runs every 5 minutes (configurable) to check if any automations are due; it
catches up on wake from sleep, so you won't miss a scheduled run.
Limitations
The if_important notification filter uses model judgment to decide whether to
alert you — it reads untrusted content (email bodies, assignment descriptions)
and makes a filtering decision. A carefully crafted email could in principle
convince the model to suppress or mislabel an alert. Critical alerts should
use when: always instead; for everything else, if_important is a useful
convenience filter that doesn't block important information.
Voice mode
An always-listening voice assistant — say the wake word, ask a question, and
hear a spoken reply. Fully local: openWakeWord for the wake word, Whisper
(faster-whisper) for transcription, macOS say for the voice.
By voice, Lydia can check email, Canvas, your macOS Calendar, weather (free
Open-Meteo, auto-located), stocks, and news; find and read files; open apps
and files ("open Spotify"); and send phone notifications. Voice can never
edit files or run shell commands.
lydia listen # start listening in the foreground (Ctrl-C stops)
lydia listen enable # start at login (launchd) and keep running in the background
lydia listen disable # stop the background listener
lydia listen status # check if background listening is enabled
Setup: The first time you run lydia listen, macOS will prompt for
microphone permission (grant it), and the Whisper model (~150MB) will download
automatically. The first calendar question triggers a one-time Calendar
automation permission prompt too. Config keys (lydia config set ...):voice_wake_word — an openWakeWord model name (default hey_jarvis) or a
path to a custom-trained .onnx model; voice_model — a small tool-calling
model for fast spoken replies (e.g. qwen3.5:4b); voice_tts_voice — a
macOS voice name from say -v '?'; weather_location — fixed place name,
or leave unset to auto-detect from IP.
lydia listen enable runs the assistant as a launchd agent, so it survives
logout/login and crash restarts. Logs go to ~/.lydia/listen.log. Note that
always-listening has a non-trivial battery cost on laptops; disable it when you
don't need it with lydia listen disable.
Memory
Lydia keeps two different kinds of history, deliberately separate:
- Session transcripts (
.lydia/history/*.jsonl) — a full append-only log
of every conversation, one file per session. Useful for debugging, not fed
back into future conversations, and git-ignored. - Remembered facts (
.lydia/memory.json) — a short, curated list of
things worth persisting across sessions (tech stack, conventions,
decisions), added either by you (/remember <fact>in chat, orlydia memory add <fact>) or by the model itself via theremembertool when
you tell it something worth keeping. These are folded into the system
prompt on every session, so Lydia actually remembers them next time you
open the project — and unlike history, this file is meant to be committed.
Architecture
lydia-cli/
├── src/lydia/
│ ├── cli/ Typer commands, the chat REPL, Rich rendering
│ ├── agent/ system prompt, tool registry, the plan→call→observe→respond loop
│ ├── tools/ pure functions: filesystem, terminal, git — no UI/agent knowledge
│ ├── connectors/ Gmail/Outlook/Canvas/stocks/AI news — same purity contract as tools/
│ ├── llm/ ModelClient protocol + two implementations: OllamaClient
│ │ (local daemon) and RemoteClient (a Lydia Server)
│ ├── context/ repository scanner + semantic search index
│ ├── database/ SQLite storage for the semantic index
│ ├── automations/ automation recipes: parser, store, launchd-driven runner
│ ├── voice/ always-listening assistant: wake word, STT, TTS, audio I/O
│ └── config/ layered JSON settings + OS-keychain-backed secrets
│
└── server/ optional: FastAPI inference proxy for a remote/GPU
Ollama — see "Running Lydia Server" above. Tool
execution never happens here; only inference does.
agent/, tools/, context/ etc. type-hint against llm.protocol.ModelClient,
not a concrete client class — this is what lets lydia talk to either a
local Ollama daemon or a remote Lydia Server with zero code changes
anywhere except llm/factory.py::build_client, which picks based on
whether server_url is configured.
See CLAUDE.md for the layering rules and the non-obvious integration
details (how thinking-model output and tool calls are actually shaped in
Ollama's streaming API, why tools/ never imports agent/ or cli/, etc.)
— written for an AI coding assistant picking this project back up, but
useful for a human too.
Development
.venv/bin/pytest # CLI suite (376 tests, no Ollama required)
.venv/bin/pytest tests/test_agent_loop.py # one file
.venv/bin/pytest tests/test_agent_loop.py::test_tool_call_then_final_answer # one test
cd server && ../.venv/bin/pytest # server suite (14 tests, no Ollama required)
All tests are hermetic — the LLM client is tested againsthttpx.MockTransport, git/filesystem tools run against a real throwaway
repo in tmp_path, the server is tested against a fake ModelClient
double. None of them require a running Ollama daemon.
Roadmap
Milestones 1 (core CLI), 2 (semantic retrieval), 3 (agent loop, tool
calling, git workflows), 5 (the automations engine), 6 (persistent project
memory), the client/server split (server/, remote inference over
Tailscale), the personal-assistant layer (Gmail/Outlook/Canvas/stocks/AI
news, scheduled briefings), voice mode, session modes (plan/ask/auto), and
parity-level editing (edit_file, multi_edit_file, auto-verify,update_todos, find_files) are all done.
See ROADMAP.md for what's left — packaging, and the M7
plugins stretch goal, plus deferred server work (real multi-user token
storage, a task queue, non-Ollama providers) that the current design
doesn't block but doesn't need yet either.
License
MIT — see LICENSE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found