Move specialized skills to skill-data/ so npx skills add only finds one (#1227)
The skills CLI metadata.internal flag was never implemented (PRs #587 and #652 were both closed). All 6 skills were showing in the installer. Move the 5 specialized skills (dogfood, electron, slack, vercel-sandbox, agentcore) from skills/ to skill-data/, which the skills CLI does not search. The bootstrap skill stays in skills/ for discovery. The Rust CLI searches both directories so agent-browser skills list/get still serves all 6.
This commit is contained in:
+118
-75
@@ -12,36 +12,35 @@ struct SkillInfo {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Locate the `skills/` directory bundled with the installation.
|
||||
/// Skill content is split across two directories:
|
||||
/// - `skills/` — the bootstrap skill (discoverable by npx skills add)
|
||||
/// - `skill-data/` — specialized skills (only served by the CLI)
|
||||
///
|
||||
/// Both are shipped in the npm package and searched by `discover_skills`.
|
||||
const SKILL_DIRS: &[&str] = &["skills", "skill-data"];
|
||||
|
||||
/// Locate the package root that contains the skill directories.
|
||||
///
|
||||
/// Resolution order:
|
||||
/// 1. AGENT_BROWSER_SKILLS_DIR env var
|
||||
/// 2. ../skills/ relative to the executable (npm installs: binary is in bin/)
|
||||
/// 1. AGENT_BROWSER_SKILLS_DIR env var (points directly at a single directory)
|
||||
/// 2. ../ relative to the executable (npm installs: binary is in bin/)
|
||||
/// 3. Walk up from the executable to find a project root with skills/
|
||||
/// (dev builds where binary is in target/debug/ or target/release/)
|
||||
fn find_skills_dir() -> Option<PathBuf> {
|
||||
if let Ok(dir) = env::var("AGENT_BROWSER_SKILLS_DIR") {
|
||||
let p = PathBuf::from(dir);
|
||||
if p.is_dir() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_package_root() -> Option<PathBuf> {
|
||||
if let Ok(exe) = env::current_exe() {
|
||||
let exe = exe.canonicalize().unwrap_or(exe);
|
||||
if let Some(parent) = exe.parent() {
|
||||
// npm install layout: bin/agent-browser-* -> ../skills/
|
||||
let candidate = parent.join("..").join("skills");
|
||||
if candidate.is_dir() {
|
||||
return Some(candidate);
|
||||
// npm install layout: bin/agent-browser-* -> ../
|
||||
let candidate = parent.join("..");
|
||||
if candidate.join("skills").is_dir() {
|
||||
return Some(candidate.canonicalize().unwrap_or(candidate));
|
||||
}
|
||||
|
||||
// dev build layout: walk up from target/debug/ or target/release/
|
||||
let mut dir = parent;
|
||||
loop {
|
||||
let candidate = dir.join("skills");
|
||||
if candidate.is_dir() {
|
||||
return Some(candidate);
|
||||
if dir.join("skills").is_dir() {
|
||||
return Some(dir.to_path_buf());
|
||||
}
|
||||
match dir.parent() {
|
||||
Some(p) => dir = p,
|
||||
@@ -54,6 +53,27 @@ fn find_skills_dir() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Collect all skill directories to search, respecting the env var override.
|
||||
fn find_skills_dirs() -> Vec<PathBuf> {
|
||||
// Env var override: single directory, used as-is
|
||||
if let Ok(dir) = env::var("AGENT_BROWSER_SKILLS_DIR") {
|
||||
let p = PathBuf::from(dir);
|
||||
if p.is_dir() {
|
||||
return vec![p];
|
||||
}
|
||||
}
|
||||
|
||||
let Some(root) = find_package_root() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
SKILL_DIRS
|
||||
.iter()
|
||||
.map(|d| root.join(d))
|
||||
.filter(|p| p.is_dir())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse YAML frontmatter from a SKILL.md file. Returns (name, description).
|
||||
fn parse_frontmatter(content: &str) -> Option<(String, String)> {
|
||||
let content = content.trim_start();
|
||||
@@ -91,33 +111,36 @@ fn parse_frontmatter(content: &str) -> Option<(String, String)> {
|
||||
Some((name?, description.unwrap_or_default()))
|
||||
}
|
||||
|
||||
/// Discover all skills in the skills directory.
|
||||
fn discover_skills(skills_dir: &Path) -> Vec<SkillInfo> {
|
||||
/// Discover all skills across the given directories.
|
||||
fn discover_skills(dirs: &[PathBuf]) -> Vec<SkillInfo> {
|
||||
let mut skills = Vec::new();
|
||||
let entries = match fs::read_dir(skills_dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return skills,
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let skill_md = path.join("SKILL.md");
|
||||
if !skill_md.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = match fs::read_to_string(&skill_md) {
|
||||
Ok(c) => c,
|
||||
for skills_dir in dirs {
|
||||
let entries = match fs::read_dir(skills_dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Some((name, description)) = parse_frontmatter(&content) {
|
||||
skills.push(SkillInfo {
|
||||
name,
|
||||
description,
|
||||
dir: path,
|
||||
});
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let skill_md = path.join("SKILL.md");
|
||||
if !skill_md.exists() {
|
||||
continue;
|
||||
}
|
||||
let content = match fs::read_to_string(&skill_md) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Some((name, description)) = parse_frontmatter(&content) {
|
||||
skills.push(SkillInfo {
|
||||
name,
|
||||
description,
|
||||
dir: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +197,8 @@ fn collect_supplementary_files(skill_dir: &Path) -> Vec<(String, String)> {
|
||||
files
|
||||
}
|
||||
|
||||
fn run_list(skills_dir: &Path, json_mode: bool) {
|
||||
let skills = discover_skills(skills_dir);
|
||||
fn run_list(skills_dirs: &[PathBuf], json_mode: bool) {
|
||||
let skills = discover_skills(skills_dirs);
|
||||
if skills.is_empty() {
|
||||
if json_mode {
|
||||
println!(
|
||||
@@ -215,8 +238,8 @@ fn run_list(skills_dir: &Path, json_mode: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_get(skills_dir: &Path, names: &[String], get_all: bool, full: bool, json_mode: bool) {
|
||||
let all_skills = discover_skills(skills_dir);
|
||||
fn run_get(skills_dirs: &[PathBuf], names: &[String], get_all: bool, full: bool, json_mode: bool) {
|
||||
let all_skills = discover_skills(skills_dirs);
|
||||
|
||||
let targets: Vec<&SkillInfo> = if get_all {
|
||||
all_skills.iter().collect()
|
||||
@@ -325,10 +348,10 @@ fn run_get(skills_dir: &Path, names: &[String], get_all: bool, full: bool, json_
|
||||
}
|
||||
}
|
||||
|
||||
fn run_path(skills_dir: &Path, name: Option<&str>, json_mode: bool) {
|
||||
fn run_path(skills_dirs: &[PathBuf], name: Option<&str>, json_mode: bool) {
|
||||
match name {
|
||||
Some(name) => {
|
||||
let all_skills = discover_skills(skills_dir);
|
||||
let all_skills = discover_skills(skills_dirs);
|
||||
match all_skills.iter().find(|s| s.name == name) {
|
||||
Some(s) => {
|
||||
let path = s.dir.to_string_lossy().to_string();
|
||||
@@ -363,50 +386,53 @@ fn run_path(skills_dir: &Path, name: Option<&str>, json_mode: bool) {
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let path = skills_dir.to_string_lossy().to_string();
|
||||
let paths: Vec<String> = skills_dirs
|
||||
.iter()
|
||||
.map(|d| d.to_string_lossy().to_string())
|
||||
.collect();
|
||||
if json_mode {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&json!({
|
||||
"success": true,
|
||||
"data": { "path": path },
|
||||
"data": { "paths": paths },
|
||||
}))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
} else {
|
||||
println!("{}", path);
|
||||
for p in &paths {
|
||||
println!("{}", p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_skills(args: &[String], json_mode: bool) {
|
||||
let skills_dir = match find_skills_dir() {
|
||||
Some(d) => d.canonicalize().unwrap_or(d),
|
||||
None => {
|
||||
if json_mode {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&json!({
|
||||
"success": false,
|
||||
"error": "Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
|
||||
}))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
|
||||
color::error_indicator()
|
||||
);
|
||||
}
|
||||
exit(1);
|
||||
let skills_dirs = find_skills_dirs();
|
||||
if skills_dirs.is_empty() {
|
||||
if json_mode {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&json!({
|
||||
"success": false,
|
||||
"error": "Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
|
||||
}))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
|
||||
color::error_indicator()
|
||||
);
|
||||
}
|
||||
};
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let subcommand = args.get(1).map(|s| s.as_str());
|
||||
|
||||
match subcommand {
|
||||
None | Some("list") => run_list(&skills_dir, json_mode),
|
||||
None | Some("list") => run_list(&skills_dirs, json_mode),
|
||||
Some("get") => {
|
||||
let names: Vec<String> = args[2..]
|
||||
.iter()
|
||||
@@ -415,11 +441,11 @@ pub fn run_skills(args: &[String], json_mode: bool) {
|
||||
.collect();
|
||||
let full = args[2..].iter().any(|a| a == "--full");
|
||||
let get_all = args[2..].iter().any(|a| a == "--all");
|
||||
run_get(&skills_dir, &names, get_all, full, json_mode);
|
||||
run_get(&skills_dirs, &names, get_all, full, json_mode);
|
||||
}
|
||||
Some("path") => {
|
||||
let name = args.get(2).map(|s| s.as_str());
|
||||
run_path(&skills_dir, name, json_mode);
|
||||
run_path(&skills_dirs, name, json_mode);
|
||||
}
|
||||
Some(unknown) => {
|
||||
if json_mode {
|
||||
@@ -491,7 +517,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discover_skills() {
|
||||
fn test_discover_skills_single_dir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
create_test_skill(tmp.path(), "alpha", "Alpha skill");
|
||||
create_test_skill(tmp.path(), "beta", "Beta skill");
|
||||
@@ -500,12 +526,29 @@ mod tests {
|
||||
fs::create_dir_all(tmp.path().join("not-a-skill")).unwrap();
|
||||
fs::write(tmp.path().join("not-a-skill").join("README.md"), "hi").unwrap();
|
||||
|
||||
let skills = discover_skills(tmp.path());
|
||||
let dirs = vec![tmp.path().to_path_buf()];
|
||||
let skills = discover_skills(&dirs);
|
||||
assert_eq!(skills.len(), 2);
|
||||
assert_eq!(skills[0].name, "alpha");
|
||||
assert_eq!(skills[1].name, "beta");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discover_skills_multiple_dirs() {
|
||||
let tmp1 = tempfile::tempdir().unwrap();
|
||||
let tmp2 = tempfile::tempdir().unwrap();
|
||||
create_test_skill(tmp1.path(), "alpha", "Alpha skill");
|
||||
create_test_skill(tmp2.path(), "beta", "Beta skill");
|
||||
create_test_skill(tmp2.path(), "gamma", "Gamma skill");
|
||||
|
||||
let dirs = vec![tmp1.path().to_path_buf(), tmp2.path().to_path_buf()];
|
||||
let skills = discover_skills(&dirs);
|
||||
assert_eq!(skills.len(), 3);
|
||||
assert_eq!(skills[0].name, "alpha");
|
||||
assert_eq!(skills[1].name, "beta");
|
||||
assert_eq!(skills[2].name, "gamma");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_description() {
|
||||
assert_eq!(truncate_description("short", 10), "short");
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"files": [
|
||||
"bin",
|
||||
"scripts",
|
||||
"skill-data",
|
||||
"skills"
|
||||
],
|
||||
"bin": {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: agentcore
|
||||
description: Run agent-browser on AWS Bedrock AgentCore cloud browsers. Use when the user wants to use AgentCore, run browser automation on AWS, use a cloud browser with AWS credentials, or needs a managed browser session backed by AWS infrastructure. Triggers include "use agentcore", "run on AWS", "cloud browser with AWS", "bedrock browser", "agentcore session", or any task requiring AWS-hosted browser automation.
|
||||
metadata:
|
||||
internal: true
|
||||
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
|
||||
---
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: dogfood
|
||||
description: Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to "dogfood", "QA", "exploratory test", "find issues", "bug hunt", "test this app/site/platform", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.
|
||||
metadata:
|
||||
internal: true
|
||||
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
|
||||
---
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: electron
|
||||
description: Automate Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) using agent-browser via Chrome DevTools Protocol. Use when the user needs to interact with an Electron app, automate a desktop app, connect to a running app, control a native app, or test an Electron application. Triggers include "automate Slack app", "control VS Code", "interact with Discord app", "test this Electron app", "connect to desktop app", or any task requiring automation of a native Electron application.
|
||||
metadata:
|
||||
internal: true
|
||||
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
|
||||
---
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: slack
|
||||
description: Interact with Slack workspaces using browser automation. Use when the user needs to check unread channels, navigate Slack, send messages, extract data, find information, search conversations, or automate any Slack task. Triggers include "check my Slack", "what channels have unreads", "send a message to", "search Slack for", "extract from Slack", "find who said", or any task requiring programmatic Slack interaction.
|
||||
metadata:
|
||||
internal: true
|
||||
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
|
||||
---
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: vercel-sandbox
|
||||
description: Run agent-browser + Chrome inside Vercel Sandbox microVMs for browser automation from any Vercel-deployed app. Use when the user needs browser automation in a Vercel app (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.), wants to run headless Chrome without binary size limits, needs persistent browser sessions across commands, or wants ephemeral isolated browser environments. Triggers include "Vercel Sandbox browser", "microVM Chrome", "agent-browser in sandbox", "browser automation on Vercel", or any task requiring Chrome in a Vercel Sandbox.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Browser Automation with Vercel Sandbox
|
||||
Reference in New Issue
Block a user