Skip to main content

PartsMatcher

A small, zero-dependency Python CLI that matches the parts you own against a database of projects and answers the workbench question: what can I build right now, and what am I one or two parts away from? A chat mode layers your locally installed Claude Code on top for improvised project ideas — no API key involved.

One inventory JSON is the shared source of truth for both modes:

  • partsmatcher match — fast deterministic report, fully offline
  • partsmatcher chat — hands the same inventory to your local claude
  • partsmatcher app — the same chat session in your browser: embedded chat beside a live inventory and match report (still your local claude, driven headlessly one turn at a time; still zero dependencies)

The matcher reads two JSON files — your parts inventory and a project database — and sorts every project into three groups:

  1. BUILD NOW — every required part is covered by your inventory.
  2. ALMOST THERE — you're short at most 2 parts total (configurable), with the exact shopping list per project. Sorted by fewest missing parts first.
  3. NOT YET — everything else.

Quantities count: if a project needs 3 LEDs and you own 2, that's 1 missing part, not zero and not "missing LEDs."

Quick start

Runs out of the box on the bundled sample data (Python 3.9+, no dependencies):

$ python -m partsmatcher
Matched 10 projects against 15 part types (93 parts on hand).

BUILD NOW (3)
  ✔ Blink Badge — The hello-world wearable: one red LED pulsing on a Nano...
  ✔ Reaction Timer — Two players, two buttons, one LED — first press after the light wins.
  ✔ Sunset Night-Light — A photoresistor watches the room and fades the LED up as it gets dark.

ALMOST THERE (4) — short at most 2 parts, fewest missing first
  ≈ LED Dice — short 1 part
      Mash the button to roll seven LEDs arranged like dice pips.
      needs Red LED: have 6 of 7 (short 1)
  ≈ Servo Radar Sweep — short 1 part
      ...
      needs 16x2 I2C LCD: have 0 of 1 (short 1)
  ...

NOT YET (3)
  ✘ Desk Weather Station — short 3 parts across 3 part types
  ...

With your own files:

$ python -m partsmatcher my_inventory.json my_projects.json

Optionally install it as a partsmatcher command:

$ pip install -e .
$ partsmatcher --help

Chat mode — project ideas grounded in exactly what you own

$ python -m partsmatcher chat

partsmatcher chat does not call the Anthropic API and needs no API key. It launches the Claude Code CLI (claude) you already have installed and logged in as an interactive child process, so your existing subscription auth applies and this tool bills nothing per token.

What happens at startup:

  1. The tool verifies claude exists on PATH, and exits with an install hint if it doesn't (npm install -g @anthropic-ai/claude-code).
  2. A session workspace is prepared (a fresh temp directory by default) with a generated CLAUDE.md containing the inventory, the deterministic match report, workbench-assistant guidance, and a ready-to-run matcher command (so Claude can re-check the report itself as the inventory changes) — plus copies of inventory.json and projects.json.
  3. claude is spawned in that workspace with your real terminal attached. Claude Code reads CLAUDE.md at session start, so the chat opens already knowing every part you own, quantities included. Without a --prompt of your own, the session opens by introducing what it knows and what it can do (photo sessions open with photo identification instead).

Then just talk: ask for project ideas beyond the database, pin-by-pin wiring explanations, or a substitute when you discover a part is missing — Claude adapts using only the parts on hand and flags anything you'd have to buy.

Conversational inventory intake

Describe parts in plain language and Claude folds them into your inventory:

you> picked up a strip of neopixels and a bag of resistors, and add two more nanos
claude> A couple of questions before I write anything:
        - the resistors — what value, and roughly how many?
        - the NeoPixel strip — how long / how many LEDs?
        ...
        raw phrasing            → normalized entry
        "a strip of neopixels"  → WS2812B LED strip (1 m) ×1
        "a bag of resistors"    → 470 ohm resistor ×25
        "two more nanos"        → Arduino Nano ×2 (merged into existing entry)
        Write these to inventory.json?

The generated CLAUDE.md instructs Claude to normalize each described part against the schema (reusing existing canonical names where the part already exists), ask clarifying questions for anything ambiguous — vague quantities, missing specs, unclear board identity — confirm the batch, then update inventory.json. "Used up four red LEDs" decrements the same way.

Vocabulary alignment. The deterministic matcher matches part names exactly, so run intake with your project database loaded (skip --no-projects) once you have one: the session then prefers the project database's part names for the same physical part, offers to expand assortment kits into the specific values projects reference (a resistor kit ×1 entry can't satisfy 220 ohm resistor ×7), and — if you ask it to "reconcile" — walks the match report's missing parts one by one to find the ones you actually own under a different name, logging each rename as a corrected alias.

Naming-alias dataset. Every normalized entry is also logged as one JSON line in aliases.jsonl, pairing your raw phrasing with the canonical name:

{"raw": "a strip of neopixels", "name": "WS2812B LED strip (1 m)", "quantity": 1, "action": "added"}

Corrections and casual references get logged too — this file grows into the naming dataset a future vision module will use to map detected parts onto your inventory vocabulary.

Sync-back. When the session ends, PartsMatcher validates the edited inventory.json and writes it back to your inventory file (previous version saved alongside as <name>.bak), does the same for projects.json when your own project database is loaded, and appends the session's new alias records to <stem>.aliases.jsonl next to it. Invalid edits never overwrite your file — you get a warning and the workspace path instead. When you're on the bundled sample data nothing is overwritten either; the summary tells you where the updated files live. --no-sync disables write-back entirely.

Recovery. If a session dies before that exit sync can run — a closed terminal window, a crash — nothing is lost. Every session records what the sync needs inside its workspace, so afterward:

$ python -m partsmatcher recover

re-runs the same validate-and-sync against the newest recoverable workspace (pass a workspace path to pick a specific one). It's safe to re-run: files already in sync are detected and left alone.

Workspaces accumulate in the temp directory — one per session, each holding a full copy of the inventory. To see what's piled up and what each one still owes your files:

$ python -m partsmatcher recover --list
2 recoverable chat workspace(s) under /tmp:
  /tmp/partsmatcher-chat-9k2f1a  (just now) — unsynced
      inventory edits not yet written to /Users/you/my_inventory.json
      1 naming-alias record(s) not yet appended to /Users/you/my_inventory.aliases.jsonl
  /tmp/partsmatcher-chat-3d81cc  (4d ago) — synced

`partsmatcher recover --clean` would remove 1 of 2 (nothing left to sync).

(The temp directory is wherever Python puts it: /tmp on most Linux, but /var/folders/.../T/ on macOS — so a /tmp/partsmatcher-chat-* glob finds nothing there. recover --list prints the real paths; so does the Session workspace: line at startup.)

recover --clean deletes the disposable ones. A workspace whose changes haven't reached your files is kept and reported — including sessions on the bundled sample, where the workspace holds the only copy — so cleaning can't throw away work you haven't recovered yet. --force overrides that and deletes everything.

Photo intake — vision v1

Photos are another intake source, flowing through the same confirmation loop:

$ python -m partsmatcher chat my_inventory.json --photo bench.jpg --photo drawer.png

Staged photos are copied into the session workspace under photos/, and the session opens by identifying them (Claude Code reads images natively — no extra tooling). Each detected part comes back as a draft entry with the identification confidence and reasoning:

photos/bench.jpg → 470 ohm resistor ×25 (confidence 0.7 — blue axial body, bands unreadable)

Uncertain identifications go through the usual clarifying questions ("about 20 axial resistors, but I can't read the bands — what value?"), and nothing is written without your confirmation. Confirmed photo entries land in inventory.json with provenance the schema already tolerates — "source": "photo" and "confidence" — and their alias records use the visual description as the raw phrasing:

{"raw": "blue axial resistor, 4-band", "name": "470 ohm resistor", "quantity": 25, "action": "photo", "photo": "bench.jpg", "confidence": 0.7}

You can also hand Claude an image path mid-session (or drop a file into the workspace photos/ folder) — same flow. Supported types: .png, .jpg, .jpeg, .gif, .webp.

A personal project database

The bundled sample projects are generic — written around parts you may not own (a "USB cable" that really means a Nano's Mini-B). The honest fix is your own database, and chat mode can author it:

$ python -m partsmatcher chat my_inventory.json my_projects.json

If my_projects.json doesn't exist yet, the session starts from an empty database and the file is created on first sync. Describe what you want to build ("add a project: plant waterer on the Uno with the pump and a soil sensor") and Claude drafts the complete parts list — quantities and hookup consumables included — using your inventory's exact part names for parts you own, which is what makes the resulting BUILD NOW rows honest: the matcher matches names exactly, so a personal project written around your real "USB Type-B cable" matches it, where the sample's generic "USB cable" never will. Ambiguities go through the usual clarifying questions, the drafted project is confirmed before writing, and adapting a sample project to your parts is a normal move ("Blink Badge (UNO)" with substitutions noted in the description).

Project edits sync back like inventory edits — validated first, previous version saved as <name>.bak, created fresh on first sync, covered by recover. Edits made while the bundled sample is loaded stay in the workspace instead; the sample is never modified.

$ python -m partsmatcher chat my_inventory.json my_projects.json
$ python -m partsmatcher chat --no-projects            # inventory only
$ python -m partsmatcher chat --prompt "What could I build in an hour?"
$ python -m partsmatcher chat --workdir ~/bench/chat   # persistent workspace
$ python -m partsmatcher chat -- --continue            # extra args go to claude

Safety note: a CLAUDE.md that PartsMatcher didn't generate is never overwritten — pick a different --workdir instead.

The deterministic matcher never requires Claude to be installed; match stays fully offline.

The local app

partsmatcher app serves the same session as a browser page instead of handing over the terminal:

$ python -m partsmatcher app my_inventory.json my_projects.json

The page pairs an embedded chat with a live sidebar — your inventory and the deterministic match report, re-read from the workspace after every turn, so intake and reconciliation update them as you talk. Under the hood each message runs your local claude headlessly for one turn (claude -p --resume — same login, no API key, no new dependencies: the server is stdlib http.server, the page a single embedded file).

The workspace mechanics are chat's, unchanged: generated CLAUDE.md context, alias logging, validated sync-back with .bak backups, and recover if the app dies uncleanly. Because headless turns can't show interactive permission prompts, the workspace pre-authorizes exactly what sessions do anyway (editing the workspace files, re-running the matcher) via a generated .claude/settings.local.json.

Sync runs when you click End session & sync (which also stops the app) or on Ctrl-C in the terminal. Flags mirror chat (--photo, --prompt, --workdir, --no-projects, --no-sync), plus --port N and --no-browser.

Input formats

Inventory — the parts you own

A list of parts, either bare or wrapped in a parts key:

{
  "parts": [
    { "name": "Red LED", "quantity": 6 },
    { "name": "220 ohm resistor", "quantity": 20 }
  ]
}
  • name (required): matched case-insensitively, with surrounding/repeated whitespace ignored — "red led" and "Red LED" are the same part.
  • quantity (optional, default 1): a non-negative integer.
  • Repeated names are merged by summing quantities.
  • Unknown fields (source, confidence, bin, ...) are tolerated and ignored, so richer producers can annotate parts freely.

Project database

A list of projects (bare, or wrapped in a projects key). Each project has a name, an optional description, and a non-empty parts list (required_parts works as an alias) with the same part shape as the inventory (quantity defaults to 1, must be at least 1):

{
  "projects": [
    {
      "name": "LED Dice",
      "description": "Mash the button to roll seven LEDs arranged like dice pips.",
      "parts": [
        { "name": "Red LED", "quantity": 7 },
        { "name": "Tactile pushbutton", "quantity": 1 }
      ]
    }
  ]
}

The bundled samples live at partsmatcher/samples/ — copy them as a starting point for your own files.

How the matching works

Each project is a multiset-coverage check (set cover with quantities) against your inventory: for every required part, the deficit is max(0, required − owned).

  • BUILD NOW: every deficit is 0.
  • ALMOST THERE: total deficit (summed in units across part types) is between 1 and --almost N (default 2). Needing 3 LEDs while owning 2 contributes 1; needing a yellow and a green LED you don't own contributes 2.
  • NOT YET: total deficit exceeds the threshold.

The ALMOST THERE and NOT YET groups are sorted by fewest missing units first, breaking ties by fewer distinct part types to buy, then by name. BUILD NOW is sorted by name.

Projects are evaluated independently — each one is checked as if it alone gets your whole inventory. Two projects that both need your only servo both count as buildable; the tool doesn't (yet) compute which combination of projects you could build simultaneously.

CLI reference

python -m partsmatcher match [INVENTORY_JSON] [PROJECTS_JSON] [options]

  --almost N     max total missing parts for the ALMOST THERE group (default: 2)
  --json         emit the report as JSON on stdout (notes go to stderr)
  -v, --verbose  also list exactly what each NOT YET project is missing
  --no-color     disable ANSI colors (NO_COLOR env var works too)

python -m partsmatcher chat [INVENTORY_JSON] [PROJECTS_JSON] [options] [-- CLAUDE_ARGS...]

  --almost N       threshold used for the match report handed to Claude
  --no-projects    give Claude only the inventory, no project database
  --photo IMAGE    stage a photo of parts for identification (repeatable)
  --workdir DIR    session workspace (default: fresh temp dir)
  --prompt TEXT    opening prompt for the Claude session
  --claude-bin P   Claude Code binary to launch (default: claude)
  --no-sync        keep inventory edits and alias records in the workspace
  --               everything after this is passed to claude verbatim

python -m partsmatcher recover [WORKSPACE_DIR]

  re-run the end-of-session sync for a chat session that ended without a
  clean exit — closed terminal, crash — validating and backing up exactly
  like the normal exit path (default: the newest recoverable workspace)

  --list           show every leftover workspace, its age, and what it
                   still owes your files; writes nothing
  --clean          delete leftover workspaces that are already synced back;
                   ones still holding changes are kept and reported
  --force          with --clean, delete those too

  --version        show version

Running without a subcommand behaves as match, so python -m partsmatcher inv.json proj.json --json keeps working.

Exit codes: 0 on success, 2 on bad input (unreadable file, invalid JSON, schema errors — reported with the file, project, and part that caused them) or when claude isn't installed; chat otherwise returns claude's own exit code.

JSON output

--json emits a machine-readable report, handy for piping into other tools:

{
  "summary": {
    "projects": 10,
    "build_now": 3,
    "almost": 4,
    "not_yet": 3,
    "almost_threshold": 2,
    "inventory_part_types": 15,
    "inventory_total_parts": 93
  },
  "build_now": [{ "name": "Blink Badge", "description": "...", "total_missing": 0, "missing": [] }],
  "almost": [
    {
      "name": "LED Dice",
      "description": "...",
      "total_missing": 1,
      "missing": [{ "name": "Red LED", "required": 7, "have": 6, "missing": 1 }]
    }
  ],
  "not_yet": ["..."]
}

The vision path

Vision v1 ships as photo intake inside chat mode (above): identification runs through your Claude Code session, drafts flow through the confirmation loop, and confirmed entries carry source/confidence provenance. A future standalone scanner (batch photo → inventory, no conversation) stays easy because the inventory schema was designed as its contract — it just emits the same JSON, and no matcher changes are needed:

  • It may write one entry per detection with quantity: 1; duplicates are merged by summing, so aggregation comes for free.
  • It may attach extra fields per part (source, confidence, a photo reference, a bin location); the matcher ignores what it doesn't know.
  • Name normalization absorbs harmless labeling differences in case and whitespace.
{
  "parts": [
    { "name": "Red LED", "quantity": 1, "source": "vision", "confidence": 0.93 },
    { "name": "Red LED", "quantity": 1, "source": "vision", "confidence": 0.88 }
  ]
}

The core logic is importable independently of the CLI (from partsmatcher import parse_inventory, parse_projects, match), so a vision pipeline can also call it directly. Chat mode already treats the inventory file as the shared source of truth — a future vision module that rewrites inventory.json feeds both the matcher and the Claude session with no further changes. And the *.aliases.jsonl dataset that conversational intake accumulates (raw human phrasing → canonical part name) is exactly the vocabulary-mapping data that module will need to label what it sees.

Development

$ python -m unittest        # run the test suite (no Claude required)
$ python -m partsmatcher    # smoke-test against the bundled samples

Layout: partsmatcher/matcher.py holds the pure matching logic (no I/O), partsmatcher/cli.py the argument parsing and rendering, partsmatcher/chat.py the Claude Code session preparation and launch, and partsmatcher/samples/ the bundled demo data pinned by the tests. The chat tests inject fake which/launch callables, so the suite runs without Claude installed.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

partsmatcher-0.7.1.tar.gz (78.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

partsmatcher-0.7.1-py3-none-any.whl (50.8 kB view details)

Uploaded Python 3

File details

Details for the file partsmatcher-0.7.1.tar.gz.

File metadata

  • Download URL: partsmatcher-0.7.1.tar.gz
  • Upload date:
  • Size: 78.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for partsmatcher-0.7.1.tar.gz
Algorithm Hash digest
SHA256 21f92d884c9989bc23008b04dbc27d37a44fe7cb7f5231441065f5e6a5587728
MD5 0d3f67bd81756cb99d5065dfd3d40a56
BLAKE2b-256 e9aa358bb6a667dba06221d3d5fa1e3ece918d94b84b2001bebe5e53bd1569f6

See more details on using hashes here.

Provenance

The following attestation bundles were made for partsmatcher-0.7.1.tar.gz:

Publisher: release.yml on BmartOcho/with-this

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file partsmatcher-0.7.1-py3-none-any.whl.

File metadata

  • Download URL: partsmatcher-0.7.1-py3-none-any.whl
  • Upload date:
  • Size: 50.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for partsmatcher-0.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 242e483007c317285898a60728df6ce6b3368a957c05c14c4f2c5af98286ddba
MD5 8a69128c5d078e76de867a9ede29d0b6
BLAKE2b-256 b0f48490fa4f89b517de78dcf96e0a0088c768fd814e1c9ca30dffed91ce5446

See more details on using hashes here.

Provenance

The following attestation bundles were made for partsmatcher-0.7.1-py3-none-any.whl:

Publisher: release.yml on BmartOcho/with-this

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.7.1 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page