Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🦀 air (Rust CLI)

AI optimization from your terminal. AIR (Artificial Intelligence Ranking) by airanks makes AI optimization visible — a 0–10 score for how often and how well AI answers cite a site. Look up any domain's AIR score live at airanks.net, or install the browser toolbar at airanks.net/toolbar. This repo is air, the native Rust build of the AIR command-line client — same API, same auth, same output, just compiled instead of interpreted.

crate version edition license platforms deps

$ air airanks.net

  🌐 airanks.net   🏆 AIR 7/10  ██████████████████████░░░░░░
  📊 tracked — 412 occurrences · 38 phrases · 19 brands
  📄 llms.txt ✔  llms-full ✔  ai.txt ✘  robots.txt ✔  json-ld ✔ (Organization)
  🛡 ai crawlers: 6 allowed · 1 partial · 2 blocked of 9

📖 Table of contents


🤔 Why a native build?

Most of what people call "SEO for AI" is guesswork — nobody outside the model vendors can see which pages actually got cited. AIR is built from real, replayable observations of what AI answers actually pointed to, not a simulation of what might rank. This client puts that number one command away — in a shell, a CI pipeline, or a git pre-push hook — instead of only a browser tab.

air is the Rust port of the reference node-cli: same wire contract, same auth file, same rendered output — down to the Font Awesome glyphs and the truecolor score gauge — just compiled to a single small binary (opt-level = "z", LTO, stripped) with no CLI-parsing framework. Flag handling is the same handful of starts_with('-') checks the Node reference implementation uses — see src/main.rs.

📦 Install

Needs a Rust toolchain (rustup.rs).

cargo install --path .          # from a checkout of this repo
cargo install air-cli           # from crates.io, once published
🍺 Homebrew

The airanks-net/tap Homebrew formula currently builds air from go-cli (brew install airanks-net/tap/air), not this Rust crate — the auto-updater's fallback hint (brew upgrade air (or) cargo install air-cli --force) assumes either build is interchangeable since both ship the same air binary name and wire contract. If you want the Rust build specifically, use cargo install.

Either install drops an air binary on your $PATH.

⚡ Quickstart

air airanks.net                # domain lookup
air login                      # required — anonymous lookups now get a 401

That's it — a domain that's never been looked up before gets hydrated on the spot; air polls quietly in the background until the score is ready.

🧭 Commands

Command What it does
air <domain> Look up a domain's AIR score. Polls while it's still being gathered.
air "<keyword phrase>" Multi-word input searches domains/brands/phrases instead.
air -d <input> / --domain Force domain mode.
air -k <input> / --keyword / -p / --phrase Force search mode.
air <input> --json Machine-readable JSON on stdout.
air <input> --txt Plain text — no color, no icons (same as TERM=dumb, CI=true, NO_COLOR).
air login Paste an API key, or log in via your browser (PKCE).
air logout Remove the locally saved token (local only — server-side token still valid until it expires).
air whoami Show who you're logged in as, and on what tier.
air --help / -h Usage text.

On first run with nothing configured (no AIR_API_KEY, no saved login) and an interactive TTY, air shows a one-time setup wizard — paste a key, log in via browser, or continue anonymously — see should_run_setup_wizard. Note: "continue anonymously" just skips the wizard, it doesn't get you working lookups — anonymous requests now get a 401 from the API, so you'll need to run air login before a lookup will actually succeed.

🔄 How a lookup works

GET /v1/domains/{host} always 200s. The first time anyone looks up a host, the server dispatches hydration and reports ai_files.status: "pending" — the CLI just polls quietly until it flips to ready or a ~180s wall-clock budget runs out (AIR_POLL_MAX_MS), honoring Retry-After on a 429 along the way. A live braille spinner runs on stderr while it waits.

flowchart TD
    A["$ air &lt;input&gt;"] --> B{login / logout / whoami?}
    B -- yes --> Z[run auth command, exit]
    B -- no --> C{looks like a hostname?<br/>-d forces domain, -k/-p forces search}
    C -- no --> S["GET /v1/search?q=..."] --> S2[render domains · brands · phrases]
    C -- yes --> D["GET /v1/domains/{host}"]
    D --> E{ai_files.status == pending?}
    E -- no --> F[render AIR score + gauge]
    E -- yes --> G{deadline left?<br/>~180s budget}
    G -- yes --> H["sleep AIR_POLL_MS,<br/>spinner on stderr"] --> D
    G -- no --> I["render 'still gathering'<br/>exit code 2"]
Loading

🔍 Search mode

Anything that doesn't parse as a single hostname (spaces, or forced with -k/-p) hits GET /v1/search?q=... and prints up to 5 matches each across domains, brands, and phrases, each with its own AIR score.

air "best crm software"

🔐 Shared authentication — one login, every AIR client

A free account is now required to use the API — grab a token at airanks.net/tokens and set it via AIR_API_KEY or air login. The official browser toolbar is the only caller exempt from this; every CLI/SDK client needs a token.

air is one of several official AIR clients — Node, Rust, Go, and a PHP Composer package — and they all resolve auth identically, per API-CONTRACT.md:

  1. AIR_API_KEY env var, if set — always wins, always attaches (explicit intent).
  2. Otherwise ~/.config/air/auth.json (mode 0600, dir 0700) — written by whichever client you last ran login in. A file-sourced token only rides to the host it was minted for, so it can't leak if you repoint AIR_API_BASE somewhere else.
  3. Otherwise you're anonymous — and the API now rejects that with a 401. Run air login first.

Run air login once, in any AIR client, and every other AIR client on the machine picks up the same session — same token, same file, same host-scoped attach rule.

🔑 Browser (PKCE S256) login sequence
sequenceDiagram
    autonumber
    participant CLI as air login
    participant API as airanks.net API
    participant Browser as your browser

    CLI->>CLI: generate PKCE verifier + S256 challenge
    CLI->>API: POST /v1/cli/auth {code_challenge}
    API-->>CLI: {nonce, browser_url, confirmation_code, interval, expires_in}
    CLI->>Browser: open browser_url (same-origin only)
    Note over CLI,Browser: user enters confirmation_code, approves
    loop every `interval`s until expires_in
        CLI->>API: POST /v1/cli/auth/poll {nonce, code_verifier}
        API-->>CLI: {status: pending | ok | denied | expired | forbidden}
    end
    API-->>CLI: status "ok" → {token, user, expires_at}
    CLI->>CLI: write ~/.config/air/auth.json (0600)
Loading

Over SSH, or headless Linux with no $DISPLAY, air prints the URL instead of trying to auto-open it. It also refuses to auto-open a cross-origin browser_url — see open_browser.

🎛️ Environment variables

Variable Effect
AIR_API_KEY Bearer token, wins over any saved login (see above).
AIR_API_BASE Override the API base — default https://airanks.net/api/v1.
AIR_POLL_MS Interval between pending polls (ms). Default 20000.
AIR_POLL_MAX_MS Total wall-clock budget for a lookup (ms) — polls and 429 backoff both count against it. Default 180000.
AIR_NO_UPDATE Set (any value) to disable the once-a-day auto-update check.
NO_COLOR Drop ANSI color; icons/glyphs stay.
TERM=dumb / CI=true Same as --txt — plain text, no color, no icons.
COLUMNS Clamps wrapped output width (max 74 cols).

🚦 Exit codes

Code Meaning
0 Normal result.
1 Hard failure — bad input, API error, or a rate-limit that outlasted the poll deadline.
2 Domain is still being gathered at the deadline — re-run in a bit.

🎨 Output modes

  • Default — truecolor score gauge, Font Awesome glyphs (Nerd Font recommended), OSC-8 clickable links in supporting terminals.
  • --json — the raw API data object, pretty-printed, for scripts and CI.
  • --txt — no color, no glyphs, no gauge. Same as TERM=dumb, CI=true, or a non-TTY stdout.

⬆️ Auto-update

Once a day, on an interactive run only, air checks GitHub Releases (api.github.com/repos/airanks-net/rust-cli) for a newer version and — if found — best-effort runs cargo install air-cli --force in the background (60s timeout, then killed). Every failure mode (offline, GitHub down, a slow install) is swallowed; your actual command always proceeds either way. Opt out with AIR_NO_UPDATE. See src/update.rs.

🛠️ Development

cargo build
cargo test
cargo run -- airanks.net
📦 Dependencies (kept deliberately small)
Crate Why
reqwest (blocking, rustls) HTTP client
serde / serde_json Wire format
url RFC-3986-correct hostname parsing
sha2 / rand / base64 PKCE (S256 challenge, verifier)
chrono Timestamps (auth.json, whoami expiry)

No CLI-parsing crate — argument handling is a handful of flag checks mirroring the Node reference implementation. Unit tests live inline (#[cfg(test)]) in hostname.rs, auth.rs, and update.rs.

🌐 Other AIR / airanks clients

Every client speaks the same API-CONTRACT.md and shares the same login.

Repo What it is
node-cli Reference implementation — the CLI this one ports.
go-cli Go build of air (also what the Homebrew tap builds today).
composer-package airanks-net/api-client — PHP library, not a CLI.
chrome-extension The AIR toolbar — any site's AI Rank in your browser.
mcp-server MCP server exposing AIR lookups as agent tools.
js-sdk JS/TS SDK for Node & browser.
python-sdk Python SDK (pip install airanks).
homebrew-tap brew install airanks-net/tap/air.

📜 License

MIT — see LICENSE.


Made to keep AI optimization honest — one terminal command, one shared login, zero dashboard tabs. airanks.net · airanks.net/toolbar 🚀

About

air (Rust) — AI optimization from your terminal: look up any site's AI Rank. airanks / airanks.net

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages