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

@chrismessina/raycast-kit

v0.2.0

Published

House-style primitives for Raycast extensions: failure toasts that always carry a Copy Error action, safe error unwrapping, and count-aware copy

Readme

@chrismessina/raycast-kit

House-style primitives for Raycast extensions. Zero runtime dependencies; @raycast/api is a peer.

Companion to @chrismessina/raycast-logger.

Install

npm install @chrismessina/raycast-kit

At a glance

import { showError, failToast } from "@chrismessina/raycast-kit";
import { getErrorMessage, isAbortError } from "@chrismessina/raycast-kit/errors";
import { countOf } from "@chrismessina/raycast-kit/plural";
import { formatBytes } from "@chrismessina/raycast-kit/bytes";

await showError(error, { title: "Couldn't Load Devices" });  // toast + Copy Error, redacted
failToast(toast, error, { title: "Export Failed" });         // same, on an existing toast
getErrorMessage(error);                                      // never "[object Object]"
countOf(1, "device");                                        // "1 device", not "1 devices"
formatBytes(1536);                                           // "1.5 KB"

One rule before your first import: UI code imports the root; pure logic and tests import the subpaths (/errors, /plural). @raycast/api is types-only and won't resolve outside the Raycast host — see Which entry point to import from.

What you get for free

showError and failToast both guarantee, by construction:

  • A Copy Error action. An error the user can't copy is an error they can't report.
  • Redaction before the text reaches the toast or the clipboard. Bearer tokens, labeled secrets, provider-shaped keys, JWTs, PEM blocks, AWS key ids, URL-authority credentials, emails. This is the one worth caring about even if you skip the rest — see below.
  • Aborts swallowed. A user typing the next keystroke cancels the in-flight request; that isn't a failure and shouldn't toast.
  • A clipboard payload richer than the toast — title, message, stack frames, and any request context you pass.

Why this exists

Every export here earned its place in a fleet audit (2026-07-25, 24 self-authored extensions, ~85k LOC). The rule wasn't "this might be handy" — it was "this is hand-written many times over, and often hand-written wrong."

| Pattern | Hand-written | Repos | | --- | --- | --- | | Toast.Style.Failure | 124 | 20 | | …of which carry a Copy Error action | 26 | 9 | | error instanceof Error ? … : … ternary | 98 | 16 | | Count-bearing copy (${n} items, item(s)) | 34 | 11 |

The toast row is the point. House Style requires every failure toast to offer a Copy Error action — an error the user can't copy is an error they can't report — and it was ~20% adopted, because the rule lives in a checklist while the ergonomic path (showFailureToast from @raycast/utils, which has no copy action) points the other way. raycast-ios-apps alone had 51 failure toasts and zero copy actions.

This package makes the compliant thing the easy thing. Three consequences worth knowing before the line-count argument:

  • It closes a latent secret leak. Nearly every hand-rolled failure toast in the fleet copies unredacted error text to the clipboard. A thrown SDK error routinely carries an authorization header or an x-api-key — one screenshot or pasted GitHub issue away from disclosure. showError redacts by default; the hand-rolled version can't, because nobody remembers to.
  • Zero runtime dependencies, @raycast/api as a peer. Nothing to justify to a Store reviewer on bundle size.
  • The compliant call is the shortest one. That's the mechanism — not discipline.

showError(error, options)

A failure toast with the Copy Error action already attached.

import { showError } from "@chrismessina/raycast-kit";

try {
  await loadDevices();
} catch (error) {
  await showError(error, { title: "Couldn't Load Devices" });
}

With a retry and request context on the clipboard:

await showError(error, {
  title: "Search Failed",
  action: { title: "Try Again", onAction: () => revalidate() },
  copyContext: `GET ${url} → ${response.status}`,
});
  • Copy Error is always primary; your action becomes secondary.
  • The clipboard gets more than the toast shows — title, message, your copyContext, and the stack frames when the thrown value had a stack.
  • Aborts are swallowed by default. A user typing the next keystroke cancels the in-flight request; that is not a failure and shouldn't toast. Pass ignoreAbort: false to opt out.
  • Returns the Toast (so you can mutate it later), or undefined for an ignored abort.

failToast(toast, error, options)

The progress-toast pattern — show an animated toast, then flip it when the work settles — cannot use showError, which creates a new toast. failToast mutates an existing one in place, with the same Copy-Error guarantee.

const toast = await showToast({ style: Toast.Style.Animated, title: "Exporting…" });
try {
  await exportIcons();
  toast.style = Toast.Style.Success;
  toast.title = "Exported";
} catch (error) {
  failToast(toast, error, { title: `Failed to export ${app.name}'s icons` });
}

Returns true if the toast was turned into a failure, false for an ignored abort (in which case the toast is left untouched). These mutation sites are exactly where Copy-Error actions went missing in the fleet — attaching one by hand costs six lines every time.

It clears a stale secondaryAction. A progress toast usually carries a "Cancel" for the work that just failed; leaving it attached offers an action that no longer means anything. Pass action to set a new one (typically "Try Again").

getErrorMessage(error: unknown)

The one canonical unwrap. Replaces the instanceof Error ternary, and handles what that ternary gets wrong.

getErrorMessage(new Error("Boom"))        // "Boom"
getErrorMessage("just a string")          // "just a string"
getErrorMessage({ message: "from API" })  // "from API"
getErrorMessage({ error: { message: "rate limited" } }) // "rate limited"
getErrorMessage({ a: 1 })                 // '{"a":1}'  — never "[object Object]"
getErrorMessage(new TypeError(""))        // "TypeError"  — never an empty toast
getErrorMessage(null)                     // "An unknown error occurred."

String(error) on a plain object yields "[object Object]", which is the useless message users actually report. This never does that.

Credentials are redacted, and output is capped. showError puts this text in a toast and on the clipboard, so it can end up in a screenshot or pasted into a GitHub issue. A thrown SDK error routinely carries an authorization header or an x-api-key — a realistic Anthropic 401 payload was putting a live sk-ant-… key into both before this was added. Masked: bearer tokens, labeled secrets (api_key/token/password/…), provider-shaped keys (sk-…, ghp_…, xoxb-…), JWTs, PEM private-key blocks, AWS access key ids, credentials in a URL authority (postgres://user:pw@host), and email addresses; output is clamped to 800 characters, because a 50 KB response body is not a toast. A message you pass yourself is redacted too — it routinely interpolates error text, and an unredacted toast is screenshot-able. redactSecrets is exported if you need it directly. This mirrors the redaction in raycast-logger — deliberately, since both packages protect the same secrets from the same payloads.

It cannot throw. Every property read goes through a guarded accessor: a hostile value ({ get message() { throw … } }, a Proxy that traps every read) returns the generic message rather than replacing the user's real failure with an unrelated one.

isAbortError(error: unknown)

try {
  await fetch(url, { signal });
} catch (error) {
  if (!isAbortError(error)) {
    await showError(error, { title: "Search Failed" });
  }
}

Recognizes AbortError, TimeoutError, and code: "ABORT_ERR" — verified against what Node's real fetch throws on an aborted AbortController, not just a synthetic error.

countOf(count, singular, options?)

Count copy that agrees across zero, one, and many. House Style prohibits "${n} item(s)" and always-plural "${n} items" (which says "1 items") in anything the user reads.

countOf(0, "device")                         // "0 devices"
countOf(0, "device", { zero: "No devices" }) // "No devices"
countOf(1, "device")                         // "1 device"
countOf(7, "device")                         // "7 devices"
countOf(2, "match")                          // "2 matches"
countOf(2, "city")                           // "2 cities"
countOf(2, "person")                         // "2 people"
countOf(1234, "item")                        // "1,234 items"

plural(count, singular, pluralForm?) is exported separately when you need only the noun.

On the -f and -o classes: these use allow-lists, not regex rules, because English splits them with no reliable pattern — leaf→leaves but chef→chefs, roof→roofs, belief→beliefs; potato→potatoes but cello→cellos, avocado→avocados. A naive -f$/-o$ rule produces "cheves", "rooves", "believes", and "celloes" — and a helper that mangles ordinary words is worse than the ternary it replaces. Anything outside the lists takes a plain -s; pass an explicit plural for the rest.

Which entry point to import from

| Import from | Gives you | Needs the Raycast runtime? | | --- | --- | --- | | @chrismessina/raycast-kit | everything (convenience barrel) | yes | | @chrismessina/raycast-kit/toast | showError, failToast, buildClipboardText | yes | | @chrismessina/raycast-kit/errors | getErrorMessage, isAbortError, redactSecrets | no | | @chrismessina/raycast-kit/plural | countOf, plural | no |

Why the split: @raycast/api ships types only — no loadable JavaScript. The Raycast host injects it at runtime, so it cannot resolve under plain node, tsx, or a test runner. The root export includes showError/failToast, which import it.

What breaks if you get it wrong: importing the root into a module you test headlessly fails to resolve @raycast/api — and the error names toast.js, never this package's export map, so it doesn't look like an import-path problem:

Error: Cannot find module '@raycast/api'
Require stack:
- node_modules/@chrismessina/raycast-kit/dist/toast.js
- node_modules/@chrismessina/raycast-kit/dist/index.js
- src/utils/your-pure-module.ts

The fix is one import path, not a local copy of the helper: switch that module to @chrismessina/raycast-kit/errors.

(This is real: it cost the first adopter all 11 of a module's headless fixtures and a hand-rolled duplicate helper before they found the subpath.)

Development

npm install
npm run build      # tsc → dist/
npm test           # node --test (83 tests)
npm run typecheck  # tsc --noEmit

Scope

Deliberately narrow. The audit also considered date formatting, text truncation, and number formatting and rejected all three: eight formatDate implementations across the fleet had eight different signatures — they shared a name, not a behavior — and consolidating them would have meant inventing a superset nobody wanted. truncate looked fleet-wide at 79 uses until 40 of them turned out to be in one extension.

Generic helpers belong in @raycast/utils, which 19 of 24 extensions already import. This package holds only what encodes a house-style rule that would otherwise depend on memory.

Empty-state components (List.EmptyView, 55 uses / 15 repos) are the strongest v0.2.0 candidate — they need JSX, and the collapsed-newline rule is worth encoding in a component.

License

MIT

formatBytes(bytes, options)

import { formatBytes, formatSpeed } from "@chrismessina/raycast-kit";

formatBytes(0);          // "0 B"
formatBytes(512);        // "512 B"    — bytes are always whole
formatBytes(1536);       // "1.5 KB"
formatBytes(1073741824); // "1.0 GB"
formatSpeed(1536);       // "1.5 KB/s"

| Option | Default | Effect | | --- | --- | --- | | base | 1024 | Divisor between units. 1000 matches macOS Finder. | | precision | 1 | Fraction digits for KB and above. Bytes stay whole regardless. | | trimZeros | false | 1 MB instead of 1.0 MB. |

The default is the developer-tooling convention, which is what Raycast's audience expects: base 1024, one decimal from KB up, whole bytes below. Note it is technically mislabelled — dividing by 1024 and calling the result KB is what KiB means. Pass base: 1000 when the number sits next to something the user can also read in Finder, which reports decimal, or the two will disagree by ~7% at GB.

The two conventions do not always render differently: at the default precision: 1 the 4.9% gap at MB rounds away and 1 MiB reads "1.0 MB" under both. It shows at GB ("1.0 GB" vs "1.1 GB") or at higher precision.

0, negatives, NaN and Infinity all return "0 B" rather than indexing the unit ladder out of bounds — Math.log(0) is -Infinity and Math.log(-1) is NaN, either of which otherwise puts "NaN undefined" on screen. The ladder clamps at PB.

formatSpeed takes the same options, so a progress line's rate and its total cannot silently end up on different bases.