use clap::{Args, Parser, Subcommand, ValueEnum};
use rqmd_core::store::chunking::ChunkStrategy;
#[derive(Debug, Parser)]
#[command(
name = "rqmd",
version,
about = "On-device hybrid search for markdown (Rust port of tobi/qmd)"
)]
pub struct Cli {
#[arg(long, global = true, value_name = "NAME")]
pub index: Option<String>,
#[arg(long, global = true)]
pub no_gpu: bool,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long)]
pub skill: bool,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Init,
#[command(subcommand)]
Collection(CollectionCmd),
#[command(subcommand)]
Context(ContextCmd),
Get(GetArgs),
#[command(name = "multi-get")]
MultiGet(MultiGetArgs),
Ls(LsArgs),
Status,
Doctor,
Update,
Cleanup,
Search(SearchArgs),
Vsearch(VsearchArgs),
Query(QueryArgs),
Bench(BenchArgs),
Embed(EmbedArgs),
Pull(PullArgs),
Mcp(McpArgs),
#[command(subcommand)]
Skill(SkillCmd),
#[command(subcommand)]
Skills(SkillsCmd),
}
#[derive(Debug, Subcommand)]
pub enum SkillCmd {
Show,
Install(SkillInstallArgs),
}
#[derive(Debug, Args)]
pub struct SkillInstallArgs {
#[arg(long)]
pub global: bool,
#[arg(long)]
pub yes: bool,
#[arg(short = 'f', long)]
pub force: bool,
}
#[derive(Debug, Subcommand)]
pub enum SkillsCmd {
List(SkillsListArgs),
Get(SkillsGetArgs),
Path(SkillsPathArgs),
}
#[derive(Debug, Args)]
pub struct SkillsListArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub struct SkillsGetArgs {
pub name: Option<String>,
#[arg(long)]
pub full: bool,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub struct SkillsPathArgs {
pub name: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum CollectionCmd {
List,
Add(CollectionAddArgs),
#[command(alias = "rm")]
Remove(CollectionRemoveArgs),
#[command(alias = "mv")]
Rename(CollectionRenameArgs),
#[command(alias = "info")]
Show(CollectionShowArgs),
#[command(name = "update-cmd", alias = "set-update")]
UpdateCmd(CollectionUpdateCmdArgs),
Include(CollectionNameArg),
Exclude(CollectionNameArg),
}
#[derive(Debug, Args)]
pub struct CollectionAddArgs {
pub path: Option<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub mask: Option<String>,
}
#[derive(Debug, Args)]
pub struct CollectionRemoveArgs {
pub name: String,
}
#[derive(Debug, Args)]
pub struct CollectionRenameArgs {
pub old: String,
pub new: String,
}
#[derive(Debug, Args)]
pub struct CollectionShowArgs {
pub name: String,
}
#[derive(Debug, Args)]
pub struct CollectionUpdateCmdArgs {
pub name: String,
pub command: Vec<String>,
}
#[derive(Debug, Args)]
pub struct CollectionNameArg {
pub name: String,
}
#[derive(Debug, Subcommand)]
pub enum ContextCmd {
Add(ContextAddArgs),
List,
#[command(alias = "remove")]
Rm(ContextRmArgs),
}
#[derive(Debug, Args)]
pub struct ContextAddArgs {
#[arg(num_args = 1.., required = true)]
pub args: Vec<String>,
}
#[derive(Debug, Args)]
pub struct ContextRmArgs {
pub path: String,
}
#[derive(Debug, Args)]
pub struct GetArgs {
pub file: String,
#[arg(long)]
pub from: Option<usize>,
#[arg(short = 'l')]
pub lines: Option<usize>,
#[arg(long = "line-numbers")]
pub line_numbers: bool,
#[arg(long)]
pub full: bool,
}
#[derive(Debug, Args)]
pub struct MultiGetArgs {
pub pattern: String,
#[arg(short = 'l')]
pub lines: Option<usize>,
#[arg(long = "max-bytes")]
pub max_bytes: Option<usize>,
#[command(flatten)]
pub format: FormatFlags,
}
#[derive(Debug, Args)]
pub struct LsArgs {
pub path: Option<String>,
}
#[derive(Debug, Args, Clone)]
pub struct SearchFlags {
#[arg(short = 'c', long)]
pub collection: Vec<String>,
#[arg(short = 'n', long)]
pub limit: Option<usize>,
#[arg(long)]
pub all: bool,
#[arg(long = "min-score")]
pub min_score: Option<f64>,
#[arg(long)]
pub full: bool,
#[arg(long = "line-numbers")]
pub line_numbers: bool,
}
#[derive(Debug, Args)]
pub struct SearchArgs {
#[command(flatten)]
pub flags: SearchFlags,
#[command(flatten)]
pub format: FormatFlags,
pub query: Vec<String>,
}
#[derive(Debug, Args)]
pub struct VsearchArgs {
#[command(flatten)]
pub flags: SearchFlags,
#[command(flatten)]
pub format: FormatFlags,
#[arg(long)]
pub intent: Option<String>,
pub query: Vec<String>,
}
#[derive(Debug, Args)]
pub struct QueryArgs {
#[command(flatten)]
pub flags: SearchFlags,
#[command(flatten)]
pub format: FormatFlags,
#[arg(long)]
pub intent: Option<String>,
#[arg(short = 'C', long = "candidate-limit")]
pub candidate_limit: Option<usize>,
#[arg(long = "no-rerank")]
pub no_rerank: bool,
#[arg(long)]
pub explain: bool,
#[arg(long = "chunk-strategy", value_enum)]
pub chunk_strategy: Option<ChunkStrategyArg>,
pub query: Vec<String>,
}
#[derive(Debug, Args)]
pub struct BenchArgs {
pub fixture: String,
#[arg(long)]
pub json: bool,
#[arg(short = 'c', long)]
pub collection: Option<String>,
}
#[derive(Debug, Args)]
pub struct EmbedArgs {
#[arg(short = 'f', long)]
pub force: bool,
#[arg(long = "max-docs-per-batch")]
pub max_docs_per_batch: Option<usize>,
#[arg(long = "max-batch-mb")]
pub max_batch_mb: Option<usize>,
#[arg(long = "chunk-strategy", value_enum)]
pub chunk_strategy: Option<ChunkStrategyArg>,
#[arg(short = 'c', long = "collection")]
pub collection: Vec<String>,
}
#[derive(Debug, Args)]
pub struct PullArgs {
#[arg(long)]
pub refresh: bool,
}
#[derive(Debug, Args)]
pub struct McpArgs {
#[arg(value_enum)]
pub action: Option<McpAction>,
#[arg(long)]
pub http: bool,
#[arg(long)]
pub port: Option<u16>,
#[arg(long)]
pub daemon: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum McpAction {
Stop,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ChunkStrategyArg {
Auto,
Regex,
}
impl From<ChunkStrategyArg> for ChunkStrategy {
fn from(v: ChunkStrategyArg) -> Self {
match v {
ChunkStrategyArg::Auto => ChunkStrategy::Auto,
ChunkStrategyArg::Regex => ChunkStrategy::Regex,
}
}
}
#[cfg(test)]
mod arg_order_tests {
use super::*;
fn cmd(argv: &[&str]) -> Command {
Cli::try_parse_from(argv)
.unwrap_or_else(|e| panic!("parse failed for {argv:?}: {e}"))
.command
.expect("subcommand present")
}
#[test]
fn search_flag_after_query_is_parsed_not_swallowed() {
let Command::Search(a) = cmd(&["rqmd", "search", "foo", "bar", "-n", "5"]) else {
panic!("expected search");
};
assert_eq!(a.query, ["foo", "bar"]);
assert_eq!(a.flags.limit, Some(5));
}
#[test]
fn search_flag_before_query_still_works() {
let Command::Search(a) = cmd(&["rqmd", "search", "-n", "5", "foo", "bar"]) else {
panic!("expected search");
};
assert_eq!(a.query, ["foo", "bar"]);
assert_eq!(a.flags.limit, Some(5));
}
#[test]
fn search_value_option_consumes_next_token_then_query() {
let Command::Search(a) = cmd(&["rqmd", "search", "-c", "concepts", "meeting"]) else {
panic!("expected search");
};
assert_eq!(a.flags.collection, ["concepts"]);
assert_eq!(a.query, ["meeting"]);
}
#[test]
fn search_format_flag_after_query() {
let Command::Search(a) = cmd(&["rqmd", "search", "foo", "--json"]) else {
panic!("expected search");
};
assert_eq!(a.query, ["foo"]);
assert!(a.format.json);
}
#[test]
fn search_hyphen_leading_query_needs_double_dash_escape() {
assert!(Cli::try_parse_from(["rqmd", "search", "-foo"]).is_err());
let Command::Search(a) = cmd(&["rqmd", "search", "--", "-foo"]) else {
panic!("expected search");
};
assert_eq!(a.query, ["-foo"]);
}
#[test]
fn vsearch_flag_after_query_is_parsed() {
let Command::Vsearch(a) = cmd(&["rqmd", "vsearch", "foo", "bar", "-n", "3"]) else {
panic!("expected vsearch");
};
assert_eq!(a.query, ["foo", "bar"]);
assert_eq!(a.flags.limit, Some(3));
}
#[test]
fn query_flag_after_query_is_parsed() {
let Command::Query(a) = cmd(&["rqmd", "query", "foo", "bar", "-n", "7"]) else {
panic!("expected query");
};
assert_eq!(a.query, ["foo", "bar"]);
assert_eq!(a.flags.limit, Some(7));
}
#[test]
fn query_intent_flag_after_query_is_parsed() {
let Command::Query(a) = cmd(&["rqmd", "query", "decision", "quality", "--intent", "find"])
else {
panic!("expected query");
};
assert_eq!(a.query, ["decision", "quality"]);
assert_eq!(a.intent.as_deref(), Some("find"));
}
}
#[derive(Debug, Args, Clone, Copy, Default)]
#[group(id = "format", multiple = false)]
pub struct FormatFlags {
#[arg(long)]
pub json: bool,
#[arg(long)]
pub csv: bool,
#[arg(long)]
pub md: bool,
#[arg(long)]
pub xml: bool,
#[arg(long)]
pub files: bool,
}