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

@classcad/script

v0.1.0

Published

Universal script executor for ClassCAD agents — model-written JavaScript against a session-abstracted API (api.v1.*, api.tree(), api.graphic()), identical in the browser (buerli), Node (MCP, harness) and CI.

Readme

@classcad/script

The universal script medium for ClassCAD agents: execute model-written JavaScript against a session-abstracted API — identical in the browser (buerli apps), in the ClassCAD MCP, and in headless Node harnesses/CI. CAD construction is mostly computation; this package lets an agent write a real program (variables, Math, loops, geometry filtering) instead of dictating one API call per model turn.

Install & entry points

npm install @classcad/script

The package has two entry points — the split exists because the WS session needs Node (ws, node:crypto), while everything else must also run in a browser bundle:

| Entry point | Environment | Exports | | --- | --- | --- | | @classcad/script | browser + Node | runScript, buildScriptApi, all types (ScriptSession, RunScriptOptions, RunScriptResult, …) | | @classcad/script/node | Node only | everything above plus connectSession (the WebSocket session for a classcad-cli worker) | | @classcad/script/docs | browser + Node | docs — the data contract documents as markdown strings (docs.DATA, docs.STRUCTURE, docs.GRAPHICS) |

In the browser you import from @classcad/script and provide your own session (buerli-ai does this over the @buerli.io/classcad WASM client); in Node you import from @classcad/script/node and get the WS session included.

Quick start (Node, against a classcad-cli worker)

import { connectSession, runScript } from '@classcad/script/node'
// The method registry makes api.v1 typo-safe. It ships with @classcad/skill:
import registry from '@classcad/skill/method-registry.json' with { type: 'json' }

const session = await connectSession()            // ws://0.0.0.0:9094/

const res = await runScript(`
  const partId = (await api.v1.part.create({ name: 'Demo' })).result
  await api.v1.part.cylinder({ id: partId, diameter: 40, height: 20 })

  // find the shell face by filtering REAL geometry: every vertex at radius 20
  const g = await api.graphic()
  const shell = g.containers.flatMap(c => c.meshes ?? []).find(m => {
    for (let i = 0; i < m.vertices.length; i += 3) {
      if (Math.abs(Math.hypot(m.vertices[i], m.vertices[i + 1]) - 20) > 0.01) return false
    }
    return m.vertices.length > 0
  })
  console.log('shell found:', !!shell)
  return { partId, shellPoint: [20, 0, 10] }   // hand faces onward as world POINTS
`, session, { registry })

// res → { ok: true, returned: { partId: 4, shellPoint: [20, 0, 10] },
//         logs: ['shell found: true'] }
session.close()

What a script sees

Scripts run as an async function body: await directly, return a small value, console.log(...) (alias log(...)) is captured and returned. The api object has a guaranteed surface that exists in every environment, plus optional capabilities a client may inject:

| Surface | Availability | What it does | | --- | --- | --- | | api.v1.<domain>.<method>(params) | guaranteed | one ClassCAD command, → { result, maxLevel, messages, … }. With a registry, unknown names throw immediately with suggestions ("v1.part.bxo" — Did you mean: box?) instead of failing downstream. | | api.tree({ refresh? }) | guaranteed | the structure tree (id → node): find parts, features, sketches by class/name | | api.graphic({ recalc? }) | guaranteed | the graphic payload (containers with face meshes, edges, vertices): scripts locate and filter geometry themselves | | api.env | guaranteed | 'node' | 'browser' | | api.facade / api.structure / api.selection / … | optional | client capabilities injected via session.namespaces (buerli browser apps have them; WS sessions don't). Guard with if (api.facade) … |

A script that sticks to the guaranteed surface runs unchanged everywhere.

Two rules worth teaching every agent:

  • Face/edge ids are payload-local. Re-tessellation reassigns mesh/edge ids between graphic frames. To hand a face to another tool (e.g. a renderer highlight), return a world point on it, not its id.
  • recalc destroys entity-injection bodies. Pass api.graphic({ recalc: false }) in solid.*/EIF direct-modeling sessions.

API

runScript(code, session, opts?)

runScript(code: string, session: ScriptSession, opts?: RunScriptOptions): Promise<RunScriptResult>

Compiles code as an async function body, builds the api object for session (via buildScriptApi), executes with console capture and a timeout, and returns the outcome. Never throws — syntax errors, runtime errors and timeouts come back as { ok: false, error, logs } with the captured logs preserved (printf debugging survives failure).

Environment globals are shadowed in BOTH flavors (window, document, fetch, process, require, …): scripts drive the CAD API, nothing else. This is defense-in-depth against accidental use, not a security sandbox — the CAD API itself is the capability boundary.

RunScriptOptions:

| Option | Type | Default | Description | | --- | --- | --- | --- | | registry | MethodRegistry | — | the v1 method registry — import it from @classcad/skill/method-registry.json (or pass the one your app already loaded). With it, api.v1 is generated and validated: typos throw with suggestions. Without it, api.v1 is a permissive proxy — any name routes to the engine, which then reports unknown commands. | | timeoutMs | number | 60000 | timeout for awaited work (max 300000). A runaway synchronous loop cannot be interrupted — scripts must terminate. | | maxLogEntries | number | 300 | captured console entries cap | | maxLogChars | number | 16000 | captured console characters cap | | maxResultChars | number | 24000 | JSON cap on the returned value — truncation is explicit (a marker says what happened), never silent |

RunScriptResult:

| Field | Type | Description | | --- | --- | --- | | ok | boolean | whether the script completed | | returned | unknown | the script's return value (JSON-capped), when ok | | logs | string[] | captured console output — present on success AND failure | | error | string | syntax/runtime/timeout message, when not ok |

buildScriptApi(session, opts?)

buildScriptApi(session: ScriptSession, opts?: { registry?: MethodRegistry }): api

Builds the api object for a session without executing anything — use it when you host script-like code yourself: hand buildScriptApi(session, { registry }) to your scripts as their api argument.

  • With opts.registry: api.v1 contains exactly the registry's domains and methods; unknown domain or method access throws immediately with edit-distance suggestions.
  • Without: api.v1 is a permissive proxy (any v1.<domain>.<method> routes to session.execute).
  • session.namespaces entries appear on api as-is — core keys (v1/tree/graphic/env) cannot be overridden.

connectSession(url?, opts?) — Node only

connectSession(url = 'ws://0.0.0.0:9094/', opts?: NodeSessionOptions): Promise<NodeSession>

Connects to a ClassCAD worker (classcad-cli) over WebSocket and returns a ready ScriptSession. It handles the protocol details for you: request/response correlation with timeouts, INFO-message filtering, and the pull-on-demand caches behind api.tree() / api.graphic() (one GetTree when something changed). The connection itself keeps the engine's emission defaults — important when it shares an engine session with an interactive app; runScript suppresses structure/graphic emission for the duration of ONE script (GetEmissionConfig → SetEmissionConfig(SUPPRESS_EMISSION) → run → restore), so a 100-command script is 100 small Results, and pulls inside the script switch the kernel graphic on around their GetTree.

NodeSessionOptions: graphics (default true — include the kernel graphic in pulls), debug (default false — disables all timeouts), namespaces (extra capabilities to expose on the script api).

NodeSession extends ScriptSession with request(command, extra?) (raw protocol commands like GetTree), getEmissionConfig() / setEmissionConfig(partial) (the connection's emission flags; runScript uses them, callers changing them by hand must restore), pull(), getLastGraphic(), getStructure() and close(). It deliberately also satisfies the @classcad/renderer node-client contract, so one connection serves scripts and renders:

import { connectSession, buildScriptApi } from '@classcad/script/node'
import { renderSession } from '@classcad/renderer/node'
import registry from '@classcad/skill/method-registry.json' with { type: 'json' }

const session = await connectSession()
const api = buildScriptApi(session, { registry })
const partId = (await api.v1.part.create({ name: 'Part' })).result
await api.v1.part.cylinder({ id: partId, diameter: 40, height: 20 })
await renderSession(session, 'check', './out', { sheet: true })   // same connection
session.close()

ScriptSession

The abstraction that makes scripts universal — implement it to plug in a new environment:

interface ScriptSession {
  env: 'node' | 'browser'
  execute(task: Task): Promise<Envelope>
  getTree(opts?: { refresh?: boolean }): Promise<Tree>
  getGraphic(opts?: { recalc?: boolean }): Promise<Graphic | null>
  namespaces?: Record<string, unknown>
  close?(): void | Promise<void>
}

| Member | Contract | | --- | --- | | execute(task) | run one command in harness-task form: execute({ 'v1.part.box': [{ id, length }] }) → the envelope { result, maxLevel, messages, … }. API errors live in the envelope (maxLevel ≥ 51), not in promise rejections. | | getTree(opts?) | the current structure tree (id → node). { refresh: true } forces a server round-trip where the environment caches. | | getGraphic(opts?) | the current graphic payload ({ containers }) or null. Must honor { recalc: false } (direct-modeling sessions). | | namespaces | optional capabilities surfaced on api (e.g. buerli's facade/structure/selection) |

Shipped implementations: connectSession (Node/WS, this package) and the browser session in @buerli.io/ai (over the buerli store + WASM client).

The data contract (docs/)

What scripts get back from api.tree() and api.graphic() is a contract of this package, documented in docs/:

| Document | Content | | --- | --- | | docs/DATA.md | The distilled day-to-day contract: node/graphic shapes, which ids are stable vs payload-local, verified selection idioms | | docs/STRUCTURE.md | The model tree in depth: part anatomy, features & history, sketches, assemblies (instances, transforms), traversal snippets | | docs/GRAPHICS.md | The graphic payload in depth: containers, meshes, edges, materials |

Agent hosts serve them to their models under the same names — buerli-ai via read_doc("DATA"), the ClassCAD MCP via describe_method("DATA") — by importing them from @classcad/script/docs (markdown strings, bundler-safe, no filesystem access needed).

Consumers in this monorepo

| Consumer | How it uses this package | | --- | --- | | @buerli.io/ai (browser panel) | run_script tool = runScript over its browser session; buerli namespaces injected as optional capabilities | | classcad-mcp (MCP server) | run_script tool over its WS client |

Execution reliability and inspection

runScript accepts strict, onOperation, signal, and timeoutMs. Strict mode rejects engine error envelopes (level ≥51); raw envelopes remain the library default for diagnostic scripts. MCP and browser run_script enable strict mode. onOperation reports method, feature name, timing and engine messages. Logs retain the latest entries under their size limits.

A timeout/cancellation prevents subsequent API calls; it does not kill a native operation already running. pending:true and isSessionBusy(session) mean work has not settled. The execution lease remains held through outstanding parallel calls and emission restoration. MCP and browser drawing tools refuse new work while that lease is held. This is not rollback. A transport request timeout leaves the session outcome unknown and requires reconnection; inspect the drawing before retrying a mutation. Synchronous infinite JavaScript loops cannot be interrupted by this cooperative executor.

Node sessions and MCP clients separately accept connectTimeoutMs and requestTimeoutMs; runScript has its own whole-run deadline. Complete empty graphics clear old geometry. Both adapters normalize missing structure and graphics to null. Connection teardown rejects pending requests.

Scripts can use api.inspect:

const capture = await api.inspect.capture();
const solids = api.inspect.currentSolids(capture);
const bounds = api.inspect.graphicBounds(capture, solids);
const edge = api.inspect.uniqueEdge(capture, [10, 0, 0], 0.05, [solids[0]]);
// Use edge.id only in this model state. Recapture after any mutation/recalculation.
return { solids, bounds, edge };

edgeCandidates returns all matching edges, their distances, tolerance and capture revision. uniqueEdge rejects zero or multiple matches. Bounds are approximate tessellated bounds in graphic coordinates; instance transforms are not applied. api.inspect.solid(id) adds native mass properties but does not certify B-rep validity. Revisions are local client mutation counters, not persistent topology identities or cross-client synchronization guarantees.

Run the monorepo's npm run test:reliability for the offline adapter, lifecycle and renderer regressions. node packages/script/test/emission-live.mjs is the optional real-worker smoke test (CLASSCAD_URL selects the worker).