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

@penumbra-systems/platform

v0.7.1

Published

The runtime client for Penumbra: typed access to your knowledge graph for capture, extraction, search, governed memory, and decision quality.

Readme

@penumbra-systems/platform

The runtime client for the Penumbra Platform API. It gives you typed access to your knowledge graph: capture and extract structured entities, search across them, recall and synthesize memory, and verify whether what you have is good enough to act on.

This is a preview release for design partners and developer testers. You need a Penumbra API key. Node.js 18 or newer is required.

Design time vs. run time

Penumbra splits into two surfaces, and this SDK is one of them.

  • Design time: the Shapes Workbench (an MCP server). You author your ontology there: the types, shapes, and relationships that structure your graph. This is where the schema of your knowledge lives and evolves.
  • Run time: this SDK. You operate against a live graph governed by those shapes: capture, extract, search, recall, and check quality. The SDK reads the ontology you designed; it does not replace the authoring experience.

If you are deciding what your knowledge looks like, you want the Workbench. If you are putting knowledge in and getting it back out from an application or agent, you want this SDK.

Install

pnpm add @penumbra-systems/platform

Set your key:

export PENUMBRA_API_KEY="pnbr-..."
import { createPenumbra } from "@penumbra-systems/platform";

const pb = createPenumbra({ apiKey: process.env.PENUMBRA_API_KEY! });

The client targets the production API by default. You can override baseUrl in createPenumbra({ apiKey, baseUrl }) for self-hosted or staging environments, but most testers never need to.

Quickstart (read-only)

Nothing here writes to your graph.

// What does this project's ontology look like?
const ontology = await pb.ontology({ format: "markdown" });
console.log(ontology.markdown || ontology.content);

// Find things across entities and sources.
const hits = await pb.search("pricing objections from enterprise deals", {
  include: ["entities", "chunks"],
  limit: 10,
});
console.log(hits.results);

What your key can do depends on its scopes (for example graph:read, search:read, graph:write) and whether it is scoped to one project or your whole organization. Project-scoped keys make projectId implicit; if your key spans multiple projects, pass projectId on calls that take it.

The surface

pb is organized into the verbs you run against a governed graph:

| Call | What it does | | ----------------------- | -------------------------------------------------------------------- | | pb.ontology(...) | Read the active ontology (types, shapes, relationships). | | pb.search(query, ...) | Hybrid / semantic / lexical retrieval over entities and sources. | | pb.capture(...) | Stage a structured entity into the graph. | | pb.extract(...) | Run text through a shape to extract entities and relationships. | | pb.memory.* | Domain-typed memory: remember, observe, recall, synthesize, archive. | | pb.dq.* | Decision quality: is this region of the graph fit to act on? | | pb.types.* | Read and manage entity types. | | pb.shapes.* | Read shapes, schemas, bindings, projections. | | pb.deltas.* | The staged-write primitive underneath capture and extract. | | pb.context.* | Discover, preview, and compile scoped context. | | pb.slices.* | Named, reusable context views. | | pb.sources.* | Upload files as sources, poll readiness, read entities and stats. |

Starter shapes

Penumbra ships a curated set of public starter shapes you can use immediately or fork to fit your domain:

const kit = await pb.shapes.starters();
// Foundation Layer (general extraction), Research, Founder Worldview, Memory

Note: pb.shapes.list() returns your project's own shapes and does not include system shapes, so the starters won't appear there. Use pb.shapes.starters() to discover them; they're resolvable by name in pb.extract({ shape }) and govern pb.memory (the memory shape) without being attached to your project.

Governed writes

Writes do not hit the graph directly. They stage through a governed delta, which you can inspect before it commits. Pass apply: false to stage without committing, which is the right default while you are testing.

const receipt = await pb.capture({
  type: "Insight",
  properties: { content: "Enterprise buyers stall on procurement, not price." },
  apply: false, // staged, not committed
});

console.log(receipt.deltaId, receipt.status); // "...", "staged"

pb.extract works the same way, and needs a shape that exists in the target project plus an extraction-capable key. Keep it out of your first smoke test unless your project has a known shape.

Sources: the headless upload loop

Get a file into Penumbra and extract from it — one key, four lines:

const src = await pb.sources.upload({ projectId, file, filename: "deck.pdf" });
await pb.sources.waitUntilReady(src.id); // polls 4-axis readiness
const run = await pb.extract({ source_file_id: src.id, shape_id, apply: true });
// poll the run via the receipt's poll path (async extraction contract)

upload() orchestrates the whole flow: it requests a signed storage URL (over-cap files fail here with a 413 before any bytes move), PUTs the bytes directly to storage (never through pnbr.io), then registers the file — where the server re-validates size against storage truth, compresses oversized PDFs at ingest, applies your org's source-approval policy (RFC 385), and triggers indexing. Indexing carries a 0.1-credit charge per upload.

If your org's approval policy is gate, API uploads land staged and wait for a governor — check src.status before polling readiness, because staged sources stay pending until approved.

Lower-level pieces are exposed too: pb.sources.createSignedUploadUrl(), pb.sources.register(), pb.sources.get(id), pb.sources.list(projectId), pb.sources.entities(id), pb.sources.entityStats(id).

Scopes for the loop

| Step | Scope required | | -------------------------------------------------- | ----------------------------- | | pb.sources.upload() (issue + register) | sources:write | | pb.sources.get() / waitUntilReady() / list() | sources:read | | pb.extract({ source_file_id }) | shapes:read + delta:write | | apply: true on extract | delta:apply |

A headless sources key wants all five. Project-scoped keys make projectId implicit on every call above.

Memory

pb.memory stores domain-typed memory as graph state, so what an agent learns is queryable structure rather than opaque text.

// Recall relevant memory as a graph object.
const memory = await pb.memory.recall({
  query: "what should be known before continuing?",
  limit: 5,
});
console.log(memory.toMarkdown());

// Write one memory. Use apply: false while testing.
await pb.memory.remember({
  type: "memory",
  kind: "preference",
  content: "The user prefers short briefings with explicit caveats.",
  apply: false,
});

The full loop is remember (write one), observe (learn from a completed interaction), recall (read as a graph), synthesize (turn recalled memory into a prose briefing), and archive (retire stale memory).

const briefing = await pb.memory.synthesize({
  query: "what should be known before the next step?",
  as: { type: "SituationBriefing", purpose: "agent-handoff" },
});
console.log(briefing.toMarkdown());

Your memory shape (strong default, full override)

Out of the box, pb.memory is governed by Penumbra's opinionated public memory shape; you don't configure anything. If you want memory to capture your domain, fork that shape (or author your own), then bind it as the project's runtime memory shape:

await pb.memory.binding(); // { shapeName: "memory", source: "default" }
await pb.memory.useShape(myShapeId); // point pb.memory at your shape
await pb.memory.resetShape(); // revert to the system default

After useShape, remember/observe are governed by your shape. Memory is defined by the memory plane, so recall still returns everything in it regardless of which shape wrote it.

Decision quality

pb.dq answers a different question than search: not "what is there?" but "is what's there good enough to act on, for this purpose?" A verdict is a disposition plus an enumerated set of findings, never a confidence score.

const verdict = await pb.dq.check({
  subject: { type: "Account", id: "acme" },
  purpose: "send a renewal proposal",
});

if (!verdict.safeToAct) {
  console.log(verdict.explain());
  console.log(pb.dq.repair(verdict)); // suggested remediation steps
}

Errors

Failed requests throw PenumbraPlatformError with status, code, and the raw response data.

import { PenumbraPlatformError } from "@penumbra-systems/platform";

try {
  await pb.search("...");
} catch (err) {
  if (err instanceof PenumbraPlatformError) {
    console.error(err.status, err.code, err.message);
  }
}