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

jevmod

v0.2.1

Published

Content moderation for user text, powered by Jev (TypeSafe System One): spam, scam, harassment, adult content, off-topic, self-harm, doxxing, minors, plus your own rules in plain language.

Readme

jevmod (npm)

Moderation for user text, powered by Jev (TypeSafe's System One model). Every message gets a probability for spam, scam, harassment, adult content, off-topic, self-harm, doxxing and sexual content involving minors, plus for rules you write in plain language. You own the thresholds and the actions; nothing is deleted until you turn that on.

This is the TypeScript port of the Python package in this repository. It asks Jev the same questions (categories.json is shared byte for byte), applies the same policy and returns the same decision shape.

Install

npm install jevmod

Node 20 or newer (the TypeSafe SDK requires it). ESM and CommonJS builds with type declarations are included. The only runtime dependency is @typesafe-ai/sdk.

Usage

import { Moderator, Policy } from "jevmod";

const mod = new Moderator(); // TYPESAFE_API_KEY from the environment
const d = await mod.check("FREE NITRO for the first 100!! claim at discord-gifts.ru/nitro", { channelTopic: "gaming" });
d.action; d.category; d.probability; // "flag", "scam", 0.97
d.scores; // { spam: 0.95, scam: 0.97, harassment: 0.03, ... }

const policy = new Policy();
policy.setCategory("scam", "delete", 0.7);
policy.setRule("no_politics", "No political discussion. Game news is fine.", "flag", 0.8);
const decisions = await new Moderator({ policy }).checkMany(["...", "...", "..."], { channelTopic: "support" });

checkMany is the cheap path: every message that passes the pre-filter goes to Jev in one request. Messages under eight letters without a link, trusted authors (authorTrusted: true) and repeats of already-judged text are never sent. Only the message text and the channel topic reach TypeSafe; author names and ids do not.

A decision looks like this, the same as the Python Decision.to_dict() and the HTTP API:

{"message_id": "0", "action": "flag", "category": "scam", "probability": 0.97,
 "scores": {"spam": 0.95, "scam": 0.97, "harassment": 0.03, "nsfw": 0.01, "selfharm": 0.0, "doxxing": 0.0, "minors": 0.0},
 "judged": true, "reason": "jev", "policy_version": 1}

action is none, flag, delete or timeout. category is the winning category or rule:<name>. reason is jev, cache, or why the pre-filter skipped it (too short, empty, trusted author).

Policy

Defaults: every category flags except offtopic, which is off (it needs a channelTopic to be useful). Thresholds: spam 0.85, scam 0.75, harassment 0.75, nsfw 0.8, offtopic 0.9, selfharm 0.8, doxxing 0.8, minors 0.7. Custom rules default to flag at 0.8; at most five per policy. policy.nudge("spam") raises a threshold by 0.03 (false-positive feedback). JSON.stringify(policy) and Policy.fromJSON(obj) use the same shape as the Python package and GET/PUT /v1/policy, so a policy can move between the three.

Lower level

Judge does the batching, caching and the single Jev request (judge(messages, categories, customRules) returns Verdicts with raw probabilities; counters requests, inputTokens, judgedMessages). decide(policy, verdict) turns a verdict into a decision. normalize and prefilter are exported for tests and tooling.

API key

The TypeSafe key comes from the TYPESAFE_API_KEY environment variable or from the constructor:

new Moderator({ apiKey: process.env.MY_SECRET_STORE_KEY });

It is passed straight to the TypeSafe SDK and is never logged, never included in error messages, and never written to disk by this package. Keep it out of git: put it in your process environment or a .env file that is ignored. The SDK's debug log level prints request bodies (message text), not credentials; the default level is warn.

HTTP client for a deployed jevmod API

If you run the Python HTTP API (jevmod api or the Docker image) so that only the server holds the TypeSafe key, callers use a tenant key minted with POST /v1/keys and this client:

import { JevmodClient } from "jevmod";

const api = new JevmodClient({ baseUrl: "https://mod.example.com", apiKey: process.env.JEVMOD_API_KEY });
const res = await api.moderate(["hello there", { id: "b", text: "DM me to double your ETH", channel_topic: "gaming" }]);
res.decisions; // same decision objects, without policy_version
await api.getPolicy();
await api.putPolicy({ actions: { scam: "delete" }, thresholds: { scam: 0.7 }, rules: { no_politics: "No politics." } });
await api.decisions(50); // the tenant's recent decisions

baseUrl and apiKey fall back to JEVMOD_API_URL and JEVMOD_API_KEY. Failures throw JevmodApiError with the HTTP status and the response body; the key is not part of the message. Uses the global fetch.

Examples

  • examples/node.mjs: check, checkMany, a custom policy, the HTTP client.
  • examples/express.mjs: an Express middleware that moderates a body field, rejects delete/timeout decisions with 422, lets staff through, and fails open when Jev is unreachable.

Build first (npm run build), then TYPESAFE_API_KEY=... node examples/node.mjs. Express is a dev dependency of this package only for the example.

Shared questions

The questions Jev is asked live in categories.json at the root of the Python package (jevmod/categories.json). This package ships a verbatim copy in src/categories.json; npm run sync-categories refreshes it and a test fails if the two files differ. Do not edit the wording here: every implementation (Python, npm, MCP) must ask Jev exactly the same thing so their numbers are comparable.

Two details of the port worth knowing: text is normalised with the same rules as Python (HTML entities, NFKC, combining marks removed using a table generated from Python's unicodedata, format characters removed), and the cache key is the same SHA-256 prefix, so verdicts and keys line up across the two packages. htmlUnescape covers numeric entities and the common named ones, not the full HTML5 table.

Development

npm install
npm run typecheck   # tsc --strict
npm run build       # tsup: dist/index.js (ESM), dist/index.cjs, .d.ts
npm test            # vitest; live tests run only when TYPESAFE_API_KEY is set

Live tests call the real Jev API on the same sample messages as the Python test suite. They are skipped, not mocked, without a key.