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

@jonaskahn/specmine

v0.3.1

Published

Mine product specifications (JSON) from text content or PDF files using an LLM.

Readme

███████╗ ██████╗  ███████╗  ██████╗ ███╗   ███╗ ██╗ ███╗   ██╗ ███████╗
██╔════╝ ██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ████║ ██║ ████╗  ██║ ██╔════╝
███████╗ ██████╔╝ █████╗   ██║      ██╔████╔██║ ██║ ██╔██╗ ██║ █████╗  
╚════██║ ██╔═══╝  ██╔══╝   ██║      ██║╚██╔╝██║ ██║ ██║╚██╗██║ ██╔══╝  
███████║ ██║      ███████╗ ╚██████╗ ██║ ╚═╝ ██║ ██║ ██║ ╚████║ ███████╗
╚══════╝ ╚═╝      ╚══════╝  ╚═════╝ ╚═╝     ╚═╝ ╚═╝ ╚═╝  ╚═══╝ ╚══════╝

Mine product specifications (JSON) from text, PDFs, or web pages — with an LLM. A library. No server, no CLI. Call a function, get a spec.

Install

npm install @jonaskahn/specmine

Requires Node.js 18+ (ESM).

Usage

Quick start — plain text

import { extract } from '@jonaskahn/specmine';

const spec = await extract('The Acme Kettle holds 1.5L, £49.');
// { "Capacity": "1.5 L", "Price": "£49" }

Input kinds

extract() accepts three input kinds:

// 1. A string — plain text or HTML (converted to Markdown automatically)
const spec = await extract('The Acme Kettle holds 1.5L, £49.');

// 2. A Blob / File — PDF or text. Works in browsers and Node
const spec = await extract(fileOrBlob);

// 3. A URL — https/http URLs are fetched, file:// URLs are read from disk
const spec = await extract(new URL('https://example.com/kettle.pdf'));

Reading a file from disk

Wrap a file in a Blob. The content type is optional — %PDF magic bytes are sniffed, so text blobs work too:

import { readFile } from 'node:fs/promises';
import { extract } from '@jonaskahn/specmine';

// PDF
const pdf = new Blob([await readFile('./kettle.pdf')], { type: 'application/pdf' });
const spec = await extract(pdf);

// Plain text file — type omitted
const txt = new Blob([await readFile('./kettle.txt')]);
const spec = await extract(txt);

Fetching a remote PDF

const spec = await extract(new URL('https://example.com/specs/kettle.pdf'));
// { "Weight": "1.5 kg", "Dimensions": "239 x 44 x 190 mm" }

Remote HTML pages are converted to Markdown before extraction, so table- and tag-heavy product pages still produce clean specs:

const spec = await extract(new URL('https://example.com/products/kettle'));

HTML input

HTML strings and blobs are detected and converted to Markdown automatically:

const html = '<html><body><h1>Acme Kettle</h1><table><tr><td>Weight</td><td>1.5 kg</td></tr></table></body></html>';
const spec = await extract(html);
// { "Weight": "1.5 kg" }

Output shape

Values are always strings. Output is nested (group → key → value) when the content has labeled sections:

{
  "Hardware": { "Power Supply": "12 V DC, external power supply" },
  "Dimensions": "239 x 44 x 190 mm"
}

Pass flattened: true to reduce output to innermost leaf pairs:

const spec = await extract(pdfBlob, { flattened: true });
// { "Power Supply": "12 V DC, external power supply", "Dimensions": "239 x 44 x 190 mm" }

With inheritance: true the parent keys are kept as Parent · Child prefixes:

const spec = await extract(pdfBlob, { flattened: true, inheritance: true });
// { "Hardware · Power Supply": "12 V DC, external power supply" }

Language

Keys and values are written in the language detected from the input. Force a language with lang:

const spec = await extract('Der Acme Wasserkocher fasst 1,5 Liter.', { lang: 'de' });
// { "Fassungsvermögen": "1,5 Liter" }

Providers

The default provider is OpenAI-compatible. Switch with provider:

const spec = await extract('The Acme Kettle holds 1.5L, £49.', {
  provider: 'anthropic',
  model: 'haiku-4.5',
});

Zero config via env: SPECMINE_LLM_API_KEY picks the key for both providers; SPECMINE_LLM_API_HOST and SPECMINE_LLM_MODEL override host and model:

export SPECMINE_LLM_API_KEY=sk-...
export SPECMINE_LLM_MODEL=gpt-5.6-luna   # optional
export SPECMINE_LLM_API_HOST=https://api.openai.com/v1   # optional
const spec = await extract('The Acme Kettle holds 1.5L, £49.'); // no options needed

Tags

Pass tags: true to also get a short list of descriptive labels:

const result = await extract('The Acme Kettle holds 1.5L, £49.', { tags: true });
// {
//   "spec": { "Capacity": "1.5 L", "Price": "£49" },
//   "tags": ["kettle", "stainless-steel"]
// }

Or tags only — a dedicated prompt and schema, no spec pairs:

import { extractTags } from '@jonaskahn/specmine';

const tags = await extractTags('The Acme Kettle holds 1.5L, £49.');
// ["kettle", "stainless-steel"]

Tags are normalized in code: lowercased, trimmed, deduplicated, max 8.

Timeouts and cancellation

Abort a hanging LLM call after N milliseconds, or wire in your own signal:

import { extract } from '@jonaskahn/specmine';

// Abort after 10 seconds
const spec = await extract(text, { timeoutMs: 10_000 });

// External abort
const controller = new AbortController();
const spec = await extract(text, { signal: controller.signal });
controller.abort(); // rejects with TIMEOUT

Errors

Every failure rejects with an ExtractionError carrying a code:

EMPTY_INPUT · UNSUPPORTED_INPUT · LLM_ERROR · INVALID_OUTPUT · TIMEOUT

import { extract, isExtractionError } from '@jonaskahn/specmine';

try {
  await extract(input);
} catch (error) {
  if (isExtractionError(error)) {
    console.error(error.code, error.message);
    // e.g. "EMPTY_INPUT Input is empty"
    //      "LLM_ERROR LLM host responded 401"
    //      "TIMEOUT LLM call aborted"
  }
}

Logging

Internal pipeline logs use pino or winston — whichever the consuming app has installed (both are optional peers; with neither, logging is a no-op). Set the level with SPECMINE_LOG_LEVEL (or the global LOG_LEVEL), and pick the driver with SPECMINE_LOG_DRIVER (pino | winston | auto):

export SPECMINE_LOG_LEVEL=info
export SPECMINE_LOG_DRIVER=auto   # auto-detects pino, then winston

Levels: silent · fatal · error · warn · info · debug · trace. API keys and prompt content are never logged — only lengths, counts, statuses, and durations.

Advanced: custom pipeline

createExtractor() builds an extractor with swappable pieces (reader, llm, validator — all optional). Wrap the LLM in createResilientProvider() for retries and a circuit breaker:

import {
  createExtractor,
  createOpenAiProvider,
  createResilientProvider,
} from '@jonaskahn/specmine';

const llm = createResilientProvider(createOpenAiProvider(), {
  retry: { maxRetries: 2, backoffMs: 200 },
  circuitBreaker: { failureThreshold: 5, cooldownMs: 30_000 },
});

const extractor = createExtractor({ llm });

const spec = await extractor.extract('The Acme Kettle holds 1.5L, £49.');

Retries only fire on LLM_ERROR and TIMEOUT. Resilient defaults: maxRetries: 2, backoffMs: 200, failureThreshold: 5, cooldownMs: 30_000.

Development

npm install
npm run typecheck   # tsc --noEmit
npm run lint        # eslint
npm run format      # prettier --write
npm test            # node:test + tsx
npm run coverage    # 100% coverage gate
npm run build       # emit dist/

Runnable examples live in examples/ — npx tsx examples/text.ts, examples/pdf.ts, examples/tags.ts, examples/flattened.ts, examples/language.ts, examples/html.ts, examples/anthropic.ts, examples/resilient.ts. See CONTRIBUTING.md.

License

MIT