jfind — find files by describing them
jfind is a command-line file search tool that understands plain English. It works like the
Unix find command, with one extra predicate — --like "…" — that matches files by what they
are rather than by name pattern. Instead of guessing a glob or a regex, describe the file:
jfind . --like "unit tests for the payment module"
jfind src -type f -name '*.py' --like "handles user authentication" --content
jfind . --like "image or binary asset" -print0 | xargs -0 ls -l
The semantic matching is done by TypeSafe.ai's jev model, which
answers typed yes/no questions with a calibrated probability. Everything else — walking the tree,
-name, -type, -maxdepth, .gitignore — runs locally, so only the files that pass your
ordinary filters are ever sent for a semantic check.
Table of contents
- Why jfind
- Installation
- Usage
- How it works
- What gets sent to the API
- Cost
- Compared to find, fd and ripgrep
- FAQ
- Development
Why jfind
Classic file search tools match names (find -name, fd) or contents (grep, ripgrep).
Neither helps when you know what a file is for but not what it's called: "the config that sets
up logging", "anything to do with refunds", "the script that seeds the database". jfind answers
those questions in one command, ranks results by confidence, and composes with xargs, fzf
and shell pipelines like any other find-alternative.
Installation
uv tool install jfind-cli # recommended: isolated, on PATH as `jfind`
pipx install jfind-cli # alternative
pip install jfind-cli # into the current environment
The PyPI package is jfind-cli; the command and the Python module are jfind.
Requires Python 3.13+ and a TypeSafe.ai API key from
console.typesafe.ai/keys:
export TYPESAFE_API_KEY=... # or put it in a .env file in the current or a parent directory
Usage
jfind [PATH ...] -l TEXT [options] # PATH may be a directory or a file
-l, --like TEXT natural-language description to match (also -like)
-name GLOB match basename against a shell glob (case-sensitive)
-type f|d restrict to files or directories
-maxdepth N descend at most N levels
-newer FILE only entries modified after FILE
--hidden include dotfiles and dotdirs
--no-ignore do not honour .gitignore (repo-root, root and nested files all apply)
--content also send the first 8 KiB of each text file (binaries are skipped)
--threshold P minimum match probability (default 0.7)
--kind also classify each match: source/test/config/docs/data/binary/other
--explain print the probability (and kind) next to each path
--dry-run show candidate count and cost estimate; makes no API calls
-print0 NUL-separate output for xargs -0
--concurrency N parallel requests (default 20)
--model ID jev model id (default: jev-latest)
Examples
# Where is the code that talks to Stripe?
jfind src --like "integration with the Stripe payment API" --content
# Find test files for a feature, then run them
jfind tests --like "tests for user login and sessions" -print0 | xargs -0 pytest
# Which of the files changed since the last pull are documentation?
jfind . -newer .git/ORIG_HEAD --like "documentation or prose" --kind --explain
# Interactive: pick one of the semantic matches with fzf (-l is short for --like)
jfind . -l "database migration" | fzf
# Estimate before you spend anything
jfind . --like "anything" --content --dry-run
A run with --explain shows the probability jev assigned to each match and the token count:
$ jfind . --like "anything about payments" --content --kind --threshold 0.5 --explain
0.99 ./docs/payments.md [docs]
0.99 ./src/payments/charge.py [source]
0.98 ./tests/test_payments.py [test]
jfind: 3/14 matched, 7,661 input tokens (~$0.0003)
Exit codes and platform notes
Exit codes follow grep: 0 = at least one match, 1 = none, 2 = error (matches are still
printed; a single failed request never discards the others), 130 = interrupted.
Symlinks are never followed (like find -P): they are neither files nor directories, and
--content never reads outside the tree through one. Works the same on macOS, Linux and
Windows: paths are printed exactly as the filesystem stores them, "hidden" always means
dot-prefixed, and .gitignore matching does not depend on the path separator. On Windows, run it
from PowerShell or cmd and quote --like the same way.
How it works
- Walk locally. jfind walks the directory tree and applies every classic predicate
(
-name,-type,-maxdepth,-newer, hidden files,.gitignore) before touching the network. - Ask one question per candidate. Each surviving entry is described to jev as structured
JSON — path, name, extension, size, modification time and, with
--content, the first 8 KiB of text — together with a single yes/no question: does this match "…"? - Keep the confident ones. jev returns a probability between 0 and 1. Entries at or above
--thresholdare printed, highest probability first.
jev is not a chat model. It is a purpose-built model for typed questions (yes/no, multiple
choice, scored), which is exactly the shape of a find predicate: cheap, parallel and calibrated.
Requests run concurrently (--concurrency), so a few thousand files take seconds.
What gets sent to the API
Only the entries that pass your local filters, and for each of them only:
- the path relative to the root you gave, the file name and extension
- size and modification time
- with
--content: the first 8 KiB of the file, if it is text (binaries are detected and skipped)
Nothing else leaves your machine. --dry-run shows how many entries would be sent and what it
would cost, without sending anything.
Cost
jev charges $0.042 per million input tokens; output is free. A metadata-only query costs
roughly 400 tokens per file, --content a few thousand — so scanning a 1,000-file project is
well under a cent. --dry-run prints an estimate first.
Compared to find, fd and ripgrep
| Tool | Matches on | Example |
|---|---|---|
find / fd |
file name, type, size, time | fd -e py test_ |
grep / ripgrep |
literal text or regex inside files | rg "def charge" |
jfind |
a natural-language description of the file | jfind --like "charges a customer's card" |
jfind is not a replacement for any of them: it takes the same predicates as find, prints the
same kind of output, and is meant to sit in the same pipelines. Use it when the thing you know
about a file is what it does, not what it's called.
FAQ
Does it work offline? No — the semantic matching runs on TypeSafe.ai's API. Everything
else (walking, filtering, --dry-run) is local.
Does it read my whole codebase? Only metadata by default. --content adds the first 8 KiB
of each text file that passed your local filters. See What gets sent.
How accurate is it? jev returns a probability, not a verdict, so you choose the trade-off:
raise --threshold for precision, lower it for recall, and use --explain to see the scores.
Descriptions in English work best.
Can I use it in scripts? Yes. Output is one path per line (or -print0), exit codes follow
grep, and errors go to stderr.
Why is the package called jfind-cli? The name jfind was already taken on PyPI. The
command you run and the module you import are still jfind.
Development
git clone https://github.com/religa/jfind && cd jfind
uv sync --group dev
uv run pytest # unit tests; the API client is stubbed, no key needed
uv run ruff check . && uv run ruff format --check .
uv run jfind . --like "the CLI entry point" --explain # live, needs TYPESAFE_API_KEY
uv build # sdist + wheel into dist/
Layout: src/jfind/walk.py (local filtering), matcher.py (jev questions, async fan-out),
cli.py (argparse + output).
Releases: bump version in pyproject.toml and CHANGELOG.md, tag vX.Y.Z, publish a GitHub
release — .github/workflows/publish.yml uploads to PyPI via Trusted Publishing.
License
Release files for jfind-cli 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jfind_cli-0.1.0.tar.gz | 13.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jfind_cli-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 27.9 kB
Release files / jfind_cli-0.1.0.tar.gz
| Download URL | jfind_cli-0.1.0.tar.gz |
|---|---|
| Size | 13.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bde24752f4ec8e2ed1e64f801bfc1942222637eea5b5163fb2dfd68b1c5fcb4c
|
|
BLAKE2b-256 checksum How to use checksums |
8dbc20d444254b18577d2e3bcd5b4162afd4ea7d06d5173031f73fb400ea62b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / jfind_cli-0.1.0-py3-none-any.whl
| Download URL | jfind_cli-0.1.0-py3-none-any.whl |
|---|---|
| Size | 14.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3c5662ba71751eb9bc318d9451f75b5b3b788b73502bd680aaad3a481070e50c
|
|
BLAKE2b-256 checksum How to use checksums |
cd03ad6d2f6aac7e6991e5c9070d89235cc4201d983aeee997fde0e8d187e5d2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log