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

@nidl/client

v0.7.0

Published

Drive a running nidl core from a Node program, over its local socket or its remote port.

Readme

@nidl/client

Drive a running nidl core from a Node program, over its local socket, or over HTTPS to its remote port on another machine.

import { connect } from "@nidl/client";

const nidl = await connect(); // the core serving ~/.nidl; or connect({ dataDir })
if (nidl.has("targets.list")) {
  const { targets } = await nidl.command("targets.list");
}
  • It never starts a core. When none is running, connect() fails with code not_running and says how to start one: nidl serve --data-dir ~/.nidl, or open the nidl app.
  • It checks the socket's folder first: each folder on the way is yours or root's and not writable by others, the control folder is 0700, the core's record is a private file describing this folder's core, and the socket is the one that record names. A check that fails throws code unsafe_folder. Node cannot ask the operating system who is at the other end of a socket, so this library cannot check the peer's user; the Python and Rust libraries can.
  • It is an app, unless you say it's the owner. By default it connects as the app client kind: it can read, draft, preview and apply, but the owner's commands, such as unlocking credentials or issuing an app token, are refused with requires_owner_action. A program that embeds nidl and commands it passes { owner: true } (see below).
  • Command names are typed. command() takes a CommandName, one of COMMAND_NAMES (the commands an app may run), or an OwnerCommandName, one of OWNER_COMMAND_NAMES, both generated from the core's registry. A misspelled name, pools.lend or a command that takes a secret is a type error. Call a command a newer core offers with a cast: nidl.command("its.name" as CommandName). Results are plain JSON.
  • Each call is one connection: the folder is checked, the core says hello, the command runs, the connection closes.

Errors are NidlErrors with a code for programs, a message for people, and an outcome: not_started, or unknown when the connection was lost after the command was sent, since it may have run.

| Code | Meaning | | --- | --- | | not_running | no core serves this folder | | unsafe_folder | the socket's folder failed a check | | not_offered | this core does not have that command; update nidl | | version_conflict | the core speaks another protocol version | | path_too_long | the socket's path is longer than the platform allows | | timeout | no answer within timeoutMs (10 seconds by default) | | protocol_error | the core's reply broke the protocol | | anything else | the core's own refusal, such as not_found or requires_owner_action |

A core on another machine

When a machine's owner has turned on nidl's remote port there (nidl remote enable) and issued you a remote token (nidl remote-tokens issue), connect to that core by its URL, https://HOST:PORT:

const nidl = await connect({
  url: "https://nidl.example:8443",
  token: process.env.NIDL_TOKEN,
  fingerprint: "sha256:…", // as `nidl remote status` shows it on that machine
});
  • The certificate is checked before the token is sent. With a fingerprint, only nidl's own certificate with that fingerprint is accepted, as SSH remembers a host's key; the handshake still proves the server holds its key. Without one, the certificate must be one Node trusts for the URL's host, as when the owner serves a certificate of their own. Anything else fails with untrusted_certificate, and nothing is sent.
  • The token's scope decides what runs. A read token lists and reads. An operate token also drafts targets, refreshes catalogs and readings, and previews and applies routing. Neither runs credential commands or starts paid model checks: those stay with the owner at the machine (requires_owner_action).
  • nidl.identity has no root or instanceNonce for a remote core, which does not say what names its files; it says the token's scope.
  • Each call is one HTTPS connection to the remote port, closed after the answer.

The remote codes, beside those above: unauthorized (the token is unknown, revoked or expired), untrusted_certificate (the certificate is not the one expected; nothing was sent), throttled (too many failed attempts from this address; wait a minute), invalid_input (the URL, token or fingerprint is malformed, or the command is over 1 MiB), unavailable (the machine could not be reached, or its core is busy or not serving), and unsupported (that nidl's remote port takes no commands; update it there).

Lending your project's key, and reporting readings

Another program can use nidl for its own accounts once the owner has made it a project there (nidl projects create), with a borrowed account for each API key it holds (nidl projects add-account, mode borrowed) and an API target with an app token (nidl targets create, nidl app-tokens issue). Your program lends nidl its key for as long as it wants requests to the target's endpoint, /h/<target>/v1/…, routed with it, and takes it back:

An endpoint you describe, an Anthropic- or OpenAI-compatible borrowed account, is lent its address and its key, if it has one, with lendEndpoint(accountId, { baseUrl, apiKey? }, { appToken }) (CUE-1122). The core checks the address as it checks a held endpoint account's.

const lease = await nidl.lend(accountId, apiKey, { appToken: process.env.NIDL_APP_TOKEN, expiresInSeconds: 3600 });
// ... calls to the target's endpoint route with the key while the lease lives ...
await nidl.withdraw(accountId);
  • The key travels in the socket's secret frame, never in a command's arguments, so command("pools.lend") is refused with invalid_input. nidl keeps it in memory only, never on disk, for 60 seconds to 12 hours (an hour unless you say) and never past the app token. It is dropped when the lease expires, when you withdraw it, when the owner revokes the token or removes its target, and when the core stops. Lend again to renew it.
  • The app token proves the account is your project's. A token of another project's target, a revoked or expired one, or none, is refused with not_authorized.
  • On this machine only. lend and withdraw on a remote core fail with unsupported before anything is sent.
  • The lease is {accountId, lentAt, expiresAt, lentBy}, lentBy being your app token's id. It never holds the key.

A JavaScript string cannot be wiped; the frame's own bytes are overwritten once the socket has taken them.

An observed account is one nidl never holds a key for, a subscription for instance, whose quota your program reports so the owner sees it beside their own:

await nidl.observe(accountId, [{ key: "five_hour", percent: 37.5, resetsAt: "2026-10-21T15:00:00Z" }]);

At most 16 windows, each a key, a percent from 0 to 100, and optionally a label and a resetsAt, captured within the last 24 hours (now, unless you say). On a remote core this needs an operate token bound to your project (nidl remote-tokens issue with its project).

Embedding nidl: the owner's connection

A program that runs its own nidl core, and decides itself when an account is used, starts one and connects as the folder's owner:

import { connect, startCore } from "@nidl/client";

const core = await startCore({ env: { ...process.env, NIDL_VAULT_HOLDER: "folder" } });
const nidl = await connect({ dataDir: core.dataDir, owner: true });
await nidl.setPassphrase(passphraseFromYourVault); // a new folder; later: unlockPassphrase()
await nidl.command("projects.create", { name: "my-app", label: "My app" });
await nidl.connectAccount(
  { providerId: "anthropic-api", label: "Team key", proof: "proven" },
  { apiKey: keyFromYourVault },
);
const { appToken, token } = await nidl.command("app_tokens.issue", { targetId, name: "my-app" });
  • startCore({ dataDir, directory, env, executable, timeoutMs }) starts the core @nidl/cli carries (or executable), with no account and no model-list refresh, and answers its dataDir and stop(). Without dataDir it makes a new private folder in directory (the temporary or home folder by default), which stop() removes; with one, it serves that folder and keeps it. The core stops when this process ends. A core that won't start says why, in its own words.
  • { owner: true } says hello as the cli client kind. The core admits it only from a program running as the core's own user, and only on this machine, never through the remote port.
  • It runs the owner's commands too, OWNER_COMMAND_NAMES: credentials.*, projects.*, app_tokens.*, the store's and the remote port's.
  • A secret never goes in a command's arguments. commandWithSecret(name, args, secret) sends it in the socket's secret frame, for SECRET_COMMAND_NAMES: accounts.connect and accounts.reconnect (the account's credential JSON), credentials.set_passphrase and credentials.unlock_passphrase (the passphrase), and the store's logins. connectAccount(), setPassphrase() and unlockPassphrase() name the common ones. command() refuses these, and an app's connection refuses them before anything is sent.
  • A vault key on a Mac without the Keychain: start the core with NIDL_VAULT_HOLDER=folder to unlock with a passphrase you send, or with NIDL_VAULT_KEY_FILE naming a key file (64 lowercase hexadecimal characters, a file only you can change). Without either, a Mac folder keeps its key in the login Keychain behind Touch ID. On Linux, a key file, else a passphrase.

Handing a session its account

A target applied with hand-over (docs/specs/nidl-hand-over-mode.md) gives a session its account's own credential instead of routing it. Start the harness with what handOver() answers:

const handed = await nidl.handOver(targetId, { dialect: "claude-code", appToken });
const env = handOverEnvironment(handed); // from process.env, without nidl's routing
spawn("claude", [...handed.args, "-p", "hello"], { env }); // Codex: [...yours, ...handed.args]
// Before handed.expiresAtMs, the same account's next token:
const next = await nidl.renewHandOver(handed.handoverId, handed.renewal, { appToken });
  • dialect names the harness on an API target: claude-code (Messages) or codex (Responses). accountId chooses the account; without it the target chooses, as Auto would for a new session.
  • The app token and the renewal secret go in the socket's secret frame; command() refuses handover.issue and handover.renew. An app is handed its project's own accounts, and the owner's subscriptions shared with its project. handOvers(targetId) reads the records, at the machine.
  • handOverEnvironment(handed, base?) builds the session's environment as nidl's launcher does: unset and every NIDL_ variable removed, the x-nidl-token header line dropped (your other headers stay), then set. Give the session a fresh CLAUDE_CONFIG_DIR (or CODEX_HOME) when its own settings files might set a login or a proxy: nidl's launcher refuses those, a library can't.
  • For Codex, args are all -c pairs, the model's too (-c model="…"), so codex exec and codex app-server take them alike. A ChatGPT plan is handed only with a model Codex 0.162.1 knows; on a newer Codex, check that yours still knows it. Run Codex 0.162.1 or later to hand it a plan, and keep the reply's -c pairs as they are: 0.157.0 passes the token to every command it runs, and 0.162.1 does too without the two shell_environment_policy pairs. Codex's notify program, when one is set, gets the token too. On Linux, the session's own environment, and so the token, can be read through /proc by any process of the same user outside its sandbox: run the harness's commands in its sandbox, or each session under its own user.
  • minLifetimeMinutes (15 to 1440, on handOver and renewHandOver) asks for a token with at least that long left, for a turn that runs long: nidl renews first, and refuses, saying why, when even a fresh token falls short. A ChatGPT plan's token lasts about an hour, so about 50 is its ceiling; a Claude subscription's lasts hours.
  • Never a refresh token. A key handed over (the target's keys on) works until its provider revokes it.

In a Trigger.dev task

A task can run its own nidl core in its container, from a task vault you export at your machine (nidl task-vault export) and give the task as two secrets, NIDL_TASK_VAULT and NIDL_TASK_VAULT_KEY:

import { startTaskCore } from "@nidl/client";

const nidl = await startTaskCore(); // once per process; a warm worker reuses it
process.env.ANTHROPIC_BASE_URL = nidl.baseUrl; // http://127.0.0.1:<port>/h/<target>
process.env.ANTHROPIC_API_KEY = nidl.appToken;
  • It removes both secrets from process.env before anything else runs, writes them to private files, opens the task vault, serves it with --stop-with-parent and unlocks it with its key file. It needs @nidl/cli beside it (or executable), both kept out of the bundle (build.external).
  • Its folder is made in the temporary folder, or in the home folder when someone else may write to the temporary one, as anyone may to /tmp; directory names another. A start that fails removes what it wrote and can be called again; stop() removes the folder once the core has stopped.
  • Linux only, where Trigger.dev deploys run. Otherwise a task reaches the hub with an app token.
  • Codes: not_configured (no secrets), not_installed (no @nidl/cli), timeout, unavailable, or the core's own, such as invalid_input for a vault that does not open.

The whole path, the hub's included, is in docs/integrations/trigger-dev.md.

Long-running commands return an operation id: poll it with nidl.command("operations.get", { operationId }).

Results are JavaScript values, so a whole number above 2^53 loses precision, as it does in any JSON.parse. nidl's results do not carry such numbers today.

The protocol is described in docs/protocol/nidl-core-protocol.md. Node 22 or later, on macOS or Linux. No dependencies: the remote port uses Node's own tls and http.