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

@glubean/port

v0.2.3

Published

Neutral runtime adapter for local coding agents.

Readme

Glubean Port

Glubean Port is a small runtime adapter layer for local coding agents.

The MVP exposes a neutral session + turn + event + capability API and includes Codex app-server, Claude Code, and Gemini ACP providers. Provider-specific runtime details are kept behind a typed adapter boundary, so callers can build their own orchestration on top without inheriting another caller's workflow concepts.

Scope

  • @glubean/port core types and createPort().
  • Codex app-server over line-delimited JSON-RPC on stdio.
  • Claude Code over --output-format stream-json.
  • Gemini over ACP JSON-RPC on stdio.
  • Event normalization from provider notifications into AgentEvent.
  • Token usage telemetry via token.usage events when the provider reports it.
  • Structured JSON turns with optional Zod 4 validation and retry.
  • Local lifecycle state for sessions, managed turns, cancellation, wait results, and in-memory event replay with sequence cursors.
  • Fixture tests that do not require a real Codex login or model call.

Not in this MVP:

  • Caller orchestration state.
  • Domain-specific high-level APIs.

API Shape

import { createPort } from "@glubean/port"

const port = await createPort({
  provider: "codex",
  cwd: process.cwd(),
})

const session = await port.sessions.create({
  approvalPolicy: "on-request",
  sandbox: "workspace-write",
})

for await (const event of port.turns.start({
  sessionId: session.id,
  input: [{ type: "text", text: "Review the current diff." }],
})) {
  console.log(event)
}

await port.close()

Managed lifecycle:

const session = await port.sessions.create()

const turn = await port.turns.submit({
  sessionId: session.id,
  input: [{ type: "text", text: "Work on the leased task." }],
  timeoutMs: 5 * 60_000,
})

for await (const event of port.turns.stream({ ...turn, afterSeq: 0 })) {
  console.log(event.seq, event.type)
  if (event.type === "turn.completed") break
}

const result = await port.turns.wait(turn)
console.log(result.status, result.text)

Structured output:

import { z } from "zod"
import { createPort } from "@glubean/port"

const port = await createPort({ provider: "claude", cwd: process.cwd() })
const session = await port.sessions.create()

const result = await port.turns.startStructured({
  sessionId: session.id,
  input: [{ type: "text", text: "Return { ok: true }." }],
  schema: z.object({ ok: z.literal(true) }),
  maxRetries: 2,
})

console.log(result.data)

Streaming partial JSON: when callers want progressive structure (rather than the all-or-nothing startStructured contract), Port ships an opt-in @glubean/port/partial subpath (EXPERIMENTAL until at least one external caller besides gstorm adopts it). It exposes a hardened parser, a generic snapshot diff, and a stream-sugar generator:

import { streamPartialJson } from "@glubean/port/partial"

async function* texts() {
  for await (const ev of port.turns.start({ sessionId: session.id, input })) {
    if (ev.type === "message.delta") yield ev.text
    if (ev.type === "turn.completed") return
  }
}

for await (const step of streamPartialJson(texts())) {
  if ("final" in step) {
    handleFinal(step.final)        // null if nothing parsed
  } else {
    for (const patch of step.patches) {
      // patch is { op: "set", path, value } or { op: "append", path, chars }.
      // Translate to your wire / UI vocabulary.
    }
  }
}

Patch records describe WHAT changed by path; how to render is caller policy. The submodule never emits "delete" patches — the partial parser can briefly drop a key when trailing bytes invalidate it, then restore it on the next delta — surfacing that as a delete would cause UI flicker.

For lower-level access, parsePartialJson(buffer) and diffPartialSnapshots(prev, next) are also exported.

If you only need a parser (no diff) and don't mind a third-party dep, the partial-json npm lib is a fine alternative: parse(accumulated, Allow.ALL) on each delta gives you a snapshot. Use startStructured instead when you need final-shape validation or retry on malformed output.

Local Development

Node 24 can run the TypeScript sources directly.

npm test
npm run typecheck

To run live provider integrations, make sure codex, claude, and gemini are installed and authenticated:

npm run test:integration

To run a live Codex smoke:

npm run smoke:codex -- "Say ok and stop."

Design Notes

The public Port API is intentionally not a mirror of any provider:

  • Codex uses thread and turn.
  • ACP providers use session and prompt.
  • Claude Code currently exposes a stream-json CLI shape rather than a public JSON-RPC appserver.

Port keeps provider-specific names in the adapter and exposes stable terms upward:

  • sessions
  • turns
  • AgentEvent
  • capabilities

Token usage is reported as a neutral token.usage event with normalized inputTokens, outputTokens, cachedInputTokens, reasoningOutputTokens, and totalTokens fields when available. Managed turn status and wait results also expose the latest usage snapshot for the turn.

Structured output is implemented above the provider adapters. Codex and Claude Code use native JSON Schema support; Gemini currently uses prompt constraints plus local validation and retry.

Runtime options are split into provider-neutral fields and provider-specific fields. The selected provider binds the options type:

await createPort({
  provider: "codex",
  options: {
    model: "gpt-5.5",
    effort: "high",
    sandbox: "workspace-write",
  },
})

await createPort({
  provider: "claude",
  options: {
    model: "sonnet",
    permissionMode: "plan",
    thinking: { type: "adaptive" },
  },
})

await createPort({
  provider: "gemini",
  options: {
    model: "gemini-2.5-pro",
    approvalMode: "plan",
    settingsPath: "/path/to/gemini-settings.json",
  },
})

Lifecycle state is also implemented above the provider adapters. Providers only need to expose create/resume/start/cancel primitives; Port records local session and turn status, stores an in-memory event log, and exposes replay via events({ afterSeq }) or turns.stream({ sessionId, turnId, afterSeq }). This is intentionally runtime-level only: workflow envelopes, policy labels, ack/result protocols, and durable controller state stay above Port.

Type sync with consumers

Port now ships a built artefact set: dist/index.js (JS) and dist/index.d.ts (full type declarations). package.json#exports points consumers at those, not at src/. Most consumers can therefore import Port's types directly — no shim needed — once they install the current packed tgz.

For consumers still pinned to an older tgz (or file: link from before the dist build), a compile-time shim of the export surface still exists. The current shim of record:

  • gloopgloop/src/types/glubean-port.d.ts (narrow ambient module declaring only the symbols gloop's reviewer adapters consume: createPort, Port, RuntimeOptions, RuntimeOptionsFor<P>, StartStructuredTurnInput, StructuredTurnResult, StructuredTurnError, the per-provider enums like ClaudePermissionMode / GeminiApprovalMode, etc.)

While the shim is in use, when you change the exported type surface in src/index.ts / src/types.ts — adding a field to RuntimeOptions, a new StructuredTurnError subclass, an additional provider id, anything that affects what import type consumers see — update the consumer's shim in the same change so the shim stays a strict subset of the dist declaration. TypeScript merges the two; the shim must NEVER tighten a field declared in the dist (only widen or add). Skipping the update leaves the consumer's tsc green against a stale view of Port's types and the divergence surfaces only at runtime.

Removing a consumer's shim is a separate change: rebuild the consumer's lockfile against a tgz that ships dist, verify tsc greenness directly against dist/index.d.ts, then delete the shim file in a follow-up commit.