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

@bitspark-ai/ba

v0.6.0

Published

ba — the bitagent family CLI runner. Mounts each service's plugin under its namespace.

Readme

ba — the bitagent family CLI

ba is one CLI for the whole bitagent family of services. Each service contributes a top-level namespace (ba runtime …, ba gateway …, …) so users learn, install, and update a single tool. The runner is TypeScript; the services it drives may be any language — ba talks to each over that service's SDK/HTTP API, never in-process.

This README is the contributor guide: if you own a bitagent service and want it to appear under ba, this is how you add your subcommand.


Table of contents


The model in 60 seconds

ba (runner)  ── owns the cross-cutting UX:
   │             dispatch · help aggregation · --version ·
   │             global --target context · session & credentials
   ├── auth           plugin → @bitspark-ai/accounts-cli              (human identity — login/out)
   ├── commons        plugin → @bitspark-ai/ba-commons-plugin         (coordination — roster/inbox/lease/…)
   ├── demo           plugin → @bitspark-ai/ba-plugin-demo            (reference plugin)
   ├── echo           plugin → @bitspark-ai/demo-cli                  (demo service — identity slice e2e)
   ├── gateway        plugin → @bitspark-ai/gateway-cli               (Claude gateway — developer)
   ├── runtime        plugin → @bitspark-ai/bitagent-runtime-cli      (durable agents)
   ├── gateway-admin  plugin → @bitspark-ai/gateway-admin-cli         (Claude gateway — operator; planned, P3)
   └── <service>      plugin → @bitspark-ai/<service>-cli

Mounted today: auth, commons, demo, echo, gateway, runtime (see packages/core/src/plugins.ts). gateway-admin is planned (P3); completion + config are P4. ba gateway invokes the agw binary at runtime via GATEWAY_AGW_BIN → an optional co-installed bitspark-agwPATH.

  • A plugin is a package that exports a single BaPlugin value.
  • Each plugin owns exactly one top-level namespace (ba <namespace> …).
  • ba mounts plugins at compile time (see Registering), esbuild-bundles them into one self-contained binary, and hands each plugin a shared BaContext so the runner — not the plugin — owns version, help, the global --target, and the session/credentials context. (Completion, richer help, and config are roadmap P4.)

Why compile-time? It gives one bundled, fully type-safe ba with zero runtime deps. The BaPlugin contract is intentionally independent of how plugins are discovered, so if we later add installed-plugin discovery (ba plugin add …), your plugin does not change — only the runner's mount step does.


Quickstart: add your subcommand

There are two halves: author a plugin in your service's repo, then register it here.

1. In your service repo, add a CLI package (convention: @bitspark-ai/<service>-cli) that exports a BaPlugin:

// packages/cli/src/index.ts
import type { BaPlugin } from "@bitspark-ai/ba-plugin-api";

export const plugin: BaPlugin = {
  namespace: "myservice",
  summary: "what my service does (one line, shown in `ba --help`)",
  register(program, ctx) {
    const ns = program.command("myservice").description("what my service does");

    ns.command("ping")
      .description("check the service is reachable")
      .option("--json", "machine-readable output")
      .action(async (opts: { json?: boolean }) => {
        const ok = await myClient(ctx).ping();
        ctx.printer.out(opts.json ? JSON.stringify({ ok }) : ok ? "ok" : "unreachable");
      });
  },
};

2. In this repo (ba-cli), wire it in — two lines:

// packages/core/package.json
"dependencies": {
  "@bitspark-ai/myservice-cli": "workspace:*"   // or a published ^version
}
// packages/core/src/plugins.ts
import { plugin as myservice } from "@bitspark-ai/myservice-cli";
export const PLUGINS: BaPlugin[] = [demo, myservice];

Done. ba myservice ping works and myservice shows up in ba --help.

The fastest way to start is to copy packages/plugin-demo — it is the reference implementation and exists purely to be copied.


The BaPlugin contract

The full contract lives in @bitspark-ai/ba-plugin-api. It is small on purpose:

export interface BaPlugin {
  /** Top-level subcommand — lowercase/hyphenated, e.g. "runtime" | "gateway-admin" ("help" is reserved). */
  readonly namespace: string;
  /** One-line summary shown in `ba --help`. Non-empty. */
  readonly summary: string;
  /** Attach your command subtree to the root program. MUST be synchronous (build it before returning). */
  register(program: Command, ctx: BaContext): void;
}

export interface BaContext {
  /** The active target/context (e.g. an env) if the user passed `--target`. Lazy. */
  readonly target: string | undefined;
  /** Output sink — use this instead of console.*. */
  readonly printer: Printer;
  /** The `ba` runner version. */
  readonly version: string;
  /** The logged-in session for the active target, if any (convenience for `credentials.get()`). */
  readonly session: Session | undefined;
  /** Read/write the local session store, scoped to the active target. */
  readonly credentials: CredentialStore;
}

export interface Printer {
  out(s: string): void;
  err(s: string): void;
}

Rules the runner relies on:

  • One plugin = one namespace. Two plugins claiming the same namespace is a hard error at startup (caught in buildProgram).
  • Keep the module side-effect-free. The file that exports your BaPlugin should do no top-level work (no network, no reading argv, no console), so the runner can import it cheaply and esbuild can tree-shake cleanly. Do all work inside .action(...) handlers.
  • register is synchronous. Build the command tree synchronously; your actions can be async.
  • The contract is commander-native (program is a commander Command). This is deliberate — see Adapting an existing commander CLI.

Authoring a plugin

Anatomy of a command

register(program, ctx) {
  const ns = program.command("myservice").description("…");   // your namespace

  ns.command("run")                                            // a verb under it
    .description("spawn a thing; returns its id")
    .argument("<task>", "the task prompt")                     // required positional
    .argument("[name]", "optional name", "default")            // optional positional w/ default
    .option("--count <n>", "how many", "1")                    // option with value
    .option("--json", "machine-readable output")               // boolean flag
    .action(async (task: string, name: string, opts: { count: string; json?: boolean }) => {
      // … do the work, print via ctx.printer …
    });
}

Conventions across the family (please follow them so ba feels like one tool):

  • Namespace = your service, lowercase, hyphenated (gateway, gateway-admin).
  • Verbs are short and imperative: run, ps, logs, stop, send, get, list. Match sibling services where it makes sense.
  • Every command that prints data supports --json for scripting. Humans get a terse default; --json gets JSON.stringify.
  • Prefer flags with values (--control-url <url>) over positional soup.

Talking to your service

ba is a thin client. Your plugin should call your service over its SDK / HTTP API, never reach into its process. Build your client inside the action (or a tiny factory) so it stays injectable for tests:

import { MyServiceClient } from "@bitspark-ai/myservice-client";

function makeClient(ctx: BaContext) {
  return new MyServiceClient({
    baseUrl: process.env.MYSERVICE_URL ?? "http://127.0.0.1:4500",
    apiKey: process.env.MYSERVICE_API_KEY,
    // resolve env-specific config from ctx.target if you support named targets
  });
}

The gateway plugins reuse the gateway's existing SDK packages — @bitspark-ai/gateway-client (transport) and @bitspark-ai/gateway-cli-targets (named env/target resolution) — rather than re-implementing transport.

Output: always use ctx.printer

Do not call console.log / console.error directly. Use ctx.printer.out and ctx.printer.err. The runner injects the printer, which is what makes your commands testable (capture output) and lets the runner control formatting/streams centrally.

The global --target context

--target <name> is a runner-global flag (e.g. select an environment). Read it via ctx.target — it is lazy, so it reflects the value parsed from argv at action time:

.action(() => {
  const env = ctx.target ?? "local";
  // …resolve config for `env`, then talk to that env's service…
});

Don't define your own --target/--env/--version on your namespace; inherit the runner's. Service-specific connection flags (--control-url) are fine to add.

Errors

Throw — don't process.exit inside an action. The runner's top-level catch in main.ts prints the message and sets exit code 1. Throw Error with a clear, user-facing message:

if (!agentId) throw new Error("myservice run: no agent id returned");

Testing your plugin

Because output and clients are injectable, you can unit-test a command without spawning a process. Mirror the runner's own smoke test:

import { describe, it, expect } from "vitest";
import { Command } from "commander";
import { plugin } from "../src/index";

function harness() {
  const lines: string[] = [];
  const program = new Command();
  program.exitOverride();                       // don't process.exit in tests
  const ctx = {
    target: undefined,
    version: "test",
    printer: { out: (s: string) => lines.push(s), err: () => {} },
    session: undefined,                          // no logged-in session in this test
    credentials: { get: () => undefined, set: () => {}, clear: () => {} }, // no-op store
  };
  plugin.register(program, ctx);
  return { program, lines };
}

it("pings", async () => {
  const { program, lines } = harness();
  await program.parseAsync(["node", "ba", "myservice", "ping", "--json"]);
  expect(lines).toEqual([JSON.stringify({ ok: true })]);
});

(For real I/O, inject a fake client via a deps param on your plugin factory, the way the runtime CLI does with CliDeps.makeClient.)


Registering your plugin with ba

Mounting is compile-time and explicit — there is exactly one place to look:

  1. Export a BaPlugin from your CLI package (above).
  2. Depend on it in packages/core/package.json (workspace:* for local dev, or a published ^version once your package ships).
  3. Add it to PLUGINS in packages/core/src/plugins.ts:
import { plugin as myservice } from "@bitspark-ai/myservice-cli";

// The mounted set today (packages/core/src/plugins.ts) is [demo, auth, echo, runtime];
// append yours. (gateway/gateway-admin are commented "planned additions" there.)
export const PLUGINS: BaPlugin[] = [demo, auth, echo, runtime, myservice];

The runner sorts namespaces for stable help and refuses duplicates. After wiring, pnpm build bundles your plugin into the one ba binary.


Naming & packaging conventions

| Thing | Convention | Example | |---|---|---| | CLI package name | @bitspark-ai/<service>-cli | @bitspark-ai/gateway-cli | | Plugin export | a named/plugin export of type BaPlugin | export const plugin: BaPlugin | | Namespace | lowercase, hyphenated, = the service | gateway, gateway-admin | | Client dep | @bitspark-ai/<service>-client (the SDK) | @bitspark-ai/gateway-client |

The package name need not equal the namespace — the first-party family diverges for product naming: @bitspark-ai/accounts-cli ships the auth namespace and @bitspark-ai/demo-cli ships echo. isBaPlugin enforces only the namespace grammar (lowercase, hyphenated, leading letter; help is reserved), not the package name.

Keep the plugin package thin: command wiring + your SDK. No business logic that belongs in the service.


One service, multiple plugins — the gateway case

A single service repo can contribute more than one plugin (more than one namespace). The Claude gateway (claude-gateway, soon bitagent/gateway) is the canonical example — it contributes two:

| Namespace | Package | Audience | Contains | |---|---|---|---| | gateway | @bitspark-ai/gateway-cli | developer (public) | auth/setup, connection, projects — day-to-day developer use | | gateway-admin | @bitspark-ai/gateway-admin-cli | operator | deploy, ops, capacity, profiles, routes, retention, audit |

Why two plugins instead of one role-aware command:

  • Structural boundary. Operator code lives in a package the developer plugin never depends on. The public developer surface cannot ship operator/deploy logic by construction — it's a dependency-graph fact, not a build-time strip. (This replaces the gateway's old single agw binary with its --public esbuild stub + content audit.)
  • Two distinct UXs. Developers and operators are different audiences; each gets a focused namespace, help, and setup flow. Someone who is both installs a ba that has both namespaces.

Both gateway plugins are built on the same gateway SDK packages (@bitspark-ai/gateway-client, @bitspark-ai/gateway-cli-targets), so the API layer is shared, not duplicated — only the command surfaces differ.

If your service has a similar split (a public surface and a privileged one), follow this pattern: two packages, two namespaces, one shared SDK.


Adapting an existing commander CLI

If your service already has a commander CLI (like the runtime CLI, which already does program.name("ba").command("runtime")…), adapting it is nearly mechanical: move the body that builds your program.command(ns) subtree into register, and take program + ctx as inputs instead of creating your own Command and reading process.env/console directly.

// before:  buildProgram(): Command { const program = new Command(); const rt = program.command("runtime")…; return program; }
// after:
export const plugin: BaPlugin = {
  namespace: "runtime",
  summary: "durable, decoupled, movable agents",
  register(program, ctx) {
    const rt = program.command("runtime").description("durable, decoupled, movable agents");
    rt.command("run") /* …unchanged… */;
    // swap console.log → ctx.printer.out; drop your own --version.
  },
};

Keep your service's own standalone bin during the transition if you want; it can import the same command-building code the plugin uses.


Adapting a service with its own command model

If your service describes commands as data (e.g. the gateway's declarative command registry with role/capability gating), don't fight it — write a small adapter in register that walks your data and calls program.command(...), mapping your gating to a pre-action check:

register(program, ctx) {
  const ns = program.command("gateway-admin").description("…");
  for (const entry of OPERATOR_COMMANDS) {           // your declarative registry
    const cmd = ns.command(entry.name).description(entry.summary);
    for (const opt of entry.options ?? []) cmd.option(opt.flag, opt.desc);
    cmd.action((...args) => {
      assertCapability(entry.group, ctx);            // your gating, surfaced as an error
      return entry.dispatch({ ctx, args });
    });
  }
}

The runner stays oblivious to your internal model; it only sees commander commands.


Cross-repo local development

The family is federatedba-cli, runtime, gateway, etc. are separate repos in sibling directories under C:\Development\bitspark\bitagent\, each its own pnpm workspace. Two ways to develop a plugin against a live ba:

  • Published version (steady state): your *-cli package is published; ba-cli depends on a ^version. Bump to upgrade.
  • Local link (while iterating): point the dependency at your working copy via a pnpm overrides entry or pnpm link, so ba bundles your local plugin source. (If the family later consolidates into one workspace, this becomes plain workspace:*.)

Today the mounted sibling plugins are wired as workspace:* members in pnpm-workspace.yaml (../runtime/*, ../commons/*).

Build prerequisite for dist-first plugins. Most family packages are source-first (main./src/index.ts), so esbuild/tsc read their TypeScript directly — no prebuild. A few are dist-first (main./dist), notably the commons plugin chain (@bitspark-ai/ba-commons-plugin@bitspark-ai/commons-client@bitspark-ai/commons). For those, the sibling workspace must be built before ba is type-checked or bundled here, or pnpm check/pnpm build can't resolve their ./dist. In a fresh checkout (or after commons changes):

pnpm -C ../commons install --frozen-lockfile && pnpm -C ../commons build
# then, in this repo:
pnpm install && pnpm check && pnpm test && pnpm build

Run the runner without building:

pnpm exec tsx packages/core/src/main.ts <args>      # e.g. … myservice ping --json

Gotcha: pnpm run dev -- --flag forwards a literal -- into argv, which commander treats as end-of-options. Use pnpm exec tsx … (above) when passing top-level flags like --help/--version/--target.


Contributor checklist

Before you open a PR to add your plugin:

  • [ ] Plugin module is side-effect-free; all work is inside .action.
  • [ ] Exactly one namespace, lowercase/hyphenated, named after your service.
  • [ ] All output goes through ctx.printer (no console.*).
  • [ ] Data commands support --json.
  • [ ] No custom --version/--target; inherit the runner's.
  • [ ] Actions throw on failure (no process.exit).
  • [ ] You talk to your service via its SDK, with an injectable client.
  • [ ] At least one vitest test that parses argv and asserts on captured output.
  • [ ] Registered in packages/core (dependency + PLUGINS entry).
  • [ ] pnpm check, pnpm test, and pnpm build are green.

Repo layout & commands

packages/
  plugin-api/       @bitspark-ai/ba-plugin-api   — the BaPlugin contract (the linchpin)
  core/             @bitspark-ai/ba              — the runner + bin `ba` + compile-time mount
  plugin-demo/      @bitspark-ai/ba-plugin-demo  — reference plugin / copy-me template
  accounts-client/  @bitspark-ai/accounts-client — isomorphic accounts SDK (login + session keys)
  accounts-cli/     @bitspark-ai/accounts-cli    — the `ba auth` plugin (human identity)
  demo-cli/         @bitspark-ai/demo-cli        — the `ba echo` plugin (identity slice, end to end)
pnpm install
pnpm exec tsx packages/core/src/main.ts --help     # run the runner via tsx
pnpm exec tsx packages/core/src/main.ts demo greet Ada
pnpm check                                          # tsc --noEmit across packages
pnpm test                                           # vitest
pnpm build                                          # esbuild → one self-contained `ba`

Toolchain: Node 24+, Corepack-pinned [email protected], TypeScript 6, commander 12, vitest 4 — matching the rest of the bitagent family.


Distribution (publishing)

ba ships as a single self-contained bundlepackages/core/dist/main.js with zero runtime dependencies (esbuild inlines every plugin + commander at build time). End users install one package and need no sibling runtime/commons/gateway checkouts.

The source packages/core/package.json stays private and keeps its workspace:* plugins as devDependencies (build-time only). The publishable manifest is derived by the dry-run packer (no runtime deps, not private, bin ba → ./dist/main.js, files: ["dist/main.js"], publishConfig.access: public).

Dry-run packaging gate (proves the tarball without publishing):

# family prerequisites (fresh checkout): commons is dist-first, so build it first
pnpm -C ../commons install --frozen-lockfile && pnpm -C ../commons build
pnpm install && pnpm check && pnpm test && pnpm build
# pack → install OUTSIDE the monorepo → smoke the bundled CLI:
pnpm --filter @bitspark-ai/ba publish:dryrun

packages/core/scripts/publish-dryrun.mjs packs the derived manifest and asserts the packed tarball (name, non-0.0.0 version, not private, no workspace:*, empty runtime deps, bin, file allowlist, no gateway-admin), installs it in a temp dir outside the workspace, and smokes ba --help, ba gateway|runtime|commons --help, and the missing-agw setup hint.

Version: the dry-run stamps 0.0.0-dryrun; a real release sets BA_PUBLISH_VERSION. No real npm publish happens in the dry-run.

Real publish

The real publish reuses the dry-run packer, so what ships is exactly what the gate verified. It is heavily guardedscripts/publish.mjs refuses unless BA_PUBLISH_VERSION is a real semver (not 0.0.0/-dryrun), only publishes when BA_PUBLISH_CONFIRM=1 (otherwise it runs the gate and prints a preview), and needs an npm auth token. In CI it emits build provenance via OIDC.

Preferred path — the manual workflow .github/workflows/publish.yml (Actions → publish → Run workflow): it checks out the sibling family, builds commons (dist-first), gates, then runs the guarded publish. It is a dry preview unless you type publish in the confirm box. It uses the org-wide secrets NPM_TOKEN (publish rights to @bitspark-ai) and BITSPARK_CI_TOKEN (read access to the sibling repos for the family checkout — the default GITHUB_TOKEN is this-repo only), both already provided on the Bitspark org, so there is nothing extra to configure.

Local equivalent (from a built family workspace, with an npm token configured):

BA_PUBLISH_VERSION=0.1.0 pnpm publish:npm                 # preview (runs the gate, no publish)
BA_PUBLISH_VERSION=0.1.0 BA_PUBLISH_CONFIRM=1 pnpm publish:npm   # actually publishes

Roadmap

  • P0 — runner foundation (done): BaPlugin contract, compile-time mount, reference plugin, smoke tests, esbuild bundle.
  • P1 — runtime plugin (done): adapted @bitspark-ai/bitagent-runtime-cli (a commander program.command("runtime")…) to export a BaPlugin; mounted as runtime.
  • Identity slice (done): ba auth over @bitspark-ai/accounts-client (login → session-key delegation), a runner-owned, target-scoped session/credential store, and ba echo proving login → service-use end to end.
  • Commons plugin (done): mounted @bitspark-ai/ba-commons-plugin as commons (the first external service to adopt the frame) — roster/inbox/lease/message/broadcast/decision.
  • P2 — gateway developer plugin (done): mounted @bitspark-ai/gateway-cli as gateway (developer command surface: status, target, projects, key, runs, usage, auth). Consumer-safe per gateway #2771; the agw binary is resolved at runtime (GATEWAY_AGW_BIN → optional co-installed bitspark-agwPATH).
  • P3 — gateway operator plugin: extract the operator surface + deploy/ops into @bitspark-ai/gateway-admin-cli; mount as gateway-admin.
  • P4 — shared runner UX: lift completion, richer help, and config from the gateway CLI into the runner so every plugin inherits them. (Runner-owned session/credentials already landed with the identity slice.)
  • P5/P6 — distribution (packaging done; first publish pending): a dry-run gate (pnpm publish:dryrun) proves a clean, self-contained, zero-runtime-dep tarball that installs + runs outside the monorepo, and a guarded real-publish path (pnpm publish:npm / .github/workflows/publish.yml, OIDC provenance, npm-token + explicit-confirm gated) is in place, wired to the org-wide NPM_TOKEN + BITSPARK_CI_TOKEN secrets. See Distribution. Pending: merge the publish workflow + run the first real publish; then retire the standalone per-service bins.