Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AetherShell

Welcome to the official AetherShell documentation!

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

FeatureDescription
Typed ValuesInt, Float, String, Bool, Array, Record, Table, Lambda
Pipeline Operators|, |>, ?> with full type inference
Pattern Matchingmatch expressions with guards and destructuring
AI Providers20 provider types; OpenAI, Anthropic, Google and Ollama have dedicated clients, the rest go over OpenAI-compatible endpoints
Agent FrameworkSingle agents with builtins as tools, over MCP or in-process
MCPae mcp stdio makes every builtin callable through a three-tool facade
Interactive TUIReal-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

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 executable
  • aimodel - 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

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

CommandDescription
ls [path]List directory contents as a table
cd pathChange directory
pwdPrint working directory
cat fileRead file contents
envList environment variables
http_get urlMake HTTP GET request
print valueDisplay 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

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

VariableDescriptionExample
AETHER_AIDefault AI provideropenai, claude, ollama
OPENAI_API_KEYOpenAI API keysk-...
ANTHROPIC_API_KEYAnthropic API keysk-ant-...
GOOGLE_API_KEYGoogle AI API keyAIza...
OLLAMA_HOSTOllama server URLhttp://localhost:11434
AGENT_ALLOW_CMDSAllowed agent commandsls,cat,grep
AETHERSHELL_LOGLog leveldebug, 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)

  1. Install Ollama: https://ollama.ai
  2. Pull a model: ollama pull llama3
  3. 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

OperatorDescriptionExample
+Addition1 + 23
-Subtraction5 - 32
*Multiplication4 * 312
/Division10 / 33
%Modulo10 % 31
**Power2 ** 8256

Comparison

OperatorDescription
==Equal
!=Not equal
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal

Logical

OperatorDescription
&&Logical AND
||Logical OR
!Logical NOT

Pipeline

OperatorDescription
|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

TypeExampleDescription
NullnullAbsence of value
Booltrue, falseBoolean
Int42, -764-bit signed integer
Float3.14, -0.564-bit floating point
String"hello"Text with interpolation support
Uriopenai:gpt-4o-miniURI with scheme (RFC 3986)
Array[1, "two", true]Heterogeneous ordered list
Record{name: "Ada", age: 36}Ordered key-value map
Tableoutput of lsStructured table with schema
Lambdafn(x) => x * 2First-class function
Errorthrow "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:

ExpressionResult TypeRule
Int + IntIntInteger arithmetic
Int + FloatFloatPromote to float
Float + FloatFloatFloat arithmetic
Int ^ Int (positive)IntInteger power
Int ^ Int (negative)FloatFloat power
Int / IntFloatAlways float division

Equality comparison between Int and Float works via casting.

Truthiness

All values have a boolean interpretation:

FalsyTruthy
nullNon-null values
falsetrue
0, 0.0Non-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

SyntaxMeaning
let x = exprImmutable declaration
let mut x = exprMutable declaration
x = exprShorthand immutable declaration (or reassign if mutable)
x := exprShorthand immutable declaration
mut x = exprShorthand mutable declaration
pub let x = exprPublic 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 sideBehavior
Lambda literalCalled with left value as argument
Named lambda (variable)Called with left value as explicit argument
Builtin functionReceives left value as input parameter
Other expressionLeft 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:

PatternMatches
42Exact integer
"hello"Exact string
true / falseExact boolean
nullNull 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:

  1. Error values — produced by throw:

    try { throw "oops" } catch e { "caught: ${e}" }
    # => "caught: oops"
    
  2. 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

ErrorCause
"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

  • null represents absence of a value (intentional)
  • Error(msg) represents a failure with a message (exceptional)
  • Both are falsy, but Error carries 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

CommandDescriptionReturns
ls pathList directoryArray[Record]
cat fileRead file contentsString
read fileRead file (alias)String
write file contentWrite to fileBool
mkdir pathCreate directoryBool
rm pathRemove file/dirBool
mv src dstMove/renameBool
cp src dstCopyBool
pwdCurrent directoryString
cd pathChange directory()

Data Processing

CommandDescriptionReturns
map(fn)Transform each elementArray
filter(fn)Keep matching elementsArray
reduce(fn, init)Fold to single valueAny
sort_by(field, dir)Sort by fieldArray
where(fn)Filter (alias)Array
select(fields...)Pick fieldsArray[Record]
take(n)First n elementsArray
skip(n)Skip n elementsArray
flatten()Flatten nested arraysArray
unique()Remove duplicatesArray
group_by(field)Group by fieldRecord

Text Processing

CommandDescriptionReturns
grep pattern pathSearch in filesArray[Record]
split str delimSplit stringArray[String]
join arr delimJoin to stringString
trim strRemove whitespaceString
replace old new strReplace textString
uppercase strTo uppercaseString
lowercase strTo lowercaseString

Network

CommandDescriptionReturns
http_get urlGET requestRecord
http_post url bodyPOST requestRecord
http_put url bodyPUT requestRecord
http_delete urlDELETE requestRecord

JSON/Data

CommandDescriptionReturns
json_parse strParse JSONAny
json_stringify valSerialize to JSONString
csv_parse strParse CSVArray[Array]
csv_stringify dataSerialize to CSVString

System

CommandDescriptionReturns
envAll env varsRecord
env_get nameGet env varString?
env_set name valSet env var()
exec cmd argsRun commandRecord
which nameFind executableString?
psProcess listArray[Record]

Math

CommandDescriptionReturns
sum arrSum of numbersNumber
avg arrAverageFloat
min arrMinimumNumber
max arrMaximumNumber
abs nAbsolute valueNumber
floor nFloorInt
ceil nCeilingInt
round nRoundInt
sqrt nSquare rootFloat

Type Conversion

CommandDescriptionReturns
int valConvert to intInt
float valConvert to floatFloat
string valConvert to stringString
bool valConvert to boolBool
array valConvert to arrayArray

Output

CommandDescriptionReturns
print valPrint value()
println valPrint with newline()
debug valDebug output()
format str args...Format stringString

AI

CommandDescriptionReturns
ai prompt opts?AI queryString
agent prompt opts?Create agentAgent

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"

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

Create a symbolic link.

fs_symlink "target_path" "link_path"

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:

OperatorDescriptionExampleResult
+Addition3 + 47
-Subtraction10 - 37
*Multiplication6 * 742
/Division10 / 33
%Modulo10 % 31

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

OperatorDescriptionExample
==Equal3 == 3
!=Not equal3 != 4
<Less than3 < 5
>Greater than5 > 3
<=Less or equal3 <= 3
>=Greater or equal5 >= 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:

ProviderModelsSetup
OpenAIGPT-4o, GPT-4, GPT-3.5OPENAI_API_KEY
AnthropicClaude 3 Opus/Sonnet/HaikuANTHROPIC_API_KEY
GoogleGemini Pro, Gemini FlashGOOGLE_API_KEY
MetaLlama 3, CodeLlamaVia Ollama/Together
MistralMistral Large, CodestralMISTRAL_API_KEY
CohereCommand R, Command R+COHERE_API_KEY
xAIGrokXAI_API_KEY
DeepSeekDeepSeek V3, R1DEEPSEEK_API_KEY
OllamaAny local modelLocal install
OpenRouter100+ modelsOPENROUTER_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

  1. Set defaults - Configure your preferred provider:

    export AETHER_AI=openai
    export OPENAI_API_KEY=sk-...
    
  2. Use appropriate models - GPT-4o for complex tasks, GPT-3.5 for simple ones

  3. Control costs - Set max_tokens to limit response length

  4. Handle errors - AI APIs can fail; always have fallbacks

  5. Stream long responses - Set stream: true for 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:

URIProviderExample
openai:model-nameOpenAIopenai:gpt-4o-mini
ollama:model-nameOllama (local)ollama:llama3
compat:model-nameOpenAI-compatible APIcompat:mixtral
tgi:model-nameHuggingFace TGItgi:mistral-7b
vllm:model-namevLLMvllm:meta-llama/Llama-3-8B
llamacpp:model-namellama.cppllamacpp: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 key
  • OPENAI_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

See Configuring Providers.

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 --agent puts the shell in default-deny mode, gating destructive effect classes behind approval.
  • ae --workspace <dir> confines writes and destructive operations to that directory.
  • dry_run lets 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. RoundRobin takes agents in turn; Router sends each turn to the agent whose declared capabilities best match. Both coordinators are implemented; neither is selectable from the shell, because the swarm builtin reads only goal, tools, max_steps and dry_run.
  • Blackboard. Agents would share a message list with kinds note, thought and final, 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_sandbox is 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 sh builtin is gated behind AETHER_ALLOW_SH=true and 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"]) returns error[E_UNKNOWN]: Tool execution failed: Tool 'ls' is not supported on Windows instead of falling back to something that merely looks similar.

  • Safety. Every tool carries a level — Safe, Caution, Dangerous, or Critical. Of the 198 catalogued tools, 131 are Safe, 49 are Caution, 14 are Dangerous and 4 are Critical:

    tool_list() | where(fn(t) => t.safety == "Dangerous") | len
    # 14
    

    Safe and Caution run normally. Dangerous and Critical are refused outright unless the third argument is true, 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_admin is 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:

ToolPurpose
ontology_manifestthe categories, with counts and effect classes
ontology_describeexpand a category into its builtins, or one builtin into full detail
aetherinvoke 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:

KeyStepExample
runcall a builtin{run: "grep", args: ["TODO", "src"]}
agentrun an agent{agent: "reviewer", prompt: "$.task"}
urlmake an HTTP request{url: "https://example.com", method: "POST", body: {...}}
delay_mswait{delay_ms: 500}
emitemit an event{emit: "ready", payload: 7}
waitwait for an event{wait: "ready", timeout_ms: 2000}
workflowrun another template{workflow: "<template id>"}

Any step also takes:

KeyMeaning
idits name in results and events (defaults to step-N)
inputthe variable to feed it, as $.name
outputwhere to store its result, as $.name
whena condition; the step is skipped unless it holds
timeout_mshow long it may run before failing
retrieshow many times to retry, with exponential backoff
compensatethe 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 run step 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 agent step goes through the agent builtin, so AGENT_ALLOW_CMDS and the rate limit still apply.
  • A url step goes through the same egress allowlist and SSRF validation as http_get.
  • In agent mode, workflow_execute and workflow_create require approval — running a workflow is the same capability as running the commands inside it. workflow_templates and the other read-only calls do not.
  • A workflow step 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 workflow step to nest one workflow inside another.
  • Choreography runs 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
└──────────────────────────────────────────────────────┘
  1. Header — Tab bar showing the current mode
  2. Main Content — Changes based on the active tab
  3. Footer — Text input (70%) and help hints (30%)

Tabs / Modes

Switch between modes using Tab / Shift+Tab or number keys 1-6:

#TabDescription
1ChatAI conversation with multimodal support
2AgentsCreate and manage AI agent swarms
3MediaBrowse and select images, audio, video
4SettingsConfigure model, preferences
5DistributedManage distributed agent networks
6ReasoningAdvanced 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:

SettingDefaultDescription
Model$AETHER_AIDefault AI model for chat
Max messages1,000Message history limit
Auto-scrollOnScroll to latest message
TimestampsOnShow message timestamps
Media previewOnEnable in-terminal image preview
Agent update interval1,000msAgent status refresh rate

Quick Start

  1. Set your AI provider:

    export AETHER_AI=openai
    export OPENAI_API_KEY=sk-...
    
  2. Launch the TUI:

    ae tui
    
  3. Press Enter to start typing, write your message, press Enter to send

  4. Press Tab to explore other modes (Agents, Media, etc.)

  5. Press q or Ctrl+C to exit

Navigation

The TUI uses a modal key binding system with Normal and Editing modes.

Global Keys (Normal Mode)

These work from any tab:

KeyAction
q / Esc / Ctrl+C / Ctrl+QQuit
TabNext tab (cycles through all modes)
Shift+TabPrevious tab
1-6Jump to specific tab
/ kMove selection up (wraps around)
/ jMove selection down (wraps around)

Chat Mode

Normal Mode

KeyAction
Enter / iEnter Editing mode
cClear conversation
mSwitch to Media tab
aSwitch to Agents tab
Ctrl+EExport conversation to Markdown
Ctrl+JExport conversation to JSON
Ctrl+LClear conversation
Ctrl+FOpen Search

Editing Mode

KeyAction
EnterSend message
EscCancel, return to Normal mode
Arrow keysNavigate within text input
Any keyTypes into the input field

Agent Swarm Mode

Normal Mode

KeyAction
nCreate new agent
d / DeleteRemove selected agent
Enter / sEnter Editing mode (type task)
mView agent metrics
rRestart selected agent
cSwitch to Chat

Media Browser Mode

Normal Mode

KeyAction
Space / EnterToggle file selection
oOpen/add file
cClear all selections
d / DeleteRemove file from library
bReturn to Chat with selected media

Search Mode

Normal Mode

KeyAction
i / /Enter search Editing mode
/ jNext search result
/ kPrevious search result
EscClear search, return to Chat
Ctrl+CCopy selected result

Distributed Agents Mode

KeyAction
sStart distributed swarm
dStop distributed swarm
rRefresh network status
tTest connection

Advanced Reasoning Mode

KeyAction
nNew reasoning session
pView planning goals
kBrowse knowledge base
eExport reasoning chains
iImport knowledge
  • 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
  • Esc always returns to Normal mode from Editing mode
  • In Chat mode, Ctrl+F enters 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

  1. Press Enter or i to enter Editing mode
  2. Type your message
  3. Press Enter to send

The TUI sends your message to the configured AI model and displays the response.

Message Types

Messages are color-coded by role:

RoleColorEmojiDescription
UserCyan👤Your messages
AssistantGreen🤖AI responses
SystemYellow⚙️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:

  1. Switch to the Media tab (m or 3)
  2. Select files with Space or Enter
  3. Press b to 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

SettingDefaultDescription
auto_summarizefalseAuto-summarize when context grows too large
context_window_size4,096Max tokens in context window
enable_media_analysistrueAnalyze media files with vision models
temperature0.7AI response temperature
max_tokensNoneMax tokens per response
system_promptNoneCustom 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 through your conversation history:

  1. Press Ctrl+F to enter Search mode
  2. Type your search query
  3. Press Enter to search
  4. Use / to navigate results
  5. Press Esc to return to Chat

Search is case-insensitive and matches against message content.

Export

Export your conversation for sharing or archival:

ShortcutFormatFile
Ctrl+EMarkdownconversation_export.md
Ctrl+JJSONconversation_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+L to 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

CategoryExtensions
Imagejpg, jpeg, png, gif, bmp, webp, tiff, svg
Videomp4, avi, mov, mkv, wmv, flv, webm, m4v
Audiomp3, 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

KeyAction
Space / EnterToggle file selection
/ / j / kNavigate file list
oOpen/add file
d / DeleteRemove file from library
cClear all selections
bReturn 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:

  1. Press m to open Media Browser
  2. Select files with Space (multiple selections allowed)
  3. Press b to return to Chat
  4. 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:

TypeDisplay 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:

TerminalImage Support
iTerm2Excellent (native image protocol)
KittyExcellent (native image protocol)
WezTermGood (Sixel graphics)
Windows TerminalBasic (Unicode blocks)
VS Code TerminalBasic (Unicode blocks)
Standard terminalsBasic (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

KeyAction
EnterSend message
Ctrl+CCancel/Exit
Ctrl+LClear screen
TabSwitch panels
↑/↓Scroll history
Ctrl+NNew conversation
Ctrl+SSave conversation
Ctrl+OOpen file
Ctrl+AToggle agent panel
Ctrl+MToggle 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-latte
  • dracula
  • nord
  • solarized-dark
  • solarized-light

Keyboard Shortcuts Reference

General

  • Ctrl+C - Exit/Cancel
  • Ctrl+L - Clear screen
  • Ctrl+Q - Quit TUI
  • ? - Help overlay
  • Tab - Next panel
  • Shift+Tab - Previous panel
  • ↑/↓ - Scroll/History
  • PgUp/PgDn - Page scroll

Actions

  • Enter - Send/Confirm
  • Ctrl+N - New conversation
  • Ctrl+S - Save
  • Ctrl+O - Open

Panels

  • Ctrl+A - Agents panel
  • Ctrl+M - Model selector
  • Ctrl+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

MethodPathDescription
POST/api/v1/stream/executeStream command execution
POST/api/v1/stream/pipelineStream pipeline processing
POST/api/v1/stream/evalStream 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

EventDescription
startOperation has begun
progressProgress update with step number and message
dataIntermediate data result
completeOperation finished successfully
errorOperation 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

MethodDescription
.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:

  1. Custom builtins — New commands available in the shell
  2. Tool schemas — OpenAI-compatible function descriptions for AI agent use
  3. 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_exec is 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 with status: "simulated" and simulated: 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-tagged Exec and 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 swarm
  • d — Stop distributed swarm
  • r — Refresh network status
  • t — 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

MethodEndpointDescription
GET/api/v1/marketplace/search?q=...Search agents
GET/api/v1/marketplace/agentsList all agents
POST/api/v1/marketplace/installInstall an agent
POST/api/v1/marketplace/uninstallUninstall an agent
POST/api/v1/marketplace/publishPublish 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

  1. Open the Marketplace tab
  2. Click Publish Agent
  3. Fill in the form: name, description, system prompt, tools, model
  4. Click Publish

Agent Structure

Marketplace agents include:

FieldDescription
nameUnique agent identifier
descriptionWhat the agent does
authorPublisher name
versionSemantic version
system_promptAgent’s system instructions
toolsAllowed builtin tools
modelRecommended model URI
tagsCategory tags for discovery
downloadsDownload count
starsCommunity 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:

LimitDefaultDescription
Timeout30 secondsMaximum execution time
Output10 MBMaximum output size
Memory512 MBMaximum 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

OperationLimit
Agent plans10 per minute
Agent executions5 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_KEY environment 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

  1. Always set AGENT_ALLOW_CMDS in production — never give agents unrestricted access
  2. Use RBAC for multi-user deployments
  3. Enable audit logging for compliance-sensitive environments
  4. Review agent plans with dry_run: true before allowing execution of destructive operations
  5. Rotate API keys regularly and use the keyring for storage
  6. 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:

ModelSpeedCostBest For
gpt-4o-miniFastLowSimple queries, classification
gpt-4oMediumHighComplex reasoning, code generation
ollama:llama3VariableFreePrivacy-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:

DocumentsIndex TimeSearch 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"]
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 shell
  • target/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

VariablePurposeExample
AETHER_AIDefault AI provideropenai
OPENAI_API_KEYOpenAI API keysk-...
OLLAMA_HOSTOllama server URLhttp://localhost:11434
AGENT_ALLOW_CMDSAgent tool allowlistls,git,cat
AETHER_LOGLog leveldebug

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

DirectoryContents
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 (rustfmt defaults)
  • Run cargo fmt before committing
  • Run cargo clippy and 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:

  1. ast.rs — Add AST node variants
  2. tokens.rs — Add tokens if new syntax is needed
  3. lexer.rs — Tokenize new syntax
  4. parser.rs — Parse tokens into AST
  5. eval.rs — Implement runtime semantics
  6. typecheck.rs — Add type inference rules
  7. tests/ — 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

LocationPurpose
tests/eval.rsCore evaluator tests
tests/parse.rsParser unit tests
tests/pipeline.rsPipeline operator tests
tests/builtins.rsBuiltin function tests
tests/typecheck.rsType inference tests
tests/smoke.rsQuick validation / smoke tests
tests/transpile_bash.rsBash transpiler tests
tests/ai_*.rsAI integration tests
tests/tui_*.rsTUI 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:

  1. cargo test passes with no failures
  2. cargo clippy produces no warnings
  3. cargo fmt --check shows no formatting issues

Changelog

The changelog is maintained in one place, at the root of the repository:

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?

FeatureBash/ZshAetherShell
Data modelRaw textTyped values (Int, Record, Array, …)
PipelinesText streamsStructured data flow
FunctionsString-basedTyped lambdas with inference
AINoneBuilt-in LLM, agents, RAG
Pattern matchingCase statementsmatch expressions
Error handlingExit codesResult 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 — OpenAI
  • ollama:llama3 — Local Ollama
  • compat:mixtral — OpenAI-compatible APIs
  • tgi:model — HuggingFace Text Generation Inference
  • vllm:model — vLLM serving
  • llamacpp: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?

  1. Chat — AI conversation interface
  2. Agent Swarm — Monitor multi-agent collaboration
  3. Media Browser — View images, audio, video
  4. Settings — Configure providers and appearance
  5. Distributed — Manage cluster nodes
  6. Reasoning — Advanced chain-of-thought display
  7. Search — Search chat history

How do I switch tabs?

Press Tab and Shift+Tab to cycle through tabs, or press 17 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.