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

@frankledo/pdfnamer

v2.1.0

Published

Rename and organize PDF bills and statements by company and date using a config file

Downloads

835

Readme

pdfnamer

npm version License: MIT

pdfnamer is a CLI tool that renames and organizes PDF bills and statements by automatically extracting the company name, date, and document type from the PDF content. Using a config file you maintain, it matches each PDF to a known company via filename patterns or keyword scanning, extracts the statement date, and moves the file to a structured output path — keeping your downloads folder clean with zero manual effort.

Installation

npm install -g @frankledo/pdfnamer

Requires Node.js 22.13 or later. No other system dependencies are needed.

Quick start

1. Install

npm install -g @frankledo/pdfnamer

2. Generate a starter config

pdfnamer --init

This writes ~/.config/pdfnamer/config.json with an annotated example you can edit.

3. Run on a folder of PDFs

pdfnamer ~/Downloads

Use --dry-run to preview renames without moving any files:

pdfnamer --dry-run ~/Downloads

Configuration

The config file lives at ~/.config/pdfnamer/config.json by default. Pass --config <path> to use a different location.

Full annotated example

{
  "output_path": "~/Documents/Bills/{year}/{company}/{date} - {company}[ - {account}][ - {doctype}]",
  "date_format": "YYYY-MM-DD",
  "date_labels": ["Statement Date:", "Bill Date:", "Invoice Date:", "Closing Date:"],
  "companies": [
    {
      "name": "Riverside Electric",
      "filename_patterns": ["riverside_bill_*", "RiversideElectric_*"],
      "keywords": ["Riverside Electric Company", "riverside-electric.example.com"],
      "noAccountNumber": true,
      "subjects": [
        { "match": "Electric Bill", "alias": "Bill" }
      ]
    },
    {
      "name": "Oakwood Bank - Sapphire Visa - 1234",
      "keywords": ["Sapphire Visa", "oakwoodbank.example.com"],
      "keyword_search_chars": 8000,
      "noAccountNumber": true,
      "date_labels": ["Closing Date:", "Statement Date:"],
      "subjects": [
        { "match": "Statement", "alias": "Statement" }
      ]
    },
    {
      "name": "Oakwood Bank - Rewards Visa - 5678",
      "keywords": ["Rewards Visa", "oakwoodbank.example.com"],
      "keyword_search_chars": 8000,
      "noAccountNumber": true,
      "date_labels": ["Closing Date:", "Statement Date:"],
      "subjects": [
        { "match": "Statement", "alias": "Statement" }
      ]
    },
    {
      "name": "Oakwood Bank",
      "keywords": ["oakwoodbank.example.com", "Oakwood Bank"],
      "accounts": [
        { "number": "9012", "alias": "Checking" }
      ],
      "subjects": [
        { "match": "Account Statement", "alias": "Statement" }
      ]
    }
  ]
}

Top-level fields

| Field | Default | Description | |---|---|---| | output_path | required | Template for the output file path. See tokens below. | | date_format | YYYY-MM-DD | Format for {date} in the output path. | | date_labels | ["Statement Date:", ...] | Labels searched in every PDF when extracting the statement date. Can be overridden per company. | | rename_in_place | false | When true, rename files in their current directory instead of moving them to output_path. The filename format is still controlled by the basename of output_path. Useful for receipts folders you want to keep in place but make searchable. | | unmatched_prefix | false | When true, files that don't match any company entry are still renamed: a date prefix ({date} - ) is prepended to the original filename. Files that can't yield any date, or whose name already starts with a date, are left unchanged. Pairs well with rename_in_place. |

Output path tokens

| Token | Description | |---|---| | {year} | Four-digit year extracted from the statement date | | {month} | Two-digit month (01–12) | | {date} | Full date formatted per date_format | | {company} | Company name from the matched config entry | | {account} | Account alias from a matched accounts entry | | {doctype} | Document type alias from a matched subjects entry |

Wrap a token and its surrounding delimiter in [...] to make the whole segment optional — it is omitted entirely when the token is empty:

{date} - {company}[ - {account}][ - {doctype}]
{date}_{company}[_{account}][_{doctype}]
{year}/{company}/{date} - {company}[/{doctype}]

Companies that have no accounts entry will silently skip the [ - {account}] block; companies that do will include it. This works with any delimiter style.

Per-company fields

| Field | Description | |---|---| | name | Display name used in the output path. | | output_path | Override the global output_path for this company only. | | filename_patterns | Glob patterns checked against the PDF filename (case-insensitive, supports *). Matched before keywords — no PDF parsing needed. | | keywords | Strings to scan for in the PDF text when no filename pattern matches. | | keyword_search_chars | How many characters from the start of the PDF text to search for keywords. Default 2000. Increase for documents where card-specific branding appears later in the file. | | date_labels | Override the global date_labels for this company. Useful when a company uses non-standard date field names. | | accounts | Array of { number, alias } objects. When a number is found in the PDF text the corresponding alias is used as {account}. | | subjects | Array of { match, alias } objects. When match is found in the PDF text the alias is used as {doctype}. First match wins. | | noAccountNumber | Set true to suppress account matching entirely (omits {account} token). |

CLI flags

| Flag | Description | |---|---| | -n, --dry-run | Preview renames without moving any files | | -c, --config <path> | Use a custom config file instead of ~/.config/pdfnamer/config.json | | --init | Write a starter config to ~/.config/pdfnamer/config.json and exit | | --install-quickaction | Install a Finder Quick Action (macOS) so you can right-click PDFs to rename them. See Quick Action (macOS) for full setup steps. | | --install-watcher [dir] | Install a launchd agent (macOS) that auto-files a folder whenever it changes. Defaults to ~/Downloads. See Folder watcher (macOS). | | --uninstall-watcher | Remove the folder watcher (macOS). | | -h, --help | Show usage information |

How matching works

pdfnamer processes each PDF in two phases. Company order matters: the first matching entry wins. Place specific entries before catch-alls.

Company identification

  1. Check each company's filename_patterns against the PDF filename (fast, no PDF parsing needed). First match wins.
  2. If no pattern matches, extract the PDF text and scan for each company's keywords in order. First match wins.
  3. If still no match, report UNMATCHED with a text snippet to help you add a new entry.

Ordering example: Two credit cards from the same bank share a generic bank keyword. Put the card-specific entries first; the generic bank entry acts as a catch-all:

{ "name": "Oakwood Bank - Sapphire Visa", "keywords": ["Sapphire Visa", "oakwoodbank.example.com"], "keyword_search_chars": 8000 },
{ "name": "Oakwood Bank - Rewards Visa",  "keywords": ["Rewards Visa",  "oakwoodbank.example.com"], "keyword_search_chars": 8000 },
{ "name": "Oakwood Bank",                 "keywords": ["oakwoodbank.example.com", "Oakwood Bank"] }

The catch-all only matches files that didn't already match a card-specific entry. Similarly, if two companies share a keyword, the one listed first always wins.

Date extraction

Dates are extracted in this order — the first successful result is used:

  1. Search for each label in date_labels (case-insensitive). When found, parse the date in the following 60 characters.
  2. Detect a date range anywhere in the document (MM/DD/YYYY - MM/DD/YYYY, MM/DD/YYYY to MM/DD/YYYY, MM/DD/YY-MM/DD/YY) and use the end date.
  3. Fall back to the first parseable date in the document header (first 2000 characters).
  4. Fall back to a date embedded in the filename itself.

Recognized date formats

  • YYYY-MM-DD (ISO)
  • Month DD, YYYY and Month DD , YYYY (e.g. January 1 , 2026)
  • MM/DD/YYYY and MM-DD-YYYY
  • MM/DD/YY and MM-DD-YY
  • YYYYMMDDHHmmss embedded in filenames (e.g. utility download filenames)
  • Bare YYYY (tax year — mapped to December 31 of that year)

Quick Action (macOS)

pdfnamer can install a Finder Quick Action so you can right-click any PDF and rename it without opening a terminal.

1. Install the Quick Action

pdfnamer --install-quickaction

This copies a Workflow file into ~/Library/Services/.

2. Enable it in System Settings

Go to System Settings → Privacy & Security → Extensions → Finder Extensions and check the box next to pdfnamer.

3. Use it

Right-click any PDF in Finder → Quick Actions → pdfnamer.

Note: The Quick Action uses your default config at ~/.config/pdfnamer/config.json. Run pdfnamer --init first if you haven't set one up yet.

Folder watcher (macOS)

pdfnamer can install a launchd agent that automatically files a folder every time its contents change — drop a statement in the folder and it's renamed and moved without running anything by hand.

Install

pdfnamer --install-watcher ~/Downloads

The folder argument is optional and defaults to ~/Downloads. This writes a LaunchAgent to ~/Library/LaunchAgents/com.pdfnamer.watcher.plist, loads it, and logs activity to ~/Library/Logs/pdfnamer.log. Matched PDFs are renamed and moved automatically (no preview); unmatched files are left in place.

Uninstall

pdfnamer --uninstall-watcher

One folder at a time: installing again with a different folder repoints the single watcher rather than adding a second one.

Pin a stable Node: the agent records the absolute path of the Node that ran the install. If that's a version-managed Node (nvm, fnm, volta, n), the path can disappear when you switch or remove versions, silently stopping the watcher. --install-watcher warns when it detects this — install pdfnamer under a stable Node (e.g. Homebrew) for a watcher that keeps working.

Note: The watcher uses your default config at ~/.config/pdfnamer/config.json.

Contributing

Bug reports and pull requests are welcome on GitHub.

License

MIT — see LICENSE.