feat(autocorrect): add fuzzy subcommand matching and flag-as-subcommand detection
Extend the CLI autocorrection pipeline with two new correction rules that help agents recover from common typos and misunderstandings: 1. SubcommandFuzzy (threshold 0.85): Fuzzy-matches typo'd subcommands against the canonical list. Examples: - "issuess" → "issues" - "timline" → "timeline" - "serach" → "search" Guards prevent false positives: - Words that look like misplaced global flags are skipped - Valid command prefixes are left to clap's infer_subcommands 2. FlagAsSubcommand: Detects when agents type subcommands as flags. Some agents (especially Codex) assume `--robot-docs` is a flag rather than a subcommand. This rule converts: - "--robot-docs" → "robot-docs" - "--generate-docs" → "generate-docs" Also improves error messages in main.rs: - MissingRequiredArgument: Contextual example based on detected subcommand - MissingSubcommand: Lists common commands - TooFewValues/TooManyValues: Command-specific help hints Added CANONICAL_SUBCOMMANDS constant enumerating all valid subcommands (including hidden ones) for fuzzy matching. This ensures agents that know about hidden commands still get typo correction. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
110
src/main.rs
110
src/main.rs
@@ -1,3 +1,5 @@
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
use clap::Parser;
|
||||
use dialoguer::{Confirm, Input};
|
||||
use serde::Serialize;
|
||||
@@ -606,6 +608,38 @@ fn handle_clap_error(e: clap::Error, robot_mode: bool, corrections: &CorrectionR
|
||||
});
|
||||
(suggestion, None, vals_vec)
|
||||
}
|
||||
ErrorKind::MissingRequiredArgument => {
|
||||
let suggestion = format!(
|
||||
"A required argument is missing. {}",
|
||||
if let Some(subcmd) = extract_subcommand_from_context(&e) {
|
||||
format!(
|
||||
"Example: {}. Run 'lore {subcmd} --help' for required arguments",
|
||||
command_example(&subcmd)
|
||||
)
|
||||
} else {
|
||||
"Run 'lore robot-docs' for command reference".to_string()
|
||||
}
|
||||
);
|
||||
(suggestion, None, None)
|
||||
}
|
||||
ErrorKind::MissingSubcommand => {
|
||||
let suggestion =
|
||||
"No command specified. Common commands: issues, mrs, search, sync, \
|
||||
timeline, who, me. Run 'lore robot-docs' for the full list"
|
||||
.to_string();
|
||||
(suggestion, None, None)
|
||||
}
|
||||
ErrorKind::TooFewValues | ErrorKind::TooManyValues => {
|
||||
let suggestion = if let Some(subcmd) = extract_subcommand_from_context(&e) {
|
||||
format!(
|
||||
"Example: {}. Run 'lore {subcmd} --help' for usage",
|
||||
command_example(&subcmd)
|
||||
)
|
||||
} else {
|
||||
"Run 'lore robot-docs' for command reference".to_string()
|
||||
};
|
||||
(suggestion, None, None)
|
||||
}
|
||||
_ => (
|
||||
"Run 'lore robot-docs' for valid commands".to_string(),
|
||||
None,
|
||||
@@ -720,6 +754,45 @@ fn extract_invalid_value_context(e: &clap::Error) -> (Option<String>, Option<Vec
|
||||
(flag, valid_values)
|
||||
}
|
||||
|
||||
/// Extract the subcommand context from a clap error for better suggestions.
|
||||
/// Looks at the error message to find which command was being invoked.
|
||||
fn extract_subcommand_from_context(e: &clap::Error) -> Option<String> {
|
||||
let msg = e.to_string();
|
||||
|
||||
let known = [
|
||||
"issues",
|
||||
"mrs",
|
||||
"notes",
|
||||
"search",
|
||||
"sync",
|
||||
"ingest",
|
||||
"count",
|
||||
"status",
|
||||
"auth",
|
||||
"doctor",
|
||||
"stats",
|
||||
"timeline",
|
||||
"who",
|
||||
"me",
|
||||
"drift",
|
||||
"related",
|
||||
"trace",
|
||||
"file-history",
|
||||
"generate-docs",
|
||||
"embed",
|
||||
"token",
|
||||
"cron",
|
||||
"init",
|
||||
"migrate",
|
||||
];
|
||||
for cmd in known {
|
||||
if msg.contains(&format!("lore {cmd}")) || msg.contains(&format!("'{cmd}'")) {
|
||||
return Some(cmd.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Phase 4: Suggest similar command using fuzzy matching
|
||||
fn suggest_similar_command(invalid: &str) -> String {
|
||||
// Primary commands + common aliases for fuzzy matching
|
||||
@@ -755,6 +828,15 @@ fn suggest_similar_command(invalid: &str) -> String {
|
||||
("drift", "drift"),
|
||||
("file-history", "file-history"),
|
||||
("trace", "trace"),
|
||||
("related", "related"),
|
||||
("me", "me"),
|
||||
("token", "token"),
|
||||
("cron", "cron"),
|
||||
// Hidden but may be known to agents
|
||||
("list", "list"),
|
||||
("show", "show"),
|
||||
("reset", "reset"),
|
||||
("backup", "backup"),
|
||||
];
|
||||
|
||||
let invalid_lower = invalid.to_lowercase();
|
||||
@@ -798,6 +880,16 @@ fn command_example(cmd: &str) -> &'static str {
|
||||
"robot-docs" => "lore robot-docs",
|
||||
"trace" => "lore --robot trace src/main.rs",
|
||||
"init" => "lore init",
|
||||
"related" => "lore --robot related issues 42 -n 5",
|
||||
"me" => "lore --robot me",
|
||||
"drift" => "lore --robot drift issues 42",
|
||||
"file-history" => "lore --robot file-history src/main.rs",
|
||||
"token" => "lore --robot token show",
|
||||
"cron" => "lore --robot cron status",
|
||||
"auth" => "lore --robot auth",
|
||||
"doctor" => "lore --robot doctor",
|
||||
"migrate" => "lore --robot migrate",
|
||||
"completions" => "lore completions bash",
|
||||
_ => "lore --robot <command>",
|
||||
}
|
||||
}
|
||||
@@ -3186,31 +3278,33 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
||||
}
|
||||
},
|
||||
"me": {
|
||||
"description": "Personal work dashboard: open issues, authored/reviewing MRs, activity feed, and cursor-based since-last-check inbox with computed attention states",
|
||||
"flags": ["--issues", "--mrs", "--activity", "--since <period>", "-p/--project <path>", "--all", "--user <username>", "--fields <list|minimal>", "--reset-cursor"],
|
||||
"description": "Personal work dashboard: open issues, authored/reviewing MRs, @mentioned-in items, activity feed, and cursor-based since-last-check inbox with computed attention states",
|
||||
"flags": ["--issues", "--mrs", "--mentions", "--activity", "--since <period>", "-p/--project <path>", "--all", "--user <username>", "--fields <list|minimal>", "--reset-cursor"],
|
||||
"example": "lore --robot me",
|
||||
"response_schema": {
|
||||
"ok": "bool",
|
||||
"data": {
|
||||
"username": "string",
|
||||
"since_iso": "string?",
|
||||
"summary": {"project_count": "int", "open_issue_count": "int", "authored_mr_count": "int", "reviewing_mr_count": "int", "needs_attention_count": "int"},
|
||||
"summary": {"project_count": "int", "open_issue_count": "int", "authored_mr_count": "int", "reviewing_mr_count": "int", "mentioned_in_count": "int", "needs_attention_count": "int"},
|
||||
"since_last_check": "{cursor_iso:string, total_event_count:int, groups:[{entity_type:string, entity_iid:int, entity_title:string, project:string, events:[{timestamp_iso:string, event_type:string, actor:string?, summary:string, body_preview:string?}]}]}?",
|
||||
"open_issues": "[{project:string, iid:int, title:string, state:string, attention_state:string, status_name:string?, labels:[string], updated_at_iso:string, web_url:string?}]",
|
||||
"open_mrs_authored": "[{project:string, iid:int, title:string, state:string, attention_state:string, draft:bool, detailed_merge_status:string?, author_username:string?, labels:[string], updated_at_iso:string, web_url:string?}]",
|
||||
"open_issues": "[{project:string, iid:int, title:string, state:string, attention_state:string, attention_reason:string, status_name:string?, labels:[string], updated_at_iso:string, web_url:string?}]",
|
||||
"open_mrs_authored": "[{project:string, iid:int, title:string, state:string, attention_state:string, attention_reason:string, draft:bool, detailed_merge_status:string?, author_username:string?, labels:[string], updated_at_iso:string, web_url:string?}]",
|
||||
"reviewing_mrs": "[same as open_mrs_authored]",
|
||||
"mentioned_in": "[{entity_type:string, project:string, iid:int, title:string, state:string, attention_state:string, attention_reason:string, updated_at_iso:string, web_url:string?}]",
|
||||
"activity": "[{timestamp_iso:string, event_type:string, entity_type:string, entity_iid:int, project:string, actor:string?, is_own:bool, summary:string, body_preview:string?}]"
|
||||
},
|
||||
"meta": {"elapsed_ms": "int"}
|
||||
},
|
||||
"fields_presets": {
|
||||
"me_items_minimal": ["iid", "title", "attention_state", "updated_at_iso"],
|
||||
"me_items_minimal": ["iid", "title", "attention_state", "attention_reason", "updated_at_iso"],
|
||||
"me_mentions_minimal": ["entity_type", "iid", "title", "state", "attention_state", "attention_reason", "updated_at_iso"],
|
||||
"me_activity_minimal": ["timestamp_iso", "event_type", "entity_iid", "actor"]
|
||||
},
|
||||
"notes": {
|
||||
"attention_states": "needs_attention | not_started | awaiting_response | stale | not_ready",
|
||||
"event_types": "note | status_change | label_change | assign | unassign | review_request | milestone_change",
|
||||
"section_flags": "If none of --issues/--mrs/--activity specified, all sections returned",
|
||||
"section_flags": "If none of --issues/--mrs/--mentions/--activity specified, all sections returned",
|
||||
"since_default": "1d for activity feed",
|
||||
"issue_filter": "Only In Progress / In Review status issues shown",
|
||||
"since_last_check": "Cursor-based inbox showing events since last run. Null on first run (no cursor yet). Groups events by entity (issue/MR). Sources: others' comments on your items, @mentions, assignment/review-request notes. Cursor auto-advances after each run. Use --reset-cursor to clear.",
|
||||
@@ -3372,6 +3466,8 @@ fn handle_robot_docs(robot_mode: bool, brief: bool) -> Result<(), Box<dyn std::e
|
||||
{"type": "flag_prefix", "example": "--proj -> --project (when unambiguous)", "mode": "all"},
|
||||
{"type": "fuzzy_flag", "example": "--projct -> --project", "mode": "all (threshold 0.9 in robot, 0.8 in human)"},
|
||||
{"type": "subcommand_alias", "example": "merge_requests -> mrs, robotdocs -> robot-docs", "mode": "all"},
|
||||
{"type": "subcommand_fuzzy", "example": "issuess -> issues, timline -> timeline, serach -> search", "mode": "all (threshold 0.85)"},
|
||||
{"type": "flag_as_subcommand", "example": "--robot-docs -> robot-docs, --generate-docs -> generate-docs", "mode": "all"},
|
||||
{"type": "value_normalization", "example": "--state Opened -> --state opened", "mode": "all"},
|
||||
{"type": "value_fuzzy", "example": "--state opend -> --state opened", "mode": "all"},
|
||||
{"type": "prefix_matching", "example": "lore iss -> lore issues, lore time -> lore timeline", "mode": "all (via clap infer_subcommands)"}
|
||||
|
||||
Reference in New Issue
Block a user