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

@liche/core

v0.8.2

Published

Bun-native CLI runtime framework for Liche.

Readme

@liche/core

Write a command as a typed function. Get a terminal CLI and extension-driven transports (agent tools, HTTP routes) from it — sharing the same parsing, validation, and result envelope. parseInvocation() exposes that pipeline as a pure function when you need the contract without running the handler.

import { defineCommand, z } from "@liche/core";

const deploy = defineCommand({
  path: ["deploy"],
  summary: "Deploy the shipyard",
  input: { options: z.object({ entrypoint: z.string() }) },
  output: z.object({ deployment_id: z.string() }),
  run({ input }) {
    return { deployment_id: `dep-${input.options.entrypoint}` };
  },
});

defineCli collects one or more commands into a runnable CLI:

import { defineCli, run } from "@liche/core";

export const cli = defineCli({
  name: "shipyard",
  version: "0.1.0",
  commands: [deploy],
});

if (import.meta.main) await run(cli);      // terminal

The same cli projects onto other transports without changing the handler. shipyard deploy --entrypoint app on the terminal turns argv flags into the options object; an agent tool or an HTTP route reaches the same run({ input }) handler, validates against the same Zod schema, and returns the same { ok, data, error } envelope. Validation failures keep track of where the value entered the pipeline, so diagnostics can point at --entrypoint or the originating env var instead of only saying $.entrypoint.

HTTP serving is composed by mounting fetch routes rather than guessing a command from the URL: @liche/mcp-server mounts /mcp, and @liche/product generates a spec server from a product contract. Additional transports (agent tools, etc.) are added by extensions; see @liche/extensions.

Transports

A transport is how an invocation reaches your command — what form the input arrives in, and how the result goes back out. The same command graph can be reached through any of these without changing the handler:

  • run(cli, argv?) — terminal transport. Reads Bun.argv.slice(2) by default, writes stdout/stderr, exits with a status code.
  • cli.fetch(request) — HTTP transport with the Web fetch shape. Pass it to Bun.serve({ fetch: cli.fetch }). It walks the mounted fetchRoutes (first match wins) and returns a structured 404 for unmatched paths — there is no generic path→command dispatch. Mount routes with defineCli({ fetchRoutes }), @liche/mcp-server (/mcp), or the spec server generated by @liche/product.
  • Extension-driven transports live in @liche/extensions and other extension packages — they reuse the same command graph through the public adapter surface.

Defining commands

  • defineCli() — root: name, version, commands, globals, extensions, hooks, middleware.
  • defineCommand() — one command: path, input (args, options, env, vars), output, run, optional interactive, format, and single-segment string aliases.
  • defineGlobal() — CLI-wide flag that feeds parsing, help, and ctx.global. Globals have a key, an optional alias, a type, and choose how they're exposed.
import { defineCli, defineCommand, defineGlobal, z } from "@liche/core";

const verbose = defineGlobal({
  key: "verbose",
  alias: "v",
  type: "boolean",
  expose: "context",
});

const status = defineCommand({
  path: ["status"],
  input: { options: z.object({ region: z.string().optional() }) },
  run: ({ input, ctx }) => ({
    region: input.options.region ?? null,
    verbose: ctx.global.verbose,
  }),
});

defineCli({
  name: "shipyard",
  version: "0.1.0",
  globals: [verbose],
  commands: [status],
});

CLI input codecs

Numeric and boolean flags arrive from argv as strings. The arg namespace exposes strict Zod schema factories that reject the usual sloppy coercions ("+3", "1e3", "Infinity", leading zeroes, whitespace) while staying composable with .optional(), .default(...), and .describe(...):

import { arg, defineCommand, z } from "@liche/core";

defineCommand({
  path: ["deploy"],
  input: {
    options: z.object({
      replicas: arg.positiveInt().default(1).describe("Number of replicas"),
      port: arg.port().default(3000),
      yes: arg.boolean().default(false),
    }),
  },
  run({ input }) {
    input.options.replicas; // number
  },
});

Available built-ins: arg.number(), arg.int(), arg.positiveInt(), arg.port(), arg.boolean(). Plain Zod schemas (z.string(), z.enum(...), z.object(...), refinements) remain valid; reach for arg.* only when a value crosses a string boundary (argv, env, query string, JSON body) and the runtime value is not a string.

Custom codecs

arg.fromString() is the escape hatch for domain types. Define the boundary input schema, the runtime output schema, and a decode (async OK):

import { arg, defineCommand, z } from "@liche/core";

const url = arg.fromString({
  input: z.url().meta({ valueLabel: "url" }),
  output: z.instanceof(URL),
  surface: "all",
  decode: (raw) => new URL(raw),
  encode: (value) => value.toString(),
});

defineCommand({
  path: ["fetch"],
  input: { options: z.object({ target: url }) },
  run({ input }) {
    input.options.target; // URL
  },
});

arg.fromString() defaults to surface: "cli" so a codec that reads local files or shells can't accidentally project onto an extension transport. Set surface: "all" or { kind: "extension", transport: "..." } to opt in; unsupported surfaces return a structured UNSUPPORTED_SURFACE error before the handler runs.

Source-aware validation errors

The same schema failure now tells you where the bad value came from. Given options: z.object({ port: arg.port() }):

| Caller | fieldErrors[0].source | | --------------------------------- | ------------------------------------------------------------------ | | myapp start --port 70000 | { kind: "argv", flag: "--port" } | | myapp start with PORT=70000 | { kind: "env", name: "PORT" } | | Extension tool input | { kind: "extension", transport: "...", key: "port" } | | Config provider binding | { kind: "provider", provider: "config", path: "server.port" } | | Positional arg | { kind: "argv", positional: 0 } | | Direct dispatch()/execute() | { kind: "programmatic", key: "port" } |

Adapters thread the source automatically — extension transports tag their own transport name, argv records the exact flag form (-p, --port, --no-port). The human renderer uses source when present and falls back to path-based inference otherwise, so existing CLIs keep their current error output.

The raw input value never enters FieldError. Only safe type labels appear under received ("string", "array", "NaN", "undefined", ...), so logging a ValidationError won't leak the secret-looking value that triggered it.

Parse without running

parseInvocation() returns everything dispatch() would compute up to calling the handler — selected command contract, decoded input, source provenance, warnings, resolved format. No stdout, no lifecycle events, no process.exit.

import { parseInvocation } from "@liche/core";

// Preview what `shipyard deploy --replicas 3` would do, without doing it.
const preview = await parseInvocation(cli, ["deploy", "--replicas", "3"]);

if (preview.ok) {
  preview.data.command.name;            // "deploy"
  preview.data.input.options;           // { replicas: 3 }  (already decoded)
  preview.data.sources.option("replicas"); // { kind: "argv" }
  preview.data.warnings;                // deprecation notices, etc.
} else {
  preview.error.fieldErrors;            // validation errors carry source too
}

Useful when a tool server needs the contract before deciding to dispatch, when a UI renders a confirmation step, or when tests want to assert on resolved input without mocking effects.

Returning results

Handlers usually return plain data; the executor validates it against output and wraps it in a success envelope:

defineCommand({ path: ["status"], run: () => ({ ready: true }) });

Use ctx.ok(data, meta) when you need result metadata such as CTA blocks, and ctx.error(error) for expected structured failures. ok() and fail() are the same primitives outside a ctx. Async generators are supported for streaming output.

defineCommand({
  path: ["deploy"],
  input: { options: z.object({ entrypoint: z.string() }) },
  run({ ctx, input }) {
    if (!input.options.entrypoint.startsWith("./")) {
      return ctx.error({
        code: "INVALID_ENTRYPOINT",
        message: "Entrypoint must be a relative path",
      });
    }
    return ctx.ok(
      { deployment_id: `dep-${input.options.entrypoint}` },
      { cta: { primary: { label: "View deployment", url: "https://example.com/deps" } } },
    );
  },
});

Never hand-write { ok, data, error } objects — only these helpers produce real envelopes.

Rendering output

Built-in renderers: json, jsonl, yaml, md, csv. Commands can set their default format; explicit output-control globals (e.g. --json) override it. Register a custom renderer with defineOutputRenderer() and expose it through outputControls:

import { defineCli, defineOutputRenderer, outputControls } from "@liche/core";

const xml = defineOutputRenderer({
  name: "xml",
  mediaType: "application/xml",
  render(value) {
    return toXml(value);
  },
});

defineCli({
  name: "shipyard",
  outputRenderers: [xml],
  extensions: [outputControls({ format: true, formats: ["json", "xml"] })],
  commands: [deploy],
});

Built-in controls

Core has no implicit behavior. There is no automatic --help, --version, --json, or --schema. Install controls explicitly:

import { defineCli, help, outputControls, reflectionControls, version } from "@liche/core";

defineCli({
  name: "shipyard",
  extensions: [help(), version(), outputControls({ json: true }), reflectionControls({ schema: true })],
  commands: [],
});

Extensions

defineCli({ extensions }) composes bundles of commands, globals, input sources, output renderers, events, hooks, middleware, and packaged skill content.

Canonical optional helpers — config files, shell completions, MCP and skill installers, agent installation flows, auth, telemetry — live in @liche/extensions. For example, agents() bundles MCP server routing and the mcp add / skills add installers:

import { defineCli, help, version, outputControls } from "@liche/core";
import { agents } from "@liche/extensions";

defineCli({
  name: "shipyard",
  extensions: [
    help(),
    version(),
    outputControls({ json: true }),
    agents(),
  ],
  commands: [deploy],
});

One line, and shipyard is also an MCP server. Run shipyard mcp add to install it into Claude Code, Cursor, or any MCP-aware client. shipyard skills add publishes its bundled skill docs into the same environments, so agents discover the tool and know how to drive it. Live tool calls hit cli.fetch at /mcp. See the @liche/extensions README for the full catalog.

Lower-level primitives

  • callHttpOperation()outbound HTTP for remote commands that call external APIs. (Not to be confused with cli.fetch, which is the inbound HTTP server.)
  • secret() — redaction primitive for values that should not stringify or inspect as raw secrets. Auth workflow and header helpers live in @liche/auth.

Requirements

Bun >= 1.3.0. This package ships Bun-only TypeScript source — no dist, no declaration artifacts.

Going further

  • SKILL.md — guidance for agents authoring or maintaining @liche/core CLIs. A CLI can publish its own skill via defineCli({ skill: { markdown, index } }), installed by skillsInstaller().
  • @liche/extensions — the canonical optional helper bundle.
  • @liche/product generates Product CLIs on top of this runtime. @liche/build compiles CLIs. @liche/releases packages compiled artifacts.