mirror of
https://github.com/instructkr/claude-code.git
synced 2026-04-03 05:58:47 +03:00
Make agents and skills commands usable beyond placeholder parsing
Wire /agents and /skills through the Rust command stack so they can run as direct CLI subcommands, direct slash invocations, and resume-safe slash commands. The handlers now provide structured usage output, skills discovery also covers legacy /commands markdown entries, and the reporting/tests line up more closely with the original TypeScript behavior where feasible. Constraint: The Rust port does not yet have the original TypeScript TUI menus or plugin/MCP skill registry, so text reports approximate those views Rejected: Rebuild the original interactive React menus in Rust now | too large for the current CLI parity slice Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep /skills discovery and the Skill tool aligned if command/skill registry parity expands later Tested: cargo test --workspace Tested: cargo clippy --workspace --all-targets -- -D warnings Tested: cargo run -q -p rusty-claude-cli -- agents --help Tested: cargo run -q -p rusty-claude-cli -- /agents Not-tested: Live Anthropic-backed REPL execution of /agents or /skills
This commit is contained in:
@@ -212,16 +212,16 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
SlashCommandSpec {
|
||||
name: "agents",
|
||||
aliases: &[],
|
||||
summary: "Manage agent configurations",
|
||||
summary: "List configured agents",
|
||||
argument_hint: None,
|
||||
resume_supported: false,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "skills",
|
||||
aliases: &[],
|
||||
summary: "List available skills",
|
||||
argument_hint: None,
|
||||
resume_supported: false,
|
||||
resume_supported: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -470,6 +470,29 @@ struct SkillSummary {
|
||||
description: Option<String>,
|
||||
source: DefinitionSource,
|
||||
shadowed_by: Option<DefinitionSource>,
|
||||
origin: SkillOrigin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum SkillOrigin {
|
||||
SkillsDir,
|
||||
LegacyCommandsDir,
|
||||
}
|
||||
|
||||
impl SkillOrigin {
|
||||
fn detail_label(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::SkillsDir => None,
|
||||
Self::LegacyCommandsDir => Some("legacy /commands"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct SkillRoot {
|
||||
source: DefinitionSource,
|
||||
path: PathBuf,
|
||||
origin: SkillOrigin,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
@@ -585,23 +608,27 @@ pub fn handle_plugins_slash_command(
|
||||
}
|
||||
|
||||
pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
||||
if let Some(args) = args.filter(|value| !value.trim().is_empty()) {
|
||||
return Ok(format!("Usage: /agents\nUnexpected arguments: {args}"));
|
||||
match normalize_optional_args(args) {
|
||||
None | Some("list") => {
|
||||
let roots = discover_definition_roots(cwd, "agents");
|
||||
let agents = load_agents_from_roots(&roots)?;
|
||||
Ok(render_agents_report(&agents))
|
||||
}
|
||||
Some("-h" | "--help" | "help") => Ok(render_agents_usage(None)),
|
||||
Some(args) => Ok(render_agents_usage(Some(args))),
|
||||
}
|
||||
|
||||
let roots = discover_definition_roots(cwd, "agents");
|
||||
let agents = load_agents_from_roots(&roots)?;
|
||||
Ok(render_agents_report(&agents))
|
||||
}
|
||||
|
||||
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
||||
if let Some(args) = args.filter(|value| !value.trim().is_empty()) {
|
||||
return Ok(format!("Usage: /skills\nUnexpected arguments: {args}"));
|
||||
match normalize_optional_args(args) {
|
||||
None | Some("list") => {
|
||||
let roots = discover_skill_roots(cwd);
|
||||
let skills = load_skills_from_roots(&roots)?;
|
||||
Ok(render_skills_report(&skills))
|
||||
}
|
||||
Some("-h" | "--help" | "help") => Ok(render_skills_usage(None)),
|
||||
Some(args) => Ok(render_skills_usage(Some(args))),
|
||||
}
|
||||
|
||||
let roots = discover_definition_roots(cwd, "skills");
|
||||
let skills = load_skills_from_roots(&roots)?;
|
||||
Ok(render_skills_report(&skills))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -697,6 +724,83 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
||||
roots
|
||||
}
|
||||
|
||||
fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
let mut roots = Vec::new();
|
||||
|
||||
for ancestor in cwd.ancestors() {
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectCodex,
|
||||
ancestor.join(".codex").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectClaude,
|
||||
ancestor.join(".claude").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectCodex,
|
||||
ancestor.join(".codex").join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectClaude,
|
||||
ancestor.join(".claude").join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(codex_home) = env::var("CODEX_HOME") {
|
||||
let codex_home = PathBuf::from(codex_home);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserCodexHome,
|
||||
codex_home.join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserCodexHome,
|
||||
codex_home.join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(home) = env::var_os("HOME") {
|
||||
let home = PathBuf::from(home);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserCodex,
|
||||
home.join(".codex").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserCodex,
|
||||
home.join(".codex").join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
home.join(".claude").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
home.join(".claude").join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
}
|
||||
|
||||
roots
|
||||
}
|
||||
|
||||
fn push_unique_root(
|
||||
roots: &mut Vec<(DefinitionSource, PathBuf)>,
|
||||
source: DefinitionSource,
|
||||
@@ -707,6 +811,21 @@ fn push_unique_root(
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unique_skill_root(
|
||||
roots: &mut Vec<SkillRoot>,
|
||||
source: DefinitionSource,
|
||||
path: PathBuf,
|
||||
origin: SkillOrigin,
|
||||
) {
|
||||
if path.is_dir() && !roots.iter().any(|existing| existing.path == path) {
|
||||
roots.push(SkillRoot {
|
||||
source,
|
||||
path,
|
||||
origin,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn load_agents_from_roots(
|
||||
roots: &[(DefinitionSource, PathBuf)],
|
||||
) -> std::io::Result<Vec<AgentSummary>> {
|
||||
@@ -750,31 +869,66 @@ fn load_agents_from_roots(
|
||||
Ok(agents)
|
||||
}
|
||||
|
||||
fn load_skills_from_roots(
|
||||
roots: &[(DefinitionSource, PathBuf)],
|
||||
) -> std::io::Result<Vec<SkillSummary>> {
|
||||
fn load_skills_from_roots(roots: &[SkillRoot]) -> std::io::Result<Vec<SkillSummary>> {
|
||||
let mut skills = Vec::new();
|
||||
let mut active_sources = BTreeMap::<String, DefinitionSource>::new();
|
||||
|
||||
for (source, root) in roots {
|
||||
for root in roots {
|
||||
let mut root_skills = Vec::new();
|
||||
for entry in fs::read_dir(root)? {
|
||||
for entry in fs::read_dir(&root.path)? {
|
||||
let entry = entry?;
|
||||
if !entry.path().is_dir() {
|
||||
continue;
|
||||
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, description) = parse_skill_frontmatter(&contents);
|
||||
root_skills.push(SkillSummary {
|
||||
name: name
|
||||
.unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()),
|
||||
description,
|
||||
source: root.source,
|
||||
shadowed_by: None,
|
||||
origin: root.origin,
|
||||
});
|
||||
}
|
||||
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, description) = parse_skill_frontmatter(&contents);
|
||||
root_skills.push(SkillSummary {
|
||||
name: name.unwrap_or(fallback_name),
|
||||
description,
|
||||
source: root.source,
|
||||
shadowed_by: None,
|
||||
origin: root.origin,
|
||||
});
|
||||
}
|
||||
}
|
||||
let skill_path = entry.path().join("SKILL.md");
|
||||
if !skill_path.is_file() {
|
||||
continue;
|
||||
}
|
||||
let contents = fs::read_to_string(skill_path)?;
|
||||
let (name, description) = parse_skill_frontmatter(&contents);
|
||||
root_skills.push(SkillSummary {
|
||||
name: name.unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()),
|
||||
description,
|
||||
source: *source,
|
||||
shadowed_by: None,
|
||||
});
|
||||
}
|
||||
root_skills.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
|
||||
@@ -830,16 +984,16 @@ fn parse_skill_frontmatter(contents: &str) -> (Option<String>, Option<String>) {
|
||||
break;
|
||||
}
|
||||
if let Some(value) = trimmed.strip_prefix("name:") {
|
||||
let value = value.trim();
|
||||
let value = unquote_frontmatter_value(value.trim());
|
||||
if !value.is_empty() {
|
||||
name = Some(value.to_string());
|
||||
name = Some(value);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = trimmed.strip_prefix("description:") {
|
||||
let value = value.trim();
|
||||
let value = unquote_frontmatter_value(value.trim());
|
||||
if !value.is_empty() {
|
||||
description = Some(value.to_string());
|
||||
description = Some(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,6 +1001,20 @@ fn parse_skill_frontmatter(contents: &str) -> (Option<String>, Option<String>) {
|
||||
(name, description)
|
||||
}
|
||||
|
||||
fn unquote_frontmatter_value(value: &str) -> String {
|
||||
value
|
||||
.strip_prefix('"')
|
||||
.and_then(|trimmed| trimmed.strip_suffix('"'))
|
||||
.or_else(|| {
|
||||
value
|
||||
.strip_prefix('\'')
|
||||
.and_then(|trimmed| trimmed.strip_suffix('\''))
|
||||
})
|
||||
.unwrap_or(value)
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn render_agents_report(agents: &[AgentSummary]) -> String {
|
||||
if agents.is_empty() {
|
||||
return "No agents found.".to_string();
|
||||
@@ -937,10 +1105,14 @@ fn render_skills_report(skills: &[SkillSummary]) -> String {
|
||||
|
||||
lines.push(format!("{}:", source.label()));
|
||||
for skill in group {
|
||||
let detail = match &skill.description {
|
||||
Some(description) => format!("{} · {}", skill.name, description),
|
||||
None => skill.name.clone(),
|
||||
};
|
||||
let mut parts = vec![skill.name.clone()];
|
||||
if let Some(description) = &skill.description {
|
||||
parts.push(description.clone());
|
||||
}
|
||||
if let Some(detail) = skill.origin.detail_label() {
|
||||
parts.push(detail.to_string());
|
||||
}
|
||||
let detail = parts.join(" · ");
|
||||
match skill.shadowed_by {
|
||||
Some(winner) => lines.push(format!(" (shadowed by {}) {detail}", winner.label())),
|
||||
None => lines.push(format!(" {detail}")),
|
||||
@@ -952,6 +1124,36 @@ fn render_skills_report(skills: &[SkillSummary]) -> String {
|
||||
lines.join("\n").trim_end().to_string()
|
||||
}
|
||||
|
||||
fn normalize_optional_args(args: Option<&str>) -> Option<&str> {
|
||||
args.map(str::trim).filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn render_agents_usage(unexpected: Option<&str>) -> String {
|
||||
let mut lines = vec![
|
||||
"Agents".to_string(),
|
||||
" Usage /agents".to_string(),
|
||||
" Direct CLI claw agents".to_string(),
|
||||
" Sources .codex/agents, .claude/agents, $CODEX_HOME/agents".to_string(),
|
||||
];
|
||||
if let Some(args) = unexpected {
|
||||
lines.push(format!(" Unexpected {args}"));
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
fn render_skills_usage(unexpected: Option<&str>) -> String {
|
||||
let mut lines = vec![
|
||||
"Skills".to_string(),
|
||||
" Usage /skills".to_string(),
|
||||
" Direct CLI claw skills".to_string(),
|
||||
" Sources .codex/skills, .claude/skills, legacy /commands".to_string(),
|
||||
];
|
||||
if let Some(args) = unexpected {
|
||||
lines.push(format!(" Unexpected {args}"));
|
||||
}
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn handle_slash_command(
|
||||
input: &str,
|
||||
@@ -1011,7 +1213,7 @@ mod tests {
|
||||
handle_plugins_slash_command, handle_slash_command, load_agents_from_roots,
|
||||
load_skills_from_roots, render_agents_report, render_plugins_report, render_skills_report,
|
||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
||||
DefinitionSource, SlashCommand,
|
||||
DefinitionSource, SkillOrigin, SkillRoot, SlashCommand,
|
||||
};
|
||||
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
||||
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
||||
@@ -1071,6 +1273,15 @@ mod tests {
|
||||
.expect("write skill");
|
||||
}
|
||||
|
||||
fn write_legacy_command(root: &Path, name: &str, description: &str) {
|
||||
fs::create_dir_all(root).expect("commands root");
|
||||
fs::write(
|
||||
root.join(format!("{name}.md")),
|
||||
format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
|
||||
)
|
||||
.expect("write command");
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[test]
|
||||
fn parses_supported_slash_commands() {
|
||||
@@ -1232,7 +1443,7 @@ mod tests {
|
||||
assert!(help.contains("/agents"));
|
||||
assert!(help.contains("/skills"));
|
||||
assert_eq!(slash_command_specs().len(), 25);
|
||||
assert_eq!(resume_supported_slash_commands().len(), 11);
|
||||
assert_eq!(resume_supported_slash_commands().len(), 13);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1425,24 +1636,41 @@ mod tests {
|
||||
fn lists_skills_from_project_and_user_roots() {
|
||||
let workspace = temp_dir("skills-workspace");
|
||||
let project_skills = workspace.join(".codex").join("skills");
|
||||
let project_commands = workspace.join(".claude").join("commands");
|
||||
let user_home = temp_dir("skills-home");
|
||||
let user_skills = user_home.join(".codex").join("skills");
|
||||
|
||||
write_skill(&project_skills, "plan", "Project planning guidance");
|
||||
write_legacy_command(&project_commands, "deploy", "Legacy deployment guidance");
|
||||
write_skill(&user_skills, "plan", "User planning guidance");
|
||||
write_skill(&user_skills, "help", "Help guidance");
|
||||
|
||||
let roots = vec![
|
||||
(DefinitionSource::ProjectCodex, project_skills),
|
||||
(DefinitionSource::UserCodex, user_skills),
|
||||
SkillRoot {
|
||||
source: DefinitionSource::ProjectCodex,
|
||||
path: project_skills,
|
||||
origin: SkillOrigin::SkillsDir,
|
||||
},
|
||||
SkillRoot {
|
||||
source: DefinitionSource::ProjectClaude,
|
||||
path: project_commands,
|
||||
origin: SkillOrigin::LegacyCommandsDir,
|
||||
},
|
||||
SkillRoot {
|
||||
source: DefinitionSource::UserCodex,
|
||||
path: user_skills,
|
||||
origin: SkillOrigin::SkillsDir,
|
||||
},
|
||||
];
|
||||
let report =
|
||||
render_skills_report(&load_skills_from_roots(&roots).expect("skill roots should load"));
|
||||
|
||||
assert!(report.contains("Skills"));
|
||||
assert!(report.contains("2 available skills"));
|
||||
assert!(report.contains("3 available skills"));
|
||||
assert!(report.contains("Project (.codex):"));
|
||||
assert!(report.contains("plan · Project planning guidance"));
|
||||
assert!(report.contains("Project (.claude):"));
|
||||
assert!(report.contains("deploy · Legacy deployment guidance · legacy /commands"));
|
||||
assert!(report.contains("User (~/.codex):"));
|
||||
assert!(report.contains("(shadowed by Project (.codex)) plan · User planning guidance"));
|
||||
assert!(report.contains("help · Help guidance"));
|
||||
@@ -1451,6 +1679,39 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(user_home);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_and_skills_usage_support_help_and_unexpected_args() {
|
||||
let cwd = temp_dir("slash-usage");
|
||||
|
||||
let agents_help =
|
||||
super::handle_agents_slash_command(Some("help"), &cwd).expect("agents help");
|
||||
assert!(agents_help.contains("Usage /agents"));
|
||||
assert!(agents_help.contains("Direct CLI claw agents"));
|
||||
|
||||
let agents_unexpected =
|
||||
super::handle_agents_slash_command(Some("show planner"), &cwd).expect("agents usage");
|
||||
assert!(agents_unexpected.contains("Unexpected show planner"));
|
||||
|
||||
let skills_help =
|
||||
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
|
||||
assert!(skills_help.contains("Usage /skills"));
|
||||
assert!(skills_help.contains("legacy /commands"));
|
||||
|
||||
let skills_unexpected =
|
||||
super::handle_skills_slash_command(Some("show help"), &cwd).expect("skills usage");
|
||||
assert!(skills_unexpected.contains("Unexpected show help"));
|
||||
|
||||
let _ = fs::remove_dir_all(cwd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_quoted_skill_frontmatter_values() {
|
||||
let contents = "---\nname: \"hud\"\ndescription: 'Quoted description'\n---\n";
|
||||
let (name, description) = super::parse_skill_frontmatter(contents);
|
||||
assert_eq!(name.as_deref(), Some("hud"));
|
||||
assert_eq!(description.as_deref(), Some("Quoted description"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installs_plugin_from_path_and_lists_it() {
|
||||
let config_home = temp_dir("home");
|
||||
|
||||
@@ -73,6 +73,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
match parse_args(&args)? {
|
||||
CliAction::DumpManifests => dump_manifests(),
|
||||
CliAction::BootstrapPlan => print_bootstrap_plan(),
|
||||
CliAction::Agents { args } => LiveCli::print_agents(args.as_deref())?,
|
||||
CliAction::Skills { args } => LiveCli::print_skills(args.as_deref())?,
|
||||
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
|
||||
CliAction::Version => print_version(),
|
||||
CliAction::ResumeSession {
|
||||
@@ -104,6 +106,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enum CliAction {
|
||||
DumpManifests,
|
||||
BootstrapPlan,
|
||||
Agents {
|
||||
args: Option<String>,
|
||||
},
|
||||
Skills {
|
||||
args: Option<String>,
|
||||
},
|
||||
PrintSystemPrompt {
|
||||
cwd: PathBuf,
|
||||
date: String,
|
||||
@@ -267,6 +275,12 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
match rest[0].as_str() {
|
||||
"dump-manifests" => Ok(CliAction::DumpManifests),
|
||||
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
|
||||
"agents" => Ok(CliAction::Agents {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
}),
|
||||
"skills" => Ok(CliAction::Skills {
|
||||
args: join_optional_args(&rest[1..]),
|
||||
}),
|
||||
"system-prompt" => parse_system_prompt_args(&rest[1..]),
|
||||
"login" => Ok(CliAction::Login),
|
||||
"logout" => Ok(CliAction::Logout),
|
||||
@@ -284,14 +298,37 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
permission_mode,
|
||||
})
|
||||
}
|
||||
other if !other.starts_with('/') => Ok(CliAction::Prompt {
|
||||
other if other.starts_with('/') => parse_direct_slash_cli_action(&rest),
|
||||
_other => Ok(CliAction::Prompt {
|
||||
prompt: rest.join(" "),
|
||||
model,
|
||||
output_format,
|
||||
allowed_tools,
|
||||
permission_mode,
|
||||
}),
|
||||
other => Err(format!("unknown subcommand: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn join_optional_args(args: &[String]) -> Option<String> {
|
||||
let joined = args.join(" ");
|
||||
let trimmed = joined.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
}
|
||||
|
||||
fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
||||
let raw = rest.join(" ");
|
||||
match SlashCommand::parse(&raw) {
|
||||
Some(SlashCommand::Help) => Ok(CliAction::Help),
|
||||
Some(SlashCommand::Agents { args }) => Ok(CliAction::Agents { args }),
|
||||
Some(SlashCommand::Skills { args }) => Ok(CliAction::Skills { args }),
|
||||
Some(command) => Err(format!(
|
||||
"unsupported direct slash command outside the REPL: {command_name}",
|
||||
command_name = match command {
|
||||
SlashCommand::Unknown(name) => format!("/{name}"),
|
||||
_ => rest[0].clone(),
|
||||
}
|
||||
)),
|
||||
None => Err(format!("unknown subcommand: {}", rest[0])),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,6 +928,20 @@ fn run_resume_command(
|
||||
)),
|
||||
})
|
||||
}
|
||||
SlashCommand::Agents { args } => {
|
||||
let cwd = env::current_dir()?;
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(handle_agents_slash_command(args.as_deref(), &cwd)?),
|
||||
})
|
||||
}
|
||||
SlashCommand::Skills { args } => {
|
||||
let cwd = env::current_dir()?;
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?),
|
||||
})
|
||||
}
|
||||
SlashCommand::Bughunter { .. }
|
||||
| SlashCommand::Commit
|
||||
| SlashCommand::Pr { .. }
|
||||
@@ -903,8 +954,6 @@ fn run_resume_command(
|
||||
| SlashCommand::Permissions { .. }
|
||||
| SlashCommand::Session { .. }
|
||||
| SlashCommand::Plugins { .. }
|
||||
| SlashCommand::Agents { .. }
|
||||
| SlashCommand::Skills { .. }
|
||||
| SlashCommand::Unknown(_) => Err("unsupported resumed slash command".into()),
|
||||
}
|
||||
}
|
||||
@@ -3718,6 +3767,8 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
)?;
|
||||
writeln!(out, " claw dump-manifests")?;
|
||||
writeln!(out, " claw bootstrap-plan")?;
|
||||
writeln!(out, " claw agents")?;
|
||||
writeln!(out, " claw skills")?;
|
||||
writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw logout")?;
|
||||
@@ -3772,6 +3823,8 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
out,
|
||||
" claw --resume session.json /status /diff /export notes.txt"
|
||||
)?;
|
||||
writeln!(out, " claw agents")?;
|
||||
writeln!(out, " claw /skills")?;
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw init")?;
|
||||
Ok(())
|
||||
@@ -3992,6 +4045,43 @@ mod tests {
|
||||
parse_args(&["init".to_string()]).expect("init should parse"),
|
||||
CliAction::Init
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["agents".to_string()]).expect("agents should parse"),
|
||||
CliAction::Agents { args: None }
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["skills".to_string()]).expect("skills should parse"),
|
||||
CliAction::Skills { args: None }
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["agents".to_string(), "--help".to_string()])
|
||||
.expect("agents help should parse"),
|
||||
CliAction::Agents {
|
||||
args: Some("--help".to_string())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_direct_agents_and_skills_slash_commands() {
|
||||
assert_eq!(
|
||||
parse_args(&["/agents".to_string()]).expect("/agents should parse"),
|
||||
CliAction::Agents { args: None }
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skills".to_string()]).expect("/skills should parse"),
|
||||
CliAction::Skills { args: None }
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skills".to_string(), "help".to_string()])
|
||||
.expect("/skills help should parse"),
|
||||
CliAction::Skills {
|
||||
args: Some("help".to_string())
|
||||
}
|
||||
);
|
||||
let error = parse_args(&["/status".to_string()])
|
||||
.expect_err("/status should remain REPL-only when invoked directly");
|
||||
assert!(error.contains("unsupported direct slash command"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4108,7 +4198,7 @@ mod tests {
|
||||
names,
|
||||
vec![
|
||||
"help", "status", "compact", "clear", "cost", "config", "memory", "init", "diff",
|
||||
"version", "export",
|
||||
"version", "export", "agents", "skills",
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -4175,6 +4265,9 @@ mod tests {
|
||||
print_help_to(&mut help).expect("help should render");
|
||||
let help = String::from_utf8(help).expect("help should be utf8");
|
||||
assert!(help.contains("claw init"));
|
||||
assert!(help.contains("claw agents"));
|
||||
assert!(help.contains("claw skills"));
|
||||
assert!(help.contains("claw /skills"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user