Skip to content

Commit 1fc5a1c

Browse files
committed
Fix slash skill invoke normalization
1 parent 549ad7c commit 1fc5a1c

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

rust/crates/commands/src/lib.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2293,10 +2293,53 @@ pub fn classify_skills_slash_command(args: Option<&str>) -> SkillSlashDispatch {
22932293
Some(args) if args == "install" || args.starts_with("install ") => {
22942294
SkillSlashDispatch::Local
22952295
}
2296-
Some(args) => SkillSlashDispatch::Invoke(format!("${args}")),
2296+
Some(args) => SkillSlashDispatch::Invoke(format!("${}", args.trim_start_matches('/'))),
22972297
}
22982298
}
22992299

2300+
/// Resolve a skill invocation by validating the skill exists on disk before
2301+
/// returning the dispatch. When the skill is not found, returns `Err` with a
2302+
/// human-readable message that lists nearby skill names.
2303+
pub fn resolve_skill_invocation(
2304+
cwd: &Path,
2305+
args: Option<&str>,
2306+
) -> Result<SkillSlashDispatch, String> {
2307+
let dispatch = classify_skills_slash_command(args);
2308+
if let SkillSlashDispatch::Invoke(ref prompt) = dispatch {
2309+
// Extract the skill name from the "$skill [args]" prompt.
2310+
let skill_token = prompt
2311+
.trim_start_matches('$')
2312+
.split_whitespace()
2313+
.next()
2314+
.unwrap_or_default();
2315+
if !skill_token.is_empty() {
2316+
if let Err(error) = resolve_skill_path(cwd, skill_token) {
2317+
let mut message =
2318+
format!("Unknown skill: {skill_token} ({error})");
2319+
let roots = discover_skill_roots(cwd);
2320+
if let Ok(available) = load_skills_from_roots(&roots) {
2321+
let names: Vec<String> = available
2322+
.iter()
2323+
.filter(|s| s.shadowed_by.is_none())
2324+
.map(|s| s.name.clone())
2325+
.collect();
2326+
if !names.is_empty() {
2327+
message.push_str(&format!(
2328+
"\n Available skills: {}",
2329+
names.join(", ")
2330+
));
2331+
}
2332+
}
2333+
message.push_str(
2334+
"\n Usage: /skills [list|install <path>|help|<skill> [args]]",
2335+
);
2336+
return Err(message);
2337+
}
2338+
}
2339+
}
2340+
Ok(dispatch)
2341+
}
2342+
23002343
pub fn resolve_skill_path(cwd: &Path, skill: &str) -> std::io::Result<PathBuf> {
23012344
let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
23022345
if requested.is_empty() {
@@ -4301,6 +4344,10 @@ mod tests {
43014344
classify_skills_slash_command(Some("help overview")),
43024345
SkillSlashDispatch::Invoke("$help overview".to_string())
43034346
);
4347+
assert_eq!(
4348+
classify_skills_slash_command(Some("/test")),
4349+
SkillSlashDispatch::Invoke("$test".to_string())
4350+
);
43044351
assert_eq!(
43054352
classify_skills_slash_command(Some("install ./skill-pack")),
43064353
SkillSlashDispatch::Local

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7778,6 +7778,17 @@ mod tests {
77787778
output_format: CliOutputFormat::Text,
77797779
}
77807780
);
7781+
assert_eq!(
7782+
parse_args(&["/skills".to_string(), "/test".to_string()])
7783+
.expect("/skills /test should normalize to a single skill prompt prefix"),
7784+
CliAction::Prompt {
7785+
prompt: "$test".to_string(),
7786+
model: DEFAULT_MODEL.to_string(),
7787+
output_format: CliOutputFormat::Text,
7788+
allowed_tools: None,
7789+
permission_mode: crate::default_permission_mode(),
7790+
}
7791+
);
77817792
let error = parse_args(&["/status".to_string()])
77827793
.expect_err("/status should remain REPL-only when invoked directly");
77837794
assert!(error.contains("interactive-only"));

0 commit comments

Comments
 (0)