seer-cli 0.52.1

Interactive CLI for Seer domain name utilities
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
<div align="center">

# 🔮 Seer

**Domain intelligence at your fingertips.**

A high-performance, multi-interface domain utility suite — query WHOIS, RDAP, DNS, SSL, and more from the terminal, Python, REST API, or AI assistants.

[![CI](https://github.com/TheZacillac/seer/actions/workflows/ci.yml/badge.svg)](https://github.com/TheZacillac/seer/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/seer-cli.svg)](https://crates.io/crates/seer-cli)
[![Homebrew](https://img.shields.io/badge/dynamic/regex?url=https%3A%2F%2Fraw.githubusercontent.com%2FTheZacillac%2Fhomebrew-tap%2Fmain%2FFormula%2Fseer.rb&search=version%20%22(%5B%5E%22%5D%2B)%22&replace=%241&label=homebrew&logo=homebrew&logoColor=white&color=fbb040)](https://github.com/TheZacillac/homebrew-tap)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.89%2B-orange.svg)](https://www.rust-lang.org)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org)

[Features](#-features) · [Quick Start](#-quick-start) · [CLI Usage](#-cli-usage) · [Python](#-python-library) · [REST API](#-rest-api) · [MCP Server](#-mcp-server)

</div>

---

## ✨ Features

<table>
<tr>
<td width="50%">

**🔍 Lookups & Discovery**
- **Smart Lookup** — concurrent RDAP + WHOIS with fallback
- **WHOIS** — registrant, registrar, and expiration data
- **RDAP** — modern protocol for domains, IPs, and ASNs
- **Domain Info** — merged RDAP + WHOIS in a flat structure
- **Reverse DNS** — PTR lookups for IP addresses
- **TLD Info** — WHOIS server, RDAP endpoint, and registry
- **Subdomain Enumeration** — CT logs, with live/dead + takeover-risk classification and baseline diffing
- **Domain Availability** — check if a domain is registered

</td>
<td width="50%">

**🌐 DNS & Propagation**
- **DNS Resolution** — dig-style queries over 20 record types: status, CNAME chain, wildcard detection
- **DNS Trace** — follow the delegation down from the root servers (`+trace`)
- **Encrypted DNS** — DoT (`tls://`) and DoH (`https://`) transports
- **DNS Propagation** — 20 public resolvers across North America, Europe and Asia Pacific
- **DNS Monitoring** — track record changes over time
- **DNS Comparison** — compare records across two nameservers
- **DNSSEC Validation** — check DNSSEC configuration
- **NS Delegation Check** — parent delegation vs. zone NS, with lame-server probing

</td>
</tr>
<tr>
<td>

**🛡️ Security & Health**
- **Domain Status** — HTTP status, title, SSL, and expiration
- **SSL Chain Inspection** — full chain, SANs, key details, validity
- **CAA Policy** — who may issue certs, vs. actual issuer
- **Email Posture** — SPF, DMARC, MTA-STS, BIMI, and DANE checks
- **Look-alike Detection** — confusable/homoglyph domains
- **Registration Drift** — diff live registration vs. a baseline
- **Domain Watchlist** — monitor expiring certs and registrations, with optional webhook alerts
- **SSRF Protection** — blocks requests to private/reserved IPs

</td>
<td>

**⚡ Power Features**
- **Bulk Operations** — process domain lists with CSV export
- **Domain Diff** — side-by-side comparison of two domains
- **Field Extraction** — `--fields` (implies `--quiet`) for scriptable output
- **4 Output Formats** — human, JSON, YAML, markdown
- **Interactive REPL** — with tab completion and history
- **Environment Doctor** — one-shot config/DNS/WHOIS/RDAP diagnosis
- **Semantic Exit Codes** — for CI/CD scripting
- **Shell Completions** — bash, zsh, fish, PowerShell (plus man pages)

</td>
</tr>
</table>

### 🏗️ Four Interfaces, One Core

```
┌──────────────────────────────────────────────────────────────────┐
│                        User Interfaces                           │
├─────────────┬─────────────┬──────────────┬───────────────────────┤
│  seer-cli   │   seer-py   │   seer-api   │       seer-api        │
│  Terminal   │   Python    │   REST API   │     MCP Server        │
└──────┬──────┴──────┬──────┴──────┬───────┴──────────┬────────────┘
       │             │             │                  │
       │             └─────────────┼──────────────────┘
       │                           │
       ▼                           ▼
┌──────────────────────────────────────────────────────────────────┐
│                          seer-core                               │
│                      Core Rust Library                           │
├──────────┬────────┬──────┬────────┬──────┬──────┬────────────────┤
│  WHOIS   │  RDAP  │ DNS  │ Status │ SSL  │ Bulk │   Diff/Watch   │
└──────────┴────────┴──────┴────────┴──────┴──────┴────────────────┘
```

---

## 🚀 Quick Start

### Install the CLI

**Homebrew** (macOS/Linux — prebuilt binary, no Rust toolchain required):

```bash
brew install TheZacillac/tap/seer
```

**Cargo** (builds from source):

```bash
cargo install seer-cli
```

Prebuilt binaries and shell/PowerShell installers for each release are also on
the [Releases page](https://github.com/TheZacillac/seer/releases).

### Run your first lookup

```bash
seer lookup example.com        # Smart RDAP + WHOIS lookup
seer dig example.com MX        # DNS query
seer status example.com        # HTTP, SSL, and expiration check
seer ssl example.com           # Full SSL chain inspection
seer                           # Launch interactive REPL
seer tui example.com           # Launch the full-screen TUI
```

Using Seer from Rust code instead? See [Rust Library](#-rust-library).

> **Requirements:** Rust 1.89+ · Python 3.10+ (for Python bindings/API)

---

## 📦 Packages

| Package | Type | Install | Description |
|---------|------|---------|-------------|
| **seer-core** | Rust library | `cargo add seer-core` | Core library — all business logic |
| **seer-cli** | Rust binary | `brew install TheZacillac/tap/seer` or `cargo install seer-cli` | The `seer` command-line tool |
| **seer-py** | Python extension | `pip install ./seer-py` | Python bindings via PyO3 (`import seer`); PyPI (`domain-seer`) planned |
| **seer-api** | Python package | `pip install -e .` | REST API + MCP server |

<details>
<summary><b>seer-cli vs seer-core — which do I need?</b></summary>

| | seer-cli | seer-core |
|---|----------|-----------|
| **What** | Executable binary | Rust library crate |
| **Install** | `brew install TheZacillac/tap/seer` or `cargo install seer-cli` | `cargo add seer-core` |
| **Use** | Run `seer` in your terminal | `use seer_core::*` in Rust code |
| **Provides** | Commands, REPL, formatted output | Structs, clients, async APIs |
| **Depends on** | seer-core internally | Nothing — it's the foundation |

</details>

---

## 💻 CLI Usage

### Interactive TUI

Launch the full-screen, keyboard-first terminal UI (Catppuccin Frappé, with a Latte light theme):

```bash
seer tui                  # start on the target prompt
seer tui example.com      # look up a domain on launch
```

The **lens** sidebar covers every Seer capability, grouped, each pulling live data from the same `seer-core` engine as the CLI with full in-pane controls:

- **LOOKUP** — Overview · WHOIS · RDAP (Domain · IP · ASN) · Reverse DNS · Availability · TLD Info
- **DNS** — Records (+ custom nameserver) · DNSSEC · Compare (two resolvers) · Trace (from the root servers) · Propagation · Follow (live monitor)
- **SECURITY** — SSL / Cert · Status · Subdomains · HTTP Headers · Takeover
- **POWER** — Diff · Bulk (streaming + CSV export) · Watchlist · History

**Keys:** `j`/`k` move · `1`–`9` jump to a lens · `Tab` focus nav⇄pane · `[` `]` sub-tabs · `r` raw output (json/yaml/markdown; `PgUp`/`PgDn` scroll) · `y` copy · `/` look up a domain · `:` command · `?` help · `:q` quit. In-pane: switchers (TLD, nameserver, Compare resolvers, Bulk op) and editable fields (Diff's 2nd domain, Follow interval/count, Bulk file path).

Subdomains and Takeover query CT logs and probe hosts, so they run only when you press `↵` on them (or use their `:` command), never just by moving onto them. Removing a watched domain (`d`) and clearing history (`c`) ask for a second press.

**Commands (`:`):** `lookup`, `whois`, `rdap <domain|ip|AS####>`, `dig [@server] <name> [type] [+trace]`, `ssl`, `status`, `headers`, `takeover`, `reverse <ip>`, `tld <.tld>`, `compare <domain> <nsA> <nsB>`, `diff <a> <b>`, `set output <human|json|yaml|markdown>`, `theme <frappe|latte>`, `copy`, `q`.

The startup theme comes from `[tui] theme` in `~/.seer/config.toml` (`"frappe"` — the default — or `"latte"`; unknown names fall back to Frappé). Switch live with `:theme latte`.

### Command Mode

```bash
# Smart lookup (concurrent RDAP + WHOIS)
seer lookup example.com

# Comprehensive domain info (merged RDAP + WHOIS)
seer info example.com

# WHOIS / RDAP
seer whois example.com
seer rdap example.com           # Domain
seer rdap 8.8.8.8               # IP address
seer rdap AS15169               # ASN

# DNS queries (dig-style: arguments in any order)
seer dig example.com             # A records (default)
seer dig example.com MX          # Specific record type
seer dig example.com A AAAA MX   # Several types, queried concurrently
seer dig example.com MX +short   # Values only, one per line (or --short)
seer dig example.com ANY         # Common types, fanned out ('*' works too)
seer dig -x 8.8.8.8              # Reverse lookup (PTR)
seer dig www.example.com +trace  # Delegation walk from the root servers (or --trace)
seer dig @8.8.8.8 example.com    # Custom nameserver (UDP), same as -s 8.8.8.8
seer dig @tls://1.1.1.1 example.com                           # DNS over TLS
seer dig example.com -s https://cloudflare-dns.com/dns-query  # DNS over HTTPS
seer dig '*.example.com'         # Wildcard record (quote it so the shell doesn't glob)

# DNS propagation & monitoring
seer prop example.com A
seer follow example.com 20 0.5       # 20 checks, 30s interval
seer follow example.com MX 10 1 --changes-only   # arguments in any order
seer follow example.com AAAA @1.1.1.1

# DNSSEC & DNS comparison
seer dnssec example.com
seer compare example.com MX @8.8.8.8 @1.1.1.1  # any order; type defaults to A
seer delegation example.com      # Parent delegation vs. zone NS + lame-server probe

# Domain health & SSL
seer status example.com
seer ssl example.com
seer caa example.com              # CAA policy vs. actual cert issuer
seer posture example.com          # SPF / DMARC / MTA-STS email posture
seer headers example.com          # HTTP security headers + cookie flags, graded A+-F

# Security intel
seer confusables example.com      # Look-alike / homoglyph domains
seer drift example.com --record   # Diff registration vs. baseline, then update it
seer takeover example.com         # Subdomain takeover scan (exit 1 on findings)
seer takeover example.com --host app.example.com   # Check known hosts, skip CT lookup

# Reverse DNS
seer reverse 8.8.8.8

# Discovery
seer avail example.com
seer subdomains example.com
seer subdomains example.com --resolve    # Resolve each name: live/dead + takeover risk
seer subdomains example.com --record     # Store a CT-log baseline
seer subdomains example.com --diff       # Exit 1 when new names appear (cron/CI)
seer tld .com

# Domain diff
seer diff example.com google.com

# Watchlist
seer watch add example.com www.example.com   # several at once; www. is kept
seer watch list
seer --format json watch list
seer watch                        # Check all watched domains
seer watch --fail-on warning      # Exit non-zero at warning severity (default: critical)
seer watch --webhook https://hooks.example.com/seer   # Also POST the report as JSON
seer watch remove example.com

# Lookup history
seer history example.com
seer history --clear

# Bulk operations (with CSV export) — ops: lookup, whois, rdap, dig, prop,
# status, avail, info, ssl, caa, posture, confusables
seer bulk lookup domains.txt
seer bulk status domains.txt -o results.csv
seer bulk dig domains.txt MX
seer bulk ssl domains.txt
seer bulk posture domains.txt
cat domains.txt | seer bulk avail -      # Read the list from stdin

# Environment diagnosis (config, DNS, WHOIS port 43, RDAP bootstrap HTTPS)
seer doctor

# Scriptable field extraction
seer --fields registrar lookup example.com        # --fields implies --quiet
seer --fields certificate.issuer status example.com
seer --fields status,answers.name dig www.example.com

# Shell completions & man pages
seer completions bash >> ~/.bashrc
seer completions zsh >> ~/.zshrc
seer mangen ./man                 # Write seer.1 + one page per subcommand
```

`seer watch --webhook` (or `webhook_url` under `[watch]` in `~/.seer/config.toml` — the flag wins) POSTs the check-all report as JSON. Delivery is best-effort: a failed POST prints a stderr warning and never changes the exit code, and the URL passes the same SSRF guard as every other outbound request.

### DNS Queries

`seer dig` reports a query the way dig does: the response status, the header
flags, and the answer with any CNAME chain first, every record under its real
owner name. Like dig, it sends one query per record type straight to the
nameserver (the default upstream, Google Public DNS, unless `@server`/`-s`
names one) and shows the response exactly as the server sent it, header
flags included; the servers are asked in order, IPv4 first, over UDP and
again over TCP when the reply is truncated, and one that does not respond
passes the query to the next, within two DNS timeouts.

```
$ seer dig www.github.com

DNS A Records: www.github.com
─────────────────────────────
  status: NOERROR  flags: qr rd ra  server: default  time: 23 ms

  Answer:
    www.github.com  2944  CNAME  github.com.
    github.com        60  A      140.82.114.3

Note: DNS responses are not DNSSEC-validated
```

- **NXDOMAIN vs NODATA.** "Name does not exist (NXDOMAIN)" and "No AAAA
  records (NODATA — the name exists)" are told apart, each shown with the
  zone's SOA when the server sends it. Behind a CNAME chain the chain is
  shown and the verdict is about its last target — "The CNAME target
  gone.example.net. does not exist (NXDOMAIN)" for a dangling CNAME — or,
  from a server that does not recurse, says it returned the CNAME without
  following it. SERVFAIL, REFUSED and other response codes (EDNS extended
  ones such as BADVERS included) are reported as the status, not as errors.
  A referral — an `@server` that serves only a parent zone pointing at the
  child zone's nameservers, with the `aa` flag clear — reads "No answer:
  referral to <zone>", which says nothing about whether the name exists.
- **Special-use names.** `localhost`, `127.in-addr.arpa` (`-x 127.0.0.1`),
  `invalid`, `onion` and the other RFC 6761 names are answered by seer's
  resolver itself and never sent to a server; the result says so (`server:
  none`, no flags and a note; `answered_locally: true` in JSON).
- **Wildcards.** For a name below its registrable domain (`www.example.com`,
  not `example.com`), a random sibling (`seer-probe-….example.com`) is queried
  alongside (never for a special-use name). When it resolves too, a note says
  a wildcard answers there and whether this answer matches it (likely
  wildcard-synthesized) or differs.
- **Record types.** `HTTPS` and `SVCB` (priority, target and parameters such
  as `alpn`, `ipv4hint` and `ech`) and the DNSSEC key-rollover records `CDS`
  and `CDNSKEY` are supported — see [DNS Record Types]#-dns-record-types.
  `ANY` queries A, AAAA, CNAME, MX, NS, TXT, SOA, CAA, HTTPS, DS and DNSKEY
  concurrently and merges the answers, since servers may answer a real `ANY`
  query minimally (RFC 8482).
- **`+short` / `--short`** prints only the values, one per line (CNAME targets
  first), and nothing when there is no answer. It ignores `--format` and
  can't be combined with `-q`/`--fields`. A `+trace +short` that stopped
  early prints why on stderr and exits `1`.
- **`+trace` / `--trace`** asks one server of each zone directly, from the
  root servers down, and shows one hop per delegation level: the server and
  address, the status, the referral or the answer, and any servers that
  failed. A server that fails or gives a lame (empty, non-authoritative)
  reply is passed over for the zone's next one, and each server is asked
  over IPv4 first, then over IPv6 when this host has no IPv4 route. The
  whole walk gets six DNS timeouts (30s by default) and otherwise stops with
  the hops so far and an error. It takes one record type and no nameserver,
  and reports a CNAME answer without following it (an NXDOMAIN beside a
  CNAME, which the zone's server followed itself, names the target that
  does not exist). Every server address is checked against private and
  reserved ranges before it is queried.
- **Scripting.** `--format json` / `yaml` prints the result object (`name`,
  `record_type`, `server`, `answered_locally`, `status`, `flags`, `answers`,
  `authority`, `wildcard`, `query_time_ms`), or an array of them for several
  types, and `--quiet --fields` picks values from it. When some of several
  types fail, the rest still print, each failure is reported on stderr, and
  the command exits `1`.

The REPL's `dig` takes the same arguments. `seer reverse <ip>` is unchanged:
it returns just the PTR records (a JSON list), where `seer dig -x <ip>`
returns the full result.

### Output Formats

```bash
seer --format human lookup example.com      # Colored, human-readable (default)
seer --format json lookup example.com       # JSON
seer --format yaml lookup example.com       # YAML
seer --format markdown lookup example.com   # Markdown table
```

### Exit Codes

Every command follows the same contract, so `$?` is safe to gate cron jobs and CI on:

| Code | Meaning |
|------|---------|
| `0` | Success (and, for check commands, the check passed) |
| `1` | Operational error (network, parse, invalid input) — or a check command's negative result |
| `2` | Usage error (bad flags or arguments) |

Check commands exit `1` on a negative result even when the command itself ran fine:

| Command | Exits `1` when… |
|---------|-----------------|
| `seer status` | HTTP status is missing or non-2xx, the SSL cert is invalid or expires within 30 days, or the domain expires within 30 days |
| `seer avail` | The domain is **not** available (already registered) |
| `seer dnssec` | The zone's status is anything other than `signed` (i.e. `unsigned`, `partial`, or `misconfigured`) |
| `seer compare` | The two nameservers give different answers (response code, CNAME chain or record set) |
| `seer delegation` | The parent's delegation NS set and the zone's own NS RRset are out of sync, or any delegated server answers lamely |
| `seer takeover` | Any host is `vulnerable` or `potential` (a host that could not be checked is `inconclusive` and does not fail the scan) |
| `seer doctor` | Any check reports **FAIL**. `WARN` (degraded but usable, e.g. a malformed config file running on defaults) still exits `0` |
| `seer drift` | Material drift is found vs. the stored baseline (a first run with no baseline exits `0`) |
| `seer subdomains --diff` | New names appeared vs. the stored baseline (removals and a missing baseline exit `0`) |
| `seer watch` | Issues at or above the `--fail-on` threshold: `critical` (default) fails only on critical issues, `warning` fails on warnings too. `add`/`remove`/`list` exit `0` on success |
| `seer bulk` | **Every** domain in the batch failed — partial failures exit `0`, since per-row status is already in the CSV/JSON output |

`seer dig` is not a check command: as with dig, any answer the server gave
exits `0` — NXDOMAIN, NODATA, SERVFAIL and REFUSED included, and a `+trace`
that stopped early reports where. It exits `1` on invalid arguments, a
timeout or other failure to get an answer, when any of several record
types failed, or when a `+trace +short` stopped early (its values alone
could not say so).

All other commands exit `0` on success and `1` on error. With `--format json`, `yaml`, or `markdown`, errors are written to stderr in that format (e.g. `{"error": "..."}`) instead of colored prose.

### Interactive REPL

Launch by running `seer` with no arguments:

```
$ seer
seer> lookup example.com
seer> dig github.com MX
seer> dig @1.1.1.1 www.github.com A AAAA +short
seer> status cloudflare.com
seer> delegation example.com
seer> doctor
seer> set output json
seer> help
seer> exit
```

Features: command history (`~/.seer_history`), tab completion, loading spinners, persistent session state.

---

## 🐍 Python Library

```python
import seer

# Smart lookup
result = seer.lookup("example.com")

# WHOIS / RDAP
whois = seer.whois("example.com")
rdap  = seer.rdap("8.8.8.8")                     # Auto-routes IP / ASN / domain (alias: rdap_auto)
rdap  = seer.rdap_domain("example.com")
rdap  = seer.rdap_ip("8.8.8.8")
rdap  = seer.rdap_asn(15169)

# DNS
answer      = seer.dig("example.com", record_type="MX", nameserver="tls://1.1.1.1")
records     = answer["answers"]                  # Also: status, flags, authority, wildcard
trace       = seer.dns_trace("www.example.com")  # Hops from the root servers down (dig +trace)
propagation = seer.propagation("example.com", record_type="A")
delegation  = seer.delegation("example.com")     # Parent vs. zone NS + lameness
follow      = seer.dns_follow("example.com", "A", iterations=3, interval_minutes=1.0)
seer.cancel_follow()                             # Stop a running dns_follow from another thread
types       = seer.record_types()                # The 20 supported record types

# Domain health & SSL
status = seer.status("example.com")
ssl    = seer.ssl("example.com")
dnssec = seer.dnssec("example.com")
caa    = seer.caa("example.com")

# Security intel
posture     = seer.posture("example.com")        # SPF / DMARC / MTA-STS
headers     = seer.headers("example.com")        # HTTP security headers, graded A+-F
takeover    = seer.takeover("example.com")       # Subdomain takeover scan
confusables = seer.confusables("example.com")    # Look-alike domains

# Availability & info
available = seer.availability("example.com")
info      = seer.info("example.com")
tld       = seer.tld_info(".com")                # WHOIS server, RDAP endpoint, registry
tlds      = seer.all_tlds()                      # Every TLD seer knows about

# Comparison & enumeration
diff       = seer.diff("example.com", "google.com")
comparison = seer.dns_compare("example.com", "A", "8.8.8.8", "1.1.1.1")
subdomains = seer.subdomains("example.com")
classified = seer.subdomains_classify("example.com")  # live/dead + takeover risk

# Bulk operations
results = seer.bulk_lookup(["example.com", "google.com"], concurrency=10)
results = seer.bulk_status(["example.com", "google.com"])
results = seer.bulk_dig(["example.com", "google.com"], record_type="A")
results = seer.bulk_info(["example.com", "google.com"])
results = seer.bulk_ssl(["example.com", "google.com"])
results = seer.bulk_availability(["example.com", "google.com"])
results = seer.bulk_whois(["example.com", "google.com"])
results = seer.bulk_propagation(["example.com"], record_type="A", concurrency=5)

# bulk_* calls return one dict per domain:
#   {"operation": {"type": ..., "domain": ...}, "success": bool,
#    "data": ... or None, "error": str or None, "duration_ms": int}
# Every bulk_* call takes an optional keyword-only progress callback
results = seer.bulk_lookup(domains, progress=lambda done, total, domain: print(f"{done}/{total} {domain}"))

# SSRF helpers (used by seer-api before any user-supplied connect target)
seer.validate_public_host("example.com", 443)   # ValueError on reserved/private addresses
seer.nameserver_target("tls://1.1.1.1")          # ("1.1.1.1", 853); None if the spec is invalid

# Errors: ValueError (invalid input), TimeoutError, ConnectionError (WHOIS
# connect), or a seer.SeerError subclass (a RuntimeError): RateLimitedError,
# WhoisServerNotFoundError, DnsError, UpstreamError, LookupFailedError,
# ParseError, TlsError, ConfigError
try:
    seer.whois("example.zz")
except seer.WhoisServerNotFoundError:
    pass
```

<details>
<summary><b>Example: Check SSL Certificate</b></summary>

```python
status = seer.status("example.com")
if cert := status.get("certificate"):
    print(f"SSL Valid: {cert['is_valid']}")
    print(f"Expires:   {cert['valid_until']}")
    print(f"Days left: {cert['days_until_expiry']}")
```

</details>

---

## 🦀 Rust Library

```toml
[dependencies]
seer-core = "0.52"
tokio = { version = "1", features = ["full"] }
```

```rust
use seer_core::{SmartLookup, DnsResolver, RecordType, StatusClient};

#[tokio::main]
async fn main() -> seer_core::Result<()> {
    // Smart lookup (RDAP → WHOIS fallback)
    let lookup = SmartLookup::new();
    let result = lookup.lookup("example.com").await?;

    // DNS resolution
    let resolver = DnsResolver::new();
    let records = resolver.resolve("example.com", RecordType::MX, None).await?;
    for record in records {
        println!("{}: {}", record.record_type, record.data);
    }

    // The whole response, as dig reports it: status, flags, CNAME chain, ...
    let answer = resolver.query("www.example.com", RecordType::A, None).await?;
    println!("{} with {} answers", answer.status, answer.answers.len());

    // Domain status check
    let client = StatusClient::new();
    let status = client.check("example.com").await?;
    println!("HTTP: {:?}", status.http_status);

    Ok(())
}
```

See [docs.rs/seer-core](https://docs.rs/seer-core) for the full API reference.

---

## 🌍 REST API

```bash
seer-api   # Starts on http://127.0.0.1:8000 (loopback-only by default)
```

Each lookup is a `GET` endpoint (`/lookup/{domain}`, `/dns/{domain}/{record_type}`,
`/dns/trace/{domain}`, `/ssl/{domain}`, `/takeover/{domain}`, …). `lookup`,
`whois`, `dns`, `propagation`, `status`, `ssl`, `availability`, and `info`
also accept a `POST …/bulk` domain list, and all but `availability` and
`info` can stream bulk results as Server-Sent Events from `…/bulk/stream`.
The full endpoint table is in
[seer-api/README.md](seer-api/README.md#endpoints).

**Secure by default:** the server binds loopback only, refuses a non-loopback
`SEER_HOST` without `SEER_API_KEY`, serves `/docs` only when
`SEER_DOCS_ENABLED=true`, and refuses multi-worker runs on the in-memory
rate-limit store. See
[Deployment defaults](seer-api/README.md#deployment-defaults).

```bash
# Examples
curl http://localhost:8000/lookup/example.com
curl http://localhost:8000/dns/example.com/MX
curl "http://localhost:8000/dns/trace/www.example.com?record_type=AAAA"
curl -X POST http://localhost:8000/lookup/bulk \
  -H "Content-Type: application/json" \
  -d '{"domains": ["example.com", "google.com"]}'
```

API docs (when `SEER_DOCS_ENABLED=true`):
[Swagger UI](http://localhost:8000/docs) · [ReDoc](http://localhost:8000/redoc)

---

## 🤖 MCP Server

Integrate Seer with AI assistants via the [Model Context Protocol](https://modelcontextprotocol.io/). Two transports expose the same tools:

```bash
# Local subprocess transport (Claude Desktop, etc.)
seer-mcp   # stdio

# Remote / web transport (mounted on the REST API)
seer-api   # exposes POST /mcp using MCP Streamable HTTP
```

The Streamable HTTP endpoint runs stateless inside the existing `seer-api`
process and inherits `SEER_API_KEY` auth, request logging, and the body-size
cap. See [seer-api/README.md](seer-api/README.md#streamable-http-transport)
for the wire format.

Generate a random bearer token for the connector:

```bash
# Capture into a shell var
KEY=$(seer generate-key)

# Or eval directly into the current shell
eval "$(seer generate-key --export)"

SEER_API_KEY=$KEY SEER_HOST=0.0.0.0 seer-api
```

**32 tools:** one per lookup (`seer_lookup`, `seer_dig`, `seer_dns_trace`,
`seer_ssl`, `seer_takeover`, …) plus bulk variants (`seer_bulk_status`, …).
The full tool list and the Claude Desktop configuration are in
[seer-api/README.md](seer-api/README.md#available-tools).

---

## 📡 DNS Record Types

| Type | Description | | Type | Description |
|------|-------------|---|------|-------------|
| `A` | IPv4 address | | `PTR` | Pointer record |
| `AAAA` | IPv6 address | | `SRV` | Service locator |
| `MX` | Mail exchange | | `NAPTR` | Naming authority pointer |
| `TXT` | Text records | | `DNSKEY` | DNSSEC public key |
| `NS` | Nameserver | | `DS` | Delegation signer |
| `SOA` | Start of authority | | `CDS` | Child DS (DNSSEC key rollover) |
| `CNAME` | Canonical name | | `CDNSKEY` | Child DNSKEY (DNSSEC key rollover) |
| `CAA` | CA authorization | | `SSHFP` | SSH key fingerprint |
| `HTTPS` | HTTPS service binding (ALPN, address hints, ECH) | | `TLSA` | DANE TLS association |
| `SVCB` | General service binding | | `ANY` | A, AAAA, CNAME, MX, NS, TXT, SOA, CAA, HTTPS, DS and DNSKEY, queried concurrently |

---

## 🌏 Global DNS Propagation Servers

Propagation checks query **30 nameservers** across **6 regions**:

| Region | Servers |
|--------|---------|
| 🇺🇸 **North America** | Google `8.8.8.8` · Cloudflare `1.1.1.1` · OpenDNS · Quad9 · Level3 |
| 🇪🇺 **Europe** | DNS.Watch · Mullvad · dns0.eu · Yandex · UncensoredDNS |
| 🌏 **Asia Pacific** | AliDNS · 114DNS · Tencent DNSPod · TWNIC · HiNet |
| 🌎 **Latin America** | Claro Brasil · Telefonica Brasil · Antel Uruguay · Telmex · CenturyLink |
| 🌍 **Africa** | Liquid Telecom · SEACOM · Safaricom · MTN South Africa · Telecom Egypt |
| 🌐 **Middle East** | Etisalat UAE · STC Saudi · Bezeq Israel · Turk Telekom · Ooredoo Qatar |

---

## ⚙️ Configuration

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `RUST_LOG` | Logging level (`trace` / `debug` / `info` / `warn` / `error`) | — |
| `SEER_LOG_LEVEL` | API log level; `ARCANUM_LOG_LEVEL` takes precedence when set | `INFO` |
| `SEER_DOMAIN_ALLOWLIST` | Comma-separated allowlist restricting which domains may be queried | — |
| `SEER_HOST` | API bind host. Non-loopback requires `SEER_API_KEY` (without it, requests arriving on a non-loopback interface get a 503 however the server was started) | `127.0.0.1` |
| `SEER_PORT` | API bind port | `8000` |
| `SEER_API_KEY` | Bearer token required for all non-`/health` requests | — |
| `SEER_CORS_ORIGINS` | Comma-separated CORS origins for the REST API. Unset: no CORS headers, and without `SEER_API_KEY` a request from a non-loopback `Origin` is refused (403) | — |
| `SEER_DOCS_ENABLED` | Expose `/docs`, `/redoc`, `/openapi.json` | `false` |
| `SEER_METRICS_ENABLED` | Expose `/metrics` to non-loopback clients | `false` |
| `SEER_RATE_LIMIT` | Per-client limit for the MCP endpoint (`POST /mcp`) as `<count>/<period>`; `;`-separated limits are all enforced. REST routes keep their own fixed limits | `30/minute` |
| `SEER_RATE_LIMIT_STORAGE` | Rate-limit storage URI (e.g. `redis://host:6379`) | `memory://` |
| `SEER_REQUEST_TIMEOUT` | Per-request deadline (seconds) covering the SSRF pre-check, the core call and bulk SSE streams (slot wait included); on expiry the client gets a 504 (a stream: an `error` event). `0` disables | `0` |
| `SEER_DISPATCH_THREADS` | Max threads in the pool running blocking core calls (REST + `/mcp`) | `50` |
| `SEER_MAX_CONCURRENT_STREAMS` | Max in-flight bulk SSE stream jobs per worker process | `8` |
| `SEER_TRUST_PROXY` | Trust `X-Forwarded-For` from `SEER_TRUSTED_PROXY_IPS` | `false` |
| `SEER_TRUSTED_PROXY_IPS` | Comma-separated IPs allowed to set `X-Forwarded-For` | — |
| `SEER_MCP_ALLOWED_HOSTS` | Comma-separated `Host:` values for the MCP `/mcp` endpoint — setting this turns on DNS-rebinding protection | — |
| `SEER_MCP_ALLOWED_ORIGINS` | Comma-separated `Origin:` values for the MCP `/mcp` endpoint (browser hosts) | — |
| `WEB_CONCURRENCY` | Uvicorn worker count. `>1` requires a non-`memory://` store | `1` |
| `UVICORN_WORKERS` | Fallback worker count when `WEB_CONCURRENCY` is unset (same `>1` store rule) | `1` |
| `SEER_RELOAD` | Uvicorn auto-reload for the `seer-api` entry point (dev only) | `false` |

### Config File

Initialize a config file at `~/.seer/config.toml`:

```bash
seer config --init
```

Settings: default output format, nameserver (UDP, `tls://`, or `https://`),
per-protocol timeouts, bulk concurrency, rate-limit delay — all clamped to
safe ranges — plus a `[watch]` table (`webhook_url` for `seer watch` report
delivery) and a `[tui]` table (`theme = "frappe"` or `"latte"`). Applies to
the CLI and REPL; the TUI reads the output format and theme.

### Timeouts

| Client | Default |
|--------|---------|
| WHOIS | 15s |
| RDAP | 15s |
| DNS | 5s (2 retries) |
| HTTP / SSL | 10s |
| CT logs (subdomains) | 30s per request (`ct_secs`); each source at most 3 of them |
| Propagation | 15s |

### Bulk Operations

- **Input:** plain text (one domain per line) or CSV (first column)
- **Comments:** lines starting with `#` are skipped
- **Concurrency:** default 10, max 50
- **API limit:** max 100 domains per request

---

## 🔮 Roadmap

- **Scheduled Monitoring Daemon** — `seer monitor --config monitors.toml` as a background service with notifications (email, Slack, PagerDuty) for expiring domains, SSL certificates, and unexpected DNS changes. The natural evolution of `watch`, `follow`, and `status` into persistent monitoring.

---

## 🛠️ Development

### Building

```bash
cargo build --release                        # All Rust packages
cd seer-py && maturin develop --release      # Python bindings
cd seer-api && pip install -e .              # REST API + MCP server
```

### Testing

```bash
cargo test                    # All Rust tests
cargo test -p seer-core       # Core library only
cd seer-api && pytest         # Python API tests
RUST_LOG=debug cargo test     # With debug logging
```

### Linting

```bash
cargo fmt --all -- --check                             # Format check
cargo clippy --workspace --all-targets -- -D warnings  # Lint
cargo deny check                                       # Supply-chain policy (advisories, licenses, sources)
```

### Project Structure

The four packages are listed under [Packages](#-packages); all business
logic lives in `seer-core`, and the other three are thin interfaces over it.
The annotated per-module map is in
[CLAUDE.md → Codebase Structure](CLAUDE.md#codebase-structure).

---

## 🔧 Technology Stack

| Layer | Technologies |
|-------|-------------|
| **Core** | Rust · Tokio · Reqwest (rustls) · Hickory-resolver (DNSSEC) · Serde · OpenTelemetry |
| **CLI** | Clap v4 · Rustyline · Indicatif · Colored · Crossterm |
| **Python** | PyO3 (ABI3) · FastAPI · Pydantic · MCP |
| **Data** | WHOIS servers from [WooMai/whois-servers]https://github.com/WooMai/whois-servers · IANA RDAP bootstrap |

---

## 📄 License

MIT License — Copyright (c) 2026 Zac Roach

See [LICENSE](LICENSE) for the full text.