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

@kuiralabs/midnight-cli-core

v0.1.0

Published

The base of the Midnight CLI packages: arguments, typed errors and exit codes, JSON output, config, the state root, network endpoints, terminal formatting, the command registry contract, the payer and the SDK engine interface

Readme

@kuiralabs/midnight-cli-core

The base of the Midnight CLI packages: the host a program passes to them, typed errors and their codes, networks, settings and the state root, the wallet session, running a command from its definition, and the engine contract the SDK engines are built against.

Install

npm install @kuiralabs/midnight-cli-core @kuiralabs/midnight-engine-rc

Install with npm 11 or older (npm 12 no longer reads a package's shrinkwrap): the engine ships an npm-shrinkwrap.json that pins its SDK set, and npm 11 honours it. bun's installer ignores it and resolves the packages again, so an engine bun installed refuses to load, before any SDK module does, with MIXED_SDK_LAYOUT and the npm command; bun runs what npm installed. A program that runs the SDK installs the engine too, @kuiralabs/midnight-engine-rc, for every network.

Entry points

| Import | What it holds | |---|---| | @kuiralabs/midnight-cli-core | The host (createHost), typed errors and classifyError, networks, settings, amounts, arguments (parseArgs), the command registry (createPackageRegistry), running a command (runCommand, captureCommand), the wallet session (openWalletSession) | | @kuiralabs/midnight-cli-core/commands | core's own command (config) as a registry entry (coreCommands), its codes (coreCodes) and the code sets other packages' commands name | | @kuiralabs/midnight-cli-core/process | What a program does to its own process: installSdkProcessGuard, syncHeapAdvice | | @kuiralabs/midnight-cli-core/ui | Terminal formatting, spinners and the terminal prompter mn uses | | @kuiralabs/midnight-cli-core/engine | The engine contract, for the engine packages |

Every export and its signature is in api-report.md.

Codes

Each error a package throws carries a code; classifyError(err, registry) gives its code and exit code by that code alone. These are core's own; each package lists its own in its README.

| Code | Exit | What went wrong | What to do | |------|------|-----------------|------------| | INVALID_ARGS | 2 | Missing/invalid argument. | Show the message to the user verbatim — it names the missing field. | | NETWORK_ERROR | 4 | Indexer/node/proof-server connection refused or DNS failure. | Check midnight_localnet_status() (start it with midnight_localnet_up()) or the endpoints the user configured. | | SYNC_TIMEOUT | 4 | Long-running wallet sync hit its deadline. Common on a hosted-network first-cold-sync. | Retry; the cache resumes from the last applied event so progress is preserved. | | WALLET_BEHIND_FORK | 4 | A transaction can't be built yet: one of the wallet's three wallets (shielded, unshielded, Dust) hasn't synced past the chain's fork to ledger 9, and the wallet acts at the lowest of their versions. | Retry; mn reads the shielded and Dust history past the fork first, and the next run starts those wallets there. Nothing was sent. | | INSUFFICIENT_BALANCE | 5 | Wallet doesn't have enough NIGHT to cover the requested amount. | Show the user. On localnet: midnight_airdrop({ amount: "..." }). On hosted networks: tell user to fund the address. | | TX_REJECTED | 6 | Chain rejected the submitted transaction (catch-all when no more specific code applies). | Read the message; usually a state mismatch resolved by re-sync + retry. | | STALE_UTXO | 6 | UTXO was already spent on-chain (substrate "error code 115"). | Re-sync (the next call will refresh state) and retry. | | STALE_CACHE | 6 | Local wallet cache disagrees with chain (e.g. localnet reset, remote testnet re-indexed). | midnight_cache_clear({ wallet: "<name>" }) then retry. | | PROOF_TIMEOUT | 6 | ZK proof generation didn't finish within the deadline. | Retry — proofs can be slow under load. | | PROOF_FAILURE | 6 | Proof server rejected or failed to generate the ZK proof. Often a stale commitment tree or unreachable proof server. | Check that the proof server is running (midnight_localnet_status(), or the endpoint the user configured); if it is, midnight_cache_clear({ wallet: "<name>" }) then retry. | | NOT_A_PROOF_SERVER | 4 | The proof server URL answers, but not as a proof server: its /version gives no version (another service on that port, a proxy that answers everything). | Ask the user to point proof-server at a running proof server (midnight_config_set), or start one; nothing was built or sent. | | INVALID_DUST_PROOF | 6 | Chain rejected the dust spend proof as malformed (substrate "Custom error 170"). Stale commitment tree. | midnight_cache_clear({ wallet: "<name>" }) then retry. | | CONTRACT_STATE_UNREADABLE | 1 | A contract's state on chain can't be decoded by this mn's ledger: the state the chain holds is at fault, not the user's setup. The message names the contract and includes what the ledger said. | Tell the user the contract's on-chain state is unreadable (show the message); don't retry, and don't suggest clearing caches or changing endpoints. Reading or calling that contract can't work until its state is fixed on chain. | | FEE_TOO_LOW | 6 | The node refused the fee as too low even after mn priced it again with its largest margin (8 blocks of rising prices): prices are climbing faster than that, as when blocks are full one after another. | Nothing was spent. Wait a minute and retry; don't retry in a loop. | | FEE_REPRICED | 6 | mn serve's node refused a transaction's fee as too low; mn serve now prices fees with more margin. The transaction keeps its old fee. | Balance the transaction again and submit it (mn contract deploy and call do this themselves). Nothing was spent. | | DUST_DOUBLE_SPENT | 6 | Another transaction from the same wallet spent the Dust coin this one pays with first (node error 196, DustDoubleSpend): two payments from one wallet at once. mn builds a transfer or a contract call again by itself, at most twice; mn serve answers it to the dApp to balance again. | Nothing of the refused transaction was spent. Run it again once the wallet's other transactions have landed; to avoid it, don't pay from one wallet in parallel. | | TX_INVALIDATED | 6 | The node accepted the transaction into its pool, then dropped it as invalid while building a block ("Transaction is invalid and was rejected by the node", no error code): another transaction from the same wallet, sent at the same time (another mn, script or machine with the same seed), spent the same Dust or coin and went in first. mn builds a transfer or a contract call again by itself, at most twice; mn serve answers it to the dApp to balance again. | Nothing of the dropped transaction was spent. Run it again once the wallet's other transactions have landed; to avoid it, don't pay from one wallet in parallel. | | DUST_REQUIRED | 5 | Wallet has NIGHT but not enough DUST for the fee. | Not registered (midnight_dust_status() shows registered: false): run midnight_dust_register(). Registered: its NIGHT is still generating the fee, which happens with time. mn has already waited when the fee was a few minutes away; the message says how long the rest takes ("generates the rest in about 4 minutes"). Wait that long and retry, don't retry at once. | | DUST_GENERATING | 5 | A Dust registration can't pay its fee yet: the wallet's NIGHT hasn't generated enough Dust. | The message says when: run midnight_dust_register() again then, or add NIGHT. | | AMBIGUOUS_LOCAL_STACKS | 4 | Several local Midnight stacks are running and no endpoints are configured, so mn won't guess. | Ask the user which stack to use; they set its endpoints with midnight config set. Don't retry blindly. | | UNSUPPORTED_NODE | 1 | Node.js is older than 22 (22.18 for mn grant). | Ask the user to install a current Node.js. | | WRONG_PASSPHRASE | 2 | MN_PASSPHRASE is not the passphrase mn's secrets were encrypted with, or a sealed file was changed. | Ask the user to correct MN_PASSPHRASE in the MCP server's environment. Nothing was changed. | | PASSPHRASE_REQUIRED | 2 | A tool needed a secret (a wallet's seed, an account's owner key) and no passphrase could be had: MN_PASSPHRASE is unset and there is no terminal to ask at (the MCP server has none). | Ask the user to set MN_PASSPHRASE in the MCP server's environment and restart it. Never ask them to type the passphrase into the chat. | | BLOCKFROST_PROJECT_ID_REQUIRED | 4 | The network (preprod or mainnet) is served by Blockfrost and no project id is set for it. | Ask the user to create a Blockfrost project for that Midnight network at https://blockfrost.io (a preprod id starts with nightpreprod, a mainnet id with nightmainnet) and run midnight config set blockfrost-project-id <project id> themselves. Never ask them to paste the id into the chat. Don't retry until they have. | | BLOCKFROST_PROJECT_ID_REJECTED | 4 | Blockfrost refused the project id (HTTP 403): mistyped, revoked, or for another network. | Ask the user to check it at blockfrost.io and set it again. Don't retry blindly. | | BLOCKFROST_LIMIT_REACHED | 4 | Blockfrost's plan limit or rate limit. | Wait and retry once; if it persists, tell the user it is their Blockfrost plan. | | ENDPOINT_SHUT_DOWN | 4 | An endpoint set by flag or config was shut down by its operator (the old preprod indexer, mainnet's old Midnight-hosted indexer and node). | midnight_config_unset({ key: "indexer-ws" }) (and node if set), then retry. | | MAINNET_REFUSED | 2 | The command is refused on mainnet before anything runs: an airdrop (mainnet has no faucet), a test suite (it deploys throwaway contracts), or mn grant (its account build has been tested on a local network only). | Tell the user why (the message says) and don't retry on mainnet; offer undeployed instead (for mn grant, the only network it runs on), or a test network for the others. | | MIXED_SDK_LAYOUT | 1 | mn's packages were installed without the engine's shrinkwrap (bun's installer): the engine's SDK tree isn't the one it was tested with. | Ask the user to install mn with npm. | | NO_STATE_ROOT | 1 | The host gave mn no place to keep its state (a program's createHost without paths). | Give the host its paths (createHost({ paths })). | | NO_PAYER | 1 | A command that submits a transaction was called without a payer. | Give the call a payer. | | COMMAND_FAILED | 1 | A command ended with a non-zero exit code without throwing (an MCP test run whose tests failed); the message carries its result. | Read the command's result in the message. | | NOT_AN_OFFER | 2 | The bytes given as a swap offer are not one. | Ask the maker for the offer again. | | TERMS_MISMATCH | 2 | A swap offer's own terms differ from the agreed ones. | Don't take it; agree the terms again with the maker. | | OFFER_EXPIRED | 2 | A swap offer is past the expiry its maker set. | Ask the maker for a new one. | | OFFER_COINS_IN_USE | 2 | A spend would use coins an open swap offer of this wallet's holds, cancelling it. | Cancel the offer first (mn swap cancel), or wait until it is taken. | | OFFER_RECORD_UNREADABLE | 1 | mn's record of a wallet's open swap offers (~/.midnight/swap-offers/<network>/<address>.json) can't be read: not JSON, or not the shape mn writes. | Show the message: it names the file. Ask the user to restore it, or to move it aside (mn then forgets those offers). Don't retry until they have. | | CANCELLED | 7 | Operation was aborted (SIGINT, or the MCP call was cancelled). | A transaction already submitted may still have landed: read the state first (midnight_grant_status, midnight_balance). Don't retry without confirming with the user. | | UNKNOWN | 1 | Error didn't match any known classifier. | Surface the message to the user. If reproducible, a new code may be warranted. |

Mounting commands in your program

Each package's commands are definitions a program mounts in its own command line: the registry is built from the packages you choose, and runCommand does around each handler what mn's shell does. The host gives the commands your state root, your settings, where notices go and, when a command asks a question, your prompter.

import { homedir } from 'node:os';
import { join } from 'node:path';
import { classifyError, createHost, createPackageRegistry, parseArgs, runCommand, settingsFromEnvironment, UsageError } from '@kuiralabs/midnight-cli-core';
import { coreCommands } from '@kuiralabs/midnight-cli-core/commands';
import { createRcEngine } from '@kuiralabs/midnight-engine-rc';

const registry = createPackageRegistry([coreCommands], []);
const host = createHost({
  paths: { stateRoot: join(homedir(), '.my-app'), home: homedir() },
  settings: () => settingsFromEnvironment(process.env),
  notice: (line) => console.error(line),
  engineLoader: async (_set, host) => createRcEngine(host),
});
const output = { result: (data) => console.log(JSON.stringify(data)), stdout: (text) => process.stdout.write(text), stderr: (text) => process.stderr.write(text) };

const args = parseArgs(process.argv.slice(2));
try {
  const definition = registry.find(args.command ?? '');
  if (definition === undefined) throw new UsageError(`Unknown command: ${args.command}`);
  const outcome = await runCommand(definition, args, { host, output });
  process.exitCode = outcome?.exitCode ?? 0;
} catch (err) {
  const { errorCode, exitCode } = classifyError(err, registry);
  console.error(`${errorCode}: ${err.message}`);
  process.exitCode = exitCode;
}

node my-cli.mjs config set network preview writes ~/.my-app/config.json, never mn's ~/.midnight. Add a package's commands by adding its entry to the registry (walletCommands, chainCommands and so on; each package's README shows its own); a command that pays (contract, swap, grant, dev) takes a payer in the run, such as connect's servePayer. For an MCP server, captureCommand(tool.command, toolArgv(tool, params), { host, text }) runs a tool's command and returns its JSON result, and toolDescription(registry, tool) gives the description to list, with the codes the tool can end with. A test in mn runs the block above as it is written.

Your process: what mn's shell does that you must do

mn's shell does a few things to its own process so that the wallet SDK can run in it. A library never does them: it would be changing a process that is yours. When your program runs the SDK through these packages, the things below are yours to do, and core gives you the parts you can't write yourself in its ./process entry point. Nothing in ./process runs when you import it; you call it, once, at start.

The setup

import { homedir } from 'node:os';
import { join } from 'node:path';
import { createHost, openWalletSession } from '@kuiralabs/midnight-cli-core';
import { installSdkProcessGuard, syncHeapAdvice } from '@kuiralabs/midnight-cli-core/process';
import { createRcEngine } from '@kuiralabs/midnight-engine-rc';

const guard = installSdkProcessGuard();
const heap = syncHeapAdvice();
if (heap) console.error(`A first sync may need more heap: start with NODE_OPTIONS=${heap.nodeOptions}`);
const stateRoot = join(homedir(), '.my-app');
const host = createHost({ paths: { stateRoot, home: homedir() }, notice: (line) => console.error(line), sdkWarnings: guard.calls, engineLoader: async (_set, host) => createRcEngine(host) });
const cancel = new AbortController();
process.once('SIGINT', () => cancel.abort());

const session = await guard.calls.run((sdkWarnings) => openWalletSession({ seed: Buffer.from(process.env.WALLET_SEED, 'hex'), network: 'preview', root: stateRoot, notice: host.notice, engines: host.engines, sdkWarnings, signal: cancel.signal, onMessage: (line) => console.error(line) }));
try {
  console.log(await session.balances());
} finally {
  await session.close();
}

This opens a wallet on preview, prints what it holds, closes it, and ends by itself. WALLET_SEED is the wallet's seed in hex: 64 bytes from a mnemonic, or 32 raw. A test in this package runs the block above as it is written here, with only the engine standing in.

What each part does

A. The SDK's transient rejections. During a sync the wallet SDK lets transient failures (Wallet.Sync: Internal Server Error, a dropped indexer socket) escape as unhandled promise rejections while it retries the work itself. Node exits 1 on an unhandled rejection, so without the guard your program dies in the middle of a sync. installSdkProcessGuard() installs one unhandledRejection handler: a transient Wallet.* rejection goes to the call that is listening for the SDK's warnings, and the program goes on. With no call listening, it goes to onWarning (a line on console.warn unless you give one). Any other unhandled rejection is printed and exits 1, as before. mn installs it with outsideCalls: 'exit', which exits 1 on a transient rejection no call listens for; that is mn's choice, not the default. guard.uninstall() takes the handler off again.

B. The SDK's console noise. The SDK prints its retried failures as stack traces on console.error, and polkadot-js prints a RPC-CORE line for each failed submit. installSdkProcessGuard({ console: true }) keeps both off the console while a call listens, handing the traces to that call as warnings; at any other time the console is untouched. It is off by default, because it replaces console.error and console.warn. polkadot-js's API-WS lines (a node connection lost and retried) are not filtered.

C. Which call a warning belongs to. guard.calls.run(work) runs work as one call and gives it its sdkWarnings; a warning raised while that call runs goes to that call alone. Pass them to openWalletSession: the session listens from the open until close(), and with onMessage you get each warning as a line, SDK: <message>. Pass guard.calls to createHost too, so library calls made through the host listen the same way.

D. The heap a cold sync needs. The first shielded or Dust sync of a wallet against a hosted network streams the chain's history through the SDK: its live working set stays near 300 MB, but short-lived objects took the heap to about 8.4 GB in a cold shielded sync measured on preprod. Under Node's default old-space limit (about 4 GB) the process can die with JavaScript heap out of memory before it writes the cache that makes every later sync cheap. The limit is fixed when Node starts, so no library can raise it. syncHeapAdvice() says whether this process's limit is too small on this machine and, if so, the flag to start with (--max-old-space-size=16384, or 70% of the machine's memory if that is less); it returns null when the limit will do. Start your program with that flag in NODE_OPTIONS, or re-run it with the flag before it loads the SDK, as mn's shell does. syncHeapTargetMb(options) is the same computation with every input given.

E. Ctrl-C. mn aborts the running work on SIGINT and SIGTERM and exits 5 seconds later whatever is still running. In a program, make one AbortController, abort it on the signal, and pass its signal to every call that takes one: an open that is cancelled stops what it started and rejects with CancelledError. Exiting after a grace period is your choice to make.

F. Ending the process. await session.close() reverts what the session built but never submitted, stops the wallet and closes what the engine opened: the indexer's sockets, the node client, the timers. After it, a program with nothing else to do ends by itself. mn still calls process.exit after a command that ran the SDK; a program does not have to.

G. Blockfrost project ids. Preprod's endpoints carry a Blockfrost project id in their URLs, and URLs end up in error messages and progress lines. mn hides them in everything it prints. Pass anything you print or log through redactProjectId(text) from this package; libraries you don't control (polkadot-js prints the URL it lost) write to the console themselves, so to catch those too, wrap your output streams as mn does.

Node and bun

The guard works on both. On Node, a warning keeps the identity of the call it was raised in through the SDK's WebSocket callbacks, timers, RxJS and Effect fibers. Under bun (1.3) the identity is lost on everything resumed from a WebSocket: such a warning goes to the one call listening, or, with several listening, to none of them (never to the wrong one); either way it stays off the console while a call listens.

The heap advice is Node's: under bun syncHeapAdvice() returns null, because bun's heap is JavaScriptCore's and neither NODE_OPTIONS nor --max-old-space-size sets it. How much memory a cold sync takes under bun has not been measured.

Troubleshooting

  • Unhandled rejection: with a Wallet.Sync error, then exit 1. The guard isn't installed, or it was installed with outsideCalls: 'exit' and no call was listening. Install it before the first sync, and open the session inside guard.calls.run with sdkWarnings.
  • JavaScript heap out of memory during the first sync. Start with the NODE_OPTIONS that syncHeapAdvice() names. Later syncs start from the cache and need far less.
  • The program doesn't end after it is done. A session was not closed: every openWalletSession needs its await session.close(), also when the work in between threw (use finally). On Node, process.getActiveResourcesInfo() lists what is still open. If the SDK's wallets don't stop within 5 seconds the engine gives up waiting and what they hold stays open; then exit explicitly, as mn does.
  • Warnings reach the console while a session is open. Without console: true the guard leaves the console alone, so the SDK's traces print there; pass console: true to keep them off while a call listens.