mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-08 00:54:49 +08:00
Restore compatibility skill lookup fallback
This commit is contained in:
@@ -2974,7 +2974,263 @@ 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 cwd = std::env::current_dir().map_err(|error| error.to_string())?;
|
let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
|
||||||
commands::resolve_skill_path(&cwd, skill).map_err(|error| error.to_string())
|
match commands::resolve_skill_path(&cwd, skill) {
|
||||||
|
Ok(path) => Ok(path),
|
||||||
|
Err(_) => resolve_skill_path_from_compat_roots(skill),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_skill_path_from_compat_roots(skill: &str) -> Result<std::path::PathBuf, String> {
|
||||||
|
let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
|
||||||
|
if requested.is_empty() {
|
||||||
|
return Err(String::from("skill must not be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for root in skill_lookup_roots() {
|
||||||
|
if let Some(path) = resolve_skill_path_in_root(&root, requested) {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("unknown skill: {requested}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum SkillLookupOrigin {
|
||||||
|
SkillsDir,
|
||||||
|
LegacyCommandsDir,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct SkillLookupRoot {
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
origin: SkillLookupOrigin,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skill_lookup_roots() -> Vec<SkillLookupRoot> {
|
||||||
|
let mut roots = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(cwd) = std::env::current_dir() {
|
||||||
|
push_project_skill_lookup_roots(&mut roots, &cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(claw_config_home) = std::env::var("CLAW_CONFIG_HOME") {
|
||||||
|
push_prefixed_skill_lookup_roots(&mut roots, std::path::Path::new(&claw_config_home));
|
||||||
|
}
|
||||||
|
if let Ok(codex_home) = std::env::var("CODEX_HOME") {
|
||||||
|
push_prefixed_skill_lookup_roots(&mut roots, std::path::Path::new(&codex_home));
|
||||||
|
}
|
||||||
|
if let Ok(home) = std::env::var("HOME") {
|
||||||
|
push_home_skill_lookup_roots(&mut roots, std::path::Path::new(&home));
|
||||||
|
}
|
||||||
|
if let Ok(claude_config_dir) = std::env::var("CLAUDE_CONFIG_DIR") {
|
||||||
|
let claude_config_dir = std::path::PathBuf::from(claude_config_dir);
|
||||||
|
push_skill_lookup_root(
|
||||||
|
&mut roots,
|
||||||
|
claude_config_dir.join("skills"),
|
||||||
|
SkillLookupOrigin::SkillsDir,
|
||||||
|
);
|
||||||
|
push_skill_lookup_root(
|
||||||
|
&mut roots,
|
||||||
|
claude_config_dir.join("skills").join("omc-learned"),
|
||||||
|
SkillLookupOrigin::SkillsDir,
|
||||||
|
);
|
||||||
|
push_skill_lookup_root(
|
||||||
|
&mut roots,
|
||||||
|
claude_config_dir.join("commands"),
|
||||||
|
SkillLookupOrigin::LegacyCommandsDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
push_skill_lookup_root(
|
||||||
|
&mut roots,
|
||||||
|
std::path::PathBuf::from("/home/bellman/.claw/skills"),
|
||||||
|
SkillLookupOrigin::SkillsDir,
|
||||||
|
);
|
||||||
|
push_skill_lookup_root(
|
||||||
|
&mut roots,
|
||||||
|
std::path::PathBuf::from("/home/bellman/.codex/skills"),
|
||||||
|
SkillLookupOrigin::SkillsDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
roots
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_project_skill_lookup_roots(roots: &mut Vec<SkillLookupRoot>, cwd: &std::path::Path) {
|
||||||
|
for ancestor in cwd.ancestors() {
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &ancestor.join(".omc"));
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &ancestor.join(".agents"));
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &ancestor.join(".claw"));
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &ancestor.join(".codex"));
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &ancestor.join(".claude"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_home_skill_lookup_roots(roots: &mut Vec<SkillLookupRoot>, home: &std::path::Path) {
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &home.join(".omc"));
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &home.join(".claw"));
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &home.join(".codex"));
|
||||||
|
push_prefixed_skill_lookup_roots(roots, &home.join(".claude"));
|
||||||
|
push_skill_lookup_root(
|
||||||
|
roots,
|
||||||
|
home.join(".agents").join("skills"),
|
||||||
|
SkillLookupOrigin::SkillsDir,
|
||||||
|
);
|
||||||
|
push_skill_lookup_root(
|
||||||
|
roots,
|
||||||
|
home.join(".config").join("opencode").join("skills"),
|
||||||
|
SkillLookupOrigin::SkillsDir,
|
||||||
|
);
|
||||||
|
push_skill_lookup_root(
|
||||||
|
roots,
|
||||||
|
home.join(".claude").join("skills").join("omc-learned"),
|
||||||
|
SkillLookupOrigin::SkillsDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_prefixed_skill_lookup_roots(roots: &mut Vec<SkillLookupRoot>, prefix: &std::path::Path) {
|
||||||
|
push_skill_lookup_root(roots, prefix.join("skills"), SkillLookupOrigin::SkillsDir);
|
||||||
|
push_skill_lookup_root(
|
||||||
|
roots,
|
||||||
|
prefix.join("commands"),
|
||||||
|
SkillLookupOrigin::LegacyCommandsDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_skill_lookup_root(
|
||||||
|
roots: &mut Vec<SkillLookupRoot>,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
origin: SkillLookupOrigin,
|
||||||
|
) {
|
||||||
|
if path.is_dir() && !roots.iter().any(|existing| existing.path == path) {
|
||||||
|
roots.push(SkillLookupRoot { path, origin });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_skill_path_in_root(
|
||||||
|
root: &SkillLookupRoot,
|
||||||
|
requested: &str,
|
||||||
|
) -> Option<std::path::PathBuf> {
|
||||||
|
match root.origin {
|
||||||
|
SkillLookupOrigin::SkillsDir => resolve_skill_path_in_skills_dir(&root.path, requested),
|
||||||
|
SkillLookupOrigin::LegacyCommandsDir => {
|
||||||
|
resolve_skill_path_in_legacy_commands_dir(&root.path, requested)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_skill_path_in_skills_dir(
|
||||||
|
root: &std::path::Path,
|
||||||
|
requested: &str,
|
||||||
|
) -> Option<std::path::PathBuf> {
|
||||||
|
let direct = root.join(requested).join("SKILL.md");
|
||||||
|
if direct.is_file() {
|
||||||
|
return Some(direct);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = std::fs::read_dir(root).ok()?;
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if !entry.path().is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let skill_path = entry.path().join("SKILL.md");
|
||||||
|
if !skill_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if entry
|
||||||
|
.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.eq_ignore_ascii_case(requested)
|
||||||
|
|| skill_frontmatter_name_matches(&skill_path, requested)
|
||||||
|
{
|
||||||
|
return Some(skill_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_skill_path_in_legacy_commands_dir(
|
||||||
|
root: &std::path::Path,
|
||||||
|
requested: &str,
|
||||||
|
) -> Option<std::path::PathBuf> {
|
||||||
|
let direct_dir = root.join(requested).join("SKILL.md");
|
||||||
|
if direct_dir.is_file() {
|
||||||
|
return Some(direct_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let direct_markdown = root.join(format!("{requested}.md"));
|
||||||
|
if direct_markdown.is_file() {
|
||||||
|
return Some(direct_markdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = std::fs::read_dir(root).ok()?;
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let candidate_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 matches_entry_name = candidate_path
|
||||||
|
.file_stem()
|
||||||
|
.is_some_and(|stem| stem.to_string_lossy().eq_ignore_ascii_case(requested))
|
||||||
|
|| entry
|
||||||
|
.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.trim_end_matches(".md")
|
||||||
|
.eq_ignore_ascii_case(requested);
|
||||||
|
if matches_entry_name || skill_frontmatter_name_matches(&candidate_path, requested) {
|
||||||
|
return Some(candidate_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skill_frontmatter_name_matches(path: &std::path::Path, requested: &str) -> bool {
|
||||||
|
std::fs::read_to_string(path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|contents| parse_skill_name(&contents))
|
||||||
|
.is_some_and(|name| name.eq_ignore_ascii_case(requested))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_skill_name(contents: &str) -> Option<String> {
|
||||||
|
parse_skill_frontmatter_value(contents, "name")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_skill_frontmatter_value(contents: &str, key: &str) -> Option<String> {
|
||||||
|
let mut lines = contents.lines();
|
||||||
|
if lines.next().map(str::trim) != Some("---") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed == "---" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(value) = trimmed.strip_prefix(&format!("{key}:")) {
|
||||||
|
let value = value
|
||||||
|
.trim()
|
||||||
|
.trim_matches(|ch| matches!(ch, '"' | '\''))
|
||||||
|
.trim();
|
||||||
|
if !value.is_empty() {
|
||||||
|
return Some(value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_AGENT_MODEL: &str = "claude-opus-4-6";
|
const DEFAULT_AGENT_MODEL: &str = "claude-opus-4-6";
|
||||||
|
|||||||
Reference in New Issue
Block a user