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

@ai-node-editor/core

v0.1.0

Published

A Blender-inspired React node editor for building AI workflows.

Readme

@ai-node-editor/core

A reusable React and TypeScript package for building AI workflow node editors. It provides a Blender-inspired dark node graph interface plus a pure TypeScript graph engine for AI pipelines, RAG workflows, agents, prompt engineering, fine-tuning, training, media processing, automation, evaluation, and deployment flows.

Important legal note: this project is Blender-inspired, but it does not include Blender logos, trademarks, icons, assets, source code, or proprietary UI resources. The components, CSS, SVG-style visuals, graph model, and implementation are original.

What You Get

  • React node editor component: AINodeEditor
  • Pure TypeScript graph model and graph utilities
  • Node and plugin registries
  • Port compatibility and validation logic
  • Serialization and deserialization helpers
  • Traversal helpers such as topological sort and upstream/downstream lookup
  • Command stack foundation for undo/redo
  • Async graph execution engine with logs, statuses, cancellation, retries, and dry-run validation
  • Schema-driven node config UI
  • Blender-inspired dark CSS theme
  • Built-in AI node definitions for RAG, LLMs, agents, training, media, automation, evaluation, and outputs
  • Demo workflows and Vite playground

Installation

Install the package and peer dependencies:

npm install @ai-node-editor/core react react-dom

Import the editor and theme CSS:

import { AINodeEditor, builtinAINodes, createGraph } from "@ai-node-editor/core";
import "@ai-node-editor/core/styles/blender-dark.css";

React and React DOM are peer dependencies so the package can be used inside existing React apps without bundling a second React copy.

Quick Start

import { AINodeEditor, builtinAINodes, createGraph } from "@ai-node-editor/core";
import "@ai-node-editor/core/styles/blender-dark.css";

const initialGraph = createGraph({
  name: "My AI Workflow"
});

export function App() {
  return (
    <AINodeEditor
      nodes={builtinAINodes}
      initialGraph={initialGraph}
      theme="blender-dark"
      onGraphChange={(graph) => console.log(graph)}
    />
  );
}

The editor fills its parent. Give the parent a real height:

html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
}

Controlled Usage

Use controlled mode when your app owns the graph state.

import { useState } from "react";
import { AINodeEditor, builtinAINodes, createGraph, type Graph } from "@ai-node-editor/core";
import "@ai-node-editor/core/styles/blender-dark.css";

export function WorkflowBuilder() {
  const [graph, setGraph] = useState<Graph>(() =>
    createGraph({
      name: "Production RAG Pipeline"
    })
  );

  return (
    <AINodeEditor
      nodes={builtinAINodes}
      graph={graph}
      onGraphChange={setGraph}
      onNodeSelect={(node) => console.log("Selected node", node)}
      theme="blender-dark"
    />
  );
}

Uncontrolled Usage

Use uncontrolled mode when you only need to provide an initial graph.

<AINodeEditor
  nodes={builtinAINodes}
  initialGraph={createGraph({ name: "Draft Workflow" })}
  onGraphChange={(graph) => saveDraft(graph)}
  theme="blender-dark"
/>

Editor Props

interface AINodeEditorProps {
  nodes?: NodeDefinition[];
  initialGraph?: Graph;
  graph?: Graph;
  onGraphChange?: (nextGraph: Graph) => void;
  onNodeSelect?: (node: NodeInstance | undefined) => void;
  onSelectionChange?: (selection: SelectionState) => void;
  onExecute?: (graph: Graph) => void | Promise<void>;
  onValidate?: (issues: ValidationIssue[]) => void;
  onError?: (error: unknown) => void;
  theme?: "blender-dark" | string;
  readonly?: boolean;
  plugins?: Plugin[];
  className?: string;
  style?: React.CSSProperties;
  themeVariables?: EditorThemeVariables;
  nodeClassName?: (node: NodeInstance, definition?: NodeDefinition) => string | undefined;
  nodeStyle?: (node: NodeInstance, definition?: NodeDefinition) => React.CSSProperties | undefined;
  edgeClassName?: (edge: Edge, graph: Graph) => string | undefined;
  edgeStyle?: (edge: Edge, graph: Graph) => React.CSSProperties | undefined;
  renderNode?: (node: NodeInstance, definition: NodeDefinition | undefined, context: RenderNodeContext) => React.ReactNode;
  renderSocket?: (node: NodeInstance, port: PortDefinition, context: RenderSocketContext) => React.ReactNode;
  renderEdge?: (edge: Edge, context: RenderEdgeContext) => React.ReactNode;
  renderInspectorField?: (fieldKey: string, field: ConfigField, node: NodeInstance) => React.ReactNode;
  executionEngine?: GraphEngine;
  registry?: NodeRegistry;
  options?: EditorOptions;
}

Editor Options

interface EditorOptions {
  snapToGrid?: boolean;
  gridSize?: number;
  showMinimap?: boolean;
  showNodeLibrary?: boolean;
  showInspector?: boolean;
  showExecutionLog?: boolean;
  allowCycles?: boolean;
  readonly?: boolean;
  enableKeyboardShortcuts?: boolean;
  enableContextMenu?: boolean;
  enableAutoOffset?: boolean;
  enableRerouteNodes?: boolean;
  enableFrameNodes?: boolean;
}

Example:

<AINodeEditor
  nodes={builtinAINodes}
  initialGraph={graph}
  theme="blender-dark"
  options={{
    snapToGrid: true,
    gridSize: 24,
    showInspector: true,
    enableContextMenu: true
  }}
/>

Creating A Custom Node

import { createNodeDefinition } from "@ai-node-editor/core";

export const sentimentNode = createNodeDefinition({
  type: "custom.sentiment-analysis",
  label: "Sentiment Analysis",
  category: "Text",
  description: "Analyzes sentiment from text.",
  version: "1.0.0",
  inputs: [
    {
      id: "text",
      label: "Text",
      direction: "input",
      dataType: "text",
      required: true,
      multiple: false
    }
  ],
  outputs: [
    {
      id: "sentiment",
      label: "Sentiment",
      direction: "output",
      dataType: "json",
      required: false,
      multiple: true
    }
  ],
  configSchema: {
    model: {
      type: "string",
      label: "Model",
      default: "gpt-4.1-mini",
      required: true
    },
    temperature: {
      type: "slider",
      label: "Temperature",
      default: 0.2,
      min: 0,
      max: 2,
      step: 0.1
    }
  },
  async execute({ inputs, config, signal }) {
    if (signal.aborted) {
      throw new Error("Cancelled");
    }

    return {
      outputs: {
        sentiment: {
          label: "positive",
          score: 0.94,
          model: config.model,
          source: inputs.text
        }
      },
      metrics: {
        latencyMs: 12
      }
    };
  }
});

Register it with built-ins:

<AINodeEditor
  nodes={[...builtinAINodes, sentimentNode]}
  initialGraph={graph}
  theme="blender-dark"
/>

NodeDefinition

interface NodeDefinition {
  type: string;
  label: string;
  category: NodeCategory;
  description?: string;
  icon?: string;
  version: string;
  inputs: PortDefinition[];
  outputs: PortDefinition[];
  configSchema?: ConfigSchema;
  defaultConfig?: NodeConfig;
  ui?: {
    headerColor?: string;
    width?: number;
    iconLabel?: string;
    compact?: boolean;
  };
  tags?: string[];
  deprecated?: boolean;
  allowCycles?: boolean;
  timeoutMs?: number;
  retryPolicy?: {
    retries: number;
    delayMs?: number;
  };
  validate?: (config: NodeConfig, node: NodeInstance, graph: Graph) => ValidationIssue[] | void;
  execute?: (context: NodeExecutionContext) => Promise<NodeExecutionResult> | NodeExecutionResult;
}

PortDefinition

interface PortDefinition {
  id: string;
  label: string;
  direction: "input" | "output";
  dataType: PortDataType;
  required: boolean;
  multiple: boolean;
  color?: string;
  description?: string;
  defaultValue?: unknown;
  accepts?: PortDataType[];
  ui?: {
    hidden?: boolean;
    compact?: boolean;
    inlineControl?: boolean;
  };
}

Supported port data types:

type PortDataType =
  | "string"
  | "number"
  | "boolean"
  | "json"
  | "text"
  | "document"
  | "documents"
  | "image"
  | "images"
  | "video"
  | "audio"
  | "embedding"
  | "embeddings"
  | "vector-store"
  | "model"
  | "llm-response"
  | "dataset"
  | "training-config"
  | "metrics"
  | "prompt"
  | "tool"
  | "agent"
  | "control"
  | "any";

Compatibility rules:

  • any connects to any data type unless explicitly restricted.
  • accepts can widen accepted input or output types.
  • Input ports accept one connection unless multiple: true.
  • Output ports can fan out to multiple inputs.
  • Invalid links are rejected by connectPorts and reported by validation.

Config Schema

Config schemas drive the right-side inspector UI and validation.

configSchema: {
  model: {
    type: "string",
    label: "Model",
    default: "gpt-4.1-mini",
    required: true,
    placeholder: "provider-model-name"
  },
  topK: {
    type: "number",
    label: "Top K",
    default: 5,
    min: 1,
    max: 50,
    step: 1
  },
  mode: {
    type: "select",
    label: "Mode",
    default: "balanced",
    options: [
      { label: "Fast", value: "fast" },
      { label: "Balanced", value: "balanced" },
      { label: "Careful", value: "careful" }
    ]
  }
}

Supported field types:

  • string
  • number
  • boolean
  • select
  • multiselect
  • textarea
  • json
  • code
  • slider
  • secret
  • file
  • color

Field options include label, description, default, required, min, max, step, options, placeholder, visibleWhen, validation, and ui.

Plugins

Plugins are collections of node definitions and optional metadata.

import {
  createNodeRegistry,
  createPlugin,
  createPluginRegistry
} from "@ai-node-editor/core";

const plugin = createPlugin({
  id: "my-company-ai-pack",
  name: "My Company AI Pack",
  version: "1.0.0",
  description: "Private workflow nodes for internal tools.",
  nodes: [sentimentNode]
});

const nodeRegistry = createNodeRegistry();
const pluginRegistry = createPluginRegistry(nodeRegistry);

pluginRegistry.register(plugin);

Use the registry in the editor:

<AINodeEditor
  registry={nodeRegistry}
  nodes={nodeRegistry.list()}
  initialGraph={graph}
  theme="blender-dark"
/>

Graph Utilities

import {
  addNode,
  cloneGraph,
  connectPorts,
  createGraph,
  createNode,
  deserializeGraph,
  detectCycles,
  getDownstreamNodes,
  getUpstreamNodes,
  removeEdge,
  removeNode,
  serializeGraph,
  topologicalSort,
  updateNode,
  validateGraph
} from "@ai-node-editor/core";

Example:

const graph = createGraph({ name: "Website RAG" });
const urlNode = createNode(urlDefinition, { position: { x: 0, y: 0 } });
const scraperNode = createNode(scraperDefinition, { position: { x: 280, y: 0 } });

let nextGraph = addNode(graph, urlNode);
nextGraph = addNode(nextGraph, scraperNode);
nextGraph = connectPorts(
  nextGraph,
  builtinAINodes,
  urlNode.id,
  "url",
  scraperNode.id,
  "url"
);

const issues = validateGraph(nextGraph, builtinAINodes);

Serialization

Save a graph:

const json = serializeGraph(graph, { pretty: true });
localStorage.setItem("workflow", json);

Load a graph:

const restored = deserializeGraph(localStorage.getItem("workflow")!);

The serialized envelope includes:

{
  "schema": "@ai-node-editor/graph",
  "schemaVersion": "1.0.0",
  "graph": {}
}

Validation

const issues = validateGraph(graph, builtinAINodes, {
  strictConfig: true,
  allowCycles: false
});

const errors = issues.filter((issue) => issue.severity === "error");

Validation checks include:

  • Missing required inputs
  • Unknown node types
  • Invalid ports
  • Incompatible data types
  • Duplicate node and edge IDs
  • Dangling edges
  • Input multiplicity violations
  • Cycles when cycles are not allowed
  • Deprecated nodes
  • Unknown config fields
  • Invalid config values
  • Missing required output node types when configured

Execution Engine

Use GraphEngine to validate and execute a graph.

import { GraphEngine, builtinAINodes } from "@ai-node-editor/core";

const engine = new GraphEngine({
  registry: builtinAINodes,
  graphTimeoutMs: 120000,
  nodeTimeoutMs: 30000
});

const unsubscribe = engine.on((event) => {
  console.log(event.type, event.nodeId, event.error);
});

const result = await engine.execute(graph);

unsubscribe();

if (!result.success) {
  console.error(result.issues, result.logs, result.error);
}

Dry-run validation:

const result = await engine.execute(graph, {
  dryRun: true
});

Cancellation:

const controller = new AbortController();

const promise = engine.execute(graph, {
  signal: controller.signal
});

controller.abort();

const result = await promise;

Partial execution:

await engine.execute(graph, {
  fromNodeId: "node_123"
});

await engine.execute(graph, {
  toNodeId: "output_node_456"
});

Lifecycle events:

  • graph:start
  • graph:validate
  • graph:error
  • graph:success
  • graph:cancel
  • node:queued
  • node:start
  • node:success
  • node:error
  • node:skipped
  • edge:data

Built-In Nodes

Import all built-ins:

import { builtinAINodes } from "@ai-node-editor/core";

Or import grouped node packs:

import {
  audioNodes,
  controlNodes,
  dataNodes,
  evaluationNodes,
  imageNodes,
  llmNodes,
  outputNodes,
  ragNodes,
  scratchTrainingNodes,
  textNodes,
  trainingNodes,
  videoNodes
} from "@ai-node-editor/core";

Built-in categories:

  • Data sources: website URL, web scraper, sitemap crawler, file upload, PDF extractor, CSV loader, JSON loader, database query, API request, webhook trigger
  • Text processing: cleaner, chunker, entity extraction, summarization, translation, regex extraction, metadata extraction
  • RAG: embedding model, vector store upsert/search, retriever, reranker, context builder, RAG answer generator
  • LLM and agents: prompt template, system prompt, chat model, completion model, tool calling, structured output parser, JSON schema validator, routers, planner, executor, memory
  • Training: dataset loader, validator, splitter, tokenizer, config, fine-tuning job, prompt tuning, LoRA config, metrics, registry push
  • Training from scratch: corpus loader, tokenizer trainer, pretraining config, distributed config, checkpoint saver, loss monitor, validation loop, artifact export
  • Image, video, and audio processing
  • Automation and control flow
  • Evaluation
  • Output and deployment

Built-in executors are intentionally mock/stub implementations. Real providers should be injected through your own custom nodes or a plugin package.

Demo Workflows

The package exports demo graphs:

import {
  agentWorkflow,
  fineTuningWorkflow,
  imageVideoWorkflow,
  ragWorkflow,
  sampleWorkflow,
  scratchTrainingWorkflow
} from "@ai-node-editor/core";

Render one:

<AINodeEditor
  nodes={builtinAINodes}
  initialGraph={ragWorkflow}
  theme="blender-dark"
/>

Styling And Theming

Default theme import:

import "@ai-node-editor/core/styles/blender-dark.css";

Base-only import:

import "@ai-node-editor/core/styles/base.css";

The theme is based on CSS variables scoped to .aine-theme-blender-dark. Override variables from your app:

.my-workflow-editor.aine-theme-blender-dark {
  --aine-bg: #181818;
  --aine-accent: #6fa0da;
  --aine-blue-selection: #477dca;
  --aine-node-bg: #2c2c2c;
}

Use a custom class:

<AINodeEditor
  className="my-workflow-editor"
  nodes={builtinAINodes}
  initialGraph={graph}
  theme="blender-dark"
/>

CSS classes are prefixed with aine- to reduce collisions.

You can also override theme variables directly from React:

<AINodeEditor
  nodes={builtinAINodes}
  initialGraph={graph}
  theme="blender-dark"
  themeVariables={{
    "--aine-bg": "#111315",
    "--aine-node-bg": "#25282c",
    "--aine-blue-selection": "#5b9cff",
    "--aine-socket-text": "#5fd0d6"
  }}
/>

Custom Node Rendering

<AINodeEditor
  nodes={nodes}
  initialGraph={graph}
  nodeClassName={(node) => (node.status === "error" ? "my-node-error" : undefined)}
  nodeStyle={(node, definition) => ({
    borderColor: node.customColor ?? definition?.ui?.headerColor
  })}
  renderNode={(node, definition, ctx) => (
    <div className="my-node-shell">
      {ctx.renderDefaultNode()}
      <footer>{definition?.category}</footer>
    </div>
  )}
/>

renderNode receives a context object so custom nodes can keep first-class editor behavior:

  • ctx.renderDefaultNode() renders the package's default compact node UI.
  • ctx.renderSocket(port) renders a socket with the correct data attributes and pointer handling.
  • ctx.inputPorts and ctx.outputPorts let you build a fully custom layout while preserving connection behavior.

Custom sockets and edges can be styled or replaced too:

<AINodeEditor
  nodes={nodes}
  initialGraph={graph}
  renderSocket={(node, port, ctx) => (
    <span className={`my-socket my-socket-${port.dataType}`}>
      {ctx.defaultSocket}
    </span>
  )}
  edgeStyle={(edge) => ({
    strokeWidth: edge.selected ? 4 : 2
  })}
  renderEdge={(edge, ctx) => (
    <>
      {ctx.defaultEdge}
      {edge.status === "running" ? <circle r={3} cx={ctx.source.x} cy={ctx.source.y} className="my-edge-pulse" /> : null}
    </>
  )}
/>

For data manipulation, keep graph state controlled with graph and onGraphChange, or use the pure graph helpers such as addNode, updateNode, connectPorts, removeEdge, validateGraph, serializeGraph, and GraphEngine.

Custom Inspector Fields

<AINodeEditor
  nodes={nodes}
  initialGraph={graph}
  renderInspectorField={(fieldKey, field, node) => {
    if (fieldKey === "apiKey") {
      return <input type="password" aria-label={field.label} />;
    }

    return undefined;
  }}
/>

Command Stack

import {
  AddNodeCommand,
  CommandStack,
  MoveNodeCommand,
  type Graph
} from "@ai-node-editor/core";

const stack = new CommandStack<Graph>();

let graph = stack.execute(new AddNodeCommand(node), currentGraph);
graph = stack.execute(
  new MoveNodeCommand(node.id, node.position, { x: 240, y: 80 }),
  graph
);

graph = stack.undo(graph);
graph = stack.redo(graph);

Local Development

Clone the project and install dependencies:

npm install

Run the demo playground:

npm run dev

Run tests:

npm test

Typecheck:

npm run typecheck

Build the library:

npm run build

The build emits:

  • dist/ai-node-editor.js
  • dist/ai-node-editor.cjs
  • dist/index.d.ts
  • dist/styles/base.css
  • dist/styles/blender-dark.css

Publishing To npm

The package is configured for public scoped npm publishing:

{
  "publishConfig": {
    "access": "public"
  }
}

Recommended release check:

npm run release:check

This runs:

  • TypeScript typecheck
  • Vitest tests
  • Clean production build
  • Package artifact/export validation
  • npm pack --dry-run --ignore-scripts

Publish:

npm login
npm publish

For a scoped package like @ai-node-editor/core, the first public publish must use public access. This repository already sets publishConfig.access to public, so plain npm publish is enough.

To inspect the package before publishing:

npm run pack:dry-run

Using From A Local Checkout

Build and pack locally:

npm run build
npm pack

Install the generated tarball in another app:

npm install ../ai-node-editor/ai-node-editor-core-0.1.0.tgz

Then import it normally:

import { AINodeEditor } from "@ai-node-editor/core";
import "@ai-node-editor/core/styles/blender-dark.css";

Package Exports

import {
  AINodeEditor,
  GraphEngine,
  NodeRegistry,
  PluginRegistry,
  addNode,
  builtinAINodes,
  cloneGraph,
  connectPorts,
  createGraph,
  createNode,
  createNodeDefinition,
  createPlugin,
  deserializeGraph,
  detectCycles,
  getDownstreamNodes,
  getUpstreamNodes,
  removeEdge,
  removeNode,
  serializeGraph,
  topologicalSort,
  updateNode,
  validateGraph
} from "@ai-node-editor/core";

CSS exports:

import "@ai-node-editor/core/styles/base.css";
import "@ai-node-editor/core/styles/blender-dark.css";

Current Implementation Status

Implemented:

  • Package build and npm-ready exports
  • Strict TypeScript graph engine
  • Registries, commands, validation, compatibility, serialization, traversal
  • Async execution engine
  • Built-in AI node definitions and demo workflows
  • React editor shell with toolbar, breadcrumbs, dark canvas, nodes, edges, inspector, and right-click Add menu
  • Node selection, dragging, panning, canvas-confined wheel zoom, schema-driven inspector fields
  • Socket-to-socket connection dragging with compatibility checks and invalid-link feedback
  • Connection removal through the small midpoint remove handle
  • Unit tests for core behavior

Planned next:

  • Box select and multi-select drag interactions
  • Copy, cut, paste, duplicate, delete, undo, and redo wired into editor shortcuts
  • Richer compatible socket highlighting
  • Reroute nodes, frame nodes, minimap polish, and execution log UI wiring
  • More React component tests

Troubleshooting

The editor is invisible

Make sure the parent element has a height:

#root {
  height: 100vh;
}

Styles are missing

Import one of the CSS exports:

import "@ai-node-editor/core/styles/blender-dark.css";

Connections fail

Check port data types and accepts:

const result = checkConnectionCompatibility(
  graph,
  builtinAINodes,
  sourceNodeId,
  sourcePortId,
  targetNodeId,
  targetPortId
);

if (!result.compatible) {
  console.warn(result.reason);
}

Validation reports missing inputs

Required input ports must either have an incoming edge, a defaultValue, or a node config value matching that input ID.

npm publish fails for a scoped package

Confirm you are logged in and the package is public:

npm whoami
npm publish --access public

The package already includes publishConfig.access = "public".

License

MIT.