mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-06 16:14:49 +08:00
Resolve claw-code main merge conflicts
This commit is contained in:
@@ -243,7 +243,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "skills",
|
||||
aliases: &[],
|
||||
aliases: &["skill"],
|
||||
summary: "List, install, or invoke available skills",
|
||||
argument_hint: Some("[list|install <path>|help|<skill> [args]]"),
|
||||
resume_supported: true,
|
||||
@@ -1312,7 +1312,7 @@ pub fn validate_slash_command_input(
|
||||
"agents" => SlashCommand::Agents {
|
||||
args: parse_list_or_help_args(command, remainder)?,
|
||||
},
|
||||
"skills" => SlashCommand::Skills {
|
||||
"skills" | "skill" => SlashCommand::Skills {
|
||||
args: parse_skills_args(remainder.as_deref())?,
|
||||
},
|
||||
"doctor" => {
|
||||
@@ -1975,9 +1975,9 @@ enum DefinitionScope {
|
||||
impl DefinitionScope {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Project => "Project (.claw)",
|
||||
Self::UserConfigHome => "User ($CLAW_CONFIG_HOME)",
|
||||
Self::UserHome => "User (~/.claw)",
|
||||
Self::Project => "Project roots",
|
||||
Self::UserConfigHome => "User config roots",
|
||||
Self::UserHome => "User home roots",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2548,6 +2548,14 @@ fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, P
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(claude_config_dir) = env::var("CLAUDE_CONFIG_DIR") {
|
||||
push_unique_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
PathBuf::from(claude_config_dir).join(leaf),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(home) = env::var_os("HOME") {
|
||||
let home = PathBuf::from(home);
|
||||
push_unique_root(
|
||||
@@ -2581,6 +2589,18 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
ancestor.join(".claw").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectClaw,
|
||||
ancestor.join(".omc").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectClaw,
|
||||
ancestor.join(".agents").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::ProjectCodex,
|
||||
@@ -2653,6 +2673,12 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
home.join(".claw").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaw,
|
||||
home.join(".omc").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaw,
|
||||
@@ -2677,6 +2703,12 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
home.join(".claude").join("skills"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
home.join(".claude").join("skills").join("omc-learned"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
@@ -2685,6 +2717,29 @@ fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(claude_config_dir) = env::var("CLAUDE_CONFIG_DIR") {
|
||||
let claude_config_dir = PathBuf::from(claude_config_dir);
|
||||
let skills_dir = claude_config_dir.join("skills");
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
skills_dir.clone(),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
skills_dir.join("omc-learned"),
|
||||
SkillOrigin::SkillsDir,
|
||||
);
|
||||
push_unique_skill_root(
|
||||
&mut roots,
|
||||
DefinitionSource::UserClaude,
|
||||
claude_config_dir.join("commands"),
|
||||
SkillOrigin::LegacyCommandsDir,
|
||||
);
|
||||
}
|
||||
|
||||
roots
|
||||
}
|
||||
|
||||
@@ -3467,10 +3522,11 @@ fn render_skills_usage(unexpected: Option<&str>) -> String {
|
||||
let mut lines = vec![
|
||||
"Skills".to_string(),
|
||||
" Usage /skills [list|install <path>|help|<skill> [args]]".to_string(),
|
||||
" Alias /skill".to_string(),
|
||||
" Direct CLI claw skills [list|install <path>|help|<skill> [args]]".to_string(),
|
||||
" Invoke /skills help overview -> $help overview".to_string(),
|
||||
" Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills".to_string(),
|
||||
" Sources .claw/skills, ~/.claw/skills, legacy /commands".to_string(),
|
||||
" Sources .claw/skills, .omc/skills, .agents/skills, .codex/skills, .claude/skills, ~/.claw/skills, ~/.omc/skills, ~/.claude/skills/omc-learned, ~/.codex/skills, ~/.claude/skills, legacy /commands".to_string(),
|
||||
];
|
||||
if let Some(args) = unexpected {
|
||||
lines.push(format!(" Unexpected {args}"));
|
||||
@@ -3484,10 +3540,24 @@ fn render_skills_usage_json(unexpected: Option<&str>) -> Value {
|
||||
"action": "help",
|
||||
"usage": {
|
||||
"slash_command": "/skills [list|install <path>|help|<skill> [args]]",
|
||||
"aliases": ["/skill"],
|
||||
"direct_cli": "claw skills [list|install <path>|help|<skill> [args]]",
|
||||
"invoke": "/skills help overview -> $help overview",
|
||||
"install_root": "$CLAW_CONFIG_HOME/skills or ~/.claw/skills",
|
||||
"sources": [".claw/skills", "legacy /commands", "legacy fallback dirs still load automatically"],
|
||||
"sources": [
|
||||
".claw/skills",
|
||||
".omc/skills",
|
||||
".agents/skills",
|
||||
".codex/skills",
|
||||
".claude/skills",
|
||||
"~/.claw/skills",
|
||||
"~/.omc/skills",
|
||||
"~/.claude/skills/omc-learned",
|
||||
"~/.codex/skills",
|
||||
"~/.claude/skills",
|
||||
"legacy /commands",
|
||||
"legacy fallback dirs still load automatically"
|
||||
],
|
||||
},
|
||||
"unexpected": unexpected,
|
||||
})
|
||||
@@ -3849,8 +3919,10 @@ mod tests {
|
||||
use runtime::{
|
||||
CompactionConfig, ConfigLoader, ContentBlock, ConversationMessage, MessageRole, Session,
|
||||
};
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temp_dir(label: &str) -> PathBuf {
|
||||
@@ -3861,6 +3933,18 @@ mod tests {
|
||||
std::env::temp_dir().join(format!("commands-plugin-{label}-{nanos}"))
|
||||
}
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
fn restore_env_var(key: &str, original: Option<OsString>) {
|
||||
match original {
|
||||
Some(value) => std::env::set_var(key, value),
|
||||
None => std::env::remove_var(key),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_external_plugin(root: &Path, name: &str, version: &str) {
|
||||
fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
|
||||
fs::write(
|
||||
@@ -4275,6 +4359,7 @@ mod tests {
|
||||
assert!(help.contains("aliases: /plugins, /marketplace"));
|
||||
assert!(help.contains("/agents [list|help]"));
|
||||
assert!(help.contains("/skills [list|install <path>|help|<skill> [args]]"));
|
||||
assert!(help.contains("aliases: /skill"));
|
||||
assert_eq!(slash_command_specs().len(), 141);
|
||||
assert!(resume_supported_slash_commands().len() >= 39);
|
||||
}
|
||||
@@ -4517,10 +4602,10 @@ mod tests {
|
||||
|
||||
assert!(report.contains("Agents"));
|
||||
assert!(report.contains("2 active agents"));
|
||||
assert!(report.contains("Project (.claw):"));
|
||||
assert!(report.contains("Project roots:"));
|
||||
assert!(report.contains("planner · Project planner · gpt-5.4 · medium"));
|
||||
assert!(report.contains("User (~/.claw):"));
|
||||
assert!(report.contains("(shadowed by Project (.claw)) planner · User planner"));
|
||||
assert!(report.contains("User home roots:"));
|
||||
assert!(report.contains("(shadowed by Project roots) planner · User planner"));
|
||||
assert!(report.contains("verifier · Verification agent · gpt-5.4-mini · high"));
|
||||
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
@@ -4628,11 +4713,11 @@ mod tests {
|
||||
|
||||
assert!(report.contains("Skills"));
|
||||
assert!(report.contains("3 available skills"));
|
||||
assert!(report.contains("Project (.claw):"));
|
||||
assert!(report.contains("Project roots:"));
|
||||
assert!(report.contains("plan · Project planning guidance"));
|
||||
assert!(report.contains("deploy · Legacy deployment guidance · legacy /commands"));
|
||||
assert!(report.contains("User (~/.claw):"));
|
||||
assert!(report.contains("(shadowed by Project (.claw)) plan · User planning guidance"));
|
||||
assert!(report.contains("User home roots:"));
|
||||
assert!(report.contains("(shadowed by Project roots) plan · User planning guidance"));
|
||||
assert!(report.contains("help · Help guidance"));
|
||||
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
@@ -4704,6 +4789,7 @@ mod tests {
|
||||
let help = handle_skills_slash_command_json(Some("help"), &workspace).expect("skills help");
|
||||
assert_eq!(help["kind"], "skills");
|
||||
assert_eq!(help["action"], "help");
|
||||
assert_eq!(help["usage"]["aliases"][0], "/skill");
|
||||
assert_eq!(
|
||||
help["usage"]["direct_cli"],
|
||||
"claw skills [list|install <path>|help|<skill> [args]]"
|
||||
@@ -4732,8 +4818,12 @@ mod tests {
|
||||
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
|
||||
assert!(skills_help
|
||||
.contains("Usage /skills [list|install <path>|help|<skill> [args]]"));
|
||||
assert!(skills_help.contains("Alias /skill"));
|
||||
assert!(skills_help.contains("Invoke /skills help overview -> $help overview"));
|
||||
assert!(skills_help.contains("Install root $CLAW_CONFIG_HOME/skills or ~/.claw/skills"));
|
||||
assert!(skills_help.contains(".omc/skills"));
|
||||
assert!(skills_help.contains(".agents/skills"));
|
||||
assert!(skills_help.contains("~/.claude/skills/omc-learned"));
|
||||
assert!(skills_help.contains("legacy /commands"));
|
||||
|
||||
let skills_unexpected =
|
||||
@@ -4744,6 +4834,7 @@ mod tests {
|
||||
.expect("nested skills help");
|
||||
assert!(skills_install_help
|
||||
.contains("Usage /skills [list|install <path>|help|<skill> [args]]"));
|
||||
assert!(skills_install_help.contains("Alias /skill"));
|
||||
assert!(skills_install_help.contains("Unexpected install"));
|
||||
|
||||
let skills_unknown_help =
|
||||
@@ -4752,9 +4843,87 @@ mod tests {
|
||||
.contains("Usage /skills [list|install <path>|help|<skill> [args]]"));
|
||||
assert!(skills_unknown_help.contains("Unexpected show"));
|
||||
|
||||
let skills_help_json =
|
||||
super::handle_skills_slash_command_json(Some("help"), &cwd).expect("skills help json");
|
||||
let sources = skills_help_json["usage"]["sources"]
|
||||
.as_array()
|
||||
.expect("skills help sources");
|
||||
assert_eq!(skills_help_json["usage"]["aliases"][0], "/skill");
|
||||
assert!(sources.iter().any(|value| value == ".omc/skills"));
|
||||
assert!(sources.iter().any(|value| value == ".agents/skills"));
|
||||
assert!(sources.iter().any(|value| value == "~/.omc/skills"));
|
||||
assert!(sources
|
||||
.iter()
|
||||
.any(|value| value == "~/.claude/skills/omc-learned"));
|
||||
|
||||
let _ = fs::remove_dir_all(cwd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_omc_skills_from_project_and_user_compatibility_roots() {
|
||||
let _guard = env_lock().lock().expect("env lock");
|
||||
let workspace = temp_dir("skills-omc-workspace");
|
||||
let user_home = temp_dir("skills-omc-home");
|
||||
let claude_config_dir = temp_dir("skills-omc-claude-config");
|
||||
let project_omc_skills = workspace.join(".omc").join("skills");
|
||||
let project_agents_skills = workspace.join(".agents").join("skills");
|
||||
let user_omc_skills = user_home.join(".omc").join("skills");
|
||||
let claude_config_skills = claude_config_dir.join("skills");
|
||||
let claude_config_commands = claude_config_dir.join("commands");
|
||||
let learned_skills = claude_config_dir.join("skills").join("omc-learned");
|
||||
let original_home = std::env::var_os("HOME");
|
||||
let original_claude_config_dir = std::env::var_os("CLAUDE_CONFIG_DIR");
|
||||
|
||||
write_skill(&project_omc_skills, "hud", "OMC HUD guidance");
|
||||
write_skill(
|
||||
&project_agents_skills,
|
||||
"trace",
|
||||
"Compatibility skill guidance",
|
||||
);
|
||||
write_skill(&user_omc_skills, "cancel", "OMC cancel guidance");
|
||||
write_skill(
|
||||
&claude_config_skills,
|
||||
"statusline",
|
||||
"Claude config skill guidance",
|
||||
);
|
||||
write_legacy_command(
|
||||
&claude_config_commands,
|
||||
"doctor-check",
|
||||
"Claude config command guidance",
|
||||
);
|
||||
write_skill(&learned_skills, "learned", "Learned skill guidance");
|
||||
std::env::set_var("HOME", &user_home);
|
||||
std::env::set_var("CLAUDE_CONFIG_DIR", &claude_config_dir);
|
||||
|
||||
let report = super::handle_skills_slash_command(None, &workspace).expect("skills list");
|
||||
assert!(report.contains("available skills"));
|
||||
assert!(report.contains("hud · OMC HUD guidance"));
|
||||
assert!(report.contains("trace · Compatibility skill guidance"));
|
||||
assert!(report.contains("cancel · OMC cancel guidance"));
|
||||
assert!(report.contains("statusline · Claude config skill guidance"));
|
||||
assert!(report.contains("doctor-check · Claude config command guidance · legacy /commands"));
|
||||
assert!(report.contains("learned · Learned skill guidance"));
|
||||
|
||||
let help =
|
||||
super::handle_skills_slash_command_json(Some("help"), &workspace).expect("skills help");
|
||||
let sources = help["usage"]["sources"]
|
||||
.as_array()
|
||||
.expect("skills help sources");
|
||||
assert_eq!(help["usage"]["aliases"][0], "/skill");
|
||||
assert!(sources.iter().any(|value| value == ".omc/skills"));
|
||||
assert!(sources.iter().any(|value| value == ".agents/skills"));
|
||||
assert!(sources.iter().any(|value| value == "~/.omc/skills"));
|
||||
assert!(sources
|
||||
.iter()
|
||||
.any(|value| value == "~/.claude/skills/omc-learned"));
|
||||
|
||||
restore_env_var("HOME", original_home);
|
||||
restore_env_var("CLAUDE_CONFIG_DIR", original_claude_config_dir);
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
let _ = fs::remove_dir_all(user_home);
|
||||
let _ = fs::remove_dir_all(claude_config_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_usage_supports_help_and_unexpected_args() {
|
||||
let cwd = temp_dir("mcp-usage");
|
||||
@@ -4991,7 +5160,7 @@ mod tests {
|
||||
let listed = render_skills_report(
|
||||
&load_skills_from_roots(&roots).expect("installed skills should load"),
|
||||
);
|
||||
assert!(listed.contains("User ($CLAW_CONFIG_HOME):"));
|
||||
assert!(listed.contains("User config roots:"));
|
||||
assert!(listed.contains("help · Helpful skill"));
|
||||
|
||||
let _ = fs::remove_dir_all(workspace);
|
||||
|
||||
@@ -920,6 +920,9 @@ pub enum PluginManifestValidationError {
|
||||
tool_name: String,
|
||||
permission: String,
|
||||
},
|
||||
UnsupportedManifestContract {
|
||||
detail: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for PluginManifestValidationError {
|
||||
@@ -965,6 +968,7 @@ impl Display for PluginManifestValidationError {
|
||||
f,
|
||||
"plugin tool `{tool_name}` requiredPermission `{permission}` must be read-only, workspace-write, or danger-full-access"
|
||||
),
|
||||
Self::UnsupportedManifestContract { detail } => f.write_str(detail),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1594,10 +1598,73 @@ fn load_manifest_from_path(
|
||||
manifest_path.display()
|
||||
))
|
||||
})?;
|
||||
let raw_manifest: RawPluginManifest = serde_json::from_str(&contents)?;
|
||||
let raw_json: Value = serde_json::from_str(&contents)?;
|
||||
let compatibility_errors = detect_claude_code_manifest_contract_gaps(&raw_json);
|
||||
if !compatibility_errors.is_empty() {
|
||||
return Err(PluginError::ManifestValidation(compatibility_errors));
|
||||
}
|
||||
let raw_manifest: RawPluginManifest = serde_json::from_value(raw_json)?;
|
||||
build_plugin_manifest(root, raw_manifest)
|
||||
}
|
||||
|
||||
fn detect_claude_code_manifest_contract_gaps(
|
||||
raw_manifest: &Value,
|
||||
) -> Vec<PluginManifestValidationError> {
|
||||
let Some(root) = raw_manifest.as_object() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for (field, detail) in [
|
||||
(
|
||||
"skills",
|
||||
"plugin manifest field `skills` uses the Claude Code plugin contract; `claw` does not load plugin-managed skills and instead discovers skills from local roots such as `.claw/skills`, `.omc/skills`, `.agents/skills`, `~/.omc/skills`, and `~/.claude/skills/omc-learned`.",
|
||||
),
|
||||
(
|
||||
"mcpServers",
|
||||
"plugin manifest field `mcpServers` uses the Claude Code plugin contract; `claw` does not import MCP servers from plugin manifests.",
|
||||
),
|
||||
(
|
||||
"agents",
|
||||
"plugin manifest field `agents` uses the Claude Code plugin contract; `claw` does not load plugin-managed agent markdown catalogs from plugin manifests.",
|
||||
),
|
||||
] {
|
||||
if root.contains_key(field) {
|
||||
errors.push(PluginManifestValidationError::UnsupportedManifestContract {
|
||||
detail: detail.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if root
|
||||
.get("commands")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|commands| commands.iter().any(Value::is_string))
|
||||
{
|
||||
errors.push(PluginManifestValidationError::UnsupportedManifestContract {
|
||||
detail: "plugin manifest field `commands` uses Claude Code-style directory globs; `claw` slash dispatch is still built-in and does not load plugin slash command markdown files.".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(hooks) = root.get("hooks").and_then(Value::as_object) {
|
||||
for hook_name in hooks.keys() {
|
||||
if !matches!(
|
||||
hook_name.as_str(),
|
||||
"PreToolUse" | "PostToolUse" | "PostToolUseFailure"
|
||||
) {
|
||||
errors.push(PluginManifestValidationError::UnsupportedManifestContract {
|
||||
detail: format!(
|
||||
"plugin hook `{hook_name}` uses the Claude Code lifecycle contract; `claw` plugins currently support only PreToolUse, PostToolUse, and PostToolUseFailure."
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
|
||||
fn plugin_manifest_path(root: &Path) -> Result<PathBuf, PluginError> {
|
||||
let direct_path = root.join(MANIFEST_FILE_NAME);
|
||||
if direct_path.exists() {
|
||||
@@ -2517,6 +2584,37 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_plugin_from_directory_rejects_claude_code_manifest_contracts_with_guidance() {
|
||||
let root = temp_dir("manifest-claude-code-contract");
|
||||
write_file(
|
||||
root.join(MANIFEST_FILE_NAME).as_path(),
|
||||
r#"{
|
||||
"name": "oh-my-claudecode",
|
||||
"version": "4.10.2",
|
||||
"description": "Claude Code plugin manifest",
|
||||
"hooks": {
|
||||
"SessionStart": ["scripts/session-start.mjs"]
|
||||
},
|
||||
"agents": ["agents/*.md"],
|
||||
"commands": ["commands/**/*.md"],
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.mcp.json"
|
||||
}"#,
|
||||
);
|
||||
|
||||
let error = load_plugin_from_directory(&root)
|
||||
.expect_err("Claude Code plugin manifest should fail with guidance");
|
||||
let rendered = error.to_string();
|
||||
assert!(rendered.contains("field `skills` uses the Claude Code plugin contract"));
|
||||
assert!(rendered.contains("field `mcpServers` uses the Claude Code plugin contract"));
|
||||
assert!(rendered.contains("field `agents` uses the Claude Code plugin contract"));
|
||||
assert!(rendered.contains("field `commands` uses Claude Code-style directory globs"));
|
||||
assert!(rendered.contains("hook `SessionStart` uses the Claude Code lifecycle contract"));
|
||||
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_plugin_from_directory_rejects_missing_tool_or_command_paths() {
|
||||
let root = temp_dir("manifest-paths");
|
||||
|
||||
@@ -128,6 +128,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
args,
|
||||
output_format,
|
||||
} => LiveCli::print_skills(args.as_deref(), output_format)?,
|
||||
CliAction::Plugins {
|
||||
action,
|
||||
target,
|
||||
output_format,
|
||||
} => LiveCli::print_plugins(action.as_deref(), target.as_deref(), output_format)?,
|
||||
CliAction::PrintSystemPrompt {
|
||||
cwd,
|
||||
date,
|
||||
@@ -188,6 +193,11 @@ enum CliAction {
|
||||
args: Option<String>,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Plugins {
|
||||
action: Option<String>,
|
||||
target: Option<String>,
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
PrintSystemPrompt {
|
||||
cwd: PathBuf,
|
||||
date: String,
|
||||
@@ -619,6 +629,10 @@ fn format_unknown_direct_slash_command(name: &str) -> String {
|
||||
message.push('\n');
|
||||
message.push_str(&suggestions);
|
||||
}
|
||||
if let Some(note) = omc_compatibility_note_for_unknown_slash_command(name) {
|
||||
message.push('\n');
|
||||
message.push_str(note);
|
||||
}
|
||||
message.push_str("\nRun `claw --help` for CLI usage, or start `claw` and use /help.");
|
||||
message
|
||||
}
|
||||
@@ -630,10 +644,21 @@ fn format_unknown_slash_command(name: &str) -> String {
|
||||
message.push('\n');
|
||||
message.push_str(&suggestions);
|
||||
}
|
||||
if let Some(note) = omc_compatibility_note_for_unknown_slash_command(name) {
|
||||
message.push('\n');
|
||||
message.push_str(note);
|
||||
}
|
||||
message.push_str("\n Help /help lists available slash commands");
|
||||
message
|
||||
}
|
||||
|
||||
fn omc_compatibility_note_for_unknown_slash_command(name: &str) -> Option<&'static str> {
|
||||
name.starts_with("oh-my-claudecode:")
|
||||
.then_some(
|
||||
"Compatibility note: `/oh-my-claudecode:*` is a Claude Code/OMC plugin command. `claw` does not yet load plugin slash commands, Claude statusline stdin, or OMC session hooks.",
|
||||
)
|
||||
}
|
||||
|
||||
fn render_suggestion_line(label: &str, suggestions: &[String]) -> Option<String> {
|
||||
(!suggestions.is_empty()).then(|| format!(" {label:<16} {}", suggestions.join(", "),))
|
||||
}
|
||||
@@ -1885,14 +1910,18 @@ impl GitWorkspaceSummary {
|
||||
#[cfg(test)]
|
||||
fn format_unknown_slash_command_message(name: &str) -> String {
|
||||
let suggestions = suggest_slash_commands(name);
|
||||
if suggestions.is_empty() {
|
||||
format!("unknown slash command: /{name}. Use /help to list available commands.")
|
||||
} else {
|
||||
format!(
|
||||
"unknown slash command: /{name}. Did you mean {}? Use /help to list available commands.",
|
||||
suggestions.join(", ")
|
||||
)
|
||||
let mut message = format!("unknown slash command: /{name}.");
|
||||
if !suggestions.is_empty() {
|
||||
message.push_str(" Did you mean ");
|
||||
message.push_str(&suggestions.join(", "));
|
||||
message.push('?');
|
||||
}
|
||||
if let Some(note) = omc_compatibility_note_for_unknown_slash_command(name) {
|
||||
message.push(' ');
|
||||
message.push_str(note);
|
||||
}
|
||||
message.push_str(" Use /help to list available commands.");
|
||||
message
|
||||
}
|
||||
|
||||
fn format_model_report(model: &str, message_count: usize, turns: u32) -> String {
|
||||
@@ -3569,6 +3598,32 @@ impl LiveCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_plugins(
|
||||
action: Option<&str>,
|
||||
target: Option<&str>,
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
let loader = ConfigLoader::default_for(&cwd);
|
||||
let runtime_config = loader.load()?;
|
||||
let mut manager = build_plugin_manager(&cwd, &loader, &runtime_config);
|
||||
let result = handle_plugins_slash_command(action, target, &mut manager)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("{}", result.message),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "plugin",
|
||||
"action": action.unwrap_or("list"),
|
||||
"target": target,
|
||||
"message": result.message,
|
||||
"reload_runtime": result.reload_runtime,
|
||||
}))?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_diff() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("{}", render_diff_report()?);
|
||||
Ok(())
|
||||
@@ -7449,6 +7504,13 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skill".to_string()]).expect("/skill should parse"),
|
||||
CliAction::Skills {
|
||||
args: None,
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skills".to_string(), "help".to_string()])
|
||||
.expect("/skills help should parse"),
|
||||
@@ -7457,6 +7519,14 @@ mod tests {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["/skill".to_string(), "list".to_string()])
|
||||
.expect("/skill list should parse"),
|
||||
CliAction::Skills {
|
||||
args: Some("list".to_string()),
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&[
|
||||
"/skills".to_string(),
|
||||
@@ -7515,6 +7585,16 @@ mod tests {
|
||||
assert!(report.contains("Use /help"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_namespaced_omc_slash_command_with_contract_guidance() {
|
||||
let report = format_unknown_slash_command_message("oh-my-claudecode:hud");
|
||||
assert!(report.contains("unknown slash command: /oh-my-claudecode:hud"));
|
||||
assert!(report.contains("Claude Code/OMC plugin command"));
|
||||
assert!(report.contains("plugin slash commands"));
|
||||
assert!(report.contains("statusline"));
|
||||
assert!(report.contains("session hooks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_resume_flag_with_slash_command() {
|
||||
let args = vec![
|
||||
@@ -8312,6 +8392,14 @@ UU conflicted.rs",
|
||||
assert!(message.contains("/help"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_omc_slash_command_guidance_explains_runtime_gap() {
|
||||
let message = format_unknown_slash_command("oh-my-claudecode:hud");
|
||||
assert!(message.contains("Unknown slash command: /oh-my-claudecode:hud"));
|
||||
assert!(message.contains("Claude Code/OMC plugin command"));
|
||||
assert!(message.contains("does not yet load plugin slash commands"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_usage_mentions_latest_shortcut() {
|
||||
let usage = render_resume_usage();
|
||||
|
||||
@@ -104,6 +104,31 @@ fn slash_command_names_match_known_commands_and_suggest_nearby_unknown_ones() {
|
||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omc_namespaced_slash_commands_surface_a_targeted_compatibility_hint() {
|
||||
let temp_dir = unique_temp_dir("slash-dispatch-omc");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_claw"))
|
||||
.current_dir(&temp_dir)
|
||||
.arg("/oh-my-claudecode:hud")
|
||||
.output()
|
||||
.expect("claw should launch");
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"stdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8");
|
||||
assert!(stderr.contains("unknown slash command outside the REPL: /oh-my-claudecode:hud"));
|
||||
assert!(stderr.contains("Claude Code/OMC plugin command"));
|
||||
assert!(stderr.contains("does not yet load plugin slash commands"));
|
||||
|
||||
fs::remove_dir_all(temp_dir).expect("cleanup temp dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_command_loads_defaults_from_standard_config_locations() {
|
||||
// given
|
||||
|
||||
@@ -2977,6 +2977,244 @@ fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
|
||||
commands::resolve_skill_path(&cwd, skill).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[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_SYSTEM_DATE: &str = "2026-03-31";
|
||||
const DEFAULT_AGENT_MAX_ITERATIONS: usize = 32;
|
||||
@@ -5796,6 +6034,305 @@ mod tests {
|
||||
fs::remove_dir_all(root).expect("temp project should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_loads_project_local_claude_skill_prompt() {
|
||||
let _guard = env_lock().lock().expect("env lock should acquire");
|
||||
let root = temp_path("project-skills");
|
||||
let home = root.join("home");
|
||||
let workspace = root.join("workspace");
|
||||
let nested = workspace.join("nested");
|
||||
let skill_dir = workspace.join(".claude").join("skills").join("trace");
|
||||
fs::create_dir_all(&skill_dir).expect("skill dir should exist");
|
||||
fs::create_dir_all(&nested).expect("nested cwd should exist");
|
||||
fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
"---\nname: trace\ndescription: Project-local trace helper\n---\n# trace\n",
|
||||
)
|
||||
.expect("skill file should exist");
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
let original_codex_home = std::env::var("CODEX_HOME").ok();
|
||||
let original_dir = std::env::current_dir().expect("cwd");
|
||||
std::env::set_var("HOME", &home);
|
||||
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||
std::env::remove_var("CODEX_HOME");
|
||||
std::env::set_current_dir(&nested).expect("set cwd");
|
||||
|
||||
let result = execute_tool("Skill", &json!({ "skill": "trace" }))
|
||||
.expect("project-local skill should resolve");
|
||||
|
||||
let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
|
||||
assert!(output["path"]
|
||||
.as_str()
|
||||
.expect("path")
|
||||
.ends_with(".claude/skills/trace/SKILL.md"));
|
||||
assert_eq!(output["description"], "Project-local trace helper");
|
||||
|
||||
std::env::set_current_dir(&original_dir).expect("restore cwd");
|
||||
match original_home {
|
||||
Some(value) => std::env::set_var("HOME", value),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
match original_codex_home {
|
||||
Some(value) => std::env::set_var("CODEX_HOME", value),
|
||||
None => std::env::remove_var("CODEX_HOME"),
|
||||
}
|
||||
fs::remove_dir_all(root).expect("temp tree should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_loads_project_local_omc_and_agents_skill_prompts() {
|
||||
let _guard = env_lock().lock().expect("env lock should acquire");
|
||||
let root = temp_path("project-omc-skills");
|
||||
let home = root.join("home");
|
||||
let workspace = root.join("workspace");
|
||||
let nested = workspace.join("nested");
|
||||
let omc_skill_dir = workspace.join(".omc").join("skills").join("hud");
|
||||
let agents_skill_dir = workspace.join(".agents").join("skills").join("trace");
|
||||
fs::create_dir_all(&omc_skill_dir).expect("omc skill dir should exist");
|
||||
fs::create_dir_all(&agents_skill_dir).expect("agents skill dir should exist");
|
||||
fs::create_dir_all(&nested).expect("nested cwd should exist");
|
||||
fs::write(
|
||||
omc_skill_dir.join("SKILL.md"),
|
||||
"---\nname: hud\ndescription: Project-local OMC HUD helper\n---\n# hud\n",
|
||||
)
|
||||
.expect("omc skill file should exist");
|
||||
fs::write(
|
||||
agents_skill_dir.join("SKILL.md"),
|
||||
"---\nname: trace\ndescription: Project-local agents compatibility helper\n---\n# trace\n",
|
||||
)
|
||||
.expect("agents skill file should exist");
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
let original_codex_home = std::env::var("CODEX_HOME").ok();
|
||||
let original_dir = std::env::current_dir().expect("cwd");
|
||||
std::env::set_var("HOME", &home);
|
||||
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||
std::env::remove_var("CODEX_HOME");
|
||||
std::env::set_current_dir(&nested).expect("set cwd");
|
||||
|
||||
let omc_result =
|
||||
execute_tool("Skill", &json!({ "skill": "hud" })).expect("omc skill should resolve");
|
||||
let agents_result = execute_tool("Skill", &json!({ "skill": "trace" }))
|
||||
.expect("agents skill should resolve");
|
||||
|
||||
let omc_output: serde_json::Value = serde_json::from_str(&omc_result).expect("valid json");
|
||||
let agents_output: serde_json::Value =
|
||||
serde_json::from_str(&agents_result).expect("valid json");
|
||||
assert!(omc_output["path"]
|
||||
.as_str()
|
||||
.expect("path")
|
||||
.ends_with(".omc/skills/hud/SKILL.md"));
|
||||
assert_eq!(omc_output["description"], "Project-local OMC HUD helper");
|
||||
assert!(agents_output["path"]
|
||||
.as_str()
|
||||
.expect("path")
|
||||
.ends_with(".agents/skills/trace/SKILL.md"));
|
||||
assert_eq!(
|
||||
agents_output["description"],
|
||||
"Project-local agents compatibility helper"
|
||||
);
|
||||
|
||||
std::env::set_current_dir(&original_dir).expect("restore cwd");
|
||||
match original_home {
|
||||
Some(value) => std::env::set_var("HOME", value),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
match original_codex_home {
|
||||
Some(value) => std::env::set_var("CODEX_HOME", value),
|
||||
None => std::env::remove_var("CODEX_HOME"),
|
||||
}
|
||||
fs::remove_dir_all(root).expect("temp tree should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_loads_learned_skill_from_claude_config_dir() {
|
||||
let _guard = env_lock().lock().expect("env lock should acquire");
|
||||
let root = temp_path("claude-config-learned-skill");
|
||||
let home = root.join("home");
|
||||
let claude_config_dir = root.join("claude-config");
|
||||
let learned_skill_dir = claude_config_dir
|
||||
.join("skills")
|
||||
.join("omc-learned")
|
||||
.join("learned");
|
||||
fs::create_dir_all(&learned_skill_dir).expect("learned skill dir should exist");
|
||||
fs::write(
|
||||
learned_skill_dir.join("SKILL.md"),
|
||||
"---\nname: learned\ndescription: Learned OMC skill\n---\n# learned\n",
|
||||
)
|
||||
.expect("learned skill file should exist");
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
let original_codex_home = std::env::var("CODEX_HOME").ok();
|
||||
let original_claude_config_dir = std::env::var("CLAUDE_CONFIG_DIR").ok();
|
||||
std::env::set_var("HOME", &home);
|
||||
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||
std::env::remove_var("CODEX_HOME");
|
||||
std::env::set_var("CLAUDE_CONFIG_DIR", &claude_config_dir);
|
||||
|
||||
let result = execute_tool("Skill", &json!({ "skill": "learned" }))
|
||||
.expect("learned skill should resolve");
|
||||
|
||||
let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
|
||||
assert!(output["path"]
|
||||
.as_str()
|
||||
.expect("path")
|
||||
.ends_with("skills/omc-learned/learned/SKILL.md"));
|
||||
assert_eq!(output["description"], "Learned OMC skill");
|
||||
|
||||
match original_home {
|
||||
Some(value) => std::env::set_var("HOME", value),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
match original_codex_home {
|
||||
Some(value) => std::env::set_var("CODEX_HOME", value),
|
||||
None => std::env::remove_var("CODEX_HOME"),
|
||||
}
|
||||
match original_claude_config_dir {
|
||||
Some(value) => std::env::set_var("CLAUDE_CONFIG_DIR", value),
|
||||
None => std::env::remove_var("CLAUDE_CONFIG_DIR"),
|
||||
}
|
||||
fs::remove_dir_all(root).expect("temp tree should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_loads_direct_skill_and_legacy_command_from_claude_config_dir() {
|
||||
let _guard = env_lock().lock().expect("env lock should acquire");
|
||||
let root = temp_path("claude-config-direct-skill");
|
||||
let home = root.join("home");
|
||||
let claude_config_dir = root.join("claude-config");
|
||||
let skill_dir = claude_config_dir.join("skills").join("statusline");
|
||||
let command_dir = claude_config_dir.join("commands");
|
||||
fs::create_dir_all(&skill_dir).expect("direct skill dir should exist");
|
||||
fs::create_dir_all(&command_dir).expect("command dir should exist");
|
||||
fs::write(
|
||||
skill_dir.join("SKILL.md"),
|
||||
"---\nname: statusline\ndescription: Claude config skill\n---\n# statusline\n",
|
||||
)
|
||||
.expect("direct skill file should exist");
|
||||
fs::write(
|
||||
command_dir.join("doctor-check.md"),
|
||||
"---\nname: doctor-check\ndescription: Claude config command\n---\n# doctor-check\n",
|
||||
)
|
||||
.expect("direct command file should exist");
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
let original_codex_home = std::env::var("CODEX_HOME").ok();
|
||||
let original_claude_config_dir = std::env::var("CLAUDE_CONFIG_DIR").ok();
|
||||
std::env::set_var("HOME", &home);
|
||||
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||
std::env::remove_var("CODEX_HOME");
|
||||
std::env::set_var("CLAUDE_CONFIG_DIR", &claude_config_dir);
|
||||
|
||||
let direct_skill =
|
||||
execute_tool("Skill", &json!({ "skill": "statusline" })).expect("direct skill");
|
||||
let direct_skill_output: serde_json::Value =
|
||||
serde_json::from_str(&direct_skill).expect("valid skill json");
|
||||
assert!(direct_skill_output["path"]
|
||||
.as_str()
|
||||
.expect("path")
|
||||
.ends_with("skills/statusline/SKILL.md"));
|
||||
assert_eq!(direct_skill_output["description"], "Claude config skill");
|
||||
|
||||
let legacy_command =
|
||||
execute_tool("Skill", &json!({ "skill": "doctor-check" })).expect("direct command");
|
||||
let legacy_command_output: serde_json::Value =
|
||||
serde_json::from_str(&legacy_command).expect("valid command json");
|
||||
assert!(legacy_command_output["path"]
|
||||
.as_str()
|
||||
.expect("path")
|
||||
.ends_with("commands/doctor-check.md"));
|
||||
assert_eq!(
|
||||
legacy_command_output["description"],
|
||||
"Claude config command"
|
||||
);
|
||||
|
||||
match original_home {
|
||||
Some(value) => std::env::set_var("HOME", value),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
match original_codex_home {
|
||||
Some(value) => std::env::set_var("CODEX_HOME", value),
|
||||
None => std::env::remove_var("CODEX_HOME"),
|
||||
}
|
||||
match original_claude_config_dir {
|
||||
Some(value) => std::env::set_var("CLAUDE_CONFIG_DIR", value),
|
||||
None => std::env::remove_var("CLAUDE_CONFIG_DIR"),
|
||||
}
|
||||
fs::remove_dir_all(root).expect("temp tree should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_loads_project_local_legacy_command_markdown() {
|
||||
let _guard = env_lock().lock().expect("env lock should acquire");
|
||||
let root = temp_path("project-legacy-command");
|
||||
let home = root.join("home");
|
||||
let workspace = root.join("workspace");
|
||||
let nested = workspace.join("nested");
|
||||
let command_dir = workspace.join(".claude").join("commands");
|
||||
fs::create_dir_all(&command_dir).expect("legacy command dir should exist");
|
||||
fs::create_dir_all(&nested).expect("nested cwd should exist");
|
||||
fs::write(
|
||||
command_dir.join("team.md"),
|
||||
"---\nname: team\ndescription: Legacy team workflow\n---\n# team\n",
|
||||
)
|
||||
.expect("legacy command file should exist");
|
||||
|
||||
let original_home = std::env::var("HOME").ok();
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
let original_codex_home = std::env::var("CODEX_HOME").ok();
|
||||
let original_dir = std::env::current_dir().expect("cwd");
|
||||
std::env::set_var("HOME", &home);
|
||||
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||
std::env::remove_var("CODEX_HOME");
|
||||
std::env::set_current_dir(&nested).expect("set cwd");
|
||||
|
||||
let result = execute_tool("Skill", &json!({ "skill": "team" }))
|
||||
.expect("legacy command markdown should resolve");
|
||||
|
||||
let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
|
||||
assert!(output["path"]
|
||||
.as_str()
|
||||
.expect("path")
|
||||
.ends_with(".claude/commands/team.md"));
|
||||
assert_eq!(output["description"], "Legacy team workflow");
|
||||
|
||||
std::env::set_current_dir(&original_dir).expect("restore cwd");
|
||||
match original_home {
|
||||
Some(value) => std::env::set_var("HOME", value),
|
||||
None => std::env::remove_var("HOME"),
|
||||
}
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||
}
|
||||
match original_codex_home {
|
||||
Some(value) => std::env::set_var("CODEX_HOME", value),
|
||||
None => std::env::remove_var("CODEX_HOME"),
|
||||
}
|
||||
fs::remove_dir_all(root).expect("temp tree should clean up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_search_supports_keyword_and_select_queries() {
|
||||
let keyword = execute_tool(
|
||||
|
||||
Reference in New Issue
Block a user