mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-07 00:24:50 +08:00
Compare commits
1 Commits
fix/plugin
...
fix/skill-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685d5fef9f |
1
rust/Cargo.lock
generated
1
rust/Cargo.lock
generated
@@ -1753,6 +1753,7 @@ name = "tools"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"api",
|
"api",
|
||||||
|
"commands",
|
||||||
"plugins",
|
"plugins",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"runtime",
|
"runtime",
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ use api::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use commands::{
|
use commands::{
|
||||||
handle_agents_slash_command, handle_plugins_slash_command, handle_skills_slash_command,
|
classify_agents_slash_command, classify_skills_slash_command, handle_agents_slash_command,
|
||||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
handle_plugins_slash_command, handle_skills_slash_command, render_slash_command_help,
|
||||||
suggest_slash_commands, SlashCommand,
|
resume_supported_slash_commands, slash_command_specs, suggest_slash_commands,
|
||||||
|
InvokeCommandAction, SlashCommand,
|
||||||
};
|
};
|
||||||
use compat_harness::{extract_manifest, UpstreamPaths};
|
use compat_harness::{extract_manifest, UpstreamPaths};
|
||||||
use init::initialize_repo;
|
use init::initialize_repo;
|
||||||
@@ -286,12 +287,30 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
match rest[0].as_str() {
|
match rest[0].as_str() {
|
||||||
"dump-manifests" => Ok(CliAction::DumpManifests),
|
"dump-manifests" => Ok(CliAction::DumpManifests),
|
||||||
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
|
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
|
||||||
"agents" => Ok(CliAction::Agents {
|
"agents" => match classify_agents_slash_command(join_optional_args(&rest[1..]).as_deref()) {
|
||||||
|
InvokeCommandAction::Invoke(prompt) => Ok(CliAction::Prompt {
|
||||||
|
prompt,
|
||||||
|
model,
|
||||||
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
}),
|
||||||
|
_ => Ok(CliAction::Agents {
|
||||||
args: join_optional_args(&rest[1..]),
|
args: join_optional_args(&rest[1..]),
|
||||||
}),
|
}),
|
||||||
"skills" => Ok(CliAction::Skills {
|
},
|
||||||
|
"skills" => match classify_skills_slash_command(join_optional_args(&rest[1..]).as_deref()) {
|
||||||
|
InvokeCommandAction::Invoke(prompt) => Ok(CliAction::Prompt {
|
||||||
|
prompt,
|
||||||
|
model,
|
||||||
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
}),
|
||||||
|
_ => Ok(CliAction::Skills {
|
||||||
args: join_optional_args(&rest[1..]),
|
args: join_optional_args(&rest[1..]),
|
||||||
}),
|
}),
|
||||||
|
},
|
||||||
"system-prompt" => parse_system_prompt_args(&rest[1..]),
|
"system-prompt" => parse_system_prompt_args(&rest[1..]),
|
||||||
"login" => Ok(CliAction::Login),
|
"login" => Ok(CliAction::Login),
|
||||||
"logout" => Ok(CliAction::Logout),
|
"logout" => Ok(CliAction::Logout),
|
||||||
@@ -309,7 +328,13 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
other if other.starts_with('/') => parse_direct_slash_cli_action(&rest),
|
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,
|
||||||
@@ -326,12 +351,40 @@ fn join_optional_args(args: &[String]) -> Option<String> {
|
|||||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_direct_slash_cli_action(rest: &[String]) -> Result<CliAction, String> {
|
fn parse_direct_slash_cli_action(
|
||||||
|
rest: &[String],
|
||||||
|
model: String,
|
||||||
|
output_format: CliOutputFormat,
|
||||||
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
|
permission_mode: PermissionMode,
|
||||||
|
) -> Result<CliAction, String> {
|
||||||
let raw = rest.join(" ");
|
let raw = rest.join(" ");
|
||||||
match SlashCommand::parse(&raw) {
|
match SlashCommand::parse(&raw) {
|
||||||
Some(SlashCommand::Help) => Ok(CliAction::Help),
|
Some(SlashCommand::Help) => Ok(CliAction::Help),
|
||||||
Some(SlashCommand::Agents { args }) => Ok(CliAction::Agents { args }),
|
Some(SlashCommand::Agents { args }) => {
|
||||||
Some(SlashCommand::Skills { args }) => Ok(CliAction::Skills { args }),
|
match classify_agents_slash_command(args.as_deref()) {
|
||||||
|
InvokeCommandAction::Invoke(prompt) => Ok(CliAction::Prompt {
|
||||||
|
prompt,
|
||||||
|
model,
|
||||||
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
}),
|
||||||
|
_ => Ok(CliAction::Agents { args }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(SlashCommand::Skills { args }) => {
|
||||||
|
match classify_skills_slash_command(args.as_deref()) {
|
||||||
|
InvokeCommandAction::Invoke(prompt) => Ok(CliAction::Prompt {
|
||||||
|
prompt,
|
||||||
|
model,
|
||||||
|
output_format,
|
||||||
|
allowed_tools,
|
||||||
|
permission_mode,
|
||||||
|
}),
|
||||||
|
_ => Ok(CliAction::Skills { args }),
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(command) => Err(format_direct_slash_command_error(
|
Some(command) => Err(format_direct_slash_command_error(
|
||||||
match &command {
|
match &command {
|
||||||
SlashCommand::Unknown(name) => format!("/{name}"),
|
SlashCommand::Unknown(name) => format!("/{name}"),
|
||||||
@@ -1321,11 +1374,17 @@ impl LiveCli {
|
|||||||
self.handle_plugins_command(action.as_deref(), target.as_deref())?
|
self.handle_plugins_command(action.as_deref(), target.as_deref())?
|
||||||
}
|
}
|
||||||
SlashCommand::Agents { args } => {
|
SlashCommand::Agents { args } => {
|
||||||
Self::print_agents(args.as_deref())?;
|
match classify_agents_slash_command(args.as_deref()) {
|
||||||
|
InvokeCommandAction::Invoke(prompt) => self.run_turn(&prompt)?,
|
||||||
|
_ => Self::print_agents(args.as_deref())?,
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Skills { args } => {
|
SlashCommand::Skills { args } => {
|
||||||
Self::print_skills(args.as_deref())?;
|
match classify_skills_slash_command(args.as_deref()) {
|
||||||
|
InvokeCommandAction::Invoke(prompt) => self.run_turn(&prompt)?,
|
||||||
|
_ => Self::print_skills(args.as_deref())?,
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
SlashCommand::Branch { .. } => {
|
SlashCommand::Branch { .. } => {
|
||||||
@@ -4332,6 +4391,17 @@ mod tests {
|
|||||||
args: Some("--help".to_string())
|
args: Some("--help".to_string())
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["skills".to_string(), "ralplan".to_string()])
|
||||||
|
.expect("skills invoke should parse"),
|
||||||
|
CliAction::Prompt {
|
||||||
|
prompt: "$ralplan".to_string(),
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4345,10 +4415,36 @@ mod tests {
|
|||||||
CliAction::Skills { args: None }
|
CliAction::Skills { args: None }
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_args(&["/skills".to_string(), "help".to_string()])
|
parse_args(&["/skills".to_string(), "help".to_string(), "overview".to_string()])
|
||||||
.expect("/skills help should parse"),
|
.expect("/skills help overview should invoke"),
|
||||||
CliAction::Skills {
|
CliAction::Prompt {
|
||||||
args: Some("help".to_string())
|
prompt: "$help overview".to_string(),
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["/skills".to_string(), "oh-my-claudecode:ralplan".to_string()])
|
||||||
|
.expect("/skills namespaced invoke should parse"),
|
||||||
|
CliAction::Prompt {
|
||||||
|
prompt: "$oh-my-claudecode:ralplan".to_string(),
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_args(&["/agents".to_string(), "planner".to_string()])
|
||||||
|
.expect("/agents planner should invoke"),
|
||||||
|
CliAction::Prompt {
|
||||||
|
prompt: "/prompts:planner".to_string(),
|
||||||
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
output_format: CliOutputFormat::Text,
|
||||||
|
allowed_tools: None,
|
||||||
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let error = parse_args(&["/status".to_string()])
|
let error = parse_args(&["/status".to_string()])
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use plugins::{PluginError, PluginManager, PluginSummary};
|
use plugins::{PluginError, PluginManager, PluginManagerConfig, PluginSummary};
|
||||||
use runtime::{compact_session, CompactionConfig, Session};
|
use runtime::{compact_session, CompactionConfig, ConfigLoader, Session};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct CommandManifestEntry {
|
pub struct CommandManifestEntry {
|
||||||
@@ -284,16 +284,16 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "agents",
|
name: "agents",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
summary: "List configured agents",
|
summary: "List or invoke configured agents",
|
||||||
argument_hint: None,
|
argument_hint: Some("[list|--help|<agent>]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Automation,
|
category: SlashCommandCategory::Automation,
|
||||||
},
|
},
|
||||||
SlashCommandSpec {
|
SlashCommandSpec {
|
||||||
name: "skills",
|
name: "skills",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
summary: "List available skills",
|
summary: "List or invoke available skills",
|
||||||
argument_hint: None,
|
argument_hint: Some("[list|--help|<skill> [args]]"),
|
||||||
resume_supported: true,
|
resume_supported: true,
|
||||||
category: SlashCommandCategory::Automation,
|
category: SlashCommandCategory::Automation,
|
||||||
},
|
},
|
||||||
@@ -631,6 +631,7 @@ enum DefinitionSource {
|
|||||||
UserCodexHome,
|
UserCodexHome,
|
||||||
UserCodex,
|
UserCodex,
|
||||||
UserClaw,
|
UserClaw,
|
||||||
|
Plugin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DefinitionSource {
|
impl DefinitionSource {
|
||||||
@@ -641,6 +642,7 @@ impl DefinitionSource {
|
|||||||
Self::UserCodexHome => "User ($CODEX_HOME)",
|
Self::UserCodexHome => "User ($CODEX_HOME)",
|
||||||
Self::UserCodex => "User (~/.codex)",
|
Self::UserCodex => "User (~/.codex)",
|
||||||
Self::UserClaw => "User (~/.claw)",
|
Self::UserClaw => "User (~/.claw)",
|
||||||
|
Self::Plugin => "Plugins",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,6 +686,30 @@ struct SkillRoot {
|
|||||||
source: DefinitionSource,
|
source: DefinitionSource,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
origin: SkillOrigin,
|
origin: SkillOrigin,
|
||||||
|
namespace: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct DefinitionRoot {
|
||||||
|
source: DefinitionSource,
|
||||||
|
path: PathBuf,
|
||||||
|
namespace: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct SkillEntry {
|
||||||
|
name: String,
|
||||||
|
path: PathBuf,
|
||||||
|
description: Option<String>,
|
||||||
|
source: DefinitionSource,
|
||||||
|
origin: SkillOrigin,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum InvokeCommandAction {
|
||||||
|
Browse,
|
||||||
|
Help,
|
||||||
|
Invoke(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
@@ -813,7 +839,7 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
|||||||
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list") => {
|
None | Some("list") => {
|
||||||
let roots = discover_skill_roots(cwd);
|
let roots = discover_skill_roots(cwd)?;
|
||||||
let skills = load_skills_from_roots(&roots)?;
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
Ok(render_skills_report(&skills))
|
Ok(render_skills_report(&skills))
|
||||||
}
|
}
|
||||||
@@ -1260,7 +1286,72 @@ fn resolve_plugin_target(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, PathBuf)> {
|
pub fn classify_agents_slash_command(args: Option<&str>) -> InvokeCommandAction {
|
||||||
|
classify_invoke_command(args, "/prompts:")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn classify_skills_slash_command(args: Option<&str>) -> InvokeCommandAction {
|
||||||
|
classify_invoke_command(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(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"skill must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let roots = discover_skill_roots(cwd)?;
|
||||||
|
let entries = load_skill_entries_from_roots(&roots)?;
|
||||||
|
|
||||||
|
if let Some((namespace, name)) = requested.split_once(':') {
|
||||||
|
return entries
|
||||||
|
.into_iter()
|
||||||
|
.find(|entry| entry.name.eq_ignore_ascii_case(&format!("{namespace}:{name}")))
|
||||||
|
.map(|entry| entry.path)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("unknown skill: {requested}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(entry) = entries
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.name.eq_ignore_ascii_case(requested))
|
||||||
|
{
|
||||||
|
return Ok(entry.path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let plugin_matches = entries
|
||||||
|
.into_iter()
|
||||||
|
.filter(|entry| {
|
||||||
|
entry
|
||||||
|
.name
|
||||||
|
.split_once(':')
|
||||||
|
.is_some_and(|(_, name)| name.eq_ignore_ascii_case(requested))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
match plugin_matches.len() {
|
||||||
|
0 => Err(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
format!("unknown skill: {requested}"),
|
||||||
|
)),
|
||||||
|
1 => Ok(plugin_matches[0].path.clone()),
|
||||||
|
_ => Err(io::Error::other(format!(
|
||||||
|
"skill `{requested}` is provided by multiple plugins; use <plugin>:<skill>"
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_invoke_command(args: Option<&str>, prefix: &str) -> InvokeCommandAction {
|
||||||
|
match normalize_optional_args(args) {
|
||||||
|
None | Some("list") => InvokeCommandAction::Browse,
|
||||||
|
Some("-h" | "--help" | "help") => InvokeCommandAction::Help,
|
||||||
|
Some(args) => InvokeCommandAction::Invoke(format!("{prefix}{args}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<DefinitionRoot> {
|
||||||
let mut roots = Vec::new();
|
let mut roots = Vec::new();
|
||||||
|
|
||||||
for ancestor in cwd.ancestors() {
|
for ancestor in cwd.ancestors() {
|
||||||
@@ -1268,11 +1359,13 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
|||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::ProjectCodex,
|
DefinitionSource::ProjectCodex,
|
||||||
ancestor.join(".codex").join(leaf),
|
ancestor.join(".codex").join(leaf),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_root(
|
push_unique_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::ProjectClaw,
|
DefinitionSource::ProjectClaw,
|
||||||
ancestor.join(".claw").join(leaf),
|
ancestor.join(".claw").join(leaf),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1281,6 +1374,7 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
|||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::UserCodexHome,
|
DefinitionSource::UserCodexHome,
|
||||||
PathBuf::from(codex_home).join(leaf),
|
PathBuf::from(codex_home).join(leaf),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1290,18 +1384,22 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
|||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::UserCodex,
|
DefinitionSource::UserCodex,
|
||||||
home.join(".codex").join(leaf),
|
home.join(".codex").join(leaf),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_root(
|
push_unique_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::UserClaw,
|
DefinitionSource::UserClaw,
|
||||||
home.join(".claw").join(leaf),
|
home.join(".claw").join(leaf),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
append_plugin_definition_roots(&mut roots, cwd, leaf);
|
||||||
|
|
||||||
roots
|
roots
|
||||||
}
|
}
|
||||||
|
|
||||||
fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
fn discover_skill_roots(cwd: &Path) -> io::Result<Vec<SkillRoot>> {
|
||||||
let mut roots = Vec::new();
|
let mut roots = Vec::new();
|
||||||
|
|
||||||
for ancestor in cwd.ancestors() {
|
for ancestor in cwd.ancestors() {
|
||||||
@@ -1310,24 +1408,28 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
|||||||
DefinitionSource::ProjectCodex,
|
DefinitionSource::ProjectCodex,
|
||||||
ancestor.join(".codex").join("skills"),
|
ancestor.join(".codex").join("skills"),
|
||||||
SkillOrigin::SkillsDir,
|
SkillOrigin::SkillsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_skill_root(
|
push_unique_skill_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::ProjectClaw,
|
DefinitionSource::ProjectClaw,
|
||||||
ancestor.join(".claw").join("skills"),
|
ancestor.join(".claw").join("skills"),
|
||||||
SkillOrigin::SkillsDir,
|
SkillOrigin::SkillsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_skill_root(
|
push_unique_skill_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::ProjectCodex,
|
DefinitionSource::ProjectCodex,
|
||||||
ancestor.join(".codex").join("commands"),
|
ancestor.join(".codex").join("commands"),
|
||||||
SkillOrigin::LegacyCommandsDir,
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_skill_root(
|
push_unique_skill_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::ProjectClaw,
|
DefinitionSource::ProjectClaw,
|
||||||
ancestor.join(".claw").join("commands"),
|
ancestor.join(".claw").join("commands"),
|
||||||
SkillOrigin::LegacyCommandsDir,
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1338,12 +1440,14 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
|||||||
DefinitionSource::UserCodexHome,
|
DefinitionSource::UserCodexHome,
|
||||||
codex_home.join("skills"),
|
codex_home.join("skills"),
|
||||||
SkillOrigin::SkillsDir,
|
SkillOrigin::SkillsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_skill_root(
|
push_unique_skill_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::UserCodexHome,
|
DefinitionSource::UserCodexHome,
|
||||||
codex_home.join("commands"),
|
codex_home.join("commands"),
|
||||||
SkillOrigin::LegacyCommandsDir,
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1354,37 +1458,52 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
|||||||
DefinitionSource::UserCodex,
|
DefinitionSource::UserCodex,
|
||||||
home.join(".codex").join("skills"),
|
home.join(".codex").join("skills"),
|
||||||
SkillOrigin::SkillsDir,
|
SkillOrigin::SkillsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_skill_root(
|
push_unique_skill_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::UserCodex,
|
DefinitionSource::UserCodex,
|
||||||
home.join(".codex").join("commands"),
|
home.join(".codex").join("commands"),
|
||||||
SkillOrigin::LegacyCommandsDir,
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_skill_root(
|
push_unique_skill_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::UserClaw,
|
DefinitionSource::UserClaw,
|
||||||
home.join(".claw").join("skills"),
|
home.join(".claw").join("skills"),
|
||||||
SkillOrigin::SkillsDir,
|
SkillOrigin::SkillsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
push_unique_skill_root(
|
push_unique_skill_root(
|
||||||
&mut roots,
|
&mut roots,
|
||||||
DefinitionSource::UserClaw,
|
DefinitionSource::UserClaw,
|
||||||
home.join(".claw").join("commands"),
|
home.join(".claw").join("commands"),
|
||||||
SkillOrigin::LegacyCommandsDir,
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
roots
|
append_plugin_skill_roots(&mut roots, cwd)?;
|
||||||
|
|
||||||
|
Ok(roots)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_unique_root(
|
fn push_unique_root(
|
||||||
roots: &mut Vec<(DefinitionSource, PathBuf)>,
|
roots: &mut Vec<DefinitionRoot>,
|
||||||
source: DefinitionSource,
|
source: DefinitionSource,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
namespace: Option<String>,
|
||||||
) {
|
) {
|
||||||
if path.is_dir() && !roots.iter().any(|(_, existing)| existing == &path) {
|
if path.is_dir()
|
||||||
roots.push((source, path));
|
&& !roots
|
||||||
|
.iter()
|
||||||
|
.any(|existing| existing.path == path && existing.namespace == namespace)
|
||||||
|
{
|
||||||
|
roots.push(DefinitionRoot {
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1393,25 +1512,29 @@ fn push_unique_skill_root(
|
|||||||
source: DefinitionSource,
|
source: DefinitionSource,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
origin: SkillOrigin,
|
origin: SkillOrigin,
|
||||||
|
namespace: Option<String>,
|
||||||
) {
|
) {
|
||||||
if path.is_dir() && !roots.iter().any(|existing| existing.path == path) {
|
if path.is_dir()
|
||||||
|
&& !roots
|
||||||
|
.iter()
|
||||||
|
.any(|existing| existing.path == path && existing.namespace == namespace)
|
||||||
|
{
|
||||||
roots.push(SkillRoot {
|
roots.push(SkillRoot {
|
||||||
source,
|
source,
|
||||||
path,
|
path,
|
||||||
origin,
|
origin,
|
||||||
|
namespace,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_agents_from_roots(
|
fn load_agents_from_roots(roots: &[DefinitionRoot]) -> std::io::Result<Vec<AgentSummary>> {
|
||||||
roots: &[(DefinitionSource, PathBuf)],
|
|
||||||
) -> std::io::Result<Vec<AgentSummary>> {
|
|
||||||
let mut agents = Vec::new();
|
let mut agents = Vec::new();
|
||||||
let mut active_sources = BTreeMap::<String, DefinitionSource>::new();
|
let mut active_sources = BTreeMap::<String, DefinitionSource>::new();
|
||||||
|
|
||||||
for (source, root) in roots {
|
for root in roots {
|
||||||
let mut root_agents = Vec::new();
|
let mut root_agents = Vec::new();
|
||||||
for entry in fs::read_dir(root)? {
|
for entry in fs::read_dir(&root.path)? {
|
||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
if entry.path().extension().is_none_or(|ext| ext != "toml") {
|
if entry.path().extension().is_none_or(|ext| ext != "toml") {
|
||||||
continue;
|
continue;
|
||||||
@@ -1421,12 +1544,13 @@ fn load_agents_from_roots(
|
|||||||
|| entry.file_name().to_string_lossy().to_string(),
|
|| entry.file_name().to_string_lossy().to_string(),
|
||||||
|stem| stem.to_string_lossy().to_string(),
|
|stem| stem.to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
|
let name = parse_toml_string(&contents, "name").unwrap_or(fallback_name);
|
||||||
root_agents.push(AgentSummary {
|
root_agents.push(AgentSummary {
|
||||||
name: parse_toml_string(&contents, "name").unwrap_or(fallback_name),
|
name: namespaced_definition_name(root.namespace.as_deref(), &name),
|
||||||
description: parse_toml_string(&contents, "description"),
|
description: parse_toml_string(&contents, "description"),
|
||||||
model: parse_toml_string(&contents, "model"),
|
model: parse_toml_string(&contents, "model"),
|
||||||
reasoning_effort: parse_toml_string(&contents, "model_reasoning_effort"),
|
reasoning_effort: parse_toml_string(&contents, "model_reasoning_effort"),
|
||||||
source: *source,
|
source: root.source,
|
||||||
shadowed_by: None,
|
shadowed_by: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1447,9 +1571,33 @@ fn load_agents_from_roots(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_skills_from_roots(roots: &[SkillRoot]) -> std::io::Result<Vec<SkillSummary>> {
|
fn load_skills_from_roots(roots: &[SkillRoot]) -> std::io::Result<Vec<SkillSummary>> {
|
||||||
|
let entries = load_skill_entries_from_roots(roots)?;
|
||||||
let mut skills = Vec::new();
|
let mut skills = Vec::new();
|
||||||
let mut active_sources = BTreeMap::<String, DefinitionSource>::new();
|
let mut active_sources = BTreeMap::<String, DefinitionSource>::new();
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let mut skill = SkillSummary {
|
||||||
|
name: entry.name,
|
||||||
|
description: entry.description,
|
||||||
|
source: entry.source,
|
||||||
|
shadowed_by: None,
|
||||||
|
origin: entry.origin,
|
||||||
|
};
|
||||||
|
let key = skill.name.to_ascii_lowercase();
|
||||||
|
if let Some(existing) = active_sources.get(&key) {
|
||||||
|
skill.shadowed_by = Some(*existing);
|
||||||
|
} else {
|
||||||
|
active_sources.insert(key, skill.source);
|
||||||
|
}
|
||||||
|
skills.push(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_skill_entries_from_roots(roots: &[SkillRoot]) -> std::io::Result<Vec<SkillEntry>> {
|
||||||
|
let mut skills = Vec::new();
|
||||||
|
|
||||||
for root in roots {
|
for root in roots {
|
||||||
let mut root_skills = Vec::new();
|
let mut root_skills = Vec::new();
|
||||||
for entry in fs::read_dir(&root.path)? {
|
for entry in fs::read_dir(&root.path)? {
|
||||||
@@ -1463,14 +1611,17 @@ fn load_skills_from_roots(roots: &[SkillRoot]) -> std::io::Result<Vec<SkillSumma
|
|||||||
if !skill_path.is_file() {
|
if !skill_path.is_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let contents = fs::read_to_string(skill_path)?;
|
let contents = fs::read_to_string(&skill_path)?;
|
||||||
let (name, description) = parse_skill_frontmatter(&contents);
|
let (name, description) = parse_skill_frontmatter(&contents);
|
||||||
root_skills.push(SkillSummary {
|
let fallback_name = entry.file_name().to_string_lossy().to_string();
|
||||||
name: name
|
root_skills.push(SkillEntry {
|
||||||
.unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()),
|
name: namespaced_definition_name(
|
||||||
|
root.namespace.as_deref(),
|
||||||
|
&name.unwrap_or(fallback_name),
|
||||||
|
),
|
||||||
|
path: skill_path,
|
||||||
description,
|
description,
|
||||||
source: root.source,
|
source: root.source,
|
||||||
shadowed_by: None,
|
|
||||||
origin: root.origin,
|
origin: root.origin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1497,32 +1648,153 @@ fn load_skills_from_roots(roots: &[SkillRoot]) -> std::io::Result<Vec<SkillSumma
|
|||||||
|stem| stem.to_string_lossy().to_string(),
|
|stem| stem.to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
let (name, description) = parse_skill_frontmatter(&contents);
|
let (name, description) = parse_skill_frontmatter(&contents);
|
||||||
root_skills.push(SkillSummary {
|
root_skills.push(SkillEntry {
|
||||||
name: name.unwrap_or(fallback_name),
|
name: namespaced_definition_name(
|
||||||
|
root.namespace.as_deref(),
|
||||||
|
&name.unwrap_or(fallback_name),
|
||||||
|
),
|
||||||
|
path: markdown_path,
|
||||||
description,
|
description,
|
||||||
source: root.source,
|
source: root.source,
|
||||||
shadowed_by: None,
|
|
||||||
origin: root.origin,
|
origin: root.origin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root_skills.sort_by(|left, right| left.name.cmp(&right.name));
|
root_skills.sort_by(|left, right| left.name.cmp(&right.name));
|
||||||
|
skills.extend(root_skills);
|
||||||
for mut skill in root_skills {
|
|
||||||
let key = skill.name.to_ascii_lowercase();
|
|
||||||
if let Some(existing) = active_sources.get(&key) {
|
|
||||||
skill.shadowed_by = Some(*existing);
|
|
||||||
} else {
|
|
||||||
active_sources.insert(key, skill.source);
|
|
||||||
}
|
|
||||||
skills.push(skill);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(skills)
|
Ok(skills)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn namespaced_definition_name(namespace: Option<&str>, name: &str) -> String {
|
||||||
|
namespace.map_or_else(|| name.to_string(), |namespace| format!("{namespace}:{name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_plugin_definition_roots(roots: &mut Vec<DefinitionRoot>, cwd: &Path, leaf: &str) {
|
||||||
|
if let Ok(plugins) = discover_enabled_plugins(cwd) {
|
||||||
|
for plugin in plugins {
|
||||||
|
let Some(root) = plugin.metadata.root.as_ref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let namespace = Some(plugin_namespace(&plugin));
|
||||||
|
push_unique_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join(".codex").join(leaf),
|
||||||
|
namespace.clone(),
|
||||||
|
);
|
||||||
|
push_unique_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join(".claw").join(leaf),
|
||||||
|
namespace.clone(),
|
||||||
|
);
|
||||||
|
push_unique_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join(leaf),
|
||||||
|
namespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_plugin_skill_roots(roots: &mut Vec<SkillRoot>, cwd: &Path) -> io::Result<()> {
|
||||||
|
for plugin in discover_enabled_plugins(cwd)? {
|
||||||
|
let Some(root) = plugin.metadata.root.as_ref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let namespace = Some(plugin_namespace(&plugin));
|
||||||
|
push_unique_skill_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join(".codex").join("skills"),
|
||||||
|
SkillOrigin::SkillsDir,
|
||||||
|
namespace.clone(),
|
||||||
|
);
|
||||||
|
push_unique_skill_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join(".claw").join("skills"),
|
||||||
|
SkillOrigin::SkillsDir,
|
||||||
|
namespace.clone(),
|
||||||
|
);
|
||||||
|
push_unique_skill_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join("skills"),
|
||||||
|
SkillOrigin::SkillsDir,
|
||||||
|
namespace.clone(),
|
||||||
|
);
|
||||||
|
push_unique_skill_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join(".codex").join("commands"),
|
||||||
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
namespace.clone(),
|
||||||
|
);
|
||||||
|
push_unique_skill_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join(".claw").join("commands"),
|
||||||
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
namespace.clone(),
|
||||||
|
);
|
||||||
|
push_unique_skill_root(
|
||||||
|
roots,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
|
root.join("commands"),
|
||||||
|
SkillOrigin::LegacyCommandsDir,
|
||||||
|
namespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_enabled_plugins(cwd: &Path) -> io::Result<Vec<PluginSummary>> {
|
||||||
|
let loader = ConfigLoader::default_for(cwd);
|
||||||
|
let runtime_config = loader.load().map_err(io::Error::other)?;
|
||||||
|
let plugin_settings = runtime_config.plugins();
|
||||||
|
let mut plugin_config = PluginManagerConfig::new(loader.config_home().to_path_buf());
|
||||||
|
plugin_config.enabled_plugins = plugin_settings.enabled_plugins().clone();
|
||||||
|
plugin_config.external_dirs = plugin_settings
|
||||||
|
.external_directories()
|
||||||
|
.iter()
|
||||||
|
.map(|path| resolve_plugin_path(cwd, loader.config_home(), path))
|
||||||
|
.collect();
|
||||||
|
plugin_config.install_root = plugin_settings
|
||||||
|
.install_root()
|
||||||
|
.map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
|
||||||
|
plugin_config.registry_path = plugin_settings
|
||||||
|
.registry_path()
|
||||||
|
.map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
|
||||||
|
plugin_config.bundled_root = plugin_settings
|
||||||
|
.bundled_root()
|
||||||
|
.map(|path| resolve_plugin_path(cwd, loader.config_home(), path));
|
||||||
|
PluginManager::new(plugin_config)
|
||||||
|
.list_installed_plugins()
|
||||||
|
.map(|plugins| plugins.into_iter().filter(|plugin| plugin.enabled).collect())
|
||||||
|
.map_err(io::Error::other)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_plugin_path(cwd: &Path, config_home: &Path, value: &str) -> PathBuf {
|
||||||
|
let path = PathBuf::from(value);
|
||||||
|
if path.is_absolute() {
|
||||||
|
path
|
||||||
|
} else if value.starts_with('.') {
|
||||||
|
cwd.join(path)
|
||||||
|
} else {
|
||||||
|
config_home.join(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_namespace(plugin: &PluginSummary) -> String {
|
||||||
|
plugin.metadata.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_toml_string(contents: &str, key: &str) -> Option<String> {
|
fn parse_toml_string(contents: &str, key: &str) -> Option<String> {
|
||||||
let prefix = format!("{key} =");
|
let prefix = format!("{key} =");
|
||||||
for line in contents.lines() {
|
for line in contents.lines() {
|
||||||
@@ -1613,6 +1885,7 @@ fn render_agents_report(agents: &[AgentSummary]) -> String {
|
|||||||
DefinitionSource::UserCodexHome,
|
DefinitionSource::UserCodexHome,
|
||||||
DefinitionSource::UserCodex,
|
DefinitionSource::UserCodex,
|
||||||
DefinitionSource::UserClaw,
|
DefinitionSource::UserClaw,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
] {
|
] {
|
||||||
let group = agents
|
let group = agents
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1671,6 +1944,7 @@ fn render_skills_report(skills: &[SkillSummary]) -> String {
|
|||||||
DefinitionSource::UserCodexHome,
|
DefinitionSource::UserCodexHome,
|
||||||
DefinitionSource::UserCodex,
|
DefinitionSource::UserCodex,
|
||||||
DefinitionSource::UserClaw,
|
DefinitionSource::UserClaw,
|
||||||
|
DefinitionSource::Plugin,
|
||||||
] {
|
] {
|
||||||
let group = skills
|
let group = skills
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1708,9 +1982,11 @@ fn normalize_optional_args(args: Option<&str>) -> Option<&str> {
|
|||||||
fn render_agents_usage(unexpected: Option<&str>) -> String {
|
fn render_agents_usage(unexpected: Option<&str>) -> String {
|
||||||
let mut lines = vec![
|
let mut lines = vec![
|
||||||
"Agents".to_string(),
|
"Agents".to_string(),
|
||||||
" Usage /agents".to_string(),
|
" Usage /agents [list|--help|<agent>]".to_string(),
|
||||||
" Direct CLI claw agents".to_string(),
|
" Direct CLI claw agents [list|--help|<agent>]".to_string(),
|
||||||
" Sources .codex/agents, .claw/agents, $CODEX_HOME/agents".to_string(),
|
" Invoke /agents planner -> /prompts:planner".to_string(),
|
||||||
|
" Sources .codex/agents, .claw/agents, $CODEX_HOME/agents, enabled plugins"
|
||||||
|
.to_string(),
|
||||||
];
|
];
|
||||||
if let Some(args) = unexpected {
|
if let Some(args) = unexpected {
|
||||||
lines.push(format!(" Unexpected {args}"));
|
lines.push(format!(" Unexpected {args}"));
|
||||||
@@ -1721,9 +1997,12 @@ fn render_agents_usage(unexpected: Option<&str>) -> String {
|
|||||||
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".to_string(),
|
" Usage /skills [list|--help|<skill> [args]]".to_string(),
|
||||||
" Direct CLI claw skills".to_string(),
|
" Direct CLI claw skills [list|--help|<skill> [args]]".to_string(),
|
||||||
" Sources .codex/skills, .claw/skills, legacy /commands".to_string(),
|
" Invoke /skills help overview -> $help overview".to_string(),
|
||||||
|
" Namespacing /skills plugin-name:skill".to_string(),
|
||||||
|
" Sources .codex/skills, .claw/skills, legacy /commands, enabled plugins"
|
||||||
|
.to_string(),
|
||||||
];
|
];
|
||||||
if let Some(args) = unexpected {
|
if let Some(args) = unexpected {
|
||||||
lines.push(format!(" Unexpected {args}"));
|
lines.push(format!(" Unexpected {args}"));
|
||||||
@@ -1790,13 +2069,15 @@ pub fn handle_slash_command(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
|
classify_agents_slash_command, classify_skills_slash_command,
|
||||||
handle_branch_slash_command, handle_commit_push_pr_slash_command,
|
handle_branch_slash_command, handle_commit_push_pr_slash_command,
|
||||||
handle_commit_slash_command, handle_plugins_slash_command, handle_slash_command,
|
handle_commit_slash_command, handle_plugins_slash_command, handle_skills_slash_command,
|
||||||
handle_worktree_slash_command, load_agents_from_roots, load_skills_from_roots,
|
handle_slash_command, handle_worktree_slash_command, load_agents_from_roots,
|
||||||
render_agents_report, render_plugins_report, render_skills_report,
|
load_skills_from_roots, render_agents_report, render_plugins_report,
|
||||||
render_slash_command_help, resume_supported_slash_commands, slash_command_specs,
|
render_skills_report, render_slash_command_help, resolve_skill_path,
|
||||||
suggest_slash_commands, CommitPushPrRequest, DefinitionSource, SkillOrigin, SkillRoot,
|
resume_supported_slash_commands, slash_command_specs, suggest_slash_commands,
|
||||||
SlashCommand,
|
CommitPushPrRequest, DefinitionRoot, DefinitionSource, InvokeCommandAction,
|
||||||
|
SkillOrigin, SkillRoot, SlashCommand,
|
||||||
};
|
};
|
||||||
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
|
||||||
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
use runtime::{CompactionConfig, ContentBlock, ConversationMessage, MessageRole, Session};
|
||||||
@@ -2336,8 +2617,16 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let roots = vec![
|
let roots = vec![
|
||||||
(DefinitionSource::ProjectCodex, project_agents),
|
DefinitionRoot {
|
||||||
(DefinitionSource::UserCodex, user_agents),
|
source: DefinitionSource::ProjectCodex,
|
||||||
|
path: project_agents,
|
||||||
|
namespace: None,
|
||||||
|
},
|
||||||
|
DefinitionRoot {
|
||||||
|
source: DefinitionSource::UserCodex,
|
||||||
|
path: user_agents,
|
||||||
|
namespace: None,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let report =
|
let report =
|
||||||
render_agents_report(&load_agents_from_roots(&roots).expect("agent roots should load"));
|
render_agents_report(&load_agents_from_roots(&roots).expect("agent roots should load"));
|
||||||
@@ -2372,16 +2661,19 @@ mod tests {
|
|||||||
source: DefinitionSource::ProjectCodex,
|
source: DefinitionSource::ProjectCodex,
|
||||||
path: project_skills,
|
path: project_skills,
|
||||||
origin: SkillOrigin::SkillsDir,
|
origin: SkillOrigin::SkillsDir,
|
||||||
|
namespace: None,
|
||||||
},
|
},
|
||||||
SkillRoot {
|
SkillRoot {
|
||||||
source: DefinitionSource::ProjectClaw,
|
source: DefinitionSource::ProjectClaw,
|
||||||
path: project_commands,
|
path: project_commands,
|
||||||
origin: SkillOrigin::LegacyCommandsDir,
|
origin: SkillOrigin::LegacyCommandsDir,
|
||||||
|
namespace: None,
|
||||||
},
|
},
|
||||||
SkillRoot {
|
SkillRoot {
|
||||||
source: DefinitionSource::UserCodex,
|
source: DefinitionSource::UserCodex,
|
||||||
path: user_skills,
|
path: user_skills,
|
||||||
origin: SkillOrigin::SkillsDir,
|
origin: SkillOrigin::SkillsDir,
|
||||||
|
namespace: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let report =
|
let report =
|
||||||
@@ -2426,6 +2718,97 @@ mod tests {
|
|||||||
let _ = fs::remove_dir_all(cwd);
|
let _ = fs::remove_dir_all(cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classifies_agents_and_skills_invocation_args() {
|
||||||
|
assert_eq!(
|
||||||
|
classify_agents_slash_command(None),
|
||||||
|
InvokeCommandAction::Browse
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_agents_slash_command(Some("--help")),
|
||||||
|
InvokeCommandAction::Help
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_agents_slash_command(Some("planner")),
|
||||||
|
InvokeCommandAction::Invoke("/prompts:planner".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_skills_slash_command(Some("help overview")),
|
||||||
|
InvokeCommandAction::Invoke("$help overview".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
classify_skills_slash_command(Some("oh-my-claudecode:ralplan")),
|
||||||
|
InvokeCommandAction::Invoke("$oh-my-claudecode:ralplan".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_project_and_plugin_scoped_skills() {
|
||||||
|
let _guard = env_lock();
|
||||||
|
let workspace = temp_dir("skill-resolve-workspace");
|
||||||
|
let home = temp_dir("skill-resolve-home");
|
||||||
|
let plugin_root = home
|
||||||
|
.join(".claw")
|
||||||
|
.join("plugins")
|
||||||
|
.join("installed")
|
||||||
|
.join("oh-my-claudecode-external");
|
||||||
|
|
||||||
|
write_skill(
|
||||||
|
&workspace.join(".codex").join("skills"),
|
||||||
|
"ralplan",
|
||||||
|
"Project ralplan",
|
||||||
|
);
|
||||||
|
fs::create_dir_all(plugin_root.join(".claw-plugin")).expect("plugin manifest dir");
|
||||||
|
fs::write(
|
||||||
|
plugin_root.join(".claw-plugin").join("plugin.json"),
|
||||||
|
r#"{
|
||||||
|
"name": "oh-my-claudecode",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Plugin-scoped skills"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("plugin manifest");
|
||||||
|
write_skill(&plugin_root.join("skills"), "ralplan", "Plugin ralplan");
|
||||||
|
fs::create_dir_all(home.join(".claw")).expect("config home");
|
||||||
|
fs::write(
|
||||||
|
home.join(".claw").join("settings.json"),
|
||||||
|
r#"{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"oh-my-claudecode@external": true
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("settings");
|
||||||
|
|
||||||
|
let old_home = env::var_os("HOME");
|
||||||
|
let old_codex_home = env::var_os("CODEX_HOME");
|
||||||
|
env::set_var("HOME", &home);
|
||||||
|
env::remove_var("CODEX_HOME");
|
||||||
|
|
||||||
|
let local = resolve_skill_path(&workspace, "ralplan").expect("local skill should resolve");
|
||||||
|
assert!(local.ends_with(".codex/skills/ralplan/SKILL.md"));
|
||||||
|
|
||||||
|
let plugin = resolve_skill_path(&workspace, "oh-my-claudecode:ralplan")
|
||||||
|
.expect("plugin skill should resolve");
|
||||||
|
assert!(plugin.ends_with("skills/ralplan/SKILL.md"));
|
||||||
|
|
||||||
|
let skills_report = handle_skills_slash_command(None, &workspace).expect("skills report");
|
||||||
|
assert!(skills_report.contains("Plugins:"));
|
||||||
|
assert!(skills_report.contains("oh-my-claudecode:ralplan · Plugin ralplan"));
|
||||||
|
|
||||||
|
match old_home {
|
||||||
|
Some(value) => env::set_var("HOME", value),
|
||||||
|
None => env::remove_var("HOME"),
|
||||||
|
}
|
||||||
|
match old_codex_home {
|
||||||
|
Some(value) => env::set_var("CODEX_HOME", value),
|
||||||
|
None => env::remove_var("CODEX_HOME"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(workspace);
|
||||||
|
let _ = fs::remove_dir_all(home);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_quoted_skill_frontmatter_values() {
|
fn parses_quoted_skill_frontmatter_values() {
|
||||||
let contents = "---\nname: \"hud\"\ndescription: 'Quoted description'\n---\n";
|
let contents = "---\nname: \"hud\"\ndescription: 'Quoted description'\n---\n";
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use api::{
|
|||||||
MessageRequest, MessageResponse, OutputContentBlock, ProviderClient,
|
MessageRequest, MessageResponse, OutputContentBlock, ProviderClient,
|
||||||
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
||||||
};
|
};
|
||||||
|
use commands::resolve_skill_path as resolve_workspace_skill_path;
|
||||||
use plugins::PluginTool;
|
use plugins::PluginTool;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use runtime::{
|
use runtime::{
|
||||||
@@ -1455,47 +1456,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() {
|
resolve_workspace_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(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(".agents").join("skills"));
|
|
||||||
candidates.push(home.join(".config").join("opencode").join("skills"));
|
|
||||||
candidates.push(home.join(".codex").join("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";
|
||||||
@@ -3488,6 +3450,92 @@ mod tests {
|
|||||||
.ends_with("/help/SKILL.md"));
|
.ends_with("/help/SKILL.md"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_resolves_project_and_plugin_scoped_prompts() {
|
||||||
|
let _guard = env_lock()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
let workspace = temp_path("skill-workspace");
|
||||||
|
let home = temp_path("skill-home");
|
||||||
|
let plugin_root = home
|
||||||
|
.join(".claw")
|
||||||
|
.join("plugins")
|
||||||
|
.join("installed")
|
||||||
|
.join("oh-my-claudecode-external");
|
||||||
|
let project_skill_root = workspace.join(".codex").join("skills").join("ralplan");
|
||||||
|
std::fs::create_dir_all(&project_skill_root).expect("project skill dir");
|
||||||
|
std::fs::write(
|
||||||
|
project_skill_root.join("SKILL.md"),
|
||||||
|
"---\nname: ralplan\ndescription: Project skill\n---\n",
|
||||||
|
)
|
||||||
|
.expect("project skill");
|
||||||
|
std::fs::create_dir_all(plugin_root.join(".claw-plugin")).expect("plugin manifest dir");
|
||||||
|
std::fs::write(
|
||||||
|
plugin_root.join(".claw-plugin").join("plugin.json"),
|
||||||
|
r#"{
|
||||||
|
"name": "oh-my-claudecode",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Plugin skills"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("plugin manifest");
|
||||||
|
std::fs::create_dir_all(home.join(".claw")).expect("config home");
|
||||||
|
std::fs::write(
|
||||||
|
home.join(".claw").join("settings.json"),
|
||||||
|
r#"{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"oh-my-claudecode@external": true
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("settings");
|
||||||
|
let plugin_skill_root = plugin_root.join("skills").join("ralplan");
|
||||||
|
std::fs::create_dir_all(&plugin_skill_root).expect("plugin skill dir");
|
||||||
|
std::fs::write(
|
||||||
|
plugin_skill_root.join("SKILL.md"),
|
||||||
|
"---\nname: ralplan\ndescription: Plugin skill\n---\n",
|
||||||
|
)
|
||||||
|
.expect("plugin skill");
|
||||||
|
|
||||||
|
let original_dir = std::env::current_dir().expect("cwd");
|
||||||
|
let old_home = std::env::var_os("HOME");
|
||||||
|
let old_codex_home = std::env::var_os("CODEX_HOME");
|
||||||
|
std::env::set_current_dir(&workspace).expect("set cwd");
|
||||||
|
std::env::set_var("HOME", &home);
|
||||||
|
std::env::remove_var("CODEX_HOME");
|
||||||
|
|
||||||
|
let project_result = execute_tool("Skill", &json!({ "skill": "ralplan" }))
|
||||||
|
.expect("project skill should resolve");
|
||||||
|
let project_output: serde_json::Value =
|
||||||
|
serde_json::from_str(&project_result).expect("valid json");
|
||||||
|
assert!(project_output["path"]
|
||||||
|
.as_str()
|
||||||
|
.expect("path")
|
||||||
|
.ends_with(".codex/skills/ralplan/SKILL.md"));
|
||||||
|
|
||||||
|
let plugin_result =
|
||||||
|
execute_tool("Skill", &json!({ "skill": "$oh-my-claudecode:ralplan" }))
|
||||||
|
.expect("plugin skill should resolve");
|
||||||
|
let plugin_output: serde_json::Value =
|
||||||
|
serde_json::from_str(&plugin_result).expect("valid json");
|
||||||
|
assert!(plugin_output["path"]
|
||||||
|
.as_str()
|
||||||
|
.expect("path")
|
||||||
|
.ends_with("skills/ralplan/SKILL.md"));
|
||||||
|
|
||||||
|
std::env::set_current_dir(&original_dir).expect("restore cwd");
|
||||||
|
match old_home {
|
||||||
|
Some(value) => std::env::set_var("HOME", value),
|
||||||
|
None => std::env::remove_var("HOME"),
|
||||||
|
}
|
||||||
|
match old_codex_home {
|
||||||
|
Some(value) => std::env::set_var("CODEX_HOME", value),
|
||||||
|
None => std::env::remove_var("CODEX_HOME"),
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(workspace);
|
||||||
|
let _ = std::fs::remove_dir_all(home);
|
||||||
|
}
|
||||||
|
|
||||||
#[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