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

@hearthdev/napi

v0.3.0

Published

Node.js bindings for Hearth: cancellable read/write/edit/bash/grep/find/graph on one resident engine

Readme

@hearthdev/napi

Node.js bindings for Hearth — one resident engine serving read, write, edit, bash, grep, find, and graph from shared warm caches and a warm shell pool.

Prebuilt binaries ship for macOS (arm64, x64) and Linux (x64, arm64 glibc); no Rust toolchain is needed to install. The package is ESM-only and exposes named exports for Node.js 18+ and Bun; CommonJS require() is not supported.

npm install @hearthdev/napi

Usage

import { HearthEngine } from "@hearthdev/napi";

// Construct one per process and keep it: the caches only pay off while it lives.
const engine = new HearthEngine({ cwd: process.cwd(), warmShell: true });

const file = await engine.readAsync({ path: "src/main.rs" });

const paths = await engine.findAsync({
  pattern: "**/*.rs",
  path: ".",
  limit: 1000,
  excludeGlobs: ["**/node_modules/**", "**/.git/**"],
}); // { paths: ["src/lib.rs", ...], totalMatches, walkCacheHit, ... }

const found = await engine.grepAsync({
  pattern: "TODO",
  path: "src",
  mode: "content",
  maxTotalCount: 100,
});

const edited = await engine.editBatchAsync({
  path: "src/main.rs",
  edits: [
    { oldText: "fn old_name", newText: "fn new_name" },
    { oldText: "old_name()", newText: "new_name()" },
  ],
});

const result = await engine.bashStream(
  { command: "cargo build", timeoutMs: 120_000 },
  (chunk) => process.stdout.write(chunk.text),
);

Cancellation

Every async method takes an optional AbortSignal:

const controller = new AbortController();
const search = engine.grepAsync({ pattern: "needle", path: "." }, controller.signal);
controller.abort();

An already-aborted signal rejects before any work starts. An abort mid-flight stops the native work at its next safe point: grep joins every worker, find polls every warm-snapshot candidate (a cold bounded walk completes as one non-preemptive step), bash kills the command's whole process group, and a file mutation keeps its per-path lock until its bytes are committed — so when the promise settles, nothing is still running.

bash is the exception to rejecting: an abort or a timeout resolves with aborted/timedOut set and the partial output intact, so a caller keeps what it already rendered.

Dependency prefetch (integration primitive)

graphPrefetch and its cancellable worker-thread twin graphPrefetchAsync let an integration adapter opportunistically warm explicitly observed source files and their resolved, in-root direct imports:

const warmed = await engine.graphPrefetchAsync(
  { root: process.cwd(), files: ["src/current.ts"] },
  controller.signal,
);

This is an integration-only primitive, not an agent-facing graph operation. It does no directory walking and discovers or parses no ignore files; only the supplied seeds and dependency targets resolved from those seeds are candidates. Consequently hidden and respectGitignore exist only for graph-call option parity and do not filter explicit/resolved paths. Canonical root containment, supported-language checks, regular-file checks, UTF-8 checks, and symlink policy still apply.

Retention is opportunistic: warmed file-cache entries and graph roots remain subject to the engine's normal bounded eviction and invalidation. The result therefore reports file-cache reuse (cacheHits) separately from graph-state changes (graphUpdates); one does not imply the other. skips accounts for individual candidates and truncated reports budget truncation.

Native hard caps remain authoritative: 32 seeds, 64 imports examined per seed, 256 unique direct targets, 2 MiB per file, and 16 MiB total source per request. The optional maxSeeds, maxTargetsPerSeed, maxTargets, maxFileBytes, and maxTotalBytes fields may only reduce those limits. They must be non-negative JavaScript safe integers; invalid numeric values are rejected instead of being coerced.

Prefetch is exposed only through the native Rust and N-API integration surfaces. It adds no GraphOp, daemon request, CLI command, or daemon protocol change.

Errors

Every failure — a synchronous throw or a promise rejection — is a JS Error carrying the structured fields to branch on:

  • code: the stable kind tag, one of notFound, permission, noMatch, multipleMatches, overlap, noChange, invalidInput, timeout, cancelled, indeterminate, io, internal
  • editIndex: the 0-based index of the failing replacement, when one editBatch edit is at fault
  • path: the file involved, when one is

The message still leads with "<kind>: ", but that is presentation — the properties are the contract. Never parse the message.

indeterminate is the one that needs care: it means a mutating command reached a warm shell and the shell then died before reporting a result. Hearth guarantees at-most-once execution, so it never retries such a command — and neither should the caller without checking what actually happened.

Pi find adapter

find returns deterministic search-root-relative POSIX paths and marks directories with /. It accepts the exclusion list Pi supplies to custom glob operations before applying count/output limits:

import { access } from "node:fs/promises";

const operations = {
  exists: async (path: string) => {
    try { await access(path); return true; } catch { return false; }
  },
  glob: async (pattern: string, root: string, options: { ignore: string[]; limit: number }) =>
    (await engine.findAsync({
      pattern,
      path: root,
      limit: options.limit,
      excludeGlobs: options.ignore,
    })).paths,
};

Patterns without / match basenames at every depth. Slash patterns follow Pi's full-path **/ transform; matching is fd-compatible smart-case and an empty pattern matches all entries. The result reports limitReached and outputLimitReached separately and always gives exact totalMatches. When the joined text crosses 50 KiB, paths includes that first complete crossing path so Pi's wrapper detects the overflow and emits its standard warning. Hearth's ignore discovery is deliberately bounded to root-local .ignore/.rgignore; it does not inherit ancestor/global Git configuration like Pi's bundled fd. Exclusions post-filter the shared snapshot, so they do not reduce cold-walk work or its safety budget. Use findAsync in the adapter so a cold walk runs off the JavaScript event loop. Pi's current custom glob hook does not pass its AbortSignal through options, so that adapter cannot forward cancellation; direct findAsync(params, signal) callers retain the full cancellation contract.

Cache coherence

trustCache: true skips the per-read freshness stat, which is where most of the warm-read speed comes from. It assumes Hearth is the only writer: changes made outside it stay cached until invalidated.

engine.invalidatePath("/abs/path/to/file");  // one file
engine.invalidateRoot("/abs/path/to/dir");   // everything beneath a directory
engine.clearCaches();                        // everything

After a shell command, invalidateRoot(cwd) is the sound choice: an arbitrary command can create, delete, rename or rewrite anything under its working directory. This also refreshes find's warm structural snapshot; without watching or explicit invalidation, out-of-band file and empty-directory changes remain absent/present in that snapshot.

Documentation

MIT licensed.