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

@llui/agent

v0.0.55

Published

LLui Agent — LAP server + browser client runtime for driving LLui apps from LLM clients

Readme

@llui/agent

Server and browser-client libraries for the LLui Agent Protocol (LAP).

What this buys you

Your app's users can install the llui-agent bridge into Claude Desktop once, paste a token you mint for them, and drive your LLui app from Claude. Same Msgs and State you're already using — Claude dispatches like a remote user.

Install

pnpm add @llui/agent @llui/effects ws
pnpm add -D @llui/vite-plugin  # if not already present

Enable agent-metadata emission in vite.config.ts:

import llui from '@llui/vite-plugin'
export default { plugins: [llui({ agent: true })] }

Server

import { createLluiAgentServer } from '@llui/agent/server'
import express from 'express'

const agent = createLluiAgentServer({
  identityResolver: async (req) => req.cookies.user_id ?? null,
})

const app = express()
// The router is Web-standards; adapt it:
app.use('/agent', async (req, res) => {
  const webReq = expressToWebRequest(req) // adapter
  const webRes = await agent.router(webReq)
  if (!webRes) {
    res.status(404).end()
    return
  }
  webRes.headers.forEach((v, k) => res.setHeader(k, v))
  res.status(webRes.status).send(await webRes.text())
})

const server = app.listen(8787)
server.on('upgrade', agent.wsUpgrade)

Client

// @doc-skip — illustration uses `...` placeholders for handlers
import { mountApp } from '@llui/dom'
import { createAgentClient, agentConnect, agentConfirm, agentLog } from '@llui/agent/client'
import { handleEffects } from '@llui/effects'
import { App } from './App'

const root = document.getElementById('app')!
const handle = mountApp(root, App)

const client = createAgentClient({
  handle,
  def: App,
  rootElement: root,
  slices: {
    getConnect: (s) => s.agent.connect,
    getConfirm: (s) => s.agent.confirm,
    wrapConnectMsg: (m) => ({ type: 'agent', sub: 'connect', msg: m }),
    wrapConfirmMsg: (m) => ({ type: 'agent', sub: 'confirm', msg: m }),
  },
})
client.start()

// Chain client.effectHandler into your onEffect:
const onEffect = handleEffects<MyEffect | AgentEffect>()
  .when('http', ...)
  .else(client.effectHandler)

App-side annotations

// @doc-skip — illustration uses `...` placeholders for init/update/view
type Msg =
  /** @intent("Increment the counter") */
  | { type: 'inc' }
  /** @intent("Delete item") @requiresConfirm */
  | { type: 'delete', id: string }
  /** @intent("Place order") @humanOnly */
  | { type: 'checkout' }
  /** @intent("Navigate") @alwaysAffordable */
  | { type: 'nav', to: 'reports' | 'settings' | 'home' }

export const App = component<State, Msg, Effect>({
  name: 'App',
  init: ...,
  update: ...,
  view: ...,
  agentAffordances: (state) => [
    { type: 'nav', to: 'reports' },
    ...(state.user ? [{ type: 'signOut' }] : []),
  ],
  agentDocs: {
    purpose: 'Kanban for a 3-person design team.',
    overview: 'Columns: To do / Doing / Done. Cards carry owner, due date, tags.',
    cautions: ['Moving to Done locks edits — reopen first.'],
  },
  agentContext: (state) => ({
    summary: `Viewing board "${state.boardName}", ${state.cards.length} cards visible.`,
    hints: state.selectedCard
      ? ['Card focused; enter advances status.']
      : ['Tab to list, arrow to select.'],
  }),
})

Annotations reference

| Tag | Semantics | | ------------------- | -------------------------------------------------------------------- | | @intent("...") | Human-readable label for Claude + confirmation UI + log | | @alwaysAffordable | Surfaces to Claude even when no live UI binding is currently mounted | | @agentOnly | No human UI binding exists; agent is the sole dispatcher | | @requiresConfirm | Claude must propose; user approves before dispatch | | @humanOnly | Claude cannot dispatch; not in list_actions |

Default (no tag). A 'shared' variant — both audiences can dispatch — is offered to the agent only when a live UI binding maps to it (e.g. an onClick is currently mounted). When the user navigates away and the corresponding subtree unmounts, the variant disappears from list_actions. This mirrors what the human can click. Tag @alwaysAffordable (or list the Msg from agentAffordances(state)) to keep an agent-driven Msg available regardless of UI state.

App state shape (host integration)

Wire your root state and Msg to include agent sub-slices:

type State = {
  // ...your app state...
  agent: {
    connect: agentConnect.State
    confirm: agentConfirm.State
    log: agentLog.State
  }
}

type Msg =
  // ...your app msgs...
  | { type: 'agent'; sub: 'connect'; msg: agentConnect.Msg }
  | { type: 'agent'; sub: 'confirm'; msg: agentConfirm.Msg }
  | { type: 'agent'; sub: 'log'; msg: agentLog.Msg }

Delegate in update:

update: (state, msg) => {
  if (msg.type === 'agent') {
    if (msg.sub === 'connect') {
      const [connect, effects] = agentConnect.update(state.agent.connect, msg.msg)
      return [{ ...state, agent: { ...state.agent, connect } }, effects]
    }
    if (msg.sub === 'confirm') {
      const [confirm, effects] = agentConfirm.update(state.agent.confirm, msg.msg)
      return [{ ...state, agent: { ...state.agent, confirm } }, effects]
    }
    if (msg.sub === 'log') {
      const [log, effects] = agentLog.update(state.agent.log, msg.msg)
      return [{ ...state, agent: { ...state.agent, log } }, effects]
    }
  }
  // ...your app logic...
}

View wiring

Render agentConnect, agentConfirm, and agentLog anywhere in your view tree:

view: ({ send, branch, show }) => {
  const connectParts = agentConnect.connect(
    (s) => s.agent.connect,
    (m) => send({ type: 'agent', sub: 'connect', msg: m }),
  )
  // `mintUrl` is optional — the agent effect handler derives it from
  // `EffectHandlerHost.agentBasePath` (default `/agent`). Pass an
  // explicit `{ mintUrl }` only when minting from a non-default path.

  const confirmParts = agentConfirm.connect(
    (s) => s.agent.confirm,
    (m) => send({ type: 'agent', sub: 'confirm', msg: m }),
  )

  return [
    // Renders the "Connect with Claude" button + token copy box + session list:
    div(connectParts.root, [
      button(connectParts.mintTrigger, ['Connect with Claude']),
      ...show({
        when: (s) => s.agent.connect.pendingToken !== null,
        render: () => [
          pre(connectParts.pendingTokenBox),
          button(connectParts.copyConnectSnippetButton, ['Copy']),
        ],
      }),
    ]),
    // Renders pending confirmation cards:
    div(confirmParts.root),
  ]
}

Entry points

  • @llui/agent/protocol — all LAP types, WS frame types, token types, audit types.
  • @llui/agent/servercreateLluiAgentServer, InMemoryTokenStore, consoleAuditSink, interfaces.
  • @llui/agent/clientcreateAgentClient, agentConnect, agentConfirm, agentLog, AgentEffect.

See the Agent Protocol doc for the full wire protocol and security model.

Custom serialization (codecs)

JSON natively supports string | number | boolean | null | array | object. Component messages and state often carry values that don't round-trip through JSON: Date, Blob, File, Map, Set, BigInt. The agent ships a codec convention that lets these values cross the LAP boundary cleanly without forcing every component to invent its own envelope.

Wire format. A non-JSON-safe runtime value travels as a tagged object:

{ "__codec": "iso-date", "wire": "2026-04-25T12:00:00.000Z" }

The runtime walks every value crossing the LAP boundary symmetrically:

  • Outgoing (stateAfter snapshots, action arguments): the encoder looks up a codec whose matchesRuntime claims the value and replaces it with the tagged shape.
  • Incoming (msg payloads dispatched by the agent): the decoder detects the tagged shape and substitutes the runtime value before update() runs.

Reducers never see the tagged form. By the time update() is called, real Date / Blob / etc. are in place.

Default codecs. makeDefaultCodecs() ships with:

| Codec name | Encodes | Wire form | | -------------- | ------- | ------------------ | | iso-date | Date | ISO 8601 string | | epoch-millis | Date | epoch milliseconds |

By default iso-date claims Date values. epoch-millis is registered but its matchesRuntime returns false so it doesn't shadow iso-date on encode — it's still available for explicit decode and for consumers who register a millis-first registry.

Authoring. Tag the variant's JSDoc with both @intent and @codec("<name>"):

type DateInputMsg = {
  /**
   * @intent("Set the parsed date directly")
   * @codec("iso-date")
   */
  type: 'setValue'
  value: Date | null
}

The @codec tag is documentation for human readers and the (eventual) schema generator. The runtime encode/decode is registry-driven and doesn't need per-field metadata at runtime.

Custom codecs. Pass a registry to createAgentClient:

// @doc-skip — illustration uses placeholder encode/decode bodies
import { CodecRegistry, isoDateCodec } from '@llui/agent/codecs'

const codecs = new CodecRegistry()
codecs.register(isoDateCodec)
codecs.register({
  name: 'base64-blob',
  matchesRuntime: (v) => v instanceof Blob,
  encode: async (b) => ({ name: b.name, type: b.type, base64: '...' }),
  decode: (wire) => new Blob([], { type: wire.type }),
})

createAgentClient({ ..., codecs })

File/Blob codecs are not in the default registry — handling is environment-specific (browser File API vs. Node Buffer vs. workers) and the encoded form is large enough that consumers should opt in deliberately.

Cloudflare-vite dual paths (dev only)

When a project ships @cloudflare/vite-plugin, that plugin proxies every non-/cdn-cgi/* request to the worker — which shadows the canonical /agent/* LAP routes. To keep agent flows working in dev, @llui/vite-plugin registers the LAP middleware at both /agent/* and /cdn-cgi/agent/*, and the client opts in by passing agentBasePath: '/cdn-cgi/agent'.

This dual-path lives in the dev middleware only. Production deployments serve LAP routes from your own server (Express, Hono, the Cloudflare Worker in @llui/agent/server/cloudflare, etc.); whichever base path you mount there is what the client should target. The /cdn-cgi/* shim does not exist in production and you do not need it — pick one canonical path (/agent or whatever you mount), point agentBasePath at it, and you're done.