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

@exellix/narrix-runner

v2.0.0

Published

Thin orchestration between callers and the narrix pipeline: CNI + datasetId → pack resolution → engine → stories and signals

Readme

@exellix/narrix-runner

Thin orchestration glue between callers (e.g. @woroces/ai-tasks) and the narrix pipeline.

The runner always speaks CNI. Give it a CniV11 and a datasetId. It resolves the right pack, runs the engine, and returns stories and signals. What produced the CNI is not its concern.

CniV11 + datasetId
  → narrix-packs-library (resolve pack by datasetId)
  → narrix-engine (run pipeline)
  → { stories, signals }

Install

npm i @exellix/narrix-runner

Three dependencies, nothing else: narrix-engine, narrix-packs-library, narrix-cni.


Quick start

import { createRunner } from "@exellix/narrix-runner";
import type { CniV11 } from "@exellix/narrix-cni";

// Create once — stateless, reuse across all calls
const runner = await createRunner();

const result = await runner.run({
  cni: myCni,                        // caller is responsible for producing this
  datasetId: "neo.vulnerabilities"   // resolves the pack AND routes in the engine
});

console.log(result.stories);              // all stories, flattened across passes
console.log(result.signals);              // all signals, flattened across passes
console.log(result.passes.scoping);       // { stories, signals } for the scoping pass
console.log(result.passes.discovery);     // { stories, signals } for the discovery pass
console.log(result.entity.entityKey);     // deterministic join key

Input contract

type RunnerInput = {
  cni: CniV11;           // always required
  datasetId: string;     // always required
  processorId?: string;  // optional — explicit processor override
  sourceMeta?: SourceMeta;
};

The runner has exactly one input shape. There is no kind field and no routing on input type. If you have unstructured data (text, chat, records) that isn't CNI yet, convert it to CniV11 before calling the runner. That conversion is not this package's responsibility.


Pack resolution

The runner calls findPackByDatasetId(datasetId) from @exellix/narrix-packs-library. The library finds the pack whose processor route.datasetIds includes the given datasetId. One datasetId maps to exactly one pack. You don't configure this — pass a datasetId and it works.

Fallback: pass the pack directly

For testing or edge cases where datasetId-based resolution isn't available:

const result = await runner.run({
  cni: myCni,
  datasetId: "test.dataset",
  pack: myResolvedPack    // bypasses library lookup
});

Resolution priority: caller-provided packfindPackByDatasetId(datasetId) → throws RUNNER_PACK_NOT_FOUND.


Result shape

type RunnerResult = {
  stories: NarrativeStory[];   // flattened across all passes
  signals: NarrativeSignal[];  // flattened across all passes

  passes: Record<string, {
    stories: NarrativeStory[];
    signals: NarrativeSignal[];
  }>;

  entity: {
    entityKind: string;
    entityId?: string;
    entityKey: string;
  };

  meta: {
    runId: string;
    producedAt: number;
    processorId: string;
    packId: string;
    packVersion: string;
    datasetId: string;
  };

  raw?: unknown;  // only present when includeRaw: true
};

stories and signals are the primary outputs — flattened for callers that don't need pass separation. passes gives per-pass access when you do. Facts are not in the default output; use includeRaw: true to get the full _narrix attachment.


Batch processing

const batchResult = await runner.runMany({
  inputs: cniList.map(cni => ({ cni, datasetId: "neo.vulnerabilities" })),
  onError: "attachError"   // "throw" | "attachError" | "skip" — default: "attachError"
});

console.log(batchResult.meta);    // { total, succeeded, failed, skipped }

for (const item of batchResult.results) {
  if ("error" in item) {
    console.error(item.code, item.message);
  } else {
    console.log(item.stories);
  }
}

Failed inputs produce a RunnerErrorResult in the results array instead of aborting the batch when onError: "attachError".


Full API

createRunner(options?)

const runner = await createRunner({
  featureRegistry?: {
    async execute(name: string, ctx: unknown): Promise<FeatureResult>
  },
  runtime?: {
    onProcessorNotMatched?: "throw" | "attachError" | "skip",
    onMissingMapping?: "throw" | "attachError",
    deterministicSort?: boolean
  }
});

Create once per process. Reuse for all calls.

runner.run(options)

const result = await runner.run({
  cni: CniV11,
  datasetId: string,
  processorId?: string,
  sourceMeta?: SourceMeta,
  pack?: NarrixPack,       // optional fallback
  includeRaw?: boolean     // default: false
});

runner.runMany(options)

const result = await runner.runMany({
  inputs: RunnerInput[],
  pack?: NarrixPack,       // if provided, used for all inputs
  onError?: "throw" | "attachError" | "skip",
  includeRaw?: boolean
});

Accessing the full engine output

const result = await runner.run({
  cni: myCni,
  datasetId: "neo.vulnerabilities",
  includeRaw: true
});

// result.raw is the full enriched record with _narrix attachment
// typed as unknown — you own the shape if you use this

Error codes

| Code | Meaning | |---|---| | RUNNER_PACK_NOT_FOUND | findPackByDatasetId returned nothing for the given datasetId | | RUNNER_ENGINE_FAILED | Engine threw an unrecoverable error | | RUNNER_PROCESSOR_NOT_MATCHED | No processor matched the input (propagated from engine) |

All errors are RunnerError extends Error with code, message, and optional cause.


Package relationships

caller (e.g. @woroces/ai-tasks)
  │  produces CniV11 however it needs to
  │
  ▼
@exellix/narrix-runner               ← you are here
  │
  ├── @exellix/narrix-packs-library
  │       findPackByDatasetId(datasetId) → NarrixPack
  │
  └── @exellix/narrix-engine
          runOne(cni, pack) → _narrix attachment
                │
                ▼
        { stories, signals }

Auth

Uses the token in your repo's .npmrc / user .npmrc. Do not paste tokens into code or docs.


License

Proprietary (internal).