mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-07 00:24:50 +08:00
Let /skills invocations reach the prompt skill path
The CLI still treated every /skills payload other than list/install/help as local usage text, so skills that appeared in /skills could not actually be invoked. This restores prompt dispatch for /skills <skill> [args], keeps list/install on the local path, and shares skill resolution with the Skill tool so project-local and legacy /commands entries resolve consistently. Constraint: --resume local slash execution still only supports local commands without provider turns Rejected: Implement full resumed prompt-turn execution for /skills | larger behavior change outside this bugfix Rejected: Keep separate skill lookups in tools and commands | drift already caused listing/invocation mismatches Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep /skills discovery, CLI prompt dispatch, and Tool Skill resolution on the same registry semantics Tested: cargo fmt --all; cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets -- -D warnings; cargo test --workspace -- --nocapture Not-tested: Live provider-backed /skills invocation against external skill packs in an interactive REPL
This commit is contained in:
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
@@ -1579,6 +1579,7 @@ name = "tools"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"api",
|
"api",
|
||||||
|
"commands",
|
||||||
"plugins",
|
"plugins",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"runtime",
|
"runtime",
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ pub struct SlashCommandSpec {
|
|||||||
pub resume_supported: bool,
|
pub resume_supported: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum SkillSlashDispatch {
|
||||||
|
Local,
|
||||||
|
Invoke(String),
|
||||||
|
}
|
||||||
|
|
||||||
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "help",
|
name: "help",
|
||||||
@@ -238,8 +244,8 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "skills",
|
name: "skills",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
summary: "List or install available skills",
|
summary: "List, install, or invoke available skills",
|
||||||
argument_hint: Some("[list|install <path>|help]"),
|
argument_hint: Some("[list|install <path>|help|<skill> [args]]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
@@ -1686,13 +1692,7 @@ fn parse_skills_args(args: Option<&str>) -> Result<Option<String>, SlashCommandP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(command_error(
|
Ok(Some(args.to_string()))
|
||||||
&format!(
|
|
||||||
"Unexpected arguments for /skills: {args}. Use /skills, /skills list, /skills install <path>, or /skills help."
|
|
||||||
),
|
|
||||||
"skills",
|
|
||||||
"/skills [list|install <path>|help]",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage_error(command: &str, argument_hint: &str) -> SlashCommandParseError {
|
fn usage_error(command: &str, argument_hint: &str) -> SlashCommandParseError {
|
||||||
@@ -2286,6 +2286,89 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn classify_skills_slash_command(args: Option<&str>) -> SkillSlashDispatch {
|
||||||
|
match normalize_optional_args(args) {
|
||||||
|
None | Some("list" | "help" | "-h" | "--help") => SkillSlashDispatch::Local,
|
||||||
|
Some(args) if args == "install" || args.starts_with("install ") => {
|
||||||
|
SkillSlashDispatch::Local
|
||||||
|
}
|
||||||
|
Some(args) => SkillSlashDispatch::Invoke(format!("${args}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_skill_path(cwd: &Path, skill: &str) -> std::io::Result<PathBuf> {
|
||||||
|
let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
|
||||||
|
if requested.is_empty() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"skill must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let roots = discover_skill_roots(cwd);
|
||||||
|
for root in &roots {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for entry in fs::read_dir(&root.path)? {
|
||||||
|
let entry = entry?;
|
||||||
|
match root.origin {
|
||||||
|
SkillOrigin::SkillsDir => {
|
||||||
|
if !entry.path().is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let skill_path = entry.path().join("SKILL.md");
|
||||||
|
if !skill_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let contents = fs::read_to_string(&skill_path)?;
|
||||||
|
let (name, _) = parse_skill_frontmatter(&contents);
|
||||||
|
entries.push((
|
||||||
|
name.unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()),
|
||||||
|
skill_path,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
SkillOrigin::LegacyCommandsDir => {
|
||||||
|
let path = entry.path();
|
||||||
|
let markdown_path = if path.is_dir() {
|
||||||
|
let skill_path = path.join("SKILL.md");
|
||||||
|
if !skill_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skill_path
|
||||||
|
} else if path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.to_string_lossy().eq_ignore_ascii_case("md"))
|
||||||
|
{
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(&markdown_path)?;
|
||||||
|
let fallback_name = markdown_path.file_stem().map_or_else(
|
||||||
|
|| entry.file_name().to_string_lossy().to_string(),
|
||||||
|
|stem| stem.to_string_lossy().to_string(),
|
||||||
|
);
|
||||||
|
let (name, _) = parse_skill_frontmatter(&contents);
|
||||||
|
entries.push((name.unwrap_or(fallback_name), markdown_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.sort_by(|left, right| left.0.cmp(&right.0));
|
||||||
|
if let Some((_, path)) = entries
|
||||||
|
.into_iter()
|
||||||
|
.find(|(name, _)| name.eq_ignore_ascii_case(requested))
|
||||||
|
{
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
format!("unknown skill: {requested}"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn render_mcp_report_for(
|
fn render_mcp_report_for(
|
||||||
loader: &ConfigLoader,
|
loader: &ConfigLoader,
|
||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
@@ -3383,8 +3466,9 @@ fn render_agents_usage_json(unexpected: Option<&str>) -> Value {
|
|||||||
fn render_skills_usage(unexpected: Option<&str>) -> String {
|
fn render_skills_usage(unexpected: Option<&str>) -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
"Skills".to_string(),
|
"Skills".to_string(),
|
||||||
" Usage /skills [list|install <path>|help]".to_string(),
|
" Usage /skills [list|install <path>|help|<skill> [args]]".to_string(),
|
||||||
" Direct CLI claw skills [list|install <path>|help]".to_string(),
|
" Direct CLI claw skills [list|install <path>|help|<skill> [args]]".to_string(),
|
||||||
|
" Invoke /skills help overview -> $help overview".to_string(),
|
||||||
" Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills".to_string(),
|
" Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills".to_string(),
|
||||||
" Sources .claw/skills, ~/.claw/skills, legacy /commands".to_string(),
|
" Sources .claw/skills, ~/.claw/skills, legacy /commands".to_string(),
|
||||||
];
|
];
|
||||||
@@ -3399,8 +3483,9 @@ fn render_skills_usage_json(unexpected: Option<&str>) -> Value {
|
|||||||
"kind": "skills",
|
"kind": "skills",
|
||||||
"action": "help",
|
"action": "help",
|
||||||
"usage": {
|
"usage": {
|
||||||
"slash_command": "/skills [list|install <path>|help]",
|
"slash_command": "/skills [list|install <path>|help|<skill> [args]]",
|
||||||
"direct_cli": "claw skills [list|install <path>|help]",
|
"direct_cli": "claw skills [list|install <path>|help|<skill> [args]]",
|
||||||
|
"invoke": "/skills help overview -> $help overview",
|
||||||
"install_root": "$CLAW_CONFIG_HOME/skills or ~/.claw/skills",
|
"install_root": "$CLAW_CONFIG_HOME/skills or ~/.claw/skills",
|
||||||
"sources": [".claw/skills", "legacy /commands", "legacy fallback dirs still load automatically"],
|
"sources": [".claw/skills", "legacy /commands", "legacy fallback dirs still load automatically"],
|
||||||
},
|
},
|
||||||
@@ -3751,13 +3836,14 @@ pub fn handle_slash_command(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
handle_agents_slash_command_json, handle_plugins_slash_command,
|
classify_skills_slash_command, handle_agents_slash_command_json,
|
||||||
handle_skills_slash_command_json, handle_slash_command, load_agents_from_roots,
|
handle_plugins_slash_command, handle_skills_slash_command_json, handle_slash_command,
|
||||||
load_skills_from_roots, render_agents_report, render_agents_report_json,
|
load_agents_from_roots, load_skills_from_roots, render_agents_report,
|
||||||
render_mcp_report_json_for, render_plugins_report, render_skills_report,
|
render_agents_report_json, render_mcp_report_json_for, render_plugins_report,
|
||||||
render_slash_command_help, render_slash_command_help_detail,
|
render_skills_report, render_slash_command_help, render_slash_command_help_detail,
|
||||||
resume_supported_slash_commands, slash_command_specs, suggest_slash_commands,
|
resolve_skill_path, resume_supported_slash_commands, slash_command_specs,
|
||||||
validate_slash_command_input, DefinitionSource, SkillOrigin, SkillRoot, SlashCommand,
|
suggest_slash_commands, validate_slash_command_input, DefinitionSource, SkillOrigin,
|
||||||
|
SkillRoot, SkillSlashDispatch, SlashCommand,
|
||||||
};
|
};
|
||||||
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
||||||
use runtime::{
|
use runtime::{
|
||||||
@@ -4105,24 +4191,36 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_invalid_agents_and_skills_arguments() {
|
fn rejects_invalid_agents_arguments() {
|
||||||
// given
|
// given
|
||||||
let agents_input = "/agents show planner";
|
let agents_input = "/agents show planner";
|
||||||
let skills_input = "/skills show help";
|
|
||||||
|
|
||||||
// when
|
// when
|
||||||
let agents_error = parse_error_message(agents_input);
|
let agents_error = parse_error_message(agents_input);
|
||||||
let skills_error = parse_error_message(skills_input);
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert!(agents_error.contains(
|
assert!(agents_error.contains(
|
||||||
"Unexpected arguments for /agents: show planner. Use /agents, /agents list, or /agents help."
|
"Unexpected arguments for /agents: show planner. Use /agents, /agents list, or /agents help."
|
||||||
));
|
));
|
||||||
assert!(agents_error.contains(" Usage /agents [list|help]"));
|
assert!(agents_error.contains(" Usage /agents [list|help]"));
|
||||||
assert!(skills_error.contains(
|
}
|
||||||
"Unexpected arguments for /skills: show help. Use /skills, /skills list, /skills install <path>, or /skills help."
|
|
||||||
));
|
#[test]
|
||||||
assert!(skills_error.contains(" Usage /skills [list|install <path>|help]"));
|
fn accepts_skills_invocation_arguments_for_prompt_dispatch() {
|
||||||
|
assert_eq!(
|
||||||
|
SlashCommand::parse("/skills help overview"),
|
||||||
|
Ok(Some(SlashCommand::Skills {
|
||||||
|
args: Some("help overview".to_string()),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_skills_slash_command(Some("help overview")),
|
||||||
|
SkillSlashDispatch::Invoke("$help overview".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_skills_slash_command(Some("install ./skill-pack")),
|
||||||
|
SkillSlashDispatch::Local
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4176,7 +4274,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
assert!(help.contains("aliases: /plugins, /marketplace"));
|
assert!(help.contains("aliases: /plugins, /marketplace"));
|
||||||
assert!(help.contains("/agents [list|help]"));
|
assert!(help.contains("/agents [list|help]"));
|
||||||
assert!(help.contains("/skills [list|install <path>|help]"));
|
assert!(help.contains("/skills [list|install <path>|help|<skill> [args]]"));
|
||||||
assert_eq!(slash_command_specs().len(), 141);
|
assert_eq!(slash_command_specs().len(), 141);
|
||||||
assert!(resume_supported_slash_commands().len() >= 39);
|
assert!(resume_supported_slash_commands().len() >= 39);
|
||||||
}
|
}
|
||||||
@@ -4541,6 +4639,25 @@ mod tests {
|
|||||||
let _ = fs::remove_dir_all(user_home);
|
let _ = fs::remove_dir_all(user_home);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_project_skills_and_legacy_commands_from_shared_registry() {
|
||||||
|
let workspace = temp_dir("resolve-project-skills");
|
||||||
|
let project_skills = workspace.join(".claw").join("skills");
|
||||||
|
let legacy_commands = workspace.join(".claw").join("commands");
|
||||||
|
|
||||||
|
write_skill(&project_skills, "plan", "Project planning guidance");
|
||||||
|
write_legacy_command(&legacy_commands, "handoff", "Legacy handoff guidance");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_skill_path(&workspace, "$plan").expect("project skill should resolve"),
|
||||||
|
project_skills.join("plan").join("SKILL.md")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_skill_path(&workspace, "/handoff").expect("legacy command should resolve"),
|
||||||
|
legacy_commands.join("handoff.md")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn renders_skills_reports_as_json() {
|
fn renders_skills_reports_as_json() {
|
||||||
let workspace = temp_dir("skills-json-workspace");
|
let workspace = temp_dir("skills-json-workspace");
|
||||||
@@ -4589,7 +4706,7 @@ mod tests {
|
|||||||
assert_eq!(help["action"], "help");
|
assert_eq!(help["action"], "help");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
help["usage"]["direct_cli"],
|
help["usage"]["direct_cli"],
|
||||||
"claw skills [list|install <path>|help]"
|
"claw skills [list|install <path>|help|<skill> [args]]"
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(workspace);
|
let _ = fs::remove_dir_all(workspace);
|
||||||
@@ -4613,7 +4730,9 @@ mod tests {
|
|||||||
|
|
||||||
let skills_help =
|
let skills_help =
|
||||||
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
|
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
|
||||||
assert!(skills_help.contains("Usage /skills [list|install <path>|help]"));
|
assert!(skills_help
|
||||||
|
.contains("Usage /skills [list|install <path>|help|<skill> [args]]"));
|
||||||
|
assert!(skills_help.contains("Invoke /skills help overview -> $help overview"));
|
||||||
assert!(skills_help.contains("Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills"));
|
assert!(skills_help.contains("Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills"));
|
||||||
assert!(skills_help.contains("legacy /commands"));
|
assert!(skills_help.contains("legacy /commands"));
|
||||||
|
|
||||||
@@ -4623,12 +4742,14 @@ mod tests {
|
|||||||
|
|
||||||
let skills_install_help = super::handle_skills_slash_command(Some("install --help"), &cwd)
|
let skills_install_help = super::handle_skills_slash_command(Some("install --help"), &cwd)
|
||||||
.expect("nested skills help");
|
.expect("nested skills help");
|
||||||
assert!(skills_install_help.contains("Usage /skills [list|install <path>|help]"));
|
assert!(skills_install_help
|
||||||
|
.contains("Usage /skills [list|install <path>|help|<skill> [args]]"));
|
||||||
assert!(skills_install_help.contains("Unexpected install"));
|
assert!(skills_install_help.contains("Unexpected install"));
|
||||||
|
|
||||||
let skills_unknown_help =
|
let skills_unknown_help =
|
||||||
super::handle_skills_slash_command(Some("show --help"), &cwd).expect("skills help");
|
super::handle_skills_slash_command(Some("show --help"), &cwd).expect("skills help");
|
||||||
assert!(skills_unknown_help.contains("Usage /skills [list|install <path>|help]"));
|
assert!(skills_unknown_help
|
||||||
|
.contains("Usage /skills [list|install <path>|help|<skill> [args]]"));
|
||||||
assert!(skills_unknown_help.contains("Unexpected show"));
|
assert!(skills_unknown_help.contains("Unexpected show"));
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(cwd);
|
let _ = fs::remove_dir_all(cwd);
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ use api::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
handle_agents_slash_command, handle_agents_slash_command_json, handle_mcp_slash_command,
|
classify_skills_slash_command, handle_agents_slash_command, handle_agents_slash_command_json,
|
||||||
handle_mcp_slash_command_json, handle_plugins_slash_command, handle_skills_slash_command,
|
handle_mcp_slash_command, handle_mcp_slash_command_json, handle_plugins_slash_command,
|
||||||
handle_skills_slash_command_json, render_slash_command_help, resume_supported_slash_commands,
|
handle_skills_slash_command, handle_skills_slash_command_json, render_slash_command_help,
|
||||||
slash_command_specs, validate_slash_command_input, SlashCommand,
|
resume_supported_slash_commands, slash_command_specs, validate_slash_command_input,
|
||||||
|
SkillSlashDispatch, SlashCommand,
|
||||||
};
|
};
|
||||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||||
use init::initialize_repo;
|
use init::initialize_repo;
|
||||||
@@ -419,10 +420,22 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
args: join_optional_args(&rest[1..]),
|
args: join_optional_args(&rest[1..]),
|
||||||
output_format,
|
output_format,
|
||||||
}),
|
}),
|
||||||
"skills" => Ok(CliAction::Skills {
|
"skills" => {
|
||||||
args: join_optional_args(&rest[1..]),
|
let args = join_optional_args(&rest[1..]);
|
||||||
output_format,
|
match classify_skills_slash_command(args.as_deref()) {
|
||||||
}),
|
SkillSlashDispatch::Invoke(prompt) => Ok(CliAction::Prompt {
|
||||||
|
prompt,
|
||||||
|
model,
|
||||||
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
}),
|
||||||
|
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||||
|
args,
|
||||||
|
output_format,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
|
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
|
||||||
"login" => Ok(CliAction::Login { output_format }),
|
"login" => Ok(CliAction::Login { output_format }),
|
||||||
"logout" => Ok(CliAction::Logout { output_format }),
|
"logout" => Ok(CliAction::Logout { output_format }),
|
||||||
@@ -440,7 +453,13 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
other if other.starts_with('/') => parse_direct_slash_cli_action(&rest, output_format),
|
other if other.starts_with('/') => parse_direct_slash_cli_action(
|
||||||
|
&rest,
|
||||||
|
model,
|
||||||
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
),
|
||||||
_other => Ok(CliAction::Prompt {
|
_other => Ok(CliAction::Prompt {
|
||||||
prompt: rest.join(" "),
|
prompt: rest.join(" "),
|
||||||
model,
|
model,
|
||||||
@@ -532,7 +551,10 @@ fn join_optional_args(args: &[String]) -> Option<String> {
|
|||||||
|
|
||||||
fn parse_direct_slash_cli_action(
|
fn parse_direct_slash_cli_action(
|
||||||
rest: &[String],
|
rest: &[String],
|
||||||
|
model: String,
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
) -> Result<CliAction, String> {
|
) -> Result<CliAction, String> {
|
||||||
let raw = rest.join(" ");
|
let raw = rest.join(" ");
|
||||||
match SlashCommand::parse(&raw) {
|
match SlashCommand::parse(&raw) {
|
||||||
@@ -550,10 +572,21 @@ fn parse_direct_slash_cli_action(
|
|||||||
},
|
},
|
||||||
output_format,
|
output_format,
|
||||||
}),
|
}),
|
||||||
Ok(Some(SlashCommand::Skills { args })) => Ok(CliAction::Skills {
|
Ok(Some(SlashCommand::Skills { args })) => {
|
||||||
args,
|
match classify_skills_slash_command(args.as_deref()) {
|
||||||
output_format,
|
SkillSlashDispatch::Invoke(prompt) => Ok(CliAction::Prompt {
|
||||||
}),
|
prompt,
|
||||||
|
model,
|
||||||
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
}),
|
||||||
|
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||||
|
args,
|
||||||
|
output_format,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
|
Ok(Some(SlashCommand::Unknown(name))) => Err(format_unknown_direct_slash_command(&name)),
|
||||||
Ok(Some(command)) => Err({
|
Ok(Some(command)) => Err({
|
||||||
let _ = command;
|
let _ = command;
|
||||||
@@ -2281,6 +2314,11 @@ fn run_resume_command(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Skills { args } => {
|
SlashCommand::Skills { args } => {
|
||||||
|
if let SkillSlashDispatch::Invoke(_) = classify_skills_slash_command(args.as_deref()) {
|
||||||
|
return Err(
|
||||||
|
"resumed /skills invocations are interactive-only; start `claw` and run `/skills <skill>` in the REPL".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
@@ -3203,7 +3241,12 @@ impl LiveCli {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Skills { args } => {
|
SlashCommand::Skills { args } => {
|
||||||
Self::print_skills(args.as_deref(), CliOutputFormat::Text)?;
|
match classify_skills_slash_command(args.as_deref()) {
|
||||||
|
SkillSlashDispatch::Invoke(prompt) => self.run_turn(&prompt)?,
|
||||||
|
SkillSlashDispatch::Local => {
|
||||||
|
Self::print_skills(args.as_deref(), CliOutputFormat::Text)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Doctor => {
|
SlashCommand::Doctor => {
|
||||||
@@ -7123,6 +7166,21 @@ mod tests {
|
|||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&[
|
||||||
|
"skills".to_string(),
|
||||||
|
"help".to_string(),
|
||||||
|
"overview".to_string()
|
||||||
|
])
|
||||||
|
.expect("skills help overview should invoke"),
|
||||||
|
CliAction::Prompt {
|
||||||
|
prompt: "$help overview".to_string(),
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: crate::default_permission_mode(),
|
||||||
|
}
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_args(&["agents".to_string(), "--help".to_string()])
|
parse_args(&["agents".to_string(), "--help".to_string()])
|
||||||
.expect("agents help should parse"),
|
.expect("agents help should parse"),
|
||||||
@@ -7264,6 +7322,21 @@ mod tests {
|
|||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&[
|
||||||
|
"/skills".to_string(),
|
||||||
|
"help".to_string(),
|
||||||
|
"overview".to_string()
|
||||||
|
])
|
||||||
|
.expect("/skills help overview should invoke"),
|
||||||
|
CliAction::Prompt {
|
||||||
|
prompt: "$help overview".to_string(),
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: crate::default_permission_mode(),
|
||||||
|
}
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_args(&[
|
parse_args(&[
|
||||||
"/skills".to_string(),
|
"/skills".to_string(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
api = { path = "../api" }
|
api = { path = "../api" }
|
||||||
|
commands = { path = "../commands" }
|
||||||
plugins = { path = "../plugins" }
|
plugins = { path = "../plugins" }
|
||||||
runtime = { path = "../runtime" }
|
runtime = { path = "../runtime" }
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||||
|
|||||||
@@ -2973,53 +2973,8 @@ fn todo_store_path() -> Result<std::path::PathBuf, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
|
fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
|
||||||
let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
|
let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
|
||||||
if requested.is_empty() {
|
commands::resolve_skill_path(&cwd, skill).map_err(|error| error.to_string())
|
||||||
return Err(String::from("skill must not be empty"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut candidates = Vec::new();
|
|
||||||
if let Ok(claw_config_home) = std::env::var("CLAW_CONFIG_HOME") {
|
|
||||||
candidates.push(std::path::PathBuf::from(claw_config_home).join("skills"));
|
|
||||||
}
|
|
||||||
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
|
|
||||||
candidates.push(std::path::PathBuf::from(codex_home).join("skills"));
|
|
||||||
}
|
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
|
||||||
let home = std::path::PathBuf::from(home);
|
|
||||||
candidates.push(home.join(".claw").join("skills"));
|
|
||||||
candidates.push(home.join(".agents").join("skills"));
|
|
||||||
candidates.push(home.join(".config").join("opencode").join("skills"));
|
|
||||||
candidates.push(home.join(".codex").join("skills"));
|
|
||||||
candidates.push(home.join(".claude").join("skills"));
|
|
||||||
}
|
|
||||||
candidates.push(std::path::PathBuf::from("/home/bellman/.claw/skills"));
|
|
||||||
candidates.push(std::path::PathBuf::from("/home/bellman/.codex/skills"));
|
|
||||||
|
|
||||||
for root in candidates {
|
|
||||||
let direct = root.join(requested).join("SKILL.md");
|
|
||||||
if direct.exists() {
|
|
||||||
return Ok(direct);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(entries) = std::fs::read_dir(&root) {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path().join("SKILL.md");
|
|
||||||
if !path.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if entry
|
|
||||||
.file_name()
|
|
||||||
.to_string_lossy()
|
|
||||||
.eq_ignore_ascii_case(requested)
|
|
||||||
{
|
|
||||||
return Ok(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!("unknown skill: {requested}"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_AGENT_MODEL: &str = "claude-opus-4-6";
|
const DEFAULT_AGENT_MODEL: &str = "claude-opus-4-6";
|
||||||
@@ -5797,6 +5752,50 @@ mod tests {
|
|||||||
fs::remove_dir_all(home).expect("temp home should clean up");
|
fs::remove_dir_all(home).expect("temp home should clean up");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_resolves_project_local_skills_and_legacy_commands() {
|
||||||
|
let _guard = env_lock().lock().expect("env lock should acquire");
|
||||||
|
let root = temp_path("project-skills");
|
||||||
|
let skill_dir = root.join(".claw").join("skills").join("plan");
|
||||||
|
let command_dir = root.join(".claw").join("commands");
|
||||||
|
fs::create_dir_all(&skill_dir).expect("skill dir should exist");
|
||||||
|
fs::create_dir_all(&command_dir).expect("command dir should exist");
|
||||||
|
fs::write(
|
||||||
|
skill_dir.join("SKILL.md"),
|
||||||
|
"---\nname: plan\ndescription: Project planning guidance\n---\n\n# plan\n",
|
||||||
|
)
|
||||||
|
.expect("skill file should exist");
|
||||||
|
fs::write(
|
||||||
|
command_dir.join("handoff.md"),
|
||||||
|
"---\nname: handoff\ndescription: Legacy handoff guidance\n---\n\n# handoff\n",
|
||||||
|
)
|
||||||
|
.expect("command file should exist");
|
||||||
|
|
||||||
|
let original_dir = std::env::current_dir().expect("cwd");
|
||||||
|
std::env::set_current_dir(&root).expect("set cwd");
|
||||||
|
|
||||||
|
let skill_result = execute_tool("Skill", &json!({ "skill": "$plan" }))
|
||||||
|
.expect("project-local skill should resolve");
|
||||||
|
let skill_output: serde_json::Value =
|
||||||
|
serde_json::from_str(&skill_result).expect("valid json");
|
||||||
|
assert!(skill_output["path"]
|
||||||
|
.as_str()
|
||||||
|
.expect("path")
|
||||||
|
.ends_with(".claw/skills/plan/SKILL.md"));
|
||||||
|
|
||||||
|
let command_result = execute_tool("Skill", &json!({ "skill": "/handoff" }))
|
||||||
|
.expect("legacy command should resolve");
|
||||||
|
let command_output: serde_json::Value =
|
||||||
|
serde_json::from_str(&command_result).expect("valid json");
|
||||||
|
assert!(command_output["path"]
|
||||||
|
.as_str()
|
||||||
|
.expect("path")
|
||||||
|
.ends_with(".claw/commands/handoff.md"));
|
||||||
|
|
||||||
|
std::env::set_current_dir(&original_dir).expect("restore cwd");
|
||||||
|
fs::remove_dir_all(root).expect("temp project should clean up");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_search_supports_keyword_and_select_queries() {
|
fn tool_search_supports_keyword_and_select_queries() {
|
||||||
let keyword = execute_tool(
|
let keyword = execute_tool(
|
||||||
|
|||||||
Reference in New Issue
Block a user