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

graphtide

v0.5.0

Published

Graph intelligence library for ingesting documents, building typed knowledge graphs, and querying them with LLMs

Readme

GraphTide

Graph intelligence library for ingesting documents, building typed knowledge graphs, and querying them with LLMs.

Install

npm install graphtide

Requires Node.js 18+.

Quick start

import { GraphClient, GeminiLLMProvider, SqliteStorageProvider, type Ontology } from "graphtide";

const ontology: Ontology = {
  nodeTypes: [
    { id: "person", name: "Person", properties: { name: "string" } },
    { id: "company", name: "Company", properties: { name: "string" } },
  ],
  edgeTypes: [{ id: "works_at", name: "Works At", from: "person", to: "company" }],
};

const client = new GraphClient({
  storageProvider: new SqliteStorageProvider(".data/graph.db"),
  llmProvider: new GeminiLLMProvider({
    apiKey: process.env.GOOGLE_API_KEY!,
    model: "gemini-3.1-flash-lite",
    embeddingModel: "gemini-embedding-001",
  }),
  ontology,
  enableEmbedding: true,
});

await client.ingestFromPath("./document.pdf");
await client.ingestFromText("Alice works at Acme Corp.");

const result = await client.query("Who works at Acme Corp?");
console.log(result.answer);
console.log(result.materials);

API overview

GraphClient is the main entry point.

| Method | Description | | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | ingestFromPath(path, options?) | Extract entities from a file; optional chunkSize / chunker | | ingestFromFile(file, options?) | Same as above, for File objects (e.g. in browsers) | | ingestFromFileURL(url, options?) | Download a document from a URL (PDF, DOCX, etc.) and ingest it | | ingestFromWebsiteURL(url, options?) | Fetch an HTML page and ingest its text content | | ingestFromText(text, options?) | Extract entities from raw text or pre-chunked strings | | createNode / updateNode / deleteNode | Strict create, partial update (properties, unsetProperties), and delete for nodes (also deletes incident edges) | | upsertNode / upsertEdge | Create or merge properties when the id already exists; returns { item, created } | | createEdge / updateEdge / deleteEdge | Strict create, update existing, and delete for edges | | getNode / getEdge | Read by id (throws if missing) | | hasNode / hasEdge | Check existence by id | | getNeighbourhood(seeds, options?) | BFS expansion from seed node(s); default maxHops 1; optional direction, topK | | getShortestPaths(from, to, limit?) | Up to limit shortest simple paths between two nodes, ordered by hop count | | query(question, options?) | Natural-language query; returns { query, answer, materials, method }. Options: { method?, topK?, seedK?, maxHops?, history? }. |

Storage providers

  • PostgresStorageProvider — persistent PostgreSQL database
  • SqliteStorageProvider — persistent local SQLite database
  • MemoryStorageProvider — in-memory store for tests and ephemeral use

Storage is fully pluggable: implement your own backend by extending BaseStorageProvider, similar to how you can extend BaseLLMProvider for custom LLMs.

LLM providers

  • OpenAILLMProvider — OpenAI chat + embeddings
  • GeminiLLMProvider — Google Gemini chat + embeddings

LLM integration is also pluggable via BaseLLMProvider.

Path finding

Find ranked paths between two nodes directly:

import { type GraphPath } from "graphtide";

const paths: GraphPath[] = await client.getShortestPaths("alice", "acme", 3);

A GraphPath is a plain list alternating node, edge, node, …, always ending on a node:

[
  { id: "alice", type: "person", properties: { name: "Alice" } },
  { id: "e1", type: "works_at", from: "alice", to: "acme", properties: { since: 2020 } },
  { id: "acme", type: "company", properties: { name: "Acme" } },
];

Use isGraphPathEdge(step) to tell nodes from edges, or the helpers graphPathNodes(path), graphPathEdges(path), graphPathNodeIds(path).

paths[0] is the shortest; ties and longer paths follow. The shortest_path query method uses the same logic internally (up to topK paths per seed pair).

import { graphPathNodeIds } from "graphtide";

for (const path of paths) {
  console.log(graphPathNodeIds(path).join(" -> "));
}

Neighborhood expansion

Expand outward from one or more seed nodes with BFS. Defaults to 1 hop (direct neighbours):

import { type GraphNeighborhood } from "graphtide";

const direct = await client.getNeighbourhood("alice");
const wider: GraphNeighborhood = await client.getNeighbourhood("alice", {
  maxHops: 2,
  topK: 5,
});

console.log(wider.nodeIds); // seeds plus closest nodes, capped at topK
console.log(wider.edges); // edges between included nodes

The bfs query method uses the same BFS logic internally (maxHops + topK from query options).

Query methods

client.query(question, options?) returns a GraphQueryResult:

const result = await client.query("Who works at Acme Corp?", { method: "bfs" });

result.query; // original question
result.answer; // LLM-generated answer
result.materials; // graph snippets fed to the LLM
result.method; // method that was used

Pass method to pick a strategy, or omit it to use combined (the default). Tuning options (topK, seedK, maxHops) apply to methods that need them.

| Method | What it does | Best for | | --------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | combined | LLM picks one or more strategies below, gathers materials from each, then synthesizes a single answer | General questions (default) | | basic | Ranks nodes and edges by relevance to the question; returns the top matches | “Find entities related to X” | | local | Picks seed nodes by relevance, then expands one hop along edges | Questions about direct relationships | | global | Clusters the graph into communities, ranks community summaries by relevance | Broad/thematic questions across the whole graph | | drift | Local 1-hop neighborhood plus community summaries that overlap that neighborhood | Connecting local facts to wider context | | bfs | Seed nodes by relevance, then multi-hop BFS expansion (maxHops, topK) | “What is connected to X within N hops?” | | shortest_path | Seed nodes by relevance, then shortest paths between seed pairs (falls back to 1-hop if no paths) | “How are A and B related?” |

Relevance ranking uses stored embeddings when nodes/edges have them (only the query text is embedded). When no embeddings are stored, ranking falls back to text term matching with no embedding API calls.

// Pick a specific method
await client.query("How are Alice and Acme connected?", { method: "shortest_path", seedK: 4 });

// Combined routing (default)
await client.query("What themes appear across the document?");

Multi-turn queries

Pass prior user / assistant turns via history so follow-up questions keep conversational context. System messages in history are ignored.

const first = await client.query("Who works at Acme Corp?");

const followUp = await client.query("What is her title?", {
  history: [
    { role: "user", content: first.query },
    { role: "assistant", content: first.answer },
  ],
});

With method: "combined", history is also passed to the strategy router so follow-ups like "tell me more about her" can pick better retrieval strategies.

Query tuning

Set defaults on the client, or override per call:

const client = new GraphClient({
  // ...
  query: { topK: 10, seedK: 5, maxHops: 3 },
});

await client.query("Who works at Acme Corp?", { method: "bfs", topK: 3, maxHops: 1 });
  • topK — max ranked nodes/edges or BFS neighborhood size
  • seedK — max seed nodes selected by similarity for expansion strategies
  • maxHops — hop limit for BFS neighborhood expansion

Ingestion chunking

Large documents are split before LLM extraction. Configure defaults on the client or override per ingest call:

import { DEFAULT_INGESTION_CHUNK_SIZE, GraphClient } from "graphtide";

const client = new GraphClient({
  // ...
  ingestion: { chunkSize: DEFAULT_INGESTION_CHUNK_SIZE },
});

await client.ingestFromPath("./large-document.pdf");
await client.ingestFromPath("./other.pdf", { chunkSize: 8_000 });
await client.ingestFromText(longText, { chunker: (text) => text.split("\n---\n") });
  • chunkSize — max characters per chunk (paragraph-aware splitting)
  • chunker — custom chunker function; takes precedence over chunkSize
  • Pre-chunked string[] inputs are respected, but each entry can be split further when chunking is enabled

Ontology

Define node and edge types with typed properties. Use the Ontology type or validate at runtime with OntologySchema.

Property shorthands like "string" are required on instances. Mark a property optional with optional: true or required: false:

const ontology: Ontology = {
  nodeTypes: [
    {
      id: "person",
      name: "Person",
      properties: {
        name: "string",
        nickname: { type: "string", optional: true },
        meta: {
          type: "object",
          optional: true,
          properties: {
            active: "boolean",
            note: { type: "string", optional: true },
          },
        },
      },
    },
  ],
  edgeTypes: [],
};

Explicit null is still allowed for any property value when the property is present.

Development

Conventional commits

This project uses Conventional Commits for semantic versioning and changelog generation.

feat: add getBfsNeighborhood to GraphClient
fix: handle empty seed lists in BFS expansion
feat!: rename QueryMethod "bfs" to "breadth_first"

Install git hooks (repo contributors only):

  • pre-commitnpm run fl via pre-commit
  • commit-msg — conventional commit lint via commitlint
npm run setup-hooks

Lint the latest commit manually:

npm run commitlint

Releases

Releases use commit-and-tag-version:

| Script | Description | | ------------------------- | ---------------------------------------------------------------- | | npm run test:unit | Run unit tests | | npm run changelog | Preview the next version and changelog (dry run) | | npm run release:initial | First release: v0.1.0, changelog, tag, and npm publish | | npm run release | Bump semver from commits since last tag, changelog, tag, publish | | npm run release:patch | Force a patch release and publish | | npm run release:minor | Force a minor release and publish | | npm run release:major | Force a major release and publish |

License

MIT