From 549ad7c3afc8f75486d6171ef638de12ef1d940a Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Mon, 6 Apr 2026 09:11:27 +0000 Subject: [PATCH] Restore compatibility skill lookup fallback --- rust/crates/tools/src/lib.rs | 258 ++++++++++++++++++++++++++++++++++- 1 file changed, 257 insertions(+), 1 deletion(-) diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index f3f8010..7de712f 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -2974,7 +2974,263 @@ fn todo_store_path() -> Result { fn resolve_skill_path(skill: &str) -> Result { 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 { + 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 { + 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, 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, 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, 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, + 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 { + 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 { + 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 { + 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 { + parse_skill_frontmatter_value(contents, "name") +} + +fn parse_skill_frontmatter_value(contents: &str, key: &str) -> Option { + 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";