Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
SAYA CLI
SAYA CLI is an open-source, terminal-native shell for a database-aware AI agent. Ask questions about your data in plain language and it discovers schema and runs bounded, read-only SQL against PostgreSQL, MySQL, SQLite, DuckDB, or Snowflake.
An interactive terminal (TTY) launches a full-screen TUI β a scrolling
transcript, a bottom-pinned input box, a slash-command popup that opens on /,
@table schema autocomplete, live streaming answers, and an approval prompt
before any query runs. Piped/non-TTY input uses a headless executor for scripts
and CI, with text/JSON/NDJSON output. Providers: Ollama, OpenAI,
OpenAI-compatible gateways, Anthropic, and Gemini. Sessions are redacted before
they are persisted, and multi-database navigation lets the agent query several
connected databases at once.
Features
- π₯οΈ Full-screen TUI β bottom-pinned input, scrolling transcript, live
streaming answers,
/command popup with fuzzy matching, and@tableschema autocomplete. Copy out withCtrl+O(selection mode),Ctrl+Y(last answer), orCtrl+B(whole transcript). - π‘οΈ Safe by default β every query is bounded and read-only, and the exact SQL is shown in the approval prompt (and echoed to the transcript) before it runs; sessions are redacted before being persisted.
- π§ Memory β tell it what your tables mean once (
saya contracts remember orders --kind time-column --value created_at) and later questions carry that context. Facts are typed, bound to a table and its schema shape, and go stale when a column they depend on changes. It never confirms anything by itself, never picks between contradictions, and is off by default. Shareable via.saya/contracts/in your repo. β docs/memory.md - π Databases β PostgreSQL, MySQL, SQLite, DuckDB, Snowflake; query several connected databases at once, or run one query across every connected database and get per-database results side by side.
- π€ Providers β Ollama, OpenAI, OpenAI-compatible gateways, Anthropic, Gemini; configurable model and temperature.
- βοΈ Scriptable β piped/non-TTY input runs headless with text/JSON/NDJSON output for scripts and CI.
Demo
Ask in plain language β saya discovers the schema, runs bounded read-only SQL, and streams the answer (here against a PostgreSQL then a MySQL database):

Query across databases at once β connect a second database and compare them in a single question:

The command popup, fuzzy matching, and help overlay:

The GIFs are generated with vhs from the
docs/*.tape scripts (the live ones need SAYA_API_KEY and reachable databases
in the environment).
Install
Prebuilt binaries for macOS (Apple Silicon + Intel), Linux (x86_64), and Windows (x86_64) are attached to every release.
# Homebrew (macOS / Linux)
# Cargo β prebuilt binary, no compile (cargo-bins.github.io/cargo-binstall)
# Cargo β from source (compiles the bundled DuckDB, so allow a few minutes)
Or download the archive for your platform from the
releases page, verify it
against SHA256SUMS, and put the saya binary on your PATH. See
installation for details.
Quick start
The five-minute path is: build from source, initialize the credential-free
.saya/ templates, set the environment SecretRef used by the example profile,
then test a bounded read-only query. Running saya without a subcommand starts
the REPL; use /help for interactive commands. The automation surface is
available as saya ask, saya query, saya config, and saya connection.
See installation for source install details and the
current crates.io/Homebrew boundary.
For complete connection-profile, dotenv, query, and cross-database examples, see the database query guide.
Configuration
The canonical files are TOML:
.saya/config.toml
.saya/connections.toml
~/.config/saya/config.toml
~/.config/saya/connections.toml
Session files default to the platform user-data directory: SAYA_SESSION_DIR
if set, then $XDG_DATA_HOME/saya/sessions, %APPDATA%/saya/sessions, or
~/.local/share/saya/sessions. The override is useful for tests and CI.
Use --config and --connections for explicit paths. Use --env-file to opt
into a dotenv-style file; .env is never loaded automatically. Process
environment values override explicit env-file values. Store only secret
references such as { env = "SAYA_ANALYTICS_PASSWORD" }, never passwords or
API keys, in committed files. See configuration and
connections.
For a local Ollama setup, use a config file plus an explicit env file:
# .saya/config.toml
[]
= "ollama"
= "qwen2.5-coder:14b"
= "http://localhost:11434"
# .env.saya (do not commit)
SAYA_PROVIDER=ollama
SAYA_MODEL=qwen2.5-coder:14b
SAYA_PROVIDER_BASE_URL=http://localhost:11434
For an OpenAI-compatible service, use a runtime-only API-key reference:
[]
= "openai_compatible"
= "your-model"
= "https://api.example.test/v1"
= { = "SAYA_API_KEY" }
SAYA_API_KEY=replace-me
The connection file remains separate:
[]
= "postgresql"
= "localhost"
= 5432
= "warehouse"
= "saya_readonly"
= { = "SAYA_ANALYTICS_PASSWORD" }
= "require"
saya config init refuses to overwrite either project file and makes a
best-effort rollback after an ordinary creation error; it is not crash-atomic.
It emits one stable result event in text, JSON, or NDJSON. The generated
templates contain SecretRefs only; they never contain credentials.
MySQL uses the same SecretRef password pattern. Its safe default is
verify-identity; use disable only for an explicitly local development
server. Supported modes are disable, prefer, require, verify-ca, and
verify-identity:
[]
= "mysql"
= "localhost"
= 3306
= "warehouse"
= "saya_readonly"
= { = "SAYA_MYSQL_PASSWORD" }
= "verify-identity"
# ssl_ca = { file = "/etc/ssl/certs/mysql-ca.pem" }
Snowflake accounts use an account identifier such as xy12345 or
org-account.us-east-1.aws, not a URL. Key-pair, password, and interactive
browser authentication are supported:
[]
= "snowflake"
= "org-account.us-east-1.aws"
= "jane"
= "keypair"
= { = "/absolute/path/to/rsa_key.p8" }
= { = "SAYA_SNOWFLAKE_PASSPHRASE" }
= "ANALYTICS"
= "PROD"
= "PUBLIC"
= "ANALYST"
[]
= "snowflake"
= "org-account.us-east-1.aws"
= "jane"
= "userpass"
= { = "SAYA_SNOWFLAKE_PASSWORD" }
[]
= "snowflake"
= "org-account.us-east-1.aws"
= "jane"
= "externalbrowser"
File SecretRef paths are literal strings: ~ and environment variables are
not expanded. The current runtime supports env and file SecretRefs;
{ keyring = "..." } is a reserved shape and currently reports unavailable.
For an environment-only profile, use an explicit env file or process
environment with SAYA_DB_TYPE, SAYA_DB_ACCOUNT, SAYA_DB_USER, and
SAYA_DB_AUTH_TYPE, plus SAYA_DB_PRIVATE_KEY for keypair or
SAYA_DB_PASSWORD for userpass. Process environment overrides --env-file.
SAYA_DB_PRIVATE_KEY is raw PEM content. Because env files are line-oriented
and literal \n is not converted to a newline, put keypair PEM in a
connections.toml file SecretRef such as { file = "/absolute/path/to/rsa_key.p8" },
or provide raw multiline PEM through a process environment that preserves it.
Browser authentication requires an interactive TTY and opens a system browser;
it fails before binding, network, or browser launch with --non-interactive or
piped input, and the localhost callback expires after 120 seconds.
For a file-backed DuckDB profile, set read_only explicitly. :memory: may
omit it. DuckDB external access, extension autoloading, community extensions,
and persistent secrets are locked off by the CLI:
[]
= "duckdb"
= "./data/warehouse.duckdb"
= true
A file-backed SQLite profile follows the same shape; read_only defaults to
true and opens the file with PRAGMA query_only = ON (an in-memory :memory:
path is rejected β point it at a file):
[]
= "sqlite"
= "./data/warehouse.sqlite3"
= true
Run saya --env-file .env.saya --connections .saya/connections.toml --approval-mode read-only ask "show revenue". The newer provider env names
(SAYA_PROVIDER, SAYA_MODEL, SAYA_PROVIDER_BASE_URL, SAYA_API_KEY) have
the same precedence as the established SAYA_AI_* aliases.
Schema discovery is cached in a private local SQLite state database. Live
authentication is always attempted first; stale fallback is explicitly marked,
and connection schema --refresh or interactive /schema refresh invalidates
before discovery. Set SAYA_STATE_DB to override the platform data path.
--non-interactive is valid for Snowflake keypair and userpass profiles, but
not for externalbrowser, which requires an interactive TTY.
Privacy and limitations
The intended MVP policy is read-only, bounded queries with cloud row sharing
disabled. PostgreSQL, MySQL, SQLite, DuckDB, and Snowflake reject parse failures, writes, DDL, transaction/control
statements, and multi-statements before execution. Results are bounded by both a
row cap (one extra row is observed to mark truncation) and byte budgets β a 1 MiB
per-cell cap and a 16 MiB total-result cap β and truncated is set when either
limit is reached. Schema discovery is auto-allowed; bounded SQL is
auto-approved only with read-only, denied with never, and explicitly
confirmed per query with ask. A non-TTY ask request is denied safely.
OpenAI, OpenAI-compatible, Anthropic, and Gemini providers are treated as
cloud: when sharing is disabled, they receive schema metadata but not SQL tools
or row data. Ollama is treated as local for this MVP. /privacy, /model,
/provider, and /connect apply to the next interactive prompt. /include
(and --include-profile) connect additional read-only databases, and the agent
navigates between all connected databases by passing an optional connection
argument to its schema and query tools; the primary database is the default.
Fully offline agent use and release signing are not implemented. Provider
execution covers Ollama, OpenAI, OpenAI-compatible endpoints, Anthropic, and
Gemini.
saya also enforces read-only at the database session level (PostgreSQL
default_transaction_read_only, MySQL transaction_read_only, SQLite
query_only, and a read-only DuckDB open) on top of the AST checks. Because AST
checks cannot prove that an arbitrary database function is side-effect free β and
Snowflake has no equivalent session switch β connect with a least-privilege,
read-only database role and give DuckDB/SQLite file paths least-privilege
filesystem permissions.
Resolved config secrets, provider headers, and raw query rows are structurally
excluded from session files. Known credential-shaped text is redacted, but
redaction cannot identify every arbitrary secretβnever paste credentials into
prompts. See SECURITY.md.
Unavailable or failed connection/schema operations return 3, while safety and
query failures return 4; provider/agent failures return 5. Non-interactive
mode defaults to never approval (schema-only) unless --approval-mode is
explicit; interactive sessions default to ask. This MVP streams token
deltas from Ollama, OpenAI, OpenAI-compatible, and Anthropic chat providers, and returns Gemini
responses as a single buffered reply. Text output writes
deltas as they arrive; JSON and NDJSON each write one stable JSON event envelope per delta.
Requests retry retryable connection setup failures, HTTP 429, and 5xx responses only before the
provider yields an event. A body transport failure is surfaced without retry because replaying a
partially consumed response cannot be proved action-free. Ctrl+C cancels a one-shot request with
exit code 130; during an interactive request it returns to the saya> prompt without persisting
the incomplete turn. Interactive prompts and --continue/--resume use bounded, redacted
user/assistant history, and /clear removes visible and provider context. Session files persist
provider/model/profile/privacy/approval settings and safe tool name/status metadata. Database-derived
assistant turns are persisted locally after redaction but omitted from cloud provider history when
sharing is disabled; v1 files fall back to current runtime settings. Raw tool arguments, tool
responses, credentials, headers, and raw tool-result rows are not persisted or reconstructed as
provider history. A natural-language assistant answer may still contain database values.
Development
See CONTRIBUTING.md. The project is Apache-2.0 licensed.