Compare commits

..

2 Commits

4 changed files with 118 additions and 4 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ Cargo.lock
.venv .venv
PicoBot.code-workspace PicoBot.code-workspace
.picobot .picobot
.claude

View File

@ -72,8 +72,10 @@ fn default_skills_sources() -> Vec<String> {
vec![ vec![
"user".to_string(), "user".to_string(),
"user_agent".to_string(), "user_agent".to_string(),
"user_openclaw".to_string(),
"project".to_string(), "project".to_string(),
"project_agent".to_string(), "project_agent".to_string(),
"project_openclaw".to_string(),
] ]
} }
@ -892,8 +894,10 @@ mod tests {
vec![ vec![
"user".to_string(), "user".to_string(),
"user_agent".to_string(), "user_agent".to_string(),
"user_openclaw".to_string(),
"project".to_string(), "project".to_string(),
"project_agent".to_string(), "project_agent".to_string(),
"project_openclaw".to_string(),
] ]
); );
} }

View File

@ -28,8 +28,10 @@ pub struct Skill {
pub enum SkillSource { pub enum SkillSource {
User, User,
UserAgent, UserAgent,
UserOpenclaw,
Project, Project,
ProjectAgent, ProjectAgent,
ProjectOpenclaw,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -327,8 +329,10 @@ impl SkillSource {
match self { match self {
SkillSource::User => "user", SkillSource::User => "user",
SkillSource::UserAgent => "user_agent", SkillSource::UserAgent => "user_agent",
SkillSource::UserOpenclaw => "user_openclaw",
SkillSource::Project => "project", SkillSource::Project => "project",
SkillSource::ProjectAgent => "project_agent", SkillSource::ProjectAgent => "project_agent",
SkillSource::ProjectOpenclaw => "project_openclaw",
} }
} }
} }
@ -555,6 +559,11 @@ fn source_order(sources: &[String]) -> Vec<SkillSource> {
result.push(SkillSource::UserAgent); result.push(SkillSource::UserAgent);
} }
} }
"user_openclaw" => {
if !result.contains(&SkillSource::UserOpenclaw) {
result.push(SkillSource::UserOpenclaw);
}
}
"project" => { "project" => {
if !result.contains(&SkillSource::Project) { if !result.contains(&SkillSource::Project) {
result.push(SkillSource::Project); result.push(SkillSource::Project);
@ -565,6 +574,11 @@ fn source_order(sources: &[String]) -> Vec<SkillSource> {
result.push(SkillSource::ProjectAgent); result.push(SkillSource::ProjectAgent);
} }
} }
"project_openclaw" => {
if !result.contains(&SkillSource::ProjectOpenclaw) {
result.push(SkillSource::ProjectOpenclaw);
}
}
unknown => { unknown => {
tracing::warn!(source = %unknown, "Unknown skills source ignored"); tracing::warn!(source = %unknown, "Unknown skills source ignored");
} }
@ -575,8 +589,10 @@ fn source_order(sources: &[String]) -> Vec<SkillSource> {
vec![ vec![
SkillSource::User, SkillSource::User,
SkillSource::UserAgent, SkillSource::UserAgent,
SkillSource::UserOpenclaw,
SkillSource::Project, SkillSource::Project,
SkillSource::ProjectAgent, SkillSource::ProjectAgent,
SkillSource::ProjectOpenclaw,
] ]
} else { } else {
result result
@ -603,28 +619,45 @@ fn project_agent_skills_root(cwd: &Path) -> PathBuf {
cwd.join(".agents").join("skills") cwd.join(".agents").join("skills")
} }
fn project_openclaw_skills_root(cwd: &Path) -> PathBuf {
cwd.join(".openclaw").join("skills")
}
fn project_skill_state_path(cwd: &Path) -> PathBuf { fn project_skill_state_path(cwd: &Path) -> PathBuf {
cwd.join(".picobot").join("skill-state.json") cwd.join(".picobot").join("skill-state.json")
} }
fn home_dir() -> Option<PathBuf> {
// First check HOME environment variable (useful for testing)
std::env::var_os("HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir())
}
fn user_skills_root() -> Option<PathBuf> { fn user_skills_root() -> Option<PathBuf> {
dirs::home_dir().map(|p| p.join(".picobot").join("skills")) home_dir().map(|p| p.join(".picobot").join("skills"))
} }
fn user_skill_state_path() -> Option<PathBuf> { fn user_skill_state_path() -> Option<PathBuf> {
dirs::home_dir().map(|p| p.join(".picobot").join("skill-state.json")) home_dir().map(|p| p.join(".picobot").join("skill-state.json"))
} }
fn user_agent_skills_root() -> Option<PathBuf> { fn user_agent_skills_root() -> Option<PathBuf> {
dirs::home_dir().map(|p| p.join(".agents").join("skills")) home_dir().map(|p| p.join(".agents").join("skills"))
}
fn user_openclaw_skills_root() -> Option<PathBuf> {
home_dir().map(|p| p.join(".openclaw").join("skills"))
} }
fn source_root(source: SkillSource, cwd: &Path) -> Option<PathBuf> { fn source_root(source: SkillSource, cwd: &Path) -> Option<PathBuf> {
match source { match source {
SkillSource::User => user_skills_root(), SkillSource::User => user_skills_root(),
SkillSource::UserAgent => user_agent_skills_root(), SkillSource::UserAgent => user_agent_skills_root(),
SkillSource::UserOpenclaw => user_openclaw_skills_root(),
SkillSource::Project => Some(cwd.join(".picobot").join("skills")), SkillSource::Project => Some(cwd.join(".picobot").join("skills")),
SkillSource::ProjectAgent => Some(project_agent_skills_root(cwd)), SkillSource::ProjectAgent => Some(project_agent_skills_root(cwd)),
SkillSource::ProjectOpenclaw => Some(project_openclaw_skills_root(cwd)),
} }
} }
@ -1023,8 +1056,10 @@ mod tests {
let ordered = source_order(&[ let ordered = source_order(&[
"user".to_string(), "user".to_string(),
"user_agent".to_string(), "user_agent".to_string(),
"user_openclaw".to_string(),
"project".to_string(), "project".to_string(),
"project_agent".to_string(), "project_agent".to_string(),
"project_openclaw".to_string(),
"project".to_string(), "project".to_string(),
"unknown".to_string(), "unknown".to_string(),
]); ]);
@ -1034,8 +1069,10 @@ mod tests {
vec![ vec![
SkillSource::User, SkillSource::User,
SkillSource::UserAgent, SkillSource::UserAgent,
SkillSource::UserOpenclaw,
SkillSource::Project, SkillSource::Project,
SkillSource::ProjectAgent, SkillSource::ProjectAgent,
SkillSource::ProjectOpenclaw,
] ]
); );
} }
@ -1244,4 +1281,72 @@ mod tests {
assert!(user_enabled.disabled_in_scopes.is_empty()); assert!(user_enabled.disabled_in_scopes.is_empty());
assert!(runtime.get_skill("demo").is_some()); assert!(runtime.get_skill("demo").is_some());
} }
#[test]
fn test_discover_loads_project_openclaw_skills() {
let _lock = acquire_test_lock();
let temp_dir = tempfile::tempdir().unwrap();
let home_dir = temp_dir.path().join("home");
let project_dir = temp_dir.path().join("project");
fs::create_dir_all(&home_dir).unwrap();
fs::create_dir_all(&project_dir).unwrap();
let _home = HomeDirGuard::enter(&home_dir);
let _guard = CurrentDirGuard::enter(&project_dir);
let openclaw_skill_dir = project_dir
.join(".openclaw")
.join("skills")
.join("demo-openclaw");
fs::create_dir_all(&openclaw_skill_dir).unwrap();
fs::write(
openclaw_skill_dir.join("SKILL.md"),
"---\ndescription: openclaw skill\n---\nUse openclaw",
)
.unwrap();
let catalog = SkillCatalog::discover(&SkillsConfig {
enabled: true,
sources: vec!["project_openclaw".to_string()],
max_index_chars: 4000,
max_listed_skills: 32,
});
assert_eq!(catalog.len(), 1);
let payload = catalog.activation_event_payload("demo-openclaw").unwrap();
assert_eq!(payload["source"], "project_openclaw");
}
#[test]
fn test_discover_loads_user_openclaw_skills() {
let _lock = acquire_test_lock();
let temp_dir = tempfile::tempdir().unwrap();
let home_dir = temp_dir.path().join("home");
let project_dir = temp_dir.path().join("project");
fs::create_dir_all(&home_dir).unwrap();
fs::create_dir_all(&project_dir).unwrap();
let _home = HomeDirGuard::enter(&home_dir);
let _guard = CurrentDirGuard::enter(&project_dir);
let user_openclaw_skill_dir = home_dir
.join(".openclaw")
.join("skills")
.join("demo-user-openclaw");
fs::create_dir_all(&user_openclaw_skill_dir).unwrap();
fs::write(
user_openclaw_skill_dir.join("SKILL.md"),
"---\ndescription: user openclaw skill\n---\nUse user openclaw",
)
.unwrap();
let catalog = SkillCatalog::discover(&SkillsConfig {
enabled: true,
sources: vec!["user_openclaw".to_string()],
max_index_chars: 4000,
max_listed_skills: 32,
});
assert_eq!(catalog.len(), 1);
let payload = catalog.activation_event_payload("demo-user-openclaw").unwrap();
assert_eq!(payload["source"], "user_openclaw");
}
} }

View File

@ -32,7 +32,7 @@ impl Tool for SkillManageTool {
} }
fn description(&self) -> &str { fn description(&self) -> &str {
"Manage PicoBot skills stored under .picobot/skills or ~/.picobot/skills, while discovery also reads .agents/skills and ~/.agents/skills. Supports actions: list, get, create, update, delete, disable, reload." "Manage PicoBot skills stored under .picobot/skills or ~/.picobot/skills, while discovery also reads .agents/skills, ~/.agents/skills, .openclaw/skills, and ~/.openclaw/skills. Supports actions: list, get, create, update, delete, disable, reload."
} }
fn parameters_schema(&self) -> serde_json::Value { fn parameters_schema(&self) -> serde_json::Value {
@ -121,8 +121,10 @@ impl Tool for SkillManageTool {
"source": match skill.source { "source": match skill.source {
crate::skills::SkillSource::User => "user", crate::skills::SkillSource::User => "user",
crate::skills::SkillSource::UserAgent => "user_agent", crate::skills::SkillSource::UserAgent => "user_agent",
crate::skills::SkillSource::UserOpenclaw => "user_openclaw",
crate::skills::SkillSource::Project => "project", crate::skills::SkillSource::Project => "project",
crate::skills::SkillSource::ProjectAgent => "project_agent", crate::skills::SkillSource::ProjectAgent => "project_agent",
crate::skills::SkillSource::ProjectOpenclaw => "project_openclaw",
}, },
"path": skill.path.display().to_string(), "path": skill.path.display().to_string(),
}), }),
@ -323,8 +325,10 @@ fn list_skills_payload(skills: &Arc<SkillRuntime>) -> serde_json::Value {
"source": match skill.source { "source": match skill.source {
crate::skills::SkillSource::User => "user", crate::skills::SkillSource::User => "user",
crate::skills::SkillSource::UserAgent => "user_agent", crate::skills::SkillSource::UserAgent => "user_agent",
crate::skills::SkillSource::UserOpenclaw => "user_openclaw",
crate::skills::SkillSource::Project => "project", crate::skills::SkillSource::Project => "project",
crate::skills::SkillSource::ProjectAgent => "project_agent", crate::skills::SkillSource::ProjectAgent => "project_agent",
crate::skills::SkillSource::ProjectOpenclaw => "project_openclaw",
}, },
"path": skill.path.display().to_string(), "path": skill.path.display().to_string(),
})).collect::<Vec<_>>() })).collect::<Vec<_>>()