# nbox

Terminal UI and CLI for NetBox — fast search, IPAM lookups, and device context.

<p align="center">
  <img src="nbox-ss.png" alt="nbox TUI — list/preview split with a live detail pane" width="900">
</p>

[![Crates.io](https://img.shields.io/crates/v/nbox.svg)](https://crates.io/crates/nbox)
[![CI](https://github.com/lance0/nbox/actions/workflows/ci.yml/badge.svg)](https://github.com/lance0/nbox/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT)
[![Ko-fi](https://img.shields.io/badge/Ko--fi-tip-ff5e5b?logo=ko-fi)](https://ko-fi.com/lance0)

Ask the questions you actually ask at the terminal — *what is this IP, where is
this device, what owns this prefix?* — from the shell, a k9s-style TUI, or an MCP
server for AI agents.

**Status: pre-1.0, read-only.** Safe, diff-confirmed `PATCH` writes come later
([ROADMAP.md](ROADMAP.md)).

## Quick Start

First run? Just launch the TUI — with no config it opens a first-run wizard that
captures a profile (url + token), test-connects, and drops you into the app:

```bash
nbox                              # first run: guided onboarding, then the TUI
```

Prefer the shell? Configure a profile by hand:

```bash
nbox profile add work https://netbox.example.com --token-env NETBOX_TOKEN
nbox profile use work
export NETBOX_TOKEN=...           # or store the token: nbox config token set

# Look things up from the shell
nbox search edge01
nbox device edge01
nbox ip 10.44.208.55
nbox prefix 10.44.208.0/24
```

See [Installation](#installation) below for setup instructions.

## Features

- **Fast shell lookups** — `device`, `ip`, `prefix`, `vlan`, `site`, `rack`, `circuit`, `provider`, `aggregate`, `asn`, `ip-range`, `tenant`, `contact`, `vm`, `cluster`, and `interface`, each as a one-liner.
- **Agent-ready** — `nbox serve` is a read-only MCP server: the same lookups exposed as eight tools (plus the objects as `nbox://{kind}/{ref}` resources), returning the exact JSON view models the CLI does, so AI agents (Claude Code, Claude Desktop, …) query NetBox safely. Stdio for a local subprocess, or a loopback HTTP transport — with OIDC resource-server auth for a network-reachable, read-only deployment. See [docs/MCP.md](docs/MCP.md).
- **Normalized search** — one `search` query runs in parallel across devices, sites, IPs, prefixes, VLANs, circuits, providers, aggregates, ASNs, IP ranges, tenants, contacts, VMs, and clusters and returns ranked, deduped hits.
- **IPAM-aware** — IP → most-specific parent prefix → VLAN → scope resolution, prefix utilization and children, `next-ip` / `next-prefix` for available addresses and free blocks (computed locally with `ipnet`).
- **Polymorphic scope** — `--site`/`--region`/`--site-group`/`--location` on `search` resolve the reference once and filter by NetBox 4.2's `scope_type` + `scope_id` (exact scope, one flag at a time); `prefix`, `vlan`, and `ip` views expose `scope`/`scope_type` (site, location, region, site-group, …) — `prefix` and `vlan` carry their own scope, `ip` inherits it from its parent prefix.
- **Interactive TUI** — list/preview split, scrolling, command palette, recents, twelve themes (including a light theme), `NO_COLOR` honored.
- **Scriptable** — `-o plain|json|csv`, `--fields`, `--raw`, versioned `--envelope`, and stable exit codes; stdout stays clean for piping, logs go to stderr (see [AGENTS.md](AGENTS.md)).
- **Open and copy** — open any object in the browser or copy a field straight from results.
- **Profiles** for multiple NetBox instances, **journals** folded into detail lookups, and **tags** listing.

See [docs/FEATURES.md](docs/FEATURES.md) for the full command reference and
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the internals.

## Real-World Use Cases

### What is this IP?

```bash
nbox ip 10.44.208.55
```

Resolves the address, its most-specific parent prefix (VRF-scoped), the prefix's
VLAN, and its scope (site, location, region, …). Add `--vrf <name>` when the same
address exists in several VRFs.

### Where is this device, and what's on it?

```bash
nbox device edge01
```

Device plus its interfaces, IP addresses, cables, VLANs, and services in one
lookup. In the TUI, the device screen splits these onto `i`/`p`/`c`/`v`/`s` tabs.

### Find the next free address or block

```bash
nbox next-ip 10.44.208.0/24 --count 4      # next available addresses
nbox next-prefix 10.0.0.0/8 --length 26    # first free /26
```

Read-only — nothing is reserved. Both take `--vrf` to scope the prefix.

### Pull data into a script

```bash
nbox device edge01 --json | jq '.primary_ip4.address'
nbox search edge01 -o csv --cols name,site,status > devices.csv
nbox prefix 10.44.208.0/24 --json --envelope --raw   # versioned, single-line
```

### Drive it from an agent

```bash
claude mcp add nbox -e NBOX_TOKEN=nbt_xxx.yyy -- nbox serve
```

`nbox serve` is a read-only MCP server; an MCP host launches it as a subprocess
and gets the same JSON view models the CLI returns. See [docs/MCP.md](docs/MCP.md).

## Installation

> The first published release was **0.1.1** (0.1.0 is a reserved name-camp). Use
> the latest release unless you need to pin a version.

### From crates.io (Recommended)

Requires [Rust 1.95+](https://www.rust-lang.org/tools/install):

```bash
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env

# Install nbox
cargo install nbox
```

### Homebrew (macOS/Linux)

```bash
brew install lance0/tap/nbox
```

### Docker (GHCR)

Multi-arch image (amd64/arm64):

```bash
docker run --rm ghcr.io/lance0/nbox:latest --help
```

Mount a config or pass `-e NBOX_TOKEN=...` plus a profile (or `--config`) to run
real lookups against your NetBox.

### Pre-built Binaries

Download from [GitHub Releases](https://github.com/lance0/nbox/releases):

| Platform | Target |
|----------|--------|
| Linux x86_64 | `nbox-x86_64-unknown-linux-musl.tar.gz` |
| Linux ARM64 | `nbox-aarch64-unknown-linux-musl.tar.gz` |
| macOS Apple Silicon | `nbox-aarch64-apple-darwin.tar.gz` |
| macOS Intel | `nbox-x86_64-apple-darwin.tar.gz` |
| Windows x86_64 | `nbox-x86_64-pc-windows-msvc.zip` |

```bash
# Download, verify, and install (Linux x86_64 example)
curl -LO https://github.com/lance0/nbox/releases/latest/download/nbox-x86_64-unknown-linux-musl.tar.gz
curl -LO https://github.com/lance0/nbox/releases/latest/download/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing   # macOS: shasum -a 256 -c
tar xzf nbox-*.tar.gz && sudo mv nbox /usr/local/bin/
```

### Quick Install Script

> **Note**: Piping a script from the internet to sh is convenient but bypasses
> your chance to read it first. Use one of the methods above, or
> [review the script](scripts/install.sh) before running. It downloads the latest
> release binary for your OS/arch and falls back to `cargo install` if there's no
> asset for your platform.

```bash
curl -fsSL https://raw.githubusercontent.com/lance0/nbox/master/scripts/install.sh | sh
```

### From Source

```bash
git clone https://github.com/lance0/nbox
cd nbox && cargo install --path .
```

### Optional Features

| Feature | Default | Description |
|---------|---------|-------------|
| `clipboard` | Yes | Copy values with `y` in the TUI (via `arboard`) |
| `cache` | No | Local SQLite cache (`rusqlite`) |
| `updates` | No | GitHub update notifications |

```bash
cargo install nbox --features cache,updates
```

### Shell Completions

```bash
# Bash
nbox completions bash > ~/.local/share/bash-completion/completions/nbox

# Zsh (add ~/.zfunc to fpath in .zshrc first)
nbox completions zsh > ~/.zfunc/_nbox

# Fish
nbox completions fish > ~/.config/fish/completions/nbox.fish

# PowerShell (add to $PROFILE)
nbox completions powershell >> $PROFILE

# Elvish
nbox completions elvish > ~/.elvish/lib/nbox.elv
```

Man pages are available too. `nbox man > nbox.1` writes the top-level page; pass
a directory — `nbox man man/` — to write the full set instead (`nbox.1` plus one
`nbox-<subcommand>.1` per subcommand, so `man nbox-device` works once installed).

## Usage

```bash
nbox                              # launch the TUI
nbox status                       # connection + NetBox/Django/Python versions
nbox search <query> [--limit N] [--status/--site/--region/--site-group/--location/--tenant/--role/--tag <v>] [--vrf <id|rd|name>] [--cols a,b,c] [--partial]
                                  # --site/--region/--site-group/--location resolve the ref once and filter
                                  # scope-capable endpoints by resolved id (prefixes/clusters via
                                  # scope_type=dcim.<kind> + scope_id; devices via *_id;
                                  # --site also filters VLANs/VMs by site_id), exact match.
                                  # At most one scope flag (else exit 2); an unknown ref errors (exit 4)
                                  # --vrf resolves id|rd|name once and filters IP/prefix results by vrf_id
                                  # (VRF-incapable endpoints skip it); orthogonal to scope; unknown → exit 4
nbox device <name-or-id> [--journal] [--journal-limit N]
nbox ip <address> [--vrf <name>] [--journal]    # --vrf disambiguates duplicates across VRFs
nbox prefix <cidr> [--vrf <name>] [--journal]   # includes utilization + children when present
nbox next-ip <cidr> [--count N] [--vrf <name>]        # next available address(es)
nbox next-prefix <cidr> [--length L] [--vrf <name>]   # available free block(s)
nbox vlan <vid-or-name> [--site <s>] [--group <g>] [--journal]
nbox site <name-or-slug> [--journal]
nbox rack <name-or-id> [--journal]
nbox circuit <cid-or-id> [--journal]
nbox provider <slug-or-name-or-id>
nbox aggregate <cidr-or-id> [--journal]
nbox asn <number> [--journal]
nbox ip-range <start-or-id> [--journal]
nbox tenant <slug-or-name-or-id>
nbox contact <name-or-id>
nbox vm <name-or-id>
nbox cluster <name-or-id>
nbox interface <device> <interface>
nbox tags                         # list tags (slug, name, count)
nbox journal <kind> <ref>         # recent journal entries for an object
                                  # --journal folds recent entries into a detail lookup (cap 5)
                                  # --journal-limit N overrides the cap (implies --journal)
nbox open <kind>/<ref>            # device, ip, prefix, vlan, site, rack, circuit, provider,
                                  # aggregate, asn, ip-range, tenant, contact, vm, cluster, and
                                  # interface/<device>/<name> (the name may contain slashes,
                                  # e.g. xe-0/0/1)
nbox raw GET <api-path>           # raw read-only API request (escape hatch)
nbox serve [--http <addr>]        # read-only MCP server for AI agents (stdio, or loopback/OIDC HTTP)
nbox config <init|path|show>
nbox profile <add|use|list|show>
nbox completions <bash|zsh|fish|powershell|elvish>
nbox man [DIR]                    # man pages: `nbox man > nbox.1` (top-level),
                                  # or `nbox man man/` for the full per-subcommand set
```

### Global flags

These apply to every command:

| Flag | Effect |
|------|--------|
| `-o, --output <fmt>` | `plain` (default), `json`, or `csv` (tabular/list results only) |
| `--json` | Shortcut for `-o json` |
| `--fields a,b,c` | JSON: keep only these top-level fields |
| `--raw` | JSON: compact (single line) instead of pretty |
| `--envelope` | JSON: wrap as `{ schema_version, data }` |
| `-p, --profile <name>` | Use a specific profile for this invocation |
| `--config <path>` | Use an alternate config file |
| `--log-level <spec>` | `tracing` filter (`info`, `debug`, `nbox=debug`, …) |
| `--log-file <path>` | Write logs to this file (and stderr); stdout stays clean |
| `--no-tui` | Refuse to launch the interactive TUI (a bare `nbox` or `nbox tui` exits `2` instead) |

`-o csv` is for tabular/list results (e.g. `search`); a single object is rejected
(use `--json` or plain). Custom fields appear as `cf.<name>` rows in plain output
and a `custom_fields` object in JSON.

Logs go to **stderr** by default (so stdout stays clean for piping and `--json`).
Point `--log-file <path>` (or `log_file` in config) at a file to also capture
them there. The level resolves `--log-level` → config `log_level` → `NBOX_LOG` →
`RUST_LOG` → `warn`. nbox never writes logs to stdout.

Exit codes are stable: `0` success, `1` generic error, `2` usage error, `3`
auth/permission (401/403), `4` not found, `5` ambiguous reference. See
[AGENTS.md](AGENTS.md) for the full machine-readable surface.

## Keybindings (TUI)

| Key | Action |
|-----|--------|
| `/` | search |
| `:` | command palette |
| `Tab` / `Shift+Tab` | switch pane focus |
| `j` / `k` | move selection / scroll detail |
| `g` / `G` | top / bottom |
| `PgUp` / `PgDn` | page up / down |
| `Enter` | open selected object |
| `o` | open in browser |
| `y` | copy selected field |
| `t` | cycle theme |
| `r` | refresh |
| `P` / `Ctrl+P` | switch profile (cycle forward / backward) |
| `S` | open the Config modal (profiles + settings) |
| `b` / `Esc` | back |
| `i p c v s` | device tabs (interfaces / IPs / cables / VLANs / services) |
| `?` / `F1` | help |
| `q` / `Ctrl+c` | quit |

The command palette (`:`) accepts `device`/`ip`/`prefix`/`vlan`/`site <ref>`,
`find <q>` (or bare text), `open`, `copy`, `theme <name>`, `profile <name>`,
`config`, and `refresh`. The home screen lists recently opened objects (deduped,
most-recent-first) when there are no search results — press `Enter` to reopen one.
Set `[ui].refresh_secs` to auto-refresh the current search on an interval (off by
default).

`P` / `Ctrl+P` (or `profile <name>` in the palette) switches between the profiles
in your config without restarting: it rebuilds the client for that instance and
re-probes `/api/status/`, so the header flips to the new profile and its NetBox
version. With a single profile the hotkey is a no-op. The cycle is session-only —
it does not rewrite `active_profile` in your config (use `nbox profile use <name>`
for that).

`S` (or `config` in the palette) opens the Config modal to manage profiles
in-app: list them (active marked), and add / edit / select / delete without
hand-editing `config.toml`. The add/edit form covers `name`, `url`, `token_env`,
`auth_scheme`, and `verify_tls`, plus an optional masked token field. When a
keyring is available, a typed token is stored there (never in `config.toml`);
otherwise nbox saves the profile metadata and tells you to use `token_env` or
`NBOX_TOKEN`. `Ctrl+T` test-connects before you commit; `Enter` saves, `Ctrl+G`
saves and switches to it. Unlike the quick `P` cycle, selecting or adding-and-using
a profile here **persists** `active_profile` to your config. Deleting the active
or last profile is blocked.

`Tab` switches the Config modal to its **Settings** section, an in-app editor for
the real `[ui]` settings: `theme` (cycle with `←`/`→`/Space, applied live),
`refresh_secs` (auto-refresh interval; empty/`0` = off), and
`open_browser_command` (a custom browser-open command; empty = OS default). `↑`/`↓`
move between fields; `Enter` or `Ctrl+S` saves each changed field back to
`config.toml` (format-preserving), re-arms the auto-refresh without a restart, and
applies the new browser command to the next open.

`[ui].open_browser_command`, when set, is what `nbox open` and the TUI `o` action
run to open a URL (the URL is appended as a literal final argument, never
shell-interpolated); leave it empty to use the OS default opener.

## Themes

Twelve built-in themes. Set with `[ui].theme` in the config or press `t` to cycle:

`default`, `kawaii`, `cyber`, `dracula`, `monochrome`, `matrix`, `nord`,
`gruvbox`, `catppuccin`, `tokyo_night`, `solarized`, `light`.

`light` is the only light-background theme. `NO_COLOR` is honored — when set, the
TUI renders without color and marks the selection with a `>` cursor.

## Configuration

First-time setup needs no hand-edited TOML: launch `nbox` with no config and the
TUI runs a first-run wizard that captures a profile (url, token or `token_env`,
`auth_scheme`, `verify_tls`), test-connects, writes it, and continues into the
app. The same wizard appears when a config exists but has no resolvable active
profile. You can still configure by hand — the file lives at:

| OS | Path |
|----|------|
| Linux / macOS | `~/.config/nbox/config.toml` |
| Windows | `%APPDATA%\nbox\config.toml` |

```toml
config_version = 1
active_profile = "work"

[ui]
theme = "default"
confirm_writes = true
# refresh_secs = 30          # TUI auto-refresh interval in seconds (omit/0 = off)
# open_browser_command = ""  # custom browser-open command (empty = OS default)

[profiles.work]
url = "https://netbox.example.com"
token_env = "NETBOX_TOKEN"
auth_scheme = "auto"          # auto | bearer | token
verify_tls = true
timeout_secs = 15
page_size = 100
exclude_config_context = true
```

Tokens are **never written to config**. nbox resolves them in order: the profile's
`token_env` variable (if set & present), then `NBOX_TOKEN`, then the OS keyring
entry for the profile (`nbox config token set` — input hidden, never echoed). Env
always overrides the keyring. `nbox config token status` shows the active source
(never the token). See [docs/CONFIG.md](docs/CONFIG.md) for the full reference.

## MCP Server

`nbox serve` runs a read-only MCP server, stdio by default. An MCP host (Claude
Desktop, Claude Code, …) launches `nbox serve` as a subprocess and speaks JSON-RPC
over stdin/stdout; it reuses the same query + view layer as the CLI, so the tools
return the same JSON view models. The NetBox URL and token come from the active
profile / env, and it takes the same `-p`/`--config` flags. JSON-RPC goes to
stdout; all logging stays on stderr.

The tools are all annotated read-only:

| Tool | What |
|------|------|
| `nbox_status` | Connection + NetBox/Django/Python versions. |
| `nbox_search` | Search devices/IPs/prefixes/VLANs/sites/circuits/providers/aggregates/ASNs/IP-ranges/tenants/contacts/VMs/clusters; `query` (required), `limit`, `status`, `site`, `region`, `site_group`, `location`, `tenant`, `role`, `tag`, `vrf` (id\|rd\|name; filters IP/prefix results only). |
| `nbox_get` | Fetch one object by `kind` (device, ip, prefix, vlan, site, rack, circuit, aggregate, asn, ip_range, tenant, contact, provider, vm, cluster) + `ref`; `vrf`/`site`/`group` disambiguate. |
| `nbox_get_interface` | One interface on a device, with its cable-path trace. |
| `nbox_next_ip` | Next available address(es) in a prefix. |
| `nbox_next_prefix` | Available free child prefix(es) of a given length, or all free blocks. |
| `nbox_journal` | Recent journal entries for an object. |
| `nbox_list_tags` | List tags. |

The same objects are also exposed as MCP **resources** via one template,
`nbox://{kind}/{ref}` (e.g. `nbox://device/edge01`), for hosts that browse or
attach resources instead of calling tools — reading one returns the same JSON
view `nbox_get` does.

### HTTP transport and OIDC

Stdio is the default. For local clients that prefer HTTP framing, serve the same
tools at `/mcp` (Streamable HTTP) on a loopback address — the transport is in the
default build (behind the `http` cargo feature, on by default; `--no-default-features`
gives a lean stdio-only build). It binds loopback only, validates `Origin`/`Host`,
and takes an optional static bearer:

```bash
nbox serve --http 127.0.0.1:8080 [--http-token "$(openssl rand -hex 16)"]
```

For a network-reachable, read-only deployment, run nbox as an OAuth 2.1 resource
server: it validates inbound IdP JWTs on `/mcp` and advertises Protected Resource
Metadata (RFC 9728). Provider-agnostic; configuring OIDC is what lifts the loopback
restriction (terminate TLS in front):

```bash
nbox serve --http 0.0.0.0:8080 \
  --oidc-issuer https://idp.example.com --audience https://nbox.example.com
```

This is accountability, not per-user RBAC — the last hop to NetBox still uses the
single profile token, so scope that token read-only. An audit log
(`nbox::audit`) and an optional per-caller rate limit (`--rate-limit`) round it
out. Full setup, security model, and IdP notes: [docs/MCP.md](docs/MCP.md).

Per-user NetBox identity bridging (so NetBox sees the real caller), writes, and a
raw escape-hatch tool come later.

## NetBox Compatibility

- **Requires NetBox 4.2+** (the polymorphic `scope` model for prefixes/VLANs).
  nbox checks the instance version via `/api/status/` on connect.
- Targets the NetBox **REST API** (`/api/`) as the primary integration path.
- Auto-detects **v2 API tokens** (NetBox 4.5+, `Authorization: Bearer nbt_…`) and
  legacy **v1 tokens** (`Authorization: Token …`); force one with `auth_scheme`.
- Optional, read-only **GraphQL** (`/graphql/`) for nested detail views is on the
  roadmap (later).

## Documentation

- [Features](docs/FEATURES.md) — full command reference
- [Configuration](docs/CONFIG.md) — config, profiles, token resolution
- [Architecture](docs/ARCHITECTURE.md) — internal design and module structure
- [MCP server](docs/MCP.md) — agent setup and tools
- [Agents](AGENTS.md) — machine-readable surface, output formats, exit codes
- [Known Issues](KNOWN_ISSUES.md) — current limitations
- [Changelog](CHANGELOG.md) — release history
- [Roadmap](ROADMAP.md) — planned features
- [Contributing](CONTRIBUTING.md) — development setup and guidelines
- [Security](SECURITY.md) — vulnerability reporting and security posture

## License

Licensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at
your option.
