SemTools: static embedding search, cloud parsing and an agent on one command line
Semantic search and document parsing tools for the command line
At a glance
- What is it?
- SemTools bundles four subcommands with three different cost models: search and workspace run entirely on local disk with static embeddings, parse uploads to the LlamaParse API by default, and ask spends an OpenAI key on every answer. The search path is fast and local, while the install story carries one real trap.
- Who is it for?
- SemTools earns a place in a terminal-first pipeline that turns PDFs into answers without assembling four separate tools, and its local search carries the weight. It is the wrong choice for a corpus that must never leave the machine, since parse defaults to a cloud backend and ask calls an OpenAI endpoint.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 7 months ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Four subcommands split across cloud, local and agent paths
SemTools ships one Rust executable with four front doors, and they cost different things. `semtools search` embeds text on your own machine and ranks it by cosine similarity, with no network traffic. `semtools workspace` does the same work against a persistent store under `~/.semtools/workspaces/`. `semtools parse` is the exception: its default backend is `llama-parse`, a LlamaIndex Cloud service, so every file you hand it gets uploaded. `semtools ask` forwards collected text to a model that defaults to OpenAI.
Read the subcommand name as a statement about where your documents go. A pipeline that only calls search keeps everything on disk. The same pipeline rewritten with parse and ask pays twice, once for parsing and once for the answer, and the project is explicit that search and workspace stay local-only while parse defaults to LlamaParse and ask wants an OpenAI API key.
The npm manifest registers five binary entry points, semtools, parse, search, workspace and ask, each a small shim in `cli/` over the same Rust core. The Cargo side registers one executable only. That difference decides which command names resolve after a pipe, and the fourth section comes back to it.
Cosine distance over static embeddings, and why top-k disappears
The search path never calls a hosted model. Embeddings come from `model2vec-rs` 0.1.3, which loads a static model published as minishlab/potion-multilingual-128M, and the distance maths is handled by the `simsimd` crate at 6.5.1. A static model maps the same word to the same vector on every run, so there is no inference step between your query and the ranking.
Three knobs decide what comes back, and they interact. `--n-lines` sets how many lines before and after each hit are returned, 3 by default. `--top-k` limits how many files come back, also 3 by default. `--max-distance` changes the contract instead: every result below that threshold is returned, and the help text marks top-k as ignored whenever max_distance is set. Pass both flags and the count limit disappears with no message saying so.
-n, --n-lines <N_LINES> How many lines before/after to return as context [default: 3]
--top-k <TOP_K> The top-k files or texts to return (ignored if max_distance is set) [default: 3]
-m, --max-distance <MAX_DISTANCE> Return all results with distance below this threshold (0.0+)Case sensitivity is opt-in through `-i, --ignore-case`, which defaults to false, and `-j, --json` reshapes the output for machine readers. Nothing in the option list selects a different embedding model, so the 128M multilingual model is the only one this tool ranks with.
Installing through npm still compiles the Rust crate
Two install paths exist and only one is self-contained. The npm route pulls `@llamaindex/semtools`, whose install script is `node scripts/install.js`, and that script builds the Rust binaries locally when no prebuilt binary is available. The documentation states this requires Rust and Cargo in your environment and points at https://www.rust-lang.org/tools/install for the toolchain. The package asks for Node at >=16 and its only two runtime dependencies are tar and unzipper, which is how a downloaded archive gets unpacked.
With a toolchain already present, the crate route is the more interesting one, because features can be selected:
# install entire crate
cargo install semtools
# install only select features
cargo install semtools --no-default-features --features=parseYour first real command is a local search over text files, and it needs no credential:
# Search some (text-based) files
semtools search "some keywords" *.txt --max-distance 0.3 --n-lines 5Expect ranked text with 5 lines of context on each side of a hit. No API key, no config file and no index are needed for this, because both search and workspace are local-only by the project's own account.
Stdin by default, files only when xargs supplies them
The command reads standard input when no file arguments are given, which is why every pipeline example puts an `xargs` in front of the file-consuming call. The help text for the file argument says it is optional if you use stdin, so parsed markdown can flow straight into ranking.
semtools parse *.pdf | xargs cat | grep -i "error" | semtools search "network error" --max-distance 0.3There is an asymmetry in the project's own quick start worth naming. One example writes `xargs search "API endpoints"` while every later example writes `xargs semtools search`. Those are not interchangeable for everybody. The npm package installs a standalone search binary, but the Cargo manifest declares exactly one executable, named semtools at `src/bin/semtools.rs`. Install from npm and the short form resolves; install from cargo and it does not, so a pipeline copied from the quick start breaks at the pipe instead of at the search.
Ordering is not fixed. The examples put grep before parse to filter what gets uploaded, after search to trim output, and between the two stages as well.
Workspaces need an export, and deleted files need a prune
`semtools workspace use my-workspace` creates or selects a store, but it does not activate it. It prints a profile line and asks you to run `export SEMTOOLS_WORKSPACE=my-workspace` yourself, so in a fresh shell no workspace is active and nothing is cached. Files land under `~/.semtools/workspaces/`, while the config file read by parse and ask sits at `~/.semtools_config.json`, a separate location chosen by the same project.
Updates are handled and deletions are not. Change a document and the next search re-embeds it. Remove one and the entry stays until prune runs, and prune is described in the command list as removing stale or missing files from the store. That subcommand list is short: use, status, prune, help.
> Active workspace: arxiv
> Root: /Users/loganmarkewich/.semtools/workspaces/arxiv
> Documents: 3000
> Index: Yes (IVF_PQ)That sample is the only concrete sizing data in the docs: 3000 documents behind an index labelled IVF_PQ, with no timing or memory figure printed beside it. The workspace feature pulls three optional crates, `rand` 0.8.5, `qdrant-edge` at version 0.0.0 and `ordered-float` 5.1.0. A vector store dependency pinned at version zero deserves a look before you point this at a corpus you would hate to rebuild.
Two agent overrides the help text does not treat equally
`semtools ask` runs an agent with search and read tools over your files, and it points at OpenAI unless you change that. Three flags override settings, and the descriptions are not symmetrical. `--api-key` is documented as overriding the config file and the environment variable, while `--base-url` and `-m, --model` are each documented as overriding the config file alone. A base URL exported in your shell is therefore not something the option list promises to read, even though an API key in the environment is.
-c, --config <CONFIG> Path to the config file. Defaults to ~/.semtools_config.json
--api-key <API_KEY> OpenAI API key (overrides config file and env var)
--base-url <BASE_URL> OpenAI base URL (overrides config file)
-m, --model <MODEL> Model to use for the agent (overrides config file)The agent talks to models through `async-openai` at version 0.31.0-beta.1, enabled with both the chat-completion and responses features, and it reaches for the `grep` family, grep, grep-searcher, grep-matcher and grep-regex, to do exact matching inside files. A beta client crate on the path that spends money is worth reading twice before you wire it into a script. For non-OpenAI endpoints the project points at its configuration section, and `example_semtools_config.json` sits at the top level as a starting shape.
The selective install advice and the bin that requires everything
The install notes sell feature selection: `cargo install semtools --no-default-features --features=parse`. The manifest then declares a single binary and lists what that binary needs:
[[bin]]
name = "semtools"
path = "src/bin/semtools.rs"
required-features = ["ask", "search", "workspace", "parse"]All four features, each of them required. Cargo does not build a binary whose required features are missing from the enabled set, so the selective install offered in the documentation does not produce the executable the documentation then tells you to run. From source, plan on the full crate or edit the feature list in a fork of your own.
A second number needs care. `package.json` says 3.0.0 and `Cargo.toml` says 3.0.0, while the newest tag on the project is v3.0.1. Manifest metadata therefore trails the newest tag by one patch, and a lockfile pinned to it will not move by itself. The crate also declares edition 2024, which sets the toolchain floor for anyone building it.
Grep, a hand-written binary, and what staying current costs
The honest alternative to this search path is grep with flags you already own. grep matches substrings against raw bytes. SemTools embeds text with a 128M parameter static model and ranks by cosine distance, so a query word that never appears in a file can still surface a line about the same idea. The project's own examples interleave the two tools, with grep before parse to narrow uploads and grep after search to trim output.
The other alternative is to skip the wrapper and write a short program yourself against `model2vec-rs` and `simsimd`, which are ordinary public crates, then keep vectors in a store with a version number you can pin. You give up the workspaces, the prune command and the parse plumbing, and you get control over the embedding model, which no search option exposes.
On staying current, the record is short. The last push was on 2026-03-11 and the code ships under the MIT licence, which allows vendoring and modification as long as the notice travels with it. Three tags landed in about six weeks, v2.0.0 on 2026-01-27, v3.0.0 on 2026-03-05 and v3.0.1 on 2026-03-11, so the version in your lockfile will move faster than the bugs surface. The repository also carries a benchmarks directory whose results are quoted nowhere in the documentation, which leaves any performance claim about this tool to you.
Editorial conclusion
SemTools earns a place in a terminal-first pipeline that turns PDFs into answers without assembling four separate tools, and its local search carries the weight. It is the wrong choice for a corpus that must never leave the machine, since parse defaults to a cloud backend and ask calls an OpenAI endpoint. Before adopting it, check what the documentation leaves open: which backend names -b accepts besides llama-parse, and whether qdrant-edge at version 0.0.0 holds up at your own collection size rather than the 3000 documents in the sample status line.
Frequently asked questions
Does SemTools need an API key to search a folder of files?
No. Search and workspace are described as local-only and take no credentials. Keys attach to the other two subcommands: parse defaults to the LlamaParse backend and wants a LlamaIndex Cloud key, while ask wants an OpenAI API key.
Why does installing SemTools from npm need Rust on my machine?
The install script is `node scripts/install.js`, and it builds the Rust binaries locally whenever no prebuilt binary is available. Installing from `cargo install semtools` needs the same toolchain, so a machine without Rust ends up without semtools either way.
How do I keep a SemTools workspace index in step with my files?
Create it with `semtools workspace use my-workspace`, then run `export SEMTOOLS_WORKSPACE=my-workspace` yourself, because the command only prints that line. Changed documents are re-embedded automatically; for deleted ones you run `semtools workspace prune`.
Can SemTools ask use something other than OpenAI?
Yes. The agent defaults to OpenAI, and `--base-url` and `-m, --model` override the config file, which the project ties to connecting to any OpenAI-compatible API. Those two options are documented as overriding the config file only, unlike `--api-key`, which also overrides the environment variable.
What happens to top-k results when I pass a max-distance in SemTools?
The search help states that --top-k is ignored when max_distance is set, so the threshold rule wins and you get every result below it. The default of 3 applies to --n-lines and to --top-k, and no default is given for --max-distance.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/run-llama-semtools)Was this useful?