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

@byokit/ui

v1.1.0

Published

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

Readme

Install

npm install @byokit/ui

npm · Latest release · All releases

Migrating from @byokit/ui-core? See the deprecated shim's migration guide.

Quickstart

npm install @byokit/ui

React is an optional peer dependency, needed only for the hooks (useSignIn, useRun, useApprovals, useHerdrTree, useBlocked, useConnect, useAccounts, useModelPicker).

import { phaseOf, stepOf } from '@byokit/ui/phase';
import { describeRoute } from '@byokit/ui/route';
import { consentWords, linkWords, pairingView, qrMatrix } from '@byokit/ui/link';

// What the back end reports: a sign-in waiting on a typed code.
const phase = phaseOf({ ready: false, signIn: { state: 'waiting', via: 'code', code: 'ABCD-1234' } });
console.log(phase, stepOf(phase));
console.log(phaseOf({ ready: true, work: true }));
console.log(phaseOf({ signIn: { state: 'failed', why: 'busy' } }));

console.log(describeRoute('wss://home.tail1234.ts.net/link'));
console.log(describeRoute('ws://100.101.102.103:7777'));

console.log(consentWords({ hostName: 'Kitchen computer', role: 'control' }));
console.log(pairingView({ phase: 'compare', hostName: 'Kitchen computer', words: 'coast comet' }));
console.log(linkWords('offline', 'Kitchen computer'));
console.log(qrMatrix('hello').length, 'rows');

Output from running it with Node:

code 1
work
busy
Tailscale
Private network
Pair with Kitchen computer? This device will be able to see and change things on it, until you remove it there.
{
  phase: 'compare',
  title: 'Check Kitchen computer shows these two words, then say yes there.',
  words: 'coast comet'
}
Can't reach Kitchen computer right now. This device keeps trying by itself.
25 rows

In React, useSignIn drives the whole sheet from three calls to your back end:

import { useSignIn } from '@byokit/ui';
import type { AccountView } from '@byokit/ui';

// Your back end's routes; the hook only needs these three calls.
declare const api: {
  account(): Promise<AccountView | null>;
  signIn(body?: { via?: 'code'; fresh?: boolean }): Promise<unknown>;
  cancel(): Promise<unknown>;
};

export function useChatGptSheet() {
  const s = useSignIn({ read: () => api.account(), start: (b) => api.signIn(b), cancel: () => api.cancel() });
  // s.phase: what to draw. s.code / s.url: what to show. s.start({ via: 'code' }): "Having trouble? Use a code instead".
  // s.cancel(), s.close(), s.keepWork(): the sheet's buttons. Leave `start` out when something else starts the
  // sign-in: the sheet then only watches, and nothing begins as it opens.
  return s;
}

API at a glance

| Export | What it does | |---|---| | phaseOf(account, { offline, cancelled, keepWork }) | The sign-in phase to draw, from an @byokit/accounts view() plus whether the account is signed in | | stepOf(phase) | Where the three-step progress bar ("Open", "Say yes", "Done") stands | | useSignIn({ read, start?, cancel, offline?, ms?, pinned? }) | React hook: starts the sign-in as the sheet opens, polls, and handles cancel, close, keep-work and "use a code instead"; without start it only watches and nothing begins | | describeRoute(url, kind?) | Names the route a dial address takes, for a pairing or settings screen | | routeShort(route) | The route as words inside a connected line (same Wi-Fi) | | connectedWords(hostName, url?, kind?) | The connected line to render (Connected to Kitchen computer - same Wi-Fi.), plain with no route | | routeChoices() | Every onboarding route in everyday words ({ code, title, sentence, needs }), in recommendation order | | qrMatrix(text, { border }?) | The pairing QR as rows of dark and light modules, with its quiet border (2 unless given) | | qrText(text, { border }?) | The same QR as half-block text rows for a terminal | | consentWords({ hostName, role, device?, detail? }) | The question before pairing | | pairingView({ phase, hostName?, words?, error?, device?, detail? }) | The pairing sheet's title and words for each phase | | linkWords(status, hostName) | The link's status in one sentence | | pairErrorWords(error) | A failed pairing as expired, wrong or the link's own sentence | | stepsView(steps) | The stepper rows to draw, from the caller's own step ids and states | | stepsText(rows, { title }) | The same rows as text lines for a terminal | | runStore(oc), runView(state, { words, name }) | One run: the reply streaming in, the tool at work, how it ended in the kit's words | | approvalsStore(oc), approvalWords(a, words, helper) | The approvals waiting for a yes, live, each gone when answered or expired | | herdrStore(hd), herdrTreeView(tree), blockedView(state) | Herdr's agents by where they run, and the ones waiting for an answer, live | | cloudSource(provider, o?), cloudStore(source), cloudView(state, { now, words }) | Your cloud computers: the list, each one's state, newest copy and cost in plain words, the search, the pick, and what the host answered when it woke one | | useCloud(source) | React hook over that store | | signInsStore(oc), signInSheetView(request, { name, holder }), livePanelView(state) | The browser sign-in sheet (sentence, buttons, chat chip) and live panel for the OpenClaw kit's handoff | | useRun, useApprovals, useHerdrTree, useBlocked | React hooks over those stores | | connectView(routes, step?), connectStep(step, action) | Every account route the kits list, grouped by how the person pays, with the picked row and its key card | | signInFor(view, { read, start?, cancel }), useConnect({ routes, read, start?, cancel }) | useSignIn's options for the picked sign-in row; the hook holds the step | | accountRows(accounts, room, now, windows?), rowsOf(source, now), nameSuggestions(email, taken, provider?) | One plain-words row per account (ChatGPT · Work / Plus · 72% left this week), the honest words for every sign-in state, and untaken account-name suggestions | | agentView(agent, accounts, defaults?, models?) | One agent's sign-in methods (an Agent tab): its plans counted as 1 of 3 enabled, the one account in use, its default model, pay-per-use keys listed apart and never chosen | | useAccounts(source), useModelPicker(source, initial?) | React hooks: the rows live with refresh() and remove() (a failing room leaves only its row unknown; a mid-refresh remove still drops it), and the run picker with select() keeping the caller's selection across refreshes; accountsSource(accounts, member, room?) and cliSource(cli, room?) fill the source with one line per member (the openclaw adapter stays pending O14) | | Types | Phase, SignInView, AccountView, UseSignIn, Route, RouteChoice, RouteCode, PairPhase, Role, DeviceKind, LinkStatus; RouteKind from @byokit/ui/route; Account, AccountRow, AccountAction, AccountsSource, AgentSpec, AgentView, AgentWay, AsOf, Billing, Room, RoomSpan, SignInState, UsageWindow, UsageWindows from @byokit/ui/accounts |

Entry points: @byokit/ui (everything, including the React hooks), @byokit/ui/phase, @byokit/ui/route, @byokit/ui/link, @byokit/ui/kits, @byokit/ui/steps, @byokit/ui/connect and @byokit/ui/accounts (no React dependency).

Sign-in phases

phaseOf() turns what your back end reports (an @byokit/accounts view() plus whether the account is signed in) into the phase to draw:

| Phase | Meaning | |---|---| | opening | getting the provider's page or a code ready | | waiting | say yes on the provider's page | | code | type this code on the provider's page instead | | done | signed in, and it works | | work | signed in with a work plan: offer a personal one | | cancelled | declined on the page, or cancelled here | | busy | something else on this computer is signing in | | expired | the page or code ran out of time | | failed | anything else, with a plain sentence | | offline | the home computer isn't answering |

phaseOf() and stepOf() also come from @byokit/ui/phase, which has no React dependency.

Routes

describeRoute(url, kind?) (also @byokit/ui/route, no React) names the route a dial address takes, for a pairing or settings screen. Pass tailscale, direct, private, or lan when known (map @byokit/discover's tailscale-direct to direct); without provenance, 100.64/10 reads as a private network rather than assumed to be Tailscale, and the other home ranges read as same Wi-Fi.

routeChoices() (same entry, no React) is the one word table for the onboarding route list: { code, title, sentence, needs } per route in recommendation order, a superset of @byokit/discover's Via (it also covers the temporary public link and your own server, which reach() there does not probe). recommend() from the same package takes sentence and needs from here and only adds availability.

connectedWords(hostName, url?, kind?) (same entry) is the connected line a consumer renders: the computer with the route just dialled (Connected to Kitchen computer - same Wi-Fi.), or plain (Connected to Kitchen computer.) when there is no route to name.

import { routeChoices } from '@byokit/ui/route';

console.log(routeChoices().map((c) => `${c.title}: ${c.sentence}`));

Connect an account

connectView(routes, step?) (also @byokit/ui/connect, no React) draws the "Connect an AI account" list from the routes the kits discover (D18 in docs/runtime-kits.md): pass @byokit/accounts routes(host) and @byokit/openclaw routes(facts) together, unfiltered. Every route is listed, unavailable ones too, in these groups (empty ones left out):

| Group | Rows | Offered by default | |---|---|---| | Plans | subscription and plan-key routes the kit offers by default | yes, when ready | | Pay per use | API-billed routes | no, only when picked | | On this computer | local runtimes | no | | Your own server | endpoint routes; the person says how it is paid for | no | | Cloud account | cloud credentials | no | | Other ways to connect | free or unknown billing ("Billing set by …") | no | | Speech, search and media | non-chat services | no |

Billing decides the group, never the route's offer alone: an API, cloud, local, server, free or unknown row can never sit among Plans. Each row has billingWords ("Uses your ChatGPT plan", "Charged per use to your OpenAI account"), method ("Sign in with a code") and status (a sentence per readiness), plus offered, ready and does: signin, key, setup (the app's own cloud, local or server form) or unavailable. The view copies only these fields, so a credential on a route never reaches it; nothing in it names a provider.

import { connectStep, connectView, signInFor } from '@byokit/ui/connect';
import { routes } from '@byokit/accounts';

declare const api: { account(id: string): Promise<null>; signIn(id: string, body?: object): Promise<void>; cancel(id: string): Promise<void> };
const step = connectStep({ at: 'list' }, { type: 'pick', key: 'accounts/openai:code' });
const view = connectView(routes({ platform: 'browser' }), step);
// view.groups → the list; view.chosen.row.does === 'signin' → draw the sign-in sheet:
const options = signInFor(view, { read: (row) => api.account(row.id), start: (row, body) => api.signIn(row.id, body), cancel: (row) => api.cancel(row.id) });

A key route's chosen.key is the existing keyView card, labelled for its billing ("Key from your plan" or "Key (charged per use)"); submit the input straight to the kit and dispatch only { type: 'result', result }. A key that acts for several workspaces (@byokit/accounts add answers workspaces) dispatches result: 'workspace': list those workspaces, then submit again with the one picked. useConnect({ routes, read, start?, cancel }) holds the step in React and returns the view with pick, back, act and signIn, the options to pass to useSignIn in a sheet keyed by chosen.row.key.

Account rows

accountRows(accounts, room, now, windows?) (also @byokit/ui/accounts, no React) turns a list of accounts and a room reading into one plain row each: the provider and the person's own name for the account (ChatGPT · Work), then the plan and the room left (Plus · 72% left this week). A row also carries state, billing, low (room left at or below 20%, exactly 20 included, never true for an unknown room) and, when it helps, an action: sign_in for a signed-out or lapsed account, wait plus the until refill time for a resting one. accountRows reads only the fields it names, so a credential on an account never reaches a row. A row that is not ready shows its own honest words ("Checking…", "Cooling down", "Not signed in", "Sign in again", "Not in your plan") instead of the room.

Pass windows, the account's windowsView(reading, now) from @byokit/usage/view, as a fourth argument and a ready row also carries windows (each usage window, tightest first: Week: 72% left with resets in 4d) and asOf (Read 2 minutes ago, or Read 2 days ago (stale) past a day). A window whose reset is already past reads Session: reset since this reading, with no percent or countdown, and the room line Reset since this reading. A stale row is never low; a reading without its own time, such as an openclaw room, shows its resets and no asOf, never a made-up age.

AccountsSource is the shape an app fills from its own back end — accounts(), room(account) and optionally windows(account) — and rowsOf(source, now) awaits them and builds the same rows; @byokit/accounts list() and a room reading fit it with one line per member. nameSuggestions(email, taken, provider?) offers up to three untaken account names from an email's local part (deduped case-insensitively against taken), falling back to the provider's name; the email itself is never offered.

import { accountRows, nameSuggestions, type Account, type Room } from '@byokit/ui/accounts';

declare const list: () => readonly Account[];
declare const reading: (account: Account) => Room;
const rows = accountRows(list(), reading, Date.now());
// rows → [{ title: 'ChatGPT · Work', detail: 'Plus · 72% left this week', state: 'ready', low: false, ... }]
console.log(nameSuggestions('[email protected]', rows.map((r) => r.title)));

In React, useAccounts(source) keeps those rows live (refresh() re-reads; remove(id) removes through the back end and drops the row even mid-refresh), and useModelPicker(source) keeps the run picker live (select() marks the selection at once and keeps it across refreshes). accountsSource(accounts, member, room?) fills the source from a live @byokit/accounts member and cliSource(cli, room?) from a live cliAccounts() kit, each with one line per member; the openclaw adapter stays pending O14:

import { accountsSource, cliSource, useAccounts, useModelPicker } from '@byokit/ui';
import type { Accounts } from '@byokit/accounts';
import { cliAccounts } from '@byokit/accounts/cli';
import type { Account, Room } from '@byokit/ui/accounts';

declare const accounts: Accounts<any, number>;
declare const cli: ReturnType<typeof cliAccounts>;
declare const reading: (account: Account) => Room;
const fromMember = accountsSource(accounts, 1, reading);
const fromCli = cliSource(cli, reading);

export function useAccountList() {
  return useAccounts(fromCli); // { rows, ready, error, refresh, remove }
}
export function useRunPicker() {
  return useModelPicker(fromMember); // { rows, picker, selection, ready, error, select, refresh, remove }
}

Agent sign-ins

agentView(agent, accounts, defaults?, models?) (also @byokit/ui/accounts, no React) draws one agent the way an Agent tab does. The app names the agent and the ways it can run ({ kind, name, ways }, each way a route as connectView takes it, the routes it also passes @byokit/accounts as AgentCapabilities). accounts is list(), defaults is defaults(member, kind) and models is the in-use account's models(). The view carries:

  • ways: the agent's plans, each with the accounts connected on it (same provider and billing) and a state: in_use, connected, sign_in, or unavailable with the route's own words.
  • count: 1 of 3 enabled, the plans with a ready account. A pay-per-use way is never counted.
  • perUse: pay-per-use and other ways, listed apart with their billing words (OpenAI API key, Charged per use to your OpenAI account). They are never offered or chosen for the person.
  • rule (one at a time), inUse and note. A saved account that is signed out or resting is never swapped for another one: note says so and the agent stays unset, as the kit's pick refuses it.
  • model: the in-use account's own live models with the saved one selected. A saved model the list no longer offers selects nothing and says so.

The app saves a choice with setDefaults(member, { account, model }, kind); choosing another account replaces the last one.

import { agentView, type Account, type AgentSpec } from '@byokit/ui/accounts';

declare const list: () => readonly Account[];
const codex: AgentSpec = { kind: 'codex', name: 'Codex', ways: [
  { id: 'chatgpt', provider: 'chatgpt', name: 'ChatGPT', company: 'OpenAI', via: 'code', billing: 'subscription', offer: 'default', readiness: 'ready' },
  { id: 'openai:key', provider: 'openai', name: 'OpenAI', company: 'OpenAI', via: 'key', billing: 'api', offer: 'explicit', readiness: 'ready' },
] };
const view = agentView(codex, list(), { account: 'chatgpt' });
// view.count → '1 of 1 enabled', view.note → 'Codex uses [email protected].', view.perUse.ways[0].title → 'OpenAI API key'

Pairing words

Pairing with @byokit/pair (published as @byokit/link up to 0.8.x), from @byokit/ui/link (no React either):

  • qrMatrix(offer.text, { border }?): the pairing QR as rows of dark and light modules, with its quiet border (2 unless given), to draw in any UI.
  • qrText(offer.text, { border }?): the same QR as half-block text rows, so console.log in a setup script shows a code a phone can scan.
  • consentWords({ hostName, role, device?, detail? }): the question before pairing ("Pair with Kitchen computer? This device will be able to see and change things on it, until you remove it there."). device names the pairing side when it isn't the app (phone, or browser for the web pairing link); detail adds one app sentence after.
  • pairingView({ phase, hostName, words, error, device?, detail? }): scan, compare the two words, waiting for a yes, paired, failed.
  • linkWords(status, hostName): the link's status in one sentence.

Stepper rows

stepsView() (also @byokit/ui/steps, no React) turns the caller's own steps into the rows a stepper or inline checks draw, so a terminal and a phone show the same words. Each step keeps the id its caller chose, the state it is in (todo, checking, ok, failed), the plain words to show, and the one next action when it failed. stepsText() draws the same rows as text lines for a terminal; a phone draws the rows in its own list. A failed step without its own fix says "Try again."

import { stepsText, stepsView } from '@byokit/ui/steps';

// Onboarding "Checking this computer": ids are the app's own, states come from its probes.
const rows = stepsView([
  { id: 'runner', state: 'ok', label: 'Agent runner', detail: 'ready' },
  { id: 'agents', state: 'ok', label: 'Agents found', detail: 'Claude Code (signed in) · Codex (needs sign-in)' },
  { id: 'wifi', state: 'ok', label: 'Wi-Fi', detail: 'this computer is at 192.168.1.144' },
  { id: 'tailscale', state: 'todo', label: 'Tailscale', detail: 'not installed on this computer' },
]);
console.log(stepsText(rows, { title: 'Checking this computer' }));

// "Setting up" reuses the same rows with its own ids; a failed check names its fix:
console.log(stepsView([
  { id: 'serve', state: 'failed', label: 'muxr', detail: 'not answering', fix: "muxr didn't start. Press r to retry." },
])[0]);

Output from running it with Node:

Checking this computer
  ✓ Agent runner: ready
  ✓ Agents found: Claude Code (signed in) · Codex (needs sign-in)
  ✓ Wi-Fi: this computer is at 192.168.1.144
  ○ Tailscale: not installed on this computer
{
  id: 'serve',
  state: 'failed',
  words: 'muxr: not answering',
  fix: "muxr didn't start. Press r to retry."
}

Runtime kits' state

Runtime kits' state, from @byokit/ui/kits (no React), typed to fit @byokit/openclaw/device and @byokit/herdr/device as they are:

  • Runs: runStore(oc) with send(message) and stop(); runView(state, { words, name }) gives the reply so far, the tool at work and, for a failed run, the kit's sentence (words from @byokit/openclaw/device, name "ChatGPT"). OpenClaw's started, thinking and account frames are accepted without changing the reply, tools or rendered state; the running phase begins at send, not Gateway acceptance.
  • Approvals: approvalsStore(oc) keeps the waiting list live (each leaves when answered anywhere or when it expires); approvalWords(approval, words, 'Your helper') asks the question.
  • Herdr: herdrStore(hd) keeps the tree and the agents waiting for an answer live from one stream; herdrTreeView(tree) groups the agents by where they run, with the status the kit's agentWords takes; blockedView(state) names each waiting agent.
  • Cloud computers: cloudStore(source) keeps the list live and cloudView(state) gives the rows — each machine's state, its newest copy and what it costs, in plain words — with the host's answer to an action in the row that asked for it.
  • Browser sign-in (@byokit/openclaw spec 5.17): signInsStore(oc) keeps the requests live from sign-in pings; signInSheetView(request, { name, holder }) gives the sentence (a kit word key and its values), the buttons and the chat chip, which reads "entered" until a verified sign-in; livePanelView(state) says when to draw frames, take input, show "Reconnecting…", or ask to confirm an address the person didn't expect. Watching asks the helper nothing.
  • Each store is { get, subscribe }: draw from get() in any UI; the first subscribe starts following, the last unsubscribe stops. runStep, approvalsStep and herdrStep are the reducers underneath.

In React or React Native: useRun(oc, { words, name }), useApprovals(oc), useHerdrTree(hd) and useBlocked(hd), with the device client made once (useMemo(() => herdrDevice(link), [link])).

Your cloud computers

cloudSource(provider, { since, monthly, known, words }) (also @byokit/ui/kits, no React) turns a cloud provider into the list a phone draws. The cloud kit's own Provider fits as it is, and so does one that lists more machines with their own address, state and newest copy.

cloudStore(source) keeps that list live: it is read when the first listener arrives and again on refresh(), and the answer to an action lands in the row that asked for it. cloudView(state, { now, words }) gives the rows to draw — each with its title, its state in plain words (Awake, Asleep, Waking up…), its newest saved copy (Saved copy 2 hours ago), what it costs ($0.50 so far this month · about $5.00 a month), the action the person can take and what the host said about it. The line above the list says how the read went: Loading your cloud computers…, No cloud computers yet., No cloud computer matches lab. or Can't read your cloud computers right now., with the host's own words under it.

cloudStep(state, action) is the reducer underneath; search(text), select(id) and act(id, action) are what the store offers. In React or React Native, useCloud(source) gives the same view with those three.

import { cloudSource, cloudStore, cloudView } from '@byokit/ui/kits';

const provider = {
  list: async () => [{ id: 'sb-1', name: 'desk' }, { id: 'sb-2', name: 'lab' }],
  status: async (ref: { id: string }) => (ref.id === 'sb-1' ? 'on' as const : 'asleep' as const),
  snapshots: async (ref: { id: string }) => (ref.id === 'sb-1' ? [{ id: 'byokit-desk-today', at: new Date(Date.now() - 7_200_000).toISOString() }] : []),
  usage: async () => ({ from: '2026-10-01T00:00:00.000Z', to: new Date().toISOString(), hours: 24, amount: 0.5, currency: 'USD' }),
  wake: async () => {},
  sleep: async () => {},
};

const store = cloudStore(cloudSource(provider, {
  since: '2026-10-01T00:00:00.000Z',
  monthly: () => ({ perMonth: 5, currency: 'USD' }),
}));

const off = store.subscribe(() => {
  const { rows, headline } = cloudView(store.get());
  console.log(headline || 'Your cloud computers');
  for (const r of rows) {
    console.log(`  ${r.title} — ${r.stateWords}${r.copy ? ` · ${r.copy}` : ''}${r.cost ? ` · ${r.cost}` : ''}${r.actionWords ? ` · ${r.actionWords}` : ''}`);
  }
});

Output from running it with Node:

Your cloud computers
  desk — Awake · Saved copy 2 hours ago · $0.50 so far this month · about $5.00 a month · Put it to sleep
  lab — Asleep · $0.50 so far this month · about $5.00 a month · Wake it

Links

License

Apache-2.0. See LICENSE and NOTICE.

@byokit/ui/kits also exports keyStep(state, action) and keyView(state, words) for an explicit API key (billed per use) entry card. States are entry, checking, workspace, ok, invalid, not_included; actions are submit, edit, and result with the kit's add-key outcome. Supply the OpenClaw kit's words function for the label and sentences. These views never contain the entered key; submit it directly to the host and clear the input afterwards.