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

glove-js

v0.3.0

Published

A JavaScript REPL for LLM tool use — expose an agent's capabilities as async functions in a tiny, sandboxed JS interpreter it drives with one execute_js tool. Tools become functions, results are plain data, top-level const/let persist across calls, big va

Downloads

733

Readme

glove-js

A JavaScript REPL for LLM tool use. Instead of loading many tool definitions into the context window, expose an agent's capabilities as async functions in a tiny, sandboxed JS interpreter it drives with ONE execute_js tool. The model already writes JavaScript fluently — so it discovers, calls, and composes capabilities by writing programs.

It is the JavaScript sibling of glove-lisp: both consume the same ToolFn catalog, so one set of functions mounts on either surface unchanged. Pick the language your models are most fluent in.

import { JsSession, mountJs } from "glove-js";
import { fnsFromMcp } from "glove-scratchpad/fns/mcp";

const session = JsSession.create();
session.registerAll(await fnsFromMcp(githubConn));   // github__list_pull_requests, …

mountJs(agent, { session });

Now the model works entirely in JavaScript through execute_js:

const prs = github.list_pull_requests({ state: "open" });
const stale = prs.filter(p => p.age_days > 30);
stale.length === 0
  ? "all fresh"
  : `${stale.length} stale: ${stale.map(p => p.number).join(", ")}`;

One call. The rows never enter the model's context — only the answer string does.

Why a REPL (and why JavaScript)

The scratchpad work showed that folding an agent's capabilities behind ONE code-eval tool beats loading dozens of tool definitions — on correctness, on context, and on cost — because the model computes over results in the sandbox instead of round-tripping every intermediate through its context window. The bet is fluency: the surface must behave like something the model already knows. glove-lisp bets on Clojure; glove-js bets on JavaScript — the single most-represented language in every model's training data.

What the surface keeps from that work:

  • One tool, progressive in-band discovery. Nothing is primed by default — the model discovers capabilities in tiers: search("open pull requests") jumps straight to matching functions, or it browses servers()fns("github")describe("name") (server list → a server's functions → one function's params + result shape). The same tiers exist as native tools (search_functions / list_servers / list_functions / describe_function), so a weak model can fire them as tool calls and a capable one can script the whole sweep in one program. Those native-tool names are also callable inside the code as aliases of the short builtins — search_functions({ query }), list_functions({ server }), list_servers(), describe_function({ name }) all work in-REPL (accepting either the { … } object form or a bare string), so a model primed on the tool names lands its call whichever way it reaches for discovery. Result shapes warm lazily — a function's row type is sampled the first time it's described, not for the whole catalog at mount. (discovery: "full" primes every signature up front for small catalogs; "auto" picks per size.)
  • Off-context data flow. const prs = github.list_pull_requests() stores the rows in the REPL and echoes only a summary; the model then works with prs.length, prs.slice(0, 5), prs.map(p => p.title).
  • Branch in one program. if (incidents.length === 0) slack.post(...) else email.send(...) — decide-and-act is ONE call, not a read, a look, and a second call.
  • Exactly-once effects by construction. A tool call fires when its expression evaluates — there is no planner that might re-run it.
  • Persistent session. Top-level const/let survive across execute_js calls, so the model builds up state without re-fetching.
  • Bounded output. The value that crosses back into context is structurally elided (arrays past 25 items, strings past 300 chars) with a marker naming the true size.

The language

A deliberately small subset — the JavaScript a model reaches for when it thinks "transform this data", and nothing else:

  • const/let, arrow functions and function declarations, template literals, destructuring (defaults + ...rest), spread, optional chaining (?.).
  • if/else, for…of, for, while, switch, try/catch/finally, throw.
  • Arrays (map/filter/reduce/find/some/every/sort/flatMap/slice/…), strings, Object.keys/values/entries/assign/fromEntries, Math, JSON, new Set/Map/Date/RegExp, console.log (captured).
  • Tool calls are async functions; promises resolve automatically, so await is optional. github.list_pull_requests({ state: "open" }) just works.

Not in the language — and rejected with a targeted message, not gibberish: class, import/require, eval, Function, this, prototypes, fetch, for…in, var, in/instanceof, generators.

How a program runs

parse → validate → run. acorn parses the full program; a whitelist walk rejects unsupported constructs before anything executes. Then an async tree-walking evaluator runs it with a fuel budget (per node + per loop back-edge, so while (true) {} can't hang), a recursion-depth cap, and AbortSignal support. Every obj.prop read and obj.method(...) call goes through a sandbox boundary (members.ts) that blocks the escape keys (constructor, __proto__, prototype, call/apply/bind) and exposes a fixed method allowlist — a program can't climb a constructor chain back to the host.

Function mode: no table modeling

A capability is a ToolFn: a name, an optional input schema (its own — JSON Schema or Zod), and a call. There are no columns, no pushdown keys, no volatility classes to declare — which is exactly what makes this the right surface when the tools are unknown up front (an arbitrary MCP server discovered at runtime).

import { defineFn } from "glove-scratchpad";
import { fnsFromMcp } from "glove-scratchpad/fns/mcp";
import { JsSession } from "glove-js";

const session = JsSession.create();

// A whole MCP server → functions:
session.registerAll(await fnsFromMcp(conn));

// Or author one inline:
session.register(defineFn({
  name: "email__send",
  input: z.object({ to: z.string(), subject: z.string() }),
  readOnlyHint: false,
  handler: (args) => sendEmail(args),
}));

A __ in a function name becomes a namespace: github__list_pull_requests binds both the flat name and github.list_pull_requests. Calling an effectful function FIRES it immediately — there is no staging or undo (the write verb is the function).

The moving parts

| Concept | Code | | --- | --- | | A capability | ToolFn — the same catalog as glove-lisp's function mode (defineFn / fnFromTool / fnsFromMcp) | | The interpreter | JsSessionexecute(code) returns { value, called, defined, defs, stdout, note } | | The single agent tool | mountJs(glove, { session }) → folds execute_js and primes the model | | Discovery | progressive by default — search("…") / servers() / fns("server") / describe("name") as REPL builtins, mirrored by the search_functions / list_servers / list_functions / describe_function tools; discovery: "full" \| "auto" primes signatures for small catalogs | | The parser | parseProgram(code) — acorn + a whitelist validation walk | | The sandbox boundary | member access mediated by members.ts (the security-critical file) |

Framing: execute_js vs execute_js_workflow

The eval tool ships three interchangeable framings, chosen at mount time with frame. The runtime is identical — only the tool NAME and the primed preamble change:

mountJs(agent, { session });                     // frame: "repl"     → execute_js (default)
mountJs(agent, { session, frame: "program" });   // frame: "program"  → execute_js_program
mountJs(agent, { session, frame: "workflow" });  // frame: "workflow" → execute_js_workflow

The bet (see benches/scratchpad-bench/FRAME-PAPER.md): the token "REPL" pattern-matches to an interactive, line-by-line session, so models degrade the surface back into an incremental tool-call loop — peek at a row, then run a second program. The workflow framing never says "REPL"; it frames the call as ONE complete program that carries the task start to finish, and demotes cross-call persistence to a retry-only recovery aid. program is the half-step (a rename that drops "REPL" but keeps the priming otherwise neutral), so a bench can separate the name alone from the full reframing. The default is repl, so existing mounts are unchanged; jsToolName(frame) and buildJsPreambleBody(frame) expose the mapping and the framing text.

Status

Draft v0.1 — a fluency exploration alongside glove-lisp. The evaluator subset and sandbox are covered by unit tests (pnpm --filter glove-js test), and the sandbox boundary survived an adversarial escape review.

A live A/B (6 models × 10 tasks × 5 arms — see benches/scratchpad-bench/JS-EXPLORATION.md) found that execute_js reproduces the SQL/Lisp off-context benefit and is fully competitive on frontier/mid models, while function mode reaches parity with the ResourceTable contract. The first run's weak-tail misses were two fluency gaps — the model calling catalog functions as folded tools, and guessing result field names the catalog never showed — both closed over two hardening batches: a preamble that frames execute_js as the only tool, and result-shape discovery (sampleResultShapes samples each read-only function once and surfaces a TS-like row type in describe(...) and the catalog, e.g. sentry__list_issues(…) → { …, count: number, status: "unresolved"|"resolved"|"ignored" }[]). That took jsrepl from 78% → 90% → 97% — the top arm, above Lisp (95%) and SQL (92%) — for a modest peak-context increase.

License

MIT.