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

jsonloupe

v1.5.0

Published

A loupe for large JSON — lossless, local-first viewer, differ, and query tool. Int64-exact, worker-powered, zstd-aware.

Readme

jsonloupe is a browser-based workbench for inspecting, diffing, editing, and querying JSON documents that are too big or too precise for ordinary viewers — and for turning them into spreadsheets: nested JSON goes in, linked tables come out, as one .xlsx or a zip of CSVs. Everything runs in your browser: documents are parsed in a web worker, stored in IndexedDB, and never uploaded anywhere. No backend, no account, no telemetry.

Run it

Use the hosted app at jsonloupe.dev — or run the identical bundle from your own machine, fully offline:

npx jsonloupe

That starts a loopback-only static server (127.0.0.1:5199) serving the prebuilt app from the package — no runtime dependencies, no network calls, ~60 lines of auditable server in bin/jsonloupe.mjs. --port <n> and --no-open do what they say.

Why another JSON viewer?

  • Lossless numbers. Int64 IDs beyond 2^53 (snowflake/order/entity IDs) and precise decimals survive paste → view → edit → copy → download with their exact digits. Nothing is ever coerced to a float.
  • Built for huge documents. The tree is virtualized and expansion is chunked ([0 … 9999] range rows), so opening a five-million-element array is instant. Parsing, diffing, search, and compression all run off the UI thread.
  • Compressed payloads are first-class. Paste or drop raw .zst, Base64-Zstd (standard or URL-safe), or PostgreSQL \x… bytea — it's detected, decoded in the worker, and opened as a document with a transformation trace. An encoder and a transport-size inspector (exact UTF-8 / Zstd / Base64 / envelope bytes against editable budgets) round-trip the other way.
  • Nested JSON becomes a spreadsheet, on your machine. Repeating arrays and object maps become their own linked tables — orders and order_items joined by order_id, never items/0/sku columns. You see real rows before anything is written, and the mapping saves as a small file that produces the same columns from next month's file, with no model involved.
  • Semantic diff. Side-by-side compare with identity-based array alignment (match by id, composite keys, or auto-detected), so reordered arrays don't drown you in false changes. Ignore noisy fields by key or path prefix.
  • A real query language. A JSONPath subset with predicates and aggregation pipes ($.tasks[?(@.status == 'FAILED')] | group(@.failureReason)), executed locally with live preview as you type. Numeric literals, predicates, sums, averages, minima, and maxima remain exact for int64 and precise decimals.

Quick start

npm install
npm run dev        # http://localhost:5199

Paste JSON on the landing page — it parses instantly and is saved to local memory (Recents sidebar: reopen, pin, delete). Malformed input (trailing commas, single quotes, Python literals, truncated log extracts) is auto-repaired and flagged, with the original bytes preserved.

Feature tour

  • Tree / Code / Split views — virtualized tree, editable CodeMirror code view (fold, search, apply-with-⌘S), or both side by side with click-to-line sync. Inline-edit primitives in the tree; full undo/redo across edits.
  • Search & filter/ to search (literal or /regex/i), filter mode prunes the tree to matching branches and restores your expansion state after.
  • Path & identity — breadcrumb with one-click copy as JSONPath, JSON Pointer, or JS accessor; "same value" jumps to every node holding a value (schema-free correlation-ID tracing).
  • Value lenses — epoch → local date, lat/lng, uuid/url/base64 flags, byte weight on collapsed containers so the heavy node is findable at a glance.
  • Tables & CSV — any array gets a sortable table view; export exact-digit CSV (RFC 4180) of tables or query results.
  • Functions — a library of your own scripts — write JavaScript for the questions no query language should have to answer, keep it under a name, and press it over tomorrow's file. Scripts run in an ephemeral sandbox worker with fetch, storage and the network removed, and the result opens as a document of its own. A function can keep JavaScript-number behavior or receive unsafe numeric literals as exact digit strings; safe numbers stay numbers in either mode. Tick several and one press answers all of them as a single report, honoring each function's number contract. (below.)
  • JSON → Excel / CSV converter — rename and reorder columns, choose source fields, add constants, set date and coordinate handling, take the column names from a target CSV header, and save or share the mapping for the next file. (below.)
  • File handles — drop .json/.jsonl/.zst files; reload re-reads from disk and shows what changed since the version you were looking at.
  • Query — run JSON queries locally, save useful ones, or turn a result into a reusable pass/fail Check. Optional English → query mode translates a question using only the document's shape (field names/types — never values), then stops so you can review the generated query before running it locally. Bring your own connection: authorize with OpenRouter without pasting a key, or use the advanced manual-key option — paste a key, or load it from a local file (a raw key or a .env line, read in the page and never uploaded). Until you explicitly connect, the feature is inert. A disclosure panel records exactly what was sent. See SECURITY.md.

Browser query cookbook

Open a document, press query, leave JSON query selected, and paste a query. It runs directly in this tab: no API key and nothing sent to a model.

Given this small document:

{"tasks":[
  {"id":9007199254740992,"status":"FAILED"},
  {"id":9007199254740993,"status":"READY"}
]}
  • Filter to the failed rows:

    $.tasks[?(@.status == 'FAILED')]

    Result: one match, $.tasks[0].

  • Count rows by status:

    $.tasks[*] | group(@.status)

    Result: FAILED 1, READY 1.

  • Compare an integer beyond JavaScript's safe range without rounding it:

    $.tasks[?(@.id > 9007199254740992)] | count

    Result: 1 — only the exact 9007199254740993 id is greater.

Nested JSON to linked tables

If a spreadsheet is the only thing you came for, jsonloupe.dev/json-to-excel.html is that job on a page of its own: paste, convert, done.

Open a document and press the table glyph in the toolbar's tools group (convert; every glyph there names itself on hover). Every repeating array becomes its own table, joined back to its parent by the id that was already in the data — so this:

{ "orders": [
    { "id": 7, "cust": "ACME",
      "items": [ { "sku": "A", "qty": 2 }, { "sku": "B", "qty": 1 } ] } ] }

comes out as two sheets rather than one row full of items/0/sku columns:

orders                    order_items
id  cust                  order_id  sku  qty
 7  ACME                         7  A      2
                                 7  B      1

jsonloupe finds those tables for you but never runs the guess blindly: the mapping and a preview of real rows stay on screen while you rename columns, choose which fields to keep, and say how dates and coordinates should be read. What you approve is saved as a small mapping file — keep it in this browser, export it, or use it from the command line:

jsonloupe inspect payload.json          # what tables are in here?
jsonloupe draft   payload.json -o payload.spec.json   # a first mapping, to read
jsonloupe convert payload.json --spec payload.spec.json -o payload.xlsx

The same mapping produces the same columns through the browser, the CLI, and the MCP server, with no model in the loop — that is the point of freezing it. What lands in the cells is what was in the document: dates arrive as real dates you can sort and subtract, numbers and coordinates as numbers, and text that only looks numeric stays text, so "1.10" keeps its trailing zero and an int64 id keeps every digit instead of being rounded. Excel's limits and values a column cannot read are reported or refused, never quietly written wrong.

Playbooks: your functions and checks as a file

The document changes daily; the handful of questions you ask of it does not. So functions opens on your library rather than an empty editor — named functions, newest first — and pressing one runs it over whatever is open. Tick several and one press answers all of them, as a single object keyed by function name: today's report, which downloads and reopens as a document like any other.

A function learns what it reads. The first time one runs it is handed the document through a recording proxy, and the paths it actually touched are kept with it. That is what lets the panel say

this reads orders, orders[].deliveredInHours — this document has none of that

before you press run, instead of leaving you to read an empty result as "none today". It is always a remark and never a gate: the reading knows only the branch that last run took, so the run button still works.

A playbook is that reusable work as a file — the questions and expectations, never the data:

{
  "playbookVersion": 3,
  "name": "carrier dumps",
  "functions": [
    {
      "name": "slow orders",
      "script": "data.orders.filter(o => o.deliveredInHours > 48)",
      "reads": ["orders", "orders[].deliveredInHours"],
      "numberMode": "exact-text"
    }
  ],
  "checks": [
    {
      "name": "no failed orders",
      "query": "$.orders[?(@.status == 'FAILED')]",
      "expectation": { "type": "no-matches" }
    }
  ]
}

Export writes one; dropping one on the window installs it. Import merges and never replaces — a name you already use keeps yours and the incoming one lands as slow orders 2, because a duplicate is one × away and an overwrite is not. Unknown fields are refused by name rather than dropped, so a file from a newer jsonloupe cannot import as a subset of itself and look like it worked.

Scripts run in an ephemeral worker with fetch, XMLHttpRequest, WebSockets, IndexedDB and importScripts removed before any user code, terminated on its result or after ten seconds — a pasted script cannot reach the network or the documents you have opened. JavaScript number mode preserves the behavior of existing functions and says when a document contains values it will round. exact text mode instead hands those unsafe literals to the function as their original digit strings ("9007199254740993"); ordinary values such as 3 and 1.5 remain numbers, so normal counting and arithmetic still work. The mode is saved and exported with the function rather than left as a browser preference. Version 1 and 2 playbooks still import; version 1 functions keep their original JavaScript-number mode.

Use with AI agents

The same engine runs as an MCP server, so an agent can work on a document that would never fit in its context:

claude mcp add jsonloupe -- npx -y -p jsonloupe jsonloupe-mcp

If you have jq and plain JSON, use jq. This is for the cases jq is awkward about: compressed payloads (.zst, Base64-Zstd, PostgreSQL \x… bytea) that have to be decoded first, int64 and decimal digits that must survive the whole pipeline exactly, identity-keyed semantic diff between two documents, and MCP hosts that have no shell to run jq in.

Eleven tools: load_doc, get_schema, run_query, profile, sample, diff_docs, export_csv, export_result, plus inspect, draft_spec, and convert for the spreadsheet path above. For one question, pass filePath straight to a tool; it opens the document and returns a reusable docId in the same call. For a sequence of questions, call load_doc once and reuse that docId. Query details default to 10 rows (use limit=0 for summary only, or offset + limit to page), and text and structured MCP results are both bounded. A typical run over a 37 MB routing payload:

run_query filePath=payload.json "$.tasks[?(@.status == 'FAILED')] | count"
                                  → docId: d1 · count: 17500
profile d1 "$.tasks[*]"          → auto-discovered field coverage, null/missing/types,
                                    exact sums/stats, lengths, distincts and top values
run_query d1 "$.tasks[?(@.status == 'FAILED')] | group(@.region, @.failureReason)"
                                  → composite breakdown, exact complete counts
run_query d1 "$.tasks[*] | top(@.delayMinutes, @.id, @.status)" limit=5
                                  → only the five highest rows; the full array never enters context
sample d1 "$.tasks[0]"           → the whole element, id 9007199254740993 intact
export_result d1 "…| pluck(@.id, @.failureReason)" format=csv out=failed.csv
                                  → complete, atomic, rows + UTF-8 bytes only
inspect d1                       → candidate tables, fields, types, deterministic suggestions
draft_spec d1 outPath=tasks.spec.json
                                  → reviewable mapping on disk
convert d1 specPath=tasks.spec.json outPath=tasks.xlsx
                                  → workbook path + row/skipped/warning counts, never its rows

The common schema → profile → query loop is deliberately cheaper than loading JSON or writing a one-off Python script: counts and aggregates stream over every match without materializing the result list; present, missing, and isNull keep false/null/absent distinct; composite grouping and bounded top/bottom replace local sort scripts; and profiles inspect multiple or automatically discovered fields in one scan. Complete CSV/JSONL exports stream to a temporary file and publish atomically. Existing paths are refused unless the agent passes overwrite=true explicitly.

The server makes no network calls and accesses only explicitly supplied input/output paths. A bad query returns the grammar and a suggestion rather than an empty result. The reproducible agent-choice evaluation measures whether an agent selects these operations when Python is also available.

If an agent knows the MCP is installed but still writes one-off scripts, put this routing rule in its project instructions (for example AGENTS.md or CLAUDE.md):

For small plain JSON, use jq when it is already the shortest safe option. Otherwise prefer the JsonLoupe MCP over ad-hoc Python whenever it supports the operation; use custom shell code only when JsonLoupe cannot answer it.

Privacy & security

Documents never leave your machine. The complete network-call inventory (four fetch calls, all in opt-in model connection or English query translation), the shape-only LLM contract, and the XSS posture are documented in SECURITY.md. The corresponding threat model, trust boundaries, and assurance argument are in SECURITY-ASSURANCE.md.

Deploying

jsonloupe builds to a fully static site:

npm run build      # dist/

Serve dist/ from any static host (GitHub Pages, Cloudflare Pages, Netlify…). There is nothing to configure server-side; the local /__api-key convenience endpoint (dev mode, or npx jsonloupe --key-file) simply doesn't exist in a static deploy, and English translation activates only when a visitor adds their own key.

Development

npm test           # vitest — query engine, worker, MCP dispatch, NL translation suites
npm run lint:security  # FLOSS security analysis + checked TypeScript style
npm run coverage   # same suite with enforced 90% statement / 80% branch floors
npm run build      # tsc --noEmit + vite build + the MCP bundle
npm run test:a11y  # real Chromium axe + keyboard/focus checks (run after build)
npm run check:site-headers # verify the live OpenSSF hardening headers
npm run check:reproducible-build  # clean locked installs must produce identical bytes
npm run smoke:mcp  # drives the built MCP server over real stdio against a 37 MB fixture
npm run eval:agent -- --help  # black-box MCP-vs-Python agent-choice benchmark

Start with CONTRIBUTING.md. Architecture and internals are documented in ARCHITECTURE.md and SPEC.md; the converter has its own SPEC-converter.md, including what it deliberately refuses to do. Project decisions and the next twelve months are in GOVERNANCE.md and ROADMAP.md. Official publishing and consumer verification are documented in RELEASING.md.

License

MIT. Bundled third-party packages are listed in THIRD_PARTY_LICENSES.md.

Software is provided "as is", without warranty of any kind — see the license text. If you enable the Ask feature, your use of the configured LLM provider is governed by that provider's terms.