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/herdr

v0.7.0

Published

Drive the Herdr on this computer — workspaces, tabs, panes, agents and blocked-approval answers — from an app, or hand it to a phone over @byokit/link.

Readme

Install

npm install @byokit/herdr

npm · Latest release · All releases

Quickstart

Available on npm; it is also used from this repo, or from its packed packages as examples/herdr-kit does:

npm install @byokit/herdr

Herdr itself is not an npm dependency: the app installs Herdr 0.9.1 (see herdr.dev) and passes its path as bin. The opt-in helper ensureHerdr (@byokit/herdr/binary, also spelled fetchHerdr) fetches that for the app: it downloads the pinned release asset for the current platform into an app-owned directory, verifies its sha256 against a committed per-platform table, marks it executable, and returns the absolute bin for own mode. Nothing downloads on install or import — the network happens only when the helper is called. This runs against the kit's stand-in Herdr from @byokit/herdr/testing, so it needs neither Herdr nor an agent:

import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { HerdrKit, agentWords, type BlockedAgent } from '@byokit/herdr';
import { startFakeHerdr } from '@byokit/herdr/testing';

// The kit's stand-in Herdr. With a real one, pass its bin and socket path instead.
const fake = await startFakeHerdr({ dir: mkdtempSync(join(tmpdir(), 'herdr-')) });
const kit = new HerdrKit({ mode: 'adopt', bin: fake.bin, socketPath: fake.socketPath });
await kit.start();

const agent = { paneId: 'w1:p2' };
await kit.prompt(agent, 'hello');                     // resolves with a receipt once delivered
console.log(await kit.wait(agent, { until: ['idle'], timeoutMs: 5000 }));
console.log((await kit.read(agent.paneId)).text);

// The question arrives once the kit has read it off the pane, so wait for it.
const asked = new Promise<BlockedAgent>((resolve) => {
  const off = kit.onBlocked((b, change) => {
    if (change === 'added' && b.paneId === agent.paneId) { off(); resolve(b); }
  });
});
await kit.prompt(agent, 'ask permission');
const question = await asked;
console.log(agentWords('blocked'), '|', question.prompt);
await kit.answer(agent.paneId, ['y'], { revision: question.revision });
console.log(await kit.wait(agent, { until: ['idle'], timeoutMs: 5000 }));

await kit.stop();
await fake.stop();
idle
ready.
fake pi: hello
Waiting for your answer. | Allow this? (y/n)
idle

To hand Herdr to a phone, spread herdrLink into a @byokit/link host and serve it; the phone calls the same things through herdrDevice:

import { ensureHerdr } from '@byokit/herdr/binary';
import { HerdrKit } from '@byokit/herdr';

// One call, only when the app wants it: the pinned Herdr 0.9.1 binary lands verified in an
// app-owned directory (never PATH, never ~/.local/bin), ready for own mode.
const bin = await ensureHerdr({ dir: './.state/herdr-bin' });
const own = new HerdrKit({ mode: 'own', bin, stateDir: './.state' });
import { HerdrKit } from '@byokit/herdr';
import { herdrLink, serve } from '@byokit/herdr/link';
import { Host, type Grant } from '@byokit/link';
import { hostKeyFile } from '@byokit/link/node';

const kit = new HerdrKit({ mode: 'own', bin: '/usr/local/bin/herdr', stateDir: './.state' });
const grants: Grant[] = [];

const host = await Host.open({
  keys: hostKeyFile('./.state/link-key.json'),
  name: 'my-computer',
  grants: { load: () => grants, save: (g) => { grants.splice(0, grants.length, ...g); } },
  confirm: async () => true,        // ask the person at the computer here
  // Each paired device sees the workspaces its grant's meta names; none by default.
  ...herdrLink(kit, {
    scopeOf: (g) => (g.meta as { scope?: { workspaces: 'all' | string[] } } | undefined)?.scope ?? { workspaces: [] },
  }),
});
const served = await serve({ host, port: 7310 });
console.log(served.urls);
kit.start().catch(() => {});        // non-fatal: retry later while Herdr is down
import type { DeviceLink } from '@byokit/link';
import { agentWords, herdrDevice } from '@byokit/herdr/device';

declare const link: DeviceLink;     // from pairing (pairWithCode / pairWithOffer)

const hd = herdrDevice(link);
const agent = await hd.startAgent({ kind: 'pi', cwd: '/home/me/project', place: { workspace: 'new' } });
await hd.prompt(agent.paneId, 'Run the tests');
for (const q of await hd.blocked()) {
  console.log(agentWords('blocked'), q.prompt);
  await hd.answer(q.paneId, ['y'], q.revision);
}

The herdr-kit example is the full version: pairing with a QR code and typed code, a persistent grant store, and the phone page above.

API at a glance

| Export | What it does | |---|---| | HerdrKit (@byokit/herdr) | The host-side kit, in adopt or own mode: start/stop, state, snapshot/onChange, startAgent, openSignInTab, move, moveToAccount, onStartAgent, prompt, sendKeys, wait, read, blocked/onBlocked/answer, closePane/closeTab/closeWorkspace, agentKinds, installedAgentKinds, agentStatus, agentInstallState, terminal, onEvent, statusWatchReady, and the pass-throughs call, subscribe and cli | | agentStatus, agentInstallState, agentProbePath, extraPathDirs, runStatusCommand, isAutoInstallShim, resolveAgentBinary (@byokit/herdr) | Onboarding readiness: per-kind install + CLI sign-in without a Herdr connection (see below) | | HERDR_VERSION, HERDR_PROTOCOL | The pinned Herdr release (0.9.1) and the protocol the kit speaks (22) | | words, agentWords, stateWords, WORDS | Plain sentences for agent statuses and kit states | | herdrLink (@byokit/herdr/link) | The host side of the hd.* link ops, spread into Host.open; scopes each grant to its workspaces and can push sealed approval notices through a relay | | serve (@byokit/herdr/link) | Finds the address with @byokit/reach and serves the link (and an optional page) on one port | | herdrDevice (@byokit/herdr/device) | The phone and browser side: typed calls over a DeviceLink (tree, startAgent, prompt, read, blocked, answer, events, terminal, notices); no Node import | | openNotice (@byokit/herdr/device) | Opens a sealed approval notice with the device's own seed | | startFakeHerdr, writeBinShim, herdrContract (@byokit/herdr/testing) | The kit's stand-in Herdr, a bin shim that runs it, and the contract suite the kit passes | | ensureHerdr, fetchHerdr, HERDR_ASSETS (@byokit/herdr/binary) | Opt-in pinned fetch: the v0.9.1 asset for the current platform into an app-owned dir, sha256-verified, executable, returned as the absolute own-mode bin; refused (nothing left) on a hash mismatch or unsupported platform |

PromptReceipt contains paneId, terminalId, revision, and status. Its optional agentSession ({ source, agent, kind, value }) identifies the conversation that received this prompt, taken directly from the agent.prompt response without another read. kind distinguishes a session id from a session path; value is that id or path. Herdr exposes no separate generation counter. When Herdr omits the session (or returns null), agentSession is absent; callers can fall back to their existing checks.

Types (HerdrKitOptions, HerdrState, StartAgent, PromptReceipt, BlockedAgent, HerdrSnapshot, HerdrMethod/HerdrParams/HerdrResult, HerdrEventName/HerdrEventOf, ...) come from the main entry.

Account-specific sign-ins and moves

The host supplies account folders and shares conversation history as needed. The kit never reads or copies credentials. To connect an account, open its CLI in a new tab and let the person complete the CLI's own login:

import { HerdrKit } from '@byokit/herdr';
const kit = new HerdrKit({ mode: 'adopt', bin: '/app/bin/herdr', socketPath: '/app/herdr.sock' });
await kit.start();

const signIn = await kit.openSignInTab({
  workspaceId: 'w1', kind: 'codex', cwd: '/home/me/project', env: { CODEX_HOME: '/app/accounts/work' },
});
// Attach terminal(signIn.paneId, …) for the CLI's own sign-in screen.
const moved = await kit.moveToAccount({ paneId: 'w1:p2' }, {
  provider: 'codex', folder: '/app/accounts/work',
});
if (moved.ok) console.log(moved.session); // new pane to follow

A move accepts mapped managed kinds, including Claude (CLAUDE_CONFIG_DIR), Codex (CODEX_HOME) and Pi (PI_CODING_AGENT_DIR, --session <id|absolute path>). Hosts own cross-account history sharing. It verifies the new shell's effective account folder, resumes the conversation there, waits for a ready session, then closes the old pane. A failed start preserves the original. A failed source close rolls back the new pane only after fresh reads verify both conversations' identities. A lost close acknowledgment can mean the original already closed: the kit preserves the replacement when the source is absent, replaced or unreachable and returns close_failed. After uncertain source close or cleanup, live names a freshly verified surviving conversation; it is omitted when neither conversation can be verified. Check the remaining panes before retrying. Independent changes can still happen after verification; the server has no conditional close transaction. Immediately before close, the source must still be idle/done with the same conversation, terminal and published state_change_seq as before splitting. Otherwise the replacement is closed and changed returned; live is freshly verified. This guard applies to idle moves too. It is a bounded observation, not atomic or a new security guarantee. Failures return a plain message. By default, working or blocked conversations return busy.

Both move APIs optionally accept:

import type { BusyHandoff } from '@byokit/herdr';
const whenBusy: BusyHandoff = {
  busy: 'wait',
  confirmed: { session: 'published-conversation-id', terminalId: 'published-terminal-id' },
  waitMs: 300_000,
}; // Pass whenBusy to either move API after the person confirms these current values.

Label this Move when this step finishes. Confirmation must match the current published session and terminal; wait requires a published sequence and finite positive waitMs ≤ 300000. Working sources wait for idle/done/blocked and are re-read before staging. Timeout returns busy, approval returns blocked, stale identity returns changed, and missing sequence or invalid bounds returns unsupported. The person may stop the step themselves in the pane. busy: 'interrupt' (confirmed session/terminal/seq) returns interrupt_unsupported, never keys or lifecycle calls. Offline fixtures do not qualify real working-step handoff or native Pi moves. StartAgent.env applies to newly created placements; existing shells cannot receive a new env. Tokens stay on the device and are never logged. Each provider's own terms apply to how you use your plan.

For managed-folder stores that supply resume arguments and credential shedding, use move:

import { HerdrKit } from '@byokit/herdr';
const kit = new HerdrKit({ mode: 'adopt', bin: '/app/bin/herdr', socketPath: '/app/herdr.sock' });
await kit.start();

const conversationId = 'published-conversation-id';
const staged = new Set<string>();
let activePane = 'w1:p2';
const moved = await kit.move({
  paneId: 'w1:p2', kind: 'codex', args: ['resume', conversationId],
  set: { CODEX_HOME: '/app/accounts/work' }, unset: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
  onStaged(paneId) { staged.add(paneId); },
  onReplaced(paneId) { staged.delete(paneId); activePane = paneId; },
});
if (moved.ok) console.log(moved.paneId);
await kit.cli(['integration', 'install', 'codex'], { env: { CODEX_HOME: '/app/accounts/work' } });

MoveResult names the new pane as paneId; moveToAccount retains its session result as MoveToAccountResult. Both share the same per-source lock. Staging runs before verification/start; a staging exception rolls back. Replacement notification runs once after an acknowledged source close and cannot undo the move. An ambiguous close returns failure without a replacement notification, even if the source has disappeared. move clears unset in the new shell, verifies the account folder and unset names' absence, and requires an interactive agent publishing a conversation before closing the source. The pinned start API has no command-prefix/env field, so this uses shell unset rather than env -u; shells that cannot clear a variable fail closed. Only folder paths belong in set, never tokens. cli env overlays the kit's explicit env for one call; it inherits no process variables. The default move step timeout is 60 seconds.

Two ways in

  • adopt talks to the socket path the app passes: the person's own Herdr, nothing spawned, nothing written.
  • own runs the Herdr binary the app names, in an app-owned state directory.

Helpers cover the common paths (start an agent, deliver a prompt with a receipt, watch blocked agents, exact closes).

Agent readiness

kit.agentStatus(kinds) reports, per kind, whether the agent's CLI is installed and whether it is signed in — the onboarding agents-and-pickers check. It needs no Herdr connection: it probes the explicit path or, for own-mode kits, the managed launch PATH. Adopt-mode inventory uses the local machine; it is not evidence of an existing pane's environment. That inventory uses the host PATH plus the extra install dirs a service PATH omits (extraPathDirs: ~/.local/bin, the mise shims, ~/.npm-global/bin, Homebrew and the system dirs — the set muxr probed before this kit did). Sign-in comes only from each CLI's own documented non-secret status command, with a 10 s timeout; a kind whose CLI has none, or whose command gives no answer, reads unknown. Credential files are never opened, read or statted — only the signed-in boolean is kept. Install detection tells a real runnable binary apart from an auto-install launcher: a mise-style shim on PATH, or nothing on PATH at all, retains the legacy installState: 'installs-on-first-start' with installed: false. Muse is different: no executable, an auto-install shim, or an official launcher without its selected native release, reads installState: 'missing'. Stock Herdr does not install an absent Muse command. That covers pi, which Herdr installs via mise: a missing or shimmed pi reads installed: false with installs on first start, while a real pi binary reads installed: true.

import { HerdrKit } from '@byokit/herdr';

const kit = new HerdrKit({ mode: 'adopt', bin: 'herdr', socketPath: '/tmp/herdr.sock' });
// Local probe only — the kit need not be started.
console.log(await kit.agentStatus(['pi', 'claude', 'codex']));
[
  { kind: 'pi', installed: false, installState: 'installs-on-first-start', signedIn: 'unknown',
    installHint: 'installs on first start' },
  { kind: 'claude', installed: true, installState: 'installed', signedIn: 'yes',
    installHint: 'Install the claude command, then check again.' },
  { kind: 'codex', installed: true, installState: 'installed', signedIn: 'no',
    installHint: 'Install the codex command, then check again.',
    signInHint: 'On this computer run `codex`, sign in, then come back.' },
]

| Kind | Status command | Why this one | |---|---|---| | claude | claude auth status (JSON loggedIn) | The CLI's own documented status; a signed-out CLI still prints its JSON, so only an empty answer reads unknown. | | codex | codex app-server with a pipelined initialize + account/read round | Codex exposes sign-in only over its app-server protocol; a record account in the second answer means signed in. | | pi | none (Herdr installs it via mise) | Missing or shimmed: installed: false, installs on first start. A real binary reads installed: true. | | any other kind | none | No documented non-secret status command, so unknown until the kit covers it. |

Options: { path, aliases } as in installedAgentKinds; { readFile } overrides the shim sniff's file-head reader (tests use fakes); { run } injects the command runner ((command, args, { stdin, timeoutMs }) => Promise<{ stdout } | undefined>, so tests use fakes); { timeoutMs } bounds each probe. signInHint rides kinds the kit knows how to check whenever they are not signed in; installHint always rides along.

Explicit private Muse installation

installMuse is an additive Node helper. Calling it authorizes the official published installer, not sign-in or a model request. Show the source (MUSE_INSTALL_URL), destination and affected files before offering that action. No hidden download happens on import or startAgent.

import { HerdrKit, installMuse, museReadiness } from '@byokit/herdr';

const installed = await installMuse({
  home: '/app-owned/private/muse',
  path: ['/usr/bin', '/bin'], // clean host-selected tools; no inherited environment
  // installDir: '/app-owned/private/muse/tools', signal, timeoutMs: 180_000
});
if (installed.ok) {
  const { env, version } = installed.receipt;
  console.log(museReadiness(env), version); // installation only, signedIn stays unknown
  const kit = new HerdrKit({ mode: 'adopt', bin: '/app-owned/tools/herdr',
    socketPath: '/app-owned/herdr.sock' }); // the app's already running stock Herdr
  await kit.start();
  // Existing env-replacement/rollback guards still apply. Use this complete environment
  // rather than shell rc files, including for an existing pane you explicitly prepare.
  await kit.startAgent({ kind: 'muse', cwd: '/app-owned/project',
    place: { tab: 'new', workspaceId: 'project' }, env: { env, unset: [] } });
} else {
  console.log(installed.code, installed.message); // e.g. protected_download, cancelled
}

Only absolute app-managed private homes/targets are allowed. The destination must be inside that home; symlink/escape/default-home/system targets are refused. Bash/curl are required on the explicit tool PATH. The unmodified official HTTPS installer runs in a fresh staging HOME/XDG/temp, with MUSE_INSTALL_DIR, MUSE_NO_MODIFY_PATH=1, MUSE_LOGIN=0, no inherited URL/auth/profile overrides and a bounded curl policy. No credentials are copied or read from the caller's existing home. HTTPS-only redirects (at most three), per-download 60 s/512 MiB, a total deadline (default 180 s, maximum 300 s) and owned-process-group cancellation are enforced. No shell profile or persistent PATH is modified.

Success requires both the executable launcher and selected executable muse-bin-<version>; the receipt records the native release version, SHA-256 hashes, actual paths and launch env. A working installation is reused without updates. A failed/cancelled new install leaves no partial target; an existing incomplete target is preserved/refused (choose a fresh directory). The returned environment disables automatic updates and login. Hosts own these private paths and must avoid concurrent writers; filesystem validation is not an OS sandbox for publisher code. Linux/macOS x64/arm64 are supported; Windows returns unsupported_platform.

Known-missing Muse refuses with agent_not_installed / launchFailed.reason: 'not-installed' before placement/start. Preflight uses the explicit per-call environment, otherwise the kit-owned launch environment for a new pane. Controller-global inventory/probe overrides never prove readiness on a known private launch PATH. An unobserved existing/adopted pane environment remains unknown (museReadiness()), not installed. Exact typed RPC pass-through is unchanged; direct stock agent.start has upstream behavior.

Installation adds no folder/resume/native-move capability: Muse remains tab-only metadata. It does not prove sign-in, account catalog or model readiness. The official public model name is muse-spark-1.3-contributor; the installed account's own catalog still requires separate owner qualification. No provider/model fallback is performed. Previously inspected published kit versions 0.3.0 and 0.5.0 lack this helper/fix; this source change is not a published-version claim.

Agent start lifecycle

startAgent keeps its typed shape and rejection, and additionally reports the launch as events so an app shows Installing… instead of a blank start: installing (with the progress words) before a start that needs an install, ready with the fresh ref, and launchFailed with a typed reason (placement-failed | pane-busy | install-failed | start-rejected | not-installed) plus plain words. Subscribe per call with onEvent, or app-wide with kit.onStartAgent — no polling either way.

import { HerdrKit } from '@byokit/herdr';

const kit = new HerdrKit({ mode: 'adopt', bin: 'herdr', socketPath: '/tmp/herdr.sock' });
const off = kit.onStartAgent((e) => {
  if (e.phase === 'installing') console.log(e.message);       // Installing pi…
  if (e.phase === 'launchFailed') console.log(e.reason, e.message);
});
await kit.startAgent({ kind: 'pi', cwd: '/home/me/project', place: { workspace: 'new' },
  onEvent: (e) => { if (e.phase === 'ready') console.log(e.ref.paneId); } });
off();

call and subscribe are typed pass-throughs to the complete socket API, and cli() reaches the full CLI.

start() is non-fatal: if Herdr is down it rejects (state failed, e.g. failed/socket) and may be called again afterwards, so the host comes up while Herdr is down by retrying start() in a backoff loop.

Turn results

runTurn prompts an existing subscription agent and resolves after Herdr reports that it worked and returned to idle or done. Subscribe app-wide with onTurnEnd, or use the call's onEnd. An initial idle or a blocked approval does not finish the turn; keep using onBlocked/answer. No provider API or additional billing path is involved.

import { HerdrKit } from '@byokit/herdr';

const kit = new HerdrKit({ mode: 'adopt', bin: 'herdr', socketPath: '/tmp/herdr.sock' });
await kit.start();
const target = await kit.startAgent({ kind: 'codex', cwd: '/app/worktree', place: { workspace: 'new' } });
const off = kit.onTurnEnd((end) => console.log(end.changedFiles));
const end = await kit.runTurn(target, {
  prompt: 'Implement the requested change, then report whether the build passed.',
  cwd: '/app/worktree',
  timeoutMs: 10 * 60_000,
  files: { exclude: (path) => path === 'node_modules' || path === 'dist' },
  result: {
    schema: { type: 'object', required: ['buildPassed'], additionalProperties: false,
      properties: { buildPassed: { type: 'boolean' } } },
    // For larger schemas, call the app's existing JSON Schema validator here.
    validate: (value: unknown): value is { buildPassed: boolean } =>
      typeof value === 'object' && value !== null && 'buildPassed' in value &&
      typeof value.buildPassed === 'boolean' && Object.keys(value).length === 1,
  },
});
if (end.result.state === 'valid') console.log(end.result.value.buildPassed);
off();

The kit asks the agent to write { turnId, result } to a fresh JSON file in that folder before finishing. It checks the id, parses and validates the bounded file, and removes it. The result is not-requested without a policy, missing if the agent did not write it, invalid with a reason if it could not be accepted, or valid with a typed value. Unchecked payloads are never returned or logged; the transient result file is excluded from changedFiles. Default result limit: 1 MiB (result.maxBytes). The agent must follow these instructions; Herdr has no native schema-enforced response API. Task success is a separate app decision.

changedFiles lists sorted relative paths with added, modified or deleted, including untracked files and changes to already-dirty files. Snapshots compare content, modes and symlink targets, do not traverse symlinks, always exclude .git, and ignore special files. Files changed and restored during the turn do not appear. Use files.exclude to prune generated or sensitive folders. Each scan defaults to 10000 entries and 128 MiB of regular-file content; set files.maxFiles/maxBytes to change these limits. An incomplete scan rejects the call.

Reserve the pane and folder exclusively while the turn runs. Overlapping calls in one kit are refused, but changes by other apps or processes cannot be attributed to this agent. The supplied absolute cwd must match Herdr's reported working directory. Timeout, watch loss, pane closure or replacement, kit.stop() and an aborted signal reject without a completion event. Monitoring cancellation does not interrupt the agent; it may still finish or write its result file later. Herdr's working-to-idle/done detection is the end signal, not proof of semantic success. The kit does not guess completion from elapsed silence or a prompt receipt.

This helper runs on the host. Apps own branches, review/apply/rollback and authorized forwarding of results to devices; it adds no link permission or device operation.

Status

Pinned to Herdr v0.9.1. The schema snapshot (schema/herdr-api-0.9.1.json, protocol 22) generates the typed surface (src/generated/, HERDR_PROTOCOL in src/constants.ts). See docs/runtime-kits.md §11.3 for the work packages.

Isolation

The kit reads no environment variables, spawns processes with an explicit env only, and, like every byokit package, never touches a person's other AI tools. Its tests run against a fake Herdr.

Links

License

Apache-2.0. See LICENSE and NOTICE.

startAgent and openSignInTab accept the accounts launchEnv() result as env: launch. The host and Herdr must share a filesystem. New panes and existing idle POSIX shells apply the settings and unsets through a private temporary file before launch; only the file path is sent to the terminal. Busy panes and unsupported shells fail closed. Legacy record env still travels on new-pane placement, and remains refused on existing panes. Credential values must never be supplied in agent args.