Mimir

Architecture

For contributors: how the pieces fit together.

Crates #

Crate What it is
crates/mimir-core Everything that isn't a UI shell: the database, bots, providers, tools, channels, routines, meetings
crates/mimir The CLI and the self-hosted server (server.rs: HTTP API + web UI)
crates/mimir-desktop The Tauri desktop app: window, tray, keychain, quick ask, notifications, meeting recorder
ui/ React + Vite UI, shared by the desktop app and the server's web UI
site/ The heymimir.app website (Vite, three.js WebGPU), including /docs/ built from docs/*.md: cd site && npm install && npm run dev; npm run build writes site/dist

mimir-core modules #

Module Does
db SQLite (one file): memories with FTS5 search and versions, bots, chats, messages (FTS5 too, for chat_search), routines, runs, tool calls, approvals, rules, suggestions, skills (versions, uses, proposals by kind), meetings
host Host::call(cmd, args): every command the UI sends; starts and stops each bot's runtime; the secret store
runtime Per-bot channel listeners (Telegram, Slack, Discord), the 30 s routine scheduler, approval cards, the daily digest
telegram, slack, discord The messaging apps (outbound connections, streaming replies)
provider AI accounts: starts claude -p / codex exec / grok -p, or calls an OpenAI-compatible API; streams replies
lib.rs (turn) One run: builds the instructions (role, standing memory, skills), calls the provider, records the run; /skill commands (skill_command)
tools Tool definitions, the allow / ask / deny gate, taint, plans, and the built-in tools' code
mcp The MCP server the CLIs start for each run (<app> mcp --bot B --chat C --run R, plus --read-only for quiet checks, --no-tools, --depth for hand-offs), exposing Mimir's tools
ext Tool servers (MCP clients over stdio and Streamable HTTP), the browser, saved logins
tidy The daily memory tidy-up (on tidy_model if set): corrections, skills from good runs, fixes for skills that went wrong, weekly upkeep (retire / merge / move to background), the weekly review; teaching a skill from a source or a SKILL.md
computer The bots' computer (Xvfb, xdotool, scrot, Chromium), self-hosted Linux only; recording a showing for teach by showing (page details through Chromium's DevTools port)
transcribe Speech to text (whisper.cpp built in, or whisper-cli, or an API), voice activity, speaker separation
calendar iCal links: parsing, recurrence (rrule), the meeting on now, events between dates (calendar_list)
email IMAP (async-imap, mail-parser): search, read, drafts by APPEND, all EXAMINE/PEEK; SMTP sending (lettre); every operation within 60 s
files The per-chat file workspace: path checks, versions under .history/, outside-content marks keyed by canonical name and modification time
library Mimir's skill library: the SKILL.md files in crates/mimir-core/skills/, compiled in, with what each needs and a suggested schedule
fmt, live, openrouter Message formatting per app, "what it's doing now", OpenRouter sign-in

How a message becomes a reply #

  1. A channel listener (or the UI's send_local) receives a message for a bot and chat.
  2. turn builds the instructions: the bot's role, standing memory, accepted skills, the conversation.
  3. provider runs the bot's AI account. For the CLIs, it passes MCP arguments so the CLI starts <app> mcp … and sees Mimir's tools; for APIs, tools are function definitions handled in-process.
  4. Each tool call goes through tools::call: the allow / ask / deny rule, taint (outside content read in this conversation), the bot's allowlist, then the tool itself. "Ask" creates an approval and waits for your answer from any app.
  5. The reply streams back to where you asked; the run and every tool call are recorded.

Desktop and server #

Both run the same Host. The desktop calls it in-process through one Tauri command (api); the server exposes it as POST /api/<cmd> with a bearer token. The UI's invoke() goes to whichever is active, so the same React app is the desktop UI and the web UI. A few commands are handled by the desktop itself, before the host, because they belong to this computer: meeting (recording, the meetings list), keep_awake, quick_shortcut, selftest.

Meetings #

mimir-desktop/src/meeting.rs records (cpal for the microphone; a Core Audio process tap on macOS 14.4+, else ScreenCaptureKit / WASAPI loopback / parec for the computer's sound), chunks at pauses, and calls transcribe (whisper.cpp through whisper-rs, with Silero VAD run separately because whisper-rs's per-state call skips whisper.cpp's own). After Stop, speaker separation runs through sherpa-rs (feature diarize, linked in, macOS and Windows) or, on Linux, sherpa-onnx's shared libraries downloaded on first use and loaded at runtime (feature diarize-dl: transcribe::sherpa_dl, structs mirroring its C API). Call detection (mic_in_use) is per OS. See Meetings.

Build notes:

Learning #

Bots can't be fine-tuned (they run on your own subscription), so they improve only through memory and skills, and everything learned is a proposal you accept, with its provenance recorded. Signals are objective (corrections, stops, denials, failed actions), never the model judging its own success. Nothing learned can change permissions, tool grants, saved logins or schedules. See Memory for the user-facing side, and reports/ (local, not published) for the research behind it.

Security rules in the code #

The Security model in terms of where it lives. Keep these when changing the code.

Data #

One SQLite file in the data folder (~/.mimir, or MIMIR_HOME). Schema changes are additive (add_col in Db::open), so an older database opens in a newer build. Installs from before the rename to Mimir (~/.botpoc, botpoc.db) are moved over once on startup.

Environment variables #

Variable
MIMIR_HOME Data folder (default ~/.mimir)
MIMIR_PROVIDER, MIMIR_MODEL CLI bot: claude (default), codex, grok, openrouter, xai, local, openai; model override
OPENROUTER_API_KEY, XAI_API_KEY, OPENAI_API_KEY, OPENAI_BASE_URL Keys for the CLI bot; OPENAI_BASE_URL also points local elsewhere (LM Studio: http://localhost:1234/v1)
TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN + SLACK_APP_TOKEN, DISCORD_BOT_TOKEN Messaging apps for mimir serve
MIMIR_BOT Which bot the CLI acts as (default 1)
MIMIR_ADMIN_TOKEN Server: a fixed admin token, at least 24 characters (else one is generated on first run)
MIMIR_UI_DIR Server: the web UI folder (default ui/dist or /app/ui)
MIMIR_COMPUTER, MIMIR_COMPUTER_TOKEN Server: the bots' computer as a separate service (computer:7300, mimir computer) and the control channel's secret (24+ characters, on both sides)
MIMIR_TELEGRAM_API Telegram Bot API base URL (a self-hosted Bot API server, or the test mock)
MIMIR_CLAUDE_BIN, MIMIR_CODEX_BIN, MIMIR_GROK_BIN, MIMIR_NPX Paths to the CLIs, if not on PATH

Releases #

.github/workflows/release.yml builds the installers when a version tag is pushed:

  1. Set version in crates/mimir-desktop/tauri.conf.json (the tag must match it; the workflow checks).
  2. git tag v0.1.0 && git push origin v0.1.0.
  3. The workflow drafts a GitHub release and attaches the .dmg, .exe/.msi, .deb/.rpm and .AppImage (macOS on Apple silicon; Linux built on Ubuntu 22.04 so it runs on older systems).
  4. Read the draft over, paste the "Unreleased" section of CHANGELOG.md into its notes (installed apps show them with the update), rename that section to the version, and publish.

The Mac app is ad-hoc signed (signingIdentity: "-"), which Apple silicon needs to open a downloaded app at all; it isn't notarized, and Windows builds aren't signed.

Updates (crates/mimir-desktop/src/update.rs, Tauri's updater plugin). Apps read latest.json from the newest published release (plugins.updater.endpoints in tauri.conf.json; drafts aren't served), so publishing the draft is what ships an update. With the TAURI_SIGNING_PRIVATE_KEY and TAURI_SIGNING_PRIVATE_KEY_PASSWORD secrets set, each build signs its update files (.app.tar.gz, the installers) and a last job writes latest.json from those signatures; without them, releases still build but installed apps don't see them. Apps refuse anything not signed by the private key whose public half is in tauri.conf.json: losing that key means users must reinstall by hand once, so keep a copy somewhere safe. End-to-end test on a Mac: build 0.1.1 signed and 0.1.0 pointed at a local server (--config '{"plugins":{"updater":{"endpoints":["http://127.0.0.1:8899/latest.json"],"dangerousInsecureTransportProtocol":true}}}'), serve the first, and start the second with MIMIR_UPDATE_TEST=1: it installs and restarts as 0.1.1.

Tests #

cargo test --workspace          # unit and integration tests (needs cmake for the desktop crate)
cd ui && npx tsc -b             # the UI typecheck (`tsc -p .` checks nothing here)

Test hooks and opt-in tests: