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

@meddevkit/crisis-signals

v0.2.0

Published

Offline keyword-and-context screening that flags first-person crisis language for human follow-up (unreviewed; not a diagnostic or risk-assessment tool)

Readme

@meddevkit/crisis-signals

A keyword-and-context screen for first-person crisis language. It is not a diagnostic or risk-assessment tool, it will miss some expressions of risk, and it must never be the only safeguard. Route any flagged text to a person according to your own escalation policy.

Status: in development, unreviewed. No clinician has reviewed the categories, the level mapping, the patterns or the test vectors yet. Do not describe this screen as validated. The measured limits below come from synthetic test vectors, not from clinical review or real-world use.

Measured limits

On text it was not written against, it caught about half of crisis statements. Use it as an explainable, fast, offline first-pass signal, never as the only safeguard.

Patterns version 0.2.0. All vectors are synthetic and unreviewed; intervals are Wilson 95% confidence intervals.

| Set | Crisis recall | Any-flag recall | Grief false alarms (→ crisis) | Idiom and everyday false alarms | |---|---|---|---|---| | Held-out v2, written blind (167 vectors). The honest number. | 15/31 = 48.4% (32.0–65.2%) | 36/56 = 64.3% (51.2–75.5%) | 0/14 (0–21.5%) | 2/34 = 5.9% (1.6–19.1%) | | Dev (534 vectors; patterns written against it) | 172/172 | 245/245 | 0/28 | 0/134 |

The dev numbers show only that the patterns match the text they were written for. The held-out numbers are the ones to plan around, and the small sample gives them wide intervals.

What it misses. Mostly oblique language: preparation described without any crisis words, imagery of a method or of an outcome, and wishes expressed by their meaning rather than by recognisable wording. Keyword-and-context rules plateau on this kind of text; more rounds of patterns mostly fit the dev set.

What it does well.

  • Explicit first-person statements of suicidal thoughts, self-harm or feeling unsafe with oneself, including slang and texting spellings.
  • Context: negation, past and resolved distress, hypotheticals, quotations, third parties and idioms change a finding's level, and the finding is kept.
  • Explainability: every flag carries a rule id, matched span, offsets and modifiers, and the full pattern list is public.
  • Speed and offline use: deterministic, no network calls, no dependencies, about 25 ms for 50,000 characters.

Recommendation. Combine it with a second opinion (see Second opinion (opt-in); the @meddevkit/crisis-adapter-nope adapter is in development) and with a human review process. A none result does not mean the text is safe.

What it does

screenText looks for first-person language suggesting the writer may be in acute distress or at risk of harming themselves, and returns a level (none, concern or crisis), an immediacy flag, and every finding with its rule id, matched span, offsets and context modifiers (negated, past, resolved, hypothetical, quoted, idiom, third party). Context changes a finding's level; it never hides it. It also flags a person threatening the writer (threatened_by_other, at concern).

The library does not show crisis resources and does not decide what happens next. Escalation is your job, because the right resources depend on locale and setting. For example, a US consumer might surface 988 (Suicide & Crisis Lifeline) on crisis.

Properties

  • Offline. No network calls and no model inference. Deterministic.
  • No logging. It does not log, store or send text, and errors never contain text. Findings carry offsets and spans of at most 120 characters.
  • Zero runtime dependencies. Works in Node >= 18 and the browser.
  • Linear time. A 50,000-character input screens in about 25 ms on a laptop (under 45 ms for the worst adversarial input in the test suite); every repetition in the patterns is bounded, and the test suite includes adversarial inputs to guard against regex backtracking.
  • English only (v1). Risk to others is out of scope (v1); a threat against the writer is flagged as threatened_by_other.
  • Not HIPAA-anything. This package makes no HIPAA or other regulatory compliance claim.

Usage

import { screenText } from '@meddevkit/crisis-signals';

const r = screenText('I keep thinking about ending it. Maybe tonight.');
// r.level      -> 'crisis'
// r.immediacy  -> true
// r.findings   -> [
//   { ruleId: 'si_active.ending_it', category: 'suicidal_ideation_active',
//     baseLevel: 'crisis', level: 'crisis', startOffset: 7, endOffset: 31,
//     text: 'thinking about ending it',
//     modifiers: { negated: false, past: false, resolved: false,
//                  hypothetical: false, quoted: false, idiom: false },
//     subject: 'self' },
//   { ruleId: 'immediacy.timing', category: 'immediacy', baseLevel: 'none',
//     level: 'crisis', text: 'tonight', ... }
// ]
// r.patternsVersion -> '0.2.0'

screenText('This commute is killing me.').level;            // 'none' (idiom, finding kept)
screenText("I'm not going to hurt myself.").level;          // 'concern' (negated, never 'none')
screenText('My friend told me she wanted to die.').level;    // 'none' (subject 'other', finding kept)
screenText('Years ago I tried to kill myself. I am doing better now.').level; // 'concern' (history)

// Audits: report idioms without suppressing them.
screenText('This commute is killing me.', { suppressIdioms: false }).level; // 'concern'

Offsets are UTF-16 code units (JavaScript string indices). findings are in text order and include findings that context softened to none. screenText throws a TypeError (with no text in the message) if text is not a string or the options are malformed.

How levels are decided

| Level | Meaning | |---|---| | crisis | Present, first-person thoughts of suicide or self-harm, or feeling unsafe with oneself | | concern | Acute distress without self-harm language, or crisis language that context softened | | none | Nothing found, or only idioms, third parties, or clearly resolved past distress |

Categories: suicidal_ideation_passive, suicidal_ideation_active, self_harm, unsafe_with_self (base crisis); acute_distress, history, threatened_by_other (base concern); immediacy (flag only).

threatened_by_other flags a person threatening or hurting the writer ("my boyfriend said he'd kill me if I left, and he keeps a knife by the bed"). Its subject is always other, and the third-party rule does not lower it. The same shape is often figurative ("my mom is going to kill me when she sees my grades"), so idiom cues still suppress it, unless means, violence or fear appear in the same or the next sentence; then it stays concern.

One modifier sets each finding's level, by precedence:

  1. idiom → none ("dying to know", "this job is killing me").
  2. third party → none, subject: 'other' ("my friend said she wanted to die").
  3. past + resolved → acute distress none; crisis language concern with category history. resolved may come from the next sentence ("I was falling apart. I'm not there anymore.").
  4. negated, hypothetical, quoted, or past alone → concern (negated acute distress → none). If several apply, the higher level wins.

Context rules worth knowing:

  • Recent past is present. "Last night", "yesterday", "this week" and a present-tense verb in the same clause keep a finding current. past needs a distant anchor ("years ago", "back in college", "when I was nineteen", "used to", "last year").
  • A first-person conditional is not hypothetical. "If things don't get better I might end my life" stays crisis. Questions about others or about procedure ("how do people know if…", "would you have to report it?") are hypothetical.
  • Negation is scoped to the clause after the cue, and pseudo-negations ("not sure", "don't know", "can't stop") do not count. It does not reach into a later subordinate clause with its own first-person subject ("so she's not stuck after I'm gone"). A denial can also follow the crisis wording ("wanting to die isn't what this is", "hurting myself has never been part of it"). A denial of self-harm is still concern, because it is worth a human look.
  • Vague wording counts only in context. "I'm going to finish it", "how I'd do it", "for when they find me" or "this is it" is crisis language only when crisis language, lethal means, a plan, preparation or a high place appears in the same, previous or next sentence (see Context-gated patterns). Final-sounding statements that are alarming on their own ("this is the last time you'll hear from me", "nobody can talk me out of it") are concern.
  • immediacy (plan, means, timing, preparation) is set only when a non-negated crisis finding is present in the same text. It never lowers a level.
  • The text's level is the maximum over its findings, so the writer's own statement wins over an idiom or a third-party finding elsewhere.

Second opinion (opt-in)

screenTextWithSecondOpinion combines the offline screen with one model or API classifier that you configure. Text leaves the device only if you install and configure an adapter; screenText alone never sends anything, and this package makes no network calls itself.

import { screenTextWithSecondOpinion } from '@meddevkit/crisis-signals';

const r = await screenTextWithSecondOpinion(text, adapter, { timeoutMs: 10000 });
// r.level, r.immediacy   combined
// r.local                exactly what screenText(text) returns
// r.secondOpinion        { name, version, status: 'ok'|'error'|'timeout'|'invalid', result?, reason?, durationMs }
// r.degraded             true when the adapter did not return a valid result
// r.raisedBy             'local' | 'secondOpinion' | 'both' | 'none'

Combination rules:

  • The offline result is computed first and returned unchanged as local.
  • The final level is the maximum of the two: a model can raise a level, never lower it. raisedBy: 'secondOpinion' means the flag has no rule, span or modifiers behind it; route it to a person like any other flag.
  • An opinion about someone else (subject: 'other') raises at most to concern. Adapters for providers that cannot tell the subject raise at most to concern.
  • immediacy is the local flag, or the model's when the final level is crisis.
  • If the adapter throws, times out (default 10 s) or returns an invalid result, you get the local level with degraded: true. The outcome never contains input text or the provider's error message.
  • Aborting your signal rejects with an AbortError and aborts the adapter's request.

Adapters implement the CrisisSecondOpinion interface and are separate packages. The first, @meddevkit/crisis-adapter-nope (self-hosted nope-edge), is in development.

Patterns are public

The full pattern list, modifier cues and clause boundaries are open source, so you can see exactly what the screen does and does not match: data/crisis.patterns.json. The raw file is also shipped in the package as @meddevkit/crisis-signals/patterns.json for vendoring into other runtimes.

Pattern file format

{
  "version": "0.2.0",
  "fragments": [  // named regex pieces, referenced as {{ID}}
    { "id": "SELF", "pattern": "\\b(?:i|im|i['’]m|i\\s+am|ive|i['’]ve|…)\\b", "note": "…", "sources": [], "relation": "informed-by" }
  ],
  "patterns": [   // detection patterns
    { "id": "si_active.ending_it", "category": "suicidal_ideation_active", "level": "crisis",
      "pattern": "…", "flags": "i", "note": "…", "sources": ["srl-v1.0", "nimh-asq"], "relation": "informed-by" },
    { "id": "si_active.vague_act_in_context", "category": "suicidal_ideation_active", "level": "crisis",
      "pattern": "…", "flags": "i", "note": "…", "sources": ["srl-v1.0"], "relation": "informed-by",
      "context": {  // context-gated: a match counts only with this context nearby
        "scope": "adjacent",
        "categories": ["suicidal_ideation_passive", "suicidal_ideation_active", "self_harm", "unsafe_with_self", "history"],
        "rules": ["immediacy.plan", "immediacy.means", "immediacy.preparation", "immediacy.location"],
        "pattern": "{{DROP}}|{{LETHAL}}|…", "flags": "i" } }
  ],
  "modifiers": [  // context cues
    { "id": "negated.not", "modifier": "negated", "scope": "forward", "maxGap": 50, "pattern": "…", … },
    { "id": "past.block_recent", "modifier": "past", "scope": "clause", "effect": "block", "pattern": "…", … }
  ],
  "boundaries": [ // clause boundaries for modifier scope
    { "id": "boundary.contrast", "strength": "strong", "pattern": "…", … }
  ]
}

Fragments. A pattern, cue, boundary or later fragment may reference a fragment as {{ID}}; the loader replaces each reference with (?:<fragment>) before compiling, and a fragment may reference only fragments listed before it. Fragments hold the shared frames and vocabulary slots: a first-person subject (SELF), bounded fillers (FILL, hedges, intensifiers, auxiliaries and negations), a clause start for subjectless texting (START), imagery and urge frames (IMAGINE), and slots such as IDEATE, INTEND, CANT, PILLS, MEANS, LETHAL, DROP (high places and tracks), BODY, SH_MARK, TIMING, PERMANENT and NUISANCE (everyday annoyances). Another runtime that vendors the file must apply the same substitution (one regex replace).

Finding span. When a capturing group takes part in a match, the first such group is the finding's span and the rest of the match is required context. A frame such as "I've never" can then precede the core "cut" without being part of the span, so a negation in the frame scopes over the finding as a cue. An empty group is a veto: the match consumes its text and yields no finding (for example "reading about self-harm"). Without a participating group, the whole match is the span.

Cue scope values: forward (the finding starts within maxGap characters after the cue, in the same clause), sentence (same sentence, unless a strong boundary lies between), sentence-or-next (same or next sentence), within (the finding lies inside the cue's match, e.g. a quotation), overlap (the cue overlaps the finding, e.g. an idiom). Cues with "effect": "block" cancel a modifier: clause and sentence blocks cancel it for findings in that clause or segment, a sentence-or-next block for findings in the same or the previous sentence, and a between block cancels a forward cue when it lies after that cue and before the end of the finding. A cue with "categories": [...] acts only on findings of those categories (for example the literal-threat evidence that keeps a threatened_by_other finding from being read as an idiom).

Context-gated patterns

A detection pattern with a context object is context-gated. It is for vague or anaphoric wording that is crisis language only in context: "I've thought about it a lot and I'm going to finish it. I'm done being alive", but not "I'm going to finish it this weekend, the deck ships Monday". A gated match yields a finding only when, within scope (sentence: the same sentence; adjacent: the same sentence, the one before it, or the one after it), at least one of these is present and does not overlap the match itself:

  • a finding of an ungated detection pattern whose category is listed in categories, or of an ungated detection or immediacy pattern whose id is listed in rules, that is neither negated nor suppressed as an idiom (past, quoted or third-party context still counts: "I tried to end my life in 2019. This time I'm going to finish what I started"); or
  • a match of pattern (with flags; fragments allowed, no capturing groups), for vocabulary that is not a finding of its own ("I keep thinking about how I'd do it. The bridge, mostly").

Context comes only from ungated patterns, so gates never depend on each other and the order of evaluation does not matter. Passing gated matches then compete with the ungated matches for overlaps as usual (the higher base level wins), and modifiers apply to them like any other finding. A gate never lowers anything: without its context, a gated match is simply not a finding, and an ungated pattern can cover the same wording at a lower level (distress.vague_finality). Another runtime implements a gate in a few lines: collect the ungated findings and their modifiers, then keep each gated match whose sentence window holds qualifying context.

Every regex stays within a portable subset (no named groups, \p{…}, inline flags, atomic or possessive groups, variable-width lookbehind, \Z/\z or $), so ECMAScript (u flag) and Python re give the same matches, before and after fragment expansion. Patterns run over the whole text; sentence splitting is structural and lives in code.

Metrics

pnpm --filter @meddevkit/crisis-signals build
pnpm --filter @meddevkit/crisis-signals metrics        # SPEC §8 metrics on data/vectors.jsonl
node packages/crisis-signals/scripts/metrics.mjs <file.jsonl>

Published metrics: see Measured limits. Each release reports the dev set and every held-out set separately (SPEC §8).

Design and evaluation plan: SPEC.md. Third-party attributions: NOTICE.

License

MIT. Some patterns are informed by third-party sources; see NOTICE.