Project Registry
TeaRAGs keeps a per-machine registry of indexed projects at
$TEA_RAGS_DATA_DIR/registry.json (default ~/.tea-rags/registry.json). The
registry stores two layers in one file:
- Collection metadata — auto-populated at the end of every indexing run:
resolved
collectionName, indexed path, embedding model and dimensions, Qdrant URL,indexedAttimestamp, tea-rags version, chunk count. - Project name — an optional, user-controlled short alias. Once registered,
the name resolves to the project's path and collection so MCP tools and CLI
commands can reference the project by name instead of by
pathorcollection.
The registry is the source of truth for project path resolution. The fallback chain is:
collection > project > path
Passing none of the three is an error — the MCP server does not guess from
cwd (it has its own working directory unrelated to the client's). The CLI
defaults --path to process.cwd().
File Format
registry.json is a single JSON document:
{
"version": 1,
"collections": {
"code_8f42a1b3": {
"path": "/Users/alice/projects/shop-backend",
"name": "shop-backend",
"embeddingModel": "Xenova/all-MiniLM-L6-v2",
"embeddingDimensions": 384,
"qdrantUrl": "http://localhost:6333",
"indexedAt": "2026-05-12T14:21:08.231Z",
"teaRagsVersion": "0.42.1",
"chunksCount": 12345
}
}
}
Key rules:
- The entry key is the collection name (
code_<md5>[0..8]), not the project name — see Collections. nameis unique across all entries. Attempting to register the same name to two projects throwsProjectNameNotUniqueError.namematches the regex^[a-z0-9][a-z0-9_-]{0,63}$(lowercase letters, digits,-,_; max 64 chars). Safe to use as a CLI argument and path fragment.nameis sticky across reindex — re-indexing a project preserves its registered name while overwriting all other fields.- Writes are atomic (write to
.tmp.<pid>, then rename) AND protected by a compare-and-swap (CAS) retry loop — 5 attempts with exponential backoff (10ms → 160ms). The CAS read-modify-write protects parallel tea-rags processes from clobbering each other's entries; exhausting the budget throwsRegistryConcurrencyError(INFRA_REGISTRY_CONCURRENCY). - The long-lived MCP server installs a directory-level
fs.watchon$TEA_RAGS_DATA_DIRso external writes (an indexing run from a parallel CLI process,tea-rags projects registerfrom a shell,tea-rags doctor --recover-registry) invalidate the in-memory cache without an MCP reconnect.
Loading and migrations
loadRegistryFile reads registry.json through a small migration framework:
- A missing file is fine — the in-memory map starts empty and gets written
on first
register_project. - For
version: 1(current), the file is loaded as-is. - For older
versionvalues, the file is passed throughKNOWN_MIGRATIONS[version]if a transformer is registered. The reserved framework is in place; no migrations are registered yet because v1 is the initial schema. - A separate, finer-grained
revisionfield on the sameversion: 1file drives a one-time data migration: every env pin whose value equals the current code default for its key (and anyTRAJECTORY_GIT_CHUNK_MAX_FILE_LINES=10000pin left over from before it defaulted to5000) is dropped, once, on first load. Before this, every index run pinned its FULL resolved env — every code default materialized — so an entry froze the defaults of the release that first indexed it and a later default change reached no project that had already been indexed. Non-default pins and keys outside the default snapshot (backend URLs, etc.) are kept unchanged. One line per affected project reports which keys were dropped. - Any other case (JSON parse failure, malformed shape, unsupported version)
is treated as corruption: the bad file is renamed to
registry.json.corrupt-<ISO>.bakso it is recoverable by hand, andRegistryFileCorruptedError(INFRA_REGISTRY_FILE_CORRUPTED) is thrown. The next boot sees no file and starts with an empty map; in this statetea-rags doctor --recover-registrycan repopulate stubs directly from Qdrant (see Doctor and Recovery).
MCP Tools
register_project
Associate a short alias with an absolute project path.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | yes | Absolute path to project root |
name | string | yes | Short alias, regex ^[a-z0-9][a-z0-9_-]{0,63}$ (max 64 chars) |
Returns:
{
"collectionName": "code_8f42a1b3",
"alreadyIndexed": true
}
collectionName— collection the alias now points at (deterministic frompath).alreadyIndexed—trueif the underlying collection already contains chunks.falsefor fresh registrations on never-indexed paths.
Example:
{
"path": "/Users/alice/projects/shop-backend",
"name": "shop-backend"
}
Errors (all mapped to HTTP 400 — InputValidationError subclasses):
ProjectNameInvalidError(INPUT_PROJECT_NAME_INVALID) — name empty, longer than 64 chars, or fails the regex.PathDoesNotExistError(INPUT_PATH_NOT_EXISTS) —pathdoes not exist on disk.ProjectNameNotUniqueError(INPUT_PROJECT_NAME_NOT_UNIQUE) — another entry already owns this name. The infra layer raisesRegistryNameConflictError(INFRA_REGISTRY_NAME_CONFLICT) as a defensive backstop if a caller bypasses the api-layer pre-check.
list_projects
Read-only. Lists every registered project with full collection metadata. No parameters.
Returns:
{
"projects": [
{
"collectionName": "code_8f42a1b3",
"name": "shop-backend",
"path": "/Users/alice/projects/shop-backend",
"embeddingModel": "Xenova/all-MiniLM-L6-v2",
"embeddingDimensions": 384,
"qdrantUrl": "http://localhost:6333",
"indexedAt": "2026-05-12T14:21:08.231Z",
"teaRagsVersion": "0.42.1",
"chunksCount": 12345
}
]
}
Entries without a registered name show name: null.
unregister_project
Remove a registry entry by name or by path — pass exactly one. Idempotent
— returns removed: false when the project was not registered. Does not
delete the underlying Qdrant collection or any indexed chunks.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | one of the two | Project name to remove |
path | string | one of the two | Project root it was registered at. Reaches projects indexed by path that have no name |
Passing both, or neither, is rejected with INPUT_INVALID_PARAMETER /
INPUT_MISSING_ARGUMENT.
Returns:
{ "removed": true }
The MCP unregister_project tool has no purge parameter — destructive
removal of the Qdrant collection is exposed only via the CLI
(tea-rags projects unregister --name <alias> --purge, see
CLI Commands). The CLI --purge also works after an MCP
unregister_project, addressed by --path <root> or --collection <code_hash>.
From an MCP client, follow unregister_project with clear_index or
delete_collection to remove the chunks.
The project parameter on other tools
Once a project is registered, the alias can be passed as project to any
project-aware tool instead of path or collection. Resolution priority is
strict: collection > project > path. Passing several is allowed; the first
non-empty one wins.
{ "project": "shop-backend", "query": "payment retry" }
Resolves through the registry to the project's collection and path, then runs
the query exactly as if path had been supplied.
If project does not exist in the registry the call throws
ProjectNotRegisteredError (INPUT_PROJECT_NOT_REGISTERED). If the entry
exists but its path is an empty string — the shape produced by
tea-rags doctor --recover-registry for a Qdrant collection that has no
matching directory yet — the call throws ProjectPathMissingError
(INPUT_PROJECT_PATH_MISSING), with a hint telling the user to run
tea-rags projects register --path <dir> --name <alias> to fill the path
back in.
The MCP server watches $TEA_RAGS_DATA_DIR with fs.watch at the
directory level, so registry changes made by external CLI invocations
(tea-rags projects register, tea-rags projects unregister,
tea-rags doctor --recover-registry) become visible to in-flight tool
calls without an MCP reconnect. The directory-level watch survives the
atomic .tmp.<pid> -> rename cycle that a file-level watch would miss.
CLI Commands
The same operations are available from the shell, useful in scripts and CI.
All project-registry operations live under the tea-rags projects command
group. Running tea-rags projects with no subcommand is equivalent to
tea-rags projects list.
tea-rags projects register
tea-rags projects register --path /Users/alice/projects/shop-backend --name shop-backend
Both --path and --name are required. On success prints
Registered '<name>' -> <collectionName> (and (already indexed) when the
collection already had chunks).
--env KEY=VALUE (repeatable) stores per-project env with the alias, the same
env tea-rags projects set-env edits (below). The env is validated before
anything is written, so a rejected key registers nothing:
tea-rags projects register --path /src/shop-backend --name shop-backend \
--env CODEGRAPH_ENABLED=true --env INGEST_CHUNK_SIZE=2500
tea-rags projects set-env / unset-env
tea-rags projects set-env --name shop-backend CODEGRAPH_ENABLED=true TRAJECTORY_GIT_LOG_MAX_AGE_MONTHS=24
# Set env of 'shop-backend': CODEGRAPH_ENABLED=true TRAJECTORY_GIT_LOG_MAX_AGE_MONTHS=24
tea-rags projects unset-env --name shop-backend TRAJECTORY_GIT_LOG_MAX_AGE_MONTHS
Every index run of a project replays the env its registry entry stores, with
the precedence outer env > registry env > code default. These two commands
edit that stored env as configuration, without running an index. The change
applies from the project's next index run. A key that shapes the indexed data
(chunking, history windows, codegraph) reaches existing chunks only through the
matching reindex.
- Accepted keys are the ones an index run reads per project: the canonical
names and their deprecated spellings. A deprecated spelling is stored under its
canonical name, and any other spelling of the same key is dropped. An unknown
key fails with
INPUT_PROJECT_ENV_KEY_UNKNOWN. Secrets (API keys) and server/process settings are never stored per project. - Values are checked against the same config schema the index run parses
them with (
INGEST_CHUNK_SIZE=abcorGIT_ADAPTER=clifails withINPUT_INVALID_PARAMETER). An empty value is rejected; useunset-envto go back to the default. CODEGRAPH_ENABLED,EMBEDDING_BASE_URL,EMBEDDING_FALLBACK_URLare stored in their dedicated entry fields (codegraphEnabled,embeddingBaseUrl,embeddingFallbackUrl), which is where replay reads them.EMBEDDING_MODELandQDRANT_URLare refused. They record what the existing index was built with and where it lives, so only the index run writes them. To change either, re-index with the variable exported.unset-envtakes any spelling and removes every spelling of that key.
An ambient variable set when an index run starts still beats the stored value, following the precedence rule above. Under the long-lived MCP server, only its runtime settings (endpoints, pool and batch sizes) take precedence this way.
An index run only pins env keys its own environment set explicitly — not
the whole resolved snapshot with every code default filled in. A key the run
left to its default is not pinned, so a later change to that default reaches
the project on its next index run instead of staying frozen at whatever the
first index happened to ship with. Separately, indexing a genuinely new
project no longer borrows the full env of whatever project was indexed most
recently: the seed narrows to the backend keys (EMBEDDING_*, OLLAMA_*,
QDRANT_*) that make a fresh index reach the same embedding service and
Qdrant instance, never another repository's git windows, chunking, test paths
or codegraph policy. The project's own path and a sibling git worktree of the
same repository still replay their own full env.
tea-rags projects list
tea-rags projects list
# (no projects registered) # when empty
# shop-backend code_8f42a1b3 /Users/alice/projects/shop-backend
Add --json for a machine-readable dump of the full registry entries. tea-rags projects (no subcommand) is an alias for tea-rags projects list.
tea-rags project exist
A scriptable membership check — answers "is this path or alias a registered
project?" via the exit code, with no human-formatted output by default. Note the
singular project (this query command) versus the plural projects command
group above.
tea-rags project exist --path /path/to/repo # exit 0 = registered, 1 = not
tea-rags project exist --name shop-backend # check by alias instead
tea-rags project exist --path /path/to/repo --print-name # also print the alias on match
tea-rags project exist --path /path/to/repo --json # {"exists":true,"name":"shop-backend"}
Exactly one of --path or --name is required. The command is built for hooks
and CI: the auto-reindex hook uses
tea-rags project exist --path <dir> --print-name to resolve a commit directory
to its collection (and to skip directories that are not registered projects).
tea-rags projects info
tea-rags projects info --name shop-backend
# name: shop-backend
# collectionName: code_8f42a1b3
# path: /Users/alice/projects/shop-backend
# qdrantUrl: http://127.0.0.1:6333
# embeddingModel: unclemusclez/jina-embeddings-v2-base-code:latest
# embeddingDimensions: 768
# chunksCount: 3832
# indexedAt: 2026-05-13T01:15:45.019Z
# teaRagsVersion: 1.24.0
# codegraphEnabled: true
# env:
# INGEST_CHUNK_SIZE=2500
# TRAJECTORY_GIT_ENABLED=true
embeddingBaseUrl, embeddingFallbackUrl and codegraphEnabled appear when
the entry has them. The env: block lists the stored per-project env, one
KEY=VALUE per line. After an index run it holds the run's full effective env,
defaults included. Add --json for a machine-readable single-entry dump. Exits 1 with
'<name>' was not registered on stderr when the name is unknown.
Realpath divergence and missing paths. projects info calls
realpathSync(entry.path) and renders the result on a dedicated line when
the live filesystem disagrees with the stored value:
| Situation | Text-mode output | JSON output |
|---|---|---|
Stored path resolves to itself | (no realpath line) | no realpath field |
Stored path resolves to a different absolute path (symlink retargeted, mount moved) | realpath: line with the resolved path plus a hint to re-register | realpath field set to the resolved path |
Stored path is gone from disk entirely | realpath: (missing on disk) | no realpath field (sentinel is render-only) |
When indexedAt is missing (e.g. a recovered registry stub from
tea-rags doctor --recover-registry where Qdrant had no indexing marker),
text mode renders (never). The earlier behaviour of synthesising
indexedAt = new Date() during enrichment has been removed — an empty
field stays empty.
tea-rags projects unregister
tea-rags projects unregister --name shop-backend
# Removed 'shop-backend' from registry. Note: Qdrant collection 'code_8f42a1b3' is still present.
# Run 'tea-rags projects unregister --path /src/shop-backend --purge' to remove it.
Address the project by --name <alias>, by --path <project root>, or by
--collection <code_hash> — exactly one. index-codebase <path> registers a
project without a name, so --path reaches such an entry; the path is
resolved the same way registration resolves it (trailing slash, .., and
symlinked spellings all match). A nameless entry is named by its path in the
output.
By default it touches only the registry — the underlying Qdrant collection (and
its indexed chunks) is preserved, and the message tells you how to remove it.
The hint never repeats --name: the name is gone once the entry is. It prints
--path when the path still derives the collection, and --collection
otherwise (an alias re-pointed at a new path, a worktree clone, a recovered stub
with no path). Following the hint works after the entry is gone: --purge
on an address no entry claims tears down the leftover footprint the path or
collection names:
tea-rags projects unregister --path /src/shop-backend --purge
# '/src/shop-backend' was already unregistered; purged its leftovers; deleted Qdrant collection 'code_8f42a1b3' (3832 chunks)
It never purges a collection that is still registered, or a _vN generation of
one. Idempotent: it exits 0 with '<address>' was not registered when no entry
matches and there is nothing to purge.
Pass --purge to also delete everything the collection owns on disk:
tea-rags projects unregister --name shop-backend --purge
# Removed 'shop-backend' from registry; deleted Qdrant collection 'code_8f42a1b3' (3832 chunks)
# qdrant: code_8f42a1b3_v1, code_8f42a1b3_v2
# codegraph: code_8f42a1b3_v1, code_8f42a1b3_v2
# cleared: quarantine, snapshot, stats
# kept: project directory /src/shop-backend — the source tree is never touched
The name in the registry is an alias, and most of a project's state is
addressed by the versioned code_<hash>_vN name behind it. --purge resolves
both, so it removes:
| Artifact | Where it lives |
|---|---|
Every Qdrant generation code_<hash>_vN | Qdrant |
| The alias itself | Qdrant |
Every codegraph database (plus .wal sidecar) | ~/.tea-rags/codegraph/<collection>_vN.duckdb |
| The file-hash snapshot directory | ~/.tea-rags/snapshots/<collection>/ |
| The collection stats cache | ~/.tea-rags/snapshots/<collection>.stats.json |
| The poison-pill quarantine file | ~/.tea-rags/snapshots/<collection>.quarantine.json |
A codegraph database whose Qdrant collection is already gone is reclaimed too — those are the files that otherwise accumulate forever after an interrupted force reindex.
Anything under kept: is left alone deliberately: the project directory, any
worktree clone derived from this project (each owns its own footprint, so remove
it with tea-rags worktree remove <name>), and the shared codegraph daemon,
which is never shut down because other projects may be using it.
Every step is best-effort. If one fails — server unreachable, a file held open — the registry entry is still removed, the rest of the sweep still runs, and the report names what survived so it can be retried:
Removed 'shop-backend' from registry; failed to delete Qdrant collection 'code_8f42a1b3_v2': <reason>
qdrant: code_8f42a1b3_v1
cleared: quarantine, snapshot, stats
failed: qdrant code_8f42a1b3_v2 — <reason>
For non-destructive inspection of Qdrant collections that no longer have a registry entry, see Doctor and Recovery.
--project flag on other commands
CLI commands that operate on a project accept --project <name> as a shortcut
that resolves --path, --qdrant-url, and --model from the registry entry.
Explicit flags always win — --project only fills in what was not passed.
tea-rags tune --project shop-backend
# resolves --path, --qdrant-url, --model from the registry entry
Resolution is performed by applyProjectDefaults, which throws typed
InputValidationError subclasses (it does not call process.exit itself —
the CLI command wraps the call and prints message + Hint to stderr):
ProjectNotRegisteredError(INPUT_PROJECT_NOT_REGISTERED) — the alias is unknown. Stderr lists the available aliases (or(none)).ProjectPathMissingError(INPUT_PROJECT_PATH_MISSING) — the alias exists but itspathis empty (recovered stub). The hint tells the user to runtea-rags projects register --path <dir> --name <alias>.
Empty-string embeddingModel / qdrantUrl values on a recovered stub are
coerced to undefined before nullish-coalesce, so downstream commands fall
through to their own defaults instead of being poisoned with "".
Shell completion
tea-rags ships with tab-completion that suggests registered project aliases
for --project <TAB>, tea-rags projects info --name <TAB>, and
tea-rags projects unregister --name <TAB>. The register subcommand
deliberately does NOT complete --name — there the value is a new alias the
user is inventing.
| Shell | Install |
|---|---|
| fish | Auto-installed by postinstall when fish is detected on PATH. Re-runs on every package update. Manual cleanup: rm ~/.config/fish/completions/tea-rags.fish |
| bash | tea-rags completion >> ~/.bashrc (one-time) |
| zsh | tea-rags completion >> ~/.zshrc (one-time) |
For bash/zsh, source the rc file (or open a new shell) after installing.
Fish auto-discovers completions from ~/.config/fish/completions/ — no rc
edit needed.
After indexing and registering a project, --project followed by TAB will
autocomplete with the alias names from ~/.tea-rags/registry.json.
Doctor and Recovery
The tea-rags doctor command is a read-only health summary plus an
opt-in recovery path that rebuilds the registry from live Qdrant state. It
is a CLI-only command — there is no MCP equivalent, by design (destructive
or repair operations should never be one MCP call away).
tea-rags doctor
tea-rags doctor
# [OK] Qdrant: http://127.0.0.1:6333
# [OK] Embeddings (ollama): http://127.0.0.1:11434
# [OK] Registry: 3 project(s)
# [WARN] Registry: 2 orphan collection(s) — run 'tea-rags doctor --recover-registry' or 'tea-rags projects orphans' to inspect
The summary lines are:
| Line | Meaning |
|---|---|
Qdrant | URL the same bootstrap path as the MCP server resolves to (embedded daemon socket or QDRANT_URL). [OK] / [FAIL] reflects the checkHealth probe. |
Embeddings | Provider name (ollama, onnx, openai, ...), base URL where applicable, and reachability via the provider's own checkHealth. |
Registry | Count of registered projects. A [WARN] line follows per finding: entries whose directory is gone (projects prune), and orphan collections (defined below). |
Add --json for a machine-readable dump:
{
"qdrant": { "url": "http://127.0.0.1:6333", "reachable": true },
"embeddings": { "provider": "ollama", "url": "http://127.0.0.1:11434", "reachable": true },
"registry": { "projectCount": 3, "orphanCount": 2, "staleCount": 1 }
}
The [FAIL] lines never crash doctor — the command is read-only and a
failed probe is a legitimate finding. Doctor never spawns the embedded
Qdrant daemon (connection refused is a real symptom worth reporting).
tea-rags doctor --recover-registry
When the registry has been wiped, corrupted, or you've moved
registry.json between machines, this flag repopulates entries from live
Qdrant state. Doctor calls ProjectRegistryOps.recoverFromQdrant, which
walks listCollections() and inserts a registry entry for every Qdrant
collection that does not already have one.
tea-rags doctor --recover-registry
# [OK] Qdrant: http://127.0.0.1:6333
# [OK] Embeddings (ollama): http://127.0.0.1:11434
# [OK] Registry: 5 project(s)
# [OK] Recovered 2 entry/entries from Qdrant; paths are empty — re-register them with 'tea-rags projects register --path <dir> --name <alias>' to enable alias resolution.
Recovered entries have path: "". Qdrant stores the collection name,
the chunk count, and the indexing metadata, but it does not store the
original filesystem path. Recovered stubs are fully usable for direct
collection operations (semantic_search with collection: code_…), but
attempting to use them as an --project <alias> shortcut throws
ProjectPathMissingError until the user re-registers with the real path.
The recovered set is also visible in the JSON output as a recovery
field, useful for scripts:
{
...,
"recovery": { "recovered": 2 }
}
tea-rags doctor --sweep-workers
tea-rags index-codebase hands the actual indexing to a detached worker
process. When the foreground CLI is killed before it hands the worker off, the
worker normally notices and exits with its children. A worker that did not —
one from an older build, or one wedged past reacting — keeps holding the
collection, and every later run on it is refused with
INGEST_INDEXING_IN_PROGRESS. This flag finds and stops such workers.
tea-rags doctor --sweep-workers
# [KILL] pid 51136 orphaned — supervisor 51120 died before handing it off; stopped · /Users/me/project
# [WARN] pid 48211 stalled — no progress for 42m; re-run with --include-stalled to stop it · /Users/me/other
# [OK] pid 47001 gone — stale record removed
# Swept 3 worker record(s): 1 stopped, 1 stale record(s) removed.
Each worker registers itself under ~/.tea-rags/workers/ while it runs. The
sweep stops a process only when it can prove the pid is still that worker —
its command line is an index-codebase worker that started when the record
says — and its CLI is gone. A pid now held by some other process only loses
its stale record; it is never signalled. Stopping sends SIGTERM, then
SIGKILL, to the worker's own process group, which holds its git and
chunker children and none of the shared daemons.
| Verdict | Meaning | Action |
|---|---|---|
attached | Its CLI is still running. | Kept |
detached | Handed off to finish enrichment in the background, still progressing. | Kept |
orphaned | Its CLI died before handing it off. | Stopped |
stalled | Handed off, but no progress for 30 minutes. | Kept; stopped with --include-stalled |
unverified | An index-codebase worker holds the pid, but ps gave a start time the sweep could not read, so it cannot prove this is the worker that registered. | Kept, record and process |
gone | No such worker any more. | Record removed |
--include-stalled is opt-in because a long daemon-side phase (cycles and
PageRank on a large graph) reports no progress either. --dry-run reports
what would be stopped without stopping or removing anything, and --json
emits one entry per worker. Not available on Windows. npm run build runs the
same sweep, limited to orphaned workers of the checkout it rebuilds.
tea-rags projects orphans
Read-only listing of Qdrant collections that have no registry entry. This
is the inspection counterpart to doctor --recover-registry:
tea-rags projects orphans
# COLLECTION CHUNKS
# code_a1b2c3d4 4.2k
# code_55667788 931
The text output is an aligned table with compact chunk counts, colored on a
terminal (NO_COLOR turns it off). Add --json for a machine-readable list
of { collectionName, chunksCount } records with exact counts — script
against that, not the table.
Alias-aware. Qdrant's zero-downtime reindex builds versioned physical
collections (e.g. code_8b243ffe_v2) that are pointed to by an alias
(code_8b243ffe). The orphan listing subtracts the alias targets from
listCollections() so a live backing collection never appears as orphan
data the user might be tempted to delete. The same filter is applied to
the orphanCount reported by tea-rags doctor — the two views agree.
tea-rags projects prune
The inverse of orphans: registry entries whose project directory is gone
from disk. They accumulate on their own — a removed worktree, a deleted test
fixture, a checkout moved to another machine — and each one may still own a
Qdrant collection and a codegraph database.
By default the command is a dry run. It prints one line per stale entry
(<collectionName>\t<alias>\t<path>\t<chunks>\t<what happens to it>) and
changes nothing:
tea-rags projects prune
# code_a1b2c3d4 (no alias) /tmp/fixture-42 1234 would remove
# code_55667788 moved /old/worktree 57 kept — re-register the alias at its new path, or run 'tea-rags projects unregister --name moved --purge'
# Dry run — nothing removed. Re-run 'tea-rags projects prune --purge' to remove 1 prunable entry and the Qdrant/codegraph footprint behind it.
A NAMED stale entry is never removed. register re-points an alias at its
new path the moment the alias is registered there, and the index behind it
survives the move — so removing it would be the destructive answer to a
recoverable situation. Only entries that never got an alias are swept: nothing
addresses them again. Each kept entry prints the route back:
| Kept entry | Hint |
|---|---|
| Plain alias | re-register it at the new path, or projects unregister --purge it |
| Worktree clone | tea-rags worktree remove <worktreeName> --force |
The worktree clone gets its own route because worktree remove also drops the
git worktree admin entry in the source repo, which unregister --purge would
leave dangling.
--purge acts. For each prunable entry it tears down the whole footprint
first — every Qdrant generation, every codegraph DuckDB generation, the
snapshot, the stats cache, the quarantine file — and removes the registry entry
only when that came back clean:
tea-rags projects prune --purge
# code_a1b2c3d4 (no alias) /tmp/fixture-42 1234 removed
# code_99887766 (no alias) /tmp/fixture-43 88 kept — purge failed: qdrant code_99887766 — ECONNREFUSED
# code_55667788 moved /old/worktree 57 kept — re-register the alias at its new path, or run 'tea-rags projects unregister --name moved --purge'
# Removed 1 · kept 2 (1 purge failed)
A failed purge leaves the registry entry in place, because the entry is what names the collection for the retry. One entry's failure never aborts the rest of the sweep, and the command exits 0 either way — a stale entry is a finding, not an error. When every attempted purge failed, the summary says so, which is the usual shape of "Qdrant is not running".
--json works in both modes and emits { stale, removed, kept }. Every entry
carries prunable, the verdict on whether the sweep may take it, so a script
never has to re-derive the rule:
{
"stale": [
{
"collectionName": "code_a1b2c3d4",
"name": null,
"path": "/tmp/fixture-42",
"chunksCount": 1234,
"indexedAt": "2026-09-01T00:00:00.000Z",
"prunable": true
}
],
"removed": [],
"kept": []
}
A dry run decides nothing, so it reports nothing as removed or kept — the
prunable flag is the preview. Under --purge, removed holds what went and
kept holds what stayed (named entries plus anything whose purge failed).
tea-rags doctor counts stale entries and points here:
[WARN] Registry: 2 stale (missing directory) → tea-rags projects prune
tea-rags projects unregister --purge
Documented in detail under CLI Commands → projects
unregister; included here for the
recovery checklist. Combined with tea-rags projects orphans it gives a
two-step "find then remove" workflow for collections that should not
exist any more.
Storage Location
The registry path follows the same convention as the rest of the per-machine data:
$TEA_RAGS_DATA_DIR/registry.json
When TEA_RAGS_DATA_DIR is not set it defaults to ~/.tea-rags/. See
Configuration Variables → Data Directories
for the full layout (snapshots/, logs/, qdrant/, registry.json).
See Also
- Collections — how
collectionNameis derived frompath - MCP Tools Atlas — every project-aware tool accepts
project - Configuration Variables —
TEA_RAGS_DATA_DIRand data directory layout