ask is a Rust CLI that allows users to interact with AI models using natural language without quotes. The architecture follows a modular design with clear separation of concerns.
ask/
├── src/
│ ├── main.rs # Entry point
│ ├── http.rs # Custom DNS and HTTP client setup
│ ├── completions.rs # Shell completions generation
│ ├── cli/
│ │ ├── mod.rs # CLI execution logic
│ │ └── parser.rs # Flexible argument parsing
│ ├── config/
│ │ ├── mod.rs # Config structs and init_config()
│ │ ├── loader.rs # TOML config loading hierarchy
│ │ ├── defaults.rs # Default constants
│ │ └── thinking.rs # Thinking mode configuration helpers
│ ├── providers/
│ │ ├── mod.rs # Provider factory
│ │ ├── traits.rs # Provider trait + PromptContext
│ │ ├── gemini.rs # Google Gemini integration
│ │ ├── openai.rs # OpenAI integration
│ │ ├── anthropic.rs # Anthropic Claude integration
│ │ └── ollama.rs # Ollama local model integration (native /api/chat NDJSON)
│ ├── context/
│ │ ├── mod.rs # Module exports
│ │ ├── storage.rs # JSON file storage
│ │ └── manager.rs # Context lifecycle management
│ ├── executor/
│ │ ├── mod.rs # Module exports
│ │ ├── safety.rs # Destructive command detection
│ │ ├── runner.rs # Command execution
│ │ └── injector.rs # Terminal command injection (tmux/screen/clipboard)
│ ├── output/
│ │ ├── mod.rs # Module exports
│ │ ├── formatter.rs # Output formatting (JSON, raw, markdown)
│ │ ├── markdown.rs # Terminal markdown rendering
│ │ ├── colorize.rs # Color scheme utilities
│ │ └── spinner.rs # Loading indicator (● blinking/streaming)
│ └── update/
│ └── mod.rs # Auto-update from GitHub releases
├── tests/
│ ├── integration_test.rs # CLI integration tests
│ └── fixtures/ # Test fixtures
├── .github/
│ └── workflows/
│ ├── ci.yml # CI/CD pipeline (lint, test, build, release)
│ └── test.yml # Tests
├── Makefile # Development commands (precommit, fmt, clippy, test, audit)
├── CLAUDE.md # Claude AI assistant instructions
├── GEMINI.md # Gemini AI assistant instructions
├── install.sh # Unix installation script
├── install.ps1 # Windows installation script
├── Cargo.toml # Rust dependencies
├── LICENSE # AGPL-3.0
├── README.md # User documentation
├── ROADMAP.md # Development roadmap
├── ADR.md # Architecture decisions
└── CODEBASE.md # This file
Implements flexible argument parsing that allows flags before or after free text:
pub struct Args {
pub context: Option<u64>, // -c, --context[=MIN]
pub command_mode: Option<bool>, // -x, --command / --question
pub yes: Option<bool>, // -y, --yes / --confirm
pub model: Option<String>,
pub provider: Option<String>, // -P, --provider
pub profile: Option<String>, // -p, --profile
pub think: Option<bool>, // -t, --think / --no-think
pub think_level: Option<String>, // thinking level/budget/effort
pub json: bool, // --json
pub markdown: Option<bool>, // --markdown / --no-markdown
pub raw: bool, // --raw
pub color: Option<bool>, // --color / --no-color
pub stream: Option<bool>, // --stream / --no-stream
pub search: Option<bool>, // -s, --search / --no-search
pub citations: Option<bool>, // --citations / --no-citations
pub verbose: bool, // -v, --verbose
pub list_profiles: bool, // profiles subcommand
pub make_config: bool, // --make-config
pub query: Vec<String>, // Free text parts
// ...
}The parser:
- Expands aliases from config before parsing
- Iterates through arguments
- Identifies flags (starting with
-) - Collects remaining text as the query
- Handles combined short flags (
-cy)
Configuration is loaded with precedence (Profile-Only Architecture):
- CLI arguments (highest):
-p,-P,-m,-k - Environment variables:
ASK_PROFILE,ASK_PROVIDER,ASK_MODEL,ASK_*_API_KEY - Project local config (recursive search upwards for
ask.tomlor.ask.toml) - Home config (
~/ask.toml- legacy, still supported) - XDG config (
~/.config/ask/ask.toml- recommended for new installs) - Hardcoded defaults (lowest)
- Built-in free profiles (
talker,coder,vision,faster) are injected if missing; default isfasterwhen no user profiles exist
Key structures:
Config- Main config container with profiles, behavior, context, update, commands, aliasesActiveConfig- Runtime-resolved config (provider, model, api_key, base_url, stream, profile_name)ProfileConfig- Named profile settings (provider, model, api_key, base_url, stream, fallback, thinking settings, web search)fallback: Profile failover behavior —"any"(first alphabetically-sorted other profile),"none"(disable fallback), or a specific profile name
FreeProfileDef- Static definition for built-in free profiles (name, provider, model, api_key, base_url)FREE_PROFILES- Array of all 4 built-in free profile definitions (talker, coder, vision, faster)FREE_PROFILE_NAMES- Array of free profile name strings for lookup/filteringBehaviorConfig- Execution behavior settingsContextConfig- Context/history settingsConfigManager- Internal helper for interactive config managementget_any_str()- Retrieve any TOML value as a String (handles Integers/Booleans)- Safe-by-Default Navigation: Menu defaults to "Back"/"Exit" after actions
- Smart Persistence: Pre-selects existing values when editing profiles
aliases: HashMap<String, String>- Command-line aliases
Key constants (src/config/defaults.rs):
DEFAULT_OLLAMA_BASE_URL- Default Ollama API base URL (http://localhost:11434)DEFAULT_OLLAMA_MODEL- Default Ollama model name
Key functions:
with_cli_overrides()- Resolves active config from CLI args, ENV, and profilesensure_default_profiles()- Injects all 4 built-in free profiles if missing (withfallback = "any")first_non_free_profile()- Finds the first user-defined profile (skips all free profiles)effective_default_profile()- Resolves default profile:default_profile> first user profile >fasterfree_profiles_toml()- Generates TOML string for all 4 free profiles (used by menu)any_free_profile_missing()- Checks if any of the 4 free profiles is missing from configinit_config()- Interactive configuration menuinit_config_non_interactive()- Non-interactive setup (for scripts)load_aliases_only()- Fast alias loading for early argument expansionconfigure_profile()- Configure a single profile (includes Ollama provider flow)fetch_ollama_models()- Queries local Ollama instance for available model namesmanage_profiles()- Profile management submenuget_thinking_config()- Get unified thinking settings (enabled, value); handles Ollama caseget_thinking_level()- Get thinking level (Gemini/Anthropic) from active profileget_reasoning_effort()- Get OpenAI reasoning effort from active profileget_thinking_budget()- Get thinking budget (Gemini/Anthropic) from active profileget_profile_web_search()- Check if web search is enabled for active profile
ask uses a custom HTTP client setup to ensure reliability across all platforms, including Termux/Android:
- Hickory DNS: Uses
hickory-resolverwith Cloudflare DNS (1.1.1.1) to bypass system DNS issues. - Cross-Platform: Works without
/etc/resolv.conf, making it robust for mobile environments. - Reqwest: Integrated with
reqwestfor all API calls (Gemini, OpenAI, Anthropic, GitHub).
All providers implement the Provider trait:
#[async_trait]
pub trait Provider: Send + Sync {
async fn complete(&self, messages: &[Message]) -> Result<String>;
async fn stream(&self, messages: &[Message], callback: StreamCallback) -> Result<()>;
fn name(&self) -> &str;
fn model(&self) -> &str;
}Supported providers:
- GeminiProvider: Google Gemini API (Thinking via
thinkingConfigorthinkingBudget) - OpenAIProvider: OpenAI and compatible APIs (Reasoning via
reasoning_effort) - AnthropicProvider: Anthropic Claude API (Thinking via
thinking.budget_tokens) - OllamaProvider: Local Ollama instance via native
/api/chatNDJSON streaming endpoint (Thinking via/thinktag toggle)
Thinking Mode Support (src/config/thinking.rs):
The system dynamically detects and selects the appropriate parameter for each provider/model:
- Gemini 2.5/Pro:
thinking_budget(tokens) - Gemini 3:
thinking_level(minimal, low, medium, high) - OpenAI (o1/o3):
reasoning_effort(none, low, medium, high) - Anthropic:
thinking_budget(tokens or levels) - Ollama:
ThinkingType::OllamaThink— toggles/thinktag in the system prompt to enable model-side chain-of-thought
The unified prompt system handles intent detection inline (command vs question vs code) without a separate API call. Key functions:
build_unified_prompt()- Builds the system prompt with contextload_custom_prompt()- Loads custom prompts from ask.md filesexpand_prompt_variables()- Replaces {os}, {shell}, {cwd}, {locale}, {now} variablesflatten_command_if_safe()- Flattens multiline commands into one-liners using&&(handles line continuations, heredocs, non-commands, long lines)strip_code_fences()- Removes enclosing fenced code blocks (bash ...) before flattening
Manages conversation history per directory:
- Storage: JSON files in
~/.local/share/ask/contexts/ - Key: SHA256 hash of current directory path
- Cleanup: Automatic removal after TTL expires
pub struct ContextManager {
storage: ContextStorage,
context_id: String,
max_messages: usize,
max_age_minutes: u64,
}Global History Commands (via ask history):
ask history— List all saved contexts globallyask history <TARGET>— Show specific context by ID prefix or pathask history search <TERM>— Search all contexts by path or message contentask history prune— Delete contexts whose directories no longer exist
Safe command execution with pattern-based safety detection:
Safe patterns (auto-execute):
ls,pwd,cat,grepgit status,git logdocker ps,docker images
Destructive patterns (require confirmation):
rm -rf,rm -rsudo *dd,mkfscurl | sh
Command Injection (src/executor/injector.rs):
Automatically detects the best injection method for the current environment:
pub enum InjectionMethod {
GuiPaste, // Wayland/X11/macOS/Windows with GUI
TmuxSendKeys, // Inside tmux session
ScreenStuff, // Inside GNU screen session
Fallback, // Headless terminal - enhanced prompt
}Detection order:
$TMUX→tmux send-keys -l(literal mode)$STY→screen -X stuff$DISPLAY/$WAYLAND_DISPLAY→ clipboard + paste simulation- Otherwise → enhanced fallback (visual print + editable prompt)
Handles output based on flags:
--json: Structured JSON output--raw: Plain text without formatting--markdown: Terminal markdown rendering (default)
Automatically detects piping and disables colors/formatting.
Loading Indicator (spinner.rs):
Spinner: Blinks ● (500ms on/off) while waiting for AI responseStreamingIndicator: Shows ● at end of text during streaming- Only active in terminal mode (not raw/json/piped)
Implements automatic update checking and installation:
- Background check: Spawns detached process to check GitHub releases.
- Throttling:
- Aggressive mode: Checks at most once per hour.
- Normal mode: Respects
check_interval_hours(default 24h).
- Notification: Saves update info for next run notification.
- Download: Fetches platform-specific binary from release assets
- Atomic replace: Safe binary replacement with backup
Disable with ASK_NO_UPDATE=1 environment variable.
Generates shell completions using clap_complete:
ask --completions bash # Bash completions
ask --completions zsh # Zsh completions
ask --completions fish # Fish completions
ask --completions powershell # PowerShell completions
ask --completions elvish # Elvish completionsSupports user-defined commands in config:
[commands.cm]
system = "Generate commit message"
type = "command"
auto_execute = false
provider = "anthropic" # Optional override
model = "claude-3-opus" # Optional override- Input: User runs
ask how to list docker containers - Alias Expansion: Aliases from config are expanded (e.g.,
q→--raw --no-color) - Parsing:
Args::parse_flexible()extracts flags and query - Config: Load configuration with precedence
- Provider: Create appropriate provider based on config
- Intent: Classify intent (COMMAND/QUESTION/CODE)
- Generation: Send to AI with appropriate system prompt
- Post-processing: Strip fenced code blocks, then flatten multi-line commands into one-liners for terminal robustness
- Output: Stream or display response
- Execution: For commands, optionally execute with safety checks
# Run all checks before committing
make precommit
# Run all tests
cargo test
# Run with verbose output
cargo test -- --nocapture
# Run specific test
cargo test test_safe_commands# Development build
cargo build
# Release build (optimized)
cargo build --release
# Cross-compile (requires cross)
cross build --release --target aarch64-unknown-linux-gnuThe Makefile provides convenient development commands:
make precommit- Run all checks (fmt, clippy, test, audit)make fmt- Check code formattingmake clippy- Run lintermake test- Run testsmake audit- Security auditmake build- Debug buildmake release- Release buildmake clean- Clean artifacts
See ADR.md for architectural decisions including:
- Why JSON storage over Native DB
- Flexible argument parsing approach
- Context opt-in design
- Safety detection patterns
- Provider abstraction
Key crates:
clap: CLI utility functions and shell completionstokio: Async runtimereqwest: HTTP client with streaming and rustlshickory-resolver: Custom DNS resolution for cross-platform compatibilityserde+toml: Configuration parsingcolored: Terminal colorsindicatif: Progress spinnerstermimad: Markdown renderingrequestty: Interactive CLI promptsarboard: Clipboard support for command injection (all platforms)enigo: Key simulation for Cmd+V/Ctrl+V paste (macOS/Windows/Linux x86_64)mouse-keyboard-input: Key simulation for Linux (uinput)clap_complete: Shell completions generationanyhow+thiserror: Error handlingserde_json: JSON serialization for context and outputsha2: SHA256 hashing for directory-based context IDs