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

keptpdf

v1.19.0

Published

Bulk PDF redaction that runs entirely on your own machine. Finds names, dates, account numbers and more, burns them into the page, and writes a verifiable certificate. No upload, no account, no network.

Readme

keptpdf

Bulk PDF redaction that runs entirely on your own machine.

It finds names, dates of birth, account and case numbers, addresses, emails, phone numbers and more, burns them out of the page, and writes a certificate for each file. Scanned pages are OCR'd first. Nothing is uploaded, there is no account, and it makes no network calls of any kind, including for its license. The one exception is opt-in and yours to invoke: keptpdf update asks keptpdf.com for the newest version number (sending only the version you have and your operating system), then installs it from npm, and only when you run it.

npx keptpdf redact ./intake -o ./produced -r

Install

npm install -g keptpdf

Node 18.17 or newer, on Windows, macOS or Linux.

Two dependencies, 27 packages, no compiler and no build step. npm audit reports no known vulnerabilities. The PDF and OCR engines ship inside the package rather than being pulled from the registry, so what you install is what was tested.

Redaction is real, and it is checked

Every output is re-opened and re-read before it is written. If any of the text that was supposed to be gone can still be extracted, the file is not written at all. The certificate beside it (.cert.txt and .cert.json, plus a branded .cert.pdf on licensed runs) records the counts, the SHA-256 of the input and output, and that verification result. On licensed runs the certificate also names the organization the license is signed to, on the document and on its online verify page, so every deliverable shows whose license produced it.

A scanned page inside an otherwise typed file is read by OCR like any other. A page that still cannot be read (with --no-ocr, or when recognition fails) is never passed off as clean: the line for that file ends with N pages could not be read in yellow, the summary counts those files apart from the clean ones, and the certificate states that the page numbers were not checked for personal information. With --json, every file carries a top-level unreadPages array of 1-based page numbers (empty when every page was read), and the summary carries withUnreadPages.

Redacted pages are flattened to images, which makes them safe but not searchable. --searchable adds a text layer back: it is read off the finished redacted page images by OCR, after the zero-character check above, so it cannot contain what was removed. The certificate records the layer.

No flags needed: the menu

Typing keptpdf alone in a terminal opens an interactive menu. Every place a path is needed opens a file browser: arrow keys move, Enter opens a folder or picks a file, left arrow goes up, and typing a few letters finds a file by name. No path is ever typed unless you want to (a "Type a path" row, with Tab completion, is always there). It walks through everything the flags do: redact a file or folder, review before burning, split, Bates-number, enter a license key, and set up watched folders. Redact and watch runs can stamp an exemption code in each box: the 14 standard FOIA codes are offered as a pick list with plain-English hints, and any other code can be typed. None is ever preselected; which code applies is your legal call. Each run prints the equivalent command line, so the menu also teaches the flags as you use it.

Watched folders are the server story. Save one in the menu (which folder, where output goes, how thorough) and the menu can write a ready-to-install service file for the machine it is running on: a systemd unit on Linux, a Task Scheduler command on Windows, a launchd plist on macOS. Install it with the printed one-liner and every PDF dropped in that folder comes out redacted, from boot, with no one logged in. A saved setup can also be run by hand with keptpdf watch --name <its-name>.

Scripts see no change: keptpdf with no arguments in a pipe or a cron job still prints the help and exits 2, exactly as before.

The commands

| Command | What it does | | --- | --- | | keptpdf | Open the interactive menu (terminal only) | | keptpdf redact <input...> -o <dir> | Redact files, directories or globs | | keptpdf review <input...> -o <file.csv> | Write one row per candidate for a person to approve | | keptpdf watch <in-dir> -o <out-dir> | Redact each new file dropped in a folder | | keptpdf watch --name <saved> | Run a watch setup saved from the menu | | keptpdf split <input...> -o <dir> | Copy pages out: --pages "1,3-5" or --every <n>, or find the documents inside with --auto (--read-scans for scanned paper) | | keptpdf bates <input...> -o <dir> | Stamp continuous Bates numbers across PDFs (--combined for one file; --restart-each to start every file at 1; --prefix / --designation / --restamp) | | keptpdf categories | List the category ids for --only / --skip | | keptpdf license | Show the license status | | keptpdf update | Install the newest version. --check reports it and installs nothing |

keptpdf --help documents every option.

Word, Excel and PowerPoint inputs

Office files (.doc .docx .xls .xlsx .ppt .pptx .odt .ods .odp .rtf) are accepted on redact, review and watch: as file arguments, in directories, in globs and in watch folders. bates is PDF-only in this version. Each one is converted to a PDF on your machine first, then redacted like any other input. The conversion uses your installed copy of LibreOffice (nothing bundled, nothing uploaded); if it is missing, the run says so up front. Spreadsheet columns are auto-widened before conversion so text a narrow column would clip (the tail of an SSN) cannot be hidden from detection.

Split a packet into the documents inside it

One PDF that is really eleven documents is the file nobody meant to make: a scanned intake folder, a discovery production, a court filing with its exhibits bound in. --auto reads every page, works out where each document starts, and writes one PDF per document named after that document's own title.

keptpdf split ./production -o ./documents --auto --dry-run   # look first
keptpdf split ./production -o ./documents --auto             # then write

A dry run prints the documents, their page ranges and the reason each break was called, and writes nothing. Add --verbose and it also prints the breaks it nearly called, which is the difference between "raise the sensitivity" and "this really is one document".

+  production.pdf   4 documents in 87 pages                      6.2s
     1. pages 1-12    Master Services Agreement    -> production-01-master-services-agreement.pdf
     2. pages 13-40   Exhibit A Statement of Work  -> production-02-exhibit-a-statement-of-work.pdf
     3. pages 41-79   Pages 41-79                  -> production-03.pdf
     4. pages 80-87   Letter to Dana Reed          -> production-04-letter-to-dana-reed.pdf

A document that starts on a scanned page still gets a real name: that one page is read so the title can come off it. Only the first page of each document, so a forty-page scanned block costs two pages of reading, not forty. --no-ocr turns that off and those documents keep their page range as a name. Nothing is ever given an invented one.

--auto-sensitivity low | medium | high decides how hard it looks. Medium is the default. Lower finds fewer, clearer breaks; higher finds most of them and some wrong ones. A wrong break costs you one merge, a missed one has to be found first, which is why the default leans toward finding them.

Scanned pages are not skipped. A page with no text layer is fingerprinted by the shape of its ink (how dark the paper is, where the margins fall, how far apart the lines sit), so a block of scans still gets split where the documents change. Only those pages are rendered, so a born-digital file pays nothing for it.

When the file is mostly scans, --read-scans goes further: it reads every page that has no text before looking for the breaks, so the detector has the words instead of just the ink.

keptpdf split ./backfile -o ./documents --auto --read-scans --dry-run

It usually finds more documents, and it costs minutes per file rather than seconds, so it is asked for rather than assumed. It buys nothing on a PDF that already has text. A run that could have used it says so at the end.

Bookmark it instead of cutting it

Cutting is one way to spend that answer. The other is to cut nothing and write ONE PDF whose sidebar lists the documents. A resident handed a 300-page scanned property folder wants to jump to the building permit, not to receive forty loose files.

keptpdf split ./folders -o ./out --auto --output-shape bookmark   # one file, bookmarked
keptpdf split ./folders -o ./out --auto --output-shape both       # and the separate files too

--output-shape files is the default and is what has always happened, so a script written before this flag existed still means what it meant. bookmark writes <name>-bookmarked.pdf: every page of the input, in order, nothing removed and nothing reordered, with a top-level bookmark at the first page of each document carrying that document's own title.

+  Property-Folder.pdf   2 documents in 15 pages                      1.8s
     1. pages 1-10    Master Services Agreement       page 1
     2. pages 11-15   Exhibit A Statement of Work     page 11
     wrote Property-Folder-bookmarked.pdf  2 bookmarks, one file, nothing cut

No table of contents page is inserted. Adding one would push every page down by one, and the page numbers you already have on the packet would stop matching the file. --dry-run names the file it would write and counts the bookmarks; --verbose prints each entry and the page it jumps to.

Sort them into piles: --by-type

--by-type adds the third question, the one a records office asks first: what kind of paper is this. Each document is read and dropped into one of thirteen piles (permit, application, inspection, certificate, enforcement, decision, plans, survey, recorded, property-card, submittal, correspondence, and other for anything it will not guess at). It works with either shape and changes only where things land: with files each document is written into a folder named for its pile, and with bookmark the sidebar gets a heading per pile with that pile's documents under it. --review shows the pile beside each document and t changes it before anything is written.

keptpdf split ./folder -o ./out --auto --by-type
keptpdf split ./folder -o ./out --auto --output-shape bookmark --by-type
out/
  permit/       Property-Folder-01-building-permit.pdf
  survey/       Property-Folder-04-certificate-of-survey.pdf
  other/        Property-Folder-07-pages-22-24.pdf

The folder is the pile's id, lower-case and ASCII, so it is a safe folder name everywhere and a stable key for a script. --json reports pile and pileConfidence for every document whether or not you asked for --by-type.

The index columns

Every document also comes back with the four things a records clerk types into the index so a file is findable years later, read off its own first page:

| field | what it is | | --- | --- | | vendor | the outside organization the document is from or names as the other party | | date | the document's own date, as YYYY-MM-DD | | amount | the sum it commits, as printed ($322,520.00) | | reference | the number it carries: a resolution, permit, quote or invoice number |

A field is an empty string when it was not found, and that is a real answer rather than a failure. Nothing is inferred: each value is copied from a caption the paper actually prints, so a resolution numbered by hand at the meeting comes back with an empty reference instead of an invented one. The listing prints the fields it found under each document's name, --review shows them for the document under the pointer, and --json carries all four on every document.

Nothing can be read off a page with no text layer, so a scanned backfile needs --read-scans for these to fill in, the same as the names do. Recognition is imperfect on a logo and reliable on printed captions, which is why the vendor is the field a scan most often leaves blank.

This is the same detector the KeptPDF website runs, and the same bookmark writer, so the two cannot propose different splits for the same file. Like everything else here, it reads your file on your machine and sends nothing anywhere.

Bates numbers on a production

Stamp a continuous PREFIX+number+SUFFIX identifier on every page of one or more PDFs, the same engine the website runs. PDF files only: Office inputs are not accepted here.

keptpdf bates ./production -o ./stamped --prefix SMITH
keptpdf bates ./production -o ./stamped --prefix SMITH --combined --designation CONFIDENTIAL
keptpdf bates ./production -o ./stamped --prefix SMITH --restart-each

Separate files (the default) write <name>-bates.pdf, numbered continuously: if the first file ends at 000010, the second starts at 000011. Add --restart-each and every separate file starts again at --start instead, so two 3-page files read 000001-000003 and 000001-000003; the range report lists each file's own range (and the numbers repeat across files, so give each its own prefix if you will load them into a review platform). --combined writes one <stem>-bates-production.pdf and numbering does not restart per file, so --restart-each cannot be used with it. A plain-text range report <stem>-bates-report.txt is always written. --restamp covers an existing number with a box on standard white-background pages; it is not a deletion of the old stamp.

Trial output carries the same footer as redact. A license key removes it. This version does not write Concordance load files, slip sheets, per-file prefix maps, or skipped pages.

Review before you burn

Redaction is not reversible, so a person can approve it first.

keptpdf review ./intake -o decisions.csv
# open decisions.csv, change any wrong "keep" to "redact" (or the reverse)
keptpdf redact ./intake -o ./produced --apply decisions.csv

Rows above the sensitivity threshold arrive marked redact, rows below it arrive keep. An untouched file applies exactly what a plain run would have done, so adopting the step can never quietly stop redacting something.

Each row also has a code column. Type an exemption code there, like (b)(7)(C), and that one box gets that code at burn, beating --label and --labels. That is how one document cites different exemptions on different redactions.

The review file holds the detected text, because a human has to read it to judge it. It is a working file. Keep it with the sources and never ship it with the production.

Keep the last four digits: --keep-last

Court filing rules (FRCP 5.2(a), Bankruptcy Rule 9037) let a filing show the last four digits of a Social Security, taxpayer or financial account number. They allow no more than four: --keep-last accepts 1 to 12, but a larger number leaves more visible than those rules permit, and the certificate then cites no rule for it.

keptpdf redact ./filings -o ./produced --keep-last 4

With --keep-last 4, only the digits before the last four are boxed on SSNs, ITINs, EINs, card numbers, account numbers and IBANs. Every other category is boxed whole, as before, and nothing changes without the flag. The box edge is placed on the printed page, not estimated from the text layer. If the digits do not separate cleanly there (a wrapped or tilted number, glyphs that touch), or the last digits hold a letter, or the number has four digits or fewer, the whole number is boxed instead; the log's note column says why. A number that already prints a mask (XXX-XX-1234) is left as it is.

The redaction log gains a kept column (the digits still visible) and a note column. The certificate counts how many numbers keep their last digits and how many were boxed in full, and states that those values are not HIPAA Safe Harbor; it never lists the digits themselves.

With --searchable, the kept digits may not come out as searchable text: the text layer is read back off the finished page by OCR, and a short run of digits next to a black box is the kind of text OCR can misread or skip.

Exemption codes and the redaction log

FOIA and most production protocols want each black bar to say why it is there.

keptpdf redact ./records -o ./produced --label "(b)(6)" --log production.csv

--label stamps the code inside the box, clipped to it, never growing the redaction. --labels <file> gives a different code per category. The log records one row per box: file, page, category, code, method, confidence, position, and the certificate id. It carries no redacted text, on purpose: the log travels with the production, so listing what you hid would undo the redaction.

KeptPDF ships no exemption preset. Which one applies is your legal call, not a default.

Licensing

Install the key from your license email with keptpdf license --set <key>: it checks the signature and, only if the key is good, saves it in ~/.keptpdf/license.txt for you. To place a key yourself instead, or to keep one per client folder, the older routes all still work.

Set KEPTPDF_LICENSE, or put the key in ./keptpdf-license.txt or ~/.keptpdf/license.txt, or point one run at a specific key with --license <file> (it beats all of the above; a missing file refuses the run instead of falling back to another key). Every licensed run prints which key it used, and keptpdf license lists every place a key was found and which one wins. The key is checked with an offline signature, so a batch that runs overnight cannot be stopped by someone else's server.

A trial key is free from https://keptpdf.com/cli. It raises the 100 free files to 500 in total (files already processed count toward the 500) and stops working after 30 days; install it with the same keptpdf license --set <key>. It needs a release newer than 1.17.0, so run npm install -g keptpdf@latest first if the command says the key is not valid. A full license always wins over a trial key.

A license covers the documents of the organization named in the key. If you are a service provider running it for clients, each client organization needs a license naming them, and you may operate the software for them under it. Partner terms are at https://keptpdf.com/cli.

Working for multiple clients

The key file in the current directory wins over the machine-wide one, so keep one folder per client with that client's key beside the documents:

clients/
├── acme-legal/
│   ├── keptpdf-license.txt   # Acme Legal's key
│   └── intake/
└── harbor-cpa/
    ├── keptpdf-license.txt   # Harbor CPA's key
    └── intake/

Run the tool from the client's folder and every certificate names that client's organization. Nothing to switch, no wrong-client mistakes. To be explicit instead, or to run from anywhere, pass the key directly:

keptpdf redact intake/ -o done/ --license clients/acme-legal/keptpdf-license.txt

Either way the run prints Licensed to ... (key from ...), and keptpdf license run in any folder shows which key that folder would use.

Without a key the trial processes your first 100 files. That is a running total, not a per-run cap: the count lives in ~/.keptpdf/trial.json, is written only on unlicensed runs, and never leaves the machine. A free trial key from https://keptpdf.com/cli#trial raises the 100 to 500 files.

split --auto has one more limit on it. Detection is never limited: the trial reads every page of a file of any length and lists every document it finds, because whether the boundaries are right is the thing you are evaluating and a partial read cannot show you that. What it writes is capped at the documents that fit inside the first 50 pages, whole documents only, so a document straddling page 50 is listed and not written. --dry-run is never capped: it writes nothing, so there is nothing to limit, and it prints the full answer on a file of any size. Everything else the trial does is uncapped.

Trial output carries one small footer line naming it as trial output, so it is fine for judging the redaction and not fit to file or produce. A key removes it.

The footer is stamped into the page as an image, not as a text object, so there is nothing to select, search for or delete, and the audit certificate still reads 0 extractable characters with nothing excluded from the count.

Updates

keptpdf update (or "Check for updates" in the menu) asks keptpdf.com for the newest version number, prints what changed in it, and then installs it for you by running npm install -g keptpdf as a visible step. The menu asks before it installs; the command, which you typed to update, just does it. keptpdf update --check stops after the report and installs nothing, which is what scripts and locked-down machines want.

That version lookup is the only network request this tool makes on its own: it happens only when you run the command. It goes to keptpdf.com, which counts the check and passes along npm's answer, and it sends only the version you have and your operating system name: nothing about you, this machine, or any document. If keptpdf.com cannot be reached, it asks npm's public registry directly instead. (Installing the update is npm downloading a package, as it would if you typed the install yourself.) The answer is cached in ~/.keptpdf, so later runs can show a one-line "Update available" notice under the banner without touching the network again.

Prefer it automatic? Set KEPTPDF_UPDATE_CHECK=1 and the CLI refreshes that cache in the background at most once a day. Nothing is ever checked, sent, or suggested without one of those two opt-ins, so an air-gapped box behaves exactly as before.

Exit codes

0 done, 1 at least one file failed, 2 usage error, 3 license problem.


Questions: [email protected] or https://keptpdf.com