pub(crate) const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("connect", "Replace the active database profile"),
("connections", "List configured database connections"),
("include", "Add a secondary database profile to query scope"),
("exclude", "Remove a database profile from query scope"),
("provider", "Set or view the AI provider"),
("model", "Set or view the AI model"),
("privacy", "Enable or disable data sharing privacy"),
("approvals", "Set approval policy for tool execution"),
("schema", "Inspect or refresh database schema"),
("doctor", "Diagnose config: secrets, provider endpoint"),
("sql", "Run a raw SQL query against the active profile"),
("export", "Export the last query result as CSV or JSON"),
("chart", "Render the last query as an HTML chart"),
("explain", "Explain the given or last SQL statement"),
("clear", "Clear current session context"),
("history", "List saved sessions as text"),
(
"sessions",
"Browse saved sessions; opens a picker in the TUI",
),
("resume", "Resume a saved session by id"),
("contracts", "List contracts, or show one object's contract"),
("contract", "Alias for /contracts"),
("remember", "Store a confirmed contract claim"),
("forget", "Tombstone a contract claim so recall excludes it"),
("queue", "Show pending candidate claims awaiting review"),
("confirm", "Confirm a pending candidate claim by id prefix"),
("reject", "Reject a pending candidate claim by id prefix"),
("help", "Show help for slash commands"),
("exit", "Exit the REPL"),
("quit", "Exit the REPL"),
];
pub(crate) fn description_for(name: &str) -> Option<&'static str> {
COMMAND_DESCRIPTIONS
.iter()
.find(|(candidate, _)| *candidate == name)
.map(|(_, description)| *description)
}
pub(crate) fn help_text() -> String {
let mut out = String::from("Slash commands:");
for (heading, commands) in LISTING_GROUPS {
out.push_str("\n\n");
out.push_str(heading);
for (name, usage) in *commands {
out.push_str("\n ");
out.push_str(usage);
if let Some(description) = description_for(name) {
out.push_str(" — ");
out.push_str(description);
}
}
}
out
}
const LISTING_GROUPS: &[(&str, &[(&str, &str)])] = &[
(
"Connections",
&[
("connect", "/connect <profile>"),
("connections", "/connections"),
("include", "/include <profile>"),
("exclude", "/exclude <profile>"),
],
),
(
"Provider, model & privacy",
&[
("provider", "/provider [name]"),
("model", "/model [name]"),
("privacy", "/privacy [on|off]"),
("approvals", "/approvals [ask|read-only|never]"),
],
),
(
"Query & data",
&[
("schema", "/schema [refresh]"),
("sql", "/sql <query>"),
("export", "/export <path>"),
("chart", "/chart [type] [path]"),
("explain", "/explain [sql]"),
],
),
(
"Session",
&[
("clear", "/clear"),
("history", "/history"),
("sessions", "/sessions"),
("resume", "/resume <id>"),
("doctor", "/doctor"),
("help", "/help [command]"),
("exit", "/exit (alias /quit)"),
],
),
(
"Memory",
&[
("contracts", "/contracts [table]"),
("remember", "/remember <table> <kind> <value…>"),
("forget", "/forget <id>"),
("queue", "/queue [limit]"),
("confirm", "/confirm <prefix>"),
("reject", "/reject <prefix>"),
],
),
];
pub(crate) fn command_help(name: &str) -> Option<&'static str> {
let clean_name = name.trim_start_matches('/').to_lowercase();
match clean_name.as_str() {
"connect" => {
Some("connect <profile> — set the active database profile. Example: /connect prod")
}
"connections" => Some(
"connections — list configured database connection profiles. Example: /connections",
),
"include" => Some(
"include <profile> — include an additional database profile. Example: /include staging",
),
"exclude" => {
Some("exclude <profile> — exclude a database profile. Example: /exclude staging")
}
"provider" => {
Some("provider [name] — view or set the AI provider. Example: /provider anthropic")
}
"model" => Some("model [name] — view or set the AI model. Example: /model gpt-4o"),
"privacy" => {
Some("privacy [on|off] — view or toggle cloud data sharing. Example: /privacy off")
}
"approvals" => Some(
"approvals [ask|read-only|never] — view or set tool execution approval policy. Example: /approvals ask",
),
"schema" => Some(
"schema [refresh] — display or refresh database schema context. Example: /schema refresh",
),
"sql" => Some(
"sql <query> — execute a raw SQL query directly. Example: /sql SELECT * FROM users LIMIT 10;",
),
"export" => Some(
"export <path> — write the last query's rows to a .csv or .json file. Example: /export results.csv",
),
"chart" => Some(
"chart [type] [path] — render the last query as an interactive HTML chart and open it. type: bar|line|area|pie|doughnut|scatter (default auto)",
),
"explain" => Some(
"explain [sql] — show the query plan (EXPLAIN) for the given SQL, or the last query if omitted",
),
"clear" => Some("clear — clear the conversation and context. Example: /clear"),
"history" => Some("history — list saved sessions as text. Example: /history"),
"sessions" => {
Some("sessions — browse saved sessions; opens a picker in the TUI. Example: /sessions")
}
"doctor" => Some(
"doctor — diagnose configuration: secrets resolve? provider endpoint? Example: /doctor",
),
"resume" => Some("resume <id> — resume a previous session by ID. Example: /resume 12345"),
"contracts" => Some(
"contracts [catalog.schema.object] — list every recalled contract for the active profile, or show one object's contract when you name it. Example: /contracts or /contracts analytics.public.orders",
),
"contract" => Some(
"contract [catalog.schema.object] — alias for /contracts: list every recalled contract, or show one object's contract when you name it. Example: /contract analytics.public.orders",
),
"remember" => Some(
"remember <catalog.schema.object> <kind> <value…> [because <reason…>] — store a confirmed claim. Kinds: description, alias, grain, time-column, column-description <column> <value…>, column-role <column> <role>. The optional `because <reason…>` (directive kinds only) records why the claim holds. Example: /remember analytics.public.orders time-column created_at because orders complete on return",
),
"forget" => Some(
"forget <claim-id> — tombstone a claim so recall excludes it. Example: /forget abc-123",
),
"queue" => {
Some("queue [limit] — list candidate claims awaiting review. Example: /queue 20")
}
"confirm" => Some(
"confirm <claim-id-prefix> — confirm the claim named by its short id prefix (the ki-xxxx form /contracts shows). Example: /confirm ki-a86a3f",
),
"reject" => Some(
"reject <claim-id-prefix> — reject the claim named by its short id prefix. Example: /reject ki-a86a3f",
),
"help" => Some(
"help [command] — display general help or detailed usage for a command. Example: /help connect",
),
"exit" | "quit" => Some("exit — exit the interactive CLI session. Example: /exit"),
_ => None,
}
}
pub(crate) fn help_for(topic: Option<&str>) -> String {
match topic {
Some(name) => {
let clean = name.trim_start_matches('/');
match command_help(clean) {
Some(help) => help.to_string(),
None => format!("No help for /{clean}. Type /help for the full list."),
}
}
None => help_text().to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::slash::registry;
#[test]
fn history_and_sessions_help_describe_their_real_surfaces() {
let history = command_help("history").expect("history has help");
let sessions = command_help("sessions").expect("sessions has help");
assert!(
!history.contains("alias") && !sessions.contains("alias"),
"neither may claim aliasing — the TUI routes them differently: {history} / {sessions}"
);
assert!(
sessions.contains("picker"),
"`/sessions` help must mention the picker it opens in the TUI, got: {sessions}"
);
assert!(
history.contains("saved") && sessions.contains("saved"),
"both must name saved sessions, got: {history} / {sessions}"
);
}
#[test]
fn clear_help_describes_the_conversation_not_history() {
let clear = command_help("clear").expect("clear has help");
assert!(
clear.contains("conversation") && clear.contains("context"),
"/clear help should describe the conversation and context, got: {clear}"
);
assert!(
!clear.contains("history"),
"/clear help must not reuse the overloaded 'history' word (now = saved sessions), got: {clear}"
);
}
#[test]
fn listing_gives_every_command_a_description() {
let summary = help_text();
let bare = summary
.lines()
.map(str::trim_start)
.filter(|line| line.starts_with('/'))
.filter(|line| !line.contains(" — "))
.collect::<Vec<_>>();
assert!(
bare.is_empty(),
"every command line should carry a description (— ...), \
but these are bare syntax with no description:\n{}",
bare.join("\n")
);
}
#[test]
fn connect_and_include_read_as_a_contrast() {
let summary = help_text();
let connect_line = summary
.lines()
.find(|line| line.trim_start().starts_with("/connect "))
.unwrap_or_else(|| panic!("listing must have a /connect line, got:\n{summary}"));
let include_line = summary
.lines()
.find(|line| line.trim_start().starts_with("/include "))
.unwrap_or_else(|| panic!("listing must have a /include line, got:\n{summary}"));
assert!(
connect_line.to_lowercase().contains("replace"),
"/connect listing must say it replaces the active profile, got: {connect_line}"
);
assert!(
include_line.to_lowercase().contains("secondary"),
"/include listing must say it adds a secondary profile, got: {include_line}"
);
}
#[test]
fn command_descriptions_cover_exactly_the_registry() {
assert_eq!(
COMMAND_DESCRIPTIONS.len(),
registry::KNOWN_COMMANDS.len(),
"the description table and the command registry must list the same commands"
);
for (name, _) in COMMAND_DESCRIPTIONS {
assert!(
registry::KNOWN_COMMANDS.contains(name),
"{name} is described but not in the registry"
);
}
for name in registry::KNOWN_COMMANDS {
assert!(
description_for(name).is_some(),
"{name} is registered but has no description"
);
}
}
#[test]
fn listing_groups_commands_under_headings() {
let summary = help_text();
for heading in [
"Connections",
"Provider, model & privacy",
"Query & data",
"Session",
"Memory",
] {
assert!(
summary.lines().any(|line| line.trim() == heading),
"listing must have a {heading:?} heading on its own line, got:\n{summary}"
);
}
}
#[test]
fn contracts_help_covers_both_forms_and_marks_the_optional_argument() {
let summary = help_text();
assert!(
summary.contains("/contracts [table]"),
"summary must show the merged /contracts [table] form, got: {summary}"
);
assert!(
!summary.contains("/contract <table>"),
"summary must not keep the old separate /contract <table> entry, got: {summary}"
);
let contracts = command_help("contracts").expect("contracts has help");
assert!(
contracts.contains("list every recalled contract"),
"/contracts help must name the list form, got: {contracts}"
);
assert!(
contracts.contains("show one object's contract"),
"/contracts help must name the show form, got: {contracts}"
);
assert!(
contracts.contains("[catalog.schema.object]"),
"/contracts help must mark the optional argument with brackets, got: {contracts}"
);
assert!(
contracts.contains("/contracts") && contracts.contains("analytics.public.orders"),
"/contracts help must show a no-arg and a with-arg example, got: {contracts}"
);
let contract = command_help("contract").expect("contract still has help");
assert!(
contract.contains("alias"),
"/contract help must name itself an alias of /contracts, got: {contract}"
);
assert!(
contract.contains("show one object's contract"),
"/contract help must describe the merged show form, got: {contract}"
);
}
}