npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

paseo-plugin-machine-status

v0.3.2

Published

Machine dashboard, open ports and Docker control inside Paseo

Readme

One sidebar item, three tabs, and a pill beside the message box.

Requires macOS. Metrics come from vm_stat, sysctl, df, pmset and memory_pressure. On any other platform the panel reports that it cannot collect metrics rather than showing wrong numbers.

Install

Plugins must be enabled daemon-wide first, under Settings → Plugins → Enable plugins (the root pluginsEnabled field in ~/.paseo/config.json). Then pick the source that matches your Paseo:

| Paseo | Source | Command | | ----- | ------ | ------- | | 0.11 and newer | Plugin directory, the reviewed version | paseo plugin add dutchakdev/machine-status | | 0.11 and newer | npm, the newest release before the directory approves it | paseo plugin install npm:paseo-plugin-machine-status | | 0.8 to 0.10, 0.11.0 betas | GitHub, release 0.2.3, the last one for these versions | paseo plugin add dutchakdev/paseo-plugin-machine-status --ref v0.2.3 |

paseo plugin ls should then show machine-status as running, and paseo plugin update machine-status pulls later releases from the same source. From Paseo 0.11 a bare owner/name names a directory entry, so a GitHub source there needs the git: prefix: git:dutchakdev/paseo-plugin-machine-status.

The plugin supports Paseo 0.11 and newer, including betas, on both the daemon, which runs its collectors, and the app, which runs its screens.

The manifest asks for >=0.11.0 because it declares the icon shown in the plugin directory, and Paseo 0.8 to 0.10 refuse a manifest with a field they do not know. Paseo also checks a prerelease's stable version core, so the betas of later releases satisfy this range. The 0.11.0 betas pass that check but predate the icon field, so they refuse the manifest as well and need the release named in the install table; see version requirements.

To work on it locally instead:

git clone https://github.com/dutchakdev/paseo-plugin-machine-status.git
cd paseo-plugin-machine-status
npm install
npm run typecheck && npm test
paseo plugin install "$PWD"
paseo plugin ls          # expect: running

Open it from the Machine sidebar item, or ⌘K → "Machine status". On Paseo 0.11 and newer, ⌘K also offers "Machine: ports & services" and "Machine: Docker", which open straight on that tab, and the tab travels in the screen's link (?tab=services, ?tab=docker), so reload and back keep it.

The screen

flowchart LR
  subgraph client["Paseo app — client/"]
    S["status: KPI tiles · charts · cleanup · processes"]
    V["charts: bars, stacks, meters (Views only)"]
  end
  subgraph shared["shared/"]
    Z["Zod contracts"]
    P["validated palette + geometry"]
  end
  subgraph server["Daemon subprocess — server/"]
    M["metrics + rolling history"]
    C["cleanup"]
    R["processes"]
  end
  OS["vm_stat · df · pmset · ps · du · fs.rm · kill"]

  S --> V --> P
  S -- "useRpc(contract, input)" --> Z
  Z -- "validated both ways" --> M & C & R
  M & C & R --> OS

Overview — top to bottom: a header with a live indicator and a Pause control, a row of four stat tiles (CPU, memory, swap, disk) that expand into detail when tapped, CPU-over-time and memory-over-time charts, a disk/swap/power card, the dev-cache scanner, and the process table.

Ports & services — every listening socket on the left, and what is running on the right. Only the visible tab is mounted, so the port collection does not run while you are looking at the overview.

The pill above the composer

The dashboard answers "what is this machine doing" when you go and ask. The pill answers it without being asked: two percentages sitting beside the message box, CPU load and memory pressure, its icon coloured by whichever of the two is worse.

Those are the two numbers that move while an agent works, and they fail in different ways — one saturates, the other swaps — so both are shown. The colour follows the worse one, because a pill has room for a single colour and you need to know something is wrong before you need to know which one. A number that cannot be read shows as — rather than as zero.

One poll every ten seconds feeds every open agent's pill at once. Paseo draws the label, so the numbers reach each pill through its update; the icon is the plugin's own and takes the warning colour when either reading turns serious. Pressing the pill opens a compact card instead of leaving the chat: both readings with their recent trend and their severity in words, the three processes using the most CPU right now, and any development ports held by an agent or by a process an agent started. Open Machine at the bottom leads to the full tab. On a phone the card is a bottom sheet.

Reading the dashboard

CPU shows load average, not utilisation. The bar treats one runnable thread per core as 100%, so a sustained bar above full width means work is queueing.

Memory follows Activity Monitor's split rather than "free vs used":

| Row | Meaning | | --- | ------- | | App | Anonymous pages minus purgeable — what your programs actually hold | | Wired | Kernel memory that cannot be paged out | | Compressed | RAM reclaimed by compressing inactive pages | | Cached files | File-backed pages, reclaimed automatically when needed |

The pressure figure is macOS's own, read from memory_pressure -Q, not a value this plugin invents. Cached files are deliberately excluded from "Used": the kernel gives them back on demand, so counting them as consumed is misleading.

Thermal reads "Not reported" on a healthy machine — macOS records a level only once it has throttled something. That is honest rather than a failure.

Per-core utilisation is absent on purpose. It needs host_processor_info, a native call macOS does not expose through any unprivileged shell tool, so the CPU detail says so instead of estimating.

History and the charts

History accumulates as a side effect of being read — there is no background timer. A plugin that sampled forever would keep the machine awake in order to report on how busy the machine is. The cost is that history only covers the time someone was watching, which is why every chart caption reports the real span between its first and last sample ("last 4 min") instead of claiming a fixed window. The buffer holds 90 samples and lives in the daemon subprocess, so closing and reopening the tab does not lose it.

React Native has no SVG, and a Paseo client bundle may import only react, react-native, @tanstack/react-query, zod and @getpaseo/plugin. Every chart here is therefore built from Views: lines and areas are a column per sample, and the disk ring is a meter. The meter is not a consolation prize — for a single ratio against a limit it is the better form, and a donut comparing close values is a documented anti-pattern.

Colour

Chrome (backgrounds, text, borders) comes from Paseo's six theme tokens. Series colour does not: a categorical hue has to stay the same hue across themes or it stops identifying anything. Paseo never says which theme is active, so the plugin measures the relative luminance of surface0 and picks the matching column.

Both columns are the reference categorical order and were checked with the validator against their own surface — not chosen by eye:

| Check | Dark | Light | | ----- | ---- | ----- | | Worst adjacent CVD ΔE | 8.4 | 9.1 | | Worst adjacent normal-vision ΔE | 19.8 | 22.9 | | Contrast vs surface | all ≥ 3:1 | aqua and yellow under 3:1 |

The light-mode shortfall is covered by the relief rule: every series carries a visible label and value, so nothing is identified by colour alone. Status colours (warning, critical) are a separate fixed set and always ship beside a word — High, Critical — never as hue on its own.

Ports & services

Where the port list comes from

netstat sees every listener on the machine; lsof sees only what this user may inspect. On this machine that is 56 sockets against 39. So netstat defines the list and lsof fills in the process — a root-owned port is reported as belonging to another user rather than quietly dropped, and the count of those appears as a note under the service list.

The dot beside each port is computed from the bind address, not guessed from the port number: a wildcard bind (*, 0.0.0.0, ::) is reachable from the network, a loopback bind is not, and a bind to one interface address is neither.

One process bound to the same port on IPv4 and IPv6 is shown once — that is one service, and kubectl port-forward produces the pair every time. Sockets with no visible owner are never merged, because without a PID there is no evidence they belong together.

Development ports only, by default

Most of what a Mac listens on has nothing to do with development. On this machine the split is 30 development ports against 44 system ones: AirPlay on 5000 and 7000, Remote Desktop on 3283, Handoff, SMB, Kerberos, and a long tail of ephemeral UDP. The list shows development ports and keeps the rest behind a Show system toggle with its count — classified, never discarded.

Relevance is decided by evidence, because port ranges cannot separate these cases: 7000 here is AirPlay, while 49165 is a coding agent.

| Rule | Result | | ---- | ------ | | Attributed to an agent, an agent's child, or a container | development | | Owner not inspectable (root, another account) | system | | Executable under /System/, /usr/libexec/, /usr/sbin/, /Library/Apple/ | system | | A known runtime — node, bun, python, kubectl, next-server, postgres, … | development | | Anything else under $HOME, except ~/Library/ | development | | Otherwise | system |

The executable path comes from ps, not from lsof, whose command column is a bare name: the path is what separates /System/Library/…/ControlCenter from ~/dev/analytics-tool/.venv/bin/python.

Attributing background processes to agents

Coding agents leave things running. A single claude agent on this machine had three MCP servers alive under it — @playwright/mcp, chrome-devtools-mcp and prisma mcp — none of which is obvious from a process list.

Matching agents by name does not work. A substring search for the providers Paseo supports finds AMPDeviceDiscoveryAgent, Campo and CursorUIViewService on a stock macOS install. The signal used instead is structural:

  1. Paseo starts each agent as a direct child of the daemon, and the plugin's own parent is the daemon — so process.ppid gives the root for free.
  2. Each daemon child is confirmed against paseo.agents.list(): the binary must match the provider as a whole word, and the process's cwd (read through lsof -d cwd) must match the agent's. Anything else under the daemon — esbuild, Paseo's helpers, this plugin — is left out.
  3. cwd also tells two agents of the same provider apart, which is the ordinary case when the same model runs in two workspaces.
  4. Everything below a confirmed agent is its background process.
  5. A listening socket is attributed by walking its owner's parents until an agent root appears — so a dev server three levels down still names the agent that started it.

If paseo.agents.list() is unavailable or changes shape, the panel says attribution is off and still lists the ports. It never falls back to guessing by name.

Docker

The third tab manages the container engine and what runs on it.

The engine itself. The card at the top separates three states, because they need different offers: running shows the server version and a guarded Quit, stopped shows a Start button, and missing says which app to install and shows no button at all. Starting is a launch rather than a daemon command — Docker Desktop brings up its own VM over tens of seconds — so the panel polls every 3 seconds instead of 10 until the daemon answers.

Containers. Right-click any row for logs, Open http://127.0.0.1:<port> for each published port, restart, stop, start and remove. Destructive actions confirm in a second menu.

Disk. docker system df broken out by images, containers, volumes and build cache, each with what it reclaims. Two prune actions are offered and both are narrow on purpose:

  • stopped containers
  • dangling images only, never -a, which would delete every image without a running container and cost gigabytes of re-pulling

Volumes are never offered. They are usually the largest reclaimable number on a development machine and they hold container data, so the panel labels that number but this is container data and gives it no button.

Quick actions

Both actions are two-step. The first press states what will happen and what it costs; the second carries it out. An armed button disarms itself after six seconds so it cannot be triggered later by a stray tap.

Dev caches

Scanning walks the directories with du, so it runs only when asked. Every candidate is a cache its own tool recreates; clearing one costs a re-download or a rebuild, never data.

| Target | Path | Cost of clearing | | ------ | ---- | ---------------- | | npm cache | ~/.npm/_cacache | Re-download on next install | | pnpm store | ~/Library/pnpm/store | Rebuilt on next install | | Yarn cache | ~/Library/Caches/Yarn | Re-download on next install | | Bun install cache | ~/.bun/install/cache | Re-download on next install | | Turbo cache | ~/.cache/turbo | Next run rebuilds instead of replaying | | Xcode DerivedData | ~/Library/Developer/Xcode/DerivedData | Next build is a full rebuild | | Cargo registry cache | ~/.cargo/registry/cache | Re-download on next build | | Go build cache | ~/Library/Caches/go-build | Next build is a full rebuild | | pip cache | ~/Library/Caches/pip | Re-download on next install | | uv cache | ~/.cache/uv | Re-download on next sync |

~/Library/Caches as a whole is deliberately absent: it also holds application state that does not regenerate.

Three guards stand between the plugin and fs.rm:

  1. Only paths in the table above can ever be touched — the plugin takes a target id, never a path from the client.
  2. Each path is resolved through realpath and must land at least two levels below the real home directory. A symlink planted inside a cache cannot redirect the deletion outside it.
  3. Only the directory's contents are removed; the directory itself stays, because several tools expect it to exist.

Top processes

The list refreshes every five seconds and pauses while a process is selected — a list that reorders under your finger is how the wrong PID gets terminated. Each row carries a CPU bar scaled to the heaviest process on screen.

Paseo and its parent processes are greyed out and marked protected. The plugin walks its own parent chain to build that set and recomputes it from a fresh ps on every kill, so a stale or forged PID from the client cannot get through.

SIGTERM is sent first and the result is reported honestly: a process that ignores it is reported as still running, and force kill becomes a separate, separately confirmed step.

Development

npm run check                         # typecheck, then the tests
paseo plugin reload machine-status    # source edits need an explicit reload
paseo plugin logs machine-status
npm run preview                       # regenerates docs/preview.svg from the real palette
./scripts/preview-png.sh              # renders it to docs/preview.png for the plugin directory

Tests cover the command-output parsers against captured real output, the path and PID guards, the rolling history buffer, the palette and chart geometry, the port parsers against captured netstat/lsof/docker output, the agent-matching rules including the real false positives above, and a round-trip of every handler result through its own Zod contract — the drift that would otherwise surface as an RPC rejection at runtime. The live tests never mutate anything: cleanup is only planned, and the kill tests assert refusals, including a refusal to terminate the test runner itself.

Paseo versions

The SDK stays pinned to 0.11.4 for typechecking and tests, so the compiler proves the plugin uses only the 0.11 API, which every supported release still provides. Paseo supplies the runtime instances on every supported release line, so none of it ships with the plugin.

Paseo 0.11 replaced surfaces with screens and kept the old calls as deprecated aliases. Up to 0.2.3 the manifest admitted 0.8, and each connected app evaluates the client entry against its own version, so client/compat.tsx describes the 0.11 additions locally and calls them only after checking that the app has them. Since the manifest asks for 0.11, every app that loads the plugin takes the first row; the fallback stays in the code until it is removed on its own:

| App version | Screen | Sidebar | Opens with | Tab shortcuts in ⌘K | | ----------- | ------ | ------- | ---------- | ------------------- | | 0.11 and newer | addScreen | addSidebarHeaderItem + SidebarRow | openScreen, with ?tab= | yes | | 0.8 to 0.10 | addSurface | addSidebarItem | openSurface, overview tab | not offered |

Both paths register under the id main, so saved /plugin/machine-status/main links open the same screen after an upgrade. tests/compat.test.ts pins both paths, including a half-present API, which falls back rather than mixing the two. The daemon half uses only server.handle, which has not changed since 0.8.

Releases are cut from tags by GitHub Actions; see CONTRIBUTING.md for the flow and CHANGELOG.md for what changed.

Troubleshooting

| Symptom | Check | | ------- | ----- | | Sidebar item missing | paseo plugin ls reports running, the client is on this host, and on 0.11+ the item is not hidden in Settings → Sidebar | | "Metrics unavailable" | The daemon is not on macOS, or a /usr/bin tool is missing | | Charts look empty | History starts empty; it fills as the tab stays open | | A port shows "another user" | It belongs to root or another account; lsof cannot inspect it unprivileged | | No agent attribution | paseo.agents.list() failed — the note under the service list says so | | A port you expected is missing | It was classified as a system port; use Show system | | Docker containers missing | Docker is not installed or not running; the note says so | | A cache shows "not present" | The tool is not installed, or it moved its cache | | Edits do not appear | npm run typecheck, then paseo plugin reload machine-status |

License

MIT