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

@sundarshahi/heatsink

v0.1.3

Published

Keep AI coding agents from cooking your laptop — load/thermal guard that right-sizes runaway parallel commands. Pure bash, no deps.

Readme

heatsink — Your AI coding agent doesn't know your laptop is on fire.

heatsink doctor on a machine at load 99

The war story

My MacBook is a 10-core machine. One afternoon it sat at a load average of 99. Not 9.9 — 99. Ten cores, ninety-nine runnable processes queued up behind them.

The cause: ten orphaned yes processes, each pinned to a core, each burning CPU for nothing, for 24 hours straight. Somewhere along the way an agent had spun up a background loop, the parent that was supposed to manage it died or moved on, and the children just kept running — orphaned, invisible, permanent. On top of that pile, a parallel vitest suite (also agent-spawned) tried to fan out across every core it could find.

The agent doing all this had no idea any of it was happening. It saw a shell, it ran a command, the command returned (eventually), it moved to the next task. It had no concept of "the machine is on fire" — no load signal, no thermal signal, no notion that the last five commands it ran are still running. It just kept scheduling more parallel work onto a machine that had none left to give.

heatsink exists so that never has to happen silently again.

What it does

heatsink sits between an AI coding agent and your shell. Before a command runs, it checks load and thermal state and does one of four things:

  • ok — nothing unusual, run as-is.
  • warn — near saturation, command runs but the agent is told to hold off on stacking more parallel work.
  • throttle — command matches a known heavy build/test runner and has a parallelism knob heatsink can safely lower; the rewritten command runs instead.
  • deny — already oversubscribed, or near saturation while the OS is thermally throttling; the command doesn't run at all.

The one invariant that matters: a rewrite only ever reduces parallelism. heatsink never raises a worker count, never adds -j to a bare make, and never guesses at a knob it isn't sure about — when it can't rewrite safely, it warns instead of mangling your command.

Auto-throttle table

| Command | How it's detected | Rewrite behavior | |---|---|---| | rspec, parallel_rspec, parallel_test(s) | PARALLEL_TEST_PROCESSORS=N env var | Lowers an existing value, or prepends the env var if absent | | vitest, jest | --maxWorkers[= ]N | Lowers an existing value, or appends --maxWorkers=N if absent | | playwright | --workers[= ]N | Lowers an existing value, or appends --workers=N if absent | | pytest | -n/--numprocesses N | Lowers an existing value; if pytest-xdist isn't already in use, warns instead of adding -n — forcing it on could break a suite that never installed xdist | | cargo test | --test-threads[= ]N | Lowers an existing value, or appends -- --test-threads=N if absent | | cargo build | CARGO_BUILD_JOBS=N env var | Lowers an existing value, or prepends the env var if absent | | go test | -p, -p=, or -pN (all three forms) | Lowers an existing value, or appends -p N if absent | | make, cmake --build | -j N | Lowers an existing value; bare invocations (no -j) are left alone — make defaults to serial, and heatsink never adds parallelism, only removes it | | npm/yarn/pnpm/bun test/build, turbo run | a forwarded --maxWorkers/--workers flag | Env var VITEST_MAX_THREADS=N always prepended; forwarded flag also lowered in place when higher than N; at/below target flag passes unchanged | | webpack, tsc, gradle, mvn, bazel | — | Flagged as heavy but has no known safe knob — always warn, never rewritten |

Usage

heatsink check --command "<cmd>" [--json]   verdict: ok|warn|throttle|deny
heatsink doctor                             why is my machine hot?
heatsink reap [--kill]                      find (and optionally kill) orphaned burners
heatsink wrap -- <cmd>                      throttle-if-needed, then run
heatsink hook <claude-code|cursor|codex>    harness hook entrypoint (stdin JSON)

The four verdicts, end to end

Exit code is 0 for ok/warn/throttle and 1 for deny (wrap exits 75/EX_TEMPFAIL on deny so a CI job can retry).

the four verdicts from heatsink check

ok — machine is quiet, nothing happens:

$ heatsink check --command "npx vitest run"
ok

throttle — load is in the throttle zone and the command has a safe knob:

$ heatsink check --command "npx vitest run" --json
{
  "verdict": "throttle",
  "load": 3.58,
  "cores": 10,
  "thermal": "ok",
  "rewritten": "npx vitest run --maxWorkers=2",
  "reason": "load 3.58 on 10 cores — throttled to 2 workers."
}

$ heatsink check --command "make -j 16"
throttle: make -j 2

warn — heavy command, no knob heatsink can safely turn:

$ heatsink check --command "tsc -b"
warn

deny — already oversubscribed, command does not run:

$ heatsink check --command "cargo build"
deny
$ echo $?
1

wrap — guard a command you're about to run

$ heatsink wrap -- go test ./...
heatsink: throttled -> go test ./... -p 2
...your test output, unchanged...

$ heatsink wrap -- npx vitest run     # when denied
heatsink: DENIED — load 21.4 on 10 cores (thermal: ok) — oversubscribed.
Wait for load to drop, or run with reduced parallelism.
$ echo $?
75

In a Makefile or CI job:

test:
	heatsink wrap -- npx vitest run

hook — what the agent actually sees

The adapters pipe the harness's hook JSON in and emit the harness's own response format back out. Claude Code, throttle case:

$ echo '{"tool_input":{"command":"npx vitest run"}}' | heatsink hook claude-code
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "updatedInput": {
      "command": "npx vitest run --maxWorkers=2"
    }
  },
  "systemMessage": "heatsink: load 9.5 on 10 cores — throttled to 2 workers. -> npx vitest run --maxWorkers=2"
}

Deny case — the command never reaches the shell, and the agent is told why:

$ echo '{"tool_input":{"command":"cargo build"}}' | heatsink hook claude-code
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "heatsink: load 21.4 on 10 cores (thermal: ok) — oversubscribed. Wait for load to drop, or run with reduced parallelism. Check with: heatsink doctor"
  },
  "systemMessage": "heatsink: blocked a heavy command (load 21.4 on 10 cores)"
}

warn returns additionalContext instead — the command runs, but the agent is told not to stack more parallel work on top of it.

Tuning, in practice

Thresholds are env vars, so you can tighten them per shell or per job:

# aggressive: throttle at half a core per job, deny at 1.2x
$ HEATSINK_THROTTLE_RATIO=0.5 HEATSINK_DENY_RATIO=1.2 heatsink wrap -- make -j 16

# never rewrite below 4 workers
$ HEATSINK_MIN_WORKERS=4 heatsink check --command "npx jest"
throttle: npx jest --maxWorkers=4

doctor — why is my machine hot?

heatsink doctor showing orphaned burners

doctor shows your load-to-core ratio, the top CPU consumers on the box, and separately calls out orphans: processes whose parent process is gone but that are still spinning (the reap-safe signal — a process that's still attached to a live parent is never a reap candidate).

reap — clean up the orphans

heatsink reap          # report-only: lists orphaned burners, kills nothing
heatsink reap --kill   # SIGTERM, wait, SIGKILL any survivors

heatsink reap listing orphaned burners, report-only

reap defaults to report-only on purpose — a guard that can autonomously kill your processes is a guard nobody will trust. You always have to ask for --kill explicitly.

Install

Prerequisites: bash and jq. macOS 15+ ships jq; on Linux install it first (apt install jq / dnf install jq). Without jq the hook adapters fail open — heatsink will appear installed but never guard anything.

macOS, Linux and Windows are all supported — see Windows for what that means there.

Claude Code (plugin):

/plugin marketplace add sundarshahi/heatsink
/plugin install heatsink@heatsink

The plugin wires itself into Claude Code's PreToolUse hook automatically — no further setup.

CLI (any agent, any shell):

git clone https://github.com/sundarshahi/heatsink
cd heatsink
make install   # installs to ~/.local/bin

Make sure ~/.local/bin is on your PATH (export PATH="$HOME/.local/bin:$PATH" — it isn't by default on macOS), then verify with heatsink doctor.

CLI (npm):

npx @sundarshahi/heatsink doctor      # run without installing
npm install -g @sundarshahi/heatsink  # or install globally

Still needs jq on your PATH — npm ships the scripts, not the dependency.

Windows

heatsink is bash, so it runs under Git Bash (Git for Windows) — which Claude Code on Windows already requires. Install with npm, not make install: npm's shim invokes the script through bash on your PATH, while make install relies on symlinks that MSYS may turn into copies.

winget install Git.Git jqlang.jq
npm install -g @sundarshahi/heatsink
heatsink doctor

Two signals are read differently there, because Windows has no POSIX equivalent:

| Signal | Windows source | |---|---| | load | ProcessorQueueLength + busy cores, via one powershell.exe call — the same quantity Linux's loadavg measures, sampled instantaneously instead of decayed over a minute | | processes | Win32_Process, sampled twice 300ms apart for a real %CPU; a process whose parent has exited is reported as ppid 1, so reap finds orphans the same way it does elsewhere | | thermal | always ok — see below | | kill | taskkill, since MSYS signals never reach native Windows processes |

Cygwin's /proc/loadavg is deliberately ignored: it exists on Git Bash but does not track the Windows scheduler, so trusting it would report a calm machine while it cooks.

The powershell.exe spawn costs a few hundred ms, so heatsink classifies the command before reading load — a git status never pays it, and check --json reports "load": null when it never needed to measure.

Everything else is identical: same rewrite table, same thresholds, same adapters, same fail-open behavior. The Claude Code plugin works unchanged.

Prefer WSL2? That works too, and is the plain Linux path — but run the agent inside WSL as well. An agent on the Windows side with heatsink in WSL guards nothing: different process tree, different load.

Adapter status

| Adapter | Status | |---|---| | claude-code | tested | | cursor | untested in the wild | | codex | experimental | | generic wrap | tested |

cursor is built against Cursor's documented beforeShellExecution hooks contract but isn't exercised in CI against a real Cursor install. codex speaks a minimal stdin/stdout contract since Codex's own hook surface isn't stable yet. If either misbehaves against a real harness, please open an issue with the raw hook input/output — that's exactly the feedback needed to move them out of "untested."

The generic wrap command works with anything that can shell out — CI jobs, Makefiles, pre-commit hooks, or an agent with no native hook surface at all:

heatsink wrap -- bundle exec rspec spec/

Tuning

heatsink's thresholds are load-ratio-based (load average ÷ core count), and every one of them is an environment variable:

| Variable | Default | Meaning | |---|---|---| | HEATSINK_DENY_RATIO | 2.0 | load/cores at or above this → deny (oversubscribed) | | HEATSINK_THROTTLE_RATIO | 0.9 | load/cores at or above this → throttle/warn zone | | HEATSINK_MIN_WORKERS | 2 | floor on the worker count heatsink will rewrite down to (target is max(MIN_WORKERS, cores/4)) |

Why no temperature on macOS or Windows

macOS exposes no root-free CPU thermometer. The only interface that reports actual junction temperature, powermetrics, requires sudo — and a guard that has to ask for sudo to make a safety decision is a guard that will get disabled the first time it's inconvenient. heatsink never asks for elevated privileges.

Instead, on macOS heatsink governs load — the thing that's actually causing the problem — and reads the OS's own throttle signal when it surfaces one (pmset -g therm, checking CPU_Speed_Limit). If macOS itself is throttling the CPU, heatsink treats that as thermal pressure and escalates the throttle-zone verdict straight to deny.

Windows is worse: MSAcpi_ThermalZoneTemperature is the only vendor-neutral interface, most laptop firmware doesn't implement it, and where it does the reading is often the chassis rather than the CPU. So Windows reports ok and load governs alone, exactly as on macOS.

On Linux, no such workaround is needed: /sys/class/thermal/thermal_zone*/temp is readable without elevated privileges, so heatsink reads real per-zone temperatures directly and treats 85°C or above as thermal pressure.

Known limitation

heatsink classifies and rewrites a command as a single unit. For a compound command (cmd1 && cmd2), only the first pattern that matches in heatsink's classifier gets rewritten — the rest of the chain runs verbatim, untouched, whether or not it's also heavy. If you're chaining multiple heavy commands together, either wrap them separately or expect only one leg of the chain to be throttled.

Fail-open guarantee

Every layer of heatsink is designed to fail open. If jq isn't installed, if stdin isn't valid JSON, if a signal read fails, if anything at all goes wrong — heatsink gets out of the way and your command runs exactly as you typed it. A guard that can break your workflow by breaking itself is worse than no guard at all.

Development

make lint   # needs shellcheck:  brew install shellcheck  /  apt install shellcheck
make test   # needs bats:        brew install bats-core   /  apt install bats

License

MIT — see LICENSE.