Skip to main content

inillucent_cli/command/
registry.rs

1//! Every command, as data.
2//!
3//! Invariant: **this array is the only list.** `inillucent --help`,
4//! `inillucent <verb>`'s argument parsing, `inillucent-mcp`'s `tools/list` and
5//! every JSON Schema it publishes are derived from it, and
6//! `command_parity.rs` fails the build if any of them stops agreeing.
7//!
8//! The descriptions are written for two readers at once. A person runs
9//! `inillucent help query`; a 27B model reads the same sentence as a tool
10//! description and has one attempt at getting the call right. That is why they
11//! say what a parameter *is for* rather than restating its name, and why the
12//! ones with a trap in them - `limit` cutting the rows handed back but not the
13//! count, `params` being positional - say so.
14
15use super::verbs;
16use super::{Command, Kind, Param, Writes, DB, FORMAT, LIMIT};
17
18/// The parameters `query` takes.
19const QUERY_PARAMS: &[Param] = &[
20    Param {
21        name: "sql",
22        kind: Kind::Text,
23        required: true,
24        positional: true,
25        description: "The SQL to run. One statement. Use ?1, ?2 ... for values you pass in \
26                      'params' rather than pasting them into the text.",
27    },
28    Param {
29        name: "params",
30        kind: Kind::Values,
31        required: false,
32        positional: false,
33        description: "The values for ?1, ?2 ... in order, as a JSON array of strings, numbers, \
34                      booleans or nulls. An array of numbers is a vector and \
35                      {\"blob\":\"<hex>\"} is bytes. Binding is how you avoid quoting mistakes \
36                      and SQL injection.",
37    },
38    Param {
39        name: "params-file",
40        kind: Kind::Text,
41        required: false,
42        positional: false,
43        description: "A file holding the JSON array for 'params', or - for standard input. What \
44                      a wrapper that spawns this binary uses: a command line has a length \
45                      ceiling, about 32 KB on Windows, and a parameter past it fails outright.",
46    },
47    LIMIT,
48    DB,
49    FORMAT,
50];
51
52/// The parameters `exec` takes.
53const EXEC_PARAMS: &[Param] = &[
54    Param {
55        name: "sql",
56        kind: Kind::Text,
57        required: true,
58        positional: true,
59        description: "One statement to run for its effect: INSERT, UPDATE, DELETE, CREATE, DROP, \
60                      ALTER or a PRAGMA that sets something.",
61    },
62    Param {
63        name: "params",
64        kind: Kind::Values,
65        required: false,
66        positional: false,
67        description: "The values for ?1, ?2 ... in order, as a JSON array. An array of numbers \
68                      is a vector and {\"blob\":\"<hex>\"} is bytes.",
69    },
70    Param {
71        name: "params-file",
72        kind: Kind::Text,
73        required: false,
74        positional: false,
75        description: "A file holding the JSON array for 'params', or - for standard input. What \
76                      a wrapper that spawns this binary uses: a command line has a length \
77                      ceiling, about 32 KB on Windows, and a parameter past it fails outright.",
78    },
79    DB,
80    FORMAT,
81];
82
83/// The parameters `batch` takes.
84const BATCH_PARAMS: &[Param] = &[
85    Param {
86        name: "sql",
87        kind: Kind::Text,
88        required: true,
89        positional: true,
90        description: "Several statements separated by semicolons. They run as one transaction: \
91                      either all of them take effect or none of them do.",
92    },
93    DB,
94    FORMAT,
95];
96
97/// The parameters `run` takes.
98const RUN_PARAMS: &[Param] = &[
99    Param {
100        name: "input",
101        kind: Kind::Text,
102        required: true,
103        positional: true,
104        description: "Shell input, exactly as you would type it: SQL statements ending in a \
105                      semicolon, and dot commands such as '.schema' or '.mode box' on their own \
106                      lines. Everything the interactive shell can do is reachable here.",
107    },
108    DB,
109];
110
111/// The parameters `create` takes.
112const CREATE_PARAMS: &[Param] = &[
113    Param {
114        name: "path",
115        kind: Kind::Text,
116        required: true,
117        positional: true,
118        description: "Where to make the new database file. It refuses a path that already \
119                      exists rather than overwriting it.",
120    },
121    FORMAT,
122];
123
124/// The parameters a pattern-filtered listing takes.
125const PATTERN_PARAMS: &[Param] = &[
126    Param {
127        name: "pattern",
128        kind: Kind::Text,
129        required: false,
130        positional: true,
131        description: "A LIKE pattern to filter the names by, such as 'user%'. Omit it for all \
132                      of them.",
133    },
134    DB,
135    FORMAT,
136];
137
138/// The parameters `schema` takes.
139const SCHEMA_PARAMS: &[Param] = &[
140    Param {
141        name: "pattern",
142        kind: Kind::Text,
143        required: false,
144        positional: true,
145        description: "A LIKE pattern naming which objects to show. Omit it for the whole schema.",
146    },
147    Param {
148        name: "indent",
149        kind: Kind::Boolean,
150        required: false,
151        positional: false,
152        description: "Pretty-print each CREATE statement over several lines instead of one.",
153    },
154    DB,
155    FORMAT,
156];
157
158/// The parameters `describe` takes.
159const DESCRIBE_PARAMS: &[Param] = &[
160    Param {
161        name: "table",
162        kind: Kind::Text,
163        required: true,
164        positional: true,
165        description: "The table or view to describe, by its exact name. Use 'tables' first if \
166                      you are not sure what it is called.",
167    },
168    DB,
169    FORMAT,
170];
171
172/// The parameters the database-only commands take.
173const DB_ONLY: &[Param] = &[DB, FORMAT];
174
175/// The parameters `explain` takes.
176const EXPLAIN_PARAMS: &[Param] = &[
177    Param {
178        name: "sql",
179        kind: Kind::Text,
180        required: true,
181        positional: true,
182        description: "The statement whose plan you want. It is not run - this only shows which \
183                      indexes and scans the planner chose.",
184    },
185    DB,
186    FORMAT,
187];
188
189/// The parameters `import` takes.
190const IMPORT_PARAMS: &[Param] = &[
191    Param {
192        name: "file",
193        kind: Kind::Text,
194        required: true,
195        positional: true,
196        description: "The delimited file to read.",
197    },
198    Param {
199        name: "table",
200        kind: Kind::Text,
201        required: true,
202        positional: false,
203        description: "The table to load into. If it does not exist it is created, taking its \
204                      column names from the file's first row.",
205    },
206    Param {
207        name: "format",
208        kind: Kind::Text,
209        required: false,
210        positional: false,
211        description: "'csv' (the default, RFC 4180 quoting), 'tabs', or 'ascii' for \\037 and \
212                      \\036 separated input.",
213    },
214    Param {
215        name: "skip",
216        kind: Kind::Integer,
217        required: false,
218        positional: false,
219        description: "How many leading rows to ignore, for a file with a preamble above its \
220                      header.",
221    },
222    DB,
223];
224
225/// The parameters `embed` takes.
226const EMBED_PARAMS: &[Param] = &[
227    Param {
228        name: "table",
229        kind: Kind::Text,
230        required: true,
231        positional: false,
232        description:
233            "The table to fill. An ordinary table with a VECTOR(768) or BLOB column, or an \
234                      inillucent_search table, whose vector column can be updated by rowid.",
235    },
236    Param {
237        name: "text",
238        kind: Kind::Text,
239        required: true,
240        positional: false,
241        description: "The column holding the text to embed. A row whose text is NULL or empty is \
242                      skipped and counted.",
243    },
244    Param {
245        name: "vector",
246        kind: Kind::Text,
247        required: true,
248        positional: false,
249        description:
250            "The column to write the vectors into. Only rows where it IS NULL are embedded, \
251                      so a stopped run continues where it ended.",
252    },
253    Param {
254        name: "prefix",
255        kind: Kind::Text,
256        required: false,
257        positional: false,
258        description: "Text put in front of every value before it is embedded, exactly as \
259                      embed('search_document: ' || body) does. For nomic-embed-text-v1.5 use \
260                      'search_document: '. The default is no prefix.",
261    },
262    Param {
263        name: "device",
264        kind: Kind::Text,
265        required: false,
266        positional: false,
267        description:
268            "The processor: 'cpu', 'cuda' or 'cuda:N' for card N. Without it the machine's \
269                      setting applies (INILLUCENT_EMBED_DEVICE, then what setup-embeddings \
270                      recorded, then the processor). A cuda device that will not start is an error \
271                      naming 'inillucent setup-embeddings runtime --gpu', and never runs on the \
272                      processor.",
273    },
274    Param {
275        name: "threads",
276        kind: Kind::Integer,
277        required: false,
278        positional: false,
279        description: "Threads ONNX Runtime uses inside one operator, per session. Without it the \
280                      machine's setting applies.",
281    },
282    Param {
283        name: "sessions",
284        kind: Kind::Integer,
285        required: false,
286        positional: false,
287        description: "How many model sessions to open on the device and run at once. The default \
288                      is 1. Two sessions on one card were measured 2.1 times faster than one.",
289    },
290    Param {
291        name: "batch-size",
292        kind: Kind::Integer,
293        required: false,
294        positional: false,
295        description: "Most texts in one call to the model. The default is 16. A batch is also \
296                      limited by a memory ceiling that shrinks it when the texts are long.",
297    },
298    Param {
299        name: "commit-every",
300        kind: Kind::Integer,
301        required: false,
302        positional: false,
303        description: "How many rows to embed and write in one transaction. The default is 1024. A \
304                      crash loses at most one transaction.",
305    },
306    Param {
307        name: "all",
308        kind: Kind::Boolean,
309        required: false,
310        positional: false,
311        description: "Embed every row again, including rows that already have a vector.",
312    },
313    DB,
314    FORMAT,
315];
316
317/// The parameters `export` takes.
318const EXPORT_PARAMS: &[Param] = &[
319    Param {
320        name: "table",
321        kind: Kind::Text,
322        required: false,
323        positional: true,
324        description: "The table to write out whole. Give this or 'sql', not both.",
325    },
326    Param {
327        name: "sql",
328        kind: Kind::Text,
329        required: false,
330        positional: false,
331        description: "A query whose rows to write out, when you want less than a whole table.",
332    },
333    Param {
334        name: "format",
335        kind: Kind::Text,
336        required: false,
337        positional: false,
338        description: "csv, json, tabs, markdown, insert, quote, line or html. Defaults to csv.",
339    },
340    Param {
341        name: "out",
342        kind: Kind::Text,
343        required: false,
344        positional: false,
345        description: "A file to write to. Omitted, the rows come back in the result instead.",
346    },
347    DB,
348];
349
350/// The parameters `dump` takes.
351const DUMP_PARAMS: &[Param] = &[
352    Param {
353        name: "objects",
354        kind: Kind::Text,
355        required: false,
356        positional: true,
357        description: "A LIKE pattern for the tables, indexes, triggers or views to dump. Omit \
358                      it for the whole database.",
359    },
360    Param {
361        name: "data_only",
362        kind: Kind::Boolean,
363        required: false,
364        positional: false,
365        description: "Write only the INSERT statements, leaving out the CREATE statements.",
366    },
367    DB,
368];
369
370/// The parameters `backup` takes.
371const BACKUP_PARAMS: &[Param] = &[
372    Param {
373        name: "file",
374        kind: Kind::Text,
375        required: true,
376        positional: true,
377        description: "Where to write the copy.",
378    },
379    DB,
380];
381
382/// The parameters `encrypt` and `decrypt` take.
383const COPY_PARAMS: &[Param] = &[
384    Param {
385        name: "file",
386        kind: Kind::Text,
387        required: true,
388        positional: true,
389        description: "Where to write the copy. A file already there is refused, not replaced.",
390    },
391    DB,
392];
393
394/// The parameters `rekey` takes.
395const REKEY_PARAMS: &[Param] = &[
396    Param {
397        name: "new-key-file",
398        kind: Kind::Text,
399        required: false,
400        positional: false,
401        description: "A file holding the new key. Without it the new key is read from the                       INILLUCENT_NEW_KEY environment variable. A key is never taken as a word                       on the command line, where every user of the machine can read it.",
402    },
403    DB,
404];
405
406/// The parameters `restore` takes.
407const RESTORE_PARAMS: &[Param] = &[
408    Param {
409        name: "file",
410        kind: Kind::Text,
411        required: true,
412        positional: true,
413        description: "The copy to read back. It replaces what is in the open database.",
414    },
415    DB,
416];
417
418/// The parameters `analyze` takes.
419const ANALYZE_PARAMS: &[Param] = &[
420    Param {
421        name: "table",
422        kind: Kind::Text,
423        required: false,
424        positional: true,
425        description: "One table to gather statistics for. Omit it for every table.",
426    },
427    DB,
428    FORMAT,
429];
430
431/// The parameters `migrate` takes.
432const MIGRATE_PARAMS: &[Param] = &[
433    Param {
434        name: "source",
435        kind: Kind::Text,
436        required: false,
437        positional: true,
438        description: "The SQLite database file to read, or a postgres:// or mysql:// connection \
439                      URL. The source is never written to. A connection URL holds a password, \
440                      and an argument is visible in the process list for the whole run - so it \
441                      may be left out and given in INILLUCENT_SOURCE_URL instead, or written as \
442                      '-' to read one line from standard input.",
443    },
444    Param {
445        name: "destination",
446        kind: Kind::Text,
447        required: true,
448        positional: false,
449        description: "The .rdb file to build. It refuses to overwrite an existing file.",
450    },
451    Param {
452        name: "kind",
453        kind: Kind::Text,
454        required: false,
455        positional: false,
456        description: "'sqlite' (the default for a path), 'postgres' or 'mysql' (the default for \
457                      a URL written with that scheme), or 'index' for a legacy retrieval index.",
458    },
459    Param {
460        name: "batch",
461        kind: Kind::Integer,
462        required: false,
463        positional: false,
464        description: "Rows per destination transaction while copying from a server. Default \
465                      10000. It changes how long the migration takes and nothing about what it \
466                      produces.",
467    },
468    Param {
469        name: "insecure-plaintext",
470        kind: Kind::Boolean,
471        required: false,
472        positional: false,
473        description: "Permit an unencrypted connection to a server. A migration to a host that \
474                      is not a loopback address uses verified TLS, and refuses rather than \
475                      falling back; this permits plaintext, and only together with \
476                      sslmode=disable in the URL. Both are needed because either one alone is \
477                      something people type without meaning it. The choice is recorded in the \
478                      migration report.",
479    },
480];
481
482/// The parameters `search` takes.
483const SEARCH_PARAMS: &[Param] = &[
484    Param {
485        name: "query",
486        kind: Kind::Text,
487        required: true,
488        positional: true,
489        description: "What to search for, in FTS5 query syntax: bare words are ANDed, \
490                      \"a phrase\" is quoted, and OR and NOT are available.",
491    },
492    Param {
493        name: "table",
494        kind: Kind::Text,
495        required: true,
496        positional: false,
497        description: "The full-text table to search - one created with USING fts5(...) or \
498                      USING inillucent_search(...).",
499    },
500    Param {
501        name: "k",
502        kind: Kind::Integer,
503        required: false,
504        positional: false,
505        description: "How many results to return, best first. Defaults to 10.",
506    },
507    Param {
508        name: "rerank",
509        kind: Kind::Boolean,
510        required: false,
511        positional: false,
512        description: "Reorder the rows the search found with the reranker, which reads the query \
513                      and each row together. The query text is passed as the question, so write \
514                      it as plain words. Needs an inillucent_search table and the reranker, \
515                      installed with 'inillucent setup-embeddings reranker'. On the processor \
516                      60 rows take about 10 seconds; on a graphics card about a tenth of one.",
517    },
518    DB,
519    FORMAT,
520];
521
522/// The parameters `vector-search` takes.
523const VECTOR_PARAMS: &[Param] = &[
524    Param {
525        name: "table",
526        kind: Kind::Text,
527        required: true,
528        positional: true,
529        description: "The table holding the vectors.",
530    },
531    Param {
532        name: "column",
533        kind: Kind::Text,
534        required: true,
535        positional: false,
536        description: "The VECTOR(N) column to measure against.",
537    },
538    Param {
539        name: "vector",
540        kind: Kind::Values,
541        required: true,
542        positional: false,
543        description: "The query vector, as a JSON array of numbers with exactly N elements.",
544    },
545    Param {
546        name: "k",
547        kind: Kind::Integer,
548        required: false,
549        positional: false,
550        description: "How many nearest rows to return. Defaults to 10.",
551    },
552    Param {
553        name: "measure",
554        kind: Kind::Text,
555        required: false,
556        positional: false,
557        description: "'cos' for cosine distance (the default), 'l2' for Euclidean, or 'dot' \
558                      for the inner product.",
559    },
560    DB,
561    FORMAT,
562];
563
564/// The parameters `capabilities` takes.
565const CAPABILITY_PARAMS: &[Param] = &[
566    Param {
567        name: "name",
568        kind: Kind::Text,
569        required: false,
570        positional: true,
571        description: "One capability to ask about, such as 'triggers'. Omit it for the whole \
572                      table. A name that is not in the table answers no, not yes.",
573    },
574    FORMAT,
575];
576
577/// The parameters `help` takes.
578const HELP_PARAMS: &[Param] = &[
579    Param {
580        name: "topic",
581        kind: Kind::Text,
582        required: false,
583        positional: true,
584        description: "A command to explain in full. Omit it for the list of every command.",
585    },
586    FORMAT,
587];
588
589/// The parameters `setup-embeddings` takes.
590const SETUP_PARAMS: &[Param] = &[
591    Param {
592        name: "component",
593        kind: Kind::Text,
594        required: false,
595        positional: true,
596        description: "What to install: 'all' for both halves, 'runtime' for the ONNX Runtime \
597                      shared library on its own, 'model' for the embedding weights on their own, or \
598                      'reranker' for the cross encoder that rerank() and a search naming question \
599                      use (about 600 MB, and not part of 'all'). Omit it to report what is \
600                      installed and download nothing.",
601    },
602    Param {
603        name: "status",
604        kind: Kind::Boolean,
605        required: false,
606        positional: false,
607        description: "Say what is installed, where, and which residency profile is in force, and \
608                      download nothing.",
609    },
610    Param {
611        name: "residency",
612        kind: Kind::Text,
613        required: false,
614        positional: false,
615        description: "When the model is in memory: 'resident' keeps it for the life of the process, \
616                      'on-demand' loads it per call and drops it, 'idle' or 'idle:90s' loads it on \
617                      use and drops it after a quiet period. Recorded for this machine; \
618                      INILLUCENT_EMBED_RESIDENCY overrides it for one process.",
619    },
620    Param {
621        name: "threads",
622        kind: Kind::Integer,
623        required: false,
624        positional: false,
625        description: "How many threads ONNX Runtime uses inside one operator, for embed(), \
626                      rerank() and a reranked search. Recorded for this machine; \
627                      INILLUCENT_EMBED_THREADS overrides it for one process. Without it ONNX \
628                      Runtime picks its own count. Four was the fastest single process setting \
629                      measured for the embedding model.",
630    },
631    Param {
632        name: "device",
633        kind: Kind::Text,
634        required: false,
635        positional: false,
636        description: "The processor the sessions run on: 'cpu', 'cuda' or 'cuda:N' for card N. \
637                      Recorded for this machine; INILLUCENT_EMBED_DEVICE overrides it for one \
638                      process. A cuda device that will not start is an error and never runs on \
639                      the processor. It needs the runtime installed with '--gpu'.",
640    },
641    Param {
642        name: "gpu",
643        kind: Kind::Boolean,
644        required: false,
645        positional: false,
646        description: "Install the ONNX Runtime build carrying the CUDA execution provider, which \
647                      exists for Windows and Linux on x86-64 only. It is a much larger download and \
648                      it needs a CUDA install of its own to be usable.",
649    },
650    Param {
651        name: "force",
652        kind: Kind::Boolean,
653        required: false,
654        positional: false,
655        description: "Fetch and install again even when the files are already there and their \
656                      digests match.",
657    },
658    Param {
659        name: "onnxruntime-version",
660        kind: Kind::Text,
661        required: false,
662        positional: false,
663        description: "The ONNX Runtime version to install. Defaults to the one this build pins a \
664                      digest for; any other version is fetched and reported as unverified.",
665    },
666    Param {
667        name: "dir",
668        kind: Kind::Text,
669        required: false,
670        positional: false,
671        description: "Install somewhere other than the per-user directory. INILLUCENT_HOME does the \
672                      same thing for every command at once.",
673    },
674    Param {
675        name: "from",
676        kind: Kind::Text,
677        required: false,
678        positional: false,
679        description: "Install the model or the reranker from a folder already on this machine \
680                      instead of downloading it, for a network that cannot reach Hugging Face. \
681                      Each file is found by its installed name or by its path in the Hugging \
682                      Face repository, such as onnx/model.onnx, and is checked against the \
683                      SHA-256 this build pins before anything is copied. One file missing or \
684                      different and nothing is installed. The runtime still downloads.",
685    },
686    FORMAT,
687];
688
689/// The parameters `version` takes.
690const VERSION_PARAMS: &[Param] = &[FORMAT];
691
692/// Every command inillucent has, in the order `help` lists them.
693pub static COMMANDS: &[Command] = &[
694    Command {
695        name: "query",
696        summary: "Run a SELECT and get its rows back.",
697        detail: "Use this for anything that reads. The rows come back as a table, or as typed \
698                 JSON with output=json. 'total' is exact even when 'limit' cut the rows handed \
699                 back, because the engine materialises the whole result - so a limit of 10 on a \
700                 million-row query still costs what the million rows cost. Put a LIMIT in the SQL \
701                 itself when you cannot afford that, where the planner can act on it.",
702        params: QUERY_PARAMS,
703        cli_only: None,
704        writes: Writes::No,
705        run: verbs::query,
706    },
707    Command {
708        name: "exec",
709        summary: "Run one statement that changes something, and get the row count back.",
710        detail: "INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, or a PRAGMA that sets a value. A \
711                 statement with RETURNING gives its rows back as well as its count.",
712        params: EXEC_PARAMS,
713        cli_only: None,
714        writes: Writes::Yes,
715        run: verbs::exec,
716    },
717    Command {
718        name: "batch",
719        summary: "Run several statements as one transaction.",
720        detail: "Separate them with semicolons. Either all of them take effect or none of them \
721                 do, which is what you want when creating a schema or loading related rows.",
722        params: BATCH_PARAMS,
723        cli_only: None,
724        writes: Writes::Yes,
725        run: verbs::batch,
726    },
727    Command {
728        name: "run",
729        summary: "Run shell input, dot commands and all, and get back what it printed.",
730        detail: "The escape hatch, and the reason every dot command is reachable from every front \
731                 end: this drives the same shell 'inillucent shell' does. Use it for the things \
732                 that have no verb of their own - '.eqp on', '.parameter set', '.testcase', \
733                 '.archive'. Run 'inillucent run \".help\"' for the full list of dot commands.",
734        params: RUN_PARAMS,
735        cli_only: None,
736        // **The verb gate stands aside and the shell refuses the writes.**
737        // `writes: true` here refused `--readonly run "SELECT count(*) FROM t;"`
738        // and the `inillucent_run` MCP tool with it, although `run` is how a
739        // read only agent reaches every dot command (task-2066 section 4.2,
740        // item 26).
741        writes: Writes::PerStatement,
742        run: verbs::run_input,
743    },
744    Command {
745        name: "create",
746        summary: "Make a new, empty database file.",
747        detail: "Refuses a path that already exists, so it can never destroy a database by being \
748                 run twice. Every other command opens whatever it is pointed at.",
749        params: CREATE_PARAMS,
750        cli_only: None,
751        writes: Writes::Yes,
752        run: verbs::create,
753    },
754    Command {
755        name: "tables",
756        summary: "List the tables and views.",
757        detail: "The names, with what each one is. System tables whose names begin with sqlite_ \
758                 are left out.",
759        params: PATTERN_PARAMS,
760        cli_only: None,
761        writes: Writes::No,
762        run: verbs::tables,
763    },
764    Command {
765        name: "describe",
766        summary: "Everything about one table: columns, types, keys, indexes, row count and DDL.",
767        detail: "Call this before writing SQL against a table you did not create. It answers in \
768                 one call what four separate pragmas would, which matters because a caller that \
769                 has to make four usually makes three and writes its query from an incomplete \
770                 picture.",
771        params: DESCRIBE_PARAMS,
772        cli_only: None,
773        writes: Writes::No,
774        run: verbs::describe,
775    },
776    Command {
777        name: "schema",
778        summary: "Show the CREATE statements for the whole database or for what a pattern names.",
779        detail: "The schema as SQL, which is the form you can paste into another database.",
780        params: SCHEMA_PARAMS,
781        cli_only: None,
782        writes: Writes::No,
783        run: verbs::schema,
784    },
785    Command {
786        name: "indexes",
787        summary: "List the indexes and the table each one is on.",
788        detail: "Including the ones a UNIQUE constraint or a primary key created, which is why \
789                 an index you did not write may appear here.",
790        params: PATTERN_PARAMS,
791        cli_only: None,
792        writes: Writes::No,
793        run: verbs::indexes,
794    },
795    Command {
796        name: "databases",
797        summary: "List the attached databases and the file behind each.",
798        detail: "'main' is the one that was opened; others come from ATTACH. 'temp' is the \
799                 session's own scratch database and has no file.",
800        params: DB_ONLY,
801        cli_only: None,
802        writes: Writes::No,
803        run: verbs::databases,
804    },
805    Command {
806        name: "explain",
807        summary: "Show the query plan for a statement without running it.",
808        detail: "Which indexes are used, which scans are full, and in what order the tables are \
809                 joined. This is how you find out why a query is slow before making it faster.",
810        params: EXPLAIN_PARAMS,
811        cli_only: None,
812        writes: Writes::No,
813        run: verbs::explain,
814    },
815    Command {
816        name: "import",
817        summary: "Load a CSV or tab-separated file into a table.",
818        detail: "The table is created from the file's first row if it does not exist. Quoting is \
819                 RFC 4180 unless a different format is asked for.",
820        params: IMPORT_PARAMS,
821        cli_only: None,
822        writes: Writes::Yes,
823        run: verbs::import,
824    },
825    Command {
826        name: "embed",
827        summary: "Fill a vector column with embeddings, on the processor or a graphics card.",
828        detail: "Reads every row of --table whose --vector column IS NULL, embeds the --text column \
829                 with the nomic-embed-text-v1.5 model, and writes the vectors in the layout embed() \
830                 returns. It sorts the texts by length, groups them under a memory ceiling, and \
831                 commits every --commit-every rows, so a stopped run continues where it ended. A \
832                 corpus of 558,429 chunks took 22 minutes on a graphics card, where embed() in SQL, \
833                 one row at a time on the processor, would take about 12 hours. It prints how many \
834                 rows were embedded, how many were skipped because the text was NULL or empty, and \
835                 how many were cut at the model's token limit, with the rowid of up to 20 of them. \
836                 Needs the model: run 'inillucent setup-embeddings all' first, and \
837                 'inillucent setup-embeddings runtime --gpu' for a card.",
838        params: EMBED_PARAMS,
839        cli_only: None,
840        writes: Writes::Yes,
841        run: crate::bulk_embed::embed_table,
842    },
843    Command {
844        name: "export",
845        summary: "Write a table or a query's rows out as CSV, JSON or one of six other formats.",
846        detail: "With 'out' the rows go to a file and the result says so; without it they come \
847                 back in the result, which is usually what an agent wants.",
848        params: EXPORT_PARAMS,
849        cli_only: None,
850        writes: Writes::No,
851        run: verbs::export,
852    },
853    Command {
854        name: "dump",
855        summary: "Render the database as the SQL that would rebuild it.",
856        detail: "Schema and data, in dependency order, inside a transaction. This is the \
857                 portable form: it is text, and another SQLite-speaking database will read it.",
858        params: DUMP_PARAMS,
859        cli_only: None,
860        writes: Writes::No,
861        run: verbs::dump,
862    },
863    Command {
864        name: "backup",
865        summary: "Write a copy of the database to another file.",
866        detail: "A consistent copy taken while the database is open. The copy is a database, not \
867                 a text dump.",
868        params: BACKUP_PARAMS,
869        cli_only: None,
870        writes: Writes::No,
871        run: verbs::backup,
872    },
873    Command {
874        name: "encrypt",
875        summary: "Write an encrypted copy of a plaintext database.",
876        detail: "The copy is encrypted with the key from --key-file or INILLUCENT_KEY, and                  holds every row, index, view and trigger of the database --db names, which is                  read without the key. The source is left as it was: replace it with the copy                  once you have checked the copy opens.",
877        params: COPY_PARAMS,
878        cli_only: Some(
879            "it takes its key from the program's own --key-file or environment, and an MCP              client has neither. Encrypt a database before serving it.",
880        ),
881        writes: Writes::No,
882        run: verbs::encrypt,
883    },
884    Command {
885        name: "decrypt",
886        summary: "Write a plaintext copy of an encrypted database.",
887        detail: "Opens the database --db names with the key from --key-file or INILLUCENT_KEY                  and writes every row, index, view and trigger to a file that is not                  encrypted. The encrypted database is left as it was.",
888        params: COPY_PARAMS,
889        cli_only: Some(
890            "it writes every row of an encrypted database to disk unprotected, which is a              decision for the person who holds the key and not for an agent the database              is served to.",
891        ),
892        writes: Writes::No,
893        run: verbs::decrypt,
894    },
895    Command {
896        name: "rekey",
897        summary: "Change the key an encrypted database is encrypted with.",
898        detail: "Opens the database with the key from --key-file or INILLUCENT_KEY and moves it                  to the new key from --new-key-file or INILLUCENT_NEW_KEY. Only the header of                  each file is rewritten, so it takes the same time whatever the database's                  size, and the old key stops opening the database when it returns. PRAGMA                  rekey does the same from SQL.",
899        params: REKEY_PARAMS,
900        cli_only: Some(
901            "an agent that could change the key could lock the database's owner out of it.",
902        ),
903        writes: Writes::Yes,
904        run: verbs::rekey,
905    },
906    Command {
907        name: "restore",
908        summary: "Point this session at a backup file, in place of the database it opened.",
909        detail: "The opposite of 'backup', and it does not overwrite anything: this engine's \
910                 databases are whole files, so restoring is opening the other file rather than \
911                 writing its pages over the one you are in. That means it lasts as long as the \
912                 session does - useful from the shell and from 'run', where the statements after \
913                 it read the restored file, and of no effect on its own, because a one-shot \
914                 process ends immediately after. To replace a file, copy the backup over it. A \
915                 backup file that is not there is refused rather than created empty.",
916        params: RESTORE_PARAMS,
917        cli_only: None,
918        writes: Writes::Yes,
919        run: verbs::restore,
920    },
921    Command {
922        name: "checkpoint",
923        summary: "Fold the write-ahead log back into the database file.",
924        detail: "Writes go to a log first and are folded in later. Doing it now shrinks the log \
925                 and is what you want before copying the file by hand.",
926        params: DB_ONLY,
927        cli_only: None,
928        writes: Writes::Yes,
929        run: verbs::checkpoint,
930    },
931    Command {
932        name: "integrity-check",
933        summary: "Read every page and report whether the database holds together.",
934        detail: "Answers 'ok' on a healthy database. Anything else names what is wrong. It reads \
935                 the whole file, so it costs what the file costs.",
936        params: DB_ONLY,
937        cli_only: None,
938        writes: Writes::No,
939        run: verbs::integrity_check,
940    },
941    Command {
942        name: "analyze",
943        summary: "Gather the statistics the query planner reads.",
944        detail: "Run it after loading a lot of data. Without statistics the planner guesses at \
945                 how selective an index is, and a wrong guess is the usual reason a query that \
946                 should use an index does not.",
947        params: ANALYZE_PARAMS,
948        cli_only: None,
949        writes: Writes::Yes,
950        run: verbs::analyze,
951    },
952    Command {
953        name: "stats",
954        summary: "Report the page cache, the pool size and the shape of the file.",
955        detail: "Cache hits and misses, how many pages the file holds and how many are free. \
956                 This is where you look when a workload is slower than it should be.",
957        params: DB_ONLY,
958        cli_only: None,
959        writes: Writes::No,
960        run: verbs::stats,
961    },
962    Command {
963        name: "search",
964        summary: "Full-text search over an FTS5 or inillucent_search table.",
965        detail: "Writes the MATCH ... ORDER BY rank idiom for you, which is the part nobody \
966                 remembers. The table has to be a full-text one; 'describe' will show you \
967                 whether it is.",
968        params: SEARCH_PARAMS,
969        cli_only: None,
970        writes: Writes::No,
971        run: verbs::search,
972    },
973    Command {
974        name: "vector-search",
975        summary: "Find the rows whose vector is nearest to one you supply.",
976        detail: "Over a VECTOR(N) column, by cosine distance unless you ask for another measure. \
977                 If there is an HNSW index on the column the planner uses it; if there is not, \
978                 this is an exhaustive scan and is still correct.",
979        params: VECTOR_PARAMS,
980        cli_only: None,
981        writes: Writes::No,
982        run: verbs::vector_search,
983    },
984    Command {
985        name: "capabilities",
986        summary: "Ask what this engine can do before composing a statement.",
987        detail: "Every row is checked against the running engine by a test, in both directions - \
988                 a claim of support that fails and a claim of absence that now works each turn \
989                 the build red. So this is worth trusting in a way a hand-maintained feature \
990                 list is not. A name that is not in the table answers no, because a capability \
991                 that was never declared was never checked.",
992        params: CAPABILITY_PARAMS,
993        cli_only: None,
994        writes: Writes::No,
995        run: verbs::capabilities,
996    },
997    Command {
998        name: "functions",
999        summary: "List the SQL functions this engine answers.",
1000        detail: "From the engine's own register, which is compared against the reference \
1001                 library's on every build - so this is what actually exists rather than what was \
1002                 documented once.",
1003        params: PATTERN_PARAMS,
1004        cli_only: None,
1005        writes: Writes::No,
1006        run: verbs::functions,
1007    },
1008    Command {
1009        name: "migrate",
1010        summary: "Build an inillucent database from a SQLite file, PostgreSQL or MySQL.",
1011        detail: "Reads the source and writes a new .rdb with the same rows. A path is a SQLite \
1012                 database file; a postgres:// or mysql:// URL is a running server, read inside \
1013                 one repeatable-read snapshot so that every table is as of one instant. The \
1014                 source is never written to and the destination is never overwritten: the new \
1015                 file is staged under another name and published by a rename, so a half-written \
1016                 database never sits where an application would open it. A server migration is \
1017                 verified per table by row count and by an order-independent digest, and nothing \
1018                 that fails a check is published.",
1019        params: MIGRATE_PARAMS,
1020        cli_only: None,
1021        writes: Writes::Yes,
1022        run: verbs::migrate,
1023    },
1024    Command {
1025        name: "setup-embeddings",
1026        summary: "Download and install the embedding model and the runtime it needs.",
1027        detail: "One command, on Windows, macOS and Linux. It fetches ONNX Runtime and the \
1028                 nomic-embed-text-v1.5 weights into a per-user directory, checks every byte \
1029                 against a digest pinned in this build, and leaves the engine able to answer \
1030                 embed(TEXT) with nothing exported by hand - so a mismatch is a refusal that \
1031                 names both digests rather than a shared library that loads and misbehaves. \
1032                 Name what you want: 'all' installs both halves, 'runtime' and 'model' one \
1033                 each. Run with no component at all and it reports what is installed and \
1034                 downloads nothing, which is what stops a 620 MB fetch being a surprise; \
1035                 '--status' does the same explicitly. About 620 MB the first time and \
1036                 nothing on a later run. '--residency' chooses when the model is in memory: \
1037                 'resident' keeps it, which is about 1.9 GB held and 12 to 36 ms a query; \
1038                 'on-demand' loads it per call, which holds nothing and costs about 0.8 s a \
1039                 query; 'idle' or 'idle:90s' loads it on use and drops it after a quiet \
1040                 period, which is the default and pays the load once for a burst of \
1041                 questions.",
1042        params: SETUP_PARAMS,
1043        cli_only: None,
1044        writes: Writes::Yes,
1045        run: crate::setup::setup_embeddings,
1046    },
1047    Command {
1048        name: "version",
1049        summary: "Report the engine, the dialect and the driver versions.",
1050        detail: "The SQLite version named here is the dialect this engine implements, not a \
1051                 library it links. There is no SQLite in this binary.",
1052        params: VERSION_PARAMS,
1053        cli_only: None,
1054        writes: Writes::No,
1055        run: verbs::version,
1056    },
1057    Command {
1058        name: "help",
1059        summary: "List every command, or explain one in full.",
1060        detail: "With no topic it prints the table. With one it prints that command's usage, \
1061                 what it is for, and every parameter it takes.",
1062        params: HELP_PARAMS,
1063        cli_only: None,
1064        writes: Writes::No,
1065        run: verbs::help,
1066    },
1067    Command {
1068        name: "shell",
1069        summary: "Start the interactive shell.",
1070        detail: "The sqlite3-shaped REPL, with all 63 dot commands. Everything it can do is also \
1071                 reachable non-interactively through 'run'.",
1072        params: &[],
1073        cli_only: Some(
1074            "it is a terminal REPL: it reads a keyboard and writes a screen, and neither exists \
1075             at the other end of an MCP call. Use 'run' instead, which drives the same shell.",
1076        ),
1077        writes: Writes::Yes,
1078        run: verbs::shell_placeholder,
1079    },
1080    Command {
1081        name: "mcp",
1082        summary: "Serve these commands to an agent over MCP on standard input and output.",
1083        detail: "Every command in this table that is not marked cli-only becomes a tool named \
1084                 inillucent_<command>, with this same description and these same parameters.",
1085        params: &[],
1086        cli_only: Some(
1087            "it is the server that would be exposing the tools, so offering it as one of them \
1088             would let a client ask the server to serve itself.",
1089        ),
1090        writes: Writes::Yes,
1091        run: verbs::mcp_placeholder,
1092    },
1093];