Skip to content
stealth-factoryPublic

About

Hollywood called. They want their terminal back. Sci-fi terminal widgets that show only real data — latency, deployments, tailnet peers, GitHub, agents and clocks. Fifteen single-binary Rust widgets, nothing to install alongside them.

Topics

Resources

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

opscope

Hollywood called. They want their terminal back.

Sci-fi hacker panels from the movies — except these ones tell you the truth.

Fill your screen with glowing panels, scrolling graphs and blinking status readouts. The difference is that every number on them is real: actual network latency, actual deployments, actual machines on your tailnet, actual agents waiting on you.

opscope — click to watch the 1m54s demo

Eight widgets running at once — every figure live. Watch the 1m54s demo →

Run it

npm is the default and recommended method: npx opscope. Nothing to install, no PATH to manage, no version to pick — it fetches the launcher and the binaries and starts whichever widget you name.

npx opscope              # the menu
npx opscope clocks       # or name one and skip it

clocks needs no configuration at all, so it is the one to try first. Other ways to get them →

The launcher

opscope is the front door: it shows every widget, what it does, and a preview without starting its data source. Pick one and it runs; quit it and you are back. It is the launcher, not another widget.

How the launcher works →

The widgets

Widget What it does Needs Docs
latency Continuous latency to a list of hosts: median, jitter, loss and a log-scale graph, so a slow link and an unsteady one look different. ping · Linux, macOS read →
vercel-deployments Vercel deployments over time — activity per hour, build-time drift, and the build log of the one you open, so a failure explains itself instead of only naming a code. A copy page carries the dashboard, preview and PR URLs. curl, a Vercel token read →
tailnet Tailscale peers, and whether each is reached directly or through a relay. Live throughput, full machine info, copyable addresses. tailscale read →
herdr-panes Every agent and process across all workspaces, ordered by which one needs a human. Enter jumps you there. herdr read →
luvus-panes The same question asked of a Luvus session over UHP 1.0 — agents ordered by who needs a human, plus the tasks they are coordinating and the file paths those have reserved. Enter jumps you there. luvus read →
github Pull requests across every org: merge rate, opened-vs-merged per day, review backlog and the contribution calendar — and ↵ for one account on a screen of its own, because a queue growing in one of them is invisible in a total the others are also feeding. curl, a GitHub token read →
github-actions GitHub Actions across your personal account and orgs: what is running or queued, which workflows are failing repeatedly, which job and step broke, and whether the pipeline is getting slower. curl, a GitHub token read →
github-prs The pull requests you have to follow up on: checks, reviews, mergeability, and a stack map with the order a stack has to merge in. curl, a GitHub token read →
linear Linear across every team: what is outstanding, the running cycles and their scope creep, issues created against completed, and every project still going. ↵ opens a cycle, a team or a project on a screen of its own. curl, a Linear API key read →
agent-usage How much each coding agent on the machine has been used — tokens, sessions, AI-written code — and what is left of each one's rate limit, one tab per agent. the agents' own logins read →
ports What is listening on this machine — the dev servers you have running, which project each was started from, how long it has been up, how much traffic it is carrying, and whether anything outside the box can reach it. k stops the selected one; ↵ opens it for a traffic chart, an address to copy, or to publish it over Tailscale or Cloudflare. lsof on macOS · Linux, macOS read →
netwatch Which processes are using the network — total since it started, current rate, up and down, per process — read from the operating system's own counters rather than by capturing packets. ip + ss on Linux, nettop + script on macOS · Linux, macOS read →
link How good the connection is between this machine and whoever is connected to it — round-trip time, jitter and loss for every inbound session, plus achieved delivery rate where the kernel exposes it, read rather than probed. ss on Linux, nettop + script on macOS · Linux, macOS read →
google-trends What a region is searching for right now, from Google's own Trending Now feed: every trend in Google's order with its traffic bucket as Google writes it, when it started and the news attached — plus, labelled as the widget's own count, what is new since the last look and what has left the feed. No account or key. curl read →
clocks Server clock, countdowns to the next hour / end of office hours / end of day, a pomodoro, and a world clock. — read →
matrix Nothing whatsoever. Digital rain, with truecolor fade trails. — read →
months A month grid you can page through: today marked, at least two weeks of context either side of it, ISO week numbers, and the zone the dates are reckoned in — clocks owns the time of day, this owns dates. — read →

All seventeen widgets run on Linux and macOS.

Each is a single self-contained binary — every library it needs is compiled in and ldd shows only libc, libm and libgcc. Widgets that read host tools declare them and check before launch instead of drawing an empty pane.

Ask the downloaded launcher about this machine at any time:

npx opscope doctor       # or: ./opscope doctor

It reports required and recommended tools separately, names the widgets that use each one, and prints installation advice. It never installs anything. For the package-managed Linux tools needed by the main data sources, the family commands are:

sudo apt install curl iproute2 iputils-ping     # Debian / Ubuntu
sudo dnf install curl iproute iputils           # Fedora / RHEL
sudo pacman -S curl iproute2 iputils            # Arch
apk add curl iproute2 iputils                    # Alpine (normally as root)

Tailscale, Herdr and Luvus keep their own installers and are reported separately; optional integrations such as Cloudflare tunnels never block a widget that can otherwise run. On macOS, the core tools ship with the operating system; doctor can offer Homebrew packages for optional tools where one exists.

How it started

Literally: "split 10 panels that make my computer look like some sci-fi hacker terminal from the movies."

So there were ten. A radar sweep, a rotating globe, a spectrum analyser, a cipher cracker, a memory scanner, a packet intercept — all beautiful, all fabricated. They looked exactly right and told you nothing.

Then one got wired to /proc and became genuinely useful, and the contrast was impossible to unsee. One by one the fakes came down, each replaced by something that answers a real question. What survived is the aesthetic with the lying removed.

Some of the theatre is still here, unapologetically. matrix computes nothing at all — it just looks good, and it knows it.

Getting them

npm / npx (recommended)

Use npm's npx command by default. If you have Node, this is the whole thing:

npx opscope                 # the menu
npx opscope clocks          # skip the menu; any widget name works
npx opscope@latest clocks   # latest release, or pin with @0.3.0
npx opscope@canary clocks   # whatever is on main right now

@canary is built and published from every merge to main, before it is in any release. It is for trying a fix or a new widget early, and it can break; --version names the commit it was built from. Plain npx opscope and @latest are never moved by a canary.

It fetches the launcher and seventeen widget binaries for this machine into npm's cache and runs whichever you named — or the menu, if you named none. Your PATH is not touched. Only opscope is exposed as a command; the widgets sit inside the package beside it, which is why link never shadows the coreutils command of that name.

Published for Linux x86-64 (glibc 2.35 or newer), macOS Apple Silicon and macOS Intel.

APT on Debian / Ubuntu

APT is an alternative for Linux amd64 users on Ubuntu 22.04 / Debian 12 or newer (glibc 2.35+). npm/npx remains the recommended method.

First complete the one-time signing-key and source setup for the Stealth Factory repository; Opscope is not in the default Debian/Ubuntu archives. Then install:

sudo apt update
sudo apt install opscope
opscope clocks

Update with sudo apt update && sudo apt install --only-upgrade opscope. No Node or Rust installation is needed. APT installs the required runtime libraries, but widget tools must currently be installed manually. Run opscope doctor to see which tools are missing and how to install them. Only opscope goes on PATH; widget binaries remain private and coreutils link is untouched. Linux arm64 packages are not available.

For a manual installation without adding the repository, download the .deb and matching .sha256 from a GitHub release, verify the checksum, and install locally:

sha256sum -c opscope_X.Y.Z_amd64.deb.sha256
sudo apt install ./opscope_X.Y.Z_amd64.deb
opscope clocks

Replace X.Y.Z with the downloaded release version. Without the repository, download and install each newer package manually. See Debian installation and upgrades for details.

Homebrew on macOS

Homebrew is an alternative for Mac users who prefer a package-managed installation without Node. npm / npx remains the recommended method.

brew install stealth-factory/tap/opscope
opscope                     # the menu
opscope clocks              # any widget name works
opscope doctor              # check optional external tools

Apple Silicon and Intel Macs use the same release binaries as npm and the tarball download. Only opscope goes on PATH; all eighteen binaries ship together, with the widgets kept private beside the launcher. No Node or Rust installation is needed.

Upgrade with brew update && brew upgrade opscope. Settings stay in ~/.config/opscope/config.json (or the location selected by OPSCOPE_CONFIG or XDG_CONFIG_HOME), outside Homebrew's versioned installation.

You can also download a release or build all eighteen binaries below.

Inside Luvus

If you work in a Luvus session, these install as a module and open as panes:

luvus module install stealth-factory/opscope
luvus module pane open opscope.widgets ports --placement split

One pane per widget, plus the launcher menu. Installing fetches this version's binaries and checks them against the sha256 published beside them, so nothing has to be on your PATH, and the settings are the same config.json every other route reads. What the module declares.

Download a release

Linux x86-64, macOS Apple Silicon and macOS Intel are built for every tag. This works out which one you want, checks it against its own checksum, and unpacks it — no version to fill in, because it asks which release is current:

R=https://github.com/stealth-factory/opscope/releases
V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' $R/latest | sed 's|.*/||')
case "$(uname -s) $(uname -m)" in
  "Darwin arm64")   A=aarch64-apple-darwin ;;
  "Darwin x86_64")  A=x86_64-apple-darwin ;;
  "Linux x86_64")   A=x86_64-unknown-linux-gnu ;;
  *) echo "no build for $(uname -sm); build from source below" >&2 ;;
esac
T=opscope-$V-$A
curl -fsSLO $R/download/$V/$T.tar.gz
curl -fsSLO $R/download/$V/$T.tar.gz.sha256
shasum -a 256 -c $T.tar.gz.sha256    # sha256sum -c on Linux; either is fine
tar -xzf $T.tar.gz && cd $T

Or take them by hand from the latest release — every tarball has a .sha256 beside it.

The eighteen binaries are right there, beside config.example.json and a copy of the docs. Nothing else is needed to run them, so this folder can live wherever you like. Start them from it — do not copy link onto your PATH, it shadows the coreutils command of that name:

./opscope            # the menu
./opscope link       # skip the menu
./opscope clocks -h  # widget flags go through

On macOS, download with curl rather than a browser. These binaries are not signed or notarised, and a browser marks what it downloads with com.apple.quarantine, which makes Gatekeeper refuse to run them. curl does not set that mark. If you did use a browser, extract the tarball and clear the mark on that extracted opscope-<version>-<arch> directory — not on Downloads, and not on .:

xattr -dr com.apple.quarantine opscope-*-apple-darwin

Or build them

Needs a Rust toolchain and nothing else:

cargo build --release   # launcher + seventeen widgets in ./target/release
./target/release/opscope # the menu, from the build tree

Running them

opscope is the front door — a menu of the seventeen widgets, with a live preview of whichever is highlighted. Name a widget to skip the menu. The same shape works from npx, from an unpacked tarball, and from a build tree:

npx opscope              # pick one and it runs
npx opscope clocks       # or name one and skip the menu
npx opscope clocks -h    # flags after the name belong to the widget

The launcher looks for each widget beside itself, so the launcher and seventeen widget binaries have to stay together — npx keeps them that way for you. An unpacked tarball or a build tree is a setup method, not the usual way to run them, and lives under Getting them.

They are built to sit side by side, but nothing assumes a multiplexer — each is an ordinary terminal program. Tile them however you like, and they degrade rather than break as panes get narrower: columns drop out in priority order, footers wrap instead of truncating, and graphs rescale. A widget in a 30-column strip still says something useful.

Scrolling and clicking

Every widget with a scrollable body scrolls the same way. matrix has no body, selection, or content below the viewport and is the sole explicit no-scroll exception.

wheel full widget scroll — the whole pane moves under a pinned title
Ctrl-Y Ctrl-E the same thing from the keyboard, a line at a time, as in vim
↑ ↓ move the selection, and the view follows to keep it in sight
click a row move the selection there — the same thing ↑ ↓ do
click it again open it — the same thing ↵ does
click a hint press the key that hint names

The split is the point: the wheel moves the view; keys and clicks move the selection. Turning the wheel never changes which row is selected — not even when it scrolls that row off the screen — and never changes which section has focus. So scrolling to look at something cannot change what ↵ opens. Press an arrow and the window comes back to the cursor.

Clicking is the other half of the same idea, in reverse: a click is a second route to a key, never a capability of its own. Clicking a row is ↑/↓; clicking [q]uit in the footer is q. Nothing here is reachable only with a mouse, so nothing is lost by not having one — and nothing new has to be learned to use one. Right-click, middle-click and drag reports are eaten, not acted on.

Clicking the row the cursor is already on opens it, which is ↵. Not a double-click: the terminal never says how many times you clicked, and the selected row is tinted anyway — you can see that the next click will open it, where a double-click shows nothing before it fires.

Every widget with a cursor answers a click, and every footer is clickable — two checks in cargo test fail the build when one is not. None of it is a setting: reporting is on wherever a widget runs.

Nothing is ever hidden because a pane is short. Each widget builds its frame at whatever height it needs and the pane shows a window onto it, so a section that will not fit is below the fold rather than dropped — a chart that is not drawn looks exactly like a chart with no data, and only one of those is your problem to fix.

Mouse reporting takes ordinary drag-to-select away while a widget is running: a drag inside the pane selects nothing. In a multiplexer or pane host the gesture that is lost is that host's copy, not the terminal's. The way to copy a value out of a widget is that widget's own copy key — eight of them have one. There was a terminal.mouse key that turned reporting off, on by default; it is retired, because the only thing turning it off ever did was leave somebody with a pane whose wheel had stopped. Ctrl-Y, Ctrl-E and the arrows scroll from the keyboard regardless.

Configuration

Every widget reads optional settings from the first readable of $OPSCOPE_CONFIG, $XDG_CONFIG_HOME/opscope/config.json (~/.config/opscope/config.json where that is unset), config.json in the working directory, and config.json beside the binary.

Nothing has to be configured. Every key is optional, and a key you leave out is not a gap — the widget uses the default built into it. Five widgets need a token before they can show you something; they are named under Credentials below. Every other widget starts on its own defaults.

There are two ways in, and they write the same file.

The settings screen

Press , in any configurable widget. The launcher has no settings of its own — that screen lives on the widget that reads the keys. It is one screen, owned by opscope-core rather than written sixteen times, so it behaves the same everywhere.

The list shows every key that widget answers to, and for each one the value in force, the default it falls back to, and what the key means. The file being written is named at the top, so there is never a question of which config.json you are editing.

↵ opens the row under the cursor, and what opens depends on the value:

the value what ↵ opens
text, or a number a single-line editor
true / false nothing — it moves the value on, default included
one of a fixed set the choices, one to a row
a list of names, or of numbers an entry-at-a-time editor: type to add one, pick one to remove it
a group of named settings the same, one entry per group, each opening its own screen
a model's prices a searchable card of every model, its publisher and its rates

The editors that both take typing and hold a list keep the two apart: you are typing until tab or ↓, and picking after. It is why d puts a d in the box rather than deleting the entry you can see — the box has focus, and a key that means two things at once will eventually do the wrong one.

[d]efault on a row removes the key from your config.json rather than writing today's default into it, so the widget's own default goes on being the answer even when a later release changes it.

Writes are atomic and owner-only. The widget reloads itself as you leave the screen, so nothing needs restarting.

Notices

Press ! in any widget to read its notices, as , opens settings. A notice is a warning that is not about a number on screen — config.json readable by other users, an old section name still being read, a recommended tool that is not installed. A new one shows above the footer for a minute, then as a coloured [!] 1 warning in the footer for as long as it is still true. The screen shows each in full with what to do about it, and dismisses one for this session, for 7 days or for 30, or all of them for this session; an error is only ever dismissed for the session. A dismissal for days is kept in notices.json in opscope's state directory ($XDG_STATE_HOME/opscope, or ~/.local/state/opscope) and every widget reads it again within a minute, so the config warning dismissed in one pane goes quiet in the others. Dismissed notices are listed on the screen, with how long each lasts, and can be restored. A warning that explains a missing or partial number is never a notice — it stays beside the number.

Editing config.json by hand

config.example.json is the reference, and it is generated — not written by hand, not kept current by remembering to. It is built from the same per-widget declarations the settings screen reads, so it carries every section, every key any widget reads, that key's real default, and a comment for each saying what it does:

"ports": {
  "_system_ports_comment": "Extra ports to hide behind `o`, which hides them by default. …",
  "system_ports": [22, 53, 123, 323, 631, 5353],
  "_refresh_comment": "Seconds between listening-socket and traffic refreshes.",
  "refresh": 4.0
}

Copy the sections you want, drop the rest. _comment keys are ignored, so they can stay where they are as a reminder.

So you do not have to read seventeen widget pages to find out what you can set. cargo test fails if a widget reads a key the example does not list, and fails again if the example lists a key no widget reads — the file is neither incomplete nor stale by construction, in both directions. The per-widget pages are for depth: where a number comes from, what it means, why a default is what it is. They are not the inventory.

Section names are the widget's name with hyphens turned to underscores, so vercel-deployments reads vercel_deployments. Getting one wrong is silent — a section no widget reads is simply never read, and the widget goes on using its defaults as though you had written nothing.

Every section is a widget's. There was one that was not — terminal, holding the single key that decided whether widgets asked for mouse reports — and it is gone. Reporting is unconditional now: the key was on by default, on is what everybody got, and turning it off only ever cost somebody their wheel. What it costs while it is on is real and small: dragging in a widget's pane selects nothing. The way to copy a value out of a widget is that widget's own copy key.

Credentials

Five widgets define their own token settings: vercel-deployments wants a Vercel token from Account Settings → Tokens, github a classic GitHub PAT with repo and read:org (fine-grained tokens reach only one org each), linear a personal API key from Settings → Security & access, and github-prs and github-actions each hold their own GitHub token rather than borrowing one. Every other widget needs no configuration to start.

All three GitHub widgets also reach GitHub Enterprise: set enterprise_hostname in each one's section (for example acme.ghe.com), or name a variable holding the host in enterprise_hostname_env, and use a token that host issued. If both are empty, the widget uses github.com. Each widget's README has the details.

Keeping all of this in config.json is what keeps hostnames, ping targets, city lists and tokens out of the source tree: the repo ships generic defaults, and config.json is git-ignored along with .env files and anything else likely to hold a secret.

Asking an assistant to configure one

Each binary carries a plain-Markdown guide for an AI assistant helping you:

opscope latency --configure-help

The guide explains real data sources, safe inspection, credentials, and what must be asked rather than guessed. It is documentation, not a skill and not permission to make changes.

Requirements

Kitty and ordinary terminals

Opscope detects terminal capabilities per session. Supporting terminals get synchronized frame updates and pixel charts through the Kitty graphics protocol. latency, link, netwatch and ports render network history; github, github-actions, vercel-deployments and linear render activity charts; github and agent-usage render calendars; linear and agent-usage also render progress/quota meters. Other sessions use Braille and text graphics with the same data and controls. Use OPSCOPE_GRAPHICS=text to keep text charts, or OPSCOPE_GRAPHICS=kitty to request pixels with an on-screen fallback explanation. The default is auto. No Kitty helper or image library needs installing. Inside Herdr, auto draws text charts: Herdr relays every image to its client uncompressed, which over SSH costs megabytes a second. Set OPSCOPE_GRAPHICS=kitty for pixels when the Herdr client is local.

To force text charts even in Kitty, run OPSCOPE_GRAPHICS=text opscope latency for one widget or OPSCOPE_GRAPHICS=text opscope for the launcher and its widgets. See forcing text rendering for preview commands and a shell-wide setting. See terminal rendering for the developer API, widget review, tests and multiplexer compatibility limits.

Platform and tools

  • Nothing to install to run them — the binaries carry what they link against, SQLite included. Node, if you use npx; a Rust toolchain only if you build rather than download or install
  • Linux x86-64 or macOS, Apple Silicon or Intel. A Linux arm64 build is not produced yet; that machine builds from source
  • A terminal with 24-bit colour
  • Per-widget, the external tools the table above names: curl, ss, ping, tailscale, herdr, ip, and the macOS system tools lsof, ps, nettop, ifconfig and script. cloudflared is an optional integration, not a hard requirement. Each widget needs only its own; one that cannot work without its tool says so rather than drawing an empty pane; and none needs root

Documentation

Every widget has a page of its own — what it shows, where each number comes from, every key it answers to, and the settings it reads when it is configurable. That README lives beside the widget's code, help, plain-Markdown AI configuration guide, and — when the widget has settings — its settings declaration. They are linked from the table above and indexed in docs/.

Eight pages are about the repository rather than a widget:

Making a widget how one folder owns a widget's code, help, preview, settings and AI configuration guide — and everything else it takes to add one
Model prices published API list prices per million tokens, with sources and as-of dates
Design conventions the rules every widget holds to, and why each was paid for
Internals opscope-core, the chart helpers, and what cargo test checks that a compiler cannot
Port decisions what the Rust port changed from the Python and why — the answer to most questions beginning why does this key do that
Building Herdr panels resize semantics, focus, and the layout mistakes worth skipping
As a Luvus module every widget as a pane inside a Luvus session: what the module declares, where its binaries come from, and why its manifest is generated
Releasing how a version is decided, what merging the release PR sets off, and what to do when it goes wrong

Bundled skill

skills/herdr/ carries the Herdr control skill, so an agent working in this repo can drive panes, tabs and workspaces without going looking for it — copy it to ~/.claude/skills/herdr/ to install. It is Herdr's own file, not covered by this repository's licence, and herdr --skill regenerates it after an upgrade.

What is being worked on

Planned widgets, open questions and the state of the Rust port are tracked in Linear: https://linear.app/stealth-company/project/opscope-e829b47d84b8/issues. The link needs access to the workspace; the issues are the canonical list either way, so a feature that looks missing may already be filed there with a reason.

Contributing

cargo test from the root is the gate: each widget's own tests, plus widgets/tests/check.rs, which reads the sources and fails on the things a compiler cannot see — a poller that dies without saying why, a key hinted but unanswered, a setting read but undocumented, a colour below WCAG AA on a selected row. Internals explains what each check is defending. Follow Making a widget for the ordered path from an empty folder through the build-tree launcher smoke test and PR handoff.

License

GNU AGPL-3.0. You may use, modify and share these widgets freely; if you distribute a modified version — or run one as a network service — you must make your source available under the same license. Commercial licenses are available; write to email@wiiiimm.codes.

About

Hollywood called. They want their terminal back. Sci-fi terminal widgets that show only real data — latency, deployments, tailnet peers, GitHub, agents and clocks. Fifteen single-binary Rust widgets, nothing to install alongside them.

Topics

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages