{"id":"machine-status","repo":"dutchakdev/paseo-plugin-machine-status","url":"https://github.com/dutchakdev/paseo-plugin-machine-status","package":"paseo-plugin-machine-status","npm":{"package":"paseo-plugin-machine-status","version":"0.1.1","integrity":"sha512-0uJszgl+sY4BwaLjItogBM25Nh6VvlLR6J/c2bzkkm771qZfXZXR3DAVZQq7bLrJEbtoPLPzWydGo7KCOnHXhw==","publishedAt":"2026-10-05T09:45:22.034Z","downloadsLast30Days":177},"name":"machine-status","description":"Machine dashboard, open ports and Docker control inside Paseo","categories":["monitoring"],"platforms":["macos"],"caveats":["Requires a macOS daemon; Linux and Windows metric collection are unsupported.","Docker controls require an installed container engine.","Process visibility is limited by the daemon user's permissions."],"images":["https://raw.githubusercontent.com/dutchakdev/paseo-plugin-machine-status/1ff9b83605831424a29cafbae4b84700b1f1fd40/docs/preview.svg"],"themes":[],"health":{"manifestValid":true,"hasReadme":true,"hasLicense":true,"hasTests":true,"hasTypecheckScript":true,"updatedRecently":true},"scannedAt":"2026-10-12T11:24:50.666Z","addedAt":"2026-10-12T16:20:46+05:00","npmSecurity":{"status":"passed","blockingFindings":0,"advisoryFindings":0,"scannedAt":"2026-10-12T11:24:39.350Z","version":"0.1.1","integrity":"sha512-0uJszgl+sY4BwaLjItogBM25Nh6VvlLR6J/c2bzkkm771qZfXZXR3DAVZQq7bLrJEbtoPLPzWydGo7KCOnHXhw=="},"version":"0.1.1","security":{"status":"passed","blockingFindings":0,"advisoryFindings":0,"scannedAt":"2026-10-12T11:24:39.350Z","commit":"1ff9b83605831424a29cafbae4b84700b1f1fd40"},"author":"dutchakdev","license":"MIT","paseoVersionRequirement":">=0.8.0","descriptionNodes":[{"type":"text","text":"Machine dashboard, open ports and Docker control inside Paseo"}],"caveatNodes":[[{"type":"text","text":"Requires a macOS daemon; Linux and Windows metric collection are unsupported."}],[{"type":"text","text":"Docker controls require an installed container engine."}],[{"type":"text","text":"Process visibility is limited by the daemon user's permissions."}]],"manifest":{"id":"machine-status","requirements":{"paseo":">=0.8.0"}},"repoMeta":{"stars":1,"defaultBranch":"main","pushedAt":"2026-10-05T09:40:16Z"},"owner":{"login":"dutchakdev","avatarUrl":"https://avatars.githubusercontent.com/u/1294321?v=4"},"installNotesHtml":"<pre><code class=\"language-bash\">paseo plugin install npm:paseo-plugin-machine-status   # Paseo 0.9 and newer\npaseo plugin ls          # expect: running\n</code></pre>\n<p>Paseo 0.8 has no npm sources, so install from Git there:\n<code>paseo plugin add dutchakdev/paseo-plugin-machine-status</code>. Either way,\n<code>paseo plugin update machine-status</code> pulls later releases. The plugin supports <strong>Paseo 0.8.x through 0.11.x, including betas</strong>,\non both the daemon, which runs its collectors, and the app, which runs its screens.</p>\n<p>The manifest remains <code>>=0.8.0</code>, which already admits 0.9.x, 0.10.x and 0.11.x. Paseo also checks a\nprerelease's stable version core, so <code>0.11.0-beta.3</code> satisfies this range; see\n<a href=\"https://paseo.sh/docs/plugins/reference#requirements\">version requirements</a>.</p>\n<p>To work on it locally instead:</p>\n<pre><code class=\"language-bash\">git clone https://github.com/dutchakdev/paseo-plugin-machine-status.git\ncd paseo-plugin-machine-status\nnpm install\nnpm run typecheck &#x26;&#x26; npm test\npaseo plugin install \"$PWD\"\npaseo plugin ls          # expect: running\n</code></pre>\n<p>Plugins must be enabled daemon-wide first, under <strong>Settings → Plugins → Enable\nplugins</strong> (the root <code>pluginsEnabled</code> field in <code>~/.paseo/config.json</code>).</p>\n<p>Open it from the <strong>Machine</strong> sidebar item, or ⌘K → \"Machine status\".</p>","readmeText":"<h1 align=\"center\">paseo-plugin-machine-status</h1>\n\n<p align=\"center\">\n  What the machine behind your agents is actually doing, inside <a href=\"https://paseo.sh\">Paseo</a>.<br>\n  Load, memory, disk and battery; what is listening on the network and which agent started it;<br>\n  Docker; and the caches and runaway processes you can clear without leaving the app.\n</p>\n\n<p align=\"center\">\n  <img src=\"https://img.shields.io/badge/Paseo-0.8.x%20%E2%80%93%200.11.x-3987e5\" alt=\"Supports Paseo 0.8.x through 0.11.x, including betas\">\n  <img src=\"https://img.shields.io/badge/macOS-only-9aa1a6\" alt=\"macOS only\">\n  <img src=\"https://img.shields.io/badge/tests-237-199e70\" alt=\"237 tests\">\n  <img src=\"https://img.shields.io/badge/dependencies-none-c98500\" alt=\"No runtime dependencies\">\n  <img src=\"https://img.shields.io/badge/license-MIT-9aa1a6\" alt=\"MIT licensed\">\n</p>\n\n<p align=\"center\">\n  <img src=\"docs/preview.svg\" alt=\"The dashboard: KPI tiles, a load history chart, memory breakdown and disk usage\" width=\"920\">\n</p>\n\n<p align=\"center\"><sub>Generated by <code>npm run preview</code> from the plugin&apos;s own palette, thresholds and chart geometry, so the picture cannot drift from the code.</sub></p>\n\nOne sidebar item, three tabs, and a pill beside the message box.\n\n> Requires macOS. Metrics come from `vm_stat`, `sysctl`, `df`, `pmset` and\n> `memory_pressure`. On any other platform the panel reports that it cannot\n> collect metrics rather than showing wrong numbers.\n\n## Install\n\n```bash\npaseo plugin install npm:paseo-plugin-machine-status   # Paseo 0.9 and newer\npaseo plugin ls          # expect: running\n```\n\nPaseo 0.8 has no npm sources, so install from Git there:\n`paseo plugin add dutchakdev/paseo-plugin-machine-status`. Either way,\n`paseo plugin update machine-status` pulls later releases. The plugin supports **Paseo 0.8.x through 0.11.x, including betas**,\non both the daemon, which runs its collectors, and the app, which runs its screens.\n\nThe manifest remains `>=0.8.0`, which already admits 0.9.x, 0.10.x and 0.11.x. Paseo also checks a\nprerelease's stable version core, so `0.11.0-beta.3` satisfies this range; see\n[version requirements](https://paseo.sh/docs/plugins/reference#requirements).\n\nTo work on it locally instead:\n\n```bash\ngit clone https://github.com/dutchakdev/paseo-plugin-machine-status.git\ncd paseo-plugin-machine-status\nnpm install\nnpm run typecheck && npm test\npaseo plugin install \"$PWD\"\npaseo plugin ls          # expect: running\n```\n\nPlugins must be enabled daemon-wide first, under **Settings → Plugins → Enable\nplugins** (the root `pluginsEnabled` field in `~/.paseo/config.json`).\n\nOpen it from the **Machine** sidebar item, or ⌘K → \"Machine status\".\n\n## The screen\n\n```mermaid\nflowchart LR\n  subgraph client[\"Paseo app — client/\"]\n    S[\"status: KPI tiles · charts · cleanup · processes\"]\n    V[\"charts: bars, stacks, meters (Views only)\"]\n  end\n  subgraph shared[\"shared/\"]\n    Z[\"Zod contracts\"]\n    P[\"validated palette + geometry\"]\n  end\n  subgraph server[\"Daemon subprocess — server/\"]\n    M[\"metrics + rolling history\"]\n    C[\"cleanup\"]\n    R[\"processes\"]\n  end\n  OS[\"vm_stat · df · pmset · ps · du · fs.rm · kill\"]\n\n  S --> V --> P\n  S -- \"useRpc(contract, input)\" --> Z\n  Z -- \"validated both ways\" --> M & C & R\n  M & C & R --> OS\n```\n\n**Overview** — top to bottom: a header with a live indicator and a **Pause**\ncontrol, a row of four stat tiles (CPU, memory, swap, disk) that expand into\ndetail when tapped, CPU-over-time and memory-over-time charts, a disk/swap/power\ncard, the dev-cache scanner, and the process table.\n\n**Ports & services** — every listening socket on the left, and what is running on\nthe right. Only the visible tab is mounted, so the port collection does not run\nwhile you are looking at the overview.\n\n## The pill above the composer\n\nThe dashboard answers \"what is this machine doing\" when you go and ask. The pill\nanswers it without being asked: two percentages sitting beside the message box,\nCPU load and memory pressure, its icon coloured by whichever of the two is worse.\n\nThose are the two numbers that move while an agent works, and they fail in\ndifferent ways — one saturates, the other swaps — so both are shown. The colour\nfollows the worse one, because a pill has room for a single colour and you need\nto know something is wrong before you need to know which one. A number that\ncannot be read shows as `—` rather than as zero.\n\nOne poll every ten seconds feeds every open agent's pill at once. Paseo draws\nthe label, so the numbers reach each pill through its `update`; the icon is the\nplugin's own and takes the warning colour when either reading turns serious.\nPressing the pill opens a compact card instead of leaving the chat: both\nreadings with their recent trend and their severity in words, the three\nprocesses using the most CPU right now, and any development ports held by an\nagent or by a process an agent started. **Open Machine** at the bottom leads to\nthe full tab. On a phone the card is a bottom sheet.\n\n## Reading the dashboard\n\n**CPU** shows load average, not utilisation. The bar treats one runnable thread\nper core as 100%, so a sustained bar above full width means work is queueing.\n\n**Memory** follows Activity Monitor's split rather than \"free vs used\":\n\n| Row | Meaning |\n| --- | ------- |\n| App | Anonymous pages minus purgeable — what your programs actually hold |\n| Wired | Kernel memory that cannot be paged out |\n| Compressed | RAM reclaimed by compressing inactive pages |\n| Cached files | File-backed pages, reclaimed automatically when needed |\n\nThe pressure figure is macOS's own, read from `memory_pressure -Q`, not a value\nthis plugin invents. Cached files are deliberately excluded from \"Used\": the\nkernel gives them back on demand, so counting them as consumed is misleading.\n\n**Thermal** reads \"Not reported\" on a healthy machine — macOS records a level only\nonce it has throttled something. That is honest rather than a failure.\n\n**Per-core utilisation is absent** on purpose. It needs `host_processor_info`, a\nnative call macOS does not expose through any unprivileged shell tool, so the CPU\ndetail says so instead of estimating.\n\n## History and the charts\n\nHistory accumulates **as a side effect of being read** — there is no background\ntimer. A plugin that sampled forever would keep the machine awake in order to\nreport on how busy the machine is. The cost is that history only covers the time\nsomeone was watching, which is why every chart caption reports the real span\nbetween its first and last sample (\"last 4 min\") instead of claiming a fixed\nwindow. The buffer holds 90 samples and lives in the daemon subprocess, so\nclosing and reopening the tab does not lose it.\n\nReact Native has no SVG, and a Paseo client bundle may import only `react`,\n`react-native`, `@tanstack/react-query`, `zod` and `@getpaseo/plugin`. Every chart\nhere is therefore built from `View`s: lines and areas are a column per sample, and\nthe disk ring is a **meter**. The meter is not a consolation prize — for a single\nratio against a limit it is the better form, and a donut comparing close values is\na documented anti-pattern.\n\n### Colour\n\nChrome (backgrounds, text, borders) comes from Paseo's six theme tokens. Series\ncolour does not: a categorical hue has to stay the same hue across themes or it\nstops identifying anything. Paseo never says which theme is active, so the plugin\nmeasures the relative luminance of `surface0` and picks the matching column.\n\nBoth columns are the reference categorical order and were checked with the\nvalidator against their own surface — not chosen by eye:\n\n| Check | Dark | Light |\n| ----- | ---- | ----- |\n| Worst adjacent CVD ΔE | 8.4 | 9.1 |\n| Worst adjacent normal-vision ΔE | 19.8 | 22.9 |\n| Contrast vs surface | all ≥ 3:1 | aqua and yellow under 3:1 |\n\nThe light-mode shortfall is covered by the relief rule: every series carries a\nvisible label and value, so nothing is identified by colour alone. Status colours\n(warning, critical) are a separate fixed set and always ship beside a word —\n`High`, `Critical` — never as hue on its own.\n\n## Ports & services\n\n### Where the port list comes from\n\n`netstat` sees every listener on the machine; `lsof` sees only what this user may\ninspect. On this machine that is 56 sockets against 39. So `netstat` defines the\nlist and `lsof` fills in the process — a root-owned port is reported as belonging\nto another user rather than quietly dropped, and the count of those appears as a\nnote under the service list.\n\nThe dot beside each port is computed from the bind address, not guessed from the\nport number: a wildcard bind (`*`, `0.0.0.0`, `::`) is reachable from the network,\na loopback bind is not, and a bind to one interface address is neither.\n\nOne process bound to the same port on IPv4 and IPv6 is shown once — that is one\nservice, and `kubectl port-forward` produces the pair every time. Sockets with no\nvisible owner are never merged, because without a PID there is no evidence they\nbelong together.\n\n### Development ports only, by default\n\nMost of what a Mac listens on has nothing to do with development. On this machine\nthe split is 30 development ports against 44 system ones: AirPlay on 5000 and\n7000, Remote Desktop on 3283, Handoff, SMB, Kerberos, and a long tail of\nephemeral UDP. The list shows development ports and keeps the rest behind a\n**Show system** toggle with its count — classified, never discarded.\n\nRelevance is decided by evidence, because port ranges cannot separate these\ncases: 7000 here is AirPlay, while 49165 is a coding agent.\n\n| Rule | Result |\n| ---- | ------ |\n| Attributed to an agent, an agent's child, or a container | development |\n| Owner not inspectable (root, another account) | system |\n| Executable under `/System/`, `/usr/libexec/`, `/usr/sbin/`, `/Library/Apple/` | system |\n| A known runtime — node, bun, python, kubectl, next-server, postgres, … | development |\n| Anything else under `$HOME`, except `~/Library/` | development |\n| Otherwise | system |\n\nThe executable path comes from `ps`, not from `lsof`, whose command column is a\nbare name: the path is what separates `/System/Library/…/ControlCenter` from\n`~/dev/analytics-tool/.venv/bin/python`.\n\n### Attributing background processes to agents\n\nCoding agents leave things running. A single `claude` agent on this machine had\nthree MCP servers alive under it — `@playwright/mcp`, `chrome-devtools-mcp` and\n`prisma mcp` — none of which is obvious from a process list.\n\nMatching agents by name does not work. A substring search for the providers\nPaseo supports finds `AMPDeviceDiscoveryAgent`, `Campo` and `CursorUIViewService`\non a stock macOS install. The signal used instead is structural:\n\n1. Paseo starts each agent as a **direct child of the daemon**, and the plugin's\n   own parent *is* the daemon — so `process.ppid` gives the root for free.\n2. Each daemon child is confirmed against `paseo.agents.list()`: the binary must\n   match the provider **as a whole word**, and the process's `cwd` (read through\n   `lsof -d cwd`) must match the agent's. Anything else under the daemon —\n   `esbuild`, Paseo's helpers, this plugin — is left out.\n3. `cwd` also tells two agents of the same provider apart, which is the ordinary\n   case when the same model runs in two workspaces.\n4. Everything below a confirmed agent is its background process.\n5. A listening socket is attributed by walking its owner's parents until an agent\n   root appears — so a dev server three levels down still names the agent that\n   started it.\n\nIf `paseo.agents.list()` is unavailable or changes shape, the panel says\nattribution is off and still lists the ports. It never falls back to guessing by\nname.\n\n## Docker\n\nThe third tab manages the container engine and what runs on it.\n\n**The engine itself.** The card at the top separates three states, because they\nneed different offers: running shows the server version and a guarded `Quit`,\nstopped shows a **Start** button, and missing says which app to install and shows\nno button at all. Starting is a launch rather than a daemon command — Docker\nDesktop brings up its own VM over tens of seconds — so the panel polls every 3\nseconds instead of 10 until the daemon answers.\n\n**Containers.** Right-click any row for logs, `Open http://127.0.0.1:<port>` for\neach published port, restart, stop, start and remove. Destructive actions confirm\nin a second menu.\n\n**Disk.** `docker system df` broken out by images, containers, volumes and build\ncache, each with what it reclaims. Two prune actions are offered and both are\nnarrow on purpose:\n\n- stopped containers\n- **dangling** images only, never `-a`, which would delete every image without a\n  running container and cost gigabytes of re-pulling\n\nVolumes are never offered. They are usually the largest reclaimable number on a\ndevelopment machine and they hold container data, so the panel labels that number\n`but this is container data` and gives it no button.\n\n## Quick actions\n\nBoth actions are two-step. The first press states what will happen and what it\ncosts; the second carries it out. An armed button disarms itself after six\nseconds so it cannot be triggered later by a stray tap.\n\n### Dev caches\n\nScanning walks the directories with `du`, so it runs only when asked. Every\ncandidate is a cache its own tool recreates; clearing one costs a re-download or a\nrebuild, never data.\n\n| Target | Path | Cost of clearing |\n| ------ | ---- | ---------------- |\n| npm cache | `~/.npm/_cacache` | Re-download on next install |\n| pnpm store | `~/Library/pnpm/store` | Rebuilt on next install |\n| Yarn cache | `~/Library/Caches/Yarn` | Re-download on next install |\n| Bun install cache | `~/.bun/install/cache` | Re-download on next install |\n| Turbo cache | `~/.cache/turbo` | Next run rebuilds instead of replaying |\n| Xcode DerivedData | `~/Library/Developer/Xcode/DerivedData` | Next build is a full rebuild |\n| Cargo registry cache | `~/.cargo/registry/cache` | Re-download on next build |\n| Go build cache | `~/Library/Caches/go-build` | Next build is a full rebuild |\n| pip cache | `~/Library/Caches/pip` | Re-download on next install |\n| uv cache | `~/.cache/uv` | Re-download on next sync |\n\n`~/Library/Caches` as a whole is deliberately absent: it also holds application\nstate that does not regenerate.\n\nThree guards stand between the plugin and `fs.rm`:\n\n1. Only paths in the table above can ever be touched — the plugin takes a target\n   id, never a path from the client.\n2. Each path is resolved through `realpath` and must land at least two levels\n   below the real home directory. A symlink planted inside a cache cannot\n   redirect the deletion outside it.\n3. Only the directory's contents are removed; the directory itself stays, because\n   several tools expect it to exist.\n\n### Top processes\n\nThe list refreshes every five seconds and pauses while a process is selected — a\nlist that reorders under your finger is how the wrong PID gets terminated. Each\nrow carries a CPU bar scaled to the heaviest process on screen.\n\nPaseo and its parent processes are greyed out and marked `protected`. The plugin\nwalks its own parent chain to build that set and recomputes it from a fresh `ps`\non every kill, so a stale or forged PID from the client cannot get through.\n\nSIGTERM is sent first and the result is reported honestly: a process that ignores\nit is reported as still running, and force kill becomes a separate, separately\nconfirmed step.\n\n## Development\n\n```bash\nnpm run typecheck\nnpm test                              # 237 tests\npaseo plugin reload machine-status    # source edits need an explicit reload\npaseo plugin logs machine-status\nnpm run preview                       # regenerates docs/preview.svg from the real palette\n```\n\nTests cover the command-output parsers against captured real output, the path and\nPID guards, the rolling history buffer, the palette and chart geometry, the\nport parsers against captured `netstat`/`lsof`/`docker` output, the agent-matching\nrules including the real false"}