saya-cli 0.3.1

Database-aware AI agent for the terminal: full-screen TUI, schema discovery, and bounded read-only SQL over PostgreSQL, MySQL, SQLite, DuckDB, and Snowflake.
docs.rs failed to build saya-cli-0.3.1
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.
Visit the last successful build: saya-cli-0.3.0

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 @table schema autocomplete. Copy out with Ctrl+O (selection mode), Ctrl+Y (last answer), or Ctrl+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):

saya live demo

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

saya cross-database demo

The command popup, fuzzy matching, and help overlay:

saya TUI demo

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)
brew install databook-studio/tap/saya

# Cargo β€” prebuilt binary, no compile (cargo-bins.github.io/cargo-binstall)
cargo binstall saya-cli

# Cargo β€” from source (compiles the bundled DuckDB, so allow a few minutes)
cargo install saya-cli

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

cargo build --release --locked -p saya-cli
./target/release/saya config init
export SAYA_ANALYTICS_PASSWORD='use-a-read-only-password'
./target/release/saya config doctor
./target/release/saya connection test analytics
./target/release/saya --profile analytics --approval-mode read-only query --sql 'SELECT 1'

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
[ai]
provider = "ollama"
model = "qwen2.5-coder:14b"
base_url = "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:

[ai]
provider = "openai_compatible"
model = "your-model"
base_url = "https://api.example.test/v1"
api_key = { env = "SAYA_API_KEY" }
SAYA_API_KEY=replace-me

The connection file remains separate:

[profiles.analytics]
type = "postgresql"
host = "localhost"
port = 5432
database = "warehouse"
user = "saya_readonly"
password = { env = "SAYA_ANALYTICS_PASSWORD" }
sslmode = "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:

[profiles.mysql]
type = "mysql"
host = "localhost"
port = 3306
database = "warehouse"
user = "saya_readonly"
password = { env = "SAYA_MYSQL_PASSWORD" }
sslmode = "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:

[profiles.snowflake_keypair]
type = "snowflake"
account = "org-account.us-east-1.aws"
user = "jane"
auth_type = "keypair"
private_key = { file = "/absolute/path/to/rsa_key.p8" }
passphrase = { env = "SAYA_SNOWFLAKE_PASSPHRASE" }
warehouse = "ANALYTICS"
database = "PROD"
schema = "PUBLIC"
role = "ANALYST"

[profiles.snowflake_userpass]
type = "snowflake"
account = "org-account.us-east-1.aws"
user = "jane"
auth_type = "userpass"
password = { env = "SAYA_SNOWFLAKE_PASSWORD" }

[profiles.snowflake_browser]
type = "snowflake"
account = "org-account.us-east-1.aws"
user = "jane"
auth_type = "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:

[profiles.local]
type = "duckdb"
path = "./data/warehouse.duckdb"
read_only = 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):

[profiles.sqlite_local]
type = "sqlite"
path = "./data/warehouse.sqlite3"
read_only = 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.

saya config doctor
saya config show --format json
saya connection test analytics --connections examples/connections.toml
saya connection schema analytics --connections examples/connections.toml
saya --non-interactive connection test snowflake_keypair \
  --connections examples/connections.toml
saya --non-interactive connection schema snowflake_keypair \
  --connections examples/connections.toml
saya --non-interactive --env-file .env.snowflake \
  --profile snowflake_userpass connection test snowflake_userpass
saya --profile snowflake_browser connection test snowflake_browser
saya query --profile analytics --sql "SELECT current_database()"
saya --non-interactive --profile snowflake_keypair query \
  --sql "SELECT CURRENT_DATABASE()"
saya --profile snowflake_browser --approval-mode read-only ask \
  "summarize the selected schema"
saya connection test local --connections examples/connections.toml
saya query --profile local --connections examples/connections.toml --sql "SELECT 1"
saya --profile local --approval-mode read-only ask "summarize the local schema"

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

cargo fmt --check
cargo test --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings

See CONTRIBUTING.md. The project is Apache-2.0 licensed.