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 `export` takes.
226const EXPORT_PARAMS: &[Param] = &[
227    Param {
228        name: "table",
229        kind: Kind::Text,
230        required: false,
231        positional: true,
232        description: "The table to write out whole. Give this or 'sql', not both.",
233    },
234    Param {
235        name: "sql",
236        kind: Kind::Text,
237        required: false,
238        positional: false,
239        description: "A query whose rows to write out, when you want less than a whole table.",
240    },
241    Param {
242        name: "format",
243        kind: Kind::Text,
244        required: false,
245        positional: false,
246        description: "csv, json, tabs, markdown, insert, quote, line or html. Defaults to csv.",
247    },
248    Param {
249        name: "out",
250        kind: Kind::Text,
251        required: false,
252        positional: false,
253        description: "A file to write to. Omitted, the rows come back in the result instead.",
254    },
255    DB,
256];
257
258/// The parameters `dump` takes.
259const DUMP_PARAMS: &[Param] = &[
260    Param {
261        name: "objects",
262        kind: Kind::Text,
263        required: false,
264        positional: true,
265        description: "A LIKE pattern for the tables, indexes, triggers or views to dump. Omit \
266                      it for the whole database.",
267    },
268    Param {
269        name: "data_only",
270        kind: Kind::Boolean,
271        required: false,
272        positional: false,
273        description: "Write only the INSERT statements, leaving out the CREATE statements.",
274    },
275    DB,
276];
277
278/// The parameters `backup` takes.
279const BACKUP_PARAMS: &[Param] = &[
280    Param {
281        name: "file",
282        kind: Kind::Text,
283        required: true,
284        positional: true,
285        description: "Where to write the copy.",
286    },
287    DB,
288];
289
290/// The parameters `restore` takes.
291const RESTORE_PARAMS: &[Param] = &[
292    Param {
293        name: "file",
294        kind: Kind::Text,
295        required: true,
296        positional: true,
297        description: "The copy to read back. It replaces what is in the open database.",
298    },
299    DB,
300];
301
302/// The parameters `analyze` takes.
303const ANALYZE_PARAMS: &[Param] = &[
304    Param {
305        name: "table",
306        kind: Kind::Text,
307        required: false,
308        positional: true,
309        description: "One table to gather statistics for. Omit it for every table.",
310    },
311    DB,
312    FORMAT,
313];
314
315/// The parameters `migrate` takes.
316const MIGRATE_PARAMS: &[Param] = &[
317    Param {
318        name: "source",
319        kind: Kind::Text,
320        required: false,
321        positional: true,
322        description: "The SQLite database file to read, or a postgres:// or mysql:// connection \
323                      URL. The source is never written to. A connection URL holds a password, \
324                      and an argument is visible in the process list for the whole run - so it \
325                      may be left out and given in INILLUCENT_SOURCE_URL instead, or written as \
326                      '-' to read one line from standard input.",
327    },
328    Param {
329        name: "destination",
330        kind: Kind::Text,
331        required: true,
332        positional: false,
333        description: "The .rdb file to build. It refuses to overwrite an existing file.",
334    },
335    Param {
336        name: "kind",
337        kind: Kind::Text,
338        required: false,
339        positional: false,
340        description: "'sqlite' (the default for a path), 'postgres' or 'mysql' (the default for \
341                      a URL written with that scheme), or 'index' for a legacy retrieval index.",
342    },
343    Param {
344        name: "batch",
345        kind: Kind::Integer,
346        required: false,
347        positional: false,
348        description: "Rows per destination transaction while copying from a server. Default \
349                      10000. It changes how long the migration takes and nothing about what it \
350                      produces.",
351    },
352    Param {
353        name: "insecure-plaintext",
354        kind: Kind::Boolean,
355        required: false,
356        positional: false,
357        description: "Permit an unencrypted connection to a server. A migration to a host that \
358                      is not a loopback address uses verified TLS, and refuses rather than \
359                      falling back; this permits plaintext, and only together with \
360                      sslmode=disable in the URL. Both are needed because either one alone is \
361                      something people type without meaning it. The choice is recorded in the \
362                      migration report.",
363    },
364];
365
366/// The parameters `search` takes.
367const SEARCH_PARAMS: &[Param] = &[
368    Param {
369        name: "query",
370        kind: Kind::Text,
371        required: true,
372        positional: true,
373        description: "What to search for, in FTS5 query syntax: bare words are ANDed, \
374                      \"a phrase\" is quoted, and OR and NOT are available.",
375    },
376    Param {
377        name: "table",
378        kind: Kind::Text,
379        required: true,
380        positional: false,
381        description: "The full-text table to search - one created with USING fts5(...) or \
382                      USING inillucent_search(...).",
383    },
384    Param {
385        name: "k",
386        kind: Kind::Integer,
387        required: false,
388        positional: false,
389        description: "How many results to return, best first. Defaults to 10.",
390    },
391    DB,
392    FORMAT,
393];
394
395/// The parameters `vector-search` takes.
396const VECTOR_PARAMS: &[Param] = &[
397    Param {
398        name: "table",
399        kind: Kind::Text,
400        required: true,
401        positional: true,
402        description: "The table holding the vectors.",
403    },
404    Param {
405        name: "column",
406        kind: Kind::Text,
407        required: true,
408        positional: false,
409        description: "The VECTOR(N) column to measure against.",
410    },
411    Param {
412        name: "vector",
413        kind: Kind::Values,
414        required: true,
415        positional: false,
416        description: "The query vector, as a JSON array of numbers with exactly N elements.",
417    },
418    Param {
419        name: "k",
420        kind: Kind::Integer,
421        required: false,
422        positional: false,
423        description: "How many nearest rows to return. Defaults to 10.",
424    },
425    Param {
426        name: "measure",
427        kind: Kind::Text,
428        required: false,
429        positional: false,
430        description: "'cos' for cosine distance (the default), 'l2' for Euclidean, or 'dot' \
431                      for the inner product.",
432    },
433    DB,
434    FORMAT,
435];
436
437/// The parameters `capabilities` takes.
438const CAPABILITY_PARAMS: &[Param] = &[
439    Param {
440        name: "name",
441        kind: Kind::Text,
442        required: false,
443        positional: true,
444        description: "One capability to ask about, such as 'triggers'. Omit it for the whole \
445                      table. A name that is not in the table answers no, not yes.",
446    },
447    FORMAT,
448];
449
450/// The parameters `help` takes.
451const HELP_PARAMS: &[Param] = &[
452    Param {
453        name: "topic",
454        kind: Kind::Text,
455        required: false,
456        positional: true,
457        description: "A command to explain in full. Omit it for the list of every command.",
458    },
459    FORMAT,
460];
461
462/// The parameters `setup-embeddings` takes.
463const SETUP_PARAMS: &[Param] = &[
464    Param {
465        name: "component",
466        kind: Kind::Text,
467        required: false,
468        positional: true,
469        description: "What to install: 'all' for both halves, 'runtime' for the ONNX Runtime \
470                      shared library on its own, or 'model' for the weights on their own. Omit \
471                      it to report what is installed and download nothing.",
472    },
473    Param {
474        name: "status",
475        kind: Kind::Boolean,
476        required: false,
477        positional: false,
478        description: "Say what is installed, where, and which residency profile is in force, and \
479                      download nothing.",
480    },
481    Param {
482        name: "residency",
483        kind: Kind::Text,
484        required: false,
485        positional: false,
486        description: "When the model is in memory: 'resident' keeps it for the life of the process, \
487                      'on-demand' loads it per call and drops it, 'idle' or 'idle:90s' loads it on \
488                      use and drops it after a quiet period. Recorded for this machine; \
489                      INILLUCENT_EMBED_RESIDENCY overrides it for one process.",
490    },
491    Param {
492        name: "gpu",
493        kind: Kind::Boolean,
494        required: false,
495        positional: false,
496        description: "Install the ONNX Runtime build carrying the CUDA execution provider, which \
497                      exists for Windows and Linux on x86-64 only. It is a much larger download and \
498                      it needs a CUDA install of its own to be usable.",
499    },
500    Param {
501        name: "force",
502        kind: Kind::Boolean,
503        required: false,
504        positional: false,
505        description: "Fetch and install again even when the files are already there and their \
506                      digests match.",
507    },
508    Param {
509        name: "onnxruntime-version",
510        kind: Kind::Text,
511        required: false,
512        positional: false,
513        description: "The ONNX Runtime version to install. Defaults to the one this build pins a \
514                      digest for; any other version is fetched and reported as unverified.",
515    },
516    Param {
517        name: "dir",
518        kind: Kind::Text,
519        required: false,
520        positional: false,
521        description: "Install somewhere other than the per-user directory. INILLUCENT_HOME does the \
522                      same thing for every command at once.",
523    },
524    FORMAT,
525];
526
527/// The parameters `version` takes.
528const VERSION_PARAMS: &[Param] = &[FORMAT];
529
530/// Every command inillucent has, in the order `help` lists them.
531pub static COMMANDS: &[Command] = &[
532    Command {
533        name: "query",
534        summary: "Run a SELECT and get its rows back.",
535        detail: "Use this for anything that reads. The rows come back as a table, or as typed \
536                 JSON with output=json. 'total' is exact even when 'limit' cut the rows handed \
537                 back, because the engine materialises the whole result - so a limit of 10 on a \
538                 million-row query still costs what the million rows cost. Put a LIMIT in the SQL \
539                 itself when you cannot afford that, where the planner can act on it.",
540        params: QUERY_PARAMS,
541        cli_only: None,
542        writes: Writes::No,
543        run: verbs::query,
544    },
545    Command {
546        name: "exec",
547        summary: "Run one statement that changes something, and get the row count back.",
548        detail: "INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, or a PRAGMA that sets a value. A \
549                 statement with RETURNING gives its rows back as well as its count.",
550        params: EXEC_PARAMS,
551        cli_only: None,
552        writes: Writes::Yes,
553        run: verbs::exec,
554    },
555    Command {
556        name: "batch",
557        summary: "Run several statements as one transaction.",
558        detail: "Separate them with semicolons. Either all of them take effect or none of them \
559                 do, which is what you want when creating a schema or loading related rows.",
560        params: BATCH_PARAMS,
561        cli_only: None,
562        writes: Writes::Yes,
563        run: verbs::batch,
564    },
565    Command {
566        name: "run",
567        summary: "Run shell input, dot commands and all, and get back what it printed.",
568        detail: "The escape hatch, and the reason every dot command is reachable from every front \
569                 end: this drives the same shell 'inillucent shell' does. Use it for the things \
570                 that have no verb of their own - '.eqp on', '.parameter set', '.testcase', \
571                 '.archive'. Run 'inillucent run \".help\"' for the full list of dot commands.",
572        params: RUN_PARAMS,
573        cli_only: None,
574        // **The verb gate stands aside and the shell refuses the writes.**
575        // `writes: true` here refused `--readonly run "SELECT count(*) FROM t;"`
576        // and the `inillucent_run` MCP tool with it, although `run` is how a
577        // read only agent reaches every dot command (task-2066 section 4.2,
578        // item 26).
579        writes: Writes::PerStatement,
580        run: verbs::run_input,
581    },
582    Command {
583        name: "create",
584        summary: "Make a new, empty database file.",
585        detail: "Refuses a path that already exists, so it can never destroy a database by being \
586                 run twice. Every other command opens whatever it is pointed at.",
587        params: CREATE_PARAMS,
588        cli_only: None,
589        writes: Writes::Yes,
590        run: verbs::create,
591    },
592    Command {
593        name: "tables",
594        summary: "List the tables and views.",
595        detail: "The names, with what each one is. System tables whose names begin with sqlite_ \
596                 are left out.",
597        params: PATTERN_PARAMS,
598        cli_only: None,
599        writes: Writes::No,
600        run: verbs::tables,
601    },
602    Command {
603        name: "describe",
604        summary: "Everything about one table: columns, types, keys, indexes, row count and DDL.",
605        detail: "Call this before writing SQL against a table you did not create. It answers in \
606                 one call what four separate pragmas would, which matters because a caller that \
607                 has to make four usually makes three and writes its query from an incomplete \
608                 picture.",
609        params: DESCRIBE_PARAMS,
610        cli_only: None,
611        writes: Writes::No,
612        run: verbs::describe,
613    },
614    Command {
615        name: "schema",
616        summary: "Show the CREATE statements for the whole database or for what a pattern names.",
617        detail: "The schema as SQL, which is the form you can paste into another database.",
618        params: SCHEMA_PARAMS,
619        cli_only: None,
620        writes: Writes::No,
621        run: verbs::schema,
622    },
623    Command {
624        name: "indexes",
625        summary: "List the indexes and the table each one is on.",
626        detail: "Including the ones a UNIQUE constraint or a primary key created, which is why \
627                 an index you did not write may appear here.",
628        params: PATTERN_PARAMS,
629        cli_only: None,
630        writes: Writes::No,
631        run: verbs::indexes,
632    },
633    Command {
634        name: "databases",
635        summary: "List the attached databases and the file behind each.",
636        detail: "'main' is the one that was opened; others come from ATTACH. 'temp' is the \
637                 session's own scratch database and has no file.",
638        params: DB_ONLY,
639        cli_only: None,
640        writes: Writes::No,
641        run: verbs::databases,
642    },
643    Command {
644        name: "explain",
645        summary: "Show the query plan for a statement without running it.",
646        detail: "Which indexes are used, which scans are full, and in what order the tables are \
647                 joined. This is how you find out why a query is slow before making it faster.",
648        params: EXPLAIN_PARAMS,
649        cli_only: None,
650        writes: Writes::No,
651        run: verbs::explain,
652    },
653    Command {
654        name: "import",
655        summary: "Load a CSV or tab-separated file into a table.",
656        detail: "The table is created from the file's first row if it does not exist. Quoting is \
657                 RFC 4180 unless a different format is asked for.",
658        params: IMPORT_PARAMS,
659        cli_only: None,
660        writes: Writes::Yes,
661        run: verbs::import,
662    },
663    Command {
664        name: "export",
665        summary: "Write a table or a query's rows out as CSV, JSON or one of six other formats.",
666        detail: "With 'out' the rows go to a file and the result says so; without it they come \
667                 back in the result, which is usually what an agent wants.",
668        params: EXPORT_PARAMS,
669        cli_only: None,
670        writes: Writes::No,
671        run: verbs::export,
672    },
673    Command {
674        name: "dump",
675        summary: "Render the database as the SQL that would rebuild it.",
676        detail: "Schema and data, in dependency order, inside a transaction. This is the \
677                 portable form: it is text, and another SQLite-speaking database will read it.",
678        params: DUMP_PARAMS,
679        cli_only: None,
680        writes: Writes::No,
681        run: verbs::dump,
682    },
683    Command {
684        name: "backup",
685        summary: "Write a copy of the database to another file.",
686        detail: "A consistent copy taken while the database is open. The copy is a database, not \
687                 a text dump.",
688        params: BACKUP_PARAMS,
689        cli_only: None,
690        writes: Writes::No,
691        run: verbs::backup,
692    },
693    Command {
694        name: "restore",
695        summary: "Point this session at a backup file, in place of the database it opened.",
696        detail: "The opposite of 'backup', and it does not overwrite anything: this engine's \
697                 databases are whole files, so restoring is opening the other file rather than \
698                 writing its pages over the one you are in. That means it lasts as long as the \
699                 session does - useful from the shell and from 'run', where the statements after \
700                 it read the restored file, and of no effect on its own, because a one-shot \
701                 process ends immediately after. To replace a file, copy the backup over it. A \
702                 backup file that is not there is refused rather than created empty.",
703        params: RESTORE_PARAMS,
704        cli_only: None,
705        writes: Writes::Yes,
706        run: verbs::restore,
707    },
708    Command {
709        name: "checkpoint",
710        summary: "Fold the write-ahead log back into the database file.",
711        detail: "Writes go to a log first and are folded in later. Doing it now shrinks the log \
712                 and is what you want before copying the file by hand.",
713        params: DB_ONLY,
714        cli_only: None,
715        writes: Writes::Yes,
716        run: verbs::checkpoint,
717    },
718    Command {
719        name: "integrity-check",
720        summary: "Read every page and report whether the database holds together.",
721        detail: "Answers 'ok' on a healthy database. Anything else names what is wrong. It reads \
722                 the whole file, so it costs what the file costs.",
723        params: DB_ONLY,
724        cli_only: None,
725        writes: Writes::No,
726        run: verbs::integrity_check,
727    },
728    Command {
729        name: "analyze",
730        summary: "Gather the statistics the query planner reads.",
731        detail: "Run it after loading a lot of data. Without statistics the planner guesses at \
732                 how selective an index is, and a wrong guess is the usual reason a query that \
733                 should use an index does not.",
734        params: ANALYZE_PARAMS,
735        cli_only: None,
736        writes: Writes::Yes,
737        run: verbs::analyze,
738    },
739    Command {
740        name: "stats",
741        summary: "Report the page cache, the pool size and the shape of the file.",
742        detail: "Cache hits and misses, how many pages the file holds and how many are free. \
743                 This is where you look when a workload is slower than it should be.",
744        params: DB_ONLY,
745        cli_only: None,
746        writes: Writes::No,
747        run: verbs::stats,
748    },
749    Command {
750        name: "search",
751        summary: "Full-text search over an FTS5 or inillucent_search table.",
752        detail: "Writes the MATCH ... ORDER BY rank idiom for you, which is the part nobody \
753                 remembers. The table has to be a full-text one; 'describe' will show you \
754                 whether it is.",
755        params: SEARCH_PARAMS,
756        cli_only: None,
757        writes: Writes::No,
758        run: verbs::search,
759    },
760    Command {
761        name: "vector-search",
762        summary: "Find the rows whose vector is nearest to one you supply.",
763        detail: "Over a VECTOR(N) column, by cosine distance unless you ask for another measure. \
764                 If there is an HNSW index on the column the planner uses it; if there is not, \
765                 this is an exhaustive scan and is still correct.",
766        params: VECTOR_PARAMS,
767        cli_only: None,
768        writes: Writes::No,
769        run: verbs::vector_search,
770    },
771    Command {
772        name: "capabilities",
773        summary: "Ask what this engine can do before composing a statement.",
774        detail: "Every row is checked against the running engine by a test, in both directions - \
775                 a claim of support that fails and a claim of absence that now works each turn \
776                 the build red. So this is worth trusting in a way a hand-maintained feature \
777                 list is not. A name that is not in the table answers no, because a capability \
778                 that was never declared was never checked.",
779        params: CAPABILITY_PARAMS,
780        cli_only: None,
781        writes: Writes::No,
782        run: verbs::capabilities,
783    },
784    Command {
785        name: "functions",
786        summary: "List the SQL functions this engine answers.",
787        detail: "From the engine's own register, which is compared against the reference \
788                 library's on every build - so this is what actually exists rather than what was \
789                 documented once.",
790        params: PATTERN_PARAMS,
791        cli_only: None,
792        writes: Writes::No,
793        run: verbs::functions,
794    },
795    Command {
796        name: "migrate",
797        summary: "Build an inillucent database from a SQLite file, PostgreSQL or MySQL.",
798        detail: "Reads the source and writes a new .rdb with the same rows. A path is a SQLite \
799                 database file; a postgres:// or mysql:// URL is a running server, read inside \
800                 one repeatable-read snapshot so that every table is as of one instant. The \
801                 source is never written to and the destination is never overwritten: the new \
802                 file is staged under another name and published by a rename, so a half-written \
803                 database never sits where an application would open it. A server migration is \
804                 verified per table by row count and by an order-independent digest, and nothing \
805                 that fails a check is published.",
806        params: MIGRATE_PARAMS,
807        cli_only: None,
808        writes: Writes::Yes,
809        run: verbs::migrate,
810    },
811    Command {
812        name: "setup-embeddings",
813        summary: "Download and install the embedding model and the runtime it needs.",
814        detail: "One command, on Windows, macOS and Linux. It fetches ONNX Runtime and the \
815                 nomic-embed-text-v1.5 weights into a per-user directory, checks every byte \
816                 against a digest pinned in this build, and leaves the engine able to answer \
817                 embed(TEXT) with nothing exported by hand - so a mismatch is a refusal that \
818                 names both digests rather than a shared library that loads and misbehaves. \
819                 Name what you want: 'all' installs both halves, 'runtime' and 'model' one \
820                 each. Run with no component at all and it reports what is installed and \
821                 downloads nothing, which is what stops a 620 MB fetch being a surprise; \
822                 '--status' does the same explicitly. About 620 MB the first time and \
823                 nothing on a later run. '--residency' chooses when the model is in memory: \
824                 'resident' keeps it, which is about 1.9 GB held and 12 to 36 ms a query; \
825                 'on-demand' loads it per call, which holds nothing and costs about 0.8 s a \
826                 query; 'idle' or 'idle:90s' loads it on use and drops it after a quiet \
827                 period, which is the default and pays the load once for a burst of \
828                 questions.",
829        params: SETUP_PARAMS,
830        cli_only: None,
831        writes: Writes::Yes,
832        run: crate::setup::setup_embeddings,
833    },
834    Command {
835        name: "version",
836        summary: "Report the engine, the dialect and the driver versions.",
837        detail: "The SQLite version named here is the dialect this engine implements, not a \
838                 library it links. There is no SQLite in this binary.",
839        params: VERSION_PARAMS,
840        cli_only: None,
841        writes: Writes::No,
842        run: verbs::version,
843    },
844    Command {
845        name: "help",
846        summary: "List every command, or explain one in full.",
847        detail: "With no topic it prints the table. With one it prints that command's usage, \
848                 what it is for, and every parameter it takes.",
849        params: HELP_PARAMS,
850        cli_only: None,
851        writes: Writes::No,
852        run: verbs::help,
853    },
854    Command {
855        name: "shell",
856        summary: "Start the interactive shell.",
857        detail: "The sqlite3-shaped REPL, with all 63 dot commands. Everything it can do is also \
858                 reachable non-interactively through 'run'.",
859        params: &[],
860        cli_only: Some(
861            "it is a terminal REPL: it reads a keyboard and writes a screen, and neither exists \
862             at the other end of an MCP call. Use 'run' instead, which drives the same shell.",
863        ),
864        writes: Writes::Yes,
865        run: verbs::shell_placeholder,
866    },
867    Command {
868        name: "mcp",
869        summary: "Serve these commands to an agent over MCP on standard input and output.",
870        detail: "Every command in this table that is not marked cli-only becomes a tool named \
871                 inillucent_<command>, with this same description and these same parameters.",
872        params: &[],
873        cli_only: Some(
874            "it is the server that would be exposing the tools, so offering it as one of them \
875             would let a client ask the server to serve itself.",
876        ),
877        writes: Writes::Yes,
878        run: verbs::mcp_placeholder,
879    },
880];