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

nakdanix

v0.4.0

Published

Hebrew niqqud (vocalization) and deterministic gender rewriting (no API key needed) with batch, templates, CLI, and an MCP server. LLM fallback for cases the rules don't cover.

Readme

nakdanix

npm version npm downloads license types

Hebrew niqqud (vocalization) and gender rewriting with batch, templates, CLI, and an MCP server.

  • TypeScript types shipped in the box · Node 18+
  • Vocalization powered by Dicta's free public Nakdan API
  • Gender rewriting deterministic by default — no API key needed. Uses Dicta's task=morph analyzer plus a compact rule set. LLM fallback kicks in only when the rules leave gender-variable words unhandled AND an LLM is configured.
npm install nakdanix

Installation & usage

| Situation | Command | | --- | --- | | Use in a project | npm install nakdaniximport { nikud } from 'nakdanix' | | Global CLI, use anywhere | npm install -g nakdanixnakdanix "..." | | One-off CLI without installing | npx -y nakdanix "..." | | One-off MCP server without installing | npx -y nakdanix-mcp | | Install straight from GitHub (pre-release testing) | npm install github:eliranchayo/nakdanix | | Develop locally against your app | npm link in this repo, then npm link nakdanix in your app |

Requires Node 18+ for native fetch. Older Node works if you inject a fetch implementation via opts.fetch.

Attribution

nakdanix is a client for the Dicta Nakdan API. Dicta is an Israeli nonprofit that publishes state-of-the-art Hebrew NLP tools and offers the Nakdan service free of charge with no formal SLA. Everything vocalized by nakdanix passes through Dicta's servers. Please be considerate with your request volumes — the built-in rate limiting and concurrency caps exist to keep pressure light.

How this came to be

I built nakdanix while working on a narrated Hebrew trivia game. The game needs hundreds of banter lines run through TTS, and I hit six problems in a row that existing Hebrew nikud clients didn't solve:

  1. My banter lines have {player_name} placeholders that need to survive the vocalization round-trip.
  2. I have hundreds of lines to process, not one.
  3. Dicta's morpheme-boundary pipes (|) were being read literally by the TTS.
  4. I wanted to add nikud from the shell for quick spot-checks.
  5. I wanted Claude to add nikud directly while writing code.
  6. Players in the game are addressed by name and by gender — so every masc line needed a fem twin, and Dicta has no inflection endpoint to generate them.

Other people building narrated / templated / batch Hebrew content probably have the same needs. Hence the package.

Quick start

import { nikud, batch } from 'nakdanix'

// One string, plain
await nikud('שלום עולם')
// → 'שָׁלוֹם עוֹלָם'

// Templates survive untouched
await nikud('נכון, {name}. יופי לך.')
// → 'נָכוֹן, {name}. יוֹפִי לְךָ.'

// Batch with retry, concurrency, per-item error capture
const results = await batch(lines, {
  concurrency: 4,
  onProgress: ({ index, total }) => console.log(`${index + 1}/${total}`),
})

Features

| | | |---|---| | Template preservation | {name}, {{var}}, ${slot} (and any regex you pass) survive the round-trip; only the Hebrew fragments between them are vocalized. | | Pipe stripping | Dicta's morpheme-boundary pipes (|) are removed by default. Opt back in with preserveMorphemePipes: true. | | Batch API | batch(items[]) runs strings in parallel with configurable concurrency (default 4), inter-request throttle (default 150 ms), and exponential-backoff retry on HTTP 5xx / network errors. | | Per-item error capture | One failing string doesn't fail the batch. Each item returns {output, error}. | | CLI | nakdanix "..." for shell one-shots. Reads from arg or stdin. | | MCP server | nakdanix-mcp exposes nikud, nikud_batch, and variant tools over stdio for Claude and other AI agents. | | Gender rewriting | variant(text, {gender}) rewrites masc↔fem across pronouns, verbs, and adjectives. Kinship nouns and placeholders preserved. See "Morphological variants" below. | | Types included | TypeScript declarations ship in dist/. |

Library

import { nikud, batch, callDicta, NakdanixError } from 'nakdanix'

nikud(text, opts?):

| Option | Type | Default | Meaning | | --- | --- | --- | --- | | preferGender | 'masculine' \| 'feminine' | — | Pick gender-matching vocalization when candidates share consonants. See "Gender-aware vocalization" below. | | preserveMorphemePipes | boolean | false | Keep Dicta's | word-segmentation markers | | disablePlaceholders | boolean | false | Turn off template preservation | | placeholderRe | RegExp | /\$\{[^}]+\}\|\{\{[^}]+\}\}\|\{[^}]+\}/g | Custom placeholder pattern | | genre | 'modern' \| 'poetry' \| 'rabbinic' | 'modern' | Vocalization style | | timeoutMs | number | 30000 | Abort after this many ms | | endpoint | string | Dicta's default | Override the API URL | | fetch | function | globalThis.fetch | Inject a custom fetch |

batch(inputs, opts?) accepts all of the above plus:

| Option | Type | Default | Meaning | | --- | --- | --- | --- | | concurrency | number | 4 | Max simultaneous requests | | minIntervalMs | number | 150 | Min ms between requests per worker | | maxRetries | number | 3 | Retry attempts on transient failure | | retryBaseMs | number | 500 | Initial backoff; doubles each attempt | | onProgress | function | — | Called after each item completes |

Lower-level exports (callDicta, splitTemplate, rejoin, stripMorphemePipes, NakdanixError) are also available for consumers who want raw control.

Gender-aware vocalization

Some Hebrew words share the same consonants across masculine and feminine but differ in vocalization — mostly ל"ה verbs in present tense (עונה, רוצה, שותה, בונה) and a handful of adjectives. Pass preferGender and nakdanix picks the matching vocalization from Dicta's candidates:

const s = 'אני עונה על השאלה'

await nikud(s, { preferGender: 'masculine' })
// → 'אֲנִי עוֹנֶה עַל הַשְּׁאֵלָה'   ("oneh")

await nikud(s, { preferGender: 'feminine' })
// → 'אֲנִי עוֹנָה עַל הַשְּׁאֵלָה'   ("onah")

Words that don't vary by gender (nouns, particles, unambiguously-gendered verbs) are unaffected — the top pick is used.

Scope: this only covers the vowel-only variation case. Cases where masculine and feminine use different consonants entirely (יודע masc vs יודעת fem — extra ת) are a morphology task, not a vocalization one. See Morphological variants (variant()) below for that.

Morphological variants (variant())

preferGender above only picks between vocalization candidates for the same consonantal skeleton. When the target gender needs different consonants — אתה יודע (masc) → את יודעת (fem, extra ת), or a verb + adjective + pronoun that all need to re-agree — reach for variant().

import { variant } from 'nakdanix'

await variant('אתה יודע לשחות', { gender: 'feminine' })
// → 'את יודעת לשחות'

await variant('אתה רואה את הבית שלך', { gender: 'feminine' })
// → 'את רואה את הבית שלך'

await variant('שלום אמא, אתה יודע?', { gender: 'feminine' })
// → 'שלום אמא, את יודעת?'   (kinship אמא preserved)

await variant('אתה יודע {name}?', { gender: 'feminine' })
// → 'את יודעת {name}?'    (placeholder round-trips)

How it works — rules first, LLM only if needed

Dicta's public API doesn't have an inflection/generation endpoint (empirically confirmed against the full task enum: nakdan / markprefixes / abbrexp / tokenize / morph). But it DOES have task=morph, which returns per-word morphological analysis — including a bitfield we've decoded for POS + gender + tense. That's enough to build a rule-based rewriter for the common cases.

variant() is a router with three strategies:

| strategy: | Behavior | | --- | --- | | 'auto' (default) | Try rules first. If they cover every gender-variable word, return that. Otherwise, if an LLM is configured (via opts.llm or ANTHROPIC_API_KEY), fall back to the LLM. Otherwise, return the partial rules result. | | 'rules-only' | Never call an LLM. Returns {path: 'rules-partial', unhandled: [...]} on partial coverage (visible via variantVerbose). Fastest, deterministic, no key needed. | | 'llm-only' | Skip rules entirely. Original 0.3 behavior. |

What the rules cover (deterministic path, no key):

  • Pronouns: אתה↔את, אתם↔אתן, הוא↔היא, הם↔הן (plus clitic-prefixed variants like שאתה, ואת).
  • Present-tense verbs, regular + guttural + ל"ה (יודע↔יודעת, רואה stays).
  • Adjectives (גדול↔גדולה, קטן↔קטנה, sofit-letter aware).
  • 2nd-plural clitic swap (לכם↔לכן, שלכם↔שלכן).
  • Person-referring nouns (ילד↔ילדה, סטודנט↔סטודנטית, הבן↔הבת via prefix).

What falls to the LLM (if configured):

  • Past/future-tense verb conjugations across person + gender.
  • Third-person narrative rewrites needing coreference (הוא הלך אליההיא הלכה אליו).
  • Any word Dicta doesn't tag confidently.

To bring your own model instead of Anthropic:

await variant('...', {
  gender: 'feminine',
  llm: async (prompt) => callMyLLM(prompt),  // OpenAI, Ollama, dicta-lm, whatever
})

To see which path fired:

import { variantVerbose } from 'nakdanix'
const r = await variantVerbose('אתם באים היום', { gender: 'feminine' })
// { text: 'אתן באים היום', path: 'rules-partial', unhandled: ['באים'], rulesHandled: 1 }

Fixed-gender words

Kinship nouns (אמא, אבא, סבא, סבתא, אחות, אח, בת, בן, דוד, דודה) don't switch when the addressee's gender switches. They ship in DEFAULT_PRESERVE_LIST and are merged into the prompt automatically. Extend with opts.preserve for proper names or domain-specific terms:

await variant('שרה שאלה אם אתה בא', {
  gender: 'feminine',
  preserve: ['שרה'],  // merged with defaults
})
// → 'שרה שאלה אם את באה'

Pass replacePreserve: true if you need to drop the default list (rare — corpus-critical rewrites where אמא really should become אבא).

Options

variant(text, opts):

| Option | Type | Default | Meaning | | --- | --- | --- | --- | | gender | 'masculine' \| 'feminine' | — (required) | Target gender | | strategy | 'auto' \| 'rules-only' \| 'llm-only' | 'auto' | Router strategy (see table above) | | preserve | string[] | [] | Extra words to keep verbatim; merged with DEFAULT_PRESERVE_LIST | | replacePreserve | boolean | false | Use only preserve[]; drop defaults | | disablePlaceholders | boolean | false | Skip placeholder round-trip verification (LLM path only) | | placeholderRe | RegExp | /\$\{[^}]+\}\|\{\{[^}]+\}\}\|\{[^}]+\}/g | Custom placeholder pattern | | endpoint | string | Dicta default | Override the Dicta API base (rules path only) | | genre | 'modern' \| 'poetry' \| 'rabbinic' | 'modern' | Dicta genre (rules path only) | | llm | (prompt) => Promise<string> | Anthropic caller | Bring your own model | | apiKey | string | ANTHROPIC_API_KEY env | Anthropic key (default caller only) | | model | string | 'claude-haiku-4-5-20251001' | Model id (default caller only) | | timeoutMs | number | 30000 | LLM/Dicta call timeout | | anthropicBaseUrl | string | 'https://api.anthropic.com' | Override API base (LLM path only) | | fetch | function | globalThis.fetch | Inject a custom fetch |

There's also variantRules(text, opts) — the deterministic-only path — which returns {text, coverage, handled, unhandled} directly. Useful if you want to inspect coverage without going through the router.

Path A vs Path B — which to reach for

| Situation | Reach for | | --- | --- | | You want to say the same thing to masc and fem, and only the vowels change (עונֶה / עונָה) | nikud(text, { preferGender: 'feminine' }) — deterministic, no LLM | | Consonants change (יודעיודעת, רוצהרוצה same skeleton but combines with pronouns that DO change) | variant(text, { gender: 'feminine' }) — LLM | | Both vowels and consonants — pipeline it | variant → nikud (or use batch with chain: 'nikud') |

Batch

batch() accepts a variant option to rewrite each string, and a chain option to layer vocalization on top:

await batch(lines, {
  variant: { gender: 'feminine' },
  chain: 'nikud',  // rewrite each line, then vocalize the rewrite
})

Retry, per-item error capture, and concurrency all work as with plain nikud batches.

CLI

$ nakdanix 'שלום עולם'
שָׁלוֹם עוֹלָם

$ echo 'הבית הגדול' | nakdanix
הַבַּיִת הַגָּדוֹל

$ nakdanix --keep-pipes 'הבית הגדול'
הַ|בַּיִת הַ|גָּדוֹל

$ nakdanix --no-templates 'נכון, {name}. יופי.'
נָכוֹן, {נָאמֶה}. יוֹפִי.
# {name} treated as literal text; braces get vocalized too

$ nakdanix --gender fem 'אני עונה על השאלה'
אֲנִי עוֹנָה עַל הַשְּׁאֵלָה

# variant subcommand — rules-based by default, no API key needed
$ nakdanix variant --gender fem 'אתה יודע לשחות'
את יודעת לשחות

$ nakdanix variant --gender fem --preserve 'שרה,דוד' 'שרה אמרה לדוד שאתה בא'
שרה אמרה לדוד שאת באה

$ echo 'אתה יודע' | nakdanix variant --gender fem
את יודעת

# --verbose reports which path fired and any unhandled words
$ nakdanix variant --gender fem --verbose 'אתם באים היום'
אתן באים היום
[variant] path=rules-partial rulesHandled=1 unhandled=["באים"]

# force the LLM path (needs ANTHROPIC_API_KEY)
$ nakdanix variant --gender fem --strategy llm-only 'הוא הלך אתמול'
היא הלכה אתמול

MCP server

Wire it into an MCP-aware host (Claude Code, Claude Desktop):

{
  "mcpServers": {
    "nakdanix": {
      "command": "npx",
      "args": ["-y", "nakdanix-mcp"]
    }
  }
}

Restart the host and the agent gains three tools:

  • nikud(text, opts?) — vocalize one string. Preserves templates by default.
  • nikud_batch(inputs, opts?) — vocalize an array. Returns per-item {output, error}.
  • variant(text, {gender, preserve?}) — rewrite gender via the built-in LLM caller. Requires ANTHROPIC_API_KEY in the MCP server's env.

Example prompt to a wired-up agent:

"Add niqqud to this line: נכון, {name}. יופי לך."

The agent will call the nikud tool and return the vocalized form with the placeholder intact.

Error handling

Every failure throws NakdanixError. It carries an optional status (populated for HTTP errors) and preserves the underlying error via .cause.

import { nikud, NakdanixError } from 'nakdanix'

try {
  await nikud(text)
} catch (err) {
  if (err instanceof NakdanixError && err.status === 429) {
    // rate-limited — back off
  } else {
    throw err
  }
}

batch() captures per-item failures into results[i].error instead of throwing, so a single bad string never sinks the run.

Troubleshooting

  • Timeouts — Dicta's endpoint occasionally slow, especially on cold start. Bump timeoutMs (default 30 s) or reduce concurrency.
  • "empty text" error — you passed "" or null. nikud() requires non-empty input.
  • variant() returned partial coverage instead of falling back to the LLM — check that ANTHROPIC_API_KEY is set (or opts.llm is passed) AND strategy !== 'rules-only'. On 'auto' with no LLM, we return the partial rules result rather than throwing.
  • variant() with strategy: 'llm-only' throws "needs an LLM" — set ANTHROPIC_API_KEY, pass opts.apiKey, or bring your own via opts.llm.
  • variant() throws "placeholder mismatch" — the LLM dropped, duplicated, or reshaped a {name} token. Retry usually fixes it; persistent failures on the same input are worth reporting.
  • Rules path leaves my past-tense verb unchanged — expected. Past/future tense isn't in the rule set; use strategy: 'auto' with ANTHROPIC_API_KEY set, or strategy: 'llm-only'.
  • Non-Hebrew input — passed through unchanged, no request made. This is by design.
  • Output has literal { and } — expected; placeholders are preserved. If you want them vocalized, pass disablePlaceholders: true.
  • HTTP 5xx — nakdanix retries automatically (default 3 attempts, exponential backoff). Persistent failures usually mean Dicta is having a bad day.
  • Batch is slow — that's minIntervalMs doing its job. Lower it (or raise concurrency) if you're sure you won't strain the free service.

What it doesn't do

  • Translation (English ↔ Hebrew): not this package.
  • Transliteration (Hebrew ↔ Latin script): not this package.
  • Morphological analysis (root, binyan, POS tags): the raw Dicta response has more of this info — use callDicta() to get it.
  • Offline vocalization: everything goes through Dicta's servers. See FAQ.

FAQ

Is my text sent to Dicta? Yes — that's how vocalization happens. Don't pass anything you can't share with a third party.

Where does my text go for variant()? To whichever LLM you point it at. The default is Anthropic (api.anthropic.com); pass your own opts.llm to route elsewhere.

Does it work offline? No. Vocalization is done by Dicta's remote model, and variant() needs a remote (or self-hosted) LLM.

Can I self-host? In principle, yes — Dicta publishes their models on Hugging Face. In practice, running the model locally is nontrivial and the free API is easier.

Cloudflare Workers / edge runtimes? Yes — inject the runtime's fetch via opts.fetch.

Node < 18? Also works if you inject a fetch implementation via opts.fetch.

Why "nakdanix"? נַקְדָן (nakdan) is Hebrew for "vocalizer/pointer". The -ix suffix is unrelated to any other package.

Versioning

Currently on 0.x — the public API may change between minor versions. Once the API has been used in real projects without needing reshaping, we cut 1.0.0 and commit to strict semver from there.

Contributing

Bug reports and PRs welcome at github.com/eliranchayo/nakdanix.

Local dev:

git clone https://github.com/eliranchayo/nakdanix
cd nakdanix
npm install
npm run build
npm test  # note: hits the real Dicta endpoint, requires network

License

MIT © Eliran Chayo

The Dicta API is a separate project by the Dicta nonprofit and retains its own terms.