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

@odla-ai/kg

v0.6.0

Published

Ontology-driven knowledge-graph builder: pluggable source connectors, LLM extraction against a config-as-data ontology, and a provenance-preserving graph writer persisting into odla-db.

Readme

@odla-ai/kg

⚠️ Early access — pre-1.0. Agents work from bounded runbooks; humans approve credentials, production changes, releases, and merges. APIs and exact package availability can change. Review the documented guarantees and limitations; this software is MIT-licensed and provided without warranty.

An ontology-driven knowledge-graph builder. Pluggable source connectors pull from the web and data feeds, an LLM extracts entities + relationships against a config-as-data ontology, and a provenance-preserving graph writer persists the result into odla-db.

The library is host-agnostic: it runs in Cloudflare Workers, Node, or tests — the host supplies the database client, LLM keys, and a KV-like store. The public platform and package manuals describe how to compose it with Workflows, scheduled triggers, an HTTP API, and a viewer UI.

Ask the runbooks first. odla's operational procedures live in a database, not in this file: npx @odla-ai/cli runbook ask "<question>" returns the current steps, and unlike anything written here it cannot be out of date. Use it before searching the web or working from memory. This README and the JSDoc in the shipped .d.ts are the version-matched API reference; a runbook is the procedure. Most tasks need an answer from both.

Architecture

Connector ──RawItem[]──▶ Extraction ──GraphFragment──▶ writeFragment ──Op[]──▶ odla-db
 (how data        (LLM forced-tool       (ontology-typed     (Lookup upserts +
  was acquired)    OR pure mapper)        nodes + edges)       links, idempotent)
  • RawItem — a connector emits either text (→ LLM extraction) or a ready-made fragment (structured feeds bypass the LLM).
  • GraphFragment — ontology-typed nodes[] + edges[]; the single shape the writer consumes, whatever produced it.
  • Ontology — data, not code. It compiles to the odla-db schema (toSerializedSchema), the writer's natural-key + merge-rule maps (naturalKeysOf, mergeRulesOf, humanAttrsOf), and by convention the extractor's tool schema.

Add a source → implement SourceConnector + one registry line. Add an entity/edge type → edit the ontology data. Neither touches the pipeline.

Usage

import { init } from "@odla-ai/db";
import {
  Store, getProvider, makeContext, ingestFresh, writeFragment,
  toSerializedSchema, type Ontology, type PipelineDeps,
} from "@odla-ai/kg";

// 1. Your ontology is ordinary application-owned data.
const ontology: Ontology = { entities: { /* … */ }, edges: { /* … */ } };

// 2. Wire the host services once. Env-var keys work standalone…
const db = init({ appId, adminToken, endpoint });
const provider = getProvider({ LLM_PROVIDER: "claude", ANTHROPIC_API_KEY: key });
// …but platform apps should bring a configured Ai instead (no LLM keys in
// worker env): createProvider({ provider: "claude", ai }) with the ai from
// @odla-ai/ai's initFromPlatform. The
// platform's default model is ignored here; kg's MODEL_TIERS govern verbs.
const deps: PipelineDeps = { db, provider, ontology, extract: myExtractor, store: new Store(kv) };

// 3. Run items through the shared pipeline (or call writeFragment directly).
await ingestFresh(step, deps, freshItems, "ingest");

Key seams the host supplies:

  • PipelineDeps — db (AdminDb), LLM Provider, the ontology, the extractor (your domain craft), and a Store over any KVLike.
  • ContextConfig (→ makeContext) — what connectors see: ontology, lazy provider, named secrets.
  • StepLike — a structural view of Cloudflare's WorkflowStep (asStepLike(step)); any do(name, cfg?, fn) runner works, including a plain inline runner in tests.

Built-in connectors

rss, url, gdelt (text → LLM extraction) · fred, edgar (structured → straight to the writer) · web-search (LLM web-search discovery) · gov-data (template proving a new source needs no pipeline change).

Enrichment & verdicts

enrichCompany and enrichPerson run a grounded web search, then a forced-tool extraction, then validate the result into a GraphFragment.

Person enrichment always returns a verdictconfirmed, probable, ambiguous, not_found, or wrong_entity — with a confidence and evidence tied to source URLs. The distinctions matter: a pass that confidently describes a different person with the same name is a much worse failure than one that found nothing, and collapsing the two makes it invisible. The fragment is empty unless the verdict permits writing, and a "confirmation" whose evidence carries no checkable source URL is downgraded to ambiguous rather than believed.

validateLinks drops profile URLs whose host contradicts their claimed kind — a linkedin.com/in/<name> pattern served from some other host is the single most common hallucination in this task. Scope is deliberately public and professional: site, blog, Substack, LinkedIn, X/Bluesky, GitHub, employer, and business contact points the person or their employer published. Never home addresses, personal phones, or personal email.

Grading, not self-reported confidence

gradeSources() derives how well an identity is established from the sources that were actually checked, rather than from what a model said about its own certainty. Measured, that number is not evidence: across real runs every lane confirmed between one and three of four people who do not exist, confidently.

const report = gradeSources([
  { sourceUrl: "https://acme.example/team", sourceClass: "first-party",
    facts: ["employer", "title", "location"], verifiedOn: "2026-08-03",
    corroboration: "confirmed" },
  { sourceUrl: "https://press.example/x", sourceClass: "third-party",
    facts: ["employer"], verifiedOn: "2026-08-03", corroboration: "confirmed" },
]);
// → { grade: "high", independentSources: 2, hasAnchor: true, reason: "…" }

high needs independent sources — two pages of one site agree by construction — with at least one first-party or authoritative anchor and enough agreeing facts, and no source naming somebody else. DEFAULT_STANDARD is those thresholds; pass your own GradeStandard to tighten them without touching code. Worth knowing before you act on medium: two third-party sources agreeing about a person who does not exist reached it in a real run, which is what anchorRequiredForMedium exists to close.

unverifiable is deliberately distinct from unresolved. In one run 60 of 93 claimed URLs came back unreachable, almost all login walls. Calling those "not established" blames the search for a wall it cannot walk through, and calling them established would be a lie.

Sources are what you store; the grade is computed. That ordering is the point — changing the standard re-scores stored evidence instead of re-running inference.

Use identityKey (not slugify) to decide whether two records are the same entity: it folds away punctuation, accents, &, and trailing legal suffixes, so "Acme Corp.", "Acme, Inc." and "Acme & Co" agree.

Strategies: gather evidence several ways, grade it once

A single research call is the wrong unit. Measured over real runs, a grounded research pass found 14 of 18 real people — and also "confirmed" one of four people who do not exist. Walking the employer's own website found only 5 of 18, and confirmed none of the four, because a company's site does not contain someone who does not work there.

They fail in opposite directions, so investigatePerson runs several and lets gradeSources settle it. Composed, the two above reached high on 16 of 18, with none of the four fabricated people getting there — a bar neither clears alone.

import { investigatePerson, researchStrategy, orgSiteStrategy, targetedProfileStrategy } from "@odla-ai/kg";

const result = await investigatePerson(
  {
    person: { slug: "abbey-chrystal", name: "Abbey Chrystal",
              companyName: "Rhizos", companyWebsite: "https://rhizos.example" },
    retrievedAt: "2026-08-03",
    ai, models: { search: "…", extract: "…" },
    readPage,          // see below
  },
  [researchStrategy, orgSiteStrategy, targetedProfileStrategy],
);
// → { grade: "high", sources: [...], links: [...], findings: [...], skipped: [] }

Strategies run concurrently and are isolated: one that throws becomes a Finding carrying its error, so a search provider being down costs you that strategy's evidence rather than the first-party page another already read. One that cannot run — orgSiteStrategy with no companyWebsite — reports itself inapplicable instead of pretending to have checked, and is named in skipped.

A strategy never decides whether the person was found; it reports what it saw and how strong the source was. That split is why adding a strategy cannot inflate a verdict, and why researchStrategy enters its sources at name-only at best however confident the model sounded.

readPage is yours to supply, deliberately. Fetched pages are attacker- influenced, so kg never reaches for the network itself and cannot quietly bypass whatever boundary you chose. In odla that is @odla-ai/camel's artifact ingress, which caps bytes, screens redirects for SSRF, and labels the body Unsafe:

const web = createWebArtifactIngress({ ingress, readers, limits: { maximumBytes: 512 * 1024 } });
const readPage = async (url: string) => {
  try { return (await web.fetchArtifact(url)).body.value; } catch { return null; }
};

A person's key should not be their name

kg has been writing person nodes keyed on slugify(name), and that fails in both directions. Two people called John Smith at two companies collapse into one node, silently merging their employers, links, and evidence. And a person whose name is written differently by the next source — a married name, an accent restored, "Bob" where the last pass saw "Robert" — mints a second node that nothing will reconcile.

identifyPerson mints an opaque key once and indexes the ways that person is recognized alongside it:

const { personKey, minted, fragment } = await identifyPerson(
  db,
  { aliasType: "person_alias" },
  person,
  result.links,
  result.fragment,   // keyed on person.slug, as personToFragment produces
);
await writeFragment(db, fragment, ontology, mutationId);

The fragment comes back rekeyed onto personKey, with every edge that touched the old key moved with it, plus one alias row per alias. Those rows are what make the next pass idempotent — without them a minted key would be re-minted every run, which is strictly worse than the name key it replaces.

Alias order is the contract. A public profile belongs to the person and survives them changing jobs, so linkedin:/in/abbeychrystal is tried before name:abbeychrystal~rhizosviticulture. The name alias includes the employer because that is what tells namesakes apart; it is the bootstrap for someone with no profile found yet, and they graduate off it as soon as any pass finds one — the new profile alias attaches to the key they already have.

A person with no employer and no profile gets no alias at all, so every pass mints them a fresh key. That is deliberate: duplicates are recoverable, a merged pair of strangers is not.

Adopting it. The alias entity is yours to declare — kg reads and writes it through the names in PersonIdentityConfig — so add one to your ontology with a unique natural key and an attribute holding the person key:

person_alias: {
  naturalKey: "alias",
  attrs: { alias: { type: "string", unique: true }, personKey: { type: "string", indexed: true } },
}

Existing graphs keep working: personToFragment still keys on person.slug, and nothing changes until you call identifyPerson. To migrate, backfill one name: alias row per existing person pointing at their current key, then start identifying — already-keyed people resolve to themselves and pick up profile aliases as passes find them.

Provenance & merging

writeFragment reads existing nodes by natural key (one batched query), merges scalar attrs per the ontology's rules (replace / prefer / earliest / latest), never writes human-owned attrs, and commits nodes-then-links in a single idempotent transaction (deterministic mutationId).