AetherShell
AetherShell is a next-generation shell that combines the power of typed functional programming with multimodal AI capabilities. It’s designed for developers who want a shell that understands structure, not just text.
Why AetherShell?
Traditional shells treat everything as text, leading to fragile scripts and endless string parsing. AetherShell takes a different approach:
- 🔷 Typed Pipelines: Data flows as structured values (arrays, records, tables), not raw text
- 🧠 AI-Native: Built-in AI agents, multi-provider support, tool calling, and reasoning
- ⚡ Functional-First: Lambdas, pattern matching, and immutable-by-default semantics
- 🎨 Modern TUI: Rich terminal interface with multimodal content (images, charts)
- 🔌 Extensible: Plugin system, MCP protocol support, and Python/Node.js SDKs
Quick Example
# Type-safe pipelines
let files = ls "."
| where(fn(f) => f.size > 1000)
| sort_by("modified")
| take(5)
# AI-powered analysis
let review = ai("Review this code for security issues:
" + cat("app.rs"), {
model: "gpt-4o"
})
# An agent, with the builtins it may call
agent("Find all TODO comments in src/", ["ls", "cat", "grep"])
Features at a Glance
| Feature | Description |
|---|---|
| Typed Values | Int, Float, String, Bool, Array, Record, Table, Lambda |
| Pipeline Operators | |, |>, ?> with full type inference |
| Pattern Matching | match expressions with guards and destructuring |
| AI Providers | 20 provider types; OpenAI, Anthropic, Google and Ollama have dedicated clients, the rest go over OpenAI-compatible endpoints |
| Agent Framework | Single agents with builtins as tools, over MCP or in-process |
| MCP | ae mcp stdio makes every builtin callable through a three-tool facade |
| Interactive TUI | Real-time chat and multimodal file references |
Getting Started
Ready to dive in? Start with Installation to set up AetherShell on your system.
If you’re coming from Bash or PowerShell, check out our Quick Start guide that translates common patterns to AetherShell.
Community
- GitHub: nervosys/AetherShell
- Issues: Report a bug or ask a question
License
AetherShell is open source under the Apache 2.0 License.
Installation
This guide covers installing AetherShell on your system.
Requirements
- Rust 1.88+ (for building from source)
- OS: Windows, macOS, or Linux
- Optional: API keys for AI providers (OpenAI, Anthropic, etc.)
Install with Cargo
The recommended way to install AetherShell:
cargo install aethershell
This installs two binaries:
ae- The main AetherShell executableaimodel- AI model management CLI
Install from Source
For the latest development version:
git clone https://github.com/nervosys/AetherShell.git
cd AetherShell
cargo build --release
# Add to PATH
cp target/release/ae ~/.local/bin/
cp target/release/aimodel ~/.local/bin/
Pre-built Binaries
Download pre-built binaries from the releases page:
macOS
# Intel Mac
curl -LO https://github.com/nervosys/AetherShell/releases/latest/download/aethershell-x86_64-apple-darwin.tar.gz
tar xzf aethershell-x86_64-apple-darwin.tar.gz
# Apple Silicon
curl -LO https://github.com/nervosys/AetherShell/releases/latest/download/aethershell-aarch64-apple-darwin.tar.gz
tar xzf aethershell-aarch64-apple-darwin.tar.gz
Linux
curl -LO https://github.com/nervosys/AetherShell/releases/latest/download/aethershell-x86_64-unknown-linux-gnu.tar.gz
tar xzf aethershell-x86_64-unknown-linux-gnu.tar.gz
Windows
Download aethershell-x86_64-pc-windows-msvc.zip from releases and extract to a directory in your PATH.
Verify Installation
ae --version
# AetherShell 0.2.0
ae --help
VS Code Extension
Install the AetherShell extension for syntax highlighting and IDE features:
code --install-extension nervosys.aethershell
Or search for “AetherShell” in the VS Code marketplace.
Next Steps
- Quick Start - Your first AetherShell commands
- Configuration - Set up AI providers and preferences
Quick Start
Get up and running with AetherShell in 5 minutes.
Launch the Shell
# Interactive REPL
ae
# TUI mode (rich terminal interface)
ae --tui
# Execute a file
ae script.ae
# Evaluate an expression
ae -e '[1,2,3] | map(fn(x) => x * 2)'
Basic Syntax
Variables
# Immutable by default
let name = "Alice"
let numbers = [1, 2, 3, 4, 5]
let config = { host: "localhost", port: 8080 }
# Mutable when needed
let mut counter = 0
counter = counter + 1
Types
AetherShell has a rich type system:
# Primitives
let n = 42 # Int
let f = 3.14 # Float
let s = "hello" # String
let b = true # Bool
# Collections
let arr = [1, 2, 3] # Array[Int]
let rec = { x: 1, y: 2 } # Record
let table = [[1,"a"], [2,"b"]] # Table
# Functions
let double = fn(x) => x * 2 # Lambda
Pipelines
The power of AetherShell is in pipelines:
# Traditional pipeline
[1, 2, 3, 4, 5]
| filter(fn(x) => x > 2)
| map(fn(x) => x * 10)
| sum()
# Result: 120
# File operations
ls "src"
| where(fn(f) => f.extension == "rs")
| sort_by("size")
| take(5)
| select("name", "size")
Pattern Matching
let describe = fn(x) => match {
0 => "zero",
n if n < 0 => "negative",
n if n > 100 => "large",
_ => "normal"
}
describe(42) # "normal"
describe(-5) # "negative"
AI Features
Simple Query
# Ask the AI a question
ai("What is the capital of France?")
# With specific model
ai("Explain monads", { model: "claude-3-sonnet" })
Run an Agent
agent takes a goal, not a persona, and returns the final answer as a string.
It is not a callable object: there is no let a = agent(...) then a("...").
# A goal, with no tools
agent("Explain what this project builds")
# A goal, with the builtins it may call
agent("Find all TODO comments under src/", ["ls", "grep", "cat"])
Shell commands are default-deny; export AGENT_ALLOW_CMDS before starting ae
to permit any. See Creating Agents.
Set Up AI Provider
# Set environment variable
env_set("OPENAI_API_KEY", "sk-...")
# Or use the config
# ~/.config/aethershell/config.toml
Common Commands
| Command | Description |
|---|---|
ls [path] | List directory contents as a table |
cd path | Change directory |
pwd | Print working directory |
cat file | Read file contents |
env | List environment variables |
http_get url | Make HTTP GET request |
print value | Display a value |
help [builtin] | Get help |
Example Script
Save as example.ae:
# Fetch and process data
let response = http_get("https://api.github.com/repos/nervosys/AetherShell")
let repo = json_parse(response.body)
print("Repository: " + repo.full_name)
print("Stars: " + string(repo.stargazers_count))
print("Language: " + repo.language)
# Find large files in the project
let large_files = ls "."
| where(fn(f) => f.size > 10000)
| sort_by("size", "desc")
| take(10)
print("\nLargest files:")
large_files | each(fn(f) => print(" " + f.name + ": " + string(f.size) + " bytes"))
Run it:
ae example.ae
Next Steps
- Language Guide - Deep dive into the syntax
- AI Integration - Set up AI providers
- Builtins Reference - All available commands
Configuration
AetherShell can be configured through environment variables and config files.
Configuration File
The main configuration file is located at:
- Linux/macOS:
~/.config/aethershell/config.toml - Windows:
%APPDATA%\aethershell\config.toml
Example Configuration
# AetherShell Configuration
[general]
# Default shell features
history_size = 10000
auto_save_history = true
multiline_prompt = true
[ai]
# Default AI provider
default_provider = "openai"
default_model = "gpt-4o-mini"
# Response settings
max_tokens = 4096
temperature = 0.7
stream = true
[ai.providers.openai]
api_key = "${OPENAI_API_KEY}" # Use env var
base_url = "https://api.openai.com/v1"
[ai.providers.anthropic]
api_key = "${ANTHROPIC_API_KEY}"
default_model = "claude-3-sonnet-20240229"
[ai.providers.ollama]
base_url = "http://localhost:11434"
default_model = "llama3"
[tui]
# TUI settings
theme = "catppuccin-mocha"
show_images = true
image_protocol = "kitty" # kitty, iterm, sixel
max_image_width = 80
show_timestamps = true
[agent]
# Agent defaults
allowed_commands = ["ls", "cat", "grep", "find", "curl"]
max_tool_calls = 10
timeout_seconds = 300
[server]
# API server settings
host = "127.0.0.1"
port = 3002
enable_cors = true
[logging]
level = "info" # debug, info, warn, error
file = "~/.local/share/aethershell/aethershell.log"
Environment Variables
| Variable | Description | Example |
|---|---|---|
AETHER_AI | Default AI provider | openai, claude, ollama |
OPENAI_API_KEY | OpenAI API key | sk-... |
ANTHROPIC_API_KEY | Anthropic API key | sk-ant-... |
GOOGLE_API_KEY | Google AI API key | AIza... |
OLLAMA_HOST | Ollama server URL | http://localhost:11434 |
AGENT_ALLOW_CMDS | Allowed agent commands | ls,cat,grep |
AETHERSHELL_LOG | Log level | debug, info, warn |
Setting Environment Variables
In your shell config (.bashrc, .zshrc, etc.):
export OPENAI_API_KEY="sk-your-key-here"
export AETHER_AI="openai"
export AGENT_ALLOW_CMDS="ls,cat,grep,find,curl,http_get"
Or in AetherShell:
env_set("OPENAI_API_KEY", "sk-your-key-here")
AI Provider Setup
OpenAI
export OPENAI_API_KEY="sk-..."
Available models: gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo
Anthropic (Claude)
export ANTHROPIC_API_KEY="sk-ant-..."
Available models: claude-3-opus, claude-3-sonnet, claude-3-haiku
Google (Gemini)
export GOOGLE_API_KEY="AIza..."
Available models: gemini-pro, gemini-pro-vision
Local Models (Ollama)
- Install Ollama: https://ollama.ai
- Pull a model:
ollama pull llama3 - Use in AetherShell:
ai("Hello", { model: "ollama:llama3" })
Multiple Providers
Use model URIs to specify the provider:
# OpenAI
ai("Query", { model: "openai:gpt-4o" })
# Anthropic
ai("Query", { model: "claude:claude-3-sonnet" })
# Ollama (local)
ai("Query", { model: "ollama:llama3" })
# OpenRouter
ai("Query", { model: "openrouter:anthropic/claude-3-opus" })
Command-Line Options
ae --help
Options:
-e, --eval <CODE> Evaluate code directly
-c, --command <CMD> Run a single command
--tui Start in TUI mode
--no-history Disable history
--config <PATH> Use alternate config file
--log-level <LEVEL> Set log level
--server Start API server mode
--port <PORT> API server port (default: 3002)
Profile Scripts
AetherShell runs profile scripts on startup:
~/.config/aethershell/init.ae- Runs on every startup~/.config/aethershell/login.ae- Runs on login shells
Example init.ae:
# Set up aliases
let ll = fn() => ls "." | sort_by("modified", "desc")
let search = fn(pattern) => grep pattern "."
# Configure AI
env_set("AETHER_AI", "openai")
# Welcome message
print("Welcome to AetherShell! 🐚")
Basic Syntax
This guide covers the fundamental syntax of AetherShell.
Comments
# This is a single-line comment
// This also works for single-line comments
# Multi-line comments use consecutive single-line comments
# like this
# and this
Expressions
Everything in AetherShell is an expression that returns a value:
# Arithmetic
1 + 2 * 3 # 7
10 / 3 # 3 (integer division)
10.0 / 3.0 # 3.333...
10 % 3 # 1 (modulo)
2 ** 10 # 1024 (power)
# Comparison
1 < 2 # true
3 >= 3 # true
"a" == "a" # true
"a" != "b" # true
# Logical
true && false # false
true || false # true
!true # false
# String concatenation
"Hello, " + "World!" # "Hello, World!"
# Ternary-like with match
match x > 0 {
true => "positive",
false => "non-positive"
}
Statements
Variable Declaration
# Immutable (default)
let x = 42
let name = "Alice"
let numbers = [1, 2, 3]
# Mutable
let mut counter = 0
counter = counter + 1
# Multiple assignments
let (a, b, c) = (1, 2, 3)
Blocks
Blocks are sequences of expressions. The last expression is the return value:
let result = {
let x = 10
let y = 20
x + y # Block returns 30
}
print(result) # 30
Control Flow
If Expressions
let max = if a > b { a } else { b }
# Multi-branch
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 70 {
"C"
} else {
"F"
}
Match Expressions
let result = match value {
0 => "zero",
1 => "one",
n if n < 0 => "negative",
n if n > 100 => "large",
_ => "other"
}
# Destructuring
let point = { x: 10, y: 20 }
match point {
{ x: 0, y: 0 } => "origin",
{ x: 0, y: _ } => "on y-axis",
{ x: _, y: 0 } => "on x-axis",
_ => "somewhere else"
}
Loops
# For-each loop
for item in [1, 2, 3, 4, 5] {
print(item)
}
# With index
for (i, item) in enumerate([1, 2, 3]) {
print(string(i) + ": " + string(item))
}
# While loop
let mut n = 0
while n < 5 {
print(n)
n = n + 1
}
# Loop (infinite, use break)
let mut count = 0
loop {
count = count + 1
if count > 10 {
break
}
}
Functions
Lambda Syntax
# Single parameter
let double = fn(x) => x * 2
double(21) # 42
# Multiple parameters
let add = fn(a, b) => a + b
add(2, 3) # 5
# With block body
let greet = fn(name) => {
let greeting = "Hello, " + name + "!"
print(greeting)
greeting
}
# No parameters
let now = fn() => timestamp()
Named Functions
# Define a named function (syntax sugar)
let factorial = fn(n) => match {
0 => 1,
1 => 1,
n => n * factorial(n - 1)
}
factorial(5) # 120
Closures
Functions capture their environment:
let make_counter = fn() => {
let mut count = 0
fn() => {
count = count + 1
count
}
}
let counter = make_counter()
counter() # 1
counter() # 2
counter() # 3
Operators
Arithmetic
| Operator | Description | Example |
|---|---|---|
+ | Addition | 1 + 2 → 3 |
- | Subtraction | 5 - 3 → 2 |
* | Multiplication | 4 * 3 → 12 |
/ | Division | 10 / 3 → 3 |
% | Modulo | 10 % 3 → 1 |
** | Power | 2 ** 8 → 256 |
Comparison
| Operator | Description |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
<= | Less than or equal |
> | Greater than |
>= | Greater than or equal |
Logical
| Operator | Description |
|---|---|
&& | Logical AND |
|| | Logical OR |
! | Logical NOT |
Pipeline
| Operator | Description |
|---|---|
| | Pipe (pass as argument) |
|> | Pipe (method-style) |
?> | Optional pipe (skip on null) |
String Interpolation
let name = "Alice"
let age = 30
# Concatenation
print("Name: " + name + ", Age: " + string(age))
# Using format
print(format("Name: {}, Age: {}", name, age))
Array Spread
let a = [1, 2, 3]
let b = [4, 5, 6]
let combined = [...a, ...b] # [1, 2, 3, 4, 5, 6]
Record Spread
let base = { x: 1, y: 2 }
let extended = { ...base, z: 3 } # { x: 1, y: 2, z: 3 }
# Override fields
let updated = { ...base, x: 10 } # { x: 10, y: 2 }
Types and Values
AetherShell is a typed shell where every expression produces a structured Value. Unlike traditional shells that pipe raw text, AetherShell pipelines carry rich, typed data.
Core Types
| Type | Example | Description |
|---|---|---|
Null | null | Absence of value |
Bool | true, false | Boolean |
Int | 42, -7 | 64-bit signed integer |
Float | 3.14, -0.5 | 64-bit floating point |
String | "hello" | Text with interpolation support |
Uri | openai:gpt-4o-mini | URI with scheme (RFC 3986) |
Array | [1, "two", true] | Heterogeneous ordered list |
Record | {name: "Ada", age: 36} | Ordered key-value map |
Table | output of ls | Structured table with schema |
Lambda | fn(x) => x * 2 | First-class function |
Error | throw "oops" | Error value |
Integers and Floats
let x = 42 # Int
let y = 3.14 # Float
let z = x + y # Float (auto-promoted)
Integer division always produces a Float:
10 / 3 # => 3.3333...
Strings
Strings support ${expr} interpolation:
let name = "world"
let greeting = "Hello, ${name}!" # => "Hello, world!"
let math = "2 + 2 = ${2 + 2}" # => "2 + 2 = 4"
String concatenation works with + and auto-converts the other operand:
"count: " + 42 # => "count: 42"
100 + " items" # => "100 items"
URIs
URIs identify resources with a scheme prefix, commonly used for AI model references:
let model = openai:gpt-4o-mini
let local = ollama:llama3
Arrays
Arrays hold any mix of types:
let nums = [1, 2, 3]
let mixed = [1, "two", true, [4, 5]]
Records
Records are key-value maps with sorted keys:
let person = {name: "Ada", age: 36, langs: ["Rust", "Python"]}
person.name # => "Ada"
person.langs # => ["Rust", "Python"]
Tables
Tables are structured arrays of records with a defined schema. Many builtins return tables:
ls "." # => Table with columns: name, path, ext, is_dir, size, modified
Tables get special pretty-printed column-aligned output in the terminal.
Type Conversion
AetherShell performs automatic numeric promotion in arithmetic:
| Expression | Result Type | Rule |
|---|---|---|
Int + Int | Int | Integer arithmetic |
Int + Float | Float | Promote to float |
Float + Float | Float | Float arithmetic |
Int ^ Int (positive) | Int | Integer power |
Int ^ Int (negative) | Float | Float power |
Int / Int | Float | Always float division |
Equality comparison between Int and Float works via casting.
Truthiness
All values have a boolean interpretation:
| Falsy | Truthy |
|---|---|
null | Non-null values |
false | true |
0, 0.0 | Non-zero numbers |
"" (empty string) | Non-empty strings |
[] (empty array) | Non-empty arrays |
{} (empty record) | Non-empty records |
Error(...) | Lambdas, Builtins |
JSON Interop
Values convert bidirectionally with JSON:
let data = from_json '{"name": "test", "count": 42}'
data.name # => "test"
let json_str = to_json {name: "test", count: 42}
# => '{"count":42,"name":"test"}'
Type Inspection
describe 42 # => "Int"
describe "hello" # => "String"
describe [1, 2, 3] # => "Array(3 elements)"
Variables and Bindings
AetherShell supports several forms for declaring and binding variables, from explicit let declarations to concise shorthand syntax.
Let Bindings
The standard way to declare a variable:
let name = "AetherShell"
let version = 3
let features = ["typed", "pipelines", "AI"]
Variables are immutable by default. Attempting to reassign an immutable variable produces an error:
let x = 10
x = 20 # Error: Cannot reassign immutable variable 'x'. Use 'let mut x' to make it mutable.
Mutable Variables
Use mut to allow reassignment:
let mut counter = 0
counter = counter + 1 # OK
counter = 42 # OK
Or with shorthand:
mut counter = 0
counter = counter + 1
Shorthand Syntax
AetherShell offers shorter forms for common patterns:
# These are all equivalent:
let x = 10
x = 10 # Inferred let
x := 10 # Walrus-style binding
Public Variables
Variables can be marked public for export from modules:
pub let API_URL = "https://api.example.com"
pub let VERSION = "1.0.0"
Public variables are accessible when the module is imported.
Type Annotations
Optional type annotations can be added to bindings:
let name: String = "hello"
let count: Int = 42
Type annotations are parsed but currently used for documentation purposes. The type inference engine (typecheck.rs) handles type validation.
Scoping Rules
AetherShell uses a flat environment model:
- All variables share a single scope
- Lambda parameters are temporarily bound during execution and restored after
- There are no block-level scopes or shadowing in the traditional sense
- Variables are visible once declared and remain available for the rest of the session
let x = 10
let f = fn(x) => x * 2 # Lambda parameter 'x' temporarily shadows outer 'x'
f(5) # => 10
x # => 10 (outer x unchanged)
Environment Variables
Shell environment variables are accessible and can be set:
# Access environment variable
let home = $HOME
# Set environment variable
export PATH = "/usr/local/bin:${$PATH}"
Assignment vs. Declaration
| Syntax | Meaning |
|---|---|
let x = expr | Immutable declaration |
let mut x = expr | Mutable declaration |
x = expr | Shorthand immutable declaration (or reassign if mutable) |
x := expr | Shorthand immutable declaration |
mut x = expr | Shorthand mutable declaration |
pub let x = expr | Public immutable declaration |
Functions and Lambdas
AetherShell treats functions as first-class values using lambda syntax. Lambdas can be passed to pipelines, stored in variables, and composed freely.
Lambda Syntax
The basic form is fn(params) => body:
let double = fn(x) => x * 2
let add = fn(a, b) => a + b
let greet = fn() => "hello"
Lambdas are expressions — they return the value of their body:
double(5) # => 10
add(3, 4) # => 7
greet() # => "hello"
Multi-Parameter Lambdas
let clamp = fn(value, lo, hi) =>
if value < lo { lo }
else if value > hi { hi }
else { value }
clamp(15, 0, 10) # => 10
clamp(-5, 0, 10) # => 0
Calling Conventions
AetherShell supports multiple ways to call functions:
Parenthesized Calls
Standard function calling:
double(5)
add(3, 4)
Word Calls
At the top level, functions can be called without parentheses (shell-style):
print "hello" # equivalent to print("hello")
echo "world" # equivalent to echo("world")
cd "/home" # equivalent to cd("/home")
Note: Word-call syntax is disabled inside lambda bodies to prevent ambiguous parsing.
Pipeline Calls
Functions can receive input through the pipe operator:
5 | double # => 10
[1, 2, 3] | double # => [2, 4, 6] (auto-maps over arrays)
Auto-Mapping
When a 1-parameter lambda receives an Array through a pipeline, it automatically maps over each element:
[1, 2, 3] | fn(x) => x * 2 # => [2, 4, 6]
["a", "b"] | fn(s) => s + "!" # => ["a!", "b!"]
A single (non-array) value is passed directly:
5 | fn(x) => x * 2 # => 10
Index Parameter
Callback functions used with map and where can accept a second parameter for the index:
["a", "b", "c"] | map fn(item, i) => "${i}: ${item}"
# => ["0: a", "1: b", "2: c"]
Closures
Lambdas capture variables from the enclosing environment by reference. Variables are resolved at call time:
let multiplier = 3
let scale = fn(x) => x * multiplier
scale(10) # => 30
# If multiplier changes (if mutable), scale reflects the new value
Async Functions
Async lambdas create futures that must be awaited:
let fetch_data = async fn(url) => http_get(url)
let future = fetch_data("https://api.example.com/data")
let result = await future
Recursion
Lambdas can reference themselves through their binding name:
let factorial = fn(n) =>
if n <= 1 { 1 }
else { n * factorial(n - 1) }
factorial(5) # => 120
This works because variable lookup happens at evaluation time, so factorial resolves to the lambda in the environment.
Higher-Order Functions
Functions that take or return functions:
let apply_twice = fn(f, x) => f(f(x))
apply_twice(fn(x) => x + 1, 0) # => 2
let make_adder = fn(n) => fn(x) => x + n
let add5 = make_adder(5)
add5(10) # => 15
Builtins as Values
Builtin functions can be referenced and passed around:
let my_sort = sort
[3, 1, 2] | my_sort # => [1, 2, 3]
Common Patterns
Pipeline with inline lambda
ls "." | where fn(f) => f.size > 1000 | map fn(f) => f.name
Compose transformations
let transform = fn(data) =>
data
| where fn(r) => r.active
| map fn(r) => {name: r.name, score: r.points * 10}
| sort
Reduce / fold
[1, 2, 3, 4, 5] | reduce fn(acc, x) => acc + x, 0 # => 15
Pipelines
Pipelines are the heart of AetherShell. The pipe operator | connects expressions so that the output of one becomes the input of the next — but unlike traditional shells, AetherShell pipelines carry typed, structured data, not raw text.
Basic Syntax
expression | transform | transform | ...
Each | takes the value on the left and passes it to the right:
[3, 1, 4, 1, 5] | sort | reverse | first
# [3,1,4,1,5] → [1,1,3,4,5] → [5,4,3,1,1] → 5
How Values Flow
The pipe operator is left-associative: a | b | c is parsed as (a | b) | c.
When the right side of a pipe is evaluated, the left side’s value is available as pipeline input. How it’s consumed depends on what’s on the right:
| Right-hand side | Behavior |
|---|---|
| Lambda literal | Called with left value as argument |
| Named lambda (variable) | Called with left value as explicit argument |
| Builtin function | Receives left value as input parameter |
| Other expression | Left value set as implicit input during evaluation |
Example: builtins in pipelines
ls "." # Array of file records
| where fn(f) => f.size > 1000 # Filter: keep large files
| map fn(f) => f.name # Transform: extract names
| sort # Sort alphabetically
Example: lambda in pipelines
42 | fn(x) => x * 2 # => 84
"hello" | fn(s) => upper(s) # => "HELLO"
Auto-Mapping
When a 1-parameter lambda receives an Array, it automatically maps over each element:
[1, 2, 3] | fn(x) => x * 2 # => [2, 4, 6]
This applies to both inline lambdas and named lambdas. If you want to operate on the array as a whole, use the length or similar builtin directly:
[1, 2, 3] | length # => 3 (operates on the whole array)
Data Pipeline Builtins
These builtins are designed for pipeline use:
Filtering
[1, 2, 3, 4, 5] | where fn(x) => x > 3
# => [4, 5]
ls "." | where fn(f) => f.ext == "rs"
# Only Rust files
Mapping
[1, 2, 3] | map fn(x) => x * 10
# => [10, 20, 30]
# With index parameter
["a", "b", "c"] | map fn(item, i) => "${i}: ${item}"
# => ["0: a", "1: b", "2: c"]
Reducing
[1, 2, 3, 4, 5] | reduce fn(acc, x) => acc + x, 0
# => 15
Selecting fields
ls "." | select "name" "size"
# => Array of records with only name and size fields
Grouping
ls "." | group "ext"
# Records grouped by file extension
Sorting
[3, 1, 4, 1, 5] | sort
# => [1, 1, 3, 4, 5]
Structured Data Pipelines
Since ls, ps, and other builtins return structured data, you can build powerful queries:
# Find the 5 largest Rust files
ls "src"
| where fn(f) => f.ext == "rs"
| sort
| reverse
| first 5
| select "name" "size"
# Calculate total size of all .toml files
ls "."
| where fn(f) => f.ext == "toml"
| map fn(f) => f.size
| reduce fn(a, b) => a + b, 0
Format Conversion Pipelines
Convert between data formats inline:
# JSON to CSV
from_json '[{"name":"Ada","age":36},{"name":"Bob","age":30}]' | to_csv
# Process HTTP response
http_get "https://api.example.com/users" | from_json | where fn(u) => u.active
Pipeline Input in Builtins
Builtins can access pipeline input implicitly. For example, sort works both ways:
sort [3, 1, 2] # Direct call with argument
[3, 1, 2] | sort # Pipeline: input received implicitly
This dual calling convention makes builtins equally useful in both interactive and pipeline contexts.
Chaining with AI
Pipelines compose naturally with AI operations:
# Read a file, ask AI to summarize it
cat "README.md" | ai "Summarize this document in 3 bullet points"
# Generate code, then format it
ai "Write a Python function to sort a list" | save "sort.py"
Pattern Matching
AetherShell provides match expressions for destructuring values and branching on their structure. Pattern matching works with all value types including arrays, records, and constructor-style tagged values.
Basic Match
let x = 42
match x {
0 => "zero",
1 => "one",
_ => "something else"
}
# => "something else"
The _ wildcard matches anything without binding a name.
Binding Patterns
Identifier patterns match any value and bind it to a name:
let result = match get_status() {
"ok" => "all good",
"error" => "something broke",
other => "unexpected: ${other}"
}
Literal Patterns
Match against exact values:
match value {
42 => "the answer",
"hello" => "greeting",
true => "affirmative",
null => "nothing",
_ => "default"
}
Supported literal patterns:
| Pattern | Matches |
|---|---|
42 | Exact integer |
"hello" | Exact string |
true / false | Exact boolean |
null | Null value |
Array Patterns
Destructure arrays by matching their elements:
match [1, 2, 3] {
[] => "empty",
[x] => "one element: ${x}",
[x, y] => "two: ${x}, ${y}",
[x, y, z] => "three: ${x}, ${y}, ${z}",
_ => "more than three"
}
# => "three: 1, 2, 3"
Array patterns require an exact length match. [x, y] won’t match a 3-element array.
Patterns can be nested:
match [[1, 2], [3, 4]] {
[[a, b], [c, d]] => a + b + c + d,
_ => 0
}
# => 10
Record Patterns
Match records by their fields:
let person = {name: "Ada", age: 36, role: "engineer"}
match person {
{name: "Ada", role} => "Found Ada, role: ${role}",
{name, age} => "${name} is ${age} years old",
_ => "unknown"
}
# => "Found Ada, role: engineer"
Shorthand: {name} is equivalent to {name: name} — it matches the field name and binds its value to variable name.
Record patterns match if all specified fields exist. Extra fields are ignored:
# This matches even though the record has 'role' too
match {name: "Ada", age: 36, role: "engineer"} {
{name, age} => "${name}, ${age}",
_ => "no match"
}
# => "Ada, 36"
Constructor Patterns
AetherShell uses tagged records to represent algebraic data types. The Some and None constructors create tagged records:
let result = Some(42) # => {_tag: "Some", _value: 42}
let empty = None # => {_tag: "None"}
Match on constructors:
match Some(42) {
Some(x) => "got value: ${x}",
None => "nothing",
_ => "unexpected"
}
# => "got value: 42"
Zero-argument constructors just check the _tag:
match None {
Some(x) => "got ${x}",
None => "nothing here",
}
# => "nothing here"
Guards
Add conditions to match arms with if:
match value {
x if x > 100 => "large",
x if x > 10 => "medium",
x if x > 0 => "small",
0 => "zero",
x => "negative: ${x}"
}
Guards are evaluated with the pattern’s bindings in scope. If the guard is falsy, the next arm is tried:
match {name: "Ada", age: 36} {
{name, age} if age >= 21 => "${name} is an adult",
{name, age} => "${name} is ${age} years old",
_ => "unknown"
}
# => "Ada is an adult"
Exhaustiveness
If no arm matches the value, a runtime error is produced:
Error: match: no arm matched the value
Always include a _ wildcard as the last arm to handle unexpected cases:
match status {
"active" => handle_active(),
"paused" => handle_paused(),
_ => throw "Unknown status: ${status}"
}
Match as Expression
match is an expression — it returns a value:
let label = match count {
0 => "none",
1 => "one",
_ => "many"
}
print label
Practical Examples
Implicit scrutinee in lambdas
When match is used inside a lambda body, the scrutinee can be omitted — it defaults to the lambda’s first parameter:
# Explicit scrutinee (always works)
let grade = fn(score) => match score {
90..100 => "A",
80..89 => "B",
_ => "C"
}
# Implicit scrutinee (same result, cleaner)
let grade = fn(score) => match {
90..100 => "A",
80..89 => "B",
_ => "C"
}
grade(85) # "B"
This works with any single-parameter lambda, including in pipelines:
[1, 2, 3, 100] | map fn(x) => match {
_ if x > 50 => "big",
_ => "small"
}
# => ["small", "small", "small", "big"]
Note:
match expr { ... }with an explicit scrutinee is always supported. The implicit form only applies inside lambdas and uses the first parameter.
Processing command output
let files = ls "."
files | map fn(f) => match {
{is_dir: true, name} => "📁 ${name}/",
{ext: "rs", name} => "🦀 ${name}",
{ext: "md", name} => "📝 ${name}",
{name} => " ${name}"
}
Option handling
let find_user = fn(id) =>
if id == 1 { Some({name: "Ada", role: "admin"}) }
else { None }
match find_user(1) {
Some({name, role}) => "Found ${name} (${role})",
None => "User not found"
}
# => "Found Ada (admin)"
HTTP response handling
let response = http_get "https://api.example.com/status"
match response {
{status: 200, body} => from_json(body),
{status: 404} => throw "Not found",
{status} => throw "HTTP error: ${status}"
}
Records and Tables
Records and tables are AetherShell’s structured data types. Records are key-value maps; tables are arrays of records with a defined schema. Together they enable typed data processing pipelines.
Records
Creating Records
let person = {name: "Ada", age: 36, active: true}
let config = {host: "localhost", port: 8080, debug: false}
Record keys are always strings. Values can be any type, including nested records and arrays:
let project = {
name: "AetherShell",
version: {major: 0, minor: 3, patch: 0},
tags: ["shell", "rust", "ai"]
}
Field Access
Use dot notation to access fields:
person.name # => "Ada"
person.age # => 36
project.version.major # => 0
Accessing a non-existent field produces an error:
person.email # Error: field 'email' not found in record
Record Operations
# Get all keys
{name: "Ada", age: 36} | keys
# => ["age", "name"] (sorted alphabetically)
# Merge records (later values win)
let defaults = {color: "blue", size: 10}
let custom = {size: 20, bold: true}
merge defaults custom
# => {bold: true, color: "blue", size: 20}
Records in Pipelines
Records flow through pipelines as structured data:
let users = [
{name: "Ada", score: 95},
{name: "Bob", score: 82},
{name: "Eve", score: 91}
]
users
| where fn(u) => u.score > 85
| map fn(u) => {name: u.name, grade: "A"}
# => [{name: "Ada", grade: "A"}, {name: "Eve", grade: "A"}]
Tables
Tables are structured data with named columns, returned by many builtins.
Table Structure
A table has:
- rows: Array of records (each row is a
{key: value}map) - schema: List of column names defining the structure
Built-in Table Sources
# List files — returns a table
ls "."
# Columns: name, path, ext, is_dir, size, modified
# Process listings
ps
# Columns: pid, name, cpu, memory
Pretty Printing
Tables get special column-aligned display in the terminal:
┌──────────────┬──────┬─────┬────────┐
│ name │ ext │ dir │ size │
├──────────────┼──────┼─────┼────────┤
│ main.rs │ rs │ no │ 2,451 │
│ lib.rs │ rs │ no │ 1,089 │
│ Cargo.toml │ toml │ no │ 456 │
└──────────────┴──────┴─────┴────────┘
Data Pipeline Operations
select — Project Fields
Keep only specific columns:
ls "." | select "name" "size"
# Records with only name and size fields
where — Filter Rows
Keep rows matching a predicate:
ls "." | where fn(f) => f.size > 1000
ls "." | where fn(f) => f.ext == "rs"
map — Transform Rows
Create new values from each row:
ls "." | map fn(f) => {
file: f.name,
kb: f.size / 1024
}
sort — Order Rows
[3, 1, 4, 1, 5] | sort
# => [1, 1, 3, 4, 5]
group / group_by — Group Rows
Group records by a field value:
ls "." | group "ext"
# Records grouped by file extension
reduce — Aggregate
Collapse an array into a single value:
ls "." | map fn(f) => f.size | reduce fn(a, b) => a + b, 0
# Total size of all files
first / last — Take Elements
[1, 2, 3, 4, 5] | first 3 # => [1, 2, 3]
[1, 2, 3, 4, 5] | last 2 # => [4, 5]
reverse — Reverse Order
[1, 2, 3] | reverse # => [3, 2, 1]
unique — Remove Duplicates
[1, 2, 2, 3, 3, 3] | unique # => [1, 2, 3]
columns — Get Column Names
ls "." | columns
# => ["ext", "is_dir", "modified", "name", "path", "size"]
Format Conversion
Convert between structured data and serialization formats:
# JSON
let data = from_json '{"name": "test"}'
data | to_json
# CSV
let csv_data = from_csv "name,age\nAda,36\nBob,30"
csv_data | to_csv
# YAML
let yaml_data = from_yaml "name: test\ncount: 42"
yaml_data | to_yaml
Practical Examples
Analyze project files
ls "src"
| where fn(f) => f.ext == "rs"
| map fn(f) => {name: f.name, kb: f.size / 1024}
| sort
| reverse
# Rust files sorted by size, largest first
Process API response
http_get "https://api.github.com/repos/user/repo/issues"
| from_json
| where fn(i) => i.state == "open"
| map fn(i) => {title: i.title, labels: i.labels | map fn(l) => l.name}
| first 10
Build a report
let files = ls "src" | where fn(f) => f.ext == "rs"
let total_size = files | map fn(f) => f.size | reduce fn(a, b) => a + b, 0
let count = files | length
{
total_files: count,
total_bytes: total_size,
avg_size: total_size / count,
largest: files | sort | reverse | first 1
}
Error Handling
AetherShell provides structured error handling with try/catch expressions and first-class Error values. Errors can be created with throw, caught with try/catch, and inspected like any other value.
Error Values
Error is a first-class value type. It’s falsy and carries a string message:
let err = throw "something went wrong"
# err is Value::Error("something went wrong")
Try / Catch
Catch errors and recover gracefully:
let result = try {
cat("config.toml")
} catch {
"fallback value"
}
Binding the Error Message
Use catch variable to capture the error message:
let result = try {
http_get "https://unreachable.example.com"
} catch e {
print "Request failed: ${e}"
null
}
The catch variable receives the error message as a String.
Throw
Create an error value explicitly:
throw "file not found"
throw "invalid argument: ${arg}"
throw evaluates its expression, converts it to a string, and returns a Value::Error. If the thrown value is already a String, it’s used directly; otherwise it’s formatted.
What Gets Caught
try/catch handles two categories of errors:
-
Error values — produced by
throw:try { throw "oops" } catch e { "caught: ${e}" } # => "caught: oops" -
Runtime errors — produced by invalid operations:
try { 1 / 0 } catch e { "caught: ${e}" } try { null.field } catch e { "caught: ${e}" }
If the try block succeeds (returns a non-error value), the catch branch is not executed:
try { 42 } catch { "never reached" }
# => 42
Common Runtime Errors
| Error | Cause |
|---|---|
"unknown builtin: name" | Calling a function that doesn’t exist |
"cannot call null" | Trying to call a null value as a function |
"field 'x' not found in record" | Accessing a missing record field |
"Cannot reassign immutable variable 'x'" | Reassigning a let binding |
"match: no arm matched the value" | Non-exhaustive match statement |
"lambda arity mismatch" | Wrong number of arguments |
"expected Bool" | Using non-boolean in if condition |
Error Propagation
Without try/catch, errors propagate up and terminate the current execution:
let validate = fn(x) =>
if x < 0 { throw "must be non-negative" }
else { x }
# This will produce an error since we don't catch it
validate(-5)
Wrap the call in try/catch to handle it:
let result = try { validate(-5) } catch e {
print "Validation failed: ${e}"
0
}
# result is 0
Practical Patterns
Default on failure
let config = try { from_json(cat "config.json") } catch {
{host: "localhost", port: 8080}
}
Retry logic
let fetch_with_retry = fn(url) => {
let result = try { http_get url } catch { null }
if result { result }
else {
sleep 1000
try { http_get url } catch e {
throw "Failed after retry: ${e}"
}
}
}
Validate and collect errors
let validate_user = fn(user) => {
if !user.name { throw "name is required" }
if user.age < 0 { throw "age must be non-negative" }
if !user.email { throw "email is required" }
user
}
let result = try { validate_user({name: "", age: -1}) } catch e {
print "Invalid user: ${e}"
null
}
Pipeline error handling
# Errors in pipelines can be caught at any stage
let safe_pipeline = fn(data) =>
try {
data
| from_json
| where fn(r) => r.value > 0
| map fn(r) => r.value * 2
} catch e {
print "Pipeline failed: ${e}"
[]
}
Error vs. Null
nullrepresents absence of a value (intentional)Error(msg)represents a failure with a message (exceptional)- Both are falsy, but
Errorcarries diagnostic information
let result = find_user("nonexistent")
match result {
null => "not found",
Error(msg) => "error: ${msg}",
user => "found: ${user.name}"
}
Builtins Overview
AetherShell provides a rich set of built-in commands that return structured data for pipeline processing.
Core Philosophy
Unlike traditional shells where commands return text, AetherShell builtins return typed Value objects:
# Traditional shell: ls returns text
# AetherShell: ls returns Array[Record]
ls "."
# Returns: [
# { name: "file.txt", size: 1234, modified: "2024-01-01T12:00:00Z", is_dir: false },
# { name: "src", size: 4096, modified: "2024-01-01T10:00:00Z", is_dir: true },
# ...
# ]
This enables powerful pipeline operations:
ls "."
| where(fn(f) => f.size > 1000)
| sort_by("modified", "desc")
| take(5)
| select("name", "size")
Categories
File System
| Command | Description | Returns |
|---|---|---|
ls path | List directory | Array[Record] |
cat file | Read file contents | String |
read file | Read file (alias) | String |
write file content | Write to file | Bool |
mkdir path | Create directory | Bool |
rm path | Remove file/dir | Bool |
mv src dst | Move/rename | Bool |
cp src dst | Copy | Bool |
pwd | Current directory | String |
cd path | Change directory | () |
Data Processing
| Command | Description | Returns |
|---|---|---|
map(fn) | Transform each element | Array |
filter(fn) | Keep matching elements | Array |
reduce(fn, init) | Fold to single value | Any |
sort_by(field, dir) | Sort by field | Array |
where(fn) | Filter (alias) | Array |
select(fields...) | Pick fields | Array[Record] |
take(n) | First n elements | Array |
skip(n) | Skip n elements | Array |
flatten() | Flatten nested arrays | Array |
unique() | Remove duplicates | Array |
group_by(field) | Group by field | Record |
Text Processing
| Command | Description | Returns |
|---|---|---|
grep pattern path | Search in files | Array[Record] |
split str delim | Split string | Array[String] |
join arr delim | Join to string | String |
trim str | Remove whitespace | String |
replace old new str | Replace text | String |
uppercase str | To uppercase | String |
lowercase str | To lowercase | String |
Network
| Command | Description | Returns |
|---|---|---|
http_get url | GET request | Record |
http_post url body | POST request | Record |
http_put url body | PUT request | Record |
http_delete url | DELETE request | Record |
JSON/Data
| Command | Description | Returns |
|---|---|---|
json_parse str | Parse JSON | Any |
json_stringify val | Serialize to JSON | String |
csv_parse str | Parse CSV | Array[Array] |
csv_stringify data | Serialize to CSV | String |
System
| Command | Description | Returns |
|---|---|---|
env | All env vars | Record |
env_get name | Get env var | String? |
env_set name val | Set env var | () |
exec cmd args | Run command | Record |
which name | Find executable | String? |
ps | Process list | Array[Record] |
Math
| Command | Description | Returns |
|---|---|---|
sum arr | Sum of numbers | Number |
avg arr | Average | Float |
min arr | Minimum | Number |
max arr | Maximum | Number |
abs n | Absolute value | Number |
floor n | Floor | Int |
ceil n | Ceiling | Int |
round n | Round | Int |
sqrt n | Square root | Float |
Type Conversion
| Command | Description | Returns |
|---|---|---|
int val | Convert to int | Int |
float val | Convert to float | Float |
string val | Convert to string | String |
bool val | Convert to bool | Bool |
array val | Convert to array | Array |
Output
| Command | Description | Returns |
|---|---|---|
print val | Print value | () |
println val | Print with newline | () |
debug val | Debug output | () |
format str args... | Format string | String |
AI
| Command | Description | Returns |
|---|---|---|
ai prompt opts? | AI query | String |
agent prompt opts? | Create agent | Agent |
Return Value Structure
File System Records
# ls returns:
{
name: String, # File name
path: String, # Full path
size: Int, # Size in bytes
modified: String, # ISO timestamp
is_dir: Bool, # Is directory
is_file: Bool, # Is file
extension: String, # File extension
permissions: String # Unix permissions
}
HTTP Response
# http_get/post returns:
{
status: Int, # HTTP status code
headers: Record, # Response headers
body: String, # Response body
ok: Bool # status >= 200 && status < 300
}
Grep Match
# grep returns:
{
file: String, # File path
line: Int, # Line number
content: String, # Matching line
match: String # Matched text
}
Pipeline Examples
# Find large Rust files
ls "src"
| where(fn(f) => f.extension == "rs" && f.size > 10000)
| sort_by("size", "desc")
| select("name", "size")
# API data processing
http_get("https://api.example.com/users")
| json_parse()
| where(fn(u) => u.active)
| map(fn(u) => { name: u.name, email: u.email })
| take(10)
# Log analysis
cat("app.log")
| split("\n")
| where(fn(line) => line.contains("ERROR"))
| map(fn(line) => {
let parts = split(line, " ")
{ timestamp: parts[0], message: join(skip(parts, 2), " ") }
})
Core Operations
Core builtins provide essential shell operations: output, environment management, JSON handling, option types, diagnostics, and shell interop.
Output
print
Print a value to stdout without a trailing newline.
print "hello" # hello
print 42 # 42
print [1, 2, 3] # [1, 2, 3]
echo
Print a value followed by a newline. Equivalent to println in many languages.
echo "Hello, world!"
echo { name: "Ada", age: 36 }
debug / dbg
Print a value with type and structure information, useful for development.
debug [1, "two", 3.0]
# Array(3): [Int(1), String("two"), Float(3.0)]
let rec = { x: 1, y: [2, 3] }
dbg rec
Help & Inspection
help
Display available commands and usage information.
help # List all builtins
help "map" # Help for a specific builtin
type_of / typeof
Return the type name of a value as a string.
type_of 42 # "Int"
type_of "hello" # "String"
type_of [1, 2, 3] # "Array"
type_of { x: 1 } # "Record"
typeof fn(x) => x # "Lambda"
inspect
Return a detailed string representation of a value including internal structure.
inspect [1, "two", true]
# "[Int(1), String(\"two\"), Bool(true)]"
Option Types
AetherShell has first-class Some/None for representing optional values.
Some
Wrap a value in an option.
let result = Some(42)
echo result # Some(42)
None
The empty option value.
let missing = None
echo missing # None
Options are useful in pipelines where operations may not find a result:
let found = [1, 2, 3] | first
# found is Some(1) or None if array is empty
Environment Variables
env
Return all environment variables as a Record.
let vars = env
echo vars.PATH
echo vars.HOME
set_env
Set an environment variable for the current session.
set_env "MY_VAR" "hello"
echo (env).MY_VAR # hello
JSON
json_parse
Parse a JSON string into a structured Value (Record, Array, etc.).
let data = json_parse '{"name": "Ada", "langs": ["Rust", "Python"]}'
echo data.name # Ada
echo data.langs[0] # Rust
json_stringify
Serialize any value to a JSON string.
let rec = { x: 1, y: [2, 3] }
let s = json_stringify rec
echo s # {"x":1,"y":[2,3]}
save_json / write_json
Write a value as formatted JSON to a file.
let config = { debug: true, port: 8080 }
save_json "config.json" config
Timing & Sleep
time
Measure execution time of an expression. Returns the elapsed time.
time (ls "." | where(fn(f) => f.size > 1000))
# Elapsed: 12ms
now / timestamp
Return the current Unix timestamp in milliseconds.
let start = now
# ... do work ...
let elapsed = now - start
echo "Took ${elapsed}ms"
sleep
Pause execution for a given number of milliseconds.
sleep 1000 # Sleep for 1 second
Shell Interop
sh / shell
Execute a raw shell command and return its output as a string.
let result = sh "git status --short"
echo result
# Capture structured output by parsing
sh "git branch" | split "\n" | map(fn(b) => trim b)
call
Call a function or builtin by name (as a string).
call "echo" "hello"
let op = "upper"
call op "hello" # "HELLO"
exit
Exit the shell with an optional exit code.
exit # Exit with code 0
exit 1 # Exit with code 1
Diagnostics
assert
Assert that a condition is true. Throws an error if false.
assert (2 + 2 == 4) # passes
assert (len [1,2,3] == 3) # passes
assert false # ERROR: assertion failed
type_assert / assert_type
Assert that a value has a specific type.
type_assert 42 "Int" # passes
type_assert "hi" "String" # passes
type_assert 42 "String" # ERROR: expected String, got Int
is_error
Check whether a value is an Error.
let result = try { json_parse "invalid" } catch(e) { e }
echo (is_error result) # true
echo (is_error 42) # false
trace
Print a trace message with context, useful for debugging pipelines.
[1, 2, 3]
| map(fn(x) => { trace "processing" x; x * 2 })
| reduce(fn(a, b) => a + b, 0)
Membership
in
Test whether a value exists in an array or a key exists in a record.
echo (3 in [1, 2, 3]) # true
echo ("x" in { x: 1, y: 2 }) # true
echo (5 in [1, 2, 3]) # false
Configuration
config
Display the current shell configuration.
config # Show all config
config_get / config_set
Read or write individual configuration values.
config_get "theme"
config_set "theme" "dark"
config_set "editor" "vim"
config_path
Return the path to the configuration file.
echo (config_path) # ~/.config/aethershell/config.toml
config_init
Create a default configuration file.
config_reload
Reload configuration from disk.
themes
List available shell themes.
themes
# ["dark", "light", "monokai", "solarized", ...]
Collections
Collection builtins are the backbone of AetherShell’s pipeline-oriented design. They operate on Arrays, Records, and Tables, returning structured data for further processing.
Transforming
map
Apply a function to each element in an array, returning a new array.
[1, 2, 3] | map(fn(x) => x * 2)
# [2, 4, 6]
# With records
ls "." | map(fn(f) => { name: f.name, kb: f.size / 1024 })
each
Like map but intended for side effects. Returns the original array unchanged.
[1, 2, 3] | each(fn(x) => print "${x} ")
# Prints: 1 2 3
# Returns: [1, 2, 3]
Filtering
where
Keep only elements that satisfy a predicate.
[1, 2, 3, 4, 5] | where(fn(x) => x > 3)
# [4, 5]
ls "." | where(fn(f) => f.extension == "rs")
any
Return true if at least one element satisfies the predicate.
[1, 2, 3] | any(fn(x) => x > 2) # true
[1, 2, 3] | any(fn(x) => x > 5) # false
all
Return true if every element satisfies the predicate.
[2, 4, 6] | all(fn(x) => x % 2 == 0) # true
[2, 4, 5] | all(fn(x) => x % 2 == 0) # false
Reducing
reduce
Fold an array down to a single value with an accumulator.
[1, 2, 3, 4] | reduce(fn(acc, x) => acc + x, 0)
# 10
# Build a record from an array
["a", "b", "c"] | reduce(fn(acc, x) => { ...acc, [x]: true }, {})
# { a: true, b: true, c: true }
sum
Sum all numeric elements.
[1, 2, 3, 4] | sum # 10
ls "." | map(fn(f) => f.size) | sum
avg / mean
Compute the arithmetic mean.
[10, 20, 30] | avg # 20.0
product
Multiply all elements together.
[2, 3, 4] | product # 24
min / max
Return the minimum or maximum value.
[3, 1, 4, 1, 5] | min # 1
[3, 1, 4, 1, 5] | max # 5
Selecting
first
Return the first element of an array.
[10, 20, 30] | first # 10
last
Return the last element of an array.
[10, 20, 30] | last # 30
take
Return the first N elements.
[1, 2, 3, 4, 5] | take 3 # [1, 2, 3]
slice
Extract a sub-array by start index and length.
[10, 20, 30, 40, 50] | slice 1 3
# [20, 30, 40]
Ordering
sort_by
Sort an array of records by a field, with optional direction.
ls "." | sort_by "size" "desc" | take 5
# Top 5 largest files
let people = [
{ name: "Charlie", age: 30 },
{ name: "Alice", age: 25 },
{ name: "Bob", age: 28 }
]
people | sort_by "name" "asc"
reverse
Reverse the order of elements.
[1, 2, 3] | reverse # [3, 2, 1]
Combining
push
Append an element to an array.
[1, 2, 3] | push 4 # [1, 2, 3, 4]
concat
Concatenate two arrays.
concat [1, 2] [3, 4] # [1, 2, 3, 4]
zip
Combine two arrays into an array of pairs.
zip ["a", "b", "c"] [1, 2, 3]
# [["a", 1], ["b", 2], ["c", 3]]
flatten
Flatten nested arrays by one level.
[[1, 2], [3, 4], [5]] | flatten
# [1, 2, 3, 4, 5]
Uniqueness
unique
Remove duplicate values from an array.
[1, 2, 2, 3, 3, 3] | unique # [1, 2, 3]
Generators
range
Generate a sequence of integers.
range 1 5 # [1, 2, 3, 4]
range 0 10 2 # [0, 2, 4, 6, 8] (with step)
Record Operations
keys
Return the keys of a record as an array.
keys { x: 1, y: 2, z: 3 } # ["x", "y", "z"]
values
Return the values of a record as an array.
values { x: 1, y: 2, z: 3 } # [1, 2, 3]
Size
len / length
Return the number of elements in an array, characters in a string, or keys in a record.
len [1, 2, 3] # 3
len "hello" # 5
len { a: 1, b: 2 } # 2
Membership
in
Test if a value is in an array or a key is in a record.
3 in [1, 2, 3] # true
"name" in { name: "Ada" } # true
Pipeline Composition
Collections compose naturally through the pipe operator:
# Data analysis pipeline
ls "src"
| where(fn(f) => f.extension == "rs")
| map(fn(f) => { name: f.name, lines: len(split(cat(f.path), "\n")) })
| sort_by "lines" "desc"
| take 5
# Aggregation
range 1 100
| where(fn(x) => x % 3 == 0 || x % 5 == 0)
| sum
# 2318
# Nested transformation
[
{ dept: "eng", people: ["Alice", "Bob"] },
{ dept: "hr", people: ["Charlie"] }
]
| map(fn(d) => d.people | map(fn(p) => { name: p, dept: d.dept }))
| flatten
File System
File system builtins return structured data, making file operations composable in pipelines.
Reading
ls / list
List directory contents. Returns Array[Record] with file metadata.
ls "."
# [
# { name: "main.rs", path: "./main.rs", size: 2048, modified: "...", is_dir: false, extension: "rs" },
# { name: "src", path: "./src", size: 4096, modified: "...", is_dir: true, extension: "" },
# ...
# ]
# Filter to Rust files over 1KB
ls "src" | where(fn(f) => f.extension == "rs" && f.size > 1024) | sort_by "size" "desc"
cat
Read the entire contents of a file as a string.
let content = cat "README.md"
echo content
# Use in pipelines
cat "data.csv" | split "\n" | take 5
read_text
Read a file as text (alias-like behavior to cat).
let cfg = read_text "config.toml"
head
Read the first N lines of a file.
head "log.txt" 10 # First 10 lines
tail
Read the last N lines of a file.
tail "log.txt" 20 # Last 20 lines
Searching
grep
Search for a pattern in file(s). Returns Array[Record] with match details.
grep "TODO" "src/"
# [
# { file: "src/main.rs", line: 42, content: "// TODO: refactor this", match: "TODO" },
# ...
# ]
# Count matches per file
grep "unwrap" "src/" | map(fn(m) => m.file) | unique | len
find
Find files matching criteria recursively.
find "." "*.rs"
# ["./src/main.rs", "./src/lib.rs", "./tests/eval.rs", ...]
wc
Word/line/character count. Returns a Record.
wc "README.md"
# { lines: 150, words: 892, chars: 5431 }
Text Processing (File-oriented)
sort
Sort lines of input alphabetically.
cat "names.txt" | sort
uniq
Remove adjacent duplicate lines (typically used after sort).
cat "words.txt" | sort | uniq
Writing
file_write / write_file
Write content to a file, creating it if needed, overwriting if it exists.
file_write "output.txt" "Hello, world!\n"
# Write data as JSON
let data = { name: "report", items: [1, 2, 3] }
file_write "data.json" (json_stringify data)
file_append / append_file
Append content to the end of a file.
file_append "log.txt" "New log entry\n"
file_insert / insert_lines
Insert content at a specific line number.
file_insert "config.txt" 5 "new_setting = true"
Editing
file_replace / str_replace_in_file
Replace text in a file.
file_replace "config.toml" "debug = false" "debug = true"
file_patch / patch_file
Apply a structured patch to a file.
file_patch "main.rs" [
{ line: 10, old: "let x = 1;", new: "let x = 2;" }
]
file_edit / edit_file
Perform line-based edits on a file.
file_edit "src/lib.rs" { delete_lines: [5, 6], insert: { 10: "// new comment" } }
file_delete_lines / delete_lines
Remove specific lines from a file.
file_delete_lines "output.txt" 3 5 # Remove lines 3-5
File Operations
file_copy / cp
Copy a file or directory.
file_copy "src/main.rs" "backup/main.rs"
cp "data/" "data_backup/"
file_move / mv
Move or rename a file or directory.
file_move "old_name.txt" "new_name.txt"
mv "temp/output.csv" "results/final.csv"
file_mkdir / mkdir
Create a directory (and parents if needed).
mkdir "output/reports/2024"
file_exists / exists
Check whether a file or directory exists.
if (file_exists "config.toml") {
echo "Config found"
} else {
echo "Using defaults"
}
file_diff
Compare two files and return their differences.
file_diff "v1.txt" "v2.txt"
file_backup
Create a timestamped backup copy of a file.
file_backup "important.db"
# Creates important.db.20240115_143022.bak
pwd
Return the current working directory as a string.
echo (pwd) # /home/user/project
Extended File System
fs_stat / stat
Get detailed file metadata.
fs_stat "main.rs"
# { size: 2048, modified: "...", created: "...", permissions: "rw-r--r--", ... }
fs_glob / glob
Find files matching a glob pattern.
fs_glob "src/**/*.rs"
# ["src/main.rs", "src/lib.rs", "src/eval.rs", ...]
fs_tree / tree
Display directory structure as a tree.
fs_tree "src" 2 # Depth limit of 2
fs_du / du
Disk usage for a path.
fs_du "target"
# { total: 524288000, files: 1234 }
fs_df / df
Disk space information for mounted filesystems.
fs_df
# [{ mount: "/", total: 500000000000, used: 250000000000, available: 250000000000 }, ...]
fs_walk
Recursively walk a directory tree, returning all entries.
fs_walk "src" | where(fn(f) => f.extension == "rs") | len
# 15
fs_symlink
Create a symbolic link.
fs_symlink "target_path" "link_path"
fs_readlink
Read the target of a symbolic link.
fs_readlink "link_path" # "/actual/target/path"
fs_realpath
Resolve a path to its absolute canonical form.
fs_realpath "../src/main.rs"
# "/home/user/project/src/main.rs"
fs_tempfile / fs_tempdir
Create temporary files or directories.
let tmp = fs_tempfile
file_write tmp "scratch data"
let tmpdir = fs_tempdir
# Use tmpdir for temporary work
fs_watch / fs_unwatch
Watch a path for filesystem changes.
fs_watch "src/" fn(event) => {
echo "Changed: ${event.path} (${event.kind})"
}
# Later:
fs_unwatch "src/"
Pipeline Examples
# Find the 10 largest files recursively
fs_walk "."
| where(fn(f) => !f.is_dir)
| sort_by "size" "desc"
| take 10
| map(fn(f) => { name: f.name, mb: round(f.size / 1048576.0) })
# Batch rename files
ls "photos"
| where(fn(f) => f.extension == "jpeg")
| each(fn(f) => {
let new_name = replace f.name ".jpeg" ".jpg"
mv f.path "photos/${new_name}"
})
# Disk usage report
ls "."
| where(fn(f) => f.is_dir)
| map(fn(d) => { dir: d.name, size: (fs_du d.path).total })
| sort_by "size" "desc"
HTTP & Networking
AetherShell provides comprehensive networking builtins for HTTP requests, web automation, and low-level network operations. All return structured data for pipeline processing.
HTTP Requests
http_get
Make an HTTP GET request. Returns a Record with status, headers, body, and ok fields.
let resp = http_get "https://api.github.com/repos/rust-lang/rust"
echo resp.status # 200
echo resp.ok # true
# Parse JSON response
let repo = json_parse resp.body
echo repo.stargazers_count
web_get / curl
Extended GET request with options support.
web_get "https://api.example.com/data" {
headers: { "Authorization": "Bearer ${token}" },
timeout: 5000
}
web_post
Make a POST request with a body.
let resp = web_post "https://api.example.com/items" {
headers: { "Content-Type": "application/json" },
body: json_stringify { name: "widget", price: 9.99 }
}
web_fetch / fetch
General-purpose HTTP client supporting all methods.
let resp = web_fetch "https://api.example.com/items/1" {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: json_stringify { name: "updated widget" }
}
JSON APIs
web_json_get
GET request that automatically parses the JSON response body.
let users = web_json_get "https://api.example.com/users"
users | where(fn(u) => u.active) | map(fn(u) => u.name)
web_json_post
POST request with automatic JSON serialization/deserialization.
let result = web_json_post "https://api.example.com/login" {
username: "admin",
password: "secret"
}
echo result.token
Web Scraping
web_scrape / scrape
Scrape content from a web page with CSS selectors.
let titles = web_scrape "https://news.ycombinator.com" "a.storylink"
titles | take 10 | each(fn(t) => echo t.text)
web_html_to_text
Extract plain text from HTML content.
let html = (http_get "https://example.com").body
let text = web_html_to_text html
web_html_to_markdown
Convert HTML content to Markdown.
let md = web_html_to_markdown (http_get "https://example.com").body
file_write "page.md" md
web_extract_emails
Extract email addresses from text or HTML.
let emails = web_extract_emails (cat "contacts.html")
echo emails # ["user@example.com", ...]
web_extract_phones
Extract phone numbers from text.
let phones = web_extract_phones page_content
URL Operations
web_parse_url
Parse a URL into its components.
web_parse_url "https://example.com:8080/path?key=val#section"
# { scheme: "https", host: "example.com", port: 8080, path: "/path", query: "key=val", fragment: "section" }
web_encode_url / web_decode_url
URL-encode or decode a string.
web_encode_url "hello world & more" # "hello%20world%20%26%20more"
web_decode_url "hello%20world" # "hello world"
web_parse_query / web_build_query
Parse query strings to Records, or build query strings from Records.
web_parse_query "name=Ada&lang=Rust"
# { name: "Ada", lang: "Rust" }
web_build_query { page: 2, limit: 50 }
# "page=2&limit=50"
web_check_url
Check if a URL is reachable (returns status info).
let check = web_check_url "https://example.com"
echo check.reachable # true
echo check.status # 200
Downloads
web_download
Download a file from a URL.
web_download "https://example.com/data.csv" "downloads/data.csv"
web_open_url
Open a URL in the system default browser.
web_open_url "https://docs.aethershell.dev"
Advanced Web
web_rest_api
High-level REST API client with authentication and pagination support.
let client = web_rest_api "https://api.example.com" {
auth: { type: "bearer", token: env.API_TOKEN },
base_headers: { "Accept": "application/json" }
}
web_websocket
Connect to a WebSocket endpoint.
let ws = web_websocket "wss://stream.example.com/events"
web_graphql
Execute a GraphQL query.
let result = web_graphql "https://api.example.com/graphql" {
query: "{ users { id name email } }",
variables: { limit: 10 }
}
web_headers / web_cookies
Extract headers or cookies from a response.
let h = web_headers (http_get "https://example.com")
echo h["Content-Type"]
web_robots_txt / web_sitemap
Fetch and parse robots.txt or sitemap.xml.
let robots = web_robots_txt "https://example.com"
let sitemap = web_sitemap "https://example.com"
web_json_path / web_xpath
Query JSON with JSONPath or HTML with XPath expressions.
let data = web_json_get "https://api.example.com/data"
web_json_path data "$.users[*].name"
let html = (http_get "https://example.com").body
web_xpath html "//h1/text()"
Network Operations
net_interfaces / ifconfig
List network interfaces with their addresses and status.
net_interfaces
# [{ name: "eth0", ip: "192.168.1.100", mac: "aa:bb:cc:dd:ee:ff", up: true }, ...]
net_ip
Get the machine’s IP address.
echo (net_ip) # 192.168.1.100
net_dns_lookup
Resolve a hostname to IP addresses.
net_dns_lookup "example.com"
# ["93.184.216.34"]
net_ping / ping
Ping a host and return latency information.
net_ping "google.com"
# { host: "google.com", latency_ms: 12.5, reachable: true }
net_ports
List open ports on the local machine.
net_ports | where(fn(p) => p.state == "LISTEN")
net_connections
List active network connections.
net_connections
| where(fn(c) => c.remote_port == 443)
| map(fn(c) => c.remote_addr)
| unique
net_whois
WHOIS lookup for a domain.
net_whois "example.com"
net_traceroute
Trace the route to a host.
net_traceroute "example.com"
# [{ hop: 1, ip: "192.168.1.1", latency_ms: 1.2 }, ...]
net_stats / net_bandwidth
Network statistics and bandwidth usage.
net_stats
# { bytes_sent: 1234567, bytes_recv: 7654321, packets_sent: 1000, ... }
Pipeline Examples
# API data analysis
web_json_get "https://api.github.com/repos/rust-lang/rust/contributors?per_page=100"
| map(fn(c) => { login: c.login, commits: c.contributions })
| sort_by "commits" "desc"
| take 10
# Health check multiple endpoints
let endpoints = ["https://api1.example.com/health", "https://api2.example.com/health"]
endpoints
| map(fn(url) => {
let check = web_check_url url
{ url: url, status: check.status, ok: check.reachable }
})
| where(fn(e) => !e.ok)
# Download and process CSV
web_download "https://data.example.com/export.csv" "/tmp/data.csv"
let rows = cat "/tmp/data.csv" | split "\n" | map(fn(line) => split line ",")
echo "Loaded ${len rows} rows"
String Operations
String builtins provide text manipulation, splitting, joining, and pattern matching. All return new values without modifying the original.
Case Conversion
upper
Convert a string to uppercase.
upper "hello" # "HELLO"
"hello" | upper # "HELLO"
lower
Convert a string to lowercase.
lower "HELLO" # "hello"
"HELLO" | lower # "hello"
Whitespace
trim
Remove leading and trailing whitespace.
trim " hello " # "hello"
" spaced " | trim # "spaced"
Splitting & Joining
split
Split a string by a delimiter, returning an array.
split "a,b,c" "," # ["a", "b", "c"]
"one two three" | split " " # ["one", "two", "three"]
# Split into lines
cat "log.txt" | split "\n"
# Split CSV row
"Alice,25,Engineer" | split ","
# ["Alice", "25", "Engineer"]
join
Join an array of strings with a delimiter.
join ["a", "b", "c"] "," # "a,b,c"
["hello", "world"] | join " " # "hello world"
# Reassemble after transformation
cat "data.csv"
| split "\n"
| where(fn(line) => contains line "ERROR")
| join "\n"
Search & Match
contains
Check if a string contains a substring.
contains "hello world" "world" # true
"hello world" | contains "xyz" # false
# Filter lines
cat "log.txt" | split "\n" | where(fn(l) => contains l "ERROR")
starts_with
Check if a string starts with a prefix.
starts_with "hello" "hel" # true
"README.md" | starts_with "READ" # true
ends_with
Check if a string ends with a suffix.
ends_with "main.rs" ".rs" # true
"photo.jpg" | ends_with ".png" # false
Replacement
replace
Replace all occurrences of a substring.
replace "hello world" "world" "Rust" # "hello Rust"
"foo-bar-baz" | replace "-" "_" # "foo_bar_baz"
# Multi-step replacement
"Hello, World!"
| replace "," ""
| replace "!" ""
| lower
# "hello world"
String in Pipelines
Strings integrate seamlessly with AetherShell’s pipeline model. When a single-parameter lambda receives an array, it auto-maps:
# Uppercase every element
["hello", "world"] | upper
# ["HELLO", "WORLD"]
# Trim all strings
[" a ", " b ", "c"] | trim
# ["a", "b", "c"]
Practical Examples
Log Parsing
cat "app.log"
| split "\n"
| where(fn(line) => contains line "ERROR")
| map(fn(line) => {
let parts = split line " "
{
timestamp: parts[0],
level: parts[1],
message: join (slice parts 2 (len parts)) " "
}
})
| sort_by "timestamp" "desc"
| take 10
CSV Processing
let lines = cat "users.csv" | split "\n"
let headers = split (first lines) ","
let rows = lines | slice 1 (len lines) | map(fn(line) => split line ",")
rows | map(fn(row) => {
name: trim row[0],
email: lower (trim row[1]),
dept: upper (trim row[2])
})
Text Transformation
# Snake_case to camelCase
let snake = "my_variable_name"
let parts = split snake "_"
let camel = (first parts) + (parts | slice 1 (len parts) | map(fn(p) => {
let chars = split p ""
(upper (first chars)) + (join (slice chars 1 (len chars)) "")
}) | join "")
echo camel # "myVariableName"
Batch File Renaming
ls "."
| where(fn(f) => ends_with f.name ".txt")
| each(fn(f) => {
let new_name = replace f.name ".txt" ".md"
mv f.path new_name
})
Encoding (via ab_encode / ab_decode)
For base64 and hex encoding, see the crypto builtins (crypto_base64_encode, crypto_hex_encode), or use the general ab_encode / ab_decode:
ab_encode "hello" "base64" # "aGVsbG8="
ab_decode "aGVsbG8=" "base64" # "hello"
Math Operations
Math builtins operate on numeric values (Int and Float). They handle type promotion automatically — operations between Int and Float produce Float results.
Rounding
floor
Round down to the nearest integer.
floor 3.7 # 3
floor -2.3 # -3
floor 5 # 5 (no-op for Int)
ceil
Round up to the nearest integer.
ceil 3.2 # 4
ceil -2.7 # -2
ceil 5 # 5 (no-op for Int)
round
Round to the nearest integer (half rounds up).
round 3.5 # 4
round 3.4 # 3
round -2.5 # -2
Absolute Value
abs
Return the absolute (non-negative) value.
abs -42 # 42
abs 3.14 # 3.14
abs -99.9 # 99.9
Powers & Roots
sqrt
Return the square root as a Float.
sqrt 16 # 4.0
sqrt 2 # 1.4142135623730951
pow
Raise a number to a power.
pow 2 10 # 1024
pow 3.0 0.5 # 1.7320508075688772 (same as sqrt 3)
Min / Max
min
Return the smaller of two values, or the minimum of an array.
min 3 7 # 3
[5, 2, 8, 1] | min # 1
max
Return the larger of two values, or the maximum of an array.
max 3 7 # 7
[5, 2, 8, 1] | max # 8
Aggregation
These also appear in Collections since they operate on arrays:
sum
Sum all numeric elements of an array.
[1, 2, 3, 4, 5] | sum # 15
range 1 101 | sum # 5050
avg / mean
Compute the arithmetic mean.
[10, 20, 30] | avg # 20.0
[1.5, 2.5, 3.0] | mean # 2.3333333333333335
product
Multiply all elements together.
[1, 2, 3, 4, 5] | product # 120 (5!)
range 1 11 | product # 3628800 (10!)
Arithmetic Operators
Standard arithmetic operators work on both Int and Float:
| Operator | Description | Example | Result |
|---|---|---|---|
+ | Addition | 3 + 4 | 7 |
- | Subtraction | 10 - 3 | 7 |
* | Multiplication | 6 * 7 | 42 |
/ | Division | 10 / 3 | 3 |
% | Modulo | 10 % 3 | 1 |
Type promotion: When mixing Int and Float, the result is Float:
3 + 2.0 # 5.0 (Float)
10 / 3 # 3 (Int division)
10 / 3.0 # 3.3333... (Float division)
Comparison Operators
| Operator | Description | Example |
|---|---|---|
== | Equal | 3 == 3 |
!= | Not equal | 3 != 4 |
< | Less than | 3 < 5 |
> | Greater than | 5 > 3 |
<= | Less or equal | 3 <= 3 |
>= | Greater or equal | 5 >= 5 |
Practical Examples
Statistical Summary
let data = [23, 45, 12, 67, 34, 89, 11, 56]
let stats = {
count: len data,
sum: data | sum,
min: data | min,
max: data | max,
avg: data | avg,
range: (data | max) - (data | min)
}
echo stats
# { count: 8, sum: 337, min: 11, max: 89, avg: 42.125, range: 78 }
File Size Analysis
let sizes = ls "src" | map(fn(f) => f.size)
echo "Total: ${sum sizes} bytes"
echo "Average: ${round(avg sizes)} bytes"
echo "Largest: ${max sizes} bytes"
echo "Smallest: ${min sizes} bytes"
Fibonacci Sequence
range 0 10 | reduce(fn(acc, _) => {
let n = len acc
if n < 2 { push acc (n) }
else { push acc (acc[n-1] + acc[n-2]) }
}, [])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Distance Calculation
let p1 = { x: 3.0, y: 4.0 }
let p2 = { x: 7.0, y: 1.0 }
let dist = sqrt(pow(p2.x - p1.x, 2) + pow(p2.y - p1.y, 2))
echo dist # 5.0
Percentage Breakdown
ls "src"
| where(fn(f) => !f.is_dir)
| map(fn(f) => { name: f.name, size: f.size })
| map(fn(f) => {
let total = ls "src" | map(fn(g) => g.size) | sum
{ ...f, pct: round(f.size * 100.0 / total) }
})
| sort_by "pct" "desc"
AI Integration
AetherShell has first-class support for AI models and agents.
Quick Start
# Simple AI query
ai("What is the capital of France?")
# → "The capital of France is Paris."
# With specific model
ai("Explain monads in simple terms", {
model: "gpt-4o"
})
Supported Providers
AetherShell supports 25+ AI providers out of the box:
| Provider | Models | Setup |
|---|---|---|
| OpenAI | GPT-4o, GPT-4, GPT-3.5 | OPENAI_API_KEY |
| Anthropic | Claude 3 Opus/Sonnet/Haiku | ANTHROPIC_API_KEY |
| Gemini Pro, Gemini Flash | GOOGLE_API_KEY | |
| Meta | Llama 3, CodeLlama | Via Ollama/Together |
| Mistral | Mistral Large, Codestral | MISTRAL_API_KEY |
| Cohere | Command R, Command R+ | COHERE_API_KEY |
| xAI | Grok | XAI_API_KEY |
| DeepSeek | DeepSeek V3, R1 | DEEPSEEK_API_KEY |
| Ollama | Any local model | Local install |
| OpenRouter | 100+ models | OPENROUTER_API_KEY |
Model URIs
Specify models using the provider:model format:
# OpenAI
ai("Query", { model: "openai:gpt-4o-mini" })
# Anthropic
ai("Query", { model: "claude:claude-3-sonnet-20240229" })
# Google
ai("Query", { model: "gemini:gemini-pro" })
# Local Ollama
ai("Query", { model: "ollama:llama3" })
# OpenRouter (any model)
ai("Query", { model: "openrouter:meta-llama/llama-3-70b-instruct" })
AI Function Options
ai("Your prompt", {
# Model selection
model: "gpt-4o",
# Generation parameters
temperature: 0.7, # Creativity (0-2)
max_tokens: 4096, # Response length limit
top_p: 0.95, # Nucleus sampling
# Context
system: "You are a helpful assistant", # System prompt
context: read("data.txt"), # Additional context
# Output format
format: "json", # Request JSON output
stream: true, # Stream response
# Images (multimodal)
images: ["image.png"],
})
Conversation History
Maintain context across queries:
let history = []
let chat = fn(message) => {
let response = ai(message, {
messages: history,
model: "gpt-4o"
})
# Update history
history = [...history,
{ role: "user", content: message },
{ role: "assistant", content: response }
]
response
}
chat("What is Rust?")
chat("How does it handle memory?") # Remembers context
Multimodal (Vision)
Analyze images with vision-capable models:
# Describe an image
ai("What's in this image?", {
model: "gpt-4o",
images: ["photo.jpg"]
})
# Multiple images
ai("Compare these two images", {
model: "claude:claude-3-sonnet",
images: ["before.png", "after.png"]
})
# URL images
ai("Analyze this diagram", {
model: "gemini:gemini-pro-vision",
images: ["https://example.com/diagram.png"]
})
Structured Output
Get structured JSON responses:
let result = ai("Extract the person's name and age from: John is 30 years old", {
model: "gpt-4o",
format: "json",
schema: {
type: "object",
properties: {
name: { type: "string" },
age: { type: "integer" }
}
}
})
let data = json_parse(result)
print(data.name) # "John"
print(data.age) # 30
Error Handling
let result = try {
ai("Query that might fail", { model: "gpt-4o" })
} catch err {
print("AI error: " + err.message)
"fallback response"
}
Provider-Specific Features
OpenAI Function Calling
let tools = [
{
name: "get_weather",
description: "Get current weather for a location",
parameters: {
type: "object",
properties: {
location: { type: "string" }
}
}
}
]
let response = ai("What's the weather in Paris?", {
model: "gpt-4o",
tools: tools
})
Claude with System Prompts
ai("Translate to French: Hello, world!", {
model: "claude:claude-3-haiku",
system: "You are a professional translator. Respond only with the translation."
})
Local Models with Ollama
# First, pull the model
ollama pull llama3
ollama pull codellama
# Use in AetherShell
ai("Write a Python function to sort a list", {
model: "ollama:codellama"
})
Best Practices
-
Set defaults - Configure your preferred provider:
export AETHER_AI=openai export OPENAI_API_KEY=sk-... -
Use appropriate models - GPT-4o for complex tasks, GPT-3.5 for simple ones
-
Control costs - Set
max_tokensto limit response length -
Handle errors - AI APIs can fail; always have fallbacks
-
Stream long responses - Set
stream: truefor better UX
AI Providers
AetherShell has built-in AI capabilities with a provider-agnostic architecture. Connect to cloud APIs, local models, or self-hosted inference servers — all using the same simple syntax.
Quick Start
# Set your API key
set_env "OPENAI_API_KEY" "sk-..."
# Ask a question
ai "What is Rust's ownership model?"
# Specify a model
ai "Explain monads" { model: "openai:gpt-4o" }
Model URI Scheme
AetherShell uses a URI scheme to reference models across providers:
| URI | Provider | Example |
|---|---|---|
openai:model-name | OpenAI | openai:gpt-4o-mini |
ollama:model-name | Ollama (local) | ollama:llama3 |
compat:model-name | OpenAI-compatible API | compat:mixtral |
tgi:model-name | HuggingFace TGI | tgi:mistral-7b |
vllm:model-name | vLLM | vllm:meta-llama/Llama-3-8B |
llamacpp:model-name | llama.cpp | llamacpp:mistral-7b |
# Use different providers
ai "Hello" { model: "openai:gpt-4o" }
ai "Hello" { model: "ollama:llama3" }
ai "Hello" { model: "compat:mixtral" }
Providers
OpenAI
The default cloud provider. Requires an API key.
set_env "OPENAI_API_KEY" "sk-..."
set_env "AETHER_AI" "openai"
ai "Explain closures in Rust"
Environment variables:
OPENAI_API_KEY— API authentication keyOPENAI_MODEL— Default model (default:gpt-4o-mini)
Ollama (Local)
Run models locally with Ollama. No API key needed.
set_env "AETHER_AI" "ollama"
ai "Summarize this code" { model: "ollama:codellama" }
Environment variables:
OLLAMA_URL— Ollama endpoint (default:http://localhost:11434)OLLAMA_MODEL— Default model (default:llama3)
OpenAI-Compatible
Any server implementing the OpenAI API format (LiteLLM, LocalAI, etc.).
set_env "AETHER_AI" "compat"
set_env "AETHER_COMPAT_BASE" "http://localhost:8000/v1"
ai "Hello" { model: "compat:mixtral" }
Environment variables:
AETHER_COMPAT_BASE— API base URL (default:http://localhost:8000/v1)AETHER_COMPAT_MODEL— Default model (default:mixtral)
HuggingFace TGI
Connect to a Text Generation Inference server.
set_env "AETHER_AI" "tgi"
set_env "TGI_URL" "http://localhost:8080"
vLLM
Connect to a vLLM inference server.
set_env "VLLM_URL" "http://localhost:8000/v1"
set_env "VLLM_MODEL" "meta-llama/Llama-3-8B"
llama.cpp
Connect to a llama.cpp server.
set_env "LLAMACPP_URL" "http://localhost:8080/v1"
Provider Selection
The AETHER_AI environment variable selects the default provider:
set_env "AETHER_AI" "openai" # Use OpenAI
set_env "AETHER_AI" "ollama" # Use Ollama
set_env "AETHER_AI" "compat" # Use OpenAI-compatible server
set_env "AETHER_AI" "tgi" # Use TGI
Override per-call with the model option:
# Default is OpenAI, but use Ollama for this one call
ai "Quick question" { model: "ollama:llama3" }
Multimodal AI
AetherShell supports sending images, audio, and video to models that accept them.
Images
ai "Describe this image" { images: ["photo.jpg"] }
ai "Compare these" { images: ["before.png", "after.png"] }
Audio
ai "Transcribe this recording" { audio: ["meeting.mp3"] }
Video
ai "What happens in this clip?" { video: ["demo.mp4"] }
Combined
ai "Analyze this screenshot and narration" {
images: ["screen.png"],
audio: ["narration.mp3"]
}
Note: Multimodal support depends on the provider. OpenAI supports images; Ollama supports images with vision models. Audio and video support varies by model.
Backend Detection
Discover which AI backends are available on your system:
ai_backends
# [
# { provider: "openai", available: true, model: "gpt-4o-mini" },
# { provider: "ollama", available: true, url: "http://localhost:11434", models: ["llama3", "codellama"] },
# { provider: "vllm", available: false },
# ...
# ]
AI Shell Helpers
Built-in AI-powered shell assistance:
# Get command suggestions
ai-suggest "find all rust files larger than 10KB"
# Suggests: ls "." | where(fn(f) => f.extension == "rs" && f.size > 10240)
# Explain a command
ai-explain 'ls "src" | where(fn(f) => f.size > 1000) | sort_by "size" "desc"'
# Fix a broken command
ai-fix 'ls src | filter(size > 100)'
# AI-powered tab completion
ai-complete "ls src | wh"
Pipeline Integration
AI calls compose naturally with pipelines:
# Summarize a file
cat "README.md" | ai "Summarize this document"
# Classify data
["bug report", "feature request", "question"]
| map(fn(item) => {
let category = ai "Classify: ${item}" { model: "openai:gpt-4o-mini" }
{ text: item, category: category }
})
# Generate documentation
ls "src"
| where(fn(f) => f.extension == "rs")
| map(fn(f) => { file: f.name, doc: ai "Write a one-line description of: ${cat f.path}" })
Global Override
Set a global model URI that overrides all defaults:
set_env "AETHER_MODEL_URI" "ollama:codellama"
# All ai/agent calls now use this model unless explicitly overridden
Creating Agents
An agent is a model in a ReAct loop: it is given a goal and a set of tools,
emits a JSON tool call, sees the result, and repeats until it emits a final
answer or runs out of steps. agent runs that loop and returns the final
answer as a string.
Prerequisites
An agent needs a provider. Without one, every call fails immediately and says so:
agent("list the files here")
# error[E_UNKNOWN]: No AI provider configured.
# Set AETHER_AI environment variable to: irongate, openai, ollama, or compat
Running an agent
agent(<goal>, [tools...], [max_steps], [dry_run])
Only the goal is required. It is a task, not a persona — the system prompt is supplied by the loop, and the goal is the user turn.
agent("Find the three largest files under src/")
Tools are named individually or as an array. They are ordinary builtins:
agent("Find every .log file over 1 MB", "ls", "find", "stat")
agent("Summarise what this project builds", ["cat", "ls", "grep"])
After the tools come two optional positional arguments: an integer step limit (default 8) and a boolean dry-run flag.
# Twenty steps, and do not actually execute the tool calls.
agent("Reorganise the test fixtures", ["ls", "mv"], 20, true)
The record form
The same call can be written as a record, which is easier to build
programmatically. Exactly four keys are read — goal, tools, max_steps and
dry_run — and anything else is ignored:
agent({
goal: "Summarise the open TODOs",
tools: ["grep", "cat"],
max_steps: 12,
dry_run: false
})
A record with no goal is refused with agent config requires {goal: String}.
Each call starts fresh
agent builds a new dialogue every time — a system prompt and your goal — and
returns a string. There is no session, no conversation history, and no reset
builtin, so a second call knows nothing about the first. To carry context
forward, put it in the next goal:
let plan = agent("Break down building a CLI todo app into steps")
let code = agent("Implement this plan: " + plan, ["write", "cat"])
This is also how multiple agents are composed; see Agent Swarms for the coordinated form.
Which tools an agent may run
Naming a tool in the call does not by itself permit it. Shell-command execution
is default-deny: with AGENT_ALLOW_CMDS unset, no command is allowed, and the
refusal says exactly that.
export AGENT_ALLOW_CMDS=ls,cat,grep,git
The list is read once, when the security configuration is first built, so
export it before starting ae. Setting it from inside a running shell with
env_set or set_env takes effect only if no command has been validated yet.
Anything outside the list is refused by name, and both the allowed and the refused attempts are written to the security audit log.
MCP tools
agent_with_mcp takes a goal and an array of MCP tool names, for agents that
should reach tools served over the Model Context Protocol rather than builtins:
agent_with_mcp("Check the deployment status", ["k8s_get_pods", "k8s_logs"])
Rate limiting
agent is capped at 10 calls per minute per process. Exceeding it fails with
Agent rate limit exceeded rather than queuing.
Security
An agent runs real commands. Beyond AGENT_ALLOW_CMDS:
ae --agentputs the shell in default-deny mode, gating destructive effect classes behind approval.ae --workspace <dir>confines writes and destructive operations to that directory.dry_runlets you watch the loop’s intent without executing it.
See Security & Auth.
Agent Swarms
What swarm currently does
swarm is a builtin, and it works, but not the way its name suggests: it takes
the same arguments as agent and delegates to the same
single-agent loop. ai::agents::swarm::run_sync is one line — it calls
ai::agents::run_sync.
swarm("Analyze this project thoroughly", ["ls", "cat", "grep"], 12)
That runs one agent with those three tools and a twelve-step limit. It does not create multiple agents.
The multi-agent engine below exists in the library and is not reachable from
the shell. Swarm, its blackboard and its two coordinators are defined in
src/ai.rs and never constructed by any builtin. They are documented here so
the distinction is on the record, not because you can call them today.
- Coordination policies.
RoundRobintakes agents in turn;Routersends each turn to the agent whose declared capabilities best match. Both coordinators are implemented; neither is selectable from the shell, because theswarmbuiltin reads onlygoal,tools,max_stepsanddry_run. - Blackboard. Agents would share a message list with kinds
note,thoughtandfinal, and could delegate with{"type": "delegate", "target": "...", "input": "..."}.
If you want several agents today, chain calls and pass each result into the next goal — see Creating Agents.
Model override
Both builtins honour an environment variable for the model URI:
set_env("AETHER_AGENT_MODEL_URI", "openai:gpt-4o")
set_env("AETHER_SWARM_AGENT_MODEL_URI", "ollama:llama3")
See Workflows for the orchestration surface, which is registered and runnable.
There is no model key in the record form. agent/swarm read exactly four
keys — goal, tools, max_steps, dry_run — and silently ignore the rest,
so a model: entry has no effect.
Choosing tools
Tools are the builtins the agent may call. Name them individually, or as an array:
agent("Find large files", ["ls", "cat", "grep"])
An empty array gives the agent no tools at all, not every tool. Tool names are resolved one by one against the registry, so an empty list resolves to an empty toolset and the agent can only answer from the model.
agent("Analyze the project", []) # no tools; the model answers unaided
A name that is not a builtin is not rejected either: the resolver wraps any string it is given, so a typo — or a model URI passed where the tool list belongs – is accepted here and only fails when the agent tries to call it.
Agent security
Command allowlist
AGENT_ALLOW_CMDS restricts which shell commands an agent may run, and it is
default-deny: unset, nothing is allowed, and the refusal says so.
export AGENT_ALLOW_CMDS=ls,cat,grep,wc
The list is read once, when the security configuration is first built. Setting
it from inside a running shell with set_env only takes effect if no command
has been validated yet, so prefer exporting it before starting ae.
Measured limits
- Prompt validation — goals are capped at 4,000 characters and at most 50 newlines, rejected if empty or containing a null byte, and screened for injection.
- Argument validation — shell metacharacters are rejected in tool arguments.
- Rate limiting — 10 agent calls per minute, and within the agent API, 10 plans and 5 executions per minute.
- Output cap — 10 MB per execution.
- Timeout — 30 seconds per execution, on every platform.
- Memory — 512 MB, on Linux and macOS only. The Windows
configure_sandboxis a documented no-op: Job Object sandboxing is a TODO, so on Windows you get the timeout and the output cap and nothing else. - No shell escape — the
shbuiltin is gated behindAETHER_ALLOW_SH=trueand is unavailable by default.
Agents with MCP tools
agent_with_mcp takes a goal, an array of tool names, and optionally one
endpoint or an array of endpoints:
agent_with_mcp("Analyze repository", ["read_file", "list_dir"], "http://localhost:9090")
The agent discovers the available tools from the server and can call them
during its loop. Note that mcp_server_start takes a configuration record,
not a URL string.
Practical examples
Code review
agent({
goal: "Review src/main.rs for potential bugs, style issues, and missing error handling",
tools: ["cat", "grep", "wc"],
max_steps: 8
})
Project analysis
agent({
goal: "Describe this Rust project: structure, dependencies, and test coverage",
tools: ["ls", "cat", "grep", "find", "wc", "fs_tree"],
max_steps: 25
})
Git summary
export AGENT_ALLOW_CMDS=ls,cat,grep,git
agent({
goal: "Summarize all changes since the last release tag",
tools: ["git_log", "git_diff", "cat"],
max_steps: 10
})
Tool Use
An agent is only as useful as the things it can actually run. AetherShell ships
a catalogue of external OS tools — grep, curl, git, and so on — each with a
description, a parameter list, the platforms it runs on, and a safety level. The
same catalogue backs both the agent loop and the tool_* builtins below, so
what an agent can reach is exactly what you can inspect from the prompt.
Listing the catalogue
tool_list() | len
# 198
tool_list() | first
# {category: TextProcessing, command: grep, description: Search text patterns in files,
# name: grep, requires_admin: false, safety: Safe, supported_os: [len=5]}
Finding a tool
tool_search takes a query and returns the tools recommended for it,
falling back to a plain name and description match when nothing is recommended:
tool_search("http") | len
# 7
tool_info returns the full record for one tool, including its parameters and
worked examples:
tool_info("ls")
# {category: FileSystem, command: ls, common_args: [len=3],
# description: List directory contents, examples: [len=1], name: ls,
# parameters: [len=3], requires_admin: false, safety_level: Safe,
# supported_os: [len=5]}
Schemas for model tool-calling
tool_schema renders the catalogue as OpenAI-style function schemas, ready to
hand to a model that supports tool calling:
tool_schema() | first
# {function: {…}, type: function}
Running a tool
tool_exec(<name>, [args], [allow_dangerous])
tool_execute is an alias for the same builtin. Only the name is required;
args may be an array of strings or a single string.
tool_exec("git", ["status", "--short"])
Two things can stop a call, and both report rather than guess:
-
Platform. A tool is only run where it is supported. On Windows,
tool_exec("ls", ["docs/book"])returnserror[E_UNKNOWN]: Tool execution failed: Tool 'ls' is not supported on Windowsinstead of falling back to something that merely looks similar. -
Safety. Every tool carries a level —
Safe,Caution,Dangerous, orCritical. Of the 198 catalogued tools, 131 areSafe, 49 areCaution, 14 areDangerousand 4 areCritical:tool_list() | where(fn(t) => t.safety == "Dangerous") | len # 14SafeandCautionrun normally.DangerousandCriticalare refused outright unless the third argument istrue, and the refusal names the level rather than failing vaguely:tool_exec("iptables", ["-L"]) # error[E_UNKNOWN]: Tool execution failed: Tool 'iptables' has safety level # Critical. Set allow_dangerous=true to execute.A tool marked
requires_adminis additionally refused unless the process is actually privileged.
Over MCP
ae mcp stdio serves the shell as an MCP server. tools/list returns three
tools, not one per builtin:
| Tool | Purpose |
|---|---|
ontology_manifest | the categories, with counts and effect classes |
ontology_describe | expand a category into its builtins, or one builtin into full detail |
aether | invoke a builtin by name |
That is deliberate. Advertising several hundred tool schemas would spend an
agent’s context before it had read a single result; the manifest is the compact
index and detail is fetched for the slice actually needed. Every builtin is
reachable through aether, and every call goes through the same safety model
as one typed at the prompt.
tool_exec also passes through the ordinary execution guard, so agent mode’s
effect gate and the workspace jail apply to it exactly as they do to any other
process-spawning builtin. See Security & Auth.
Workflows
A workflow is a template of steps plus an instance that runs it. Steps are
records, a step’s run names a builtin, and the engine passes data between them
through named variables.
This chapter was removed from the book once, because the sixteen workflow_*
names existed in the source and none of them were registered: workflow_create
at the prompt answered unknown builtin. Everything below was run against the
shell before being written down.
A first workflow
let t = workflow_pipeline("demo", [
{id: "up", run: "upper", args: ["hello"], output: "$.shouted"},
{id: "count", run: "len", input: "$.shouted"}
])
let w = workflow_create(t, {input: null})
workflow_execute(w)
# 5
workflow_pipeline returns a template id; workflow_create returns a workflow
id; workflow_execute runs it and returns the last step’s value. The pipeline
template declares input as a required parameter, which is why the record is
passed even when the value is unused.
Steps
A step is a record. Exactly one key decides what kind of step it is:
| Key | Step | Example |
|---|---|---|
run | call a builtin | {run: "grep", args: ["TODO", "src"]} |
agent | run an agent | {agent: "reviewer", prompt: "$.task"} |
url | make an HTTP request | {url: "https://example.com", method: "POST", body: {...}} |
delay_ms | wait | {delay_ms: 500} |
emit | emit an event | {emit: "ready", payload: 7} |
wait | wait for an event | {wait: "ready", timeout_ms: 2000} |
workflow | run another template | {workflow: "<template id>"} |
Any step also takes:
| Key | Meaning |
|---|---|
id | its name in results and events (defaults to step-N) |
input | the variable to feed it, as $.name |
output | where to store its result, as $.name |
when | a condition; the step is skipped unless it holds |
timeout_ms | how long it may run before failing |
retries | how many times to retry, with exponential backoff |
compensate | the step to run if a later saga step fails |
input/output are how stages connect. Above, up stores "HELLO" in
shouted and count reads it.
Conditions
when is AetherShell source, evaluated against the workflow’s variables:
let t = workflow_pipeline("guarded", [
{id: "big", run: "upper", args: ["over"], when: "threshold > 5"}
])
A condition that does not hold skips the step; one that fails to parse is treated as not holding, because a guard you cannot read must not count as satisfied.
Patterns
workflow_pipeline(name, steps) # sequential
workflow_map_reduce(name, mapper, reducer) # parallel map, then reduce
workflow_fan_out(name, workers, aggregator) # parallel workers, then fan-in
workflow_scatter_gather(name, targets, strategy) # scatter to agents, gather
workflow_saga(name, transactions) # with compensation on failure
workflow_register({name: ..., steps: [...]}) # a plain sequence
Fan-out runs its workers concurrently and hands their results to the aggregator:
let t = workflow_fan_out("scan",
[{id: "a", run: "upper", args: ["a"]},
{id: "b", run: "upper", args: ["b"]}],
{id: "agg", run: "len"})
workflow_execute(workflow_create(t, {input: null}))
# 2
workflow_scatter_gather’s third argument is a strategy, not a step: "all",
"first", {first: 3}, {timeout_ms: 5000} or {consensus: 0.75}.
Sagas
A saga takes [action, compensation] pairs. If a later action fails, the
compensations for the actions that already succeeded run in reverse order:
let t = workflow_saga("order", [
[{id: "charge", run: "upper", args: ["charged"]},
{id: "refund", run: "upper", args: ["refunded"], output: "$.undone"}],
[{id: "ship", run: "nope_not_real"},
{id: "unship", run: "upper", args: ["unshipped"]}]
])
workflow_execute(workflow_create(t, {input: null}))
# error[E_UNKNOWN]: Saga failed at step saga-step-1: unknown builtin: nope_not_real
The refund ran; $.undone holds "REFUNDED".
Inspecting a run
workflow_status(w)
# {id: …, template: …, status: completed, steps_completed: 1, variables: {…}}
workflow_status(w).variables.out
workflow_list()
workflow_templates()
workflow_cancel, workflow_pause and workflow_resume take a workflow id.
Circuit breakers
circuit_breaker_create("payments", {failure_threshold: 3})
circuit_breaker_status("payments")
# closed
States are closed, open and half-open. The optional record also takes
success_threshold and reset_timeout_ms.
Safety
A workflow is not a way around the shell’s gates.
- Every
runstep goes through the same dispatcher as a call typed at the prompt, so the effect gate, the workspace jail and the audit chain all apply to it. - An
agentstep goes through theagentbuiltin, soAGENT_ALLOW_CMDSand the rate limit still apply. - A
urlstep goes through the same egress allowlist and SSRF validation ashttp_get. - In agent mode,
workflow_executeandworkflow_createrequire approval — running a workflow is the same capability as running the commands inside it.workflow_templatesand the other read-only calls do not. - A
workflowstep that reaches its own template is refused after 8 levels rather than recursing until the stack ends.
Limits
- Templates and instances live in the shell process. They are not persisted, so they are gone when it exits.
- Workflow builtins cannot be called from inside a single-threaded async
context; blocking it would deadlock, so they report instead. Use a
workflowstep to nest one workflow inside another. Choreographyruns its steps sequentially and emits events; there is no event-driven scheduler behind it.
TUI Overview
AetherShell’s Terminal User Interface (TUI) provides a rich, multi-pane interface for AI chat, agent management, media browsing, and more — all within your terminal.
Launching the TUI
ae tui # standard launch
ae --tui # alternative flag
RUST_LOG=debug ae tui # with debug logging
Interface Layout
The TUI has three main areas:
┌──────────────────────────────────────────────────────┐
│ Chat │ Agents │ Media │ Settings │ Distributed │ … │ ← Tabs
├──────────────────────────────────────────────────────┤
│ │
│ Main Content │ ← Mode-specific
│ │
├──────────────────────────────────────────────────────┤
│ > Type a message... │ Help: ? │ ← Input + Help
└──────────────────────────────────────────────────────┘
- Header — Tab bar showing the current mode
- Main Content — Changes based on the active tab
- Footer — Text input (70%) and help hints (30%)
Tabs / Modes
Switch between modes using Tab / Shift+Tab or number keys 1-6:
| # | Tab | Description |
|---|---|---|
| 1 | Chat | AI conversation with multimodal support |
| 2 | Agents | Create and manage AI agent swarms |
| 3 | Media | Browse and select images, audio, video |
| 4 | Settings | Configure model, preferences |
| 5 | Distributed | Manage distributed agent networks |
| 6 | Reasoning | Advanced reasoning chains and knowledge |
Input Modes
The TUI operates in two input modes:
Normal Mode
Key presses are interpreted as navigation commands. Use arrow keys, j/k for movement, Tab to switch tabs, q to quit.
Editing Mode
Key presses go to the text input field. Press Enter or i to enter Editing mode, Esc to return to Normal mode.
The current mode is indicated in the input box border style.
Configuration
The TUI reads configuration from environment variables and defaults:
| Setting | Default | Description |
|---|---|---|
| Model | $AETHER_AI | Default AI model for chat |
| Max messages | 1,000 | Message history limit |
| Auto-scroll | On | Scroll to latest message |
| Timestamps | On | Show message timestamps |
| Media preview | On | Enable in-terminal image preview |
| Agent update interval | 1,000ms | Agent status refresh rate |
Quick Start
-
Set your AI provider:
export AETHER_AI=openai export OPENAI_API_KEY=sk-... -
Launch the TUI:
ae tui -
Press
Enterto start typing, write your message, pressEnterto send -
Press
Tabto explore other modes (Agents, Media, etc.) -
Press
qorCtrl+Cto exit
Navigation
The TUI uses a modal key binding system with Normal and Editing modes.
Global Keys (Normal Mode)
These work from any tab:
| Key | Action |
|---|---|
q / Esc / Ctrl+C / Ctrl+Q | Quit |
Tab | Next tab (cycles through all modes) |
Shift+Tab | Previous tab |
1-6 | Jump to specific tab |
↑ / k | Move selection up (wraps around) |
↓ / j | Move selection down (wraps around) |
Chat Mode
Normal Mode
| Key | Action |
|---|---|
Enter / i | Enter Editing mode |
c | Clear conversation |
m | Switch to Media tab |
a | Switch to Agents tab |
Ctrl+E | Export conversation to Markdown |
Ctrl+J | Export conversation to JSON |
Ctrl+L | Clear conversation |
Ctrl+F | Open Search |
Editing Mode
| Key | Action |
|---|---|
Enter | Send message |
Esc | Cancel, return to Normal mode |
| Arrow keys | Navigate within text input |
| Any key | Types into the input field |
Agent Swarm Mode
Normal Mode
| Key | Action |
|---|---|
n | Create new agent |
d / Delete | Remove selected agent |
Enter / s | Enter Editing mode (type task) |
m | View agent metrics |
r | Restart selected agent |
c | Switch to Chat |
Media Browser Mode
Normal Mode
| Key | Action |
|---|---|
Space / Enter | Toggle file selection |
o | Open/add file |
c | Clear all selections |
d / Delete | Remove file from library |
b | Return to Chat with selected media |
Search Mode
Normal Mode
| Key | Action |
|---|---|
i / / | Enter search Editing mode |
↓ / j | Next search result |
↑ / k | Previous search result |
Esc | Clear search, return to Chat |
Ctrl+C | Copy selected result |
Distributed Agents Mode
| Key | Action |
|---|---|
s | Start distributed swarm |
d | Stop distributed swarm |
r | Refresh network status |
t | Test connection |
Advanced Reasoning Mode
| Key | Action |
|---|---|
n | New reasoning session |
p | View planning goals |
k | Browse knowledge base |
e | Export reasoning chains |
i | Import knowledge |
Navigation Tips
- Use
j/k(vim-style) or arrow keys to scroll through lists - List selection wraps around — pressing
↑at the top jumps to the bottom - Number keys (
1-6) provide the fastest way to switch tabs Escalways returns to Normal mode from Editing mode- In Chat mode,
Ctrl+Fenters Search for finding messages in history
Chat Interface
The Chat tab is the primary AI interaction mode — a conversational interface with multimodal support, message history, and export capabilities.
Sending Messages
- Press
Enterorito enter Editing mode - Type your message
- Press
Enterto send
The TUI sends your message to the configured AI model and displays the response.
Message Types
Messages are color-coded by role:
| Role | Color | Emoji | Description |
|---|---|---|---|
| User | Cyan | 👤 | Your messages |
| Assistant | Green | 🤖 | AI responses |
| System | Yellow | ⚙️ | Status and system messages |
Each message displays:
[14:32:05] 👤 [gpt-4o-mini] How does ownership work in Rust?
[14:32:08] 🤖 [gpt-4o-mini] Ownership is one of Rust's key features...
Media Attachments
Attach media files to your messages for multimodal AI interaction:
- Switch to the Media tab (
mor3) - Select files with
SpaceorEnter - Press
bto return to Chat with files attached
Attached media appears with a 📎 prefix in the sidebar. When you send a message, the selected media is included as context for the AI.
Chat Layout
The chat area is split into two panels:
┌──────────────────────────┬────────────────┐
│ │ 📎 Media │
│ Message History │ 🤖 Agents │
│ │ 📊 Stats │
│ │ • Total: 24 │
│ │ • User: 12 │
│ │ • Chars: 8,432 │
└──────────────────────────┴────────────────┘
- Left (70%) — Scrollable message history
- Right (30%) — Sidebar with attached media, active agents, and conversation statistics
Conversation Statistics
The sidebar shows real-time stats:
- Total messages — Count of all messages
- User / Assistant / System — Breakdown by role
- Total characters — Sum of all message content
- Average length — Mean characters per message
- Media attachments — Count of attached files
- Active agents — Number of running agents
Chat Sessions
The TUI supports multiple chat sessions through the ChatManager:
Session Settings
| Setting | Default | Description |
|---|---|---|
auto_summarize | false | Auto-summarize when context grows too large |
context_window_size | 4,096 | Max tokens in context window |
enable_media_analysis | true | Analyze media files with vision models |
temperature | 0.7 | AI response temperature |
max_tokens | None | Max tokens per response |
system_prompt | None | Custom system prompt |
Auto-Summarization
When enabled and the conversation exceeds the context window size, older messages are automatically summarized into a single system message, preserving recent context.
Search
Search through your conversation history:
- Press
Ctrl+Fto enter Search mode - Type your search query
- Press
Enterto search - Use
↑/↓to navigate results - Press
Escto return to Chat
Search is case-insensitive and matches against message content.
Export
Export your conversation for sharing or archival:
| Shortcut | Format | File |
|---|---|---|
Ctrl+E | Markdown | conversation_export.md |
Ctrl+J | JSON | conversation_export.json |
Markdown Export
# AetherShell Conversation Export
**Model:** gpt-4o-mini
**Messages:** 24
**Exported:** 2024-01-15T14:32:05Z
---
**👤 User** *14:30:00 [gpt-4o-mini]*
How does ownership work?
**🤖 Assistant** *14:30:03 [gpt-4o-mini]*
Ownership is a set of rules that govern...
JSON Export
{
"exported_at": "2024-01-15T14:32:05Z",
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "How does ownership work?",
"timestamp": "2024-01-15T14:30:00Z"
}
]
}
Clear History
- Press
c(Normal mode) to clear all messages - Press
Ctrl+Lto clear the conversation - This removes all messages from the current session
Tips
- If no AI model is configured, the TUI shows a warning with setup instructions
- Messages are limited by
max_messages(default 1,000) — oldest messages are dropped - Auto-scroll keeps the latest messages visible; scroll up manually to review history
- The model name appears in each message header so you can see which model responded
Multimodal Support
The TUI recognises image, video and audio files, classifies them by extension, and carries the reference into the conversation so a multimodal model can be asked about them.
It does not render them. Inline image display is intended, not present: the source contains no kitty, iterm or sixel support. What follows describes attaching and referencing files, not viewing them in the terminal. See TUI Guide.
Supported Formats
| Category | Extensions |
|---|---|
| Image | jpg, jpeg, png, gif, bmp, webp, tiff, svg |
| Video | mp4, avi, mov, mkv, wmv, flv, webm, m4v |
| Audio | mp3, wav, flac, aac, ogg, wma, m4a |
Media Browser
Access the Media Browser from the Chat tab by pressing m or switching to tab 3.
Layout
┌────────── Files ──────────┬──────── Preview ────────┐
│ photo.jpg │ Path: ./photo.jpg │
│ diagram.png ✓ │ Type: Image │
│ recording.mp3 │ Size: 1920×1080 │
│ demo.mp4 │ │
│ │ [Image Preview] │
└───────────────────────────┴─────────────────────────┘
- Left panel: File list with
✓markers for selected files - Right panel: Metadata and preview for the highlighted file
Controls
| Key | Action |
|---|---|
Space / Enter | Toggle file selection |
↑ / ↓ / j / k | Navigate file list |
o | Open/add file |
d / Delete | Remove file from library |
c | Clear all selections |
b | Return to Chat with selected files attached |
Image Preview
Images are rendered directly in the terminal using Unicode block characters. The preview adapts to your terminal size and supports:
- Inline rendering: Images displayed within the TUI layout
- Automatic thumbnailing: 64×64 pixel thumbnails for the file list
- Full preview: Larger rendering in the preview panel
Tip: Image quality depends on your terminal emulator. Modern terminals like iTerm2, Kitty, and WezTerm provide the best results.
Attaching Media to Chat
To send images or other media with your chat message:
- Press
mto open Media Browser - Select files with
Space(multiple selections allowed) - Press
bto return to Chat - Type your message and press
Enter
The selected media is sent as context to the AI model:
📎 Attached: photo.jpg, diagram.png
👤 What differences do you see between these two images?
🤖 The first image shows... while the second...
Media Analysis
When enable_media_analysis is enabled (default), the TUI automatically analyzes attached media:
- Images: Sent to a vision-capable model for description
- Audio: Duration and format metadata extracted
- Video: Duration and format metadata extracted
Analysis results appear as annotations in the conversation:
[Media Analysis: The image shows a flowchart diagram with 5 nodes connected by arrows...]
Multimodal Agents
Agents in the TUI support different modalities:
Supported modalities:
• Text ✓
• Image ✓ (requires vision model)
• Audio ✓ (model-dependent)
• Video ✓ (model-dependent)
Each agent declares which modalities it supports. When you assign a task involving media, the system validates that the selected agent can handle the required modality.
Display Info
Files show contextual information in the browser:
| Type | Display Format |
|---|---|
| Image | 🖼️ 1920×1080 - photo.jpg |
| Video | 🎬 demo.mp4 (30.5s) |
| Audio | 🎵 recording.mp3 (120.0s) |
| Unknown | ❓ file.xyz |
Terminal Compatibility
Image rendering quality varies by terminal:
| Terminal | Image Support |
|---|---|
| iTerm2 | Excellent (native image protocol) |
| Kitty | Excellent (native image protocol) |
| WezTerm | Good (Sixel graphics) |
| Windows Terminal | Basic (Unicode blocks) |
| VS Code Terminal | Basic (Unicode blocks) |
| Standard terminals | Basic (Unicode blocks/ASCII art) |
For the best multimodal experience, use a terminal that supports inline image protocols.
TUI Guide
AetherShell’s Terminal User Interface (TUI) provides a rich, interactive environment for working with AI, viewing multimodal content, and managing agents.
Starting TUI Mode
# Start in TUI mode
ae --tui
# Or from the REPL
tui()
Interface Overview
┌─────────────────────────────────────────────────────────────┐
│ AetherShell TUI [Agents]│
├─────────────────────────────────────────────────────────────┤
│ │
│ User: Explain the concept of monads │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ AI: A monad is a design pattern used in functional ││
│ │ programming to handle computations with context... ││
│ │ ││
│ │ Think of it as a wrapper that: ││
│ │ 1. Contains a value ││
│ │ 2. Has a way to wrap values (return/unit) ││
│ │ 3. Has a way to chain operations (bind/flatMap) ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ User: Show me an example in Rust │
│ ... │
│ │
├─────────────────────────────────────────────────────────────┤
│ > Enter message... [?] │
└─────────────────────────────────────────────────────────────┘
Key Bindings
| Key | Action |
|---|---|
Enter | Send message |
Ctrl+C | Cancel/Exit |
Ctrl+L | Clear screen |
Tab | Switch panels |
↑/↓ | Scroll history |
Ctrl+N | New conversation |
Ctrl+S | Save conversation |
Ctrl+O | Open file |
Ctrl+A | Toggle agent panel |
Ctrl+M | Toggle model selector |
? | Help |
Chat Commands
Within the TUI, you can use these commands:
/model <name> - Switch AI model
/clear - Clear conversation
/save <file> - Save conversation
/load <file> - Load conversation
/agent <name> - Switch to agent
/system <prompt> - Set system prompt
/image <path> - Send image
/help - Show help
Multimodal Content
Viewing Images
Not implemented. Inline image rendering is intended, not present. The source contains no kitty, iterm or sixel support and reads no
AETHER_TUI_IMAGE_PROTOCOL— exporting it does nothing.
# In TUI, send an image
/image screenshot.png
# Or via AI vision
ai("What's in this image?", { images: ["photo.jpg"] })
Supported terminals:
- Kitty - Full image support
- iTerm2 - macOS with inline images
- WezTerm - Cross-platform
- Sixel - Many terminals
Code Blocks
Code responses are syntax highlighted:
AI: Here's the implementation:
┌─rust─────────────────────────────────────────────┐
│ fn fibonacci(n: u64) -> u64 { │
│ match n { │
│ 0 => 0, │
│ 1 => 1, │
│ n => fibonacci(n - 1) + fibonacci(n - 2)│
│ } │
│ } │
└──────────────────────────────────────────────────┘
Tables
Data is rendered as formatted tables:
AI: Here are the results:
┌──────────┬───────┬──────────────┐
│ Name │ Size │ Modified │
├──────────┼───────┼──────────────┤
│ main.rs │ 2.4KB │ 2 hours ago │
│ lib.rs │ 1.8KB │ 3 hours ago │
│ test.rs │ 892B │ 1 day ago │
└──────────┴───────┴──────────────┘
Agent Panel
Press Ctrl+A to toggle the agent panel:
┌─ Agents ──────────────────────────┐
│ │
│ ● coder (idle) │
│ "You are a Python expert" │
│ Tools: [cat, write, grep] │
│ │
│ ○ devops (idle) │
│ "You help with infrastructure" │
│ Tools: [ls, ps, curl] │
│ │
│ [+ New Agent] │
└───────────────────────────────────┘
Model Selector
Press Ctrl+M to select a model:
┌─ Select Model ─────────────────────┐
│ │
│ OpenAI │
│ ● gpt-4o │
│ ○ gpt-4o-mini │
│ ○ gpt-4-turbo │
│ │
│ Anthropic │
│ ○ claude-3-opus │
│ ○ claude-3-sonnet │
│ │
│ Local (Ollama) │
│ ○ llama3 │
│ ○ codellama │
│ │
└────────────────────────────────────┘
Reasoning Display
When using reasoning models (o1, R1), the TUI shows the reasoning process:
┌─ Thinking... ──────────────────────────────────┐
│ Let me break down this problem: │
│ 1. First, I need to understand the constraint │
│ 2. The input array could be empty │
│ 3. Edge case: negative numbers │
│ ... │
└────────────────────────────────────────────────┘
Final Answer:
Here's the optimized solution...
Streaming Responses
Responses stream in real-time:
AI: The quick brown fox | ← Cursor shows typing
Configuration
TUI settings in ~/.config/aethershell/config.toml:
[tui]
# Theme
theme = "catppuccin-mocha"
# Image display
show_images = true
image_protocol = "kitty" # kitty, iterm, sixel
max_image_width = 80
# Chat
show_timestamps = true
show_token_count = true
# Colors (Catppuccin Mocha)
background = "#1e1e2e"
foreground = "#cdd6f4"
accent = "#cba6f7"
Themes
Built-in themes:
catppuccin-mocha(default)catppuccin-lattedraculanordsolarized-darksolarized-light
Keyboard Shortcuts Reference
General
Ctrl+C- Exit/CancelCtrl+L- Clear screenCtrl+Q- Quit TUI?- Help overlay
Navigation
Tab- Next panelShift+Tab- Previous panel↑/↓- Scroll/HistoryPgUp/PgDn- Page scroll
Actions
Enter- Send/ConfirmCtrl+N- New conversationCtrl+S- SaveCtrl+O- Open
Panels
Ctrl+A- Agents panelCtrl+M- Model selectorCtrl+H- History
Agent API
The Agent API provides HTTP endpoints for executing AetherShell code, managing agents, orchestrating workflows, and accessing the marketplace. The server uses axum and runs on port 3000 by default.
Starting the Server
ae serve # Start on default port 3000
ae serve --port 8080 # Custom port
Execution Endpoints
POST /api/v1/execute
Execute an AetherShell command and return the result.
Request:
{
"command": "ls \"src\" | where(fn(f) => f.extension == \"rs\") | len"
}
Response:
{
"success": true,
"result": "15",
"type": "Int"
}
POST /api/v1/call/:builtin
Call a specific builtin by name with arguments.
Request:
{
"args": ["src"]
}
Example: POST /api/v1/call/ls
POST /api/v1/pipeline
Execute a multi-step pipeline.
Request:
{
"input": [1, 2, 3, 4, 5],
"steps": ["map(fn(x) => x * 2)", "where(fn(x) => x > 4)"]
}
POST /api/v1/eval
Evaluate an arbitrary AetherShell expression.
Request:
{
"code": "let x = 42; x * 2"
}
Streaming Endpoints (SSE)
These endpoints return Server-Sent Events for long-running operations.
POST /api/v1/stream/execute
Stream execution results as they’re produced.
POST /api/v1/stream/pipeline
Stream pipeline results step-by-step.
POST /api/v1/stream/eval
Stream evaluation output.
SSE Event Format:
event: start
data: {"id": "exec-123"}
event: progress
data: {"step": 1, "total": 5, "message": "Processing..."}
event: data
data: {"result": "partial output"}
event: complete
data: {"result": "final result", "elapsed_ms": 150}
event: error
data: {"message": "Syntax error at line 3"}
Discovery Endpoints
GET /api/v1/schema
Return the complete AetherShell language schema (types, builtins, syntax).
GET /api/v1/schema/:format
Return the schema in a specific format (e.g., json, openapi).
GET /api/v1/builtins
List all available builtins with their descriptions.
Response:
[
{ "name": "ls", "description": "List directory contents", "category": "filesystem" },
{ "name": "map", "description": "Transform each element", "category": "collections" },
...
]
GET /api/v1/builtins/:name
Get detailed information about a specific builtin.
Response:
{
"name": "map",
"description": "Apply a function to each element in an array",
"category": "collections",
"signature": "map(fn) -> Array",
"examples": ["[1,2,3] | map(fn(x) => x * 2)"]
}
GET /api/v1/types
List all AetherShell value types and their properties.
Orchestration Endpoints
GET /api/v1/orchestration/agents
List all registered agents and their status.
Response:
[
{
"id": "agent-1",
"status": "idle",
"capabilities": ["code-review", "testing"],
"model": "openai:gpt-4o-mini"
}
]
GET /api/v1/orchestration/tasks
List all tasks.
POST /api/v1/orchestration/tasks
Create a new task.
Request:
{
"goal": "Analyze code quality in src/",
"tools": ["ls", "cat", "grep"],
"max_steps": 10
}
POST /api/v1/orchestration/workflows
Create and start a new workflow.
GET /api/v1/orchestration/workflows
List all workflows.
GET /api/v1/orchestration/workflows/:id
Get workflow details and status.
POST /api/v1/orchestration/workflows/:id/cancel
Cancel a running workflow.
GET /api/v1/orchestration/metrics
Get orchestration metrics (agent count, task counts, performance).
Response:
{
"total_agents": 3,
"active_tasks": 2,
"completed_tasks": 15,
"avg_task_duration_ms": 2300
}
Marketplace Endpoints
GET /api/v1/marketplace/search?q=code-review&category=dev
Search the agent marketplace.
GET /api/v1/marketplace/agents
List all marketplace agents.
POST /api/v1/marketplace/install
Install an agent from the marketplace.
Request:
{
"name": "code-reviewer",
"version": "1.0.0"
}
POST /api/v1/marketplace/uninstall
Uninstall a marketplace agent.
Request:
{
"name": "code-reviewer"
}
POST /api/v1/marketplace/publish
Publish an agent to the marketplace.
Request:
{
"name": "my-agent",
"description": "A helpful coding agent",
"system_prompt": "You are a code reviewer...",
"tools": ["cat", "grep"],
"model": "openai:gpt-4o-mini"
}
Health
GET /health
Health check endpoint.
Response:
{
"status": "healthy",
"version": "0.3.0",
"uptime_seconds": 3600
}
AI Model API
The AI Model API provides an OpenAI-compatible HTTP interface for managing local AI models and performing inference. It supports model downloading, format conversion, and serving multiple providers through a unified API.
Starting the Server
aimodel serve # Start on default port
aimodel serve --port 8080 # Custom port
OpenAI-Compatible Endpoints
POST /v1/chat/completions
Chat completion API, compatible with the OpenAI format.
Request:
{
"model": "llama3",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What is Rust?" }
],
"temperature": 0.7,
"max_tokens": 1024,
"stream": false
}
Response:
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1705300000,
"model": "llama3",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Rust is a systems programming language..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 150,
"total_tokens": 175
}
}
Streaming: Set "stream": true to receive Server-Sent Events:
data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":"Rust"},"index":0}]}
data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":" is"},"index":0}]}
data: [DONE]
POST /v1/embeddings
Generate text embeddings.
Request:
{
"model": "nomic-embed-text",
"input": "What is the meaning of life?"
}
Response:
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.023, -0.041, 0.015, ...]
}
],
"model": "nomic-embed-text",
"usage": { "prompt_tokens": 8, "total_tokens": 8 }
}
Model Management
GET /v1/models
List all available models.
Response:
{
"object": "list",
"data": [
{
"id": "llama3",
"object": "model",
"owned_by": "local",
"created": 1705300000
}
]
}
GET /v1/models/:model_id
Get details about a specific model.
POST /v1/models/:model_id/download
Download a model from a supported source.
Request:
{
"source": "huggingface",
"revision": "main"
}
POST /v1/models/:model_id/convert
Convert a model between formats (e.g., GGUF, ONNX).
Request:
{
"target_format": "gguf",
"quantization": "q4_0"
}
DELETE /v1/models/:model_id
Delete a downloaded model.
Provider Management
GET /v1/providers
List configured inference providers.
Response:
[
{ "id": "ollama", "status": "available", "url": "http://localhost:11434" },
{ "id": "openai", "status": "available" },
{ "id": "vllm", "status": "unavailable" }
]
POST /v1/providers/:provider_id/validate
Test connectivity to a provider.
Response:
{
"provider": "ollama",
"valid": true,
"latency_ms": 12,
"models_available": 3
}
Storage Management
GET /v1/storage/stats
Get storage usage statistics for downloaded models.
Response:
{
"total_size_bytes": 15000000000,
"model_count": 5,
"cache_size_bytes": 500000000,
"storage_path": "/home/user/.aethershell/models"
}
POST /v1/storage/cleanup
Clean up cached files and temporary data.
Health & Status
GET /v1/health
Quick health check.
Response:
{ "status": "ok" }
GET /v1/status
Detailed server status with provider information.
Response:
{
"status": "running",
"version": "0.3.0",
"uptime_seconds": 7200,
"providers": { "ollama": "connected", "openai": "configured" },
"models_loaded": 2,
"requests_served": 150
}
Documentation
GET /swagger-ui
Interactive API documentation (when enable_openapi is configured).
GET /api-docs/openapi.json
OpenAPI specification in JSON format.
Client Usage
The AI Model API is compatible with any OpenAI client library:
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/v1",
api_key="not-needed" # Local models don't need keys
)
response = client.chat.completions.create(
model="llama3",
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"llama3","messages":[{"role":"user","content":"Hello"}]}'
WebSocket & SSE
AetherShell provides real-time communication through WebSocket connections and Server-Sent Events (SSE) for streaming results.
WebSocket
Connecting
Connect to the WebSocket endpoint:
ws://localhost:3000/api/v1/ws
const ws = new WebSocket("ws://localhost:3000/api/v1/ws");
ws.onopen = () => console.log("Connected");
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(msg.type, msg);
};
Client Messages
Messages from client to server are JSON objects with a type field.
execute
Execute an AetherShell command.
{
"type": "execute",
"id": "req-1",
"request": {
"command": "ls \"src\" | len"
}
}
register
Register as an agent on the network.
{
"type": "register",
"agent_id": "my-agent",
"capabilities": ["code-review", "testing"]
}
agent_message
Send a message to another registered agent.
{
"type": "agent_message",
"to": "target-agent-id",
"payload": { "task": "review", "file": "main.rs" }
}
broadcast
Broadcast a message to all subscribers of a channel.
{
"type": "broadcast",
"channel": "status-updates",
"payload": { "status": "analysis complete" }
}
subscribe / unsubscribe
Subscribe to or unsubscribe from a broadcast channel.
{ "type": "subscribe", "channel": "status-updates" }
{ "type": "unsubscribe", "channel": "status-updates" }
ping
Keep-alive ping.
{ "type": "ping", "id": "ping-1" }
Server Messages
Messages from server to client.
response
Result of an execute request.
{
"type": "response",
"id": "req-1",
"response": {
"success": true,
"result": "15",
"value_type": "Int"
}
}
stream
Streaming event from an execute/eval/pipeline operation.
{
"type": "stream",
"id": "req-1",
"event": {
"kind": "data",
"data": "partial result"
}
}
channel
Broadcast message received on a subscribed channel.
{
"type": "channel",
"channel": "status-updates",
"payload": { "status": "analysis complete" }
}
pong
Response to a ping.
{
"type": "pong",
"id": "ping-1",
"timestamp": 1705300000000
}
error
Error notification.
{
"type": "error",
"id": "req-1",
"message": "Unknown command: invalid_builtin"
}
agent_message
Message from another agent.
{
"type": "agent_message",
"from": "analyzer-agent",
"payload": { "result": "3 issues found" }
}
registered
Confirmation of agent registration.
{
"type": "registered",
"agent_id": "my-agent"
}
agents
List of currently registered agents (sent on request or when agent list changes).
{
"type": "agents",
"agents": [
{ "id": "agent-1", "capabilities": ["code-review"] },
{ "id": "agent-2", "capabilities": ["testing"] }
]
}
Server-Sent Events (SSE)
SSE endpoints provide one-way streaming from server to client. They’re used for long-running operations where you want incremental results.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/stream/execute | Stream command execution |
| POST | /api/v1/stream/pipeline | Stream pipeline processing |
| POST | /api/v1/stream/eval | Stream code evaluation |
Event Format
SSE uses the standard text/event-stream format:
event: start
data: {"id": "exec-123", "timestamp": 1705300000}
event: progress
data: {"step": 1, "total": 5, "message": "Loading files..."}
event: data
data: {"result": "[{\"name\": \"main.rs\", \"size\": 2048}]"}
event: complete
data: {"result": "final output", "elapsed_ms": 250}
Event Types
| Event | Description |
|---|---|
start | Operation has begun |
progress | Progress update with step number and message |
data | Intermediate data result |
complete | Operation finished successfully |
error | Operation failed |
Client Usage
// SSE with fetch
const response = await fetch("/api/v1/stream/execute", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ command: "ls src | map(fn(f) => f.name)" })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
// Parse SSE events from text
for (const line of text.split("\n")) {
if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
console.log(data);
}
}
}
AI Chat Streaming
The AI Model API at /v1/chat/completions also supports SSE streaming when "stream": true:
const response = await fetch("http://localhost:8080/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3",
messages: [{ role: "user", content: "Hello!" }],
stream: true
})
});
// Process SSE chunks
// Each chunk: data: {"choices":[{"delta":{"content":"token"}}]}
// Final: data: [DONE]
Dashboard WebSocket
The web dashboard connects to WebSocket at /api/v1/ws for real-time updates. The dashboard automatically:
- Reconnects on disconnection (3-second retry)
- Receives agent status updates
- Gets workflow progress notifications
- Monitors marketplace changes
// Dashboard auto-connect pattern
const ws = new WebSocket(`ws://${location.host}/api/v1/ws`);
ws.onclose = () => setTimeout(connect, 3000); // Auto-reconnect
Python SDK
The AetherShell Python SDK (aethershell package v1.5.0) provides a Pythonic interface for evaluating AetherShell code, running agents, building pipelines, and integrating with Python AI ecosystems.
Installation
pip install aethershell
Quick Start
from aethershell import evaluate, pipeline
# Evaluate AetherShell code
result = evaluate('[1, 2, 3] | map(fn(x) => x * 2)')
print(result) # [2, 4, 6]
# Build a pipeline
result = pipeline([1, 2, 3, 4, 5]).filter(lambda x: x > 2).map(lambda x: x * 10).run()
print(result) # [30, 40, 50]
AetherRuntime
The core runtime class for evaluating AetherShell code.
from aethershell import AetherRuntime
runtime = AetherRuntime()
# Evaluate a single expression
result = runtime.eval('42 * 2')
# Evaluate a file
result = runtime.eval_file('script.ae')
Creating Agents
agent = runtime.create_agent(
goal="Find large files in src/",
tools=["ls", "cat", "grep"],
max_steps=10,
model="openai:gpt-4o-mini"
)
result = await agent.run("Find all files over 10KB")
print(result.output)
Creating Swarms
swarm = runtime.create_swarm(
goal="Analyze project quality",
tools=["ls", "cat", "grep", "wc"],
max_steps=20
)
result = await swarm.run("Review src/ for code quality issues")
print(result.output)
A2UI Events
Subscribe to agent-to-UI events for real-time feedback:
def on_event(event):
if event.type == "progress":
print(f"Progress: {event.data['step']}/{event.data['total']}")
elif event.type == "notification":
print(f"[{event.level}] {event.message}")
runtime.subscribe_a2ui(on_event)
PipelineBuilder
A fluent API for building data transformation pipelines:
from aethershell import pipeline
result = (
pipeline([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
.filter(lambda x: x % 2 == 0) # Keep even numbers
.map(lambda x: x ** 2) # Square them
.sort() # Sort ascending
.take(3) # First 3
.run()
)
print(result) # [4, 16, 36]
Pipeline Methods
| Method | Description |
|---|---|
.map(fn) | Transform each element |
.filter(fn) | Keep elements matching predicate |
.reduce(fn, init) | Fold to single value |
.sort() / .sort(key) | Sort elements |
.reverse() | Reverse order |
.flatten() | Flatten nested arrays |
.unique() | Remove duplicates |
.take(n) | First N elements |
.skip(n) | Skip N elements |
.to_code() | Generate AetherShell code |
.run() | Execute the pipeline |
Generating AetherShell Code
code = (
pipeline([1, 2, 3])
.map(lambda x: x * 2)
.filter(lambda x: x > 2)
.to_code()
)
print(code) # [1, 2, 3] | map(fn(x) => x * 2) | where(fn(x) => x > 2)
Workflows
Build structured AI workflows with retry and circuit-breaker patterns:
from aethershell.workflows import Workflow, WorkflowStep, WorkflowPattern
# Create a sequential workflow
wf = Workflow(pattern=WorkflowPattern.SEQUENTIAL)
wf.add_step(WorkflowStep(name="gather", code='ls "src"'))
wf.add_step(WorkflowStep(name="analyze", code='grep "TODO" "src/"'))
wf.add_step(WorkflowStep(name="report", code='echo "Analysis complete"'))
result = await wf.run(input_data={})
print(result.outputs)
MapReduce Workflow
from aethershell.workflows import MapReduceWorkflow
wf = MapReduceWorkflow(
map_code='fn(item) => ai("Summarize: " + item)',
reduce_code='fn(summaries) => join(summaries, "\n\n")'
)
result = await wf.run(["doc1.md", "doc2.md", "doc3.md"])
Circuit Breaker
Protect against cascading failures:
from aethershell.workflows import CircuitBreaker
breaker = CircuitBreaker(
failure_threshold=3,
recovery_timeout=30.0
)
try:
result = await breaker.call_async(lambda: runtime.eval('http_get "https://api.example.com"'))
except Exception:
print("Circuit open — using fallback")
Metrics
Production-grade observability with Prometheus-compatible metrics:
from aethershell.metrics import Counter, Histogram, Timer
# Count operations
requests = Counter("requests_total", "Total requests processed")
requests.inc()
# Track latencies
latency = Histogram("request_duration_seconds", "Request latency")
with Timer(latency):
result = runtime.eval('ai("Summarize this")')
# Export for Prometheus
print(latency.to_prometheus())
Distributed
Service discovery and leader election for multi-node deployments:
from aethershell.distributed import ServiceRegistry, ServiceInfo
registry = ServiceRegistry()
# Register a service
registry.register(ServiceInfo(
id="worker-1",
name="aethershell-worker",
address="192.168.1.10",
port=3000,
metadata={"gpu": "true"}
))
# Discover services
workers = registry.get_services_by_name("aethershell-worker")
for w in workers:
print(f"{w.id} at {w.address}:{w.port}")
Leader Election
from aethershell.distributed import LeaderElection
election = LeaderElection(service_info)
election.on_leadership_change(lambda is_leader:
print("I am the leader!" if is_leader else "Following leader")
)
if election.is_leader():
# Coordinate work distribution
pass
LangChain Integration
Use AetherShell tools within LangChain agents:
from aethershell.langchain import AetherShellTool, AetherAgentTool
# Use AetherShell as a LangChain tool
shell_tool = AetherShellTool()
result = shell_tool.run('ls "src" | where(fn(f) => f.size > 1000)')
# Run an AetherShell agent from LangChain
agent_tool = AetherAgentTool(tools=["ls", "cat"])
result = agent_tool.run("Find TODO comments in the project")
Cloud Deployment
Deploy AetherShell as serverless functions:
from aethershell.cloud import LambdaRuntime, FunctionConfig
runtime = LambdaRuntime()
handler = runtime.create_handler(FunctionConfig(
name="data-processor",
code='fn(event) => event.body | json_parse | map(fn(x) => x * 2)',
timeout=30
))
# Generate deployment configuration
deployment = runtime.generate_deployment()
Supported platforms:
- AWS Lambda via
LambdaRuntime - Azure Functions via
AzureFunctionsRuntime
Plugins
AetherShell supports a plugin system for extending the shell with custom builtins, tools, and integrations.
Managing Plugins
Listing Plugins
plugins
# [
# { name: "git-tools", version: "1.0", enabled: true, category: "dev" },
# { name: "docker-tools", version: "0.5", enabled: false, category: "containers" },
# ...
# ]
Plugin Information
plugin_info "git-tools"
# {
# name: "git-tools",
# version: "1.0",
# description: "Git integration tools for AetherShell",
# builtins: ["git_status", "git_log", "git_diff"],
# enabled: true
# }
Enabling and Disabling
plugin_enable "docker-tools"
plugin_disable "git-tools"
Loading and Unloading
plugin_load "my-custom-plugin" # Load into memory
plugin_unload "my-custom-plugin" # Remove from memory
Categories
plugin_categories
# ["dev", "containers", "cloud", "data", "security", ...]
Plugin Architecture
Plugins are Rust crates that implement the tool and builtin interfaces. They provide:
- Custom builtins — New commands available in the shell
- Tool schemas — OpenAI-compatible function descriptions for AI agent use
- Configuration — Plugin-specific settings
Feature Flags
AetherShell uses feature flags to enable/disable built-in capabilities:
features # List all features
feature_enabled "ai" # Check if AI is enabled
feature_enable "distributed"
feature_disable "experimental"
feature_set "max_agents" 10
Distributed Computing
AetherShell supports distributed agent execution across multiple nodes with cluster management, job scheduling, and result aggregation.
Cluster Management
Creating a Cluster
cluster_create "my-cluster" { max_nodes: 10, timeout: 30000 }
Adding Nodes
cluster_add_node "my-cluster" { address: "192.168.1.10", port: 3000 }
cluster_add_node "my-cluster" { address: "192.168.1.11", port: 3000 }
Cluster Status
cluster_status "my-cluster"
# {
# name: "my-cluster",
# nodes: 2,
# healthy: 2,
# total_jobs: 0,
# uptime_seconds: 120
# }
Node Management
cluster_nodes "my-cluster"
# [
# { address: "192.168.1.10", port: 3000, status: "active", load: 0.2 },
# { address: "192.168.1.11", port: 3000, status: "active", load: 0.1 }
# ]
cluster_remove_node "my-cluster" "192.168.1.11"
Job Scheduling
Submitting Jobs
let job_id = job_submit "my-cluster" {
code: 'ls "src" | map(fn(f) => f.name)',
priority: "high"
}
echo job_id # "job-abc123"
Job Status
job_status "job-abc123"
# { id: "job-abc123", status: "running", node: "192.168.1.10", progress: 0.5 }
Job Results
let results = job_results "job-abc123"
echo results
Listing and Canceling
job_list "my-cluster"
# [{ id: "job-abc123", status: "running" }, { id: "job-def456", status: "completed" }]
job_cancel "job-abc123"
Remote Execution
remote_execis a stub — it does not run anything. There is no SSH/RPC transport behind it. It validates that the node is registered and echoes the request back withstatus: "simulated"andsimulated: true. This page previously showed it returning a real result (# 15), which it never did.For real remote execution use
ssh_exec, which is effect-taggedExecand approval-gated in agent mode.
remote_exec "192.168.1.10:3000" 'ls "src" | len'
# { node_id: "192.168.1.10:3000", command: "ls \"src\" | len",
# status: "simulated", simulated: true,
# output: "remote_exec is a stub: the command was NOT run. …" }
# Actually run it:
ssh_exec "user@192.168.1.10" "ls src | wc -l"
Result Aggregation
Collect and merge results from multiple nodes:
let results = aggregate_results "my-cluster" "job-batch-1"
# Merges results from all nodes into a single value
NANDA Consensus
For coordinated multi-agent decisions, AetherShell provides a consensus protocol:
# Propose a decision
let proposal_id = nanda_propose "Should we deploy v2.0?" {
options: ["yes", "no", "defer"],
quorum: 3,
timeout: 60000
}
# Agents vote
nanda_vote proposal_id "yes" { reason: "All tests pass" }
# Check status
nanda_status proposal_id
# { proposal: "...", votes: 2, quorum: 3, status: "pending" }
# Check if quorum reached
nanda_quorum proposal_id
# false
# Final consensus
nanda_consensus proposal_id
# { decision: "yes", votes_for: 3, votes_against: 0 }
TUI Distributed Panel
The TUI provides a dedicated Distributed Agents tab (Tab 5) for visual management:
s— Start distributed swarmd— Stop distributed swarmr— Refresh network statust— Test node connections
See TUI Navigation for all key bindings.
Marketplace
The AetherShell Marketplace is a registry for sharing and discovering pre-built AI agents.
Browsing
Search
# Search for agents
marketplace_search "code review"
# [
# { name: "code-reviewer", author: "aethershell", version: "1.0.0", downloads: 500 },
# { name: "pr-analyzer", author: "community", version: "0.8.0", downloads: 120 },
# ...
# ]
The web dashboard provides a visual marketplace browser at the Marketplace tab with filtering, sorting, and one-click installation.
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/marketplace/search?q=... | Search agents |
| GET | /api/v1/marketplace/agents | List all agents |
| POST | /api/v1/marketplace/install | Install an agent |
| POST | /api/v1/marketplace/uninstall | Uninstall an agent |
| POST | /api/v1/marketplace/publish | Publish an agent |
Installing Agents
marketplace_install "code-reviewer"
marketplace_install "code-reviewer" "1.0.0" # Specific version
Via the API:
curl -X POST http://localhost:3000/api/v1/marketplace/install \
-H "Content-Type: application/json" \
-d '{"name": "code-reviewer", "version": "1.0.0"}'
Uninstalling
marketplace_uninstall "code-reviewer"
Publishing
Share your agents with the community:
marketplace_publish {
name: "my-agent",
description: "Analyzes Rust code for common patterns",
system_prompt: "You are an expert Rust developer...",
tools: ["cat", "grep", "ls"],
model: "openai:gpt-4o-mini",
tags: ["rust", "code-analysis"]
}
Via the Dashboard
- Open the Marketplace tab
- Click Publish Agent
- Fill in the form: name, description, system prompt, tools, model
- Click Publish
Agent Structure
Marketplace agents include:
| Field | Description |
|---|---|
name | Unique agent identifier |
description | What the agent does |
author | Publisher name |
version | Semantic version |
system_prompt | Agent’s system instructions |
tools | Allowed builtin tools |
model | Recommended model URI |
tags | Category tags for discovery |
downloads | Download count |
stars | Community rating |
Local Registry
The RegistryClient manages local state:
- Search: Full-text search against the registry (local fallback when offline)
- Install: Downloads agent config and registers locally
- Cache: Installed agents stored in
~/.aethershell/marketplace/
Dashboard Integration
The web dashboard’s Marketplace page provides:
- Search bar with real-time results
- Category filtering and sorting
- Install/Uninstall buttons with loading states
- Publish dialog with form validation
- Agent cards showing name, description, version, downloads, and verified badges
Security
AetherShell includes multiple security layers for safe AI agent execution and enterprise deployments.
Agent Sandboxing
Agents run in a restricted sandbox with configurable limits:
| Limit | Default | Description |
|---|---|---|
| Timeout | 30 seconds | Maximum execution time |
| Output | 10 MB | Maximum output size |
| Memory | 512 MB | Maximum memory usage |
Command Allowlist
Restrict which commands agents can execute:
export AGENT_ALLOW_CMDS="ls,cat,grep,wc,find"
Only listed commands will be available to agents. The allowlist is enforced by validate_command() which:
- Checks the tool name against the allowlist
- Blocks shell metacharacters (
;,|,&&,||,`,$()) - Prevents path traversal in arguments
Prompt Validation
Agent goals are validated before execution:
- Maximum 4,000 characters
- Injection pattern detection
- Sanitization of control characters
Rate Limiting
| Operation | Limit |
|---|---|
| Agent plans | 10 per minute |
| Agent executions | 5 per minute |
RBAC (Role-Based Access Control)
Enterprise deployments support RBAC for fine-grained access control:
# Create roles
role_create "developer" { permissions: ["read", "execute", "agent"] }
role_create "admin" { permissions: ["read", "write", "execute", "agent", "admin"] }
# Grant roles to users
role_grant "alice" "developer"
role_grant "bob" "admin"
# Check permissions
check_permission "alice" "execute" # true
check_permission "alice" "admin" # false
# List roles
roles_list
user_roles "alice" # ["developer"]
Audit Logging
Track all operations for compliance:
# View recent audit events
audit_log 20
# [{ timestamp: "...", user: "alice", action: "execute", details: "ls src/" }, ...]
# Query audit log
audit_query { user: "bob", action: "agent", since: "2024-01-01" }
# Export audit log
audit_export "audit_2024.json"
# Audit statistics
audit_stats
# { total_events: 1500, users: 3, actions: { execute: 800, agent: 200, ... } }
SSO (Single Sign-On)
Integrate with enterprise identity providers:
sso_init { provider: "oauth2", client_id: "...", issuer: "https://auth.example.com" }
sso_auth # Initiate authentication flow
sso_validate # Validate current session
sso_status # Check SSO status
sso_logout # End session
Compliance
Run compliance checks against security policies:
compliance_check
# { passed: 12, failed: 2, warnings: 3 }
compliance_report
# Generates detailed compliance report with remediation steps
Cryptographic Operations
# Hashing
crypto_hash "sha256" "Hello, world!"
crypto_hash_file "sha256" "document.pdf"
# Random data
crypto_random_bytes 32
crypto_uuid # Generate UUID v4
# Encoding
crypto_base64_encode "hello" # "aGVsbG8="
crypto_base64_decode "aGVsbG8="
# Password hashing
let hash = crypto_password_hash "my-password"
crypto_password_verify "my-password" hash # true
# JWT
crypto_jwt_decode token
API Key Management
API keys are stored securely:
- Keyring integration: Keys stored in OS keyring when available
- Environment fallback:
OPENAI_API_KEY,AETHER_API_KEYenvironment variables - No logging: API keys are never written to logs or audit trails
# Keys loaded automatically from keyring or environment
# SecureApiConfig::from_keyring_or_env() handles the priority
Best Practices
- Always set
AGENT_ALLOW_CMDSin production — never give agents unrestricted access - Use RBAC for multi-user deployments
- Enable audit logging for compliance-sensitive environments
- Review agent plans with
dry_run: truebefore allowing execution of destructive operations - Rotate API keys regularly and use the keyring for storage
- Set appropriate rate limits to prevent abuse
Performance
Tips and techniques for optimizing AetherShell performance in production workloads.
Pipeline Optimization
Lazy Evaluation
AetherShell pipelines process elements lazily where possible. Order your operations to minimize work:
# Good: filter first, then transform
ls "." | where(fn(f) => f.size > 10000) | map(fn(f) => expensive_operation(f))
# Bad: transform everything, then filter
ls "." | map(fn(f) => expensive_operation(f)) | where(fn(f) => f.size > 10000)
Use take Early
Limit results early to avoid processing unnecessary elements:
# Good: limit early
ls "." | sort_by "size" "desc" | take 5
# Bad: process all, then take (sort_by processes all anyway, but downstream operations are limited)
Prefer Builtins Over AI
Builtins execute instantly; AI calls have network latency:
# Fast: builtin string operation
"hello world" | upper
# Slow: AI for simple text tasks
ai "Convert to uppercase: hello world"
AI Performance
Semantic Caching
Avoid redundant API calls by caching responses:
# First call: hits API
let answer = ai "What is Rust?"
semantic_cache "What is Rust?" answer
# Second call: cache hit (< 1ms vs ~500ms API call)
let cached = semantic_cache_get "Tell me about Rust"
Model Selection
Choose the right model for the task:
| Model | Speed | Cost | Best For |
|---|---|---|---|
gpt-4o-mini | Fast | Low | Simple queries, classification |
gpt-4o | Medium | High | Complex reasoning, code generation |
ollama:llama3 | Variable | Free | Privacy-sensitive, offline |
Batch Operations
Process multiple items in a single AI call when possible:
# One call for multiple items (faster)
let items = join(data, "\n")
ai "Classify each line:\n${items}"
# vs N separate calls (slower)
data | map(fn(item) => ai "Classify: ${item}")
Agent Performance
Limit Max Steps
Set appropriate max_steps to prevent runaway agents:
# Quick lookup: 3-5 steps
agent "What's in src/main.rs?" ["cat"] 3
# Deep analysis: 10-15 steps
agent "Full project review" ["ls", "cat", "grep"] 15
Targeted Tool Sets
Give agents only the tools they need—fewer tools means faster decisions:
# Good: specific tools
agent "Find TODOs" ["grep"]
# Bad: everything
agent "Find TODOs" ["ls", "cat", "grep", "find", "wc", "head", "tail", "sort"]
RAG Performance
Index Size
The built-in RAG uses in-memory indexing with hash-based embeddings. Performance characteristics:
| Documents | Index Time | Search Time |
|---|---|---|
| 100 | < 100ms | < 10ms |
| 1,000 | < 1s | < 50ms |
| 10,000 | < 10s | < 200ms |
Search Tuning
Adjust top_k based on your needs:
rag_search query 3 # Fast, fewer results
rag_search query 20 # Slower, more comprehensive
Memory Management
Max Messages
The TUI limits chat history to prevent memory growth:
config.max_messages = 1000 # Default
Older messages are dropped when the limit is reached.
Cache Limits
Semantic cache has built-in limits:
- Max entries: 1,000
- TTL: 1 hour
- Oldest entries evicted automatically
Build Optimization
Release Builds
Always use release builds for production:
cargo build --release --bins
Release builds are significantly faster than debug builds (10-100x for CPU-bound operations).
Binary Size
Strip debug symbols for smaller binaries:
cargo build --release
strip target/release/ae
Data Processing
Real-world examples of using AetherShell for data processing tasks.
CSV Analysis
Load and Analyze CSV
# Parse CSV manually
let lines = cat "sales.csv" | split "\n"
let headers = split (first lines) ","
let rows = lines | slice 1 (len lines) | map(fn(line) => split line ",")
# Find top sellers
rows
| map(fn(r) => { name: r[0], amount: float(r[2]) })
| sort_by "amount" "desc"
| take 10
Aggregate by Category
let data = cat "products.csv" | split "\n" | slice 1 100
| map(fn(line) => {
let cols = split line ","
{ category: cols[1], price: float(cols[2]), qty: int(cols[3]) }
})
# Revenue per category
data
| map(fn(r) => { category: r.category, revenue: r.price * r.qty })
| group_by "category"
JSON Processing
API Data Pipeline
# Fetch and process API data
let users = web_json_get "https://jsonplaceholder.typicode.com/users"
users
| map(fn(u) => { name: u.name, city: u.address.city, company: u.company.name })
| sort_by "city" "asc"
Merge Multiple Sources
let users = web_json_get "https://api.example.com/users"
let orders = web_json_get "https://api.example.com/orders"
# Join users with their order counts
users | map(fn(u) => {
let user_orders = orders | where(fn(o) => o.user_id == u.id)
{ ...u, order_count: len user_orders, total_spent: user_orders | map(fn(o) => o.amount) | sum }
}) | sort_by "total_spent" "desc"
Log Analysis
Error Frequency
cat "app.log"
| split "\n"
| where(fn(line) => contains line "ERROR")
| map(fn(line) => {
let parts = split line " "
{ date: parts[0], error: join(slice(parts, 3, len(parts)), " ") }
})
| map(fn(e) => e.error)
| sort
| uniq
Request Latency Analysis
cat "access.log"
| split "\n"
| where(fn(line) => contains line "GET /api")
| map(fn(line) => {
let parts = split line " "
float(last parts)
})
| map(fn(latencies) => {
{
count: len latencies,
avg_ms: avg latencies,
p50: sort latencies | nth(len(latencies) / 2),
max: max latencies
}
})
File System Analysis
Disk Usage Report
ls "."
| where(fn(f) => f.is_dir)
| map(fn(d) => {
let usage = fs_du d.path
{ dir: d.name, size_mb: round(usage.total / 1048576.0), files: usage.files }
})
| sort_by "size_mb" "desc"
Find Duplicate Files
fs_walk "."
| where(fn(f) => !f.is_dir)
| map(fn(f) => { path: f.path, hash: crypto_hash_file "md5" f.path, size: f.size })
| sort_by "hash" "asc"
| reduce(fn(acc, f) => {
# Group by hash to find duplicates
...acc
}, {})
Statistical Summary
let data = [23, 45, 12, 67, 34, 89, 11, 56, 78, 42]
let stats = {
n: len data,
sum: data | sum,
mean: data | avg,
min: data | min,
max: data | max,
range: (data | max) - (data | min),
sorted: data | sort
}
echo stats
Web Scraping
Examples of web scraping and data extraction with AetherShell.
Basic Page Fetching
# Fetch a page
let page = http_get "https://example.com"
echo page.status # 200
echo page.ok # true
# Extract text content
let text = web_html_to_text page.body
echo text
Scraping with Selectors
# Scrape article titles
let titles = web_scrape "https://news.ycombinator.com" "a.storylink"
titles | take 10 | each(fn(t) => echo t.text)
API-Based Scraping
# GitHub repository stats
let repos = ["rust-lang/rust", "tokio-rs/tokio", "serde-rs/serde"]
repos | map(fn(repo) => {
let data = web_json_get "https://api.github.com/repos/${repo}"
{
name: data.full_name,
stars: data.stargazers_count,
forks: data.forks_count,
language: data.language
}
}) | sort_by "stars" "desc"
Content Extraction
HTML to Markdown
let html = (http_get "https://blog.example.com/post/1").body
let md = web_html_to_markdown html
file_write "post.md" md
Extract Emails
let page = (http_get "https://example.com/contact").body
let emails = web_extract_emails page
echo emails # ["info@example.com", "support@example.com"]
Extract Links
let links = web_scrape "https://example.com" "a[href]"
links | map(fn(a) => a.href) | where(fn(h) => starts_with h "https://") | unique
Download Pipeline
# Download multiple files
let urls = [
"https://data.example.com/dataset1.csv",
"https://data.example.com/dataset2.csv",
"https://data.example.com/dataset3.csv"
]
mkdir "downloads"
urls | each(fn(url) => {
let filename = last(split(url, "/"))
echo "Downloading ${filename}..."
web_download url "downloads/${filename}"
})
URL Health Checking
let urls = cat "urls.txt" | split "\n" | where(fn(u) => len(u) > 0)
let results = urls | map(fn(url) => {
let check = web_check_url url
{ url: url, status: check.status, ok: check.reachable }
})
# Report broken links
let broken = results | where(fn(r) => !r.ok)
echo "${len broken} broken links found:"
broken | each(fn(r) => echo " ✗ ${r.url} (${r.status})")
Paginated API
# Fetch all pages from a paginated API
let all_items = []
let page = 1
let has_more = true
# Note: AetherShell supports while loops via recursion
let fetch_page = fn(page, acc) => {
let data = web_json_get "https://api.example.com/items?page=${page}&limit=100"
let items = concat acc data.items
if len(data.items) == 100 {
fetch_page (page + 1) items
} else {
items
}
}
let all_items = fetch_page 1 []
echo "Total items: ${len all_items}"
RSS Feed Parsing
let feed = (http_get "https://blog.example.com/feed.xml").body
let items = web_xpath feed "//item"
items | take 5 | map(fn(item) => {
{
title: web_xpath item "title/text()",
link: web_xpath item "link/text()",
date: web_xpath item "pubDate/text()"
}
})
AI Automation
Examples of using AetherShell’s AI capabilities for automation tasks.
Code Documentation Generator
# Generate documentation for all Rust files
ls "src"
| where(fn(f) => f.extension == "rs")
| map(fn(f) => {
let code = cat f.path
let doc = ai "Write a brief module-level doc comment for this Rust file. Include purpose, key types, and public API:\n\n${code}" {
model: "openai:gpt-4o-mini"
}
{ file: f.name, documentation: doc }
})
| each(fn(d) => {
echo "## ${d.file}\n${d.documentation}\n"
})
Intelligent Log Analysis
# Index logs for RAG
let errors = cat "app.log" | split "\n" | where(fn(l) => contains l "ERROR")
errors | each(fn(e) => rag_index e "app.log")
# Ask questions about the errors
let ctx = rag_query "What are the most common error patterns?" 5
ai "Based on these log entries:\n${ctx.context}\n\nIdentify the top 3 error patterns and suggest fixes."
Automated Code Review
# Agent-powered code review
agent {
goal: "Review all .rs files in src/ for: 1) unwrap() calls that should use ? or expect(), 2) TODO/FIXME comments, 3) functions over 50 lines. Provide a summary with file:line references.",
tools: ["ls", "cat", "grep", "wc"],
max_steps: 15
}
Commit Message Generator
# Generate a commit message from the current diff
let diff = sh "git diff --staged"
if len(diff) > 0 {
let msg = ai "Write a concise conventional commit message for this diff. Use format: type(scope): description\n\nDiff:\n${diff}" {
model: "openai:gpt-4o-mini"
}
echo "Suggested commit message:"
echo msg
} else {
echo "No staged changes"
}
RAG-Powered Q&A System
# Step 1: Index your project docs
ls "docs" | where(fn(f) => f.extension == "md")
| each(fn(f) => {
echo "Indexing ${f.name}..."
rag_index (cat f.path) f.name
})
# Step 2: Interactive Q&A
let question = "How do I create a custom pipeline?"
let cached = semantic_cache_get question
let answer = if cached.hit {
echo "(cached)"
cached.response
} else {
let ctx = rag_query question 5
let resp = ai "Context:\n${ctx.context}\n\nQuestion: ${question}\n\nAnswer based only on the context above."
semantic_cache question resp
resp
}
echo answer
Knowledge Graph Builder
# Build a project knowledge graph from source code
ls "src" | where(fn(f) => f.extension == "rs") | each(fn(f) => {
# Add each file as an entity
let file_id = (kg_add "File" f.name { path: f.path, size: f.size }).entity_id
# Find imports
let imports = cat f.path | split "\n"
| where(fn(l) => starts_with (trim l) "use crate::")
| map(fn(l) => trim l | replace "use crate::" "" | replace ";" "")
imports | each(fn(imp) => {
let mod_id = (kg_add "Module" imp {}).entity_id
kg_relate file_id mod_id "imports" {}
})
})
# Query the graph
echo "Files importing eval:"
kg_query "eval"
Multi-Agent Analysis
# Swarm-based project analysis
swarm {
goal: "Perform a comprehensive analysis: 1) Code quality assessment, 2) Dependency audit, 3) Test coverage estimate, 4) Documentation completeness. Produce a structured report.",
tools: ["ls", "cat", "grep", "find", "wc", "fs_tree"],
max_steps: 30
}
Automated Testing
# Generate test cases with AI
let source = cat "src/parser.rs"
let tests = ai "Generate 5 unit test functions in Rust for the parser module. Cover edge cases:\n\n${source}" {
model: "openai:gpt-4o"
}
file_write "tests/generated_parser.rs" tests
echo "Generated test file"
Batch Classification
# Classify support tickets
let tickets = web_json_get "https://api.example.com/tickets?status=new"
tickets | map(fn(t) => {
let category = ai "Classify this support ticket into one of: bug, feature, question, billing. Respond with only the category.\n\nTicket: ${t.description}" {
model: "openai:gpt-4o-mini"
}
{ ...t, category: trim(category) }
}) | save_json "classified_tickets.json"
DevOps
Examples of using AetherShell for DevOps automation, system administration, and infrastructure management.
System Monitoring
Health Dashboard
let cpu = sys_cpu_info
let mem = sys_mem_info
let disk = sys_disk_info
let load = sys_load_avg
echo "=== System Health ==="
echo "Hostname: ${sys_hostname}"
echo "OS: ${sys_os} ${sys_arch}"
echo "Uptime: ${sys_uptime}"
echo "Load: ${load}"
echo "CPU: ${cpu.cores} cores"
echo "Memory: ${round(mem.used / 1048576)}MB / ${round(mem.total / 1048576)}MB"
echo "Disk: ${round(disk.used / 1073741824)}GB / ${round(disk.total / 1073741824)}GB"
Process Monitor
# Find top memory-consuming processes
proc_list
| sort_by "mem_usage" "desc"
| take 10
| map(fn(p) => { pid: p.pid, name: p.name, mem_mb: round(p.mem_usage / 1048576) })
Port Scanning
# Check which services are listening
net_ports
| where(fn(p) => p.state == "LISTEN")
| map(fn(p) => { port: p.port, pid: p.pid, process: p.process })
| sort_by "port" "asc"
Service Management
Service Status Check
let services = ["nginx", "postgresql", "redis"]
services | map(fn(svc) => {
let status = svc_status svc
{ name: svc, status: status.state, pid: status.pid }
})
Restart All Services
["nginx", "postgresql", "redis"]
| each(fn(svc) => {
echo "Restarting ${svc}..."
svc_restart svc
})
Deployment Pipeline
Build and Deploy
# Build
echo "Building..."
let build = sh "cargo build --release 2>&1"
echo build
# Run tests
echo "Testing..."
let test_result = sh "cargo test 2>&1"
if contains test_result "FAILED" {
echo "Tests failed! Aborting deployment."
exit 1
}
# Deploy
echo "Deploying..."
file_copy "target/release/ae" "/opt/aethershell/ae"
svc_restart "aethershell"
echo "Deployed successfully"
Rolling Deployment
let nodes = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]
nodes | each(fn(node) => {
echo "Deploying to ${node}..."
# Remove from load balancer
echo " Draining connections..."
sleep 5000
# Deploy. Use ssh_exec: it really runs the command. `remote_exec` is a stub
# that reports `simulated` and executes nothing — in a rolling deploy that
# would mean every node "succeeds" without ever being restarted.
ssh_exec "${node}" "sudo systemctl restart aethershell"
# Health check
let health = web_check_url "http://${node}:3000/health"
if health.reachable {
echo " ✓ ${node} healthy"
} else {
echo " ✗ ${node} FAILED - rolling back"
exit 1
}
})
echo "All nodes deployed successfully"
Git Operations
Change Summary
# Summarize recent changes
let log = sh "git log --oneline -20"
echo "Recent commits:"
echo log
# AI-powered summary
ai "Summarize these git commits into a changelog entry:\n${log}"
Branch Cleanup
# Find merged branches
let branches = sh "git branch --merged" | split "\n" | map(fn(b) => trim b)
| where(fn(b) => b != "main" && b != "master" && !starts_with(b, "*"))
echo "Merged branches to clean up:"
branches | each(fn(b) => echo " ${b}")
Container Management
# List running containers
sh "docker ps --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'"
| split "\n"
| map(fn(line) => {
let parts = split line "\t"
{ name: parts[0], status: parts[1], ports: parts[2] }
})
Backup Script
let timestamp = sh "date +%Y%m%d_%H%M%S" | trim
let backup_dir = "/backups/${timestamp}"
mkdir backup_dir
echo "Backing up database..."
sh "pg_dump mydb > ${backup_dir}/db.sql"
echo "Backing up config..."
file_copy "/etc/aethershell" "${backup_dir}/config"
echo "Backing up data..."
sh "tar czf ${backup_dir}/data.tar.gz /var/lib/aethershell"
# Cleanup old backups (keep last 7)
ls "/backups"
| sort_by "name" "desc"
| slice 7 (ls "/backups" | len)
| each(fn(old) => {
echo "Removing old backup: ${old.name}"
sh "rm -rf ${old.path}"
})
echo "Backup complete: ${backup_dir}"
Cron Jobs
# List scheduled jobs
cron_list
# [{ id: "1", schedule: "0 * * * *", command: "ae health-check.ae" }, ...]
# Add a new cron job
cron_add "0 2 * * *" "ae backup.ae" # Daily at 2 AM
# Remove a job
cron_remove "1"
Network Diagnostics
let target = "api.example.com"
echo "=== Network Diagnostics for ${target} ==="
# DNS resolution
let dns = net_dns_lookup target
echo "DNS: ${dns}"
# Ping
let ping = net_ping target
echo "Ping: ${ping.latency_ms}ms (${if ping.reachable { 'OK' } else { 'FAILED' }})"
# Traceroute
echo "Route:"
net_traceroute target | each(fn(hop) => {
echo " ${hop.hop}. ${hop.ip} (${hop.latency_ms}ms)"
})
Development Setup
How to set up a development environment for contributing to AetherShell.
Prerequisites
- Rust 1.88+ (install via rustup)
- Git
- pkg-config and OpenSSL dev headers (Linux)
- Optional: Ollama for local AI model testing
Platform-Specific
Ubuntu/Debian:
sudo apt install build-essential pkg-config libssl-dev
macOS:
brew install openssl pkg-config
Windows:
# Rust includes MSVC build tools; ensure Visual Studio C++ Build Tools are installed
Clone and Build
git clone https://github.com/nervosys/AetherShell.git
cd AetherShell
cargo build --bins
This produces two binaries:
target/debug/ae— the main shelltarget/debug/aimodel— the AI model management CLI
Run Tests
cargo test # All tests (~272)
cargo test --lib # Library unit tests
cargo test --test eval # Specific test file
cargo test pipeline # Tests matching "pipeline"
Launch for Development
# REPL mode
cargo run
# TUI mode
cargo run -- --tui
# Execute a script
cargo run -- examples/00_hello.ae
# AI model server
cargo run --bin aimodel -- serve --port 8080
Environment Variables
| Variable | Purpose | Example |
|---|---|---|
AETHER_AI | Default AI provider | openai |
OPENAI_API_KEY | OpenAI API key | sk-... |
OLLAMA_HOST | Ollama server URL | http://localhost:11434 |
AGENT_ALLOW_CMDS | Agent tool allowlist | ls,git,cat |
AETHER_LOG | Log level | debug |
IDE Setup
VS Code
Install the AetherShell extension from the marketplace, or load it from source:
cd vscode-extension
npm install
npm run compile
# Press F5 to launch Extension Development Host
Recommended extensions:
- rust-analyzer — Rust language support
- Even Better TOML — Cargo.toml editing
- CodeLLDB — Debugger
Configuration
.vscode/settings.json:
{
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.command": "clippy",
"editor.formatOnSave": true
}
Project Layout
| Directory | Contents |
|---|---|
src/ | Core shell source |
src/ai_api/ | AI model server |
src/tui/ | Terminal UI |
src/transpile/ | Bash transpiler |
src/bin/ | Additional binaries |
tests/ | Integration tests |
test-scripts/ | Script-based tests |
examples/ | Example .ae scripts |
docs/book/ | mdBook documentation |
web/ | Web dashboard & WASM |
vscode-extension/ | VS Code extension |
Code Style
Coding conventions and style guidelines for AetherShell contributors.
Rust Style
General
- Follow standard Rust conventions (
rustfmtdefaults) - Run
cargo fmtbefore committing - Run
cargo clippyand address warnings - Maximum line length: 100 characters (soft limit)
Naming
#![allow(unused)]
fn main() {
// Types: PascalCase
struct PipelineStage { ... }
enum Value { ... }
// Functions/methods: snake_case
fn evaluate_expr(expr: &Expr) -> Result<Value> { ... }
// Constants: SCREAMING_SNAKE_CASE
const MAX_PIPELINE_DEPTH: usize = 64;
// Module files: snake_case
// src/os_tools.rs, src/shell_features.rs
}
Error Handling
#![allow(unused)]
fn main() {
// Use anyhow::Result for evaluator functions
fn eval_expr(expr: &Expr, env: &mut Env) -> anyhow::Result<Value> {
// Add context for debugging
some_operation().context("failed to evaluate pipeline stage")?;
// Avoid .unwrap() in production code — use .expect() with a message or ?
let val = map.get("key").context("missing required key")?;
Ok(val)
}
}
Return Types
Builtins must return structured Value types:
#![allow(unused)]
fn main() {
// Good: structured data for pipeline processing
fn builtin_ls(args: &[Value], env: &mut Env) -> Result<Value> {
Ok(Value::Array(entries.into_iter().map(|e| {
Value::Record(BTreeMap::from([
("name".into(), Value::String(e.name)),
("size".into(), Value::Int(e.size as i64)),
("is_dir".into(), Value::Bool(e.is_dir)),
]))
}).collect()))
}
// Bad: raw text output
fn builtin_ls_bad() -> Result<Value> {
Ok(Value::String("file1.txt\nfile2.txt".into()))
}
}
Imports
#![allow(unused)]
fn main() {
// Group imports: std, external crates, local modules
use std::collections::BTreeMap;
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::ast::{Expr, Stmt};
use crate::value::Value;
}
Commit Messages
Follow Conventional Commits:
type(scope): description
feat(parser): add pattern matching syntax
fix(eval): correct lambda capture semantics
docs(book): add pipeline examples chapter
test(builtins): add filesystem operation tests
refactor(ai): extract provider trait
chore(deps): update tokio to 1.35
Types: feat, fix, docs, test, refactor, chore, perf, ci
Scopes: parser, eval, builtins, ai, tui, agent, api, transpile, docs, deps
Documentation
Code Comments
#![allow(unused)]
fn main() {
/// Evaluates an expression in the given environment.
///
/// # Arguments
/// * `expr` - The AST expression to evaluate
/// * `env` - Mutable reference to the variable environment
///
/// # Returns
/// The resulting `Value`, or an error if evaluation fails.
///
/// # Examples
/// ```
/// let result = eval_expr(&Expr::Int(42), &mut env)?;
/// assert_eq!(result, Value::Int(42));
/// ```
fn eval_expr(expr: &Expr, env: &mut Env) -> Result<Value> { ... }
}
Module-Level Docs
Every .rs file should have a module-level doc comment:
#![allow(unused)]
fn main() {
//! Pipeline evaluation and data flow.
//!
//! This module handles the core pipeline operator (`|`), connecting
//! expressions so that the output of one feeds into the next.
}
Adding New Features
The typical flow for language features:
ast.rs— Add AST node variantstokens.rs— Add tokens if new syntax is neededlexer.rs— Tokenize new syntaxparser.rs— Parse tokens into ASTeval.rs— Implement runtime semanticstypecheck.rs— Add type inference rulestests/— Write comprehensive tests
Testing
How to write and run tests for AetherShell.
Running Tests
# All tests
cargo test
# Library tests only
cargo test --lib
# Specific test file
cargo test --test eval
cargo test --test pipeline
# Tests matching a pattern
cargo test parse_lambda
cargo test "test_builtin_"
# With output (for debugging)
cargo test -- --nocapture
# Single-threaded (for tests that share state)
cargo test -- --test-threads=1
Test Organization
| Location | Purpose |
|---|---|
tests/eval.rs | Core evaluator tests |
tests/parse.rs | Parser unit tests |
tests/pipeline.rs | Pipeline operator tests |
tests/builtins.rs | Builtin function tests |
tests/typecheck.rs | Type inference tests |
tests/smoke.rs | Quick validation / smoke tests |
tests/transpile_bash.rs | Bash transpiler tests |
tests/ai_*.rs | AI integration tests |
tests/tui_*.rs | TUI component tests |
test-scripts/ | Script-based integration tests |
Writing Tests
Evaluator Tests
Test that AetherShell expressions produce expected values:
#![allow(unused)]
fn main() {
use aether_shell::{eval_str, Value};
#[test]
fn test_arithmetic() {
let result = eval_str("2 + 3").unwrap();
assert_eq!(result, Value::Int(5));
}
#[test]
fn test_pipeline() {
let result = eval_str("[1,2,3] | map(fn(x) => x * 2)").unwrap();
assert_eq!(result, Value::Array(vec![
Value::Int(2), Value::Int(4), Value::Int(6)
]));
}
#[test]
fn test_record_access() {
let result = eval_str("let r = {name: \"test\", val: 42}; r.val").unwrap();
assert_eq!(result, Value::Int(42));
}
}
Parser Tests
Verify that source text parses to the expected AST:
#![allow(unused)]
fn main() {
use aether_shell::parser::parse;
use aether_shell::ast::*;
#[test]
fn test_parse_let() {
let stmts = parse("let x = 42").unwrap();
assert!(matches!(&stmts[0], Stmt::Let { name, .. } if name == "x"));
}
#[test]
fn test_parse_lambda() {
let stmts = parse("fn(x) => x + 1").unwrap();
// Verify AST structure
assert!(matches!(&stmts[0], Stmt::Expr(Expr::Lambda { .. })));
}
}
Builtin Tests
Test that builtins return the correct structured values:
#![allow(unused)]
fn main() {
#[test]
fn test_builtin_len() {
let result = eval_str("len [1, 2, 3]").unwrap();
assert_eq!(result, Value::Int(3));
}
#[test]
fn test_builtin_map() {
let result = eval_str("[1,2,3] | map(fn(x) => x * 10)").unwrap();
assert_eq!(result, Value::Array(vec![
Value::Int(10), Value::Int(20), Value::Int(30)
]));
}
#[test]
fn test_builtin_where() {
let result = eval_str("[1,2,3,4,5] | where(fn(x) => x > 3)").unwrap();
assert_eq!(result, Value::Array(vec![Value::Int(4), Value::Int(5)]));
}
}
Type Checker Tests
Verify type inference results:
#![allow(unused)]
fn main() {
#[test]
fn test_typecheck_int() {
let ty = typecheck_str("42").unwrap();
assert_eq!(ty, Type::Int);
}
#[test]
fn test_typecheck_lambda() {
let ty = typecheck_str("fn(x) => x + 1").unwrap();
assert!(matches!(ty, Type::Function(_, _)));
}
}
Test Patterns
Error Cases
Always test error conditions:
#![allow(unused)]
fn main() {
#[test]
fn test_divide_by_zero() {
let result = eval_str("10 / 0");
assert!(result.is_err());
}
#[test]
fn test_undefined_variable() {
let result = eval_str("unknown_var");
assert!(result.is_err());
}
}
Script-Based Tests
For complex scenarios, use .ae test scripts in test-scripts/:
# test-scripts/builtins/test_basic.ae
# Each line is a self-contained assertion
let x = 42
assert x == 42
let arr = [1, 2, 3]
assert (len arr) == 3
let s = upper "hello"
assert s == "HELLO"
AI Tests
AI tests that require API keys should check for availability:
#![allow(unused)]
fn main() {
#[test]
fn test_ai_completion() {
if std::env::var("OPENAI_API_KEY").is_err() {
eprintln!("Skipping: OPENAI_API_KEY not set");
return;
}
let result = eval_str(r#"ai "Say hello" { model: "openai:gpt-4o-mini" }"#).unwrap();
assert!(matches!(result, Value::String(_)));
}
}
Coverage
While there’s no strict coverage requirement, aim for:
- Core evaluator: High coverage — test every expression type
- Builtins: At least one test per builtin function
- Parser: Test both valid syntax and error cases
- Type checker: Test each inference rule
- AI/TUI: Test structure and state, mock network calls
Continuous Integration
Tests run automatically on pull requests. Ensure:
cargo testpasses with no failurescargo clippyproduces no warningscargo fmt --checkshows no formatting issues
Changelog
The changelog is maintained in one place, at the root of the repository:
- CHANGELOG.md — every release, in Keep a Changelog form.
- Releases — the tagged builds, with binaries for Linux, macOS and Windows.
This page used to carry a second, hand-written copy covering v0.1.0 to v0.3.0, with v0.3.0 marked “(Current)”. It had not been touched in the eleven major versions since, and because the book was never built, nobody saw it say so. A duplicate that drifts is worse than a link.
FAQ
General
What is AetherShell?
AetherShell is a next-generation shell written in Rust that combines typed functional programming with multimodal AI capabilities. Unlike traditional shells that pass raw text between commands, AetherShell uses structured data types (records, arrays, tables) throughout its pipeline system.
How is AetherShell different from Bash/Zsh?
| Feature | Bash/Zsh | AetherShell |
|---|---|---|
| Data model | Raw text | Typed values (Int, Record, Array, …) |
| Pipelines | Text streams | Structured data flow |
| Functions | String-based | Typed lambdas with inference |
| AI | None | Built-in LLM, agents, RAG |
| Pattern matching | Case statements | match expressions |
| Error handling | Exit codes | Result types with context |
Can I use AetherShell as my daily driver?
AetherShell is under active development. It’s excellent for data processing, AI automation, and scripting. For interactive daily use, you may want to keep your current shell available while adopting AetherShell incrementally.
How do I run Bash commands in AetherShell?
Use the sh builtin:
sh "git status"
sh "docker ps"
Language
Why typed pipelines?
Typed pipelines eliminate an entire class of bugs. When ls returns an array of records with known fields, where, map, and sort_by can validate their arguments at parse time rather than failing silently at runtime.
Does AetherShell support loops?
AetherShell favors functional iteration via map, each, reduce, and where. Recursion is supported for looping patterns:
let countdown = fn(n) => if n > 0 { echo n; countdown(n - 1) } else { echo "done" }
countdown 5
What is fn(x) => expr?
This is a lambda (anonymous function). Lambdas are first-class values — they can be stored in variables, passed to builtins, and returned from functions:
let double = fn(x) => x * 2
[1,2,3] | map(double) # [2, 4, 6]
AI
Which AI providers are supported?
AetherShell supports six providers via model URIs:
openai:gpt-4o— OpenAIollama:llama3— Local Ollamacompat:mixtral— OpenAI-compatible APIstgi:model— HuggingFace Text Generation Inferencevllm:model— vLLM servingllamacpp:model— llama.cpp server
Do I need an API key?
For cloud providers (OpenAI), yes — set OPENAI_API_KEY. For local providers (Ollama, llama.cpp), no API key is needed.
How do agents work?
Agents use a ReAct (Reason + Act) loop: they think about the task, choose a tool, execute it, observe the result, and repeat until the goal is met:
agent {
goal: "Find the largest file in the current directory",
tools: ["ls", "sort_by"],
max_steps: 5
}
What is a swarm?
A swarm is a group of AI agents that collaborate on a task. A coordinator routes subtasks to specialized agents, and they share state via a blackboard:
swarm {
goal: "Analyze this project",
tools: ["ls", "cat", "grep"],
max_steps: 20
}
TUI
How do I launch the TUI?
ae --tui
What are the TUI tabs?
- Chat — AI conversation interface
- Agent Swarm — Monitor multi-agent collaboration
- Media Browser — View images, audio, video
- Settings — Configure providers and appearance
- Distributed — Manage cluster nodes
- Reasoning — Advanced chain-of-thought display
- Search — Search chat history
How do I switch tabs?
Press Tab and Shift+Tab to cycle through tabs, or press 1–7 in Normal mode.
Troubleshooting
cargo build fails with OpenSSL errors
Install OpenSSL development headers:
# Ubuntu/Debian
sudo apt install libssl-dev pkg-config
# macOS
brew install openssl
AI commands return “no provider configured”
Set the provider environment variable:
export AETHER_AI=openai
export OPENAI_API_KEY=sk-...
Tests fail with “connection refused”
Some tests require a running Ollama instance or API keys. Tests that need external services will skip gracefully if the required environment is not available.
TUI looks broken
Ensure your terminal supports 256 colors and Unicode. Recommended terminals: Windows Terminal, iTerm2, Alacritty, kitty.