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

cordierite

v0.7.0

Published

Driving React Native tools without in-app debug UI.

Readme

Cordierite

Drive app tools without shipping debug UI

MIT license npm downloads PRs Welcome

The cordierite package is the operator/agent side of Cordierite: a long-lived daemon that owns a TLS-terminated wss:// listener and any number of device sessions, a CLI and an MCP server that are both thin RPC clients of that daemon, and the key management that ties it all together — so developers, QA, and agents can steer app state from a terminal or an MCP-speaking client instead of a hidden in-app debug menu.

Why use this package

  • One daemon, many devices: cordierite auto-spawns its daemon on first use; it serves every connected device on one wss:// port and survives Metro reloads, backgrounding, and network flaps by suspending and resuming sessions instead of dying with them.
  • Same surface for humans and agents: the CLI (tools, invoke, events, ...) and cordierite mcp both talk to the daemon over the same RPC methods — an agent sees the same tools a human operator does.
  • Hardened control plane: the CLI/MCP never touch sockets, keys, or state files directly; everything goes through a Unix-domain-socket RPC surface gated by filesystem permissions (see docs/SECURITY.md).
  • Fits production-minded apps: the app still only exposes what you register; trust boundaries are pins + TLS, and production deployments can gate tools with policy and get every call audited (see docs/ARCHITECTURE.md §12).

Key setup

For a debug build app, you can skip this entirely: the daemon auto-generates its own key.pem the first time it starts if one isn't already there (mode 0600), and prints its sha256/... fingerprint on that first run. cordierite link (below) carries that fingerprint on the deep link itself for the app to pick up — see the app-side dev-mode flow in the root README and docs/SECURITY.md's "Dev trust mode" section.

For a release build (or any build where you want a real embedded pin instead of trusting whatever link shows up), generate a daemon key explicitly with:

cordierite keygen

The command writes an unencrypted PEM private key (PKCS#8) to <state-dir>/key.pem by default (override with --out; add --force to overwrite) and prints the exact sha256/... SPKI fingerprint your app should place into cliPins. It runs non-interactively — safe to call from CI or a setup script.

Commands

| Command | Role | | --- | --- | | cordierite keygen [--out <path>] [--force] | generate a daemon private key, print its app pin | | cordierite link [--ttl <s>] [--qr] [--open android\|ios-sim] [--scheme <s>] | mint a pending session and print its deep link | | cordierite ls | list sessions: alias, state, device, tool count | | cordierite tools [selector] [name] [--full] | list a session's tools, or show one tool's full schema | | cordierite invoke [selector] <tool> --input '<json>' [--timeout <ms>] | call a tool | | cordierite events [selector] [--follow] [--since <cursor>] | stream session/tool events (default), or one-shot pull everything retained since <cursor> (--since); --json emits NDJSON | | cordierite revoke [selector] | revoke a session | | cordierite daemon run\|start\|stop\|status | daemon lifecycle | | cordierite mcp | start a stdio MCP server proxying connected apps' tools to MCP clients | | cordierite doctor <artifact> [--assert-present\|--assert-absent] | release-gate step: report or assert whether a built .app/.ipa/.apk/.aab contains Cordierite |

Every command that targets a session accepts an optional selector (a session id or an alias from cordierite ls); omit it when exactly one session is active. Global flags: --json (machine-readable output), --no-color, --state-dir <path> (default ~/.cordierite). Run cordierite <command> --help for the exact flags of any command, or cordierite --help for the full list.

cordierite link's deep link is <scheme>:///?cordierite=<payload>&pin=<sha256/...>: the cordierite param is the existing binary v2 bootstrap payload (address, session id, token, expiry — unchanged), and pin is a separate, out-of-band query param carrying the daemon's current SPKI fingerprint for apps that want to pick it up. An app build with embedded cliPins ignores pin outright — embedded pins always win. A debug build with no embedded pins trusts it for that session only (see docs/SECURITY.md's "Dev trust mode" section); a release build with the module enabled never accepts it, embedded pins only.

Daemon lifecycle

The daemon auto-starts the first time any CLI or MCP command needs it — you don't normally run cordierite daemon start yourself. It writes its state to <state-dir>/ (daemon.sock, daemon.pid, daemon.log, key.pem, config.json, audit/), holds a single-instance lock via the pidfile, and keeps running independently of any one device's connection so a reload or crash on the device side never costs you the daemon process. Use cordierite daemon status to see what's running (version, pid, wssPort, pinned keys, live sessions, effective policy) and cordierite daemon stop to shut it down explicitly.

MCP setup (Claude Code, Cursor, and similar)

Add cordierite mcp to the agent's MCP server config — no separate server process to manage; it auto-spawns the daemon the same way the CLI does:

{
  "mcpServers": {
    "cordierite": {
      "command": "cordierite",
      "args": ["mcp"]
    }
  }
}

Once configured, the connected app's tools appear as MCP tools automatically: tools/list mirrors the live registry (namespaced <alias>__<name> when more than one session is active), and tools/call proxies straight to the app with progress and errors preserved. Two built-in management tools let an agent bootstrap a session without shell access: cordierite_connect mints a link (optionally delivering it directly to a booted android/ios-sim target), and cordierite_wait_for_session waits for that session to be claimed. Two more built-in tools give an agent a pull surface over postEvent()-pushed app events: cordierite_events drains everything retained since a cursor, and cordierite_wait_for_event blocks for a matching event (checking what's already retained before waiting live), rejecting with tool_timeout if none arrives in time.

Release gate: cordierite doctor

Cordierite's inclusion in a build is controlled entirely by autolinking exclusion (see the root README and docs/SECURITY.md) — there is no runtime debuggable check to catch a pipeline that forgot to exclude the package. cordierite doctor is the replacement: an artifact-level assertion you run against the thing you're about to ship, not the config you think produced it.

cordierite doctor ./build/MyApp.ipa --assert-absent
cordierite doctor ./build/app-release.apk --assert-absent

It inspects a built .app/.ipa/.apk/.aab for Cordierite's native code (the RCTNativeCordierite Objective-C class and the plugin-authored Info.plist keys on iOS; the com.callstackincubator.cordierite dex package and AndroidManifest.xml meta-data keys on Android) and reports present/absent — or, with --assert-present/--assert-absent, exits non-zero when the artifact doesn't match what you expected:

| Exit code | Meaning | | --- | --- | | 0 | inspection succeeded; no assertion was given, or the assertion held | | 3 | inspection succeeded, but --assert-present/--assert-absent did not hold | | 64 | usage error (bad flags, or an artifact extension that isn't .app/.ipa/.apk/.aab) | | 66 | the artifact could not be inspected — a required external tool (unzip) was missing, or the artifact was unreadable/corrupt. Never treated as "absent": a broken check must fail loudly, not rubber-stamp a release. |

CI snippet, run against your production release build right before you ship it:

- name: Assert Cordierite is excluded from the production build
  run: npx cordierite doctor ./build/app-release.apk --assert-absent
- name: Assert Cordierite is excluded from the production build (iOS)
  run: npx cordierite doctor ./build/MyApp.ipa --assert-absent

For an internally-distributed "testing" build that an agent drives, invert the assertion (--assert-present) so a forgotten inclusion — not just a forgotten exclusion — fails CI too.

Android detection has three signals, in order of reliability:

  1. CordieriteNativeMarker, kept unminified by the keep rule @cordierite/react-native ships via consumerProguardFiles. It reaches every consuming app's R8 run without the app authoring any rule, so it holds for bare-RN apps that never touch the Expo config plugin.
  2. The dex package name — survives ordinary minification but not R8 full mode's repackaging.
  3. The config plugin's AndroidManifest.xml meta-data keys — only present if the plugin ran.

Signals 2 and 3 are retained as fallbacks for artifacts built before the marker existed.

Verified against a real R8-minified release APK (assembleRelease -Pandroid.enableMinifyInReleaseBuilds=true): the R8 mapping shows sibling classes obfuscated (CordieriteBuildConfig -> …cordierite.a) while CordieriteNativeMarker keeps its fully-qualified name, and doctor --assert-present passes on that artifact.

Still not covered: R8 full mode with -repackageclasses was not exercised, and no artifact was tested from a bare-RN app that uses neither Expo nor the config plugin — the configuration the marker most directly targets. The marker should hold in both (a -keep rule prevents renaming and removal regardless of mode), but that is reasoning, not a measurement.

Programmatic use

Test runners: cordierite/client

A thin typed wrapper over the same daemon RPC the CLI and MCP server use — for a Jest/Vitest/Detox spec that wants to drive a running app without spawning cordierite invoke ... --json and parsing stdout:

import { connect } from "cordierite/client";

// auto-spawns the daemon and picks the single session, or pass { selector: "pixel-8" } to target one explicitly
const app = await connect();

await app.tools();                                  // ToolDescriptor[]
const { total } = await app.call("sum", { a: 2, b: 3 });
const { payload } = await app.waitForEvent("checkout_done", { timeoutMs: 5_000 });
app.close();

Errors surface as a CordieriteError whose type preserves the daemon's wire error type verbatim, so a test can assert on it directly instead of string-matching stderr:

await expect(app.call("checkout", {})).rejects.toMatchObject({ type: "policy_denied" });

Pair a simulator/emulator in a globalSetup without shelling out via link/waitForSession:

import { link, waitForSession } from "cordierite/client";

const { sessionId } = await link({ target: "ios-sim" });
const app = await waitForSession(sessionId, { timeoutMs: 60_000 });

app.call's tool name and args/result types can't be inferred automatically (tools are registered by the connected app at runtime) — declare your own tool map once for typed calls throughout a suite:

type Tools = {
  sum: { args: { a: number; b: number }; result: { total: number } };
};

const app = await connect<Tools>();
const { total } = await app.call("sum", { a: 2, b: 3 }); // typed

waitForEvent first drains the daemon's per-session retained buffer for an already-arrived match before falling back to a live wait, so it's safe to call after the action that emits the event too — no need to call it first. Pass since (the cursor from a previous app.events()/waitForEvent() call) to skip events already handled and wait only for a new one:

const { events, cursor } = await app.events();               // pull: what already happened
const next = await app.waitForEvent("checkout_done", { since: cursor });

Everything else: runCli

The package also exports runCli and command handlers from src/index.ts so you can embed the same behavior in Node or Bun scripts that need to drive Cordierite without shelling out.

Related packages

Documentation

Made with ❤️ at Callstack

cordierite is an open source project and will always remain free to use. If you think it's cool, please star it 🌟. Callstack is a group of React and React Native geeks, contact us at [email protected] if you need any help with these or just want to say hi!

Like the project? ⚛️ Join the team who does amazing stuff for clients and drives React Native Open Source! 🔥