cds is a cd replacement with semantic directory search.
cds has full cd compatibility via a passthrough. You can use cds exactly like
you use cd today.
Have you ever wanted to go to a directory but you forget exactly what it is and where it is? cds solves this problem.
cds the chrome extension I worked on last month will take you right there.
When you first download cds, you define directories that you'd like to index. ~/Projects for example.
When you then run cds --init, cds will scan all of the folders/files in those directories (excluding hidden directories
and sensitive files) and generate embeddings for them that are stored in a local SQLite database. File chunk embeddings
are also indexed in sqlite-vec virtual tables so semantic search can ask SQLite for nearest-neighbor candidates before
the Rust ranking pass applies path, recency, and directory heuristics.
When you later run cds the chrome extension I worked on last month cds will search that embeddings data to find
the directory you're looking for and invoke cd to navigate you there.
Embeddings get expensive fast and I have to use a very tiny model (bge-small-en-v1.5) to keep this lean. If we have a ton of parameters in the database A: The db would be huge and B: The search would take a lot of time. sqlite-vec keeps query-time vector candidate retrieval from turning into a full Rust-side scan of every chunk embedding.
cargo buildFrom the repository root:
./install.shThe installer runs cargo install --path ., adds an idempotent shell integration block to
your ~/.zshrc or ~/.bashrc, and starts the indexing daemon in the background. The daemon
pid and log are written under ~/.cache/cds/ by default. After it finishes:
source ~/.zshrc
cds --initUse source ~/.bashrc instead if you use bash.
To replace an existing installed binary:
./install.sh --forcecargo test --allDefault builds include the real local embedding runtime. Use --no-default-features with
CDS_EMBEDDER=fake for shell-only checks that do not need bge-small-en-v1.5.
cds only indexes directories you configure. It does not crawl the whole filesystem.
Create the default config and SQLite database, then index the configured roots:
cds --initCreate only the default config without creating the database, loading the model, or scanning:
cds --init-configBy default --init creates:
~/.config/cds/config.json
~/.local/share/cds/cds.sqlite
--init-config creates only ~/.config/cds/config.json.
The initial config looks like this:
{
"index": {
"roots": ["~/Projects"],
"exclude": [
".git",
"Applications",
"Library",
"Network",
"System",
"Volumes",
"cores",
"private",
"node_modules",
"target",
"dist",
"build",
".next",
".cache",
".venv",
"venv",
"vendor",
"*.xcassets",
"*.imageset",
"*.appiconset",
"*.colorset"
],
"high_signal_files": [
"README*",
"Cargo.toml",
"package.json",
"manifest.json",
"pyproject.toml",
"requirements.txt",
"go.mod",
"Gemfile",
"Dockerfile",
"docker-compose.yml",
"compose.yml",
"compose.yaml",
"tsconfig.json"
],
"max_file_bytes": 65536,
"max_excerpt_bytes": 4096,
"max_entries_per_directory": 80,
"max_depth_per_top_level_directory": 3,
"max_chunk_bytes": 4096,
"generic_terms": [
"a",
"an",
"and",
"app",
"application",
"code",
"dir",
"directory",
"for",
"from",
"game",
"in",
"last",
"me",
"my",
"of",
"on",
"program",
"project",
"repo",
"repository",
"search",
"site",
"that",
"the",
"thing",
"to",
"tool",
"web",
"website",
"with",
"work"
]
},
"detectors": []
}exclude is for directory names and legacy general patterns. high_signal_files is the
file-content embedding allowlist. It matches exact basenames such as package.json and
wildcard patterns such as README* or *.md; files outside this list are not content
embedded.
generic_terms controls low-signal query words that should not dominate path filtering or
ranking. You can add or remove words from that list without reindexing.
Run an indexing pass over configured roots:
cds --indexOr index explicit roots:
cds --index ~/Projects ~/workRun the indexing daemon in the foreground:
cds --daemonKill existing cds daemons and start a fresh background daemon:
cds --restart-daemonThe daemon keeps the embedding model loaded, polls configured roots for filesystem changes,
and reindexes changed roots without requiring another manual scan. Generated file chunk
embeddings are also recorded in an append-only history table keyed by file fingerprint, so
search can still match earlier content versions after a file changes. On Unix platforms, the
daemon also opens a local socket under the cds cache directory; normal cds --search and
shell-integration searches use that socket when available so the query can reuse the
daemon's already-loaded embedding model. If the daemon is not running or the socket cannot be
created, searches fall back to the normal direct path.
The current indexer stores directory metadata and high-signal text-file content separately
in SQLite. Directory rows store structured filesystem metadata: name, type, parent path,
size in bytes, created time, modified time, accessed time, readonly status, and index time.
Files matching high_signal_files, such as READMEs and project descriptor files
(package.json, Cargo.toml, pyproject.toml, manifest.json, go.mod, dependency
manifests, and container manifests), are stored as file metadata plus embedded content
chunks linked back to their containing directories. Files outside that allowlist are not
content embedded by default, which keeps source code, arbitrary notes, and generated output
out of the vector index unless explicitly added.
Directory metadata itself is not embedded.
Hidden directories are always skipped and pruned from the local index. Each configured index
root is treated as a container, and each top-level directory inside it is indexed only through
max_depth_per_top_level_directory levels.
Text-file chunks are embedded locally with BAAI/bge-small-en-v1.5 through FastEmbed/ONNX.
Model files are cached under ~/.cache/cds/models by default, or under CDS_CACHE_DIR/models
when that environment variable is set. Tests can force deterministic fake embeddings with
CDS_EMBEDDER=fake.
SQLite remains the durable source of indexed data. Embeddings are kept as packed f32 BLOBs
for exact reranking and compatibility, and mirrored into per-dimension sqlite-vec vec0
tables for nearest-neighbor candidate selection. Existing databases are backfilled into
the sqlite-vec tables on open after migrations run.
Directory type inference is rule-based and data-driven. cds does not ask the embedding
model to guess whether a directory is a rust project, chrome extension, rails app, etc.
Instead, it runs JSON detectors against each indexed directory during cds --init and
cds --index.
Built-in detectors live in src/index/directory_types/ and currently cover:
.net projectandroid appastro sitechrome extensiondatabase migrationsdjango appdockerized appgo projectios appjava projectkubernetes configlaravel appmonoreponext.js appnode projectpython projectrails appreact apprust projectsveltekit appterraform projectvite app
Each detector has a label and one or more rules. A rule matches only when all of its
signals match. If a detector has multiple matching rules for the same directory, cds
keeps one classification for that label and prefers the highest-confidence rule. Each stored
classification includes the directory path, label, confidence, rule id, evidence path,
evidence summary, and detection timestamp.
Custom detectors are added to the detectors array in ~/.config/cds/config.json.
cds --index loads that array every time it indexes, so editing the config and re-running
cds --index is enough to apply a new directory type locally.
Example detector:
{
"label": "rust project",
"rules": [
{
"id": "cargo_toml_package",
"confidence": 0.98,
"evidence_summary": "Cargo.toml contains [package] or [workspace]",
"signals": [
{
"kind": "file_contains",
"path": "Cargo.toml",
"contains_any": ["[package]", "[workspace]"]
}
]
}
]
}Supported signal kinds:
file_exists: matches when a relative file path exists inside the directory.file_contains: reads a small text file and checks for text. Usecontains_any,contains_all, or both.directory_name: checks the directory's own name withequals_anyand/orcontains_any.child_name: checks immediate child names withcontains_any,starts_with_any, and/orends_with_any.child_file_contains: reads matching immediate child text files and checks their text. Usename_contains_any,name_starts_with_any, and/orname_ends_with_anyto select child files, pluscontains_anyand/orcontains_allfor content.
Signals are case-insensitive. Paths are relative to the directory being classified. Content-reading signals ignore binary files and files larger than 128 KiB.
For example, a custom detector for documentation projects can be added directly to
~/.config/cds/config.json:
{
"index": {
"roots": ["~/Projects"],
"exclude": [
".git",
"Applications",
"Library",
"Network",
"System",
"Volumes",
"cores",
"private",
"node_modules",
"target",
"dist",
"build",
".next",
".cache",
".venv",
"venv",
"vendor",
"*.xcassets",
"*.imageset",
"*.appiconset",
"*.colorset"
],
"high_signal_files": [
"README*",
"Cargo.toml",
"package.json",
"manifest.json",
"pyproject.toml",
"requirements.txt",
"go.mod",
"Gemfile",
"Dockerfile",
"docker-compose.yml",
"compose.yml",
"compose.yaml",
"tsconfig.json"
],
"max_file_bytes": 65536,
"max_excerpt_bytes": 4096,
"max_entries_per_directory": 80,
"max_depth_per_top_level_directory": 3,
"max_chunk_bytes": 4096
},
"detectors": [
{
"label": "documentation project",
"rules": [
{
"id": "mkdocs_config",
"confidence": 0.9,
"evidence_summary": "mkdocs.yml exists",
"signals": [
{
"kind": "file_exists",
"path": "mkdocs.yml"
}
]
}
]
}
]
}After saving the config:
cds --index
cds --dir-type-countTo add a built-in/community detector to the project itself, add a new JSON file under
src/index/directory_types/, then register it in BUILTIN_DEFINITIONS in
src/index/classify.rs. Prefer high-confidence signals that are difficult to trigger by
accident. For example, checking that manifest.json contains "manifest_version" and at
least one browser-extension field is better than classifying every directory with any
manifest.json as a Chrome extension.
Search indexed directories:
cds --search chrome extensionList detected directory types:
cds --dir-type-countRestart the background daemon:
cds --restart-daemonDelete all indexed data from the SQLite database:
cds --resetThis prompts before deleting directory metadata, file metadata, current and historical content chunks, and directory type classifications. The database file and schema are kept in place.
When the shell integration is installed, cds also tries semantic search automatically for
plain directory changes that do not look like local cd usage. For example, cds Projects
first checks whether ./Projects exists exactly. If it does, cds delegates to the shell's
built-in cd exactly as usual. If it does not, cds searches the index and emits a cd to
the best existing indexed directory. Flags, -, --, ~, ., .., and paths containing
/ always use normal cd behavior.
Install the shell integration in your current shell:
eval "$(command target/debug/cds --shell-init zsh)"For bash:
eval "$(command target/debug/cds --shell-init bash)"After that, use cds like cd:
cds
cds -
cds -P ../somewhere
cds -- -directory-starting-with-dashTo make this permanent after cds is installed somewhere on PATH, add the appropriate
line to your shell profile:
eval "$(command cds --shell-init zsh)"