flyr-cli 1.6.0

Search Google Flights from the terminal
Documentation

flyr

crates.io License: GPL-3.0 Rust GitHub stars GitHub last commit

Google Flights from your terminal. Single binary, no API key, no browser.

prompt: find the cheapest round-trip from Helsinki to somewhere warm, departing tomorrow. open the best one in my browser.

agent runs:

flyr search -f HEL -t AYT,NCE,ATH,RAK,LPA,SIN,BKK -d 2026-03-01 --compact --top 1 --currency EUR --return-date 2026-03-08 --open

output:

=== AYT ===
€236 | HEL>SAW>AYT | 7h15m | 1 stop SAW | Pegasus | Mar01 06:30>15:45
=== NCE ===
€260 | HEL>CDG>NCE | 5h50m | 1 stop CDG | Air France | Mar01 09:10>14:00
=== ATH ===
€331 | HEL>AMS>ATH | 8h20m | 1 stop AMS | KLM | Mar01 07:45>18:05
=== RAK ===
€347 | HEL>LIS>RAK | 10h05m | 1 stop LIS | Finnair, TAP | Mar01 11:00>21:05
=== LPA ===
€443 | HEL>LPA | 6h30m | nonstop | Norwegian | Mar01 08:00>14:30
=== SIN ===
€710 | HEL>AMS>SIN | 16h45m | 1 stop AMS | KLM | Mar01 07:45>06:30
=== BKK ===
€859 | HEL>DOH>BKK | 14h20m | 1 stop DOH | Finnair, Qatar | Mar01 17:00>11:20
Opening: https://www.google.com/travel/flights/search?tfs=GhoSCjIwMjYtMDMtMDFqBRIDSEVMcgUSA0xQQRoaEgoyMDI2LTAzLTA4agUSA0xQQXIFEgNIRUxCAQFIAZgBAQ&tfu=EgYIABAAGAA&curr=EUR&hl=en
One command, 7 destinations, ~5k tokens. No browser, no clicking, no cookie banners. Pick the flights you want and the agent opens them directly on Google Flights to book.

Why

Google Flights has no API. The website is slow, requires a browser, and you can only search one route at a time. If you want to compare 10 destinations you're clicking through 10 separate searches, waiting for each page to load, fighting cookie banners.

flyr fixes this. It's a single static binary that scrapes Google Flights directly. Search multiple destinations in one call. Pipe output into scripts, or let an AI agent search dozens of routes and compile results -- like the table above.

Built for people who book flights programmatically -- whether that's a bash loop, a Python script, or an LLM agent that can run shell commands.

Install

cargo install flyr-cli

Requires cmake, perl, and clang for the BoringSSL build (wreq dependency).

Arch: pacman -S cmake perl clang

Usage

flyr search -f HEL -t BKK -d 2026-03-01
flyr search -f LAX -t NRT -d 2026-05-01 --return-date 2026-05-15
flyr search -f HEL -t BKK -d 2026-03-01 --json --currency EUR
flyr search -f HEL -t DXB -d 2026-03-01 --open

Agent mode

flyr is designed for LLM agents. Three flags minimize token consumption:

  • --compact -- one line per flight, pipe-delimited, no box-drawing characters
  • --top N -- only return the N cheapest results
  • -t BCN,ATH,AYT -- multi-destination in a single invocation
flyr search -f HEL -t BCN,ATH,AYT -d 2026-03-01 --compact --top 3 --currency EUR

An agent can:

  • Search dozens of routes in one call
  • Filter by price, stops, departure time
  • Compare destinations and compile results
  • Open the best options directly in a browser

MCP (Model Context Protocol)

flyr includes a built-in MCP server (flyr mcp) that works with local LLMs via opencode + Ollama. Search flights, compare destinations, and open results in your browser — entirely offline, no API keys, no cloud LLMs. Any model with tool support works (Ministral, Llama, Qwen, etc.).

This is one way to use flyr with AI. Agents with shell access (Claude Code, Cursor, aider) don't need MCP — they can run flyr search directly, which exposes more options.

opencode config (opencode.jsonc):

{ "mcp": { "flyr": { "type": "local", "command": ["flyr", "mcp"] } } }

Multi-destination search

Comma-separate destination codes in -t:

flyr search -f HEL -t BKK,SIN,KUL,HKT,DPS -d 2026-03-01 --compact --top 3 --currency EUR

All destinations are searched concurrently. Output is grouped by destination.

Works with --return-date (each destination gets its own return leg), --top, all output modes.

Cannot be combined with --leg (use separate invocations for multi-city itineraries).

Concurrent searches (advanced)

For more complex scenarios beyond multi-destination, you can still run parallel shell processes:

for dest in BKK SIN KUL HKT DPS; do
  flyr search -f HEL -t $dest -d 2026-03-01 --return-date 2026-03-08 --json --currency EUR &
done | jq -s '[.[] | .flights[0] | {dest: .segments[0].to_airport.code, price, airlines}] | sort_by(.price)'

Localization

Results adapt to any language and currency Google Flights supports:

flyr search -f HEL -t BKK -d 2026-03-01 --currency EUR --lang fi
flyr search -f HEL -t BKK -d 2026-03-01 --currency JPY --lang ja
flyr search -f HEL -t BKK -d 2026-03-01 --currency THB --lang th
flyr search [OPTIONS]

REQUIRED (simple mode):
  -f, --from <IATA>           Departure airport (3-letter IATA code)
  -t, --to <IATA>             Arrival airport (comma-separate for multi-destination)
  -d, --date <YYYY-MM-DD>     Departure date

MULTI-CITY (replaces -f/-t/-d):
  --leg <"DATE FROM TO">      Flight leg, repeatable

TRIP:
  --return-date <YYYY-MM-DD>  Return date (auto-sets round-trip)
  --trip <TYPE>                one-way | round-trip | multi-city  [default: one-way]
  --seat <CLASS>               economy | premium-economy | business | first  [default: economy]

FILTERS:
  --max-stops <N>              0 = nonstop only
  --airlines <AA,DL,...>       Comma-separated IATA codes

PASSENGERS:
  --adults <N>                 [default: 1]
  --children <N>               [default: 0]
  --infants-in-seat <N>        [default: 0]
  --infants-on-lap <N>         [default: 0]

OUTPUT:
  --compact                    One-line-per-flight (recommended for scripts and AI agents)
  --top <N>                   Show only the N cheapest results
  --json                      JSON to stdout
  --pretty                    Pretty-printed JSON to stdout
  --open                      Open results in Google Flights
  --url                       Output Google Flights URL only (for AI agents)
  --currency <CODE>           [default: USD]
  --lang <CODE>              [default: en]

CONNECTION:
  --proxy <URL>                HTTP or SOCKS5 proxy
  --timeout <SECS>             [default: 30]

Output

Compact (recommended for agents)

$ flyr search -f HEL -t BCN -d 2026-03-01 --compact --top 3 --currency EUR
€199 | HEL>MUC>BCN | 4h45m | 1 stop MUC | Lufthansa | Mar01 12:45>17:30
€215 | HEL>ARN>BCN | 5h20m | 1 stop ARN | SAS | Mar01 08:00>13:20
€312 | HEL>BCN | 3h20m | nonstop | Finnair | Mar01 08:00>11:20

Table (default)

$ flyr search -f HEL -t BKK -d 2026-03-01 --currency EUR
┌───────────────────┬───────────┬──────────────────┬──────────────────┬────────┬────────┬────────────┬───────┐
│ Airlines          │ Route     │ Depart           │ Arrive           │ Dur.   │ Stops  │ Aircraft   │ Price │
├───────────────────┼───────────┼──────────────────┼──────────────────┼────────┼────────┼────────────┼───────┤
│ Finnair           │ HEL → BKK │ 2026-03-01 17:00 │ 2026-03-02 07:15 │ 10h 15 │ Nonstop│ Airbus A350│ €589  │
│ Turkish, THAI     │ HEL → IST │ 2026-03-01 19:00 │ 2026-03-02 05:20 │ 12h 40 │ 1 (IST)│ A321, A350 │ €498  │
│                   │ IST → BKK │                  │                  │        │        │            │       │
└───────────────────┴───────────┴──────────────────┴──────────────────┴────────┴────────┴────────────┴───────┘

JSON

flyr search -f HEL -t BKK -d 2026-03-01 --json --pretty
{
  "flights": [
    {
      "flight_type": "AY",
      "airlines": ["Finnair"],
      "segments": [
        {
          "from_airport": { "code": "HEL", "name": "Helsinki Airport" },
          "to_airport": { "code": "BKK", "name": "Suvarnabhumi Airport" },
          "departure": {
            "year": 2026,
            "month": 3,
            "day": 1,
            "hour": 17,
            "minute": 0
          },
          "arrival": {
            "year": 2026,
            "month": 3,
            "day": 2,
            "hour": 7,
            "minute": 15
          },
          "duration_minutes": 675,
          "aircraft": "Airbus A350"
        }
      ],
      "price": 589,
      "carbon": { "emission_grams": 570000, "typical_grams": 690000 }
    }
  ],
  "metadata": {
    "airlines": [{ "code": "AY", "name": "Finnair" }],
    "alliances": [{ "code": "ONEWORLD", "name": "Oneworld" }]
  }
}
flyr search -f HEL -t BKK -d 2026-03-01 --json | jq '.flights | sort_by(.price) | first'

flyr search -f JFK -t LHR -d 2026-04-01 --json | jq '[.flights[] | select(.segments | length == 1)]'

flyr search -f HEL -t BCN -d 2026-03-01 --json | jq '.flights[] | {airlines, price}'

Exit codes

Code Meaning
0 Success
2 Validation error (bad airport code, invalid date, etc.)
3 Network error (timeout, DNS, TLS, proxy)
4 Rate limited or blocked by Google
5 Unexpected HTTP status
6 Parse error (Google changed their page structure)

In --json mode, errors are structured JSON to stdout:

{
  "error": {
    "kind": "invalid_airport",
    "message": "invalid airport code \"XX\" -- must be exactly 3 letters"
  }
}

In human mode, errors go to stderr.

  1. Query encoding -- Flight parameters are protobuf-encoded (hand-rolled encoder, ~130 LOC) and base64-encoded into the tfs URL parameter, matching what Google Flights expects.

  2. HTTP request -- Uses wreq (reqwest fork) with Chrome 137 TLS fingerprint emulation to avoid bot detection. Pre-loads GDPR consent cookies to bypass the EU consent wall.

  3. HTML parsing -- Extracts the <script class="ds:1"> tag, isolates the data: JSON payload, parses with serde_json.

  4. Payload navigation -- The JSON payload is deeply nested arrays. Flight data lives at payload[3][0][i], with segments, prices, carbon data, and metadata at fixed indices. All access is safe (no panics on missing data).

The crate exposes a public API for use as a library:

use flyr::query::*;
use flyr::fetch::FetchOptions;

let params = QueryParams {
    legs: vec![FlightLeg {
        date: "2026-03-01".into(),
        from_airport: "HEL".into(),
        to_airport: "BKK".into(),
        max_stops: None,
        airlines: None,
    }],
    passengers: Passengers::default(),
    seat: Seat::Economy,
    trip: TripType::OneWay,
    language: "en".into(),
    currency: "USD".into(),
};

params.validate()?;

let result = flyr::search(
    SearchQuery::Structured(params),
    FetchOptions::default(),
).await?;

for flight in &result.flights {
    println!("{}: {} segments, ${}", flight.airlines.join(", "), flight.segments.len(), flight.price.unwrap_or(0));
}
src/
├── main.rs     CLI entry point (clap)
├── lib.rs      Public API: search(query, options) -> Result<SearchResult>
├── mcp.rs      Built-in MCP server (rmcp, stdio transport)
├── proto.rs    Hand-rolled protobuf encoder (~130 LOC)
├── query.rs    Query building, validation, URL param generation
├── fetch.rs    HTTP client with Chrome TLS impersonation + GDPR cookies
├── parse.rs    HTML script extraction + JSON payload navigation
├── model.rs    All data types (Serialize + Debug + Clone)
├── table.rs    Human-readable table rendering with currency symbols
└── error.rs    Error types with actionable messages
tests/
├── cli_test.rs     CLI tests -- arg parsing, help output, error messages, exit codes
├── parse_test.rs   13 tests -- script extraction, JSON parsing, edge cases
├── proto_test.rs    6 tests -- byte-level protobuf correctness
└── query_test.rs   23 tests -- validation rules, date handling, leap years, browser URLs

License

GPL-3.0