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

@porkytheblack/glorp-client

v0.8.1

Published

Typed client for driving a remote Glorp Garage — create workspaces, run agents, poll/stream results over an API-key-secured HTTP/WS API.

Readme

@porkytheblack/glorp-client

A small, typed client for driving a remote Glorp Garage: create workspaces, run coding agents, and poll or stream their results over an API-key-secured HTTP/WS API. Zero runtime dependencies; runs in Node 18+, Bun, and the browser.

npm add @porkytheblack/glorp-client

Quick start

import { configure, run } from "@porkytheblack/glorp-client";

configure({ endpoint: "https://glorp.example.com", apiKey: "glsk_…" });

const handle = await run({
  workspace: "/srv/projects/acme",   // or { workspaceId: "ws_…" }
  prompt: "Add a /health endpoint and a test for it.",
});

const { text, status } = await handle.result();   // waits for the run to finish
console.log(status, text);

configure() is optional — the client auto-reads GLORP_ENDPOINT and GLORP_API_KEY from the environment on first use.

Run handle

run() creates a session, sends the first prompt, and returns a handle:

const h = await run({ workspaceId, prompt });

h.sessionId;                       // the new session id
await h.status();                  // SessionDto { state, busy, … }  — poll this
for await (const ev of h.events()) { … }   // live BridgeEvent stream (WebSocket)
await h.result({ timeoutMs });     // resolves with the latest agent answer
await h.abort();                   // stop the running turn

run() defaults permissionMode to "auto" so unattended runs don't deadlock on a tool-permission prompt. Use "bypass" for zero prompts (disposable workspaces only).

Streaming vs. polling — pick the right one

For long or unattended runs, stream with h.events() / streamSession() (WebSocket) — it's the reliable path. A turn that fails mid-run (e.g. a model 400) emits an error BridgeEvent immediately, with the real reason:

for await (const ev of h.events()) {
  if (ev.type === "error") throw new Error(ev.message); // surfaced as it happens
}

result() / status() are a best-effort snapshot, not a run log. A failed turn settles to busy:false with no new text — indistinguishable from "the agent finished and wrote nothing" if you look only at text. To disambiguate without the stream, read the snapshot's last_error / last_turn_state:

const r = await h.result();
if (r.last_turn_state === "error") throw new Error(r.last_error ?? "turn failed");

Full client

import { createClient } from "@porkytheblack/glorp-client";

const glorp = createClient({ endpoint, apiKey });

await glorp.ping();
const ws = await glorp.workspaces.create("/srv/projects/acme");
const s  = await glorp.sessions.createInWorkspace(ws.id, { permissionMode: "auto" });
await glorp.sessions.sendMessage(s.id, "Refactor the auth module.");
const res = await glorp.sessions.result(s.id);

// or block until done in a single call:
const { text } = await glorp.sessions.sendMessageAndWait(s.id, "Now add tests.");

// admin:
const { key } = await glorp.keys.create("worker", ["run"]);

API groups: workspaces, sessions, models, keys, namespaces, plus run(), streamSession(), and forNamespace().

Multi-tenancy (namespaces)

A Garage can host isolated namespaces (one per user). An admin key provisions them and mints namespace-bound keys; a tenant key transparently scopes every call.

const admin = createClient({ endpoint, apiKey: adminKey });

// provision a tenant + mint its key (raw key returned once)
const ns = await admin.namespaces.create("Acme");          // -> { id: "ns_acme", … }
const { key } = await admin.namespaces.createKey(ns.id, "acme-bot");

// the tenant uses ITS key — every call is auto-scoped to ns_acme
const tenant = createClient({ endpoint, apiKey: key });
await tenant.sessions.create({ permissionMode: "bypass" });

// an admin can act inside any namespace via forNamespace() (or { namespace } on run())
await admin.forNamespace("ns_acme").sessions.list();
await admin.run({ namespace: "ns_acme", workspace: "/srv/x", prompt: "…" });

// deprovision (revokes keys, stops sessions; `true` also wipes the data subtree)
await admin.namespaces.delete(ns.id, true);

admin.namespaces also exposes list(), get(id), and listKeys(id). Namespaces require the Garage to run with auth on.

Errors

Non-2xx responses throw a typed GlorpRemoteError with .status and .code:

import { GlorpRemoteError } from "@porkytheblack/glorp-client";

try { await glorp.sessions.list(); }
catch (e) {
  if (e instanceof GlorpRemoteError && e.status === 401) { /* bad key */ }
}

Config

configure({
  endpoint: "https://glorp.example.com",  // required
  apiKey: "glsk_…",                        // required unless the server is auth-off
  namespace: "ns_acme",                    // optional (admin keys: act inside a namespace)
  timeoutMs: 30_000,                       // optional per-request timeout
  fetch: customFetch,                      // optional (Node < 18, testing)
  WebSocketImpl: WebSocket,                // optional (e.g. Node's `ws`)
});

See docs/remote-orchestration.md and docs/openapi.yaml for the full HTTP/WS contract.