Skip to main content

sqlite_graphrag/cli/
commands.rs

1//! The subcommand surface: every `Commands` variant and its classification.
2//!
3//! Holds the `Commands` enum, the predicates `main` uses to route a variant,
4//! and the manual `Debug` impl.
5
6use crate::commands::*;
7use clap::Subcommand;
8
9/// Every subcommand the CLI dispatches, in the order `--help` renders them.
10#[derive(Subcommand)]
11pub enum Commands {
12    /// Initialize the database and write the schema (no model download, no subprocess)
13    #[command(after_long_help = "EXAMPLES:\n  \
14        # Initialize in current directory (default behavior)\n  \
15        sqlite-graphrag init\n\n  \
16        # Initialize at a specific path\n  \
17        sqlite-graphrag init --db /path/to/graphrag.sqlite\n\n  \
18        # Persist default db path via XDG config (no product env)\n  \
19        sqlite-graphrag config set db.path /data/graphrag.sqlite\n  \
20        sqlite-graphrag init\n\n\
21        NOTES:\n  \
22        - `init` is OPTIONAL: any subsequent CRUD command auto-initializes graphrag.sqlite if missing.\n  \
23        - As a side effect, `init` warms a smoke-test embedding via the LLM-only one-shot pipeline.")]
24    Init(init::InitArgs),
25    /// Save a memory with optional entity graph
26    #[command(after_long_help = "EXAMPLES:\n  \
27        # Inline body\n  \
28        sqlite-graphrag remember --name onboarding --type user --description \"intro\" --body \"hello\"\n\n  \
29        # Body from file\n  \
30        sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-file ./README.md\n\n  \
31        # Body from stdin (pipe)\n  \
32        cat README.md | sqlite-graphrag remember --name doc1 --type document --description \"...\" --body-stdin\n\n  \
33        # Enable automatic URL extraction (URL-regex only since v1.0.79)\n  \
34        sqlite-graphrag remember --name rich --type note --description \"...\" --body \"...\" --enable-ner\n\n  \
35        # Positional name, the same form edit/read/forget/history accept\n  \
36        sqlite-graphrag remember onboarding --type user --description \"intro\" --body \"hello\"\n\n\
37        NOTES:\n  \
38        - The name may be given positionally OR via --name, never both.\n  \
39        - Pick exactly one body source: --body, --body-file, --body-stdin or --graph-stdin.\n  \
40        - --graph-file combines with any of the first three.\n\n\
41        ENTITY TYPES (for graph entities, NOT the memory --type):\n  \
42        The entity vocabulary is OPEN since v1.2.8: any label is stored as\n  \
43        you write it, and none is ever rewritten into another.\n  \
44        RECOMMENDED labels: concept, tool, person, file, project, decision,\n  \
45        incident, organization, location, date, dashboard, issue_tracker,\n  \
46        memory. A label outside them is accepted and reported in the\n  \
47        response `warnings` array, never substituted.\n  \
48        Pass --strict-entity-types to refuse anything outside the thirteen.\n  \
49        Shape is still checked: a label cannot be empty, digits only,\n  \
50        contain a line break, or exceed 64 characters.\n  \
51        The memory --type is a DIFFERENT and CLOSED vocabulary (user,\n  \
52        feedback, project, reference, decision, incident, skill, document,\n  \
53        note); the three names in both mean different things.\n  \
54        Inspect what your database actually uses: sqlite-graphrag graph entity-types\n  \
55        Wire contract: sqlite-graphrag schema --name graph-input")]
56    Remember(remember::RememberArgs),
57    /// Batch-create memories from NDJSON stdin (one invocation, one slot)
58    #[command(after_long_help = "EXAMPLES:\n  \
59        # Batch create from NDJSON\n  \
60        cat memories.ndjson | sqlite-graphrag remember-batch --force-merge --json\n\n  \
61        # Atomic batch\n  \
62        cat memories.ndjson | sqlite-graphrag remember-batch --transaction --json")]
63    RememberBatch(remember_batch::RememberBatchArgs),
64    /// Bulk-ingest every file under a directory as separate memories (NDJSON output)
65    Ingest(Box<ingest::IngestArgs>),
66    /// Search memories semantically
67    #[command(after_long_help = "EXAMPLES:\n  \
68        # Top 10 semantic matches (default)\n  \
69        sqlite-graphrag recall \"agent memory\"\n\n  \
70        # Top 3 only\n  \
71        sqlite-graphrag recall \"agent memory\" -k 3\n\n  \
72        # Search across all namespaces\n  \
73        sqlite-graphrag recall \"agent memory\" --all-namespaces\n\n  \
74        # Disable graph traversal (vector-only)\n  \
75        sqlite-graphrag recall \"agent memory\" --no-graph")]
76    Recall(recall::RecallArgs),
77    /// Read a memory by exact name
78    Read(read::ReadArgs),
79    /// List memories with filters
80    List(list::ListArgs),
81    /// Soft-delete a memory
82    Forget(forget::ForgetArgs),
83    /// Permanently delete soft-deleted memories
84    Purge(purge::PurgeArgs),
85    /// Rename a memory preserving history
86    Rename(rename::RenameArgs),
87    /// Split an oversized memory body into N child memories (v1.1.03, GAP-V8)
88    SplitBody(split_body::SplitBodyArgs),
89    /// Edit a memory's body or description
90    Edit(edit::EditArgs),
91    /// List all versions of a memory
92    History(history::HistoryArgs),
93    /// Restore a memory to a previous version
94    Restore(restore::RestoreArgs),
95    /// Search using hybrid vector + full-text search
96    #[command(after_long_help = "EXAMPLES:\n  \
97        # Hybrid search combining KNN + FTS5 BM25 with RRF\n  \
98        sqlite-graphrag hybrid-search \"agent memory architecture\"\n\n  \
99        # Custom weights for vector vs full-text components\n  \
100        sqlite-graphrag hybrid-search \"agent\" --weight-vec 0.7 --weight-fts 0.3")]
101    HybridSearch(hybrid_search::HybridSearchArgs),
102    /// Show database health
103    Health(health::HealthArgs),
104    /// Apply pending schema migrations
105    Migrate(migrate::MigrateArgs),
106    /// Resolve namespace precedence for the current invocation
107    NamespaceDetect(namespace_detect::NamespaceDetectArgs),
108    /// Run PRAGMA optimize on the database
109    Optimize(optimize::OptimizeArgs),
110    /// Show database statistics
111    Stats(stats::StatsArgs),
112    /// Create a checkpointed copy safe for file sync
113    SyncSafeCopy(sync_safe_copy::SyncSafeCopyArgs),
114    /// Back up the database using the SQLite Online Backup API
115    Backup(backup::BackupArgs),
116    /// Run VACUUM after checkpointing the WAL
117    Vacuum(vacuum::VacuumArgs),
118    /// Create an explicit relationship between two entities
119    Link(link::LinkArgs),
120    /// Remove a specific relationship between two entities
121    Unlink(unlink::UnlinkArgs),
122    /// Deep parallel multi-hop GraphRAG research
123    #[command(name = "deep-research")]
124    DeepResearch(deep_research::DeepResearchArgs),
125    /// List memories connected via the entity graph
126    Related(related::RelatedArgs),
127    /// Export a graph snapshot in json, dot or mermaid
128    Graph(graph_export::GraphArgs),
129    /// Export memories as NDJSON (one JSON line per memory, plus a summary line)
130    Export(export::ExportArgs),
131    /// FTS5 full-text search index management (rebuild or check)
132    Fts(fts::FtsArgs),
133    /// Vector index maintenance (orphan detection, purge, stats) — G39
134    Vec(vec::VecArgs),
135    /// Bulk-delete all relationships of a given type (e.g. mentions)
136    PruneRelations(prune_relations::PruneRelationsArgs),
137    /// Remove NER bindings (memory_entities rows) for an entity or all entities
138    #[command(name = "prune-ner")]
139    PruneNer(prune_ner::PruneNerArgs),
140    /// Inspect and manage cross-process LLM slot semaphore (GAP-004, v1.0.82)
141    Slots(slots::SlotsArgs),
142    /// Health and per-entry inspection of the pending-embeddings queue (GAP-005, v1.0.82)
143    Embedding(embedding::EmbeddingArgs),
144    /// Batch operations over the pending-embeddings queue (GAP-005, v1.0.82)
145    #[command(name = "pending-embeddings")]
146    PendingEmbeddings(pending_embeddings::PendingEmbeddingsArgs),
147    /// Remove entities that have no memories and no relationships
148    CleanupOrphans(cleanup_orphans::CleanupOrphansArgs),
149    /// List entities linked to a specific memory
150    MemoryEntities(memory_entities::MemoryEntitiesArgs),
151    /// Manage cached resources (embedding models, etc.)
152    Cache(cache::CacheArgs),
153    /// Delete an entity and all its relationships from the graph
154    #[command(name = "delete-entity")]
155    DeleteEntity(delete_entity::DeleteEntityArgs),
156    /// Reclassify one entity or a batch of entities to a new type
157    Reclassify(reclassify::ReclassifyArgs),
158    /// Rename an entity preserving all relationships and memory bindings
159    #[command(name = "rename-entity")]
160    RenameEntity(rename_entity::RenameEntityArgs),
161    /// Merge multiple source entities into a single target entity
162    #[command(name = "merge-entities")]
163    MergeEntities(merge_entities::MergeEntitiesArgs),
164    /// Enrich graph memories and entities using an LLM provider
165    Enrich(Box<enrich::EnrichArgs>),
166    /// Reclassify relationship types across the graph using rules or LLM judgment
167    #[command(name = "reclassify-relation")]
168    ReclassifyRelation(reclassify_relation::ReclassifyRelationArgs),
169    /// Normalize entity names (deduplicate, kebab-case, merge near-duplicates)
170    #[command(name = "normalize-entities")]
171    NormalizeEntities(normalize_entities::NormalizeEntitiesArgs),
172    /// Generate shell completions for Bash, Zsh, Fish, PowerShell, or Elvish
173    Completions(completions::CompletionsArgs),
174    /// List every shipped JSON Schema, or emit one by id (`--name <ID>`)
175    #[command(after_long_help = "EXAMPLES:\n  \
176        # Catalogue: one NDJSON record per contract\n  \
177        sqlite-graphrag schema\n\n  \
178        # One contract by id\n  \
179        sqlite-graphrag schema --name recall\n\n\
180        NOTES:\n  \
181        - Never opens the database and never requires an embedding API key.\n  \
182        - The per-subcommand `--print-schema` flags keep working unchanged.")]
183    Schema(crate::print_schema::SchemaArgs),
184    /// `debug-schema` subcommand.
185    #[command(name = "debug-schema", hide = true)]
186    DebugSchema(debug_schema::DebugSchemaArgs),
187    /// Manage API keys and diagnose provider configuration (v1.0.93)
188    Config(config_cmd::ConfigArgs),
189}
190
191impl Commands {
192    /// Names the subcommand for [`crate::agent_surface`] alias suppression.
193    ///
194    /// The suppression table used to match on the KEY alone, so `results` meant
195    /// the same thing everywhere. It does not: in `recall`, `results` really is
196    /// the concatenation of `direct_matches` and `graph_matches`, so dropping
197    /// the halves loses nothing. In `hybrid-search` the two arrays are DISJOINT
198    /// by construction — the graph expansion skips every id already fused — and
199    /// they do not even hold the same type. Suppressing there deleted unique
200    /// rows and then labelled them redundant, which is worse than losing them
201    /// silently: the envelope asserted the removal was safe.
202    ///
203    /// `None` for every subcommand that declares no alias, which makes the
204    /// default fail-safe: a new command is never suppressed until someone adds
205    /// it to the table deliberately.
206    /// GAP-SG-274: `graph` reports TWO slugs, because it emits two shapes.
207    ///
208    /// The NDJSON snapshot emits one self-contained record per line, discriminated
209    /// by a `kind` field valued `node`, `edge` or `summary`; every other form of
210    /// `graph` emits a single envelope in which `kind` is instead the deprecated
211    /// alias of an entity's `type`. One slug for both made the vocabulary layer
212    /// blind to a distinction [`Self::streams`] was already computing three
213    /// methods away, which is why a field name meaning two different things had
214    /// to be excluded everywhere rather than scoped where it is unambiguous.
215    #[must_use]
216    pub fn agent_surface_slug(&self) -> Option<&'static str> {
217        match self {
218            Self::List(_) => Some("list"),
219            Self::Graph(args) => Some(if Self::is_graph_ndjson(args) {
220                "graph-ndjson"
221            } else {
222                "graph"
223            }),
224            Self::Recall(_) => Some("recall"),
225            Self::Related(_) => Some("related"),
226            _ => None,
227        }
228    }
229
230    /// `true` when this invocation is the NDJSON snapshot form of `graph`.
231    ///
232    /// GAP-SG-274. Both [`Self::streams`] and [`Self::agent_surface_slug`] need
233    /// this answer, and before it was named they disagreed: one computed it, the
234    /// other ignored the args entirely. Asking the question in one place is what
235    /// stops the two from drifting apart again — the same argument the
236    /// [`Self::persists`] conjunction records for its own pair.
237    fn is_graph_ndjson(args: &graph_export::GraphArgs) -> bool {
238        !args.json
239            && args.subcommand.is_none()
240            && args.format == crate::cli::GraphExportFormat::Ndjson
241    }
242
243    /// `true` when this subcommand can change durable state.
244    ///
245    /// GAP-SG-205 reads it to decide whether the target database may be
246    /// inherited from ambient configuration; [`crate::agent_surface::gate`]
247    /// reads it to decide whether a refusal is still safe.
248    ///
249    /// The refusal question is the sharper one. The agent-native surface runs at
250    /// OUTPUT time, after the handler has already done its work, so refusing
251    /// there would hand the caller a non-zero exit for an operation that
252    /// succeeded — and a caller that retries a succeeded `remember` writes the
253    /// memory twice. The gate therefore stays silent on anything this reports as
254    /// mutating.
255    ///
256    /// Read-only variants are listed EXPLICITLY and everything else answers
257    /// `true`. The default has to be the conservative one: a subcommand added
258    /// later and forgotten here loses a refusal it might have wanted, which
259    /// costs a diagnostic, while the opposite default would let the gate fire
260    /// after an unlisted write, which costs data.
261    pub fn mutates(&self) -> bool {
262        match self {
263            Self::Recall(_)
264            | Self::Read(_)
265            | Self::List(_)
266            | Self::History(_)
267            | Self::HybridSearch(_)
268            | Self::Health(_)
269            | Self::NamespaceDetect(_)
270            | Self::Stats(_)
271            | Self::DeepResearch(_)
272            | Self::Related(_)
273            | Self::Export(_)
274            | Self::MemoryEntities(_)
275            | Self::Schema(_)
276            | Self::DebugSchema(_)
277            | Self::Completions(_) => false,
278            // `graph` is read-only in four of its five forms; `recompute-degree`
279            // rewrites the cached degree column.
280            Self::Graph(args) => matches!(
281                args.subcommand,
282                Some(crate::commands::graph_export::GraphSubcommand::RecomputeDegree(_))
283            ),
284            _ => true,
285        }
286    }
287
288    /// `true` when this subcommand actually persists, so its envelope is a receipt.
289    ///
290    /// GAP-SG-206. Neither half answers this on its own. [`Self::mutates`] lists
291    /// the read-only variants explicitly and answers `true` for all the rest, so
292    /// it reports `true` for `config list-keys`, `embedding list` and `fts check`,
293    /// which write nothing — right default for a refusal fence, wrong one for
294    /// withholding an answer. [`Self::may_inherit_target`] classifies exactly
295    /// those split families at the subcommand level.
296    ///
297    /// The conjunction is not a new list: `Cli::install_write_policy` already
298    /// asks precisely this question to decide whether the target must be named
299    /// in the argv, which is the same question — "did something get written".
300    /// Naming it once is what stops a third hand-written copy from drifting away
301    /// from the other two.
302    ///
303    /// That hook is a plain code span rather than an intra-doc link because it is
304    /// private, and `rustdoc::private_intra_doc_links` — denied in `Cargo.toml`
305    /// — rejects a link from public documentation to an item the public
306    /// documentation does not contain. `tests/rustdoc_link_gate.rs` now catches
307    /// that class, which `cargo test` and `cargo clippy` are both blind to.
308    pub fn persists(&self) -> bool {
309        self.mutates() && !self.may_inherit_target()
310    }
311
312    /// `true` when this subcommand emits one self-contained record per line.
313    ///
314    /// GAP-SG-209. [`crate::agent_surface::gate`] reads it to refuse the knobs
315    /// that need a complete set, because the surface runs once per emitted
316    /// envelope and a stream has no complete set by construction. Measured:
317    /// `--count-only export --limit 10` answered with eleven `{"count":1}` lines.
318    ///
319    /// The property belongs to the SUBCOMMAND and not to the emitting function,
320    /// which is the distinction that makes this a list rather than a flag on
321    /// `emit_json_compact`. That function is also how `config path`, `slots
322    /// release` and `embedding list` emit ONE envelope; keying the refusal off it
323    /// would have rejected `--count-only config path`, which is perfectly
324    /// answerable.
325    ///
326    /// Streaming variants are listed EXPLICITLY and everything else answers
327    /// `false`. The conservative default is the opposite of [`Self::mutates`]
328    /// here, and deliberately so: a subcommand added later and forgotten keeps
329    /// exactly today's behaviour, while the opposite default would refuse flags
330    /// on a command that can honour them perfectly well.
331    ///
332    /// GAP-SG-229 added `graph --format ndjson`, which had been streaming since
333    /// v1.0.35 without ever answering `true` here. The consequence was worse than
334    /// a missing refusal: `render_ndjson_streaming` returns before the surface
335    /// layer runs, so `--select`, `--filter`, `--sort` and `--dedupe-by` were
336    /// ACCEPTED and then IGNORED, with no refusal and no warning — the exact
337    /// shape of "flag aceita e silenciosamente ignorada" this project catalogues.
338    ///
339    /// The format has to be read from `args`, and it can be: this predicate
340    /// matches on the parsed arguments exactly as [`Self::mutates`] already does
341    /// for `graph recompute-degree`. GAP-SG-274 gave
342    /// [`Self::agent_surface_slug`] the same reach through the shared
343    /// `is_graph_ndjson` helper, so the two no longer disagree about which shape
344    /// of `graph` is in front of them. That helper is a plain code span rather
345    /// than an intra-doc link because it is private, and
346    /// `rustdoc::private_intra_doc_links` — denied in `Cargo.toml` — rejects a
347    /// link from public documentation to an item the public documentation does
348    /// not contain, exactly as [`Self::persists`] records for its own hook. The
349    /// `--json` override is mirrored from
350    /// `graph_export::handlers`, where it promotes the format to `Json` and turns
351    /// the streaming path off entirely; forgetting it here would refuse
352    /// whole-set knobs on an invocation that emits a single envelope.
353    ///
354    /// `dot` and `mermaid` stay outside: they are rendered text, not JSON, so
355    /// there is no record for a knob to act on.
356    pub fn streams(&self) -> bool {
357        match self {
358            Self::Export(_) | Self::Ingest(_) => true,
359            Self::Graph(args) => Self::is_graph_ndjson(args),
360            _ => false,
361        }
362    }
363
364    /// Whether this subcommand may resolve its target from ambient configuration.
365    ///
366    /// GAP-SG-207. [`Self::mutates`] answers "does this change durable state";
367    /// this answers "is naming the target nonetheless optional for THIS
368    /// invocation". The two differ, and reusing `mutates` alone would have been
369    /// a defect: it lists the read-only variants explicitly and answers `true`
370    /// for everything else, which is the right conservative default for the
371    /// output-time refusal fence and the WRONG one here. For the fence a
372    /// mistaken `true` costs a diagnostic; here it would cost a false refusal on
373    /// a command that has no side effect to protect — `fts check`, `vec stats`,
374    /// `embedding list` and `embedding status` all read and write nothing.
375    ///
376    /// So the families whose subcommands split between reading and writing are
377    /// classified at the SUBCOMMAND level. The Explicit Target Designation rule
378    /// governs side effects, and a read inherits no authority it could misuse.
379    ///
380    /// Enforcement lives in [`crate::paths::AppPaths::resolve`]. That placement
381    /// keeps this list short: a subcommand that never resolves a database —
382    /// `config`, `completions`, `locale`, `slots`, `cache` — is exempt by
383    /// construction and needs no entry here at all.
384    pub fn may_inherit_target(&self) -> bool {
385        use crate::commands::embedding::EmbeddingCmd;
386        use crate::commands::fts::FtsSubcommand;
387        use crate::commands::pending_embeddings::PendingEmbeddingsCmd;
388        use crate::commands::vec::VecSubcommand;
389
390        match self {
391            // Creating the XDG database when no `--db` is given IS the command,
392            // so requiring the flag would invert `init` rather than protect it.
393            Self::Init(_) => true,
394            // Host leaves. GAP-SG-139 fixed these to accept `--db` as a no-op
395            // precisely because they touch no database — but they still call
396            // `AppPaths::resolve` to locate the MODELS directory, which shares
397            // that resolver. Without this arm the target policy fired on
398            // `cache list`, a command that reads a cache and nothing else.
399            Self::Config(_) | Self::Cache(_) | Self::Slots(_) | Self::Completions(_) => true,
400            Self::Fts(args) => matches!(
401                args.command,
402                FtsSubcommand::Check(_) | FtsSubcommand::Stats(_)
403            ),
404            Self::Vec(args) => matches!(
405                args.command,
406                VecSubcommand::OrphanList(_) | VecSubcommand::Stats(_)
407            ),
408            Self::Embedding(args) => {
409                matches!(args.cmd, EmbeddingCmd::List(_) | EmbeddingCmd::Status(_))
410            }
411            Self::PendingEmbeddings(args) => matches!(
412                args.cmd,
413                PendingEmbeddingsCmd::List(_) | PendingEmbeddingsCmd::Status(_)
414            ),
415            _ => false,
416        }
417    }
418
419    /// Returns true for subcommands that load the ONNX model locally.
420    pub fn is_embedding_heavy(&self) -> bool {
421        matches!(
422            self,
423            Self::Init(_)
424                | Self::Remember(_)
425                | Self::RememberBatch(_)
426                | Self::Recall(_)
427                | Self::HybridSearch(_)
428                | Self::DeepResearch(_)
429        )
430    }
431
432    /// Return whether this command occupies a CLI concurrency slot.
433    pub fn uses_cli_slot(&self) -> bool {
434        true
435    }
436
437    /// Read-only / no-embedding subcommands that MUST run without an embedding
438    /// API key. `init` warms a best-effort smoke test internally and degrades to
439    /// `ok_no_embedding` when the backend is unreachable; the `enrich` queue
440    /// inspectors (`--status` / `--list-dead` / `--requeue-dead` /
441    /// `--prune-dead-orphans`) never embed and never call the LLM. The eager
442    /// OpenRouter key preflight in `main` must skip its hard-fail for these.
443    pub fn tolerates_missing_embedding_key(&self) -> bool {
444        match self {
445            Self::Init(_) => true,
446            // `schema` only writes embedded documents: no database, no key.
447            Self::Schema(_) => true,
448            // The host leaves must never be gated by embedding configuration:
449            // they are the only way to REPAIR that configuration. Registering
450            // `embedding.backend` (v1.2.5, GAP-SG-198) made the omission
451            // load-bearing — `config set embedding.backend openrouter` with no
452            // model stored started failing every later invocation at the
453            // preflight, `config unset` included, leaving hand-editing the TOML
454            // as the only exit. `cache`, `slots` and `completions` never embed
455            // either, so the same reasoning covers them.
456            Self::Config(_) | Self::Cache(_) | Self::Slots(_) | Self::Completions(_) => true,
457            Self::Enrich(args) => {
458                args.status
459                    || args.list_dead
460                    || args.requeue_dead
461                    || args.list_skipped
462                    || args.requeue_skipped
463                    || args.prune_dead_orphans
464                    || args.prune_dead_entity_orphans
465                    || args.print_schema
466            }
467            _ => false,
468        }
469    }
470}
471
472// FIX-1 (v1.0.89): manual `Debug` impl so test panic messages that print
473// `{:?}` on a captured `Commands` variant compile without requiring every
474// contained subcommand arg struct to derive `Debug`. The Debug output is
475// only used in test assertions for diagnostic messages; we emit the variant
476// name only — arg payload is intentionally omitted.
477impl std::fmt::Debug for Commands {
478    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479        let name = match self {
480            Self::Init(_) => "Init",
481            Self::Health(_) => "Health",
482            Self::Stats(_) => "Stats",
483            Self::List(_) => "List",
484            Self::Read(_) => "Read",
485            Self::Edit(_) => "Edit",
486            Self::Rename(_) => "Rename",
487            Self::SplitBody(_) => "SplitBody",
488            Self::Restore(_) => "Restore",
489            Self::History(_) => "History",
490            Self::Forget(_) => "Forget",
491            Self::Purge(_) => "Purge",
492            Self::Remember(_) => "Remember",
493            Self::RememberBatch(_) => "RememberBatch",
494            Self::Recall(_) => "Recall",
495            Self::HybridSearch(_) => "HybridSearch",
496            Self::Enrich(_) => "Enrich",
497            Self::Ingest(_) => "Ingest",
498            Self::Optimize(_) => "Optimize",
499            Self::Migrate(_) => "Migrate",
500            Self::SyncSafeCopy(_) => "SyncSafeCopy",
501            Self::Backup(_) => "Backup",
502            Self::Vacuum(_) => "Vacuum",
503            Self::Link(_) => "Link",
504            Self::Unlink(_) => "Unlink",
505            Self::DeepResearch(_) => "DeepResearch",
506            Self::Related(_) => "Related",
507            Self::Graph(_) => "Graph",
508            Self::Export(_) => "Export",
509            Self::Fts(_) => "Fts",
510            Self::Vec(_) => "Vec",
511            Self::PruneRelations(_) => "PruneRelations",
512            Self::PruneNer(_) => "PruneNer",
513            Self::Slots(_) => "Slots",
514            Self::Embedding(_) => "Embedding",
515            Self::PendingEmbeddings(_) => "PendingEmbeddings",
516            Self::CleanupOrphans(_) => "CleanupOrphans",
517            Self::MemoryEntities(_) => "MemoryEntities",
518            Self::Cache(_) => "Cache",
519            Self::DeleteEntity(_) => "DeleteEntity",
520            Self::Reclassify(_) => "Reclassify",
521            Self::RenameEntity(_) => "RenameEntity",
522            Self::ReclassifyRelation(_) => "ReclassifyRelation",
523            Self::NormalizeEntities(_) => "NormalizeEntities",
524            Self::MergeEntities(_) => "MergeEntities",
525            Self::NamespaceDetect(_) => "NamespaceDetect",
526            Self::Completions(_) => "Completions",
527            Self::Schema(_) => "Schema",
528            Self::DebugSchema(_) => "DebugSchema",
529            Self::Config(_) => "Config",
530        };
531        f.write_str(name)
532    }
533}