mindfork 0.10.2

A terminal AI chat written in Rust: local models via llama.cpp or OpenAI, Anthropic, Gemini and Grok in the cloud, with persistent memory, notes, RAG and tools.
[package]
# The crate is published as `mindfork`. The GitHub repository stays
# `mindfork-rs`, and so do the Linux package, `/usr/lib/mindfork-rs/`, the
# release assets and the data directory: there the `-rs` says "written in
# Rust" and keeps the project apart from an unrelated company of the same
# name (docs/research/binary-rename.md §10).
name = "mindfork"
version = "0.10.2"
edition = "2024"
# The one sentence that describes the app. The same words are in site/zola.toml,
# packaging/nfpm.yaml and the README masthead — one description, four places that
# have to agree (docs/research/public-documents.md N4).
description = "A terminal AI chat written in Rust: local models via llama.cpp or OpenAI, Anthropic, Gemini and Grok in the cloud, with persistent memory, notes, RAG and tools."
license = "MIT"
readme = "README.md"
homepage = "https://mindfork.io"
repository = "https://github.com/vshylov/mindfork-rs"
documentation = "https://github.com/vshylov/mindfork-rs/blob/main/docs/manual.md"
keywords = ["llm", "chat", "tui", "llama-cpp", "ai"]
categories = ["command-line-utilities", "text-processing"]
# The toolchain this is developed and gated on (rust-toolchain.toml pins the same
# version): a pin, not a floor discovered by bisecting older compilers.
rust-version = "1.96"
# What a build actually needs, measured: without this the package is 700 files
# and 26.8 MiB (9.0 MiB compressed) and carries docs/, assets/ and the website —
# none of which takes part in compiling anything.
include = [
    "src/**/*",
    "build.rs",
    "locales/*.json",
    "syntaxes/**/*",
    "dictionaries/**/*",
    # The Windows icon `build.rs` embeds into the .exe. Caught by the
    # verification build, which warned "icon not found, .exe will ship without
    # it" — a crate that installs a binary without its icon.
    "assets/mindfork.ico",
    "README.md",
    "CHANGELOG.md",
    # Compiled into the binary, not shipped beside it: the `F1` legal tabs read
    # these through `include_str!` (`shared/credits.rs`), so leaving any of them
    # out is a build failure rather than a missing document.
    "LICENSE",
    "DISCLAIMER.md",
    "PRIVACY.md",
    "docs/legal/*",
]

# The user-facing command is the brand name (`shared/credits.rs::APP_NAME`, held
# by a gate test there); the package keeps the project name. See
# docs/research/binary-rename.md.
[[bin]]
name = "mindfork"
path = "src/main.rs"

[dependencies]
ansi-to-tui = "8.0.1"
anyhow = "1.0.103"
# Clipboard (Windows + Linux). `image-data` is on for pasting an image into a message
# (spec §9.10): it pulls the `image` crate, which this project already depends on for
# preparing attached images, so the feature costs a couple of extra decoders rather than a
# new dependency tree. No other default feature is wanted.
arboard = { version = "3.4.1", default-features = false, features = ["image-data"] }
async-stream = "0.3.6"
async-trait = "0.1.89"
# Decoding audio from TTS-provider responses (Gemini returns PCM as base64
# inside `inlineData.data`). Already in the graph transitively; the version is pinned to the one
# in Cargo.lock, so as not to spawn a duplicate (precedent — quick-xml). See shared/tts.
base64 = "0.23.1"
bytemuck = "1.25.0"
# AEAD encryption of stored API keys on Linux (the `machine-key-v1` scheme: the key
# is derived from /etc/machine-id via HKDF). RustCrypto, pure Rust. On Windows
# the same task is solved by the system's DPAPI (windows-sys below). See shared/secrets.rs,
# docs/research/api-key-storage.md.
chacha20poly1305 = "0.10.1"
# Guessing the encoding of a fetched page that declares none usable (`fetch_url`,
# `web_search`'s result pages): the detector Firefox runs on unlabelled pages, by the
# encoding_rs author. Pure Rust. See shared/http_text.rs, docs/research/page-charset.md.
chardetng = "1.0.0"
chrono = { version = "0.4.45", features = ["serde"] }
crossterm = "0.29.0"
# Standard OS folders for non-portable data-storage mode
# (Windows %APPDATA%\mindfork-rs, Linux ~/.local/share/mindfork-rs). See shared/paths.rs.
directories = "6.0.0"
# Decoding a fetched page in the encoding it declares (windows-1251, KOI8-R, Shift_JIS,
# GBK, ...). reqwest's own `charset` feature left with its default features, and it only
# ever read the header — a page declaring itself in `<meta>` would stay unreadable.
# Already in the graph transitively (via pdf-extract); the version is pinned to the one in
# Cargo.lock, so as not to spawn a duplicate. See shared/http_text.rs.
encoding_rs = "0.8.35"
eventsource-stream = "0.2.3"
# Unpacking the `wasmer` archive (tar.gz) in `mindfork sandbox setup` (Phase 2 of the Python
# sandbox). flate2 — pure Rust (miniz_oxide, no C dependencies), tar — pure Rust.
flate2 = "1.0"
futures-util = "0.3.32"
# Deriving the API-key encryption key from machine-id (HKDF-SHA256; `sha2` is already present).
# 0.13 is the generation built on `digest` 0.11 — the one `sha2` 0.11 speaks; 0.12 is
# `digest` 0.10 and will not type-check against it.
# The pattern is prescribed by systemd's documentation — machine-id isn't used raw.
hkdf = "0.13.0"
# Decoding, downscaling and re-encoding images attached with `/image attach`
# (spec §9.10). `default-features = false` keeps only the decoders users actually
# drop into a chat; the encoders we emit are png and jpeg alone, because xAI accepts
# nothing else and normalizing once at attach time keeps the provider matrix uniform.
# Without it a phone photo would ride **every** turn at full size, and a webp would be
# attachable on three providers out of four. See docs/research/multimodal-images.md §5 F3.
# The unified diff the changes screen shows: baseline bytes against the file as
# it stands now, per file the assistant touched (spec §9.12). Diffing is one of
# those problems that looks like an afternoon and is not - Myers with the usual
# heuristics, grapheme-aware, and a `unified_diff` writer that already groups
# hunks. Pure Rust, no build script, and the only crate in the graph that needs
# it. See features/workspace_diff.rs.
similar = "3.2.0"

# Walking an attached project the way a developer expects: `.gitignore` (nested,
# plus the repo's exclude file), hidden entries and `.git/` itself are skipped by
# `code_list`/`code_grep`. A hand-rolled skip list gets the common cases and then
# lies about every project-specific ignore - and on a Rust checkout the difference
# is `target/`, which is most of the bytes on disk. ripgrep's own crate; pure Rust,
# and `walkdir`/`regex-automata` were already in the graph. See features/tools/code.rs.
ignore = "0.4.23"
image = { version = "0.25.9", default-features = false, features = [
    "png",
    "jpeg",
    "webp",
    "gif",
    "bmp",
] }
# Rendering ```mermaid blocks in the feed as Unicode/ASCII graphics (flowchart/sequence,
# a whitelist + a hard fallback to the source). Pure Rust (only ascii-dag is net-new);
# ≥0.56.1 is mandatory — 0.56.0 panicked/corrupted labels on Cyrillic (our upstream fix,
# leboiko/markdown-reader#29/#30). 0.57.0 closed our feature request #32 (max_width as a
# hard budget: RenderOptions::max_width_strict + Error::TooWide) — we do NOT use it,
# our post-check via display_width is more accurate (see mermaid.rs). See docs/research/mermaid-ascii-rendering.md.
mermaid-text = "0.57.0"
# Extracting text from PDFs for indexing into RAG (`/rag add`, stage B1b). Pure Rust
# (lopdf + font/CFF/CMap parsers, no C dependencies); extraction quality is
# "best effort" — a scanned/image-only PDF yields empty text, which is acceptable.
# See features/doc_extract.rs, docs/rag-sources-retrieval.md §B1b.
pdf-extract = "0.12.0"
# Decoding `%XX` in the last path segment of an image URL into the display name
# (`/image attach <url>`). Already in the graph transitively (via `url`/`reqwest`); the
# version is pinned to the one in Cargo.lock, so as not to spawn a duplicate.
# See features/image_fetch.rs.
percent-encoding = "2.3.2"
pulldown-cmark = { version = "0.13.4", default-features = false }
# Parsing `word/document.xml` inside a DOCX for indexing into RAG (`/rag add`, stage B1b).
# Ours alone now: syntect is taken with `default-features = false`, so the plist
# edge that once pinned this version is gone (`cargo tree --invert quick-xml`
# prints one edge). Kept at the version that carries the fixes for
# RUSTSEC-2026-0194 and -0195, which is why `deny.toml` no longer ignores them.
# See features/doc_extract.rs.
quick-xml = "0.42.0"
# `unstable-rendered-line-info` is on for one call: `Paragraph::line_count`, which the
# tool-confirmation popup sizes itself by. The alternative is a second implementation of
# word wrapping whose only job is to agree with ratatui's own — and where it disagreed,
# the user approved code the popup had clipped out of view. The version is pinned, so a
# change to the unstable API can only arrive with a deliberate upgrade, and the compiler
# catches it. See src/screens/chat/popups.rs.
ratatui = { version = "0.30.1", features = ["unstable-rendered-line-info"] }
# `code_grep`'s pattern (spec 9.12). Already in the graph transitively; the version
# is pinned to the one in Cargo.lock, so as not to spawn a duplicate (precedent -
# quick-xml, base64). Deliberately *not* `grep-searcher`/`grep-regex`: matching line
# by line over files this walker already opened is a dozen lines, and the two extra
# crates would buy throughput this app has no use for. See features/tools/code.rs.
regex = "1.12.4"
# Playing synthesized speech (`/tts`) within the application process: a source
# queue (`Player::append`) gives a "synthesize chunk N+1 while chunk N
# plays" pipeline and instant cancellation. `default-features = false` — only the needed
# decoders (cloud providers' raw PCM bypasses the decoder via `SamplesBuffer`). The only
# new system dependency — ALSA on Linux (`libasound2-dev` at build time).
# See docs/research/tts.md §6.
rodio = { version = "0.22.0", default-features = false, features = ["playback", "wav", "mp3"] }
reqwest = { version = "0.13.4", default-features = false, features = ["json", "stream", "rustls", "form"] }
# `serialize` — `Connection::deserialize_read_exact`: `mindfork stats <archive>` reads a
# backup's `data.db` from the zip entry straight into memory, so no decrypted copy of an
# encrypted backup's database ever lands on disk (docs/history/data-stats.md F5). A module of
# rusqlite itself; it adds no crate.
rusqlite = { version = "0.40.1", features = ["bundled", "serialize"] }
scraper = "0.27.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
# Integrity check (sha256) of downloaded sandbox assets (Phase 2). Pure Rust.
sha2 = "0.11"
single-instance = "0.3.3"
spellbook = "0.4.2"
sqlite-vec = "0.1.9"
# Detecting the language from the OS locale on a fresh install (no defaults.json/settings.json).
# Cross-platform (Windows/Linux), pure Rust. See shared/i18n::detect_os_language.
sys-locale = "0.3"
# `default-syntaxes` is off: the app loads its own dump (syntect's bundled
# syntaxes **plus** the vendored grammars, assembled by build.rs), so syntect's
# own packdump would be dead weight. `html`/`metadata` are unused, and the theme
# is built from our palette (ADR 0003), so `default-themes`/`plist-load` are not
# needed either; `yaml-load` belongs to the build script alone. Measured on the
# release binary: 20 397 056 -> 20 582 400 bytes — **+181 KiB** for 19 grammars.
# (An earlier guess that trimming these would make the binary *smaller* was
# wrong: under LTO the linker already drops syntect's unused dumps.)
syntect = { version = "5.3.0", default-features = false, features = [
    "parsing",
    "regex-onig",
    "dump-load",
] }
# Unpacking the `wasmer` tar archive (see flate2 above). Pure Rust.
tar = "0.4"
thiserror = "2.0.18"
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "sync", "process", "time", "io-util", "rt"] }
tokio-util = "0.7.18"
tracing = "0.1.44"
tracing-appender = "0.2.5"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tui-scrollview = "0.6.5"
unicode-segmentation = "1.13.3"
unicode-width = "0.2.2"
uuid = { version = "1.23.3", features = ["v4", "v5", "serde"] }
# Backup/restore of user data (a zip archive with a
# configurable deflate compression level). default-features=false disables bzip2/zstd
# (native C dependencies) — only deflate + store are needed. `aes-crypto` adds
# password-protected backups (WinZip AES-256, spec §12.3): it pulls RustCrypto
# crates only (pbkdf2/sha1/constant_time_eq on top of aes/hmac, already in the
# graph) — still no C dependency. See features/backup.rs, docs/backup-password.md.
zip = { version = "2.2.2", default-features = false, features = ["deflate", "aes-crypto"] }

# Windows specifics:
# * `Win32_System_JobObjects` — an OS-level memory limit for the Python sandbox via a Job
#   Object (`JOB_OBJECT_LIMIT_PROCESS_MEMORY`), protecting the host from OOM during a runtime
#   script. Windows only: on Unix, rlimit is unreliable with the V8 backend (it reserves a
#   large virtual address space). See ADR 0005 / features/tools/python.rs.
# * `Win32_Security_Cryptography` — DPAPI (`CryptProtectData`/`CryptUnprotectData`)
#   for machine-bound encryption of stored API keys. See shared/secrets.rs,
#   docs/research/api-key-storage.md.
# * `Win32_UI_Input_KeyboardAndMouse` + `Win32_UI_WindowsAndMessaging` — resolving a
#   character back to its physical key through the active/installed keyboard layouts
#   (`VkKeyScanExW`/`MapVirtualKeyExW`), so `Ctrl+<letter>` shortcuts work under any
#   layout without a per-language table. See shared/keys.rs,
#   docs/research/layout-independent-hotkeys.md.
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_System_JobObjects", "Win32_Foundation", "Win32_Security", "Win32_Security_Cryptography", "Win32_System_Console", "Win32_System_Threading", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }

# `killpg` — ending a spawned process **tree** on unix (the code workspace's
# build/run/test commands: `cargo build` outlives its own kill through the
# `rustc` children it spawned). The Windows half of the same job is a Job Object
# from `windows-sys` above; unix needs one libc call, so the dependency is one
# call wide and unix-only. See shared/proc.rs, docs/code-workspace.md §3.3.
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[build-dependencies]
# Assembling the vendored syntax grammars into a dump (see build.rs and
# syntaxes/SOURCES.md). The regex engine must be the **same as the runtime's**
# (`regex-onig`): loading a grammar compiles its regexes, so the build is also
# what validates them — and the two engines disagree. Measured: with
# `regex-fancy` the Swift grammar is rejected over a subroutine call (`\g<1>`),
# which oniguruma accepts and the app therefore highlights fine. Validating with
# a different engine than the one that runs would reject working grammars and,
# worse, could accept broken ones.
syntect = { version = "5.3.0", default-features = false, features = [
    "parsing",
    "default-syntaxes",
    "regex-onig",
    "yaml-load",
    "dump-create",
    "dump-load",
] }

[target.'cfg(windows)'.build-dependencies]
winresource = "0.1.31"

[dev-dependencies]
tempfile = "3.27.0"
# `test-util` enables pausing/fast-forwarding tokio's virtual time in tests
# (`start_paused`) — deterministic tests of the server-restart debounce without
# real waiting. Test builds only (feature unification dev-deps).
tokio = { version = "1.52.3", features = ["test-util"] }

# The release profile (Windows + Linux): size/speed optimization.
# panic=unwind is kept deliberately — drop guards/a panic hook are needed to restore
# the terminal on a panic (ratatui::init installs the hook; M0).
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true