Skip to content

Commit 7c2ff0a

Browse files
authored
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.
1 parent 7134306 commit 7c2ff0a

11 files changed

Lines changed: 119 additions & 85 deletions

File tree

cli/src/skills.rs

Lines changed: 118 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,35 @@ struct SkillInfo {
1212
dir: PathBuf,
1313
}
1414

15-
/// Locate the `skills/` directory bundled with the installation.
15+
/// Skill content is split across two directories:
16+
/// - `skills/` — the bootstrap skill (discoverable by npx skills add)
17+
/// - `skill-data/` — specialized skills (only served by the CLI)
18+
///
19+
/// Both are shipped in the npm package and searched by `discover_skills`.
20+
const SKILL_DIRS: &[&str] = &["skills", "skill-data"];
21+
22+
/// Locate the package root that contains the skill directories.
1623
///
1724
/// Resolution order:
18-
/// 1. AGENT_BROWSER_SKILLS_DIR env var
19-
/// 2. ../skills/ relative to the executable (npm installs: binary is in bin/)
25+
/// 1. AGENT_BROWSER_SKILLS_DIR env var (points directly at a single directory)
26+
/// 2. ../ relative to the executable (npm installs: binary is in bin/)
2027
/// 3. Walk up from the executable to find a project root with skills/
2128
/// (dev builds where binary is in target/debug/ or target/release/)
22-
fn find_skills_dir() -> Option<PathBuf> {
23-
if let Ok(dir) = env::var("AGENT_BROWSER_SKILLS_DIR") {
24-
let p = PathBuf::from(dir);
25-
if p.is_dir() {
26-
return Some(p);
27-
}
28-
}
29-
29+
fn find_package_root() -> Option<PathBuf> {
3030
if let Ok(exe) = env::current_exe() {
3131
let exe = exe.canonicalize().unwrap_or(exe);
3232
if let Some(parent) = exe.parent() {
33-
// npm install layout: bin/agent-browser-* -> ../skills/
34-
let candidate = parent.join("..").join("skills");
35-
if candidate.is_dir() {
36-
return Some(candidate);
33+
// npm install layout: bin/agent-browser-* -> ../
34+
let candidate = parent.join("..");
35+
if candidate.join("skills").is_dir() {
36+
return Some(candidate.canonicalize().unwrap_or(candidate));
3737
}
3838

3939
// dev build layout: walk up from target/debug/ or target/release/
4040
let mut dir = parent;
4141
loop {
42-
let candidate = dir.join("skills");
43-
if candidate.is_dir() {
44-
return Some(candidate);
42+
if dir.join("skills").is_dir() {
43+
return Some(dir.to_path_buf());
4544
}
4645
match dir.parent() {
4746
Some(p) => dir = p,
@@ -54,6 +53,27 @@ fn find_skills_dir() -> Option<PathBuf> {
5453
None
5554
}
5655

56+
/// Collect all skill directories to search, respecting the env var override.
57+
fn find_skills_dirs() -> Vec<PathBuf> {
58+
// Env var override: single directory, used as-is
59+
if let Ok(dir) = env::var("AGENT_BROWSER_SKILLS_DIR") {
60+
let p = PathBuf::from(dir);
61+
if p.is_dir() {
62+
return vec![p];
63+
}
64+
}
65+
66+
let Some(root) = find_package_root() else {
67+
return vec![];
68+
};
69+
70+
SKILL_DIRS
71+
.iter()
72+
.map(|d| root.join(d))
73+
.filter(|p| p.is_dir())
74+
.collect()
75+
}
76+
5777
/// Parse YAML frontmatter from a SKILL.md file. Returns (name, description).
5878
fn parse_frontmatter(content: &str) -> Option<(String, String)> {
5979
let content = content.trim_start();
@@ -91,33 +111,36 @@ fn parse_frontmatter(content: &str) -> Option<(String, String)> {
91111
Some((name?, description.unwrap_or_default()))
92112
}
93113

94-
/// Discover all skills in the skills directory.
95-
fn discover_skills(skills_dir: &Path) -> Vec<SkillInfo> {
114+
/// Discover all skills across the given directories.
115+
fn discover_skills(dirs: &[PathBuf]) -> Vec<SkillInfo> {
96116
let mut skills = Vec::new();
97-
let entries = match fs::read_dir(skills_dir) {
98-
Ok(e) => e,
99-
Err(_) => return skills,
100-
};
101117

102-
for entry in entries.flatten() {
103-
let path = entry.path();
104-
if !path.is_dir() {
105-
continue;
106-
}
107-
let skill_md = path.join("SKILL.md");
108-
if !skill_md.exists() {
109-
continue;
110-
}
111-
let content = match fs::read_to_string(&skill_md) {
112-
Ok(c) => c,
118+
for skills_dir in dirs {
119+
let entries = match fs::read_dir(skills_dir) {
120+
Ok(e) => e,
113121
Err(_) => continue,
114122
};
115-
if let Some((name, description)) = parse_frontmatter(&content) {
116-
skills.push(SkillInfo {
117-
name,
118-
description,
119-
dir: path,
120-
});
123+
124+
for entry in entries.flatten() {
125+
let path = entry.path();
126+
if !path.is_dir() {
127+
continue;
128+
}
129+
let skill_md = path.join("SKILL.md");
130+
if !skill_md.exists() {
131+
continue;
132+
}
133+
let content = match fs::read_to_string(&skill_md) {
134+
Ok(c) => c,
135+
Err(_) => continue,
136+
};
137+
if let Some((name, description)) = parse_frontmatter(&content) {
138+
skills.push(SkillInfo {
139+
name,
140+
description,
141+
dir: path,
142+
});
143+
}
121144
}
122145
}
123146

@@ -174,8 +197,8 @@ fn collect_supplementary_files(skill_dir: &Path) -> Vec<(String, String)> {
174197
files
175198
}
176199

177-
fn run_list(skills_dir: &Path, json_mode: bool) {
178-
let skills = discover_skills(skills_dir);
200+
fn run_list(skills_dirs: &[PathBuf], json_mode: bool) {
201+
let skills = discover_skills(skills_dirs);
179202
if skills.is_empty() {
180203
if json_mode {
181204
println!(
@@ -215,8 +238,8 @@ fn run_list(skills_dir: &Path, json_mode: bool) {
215238
}
216239
}
217240

218-
fn run_get(skills_dir: &Path, names: &[String], get_all: bool, full: bool, json_mode: bool) {
219-
let all_skills = discover_skills(skills_dir);
241+
fn run_get(skills_dirs: &[PathBuf], names: &[String], get_all: bool, full: bool, json_mode: bool) {
242+
let all_skills = discover_skills(skills_dirs);
220243

221244
let targets: Vec<&SkillInfo> = if get_all {
222245
all_skills.iter().collect()
@@ -325,10 +348,10 @@ fn run_get(skills_dir: &Path, names: &[String], get_all: bool, full: bool, json_
325348
}
326349
}
327350

328-
fn run_path(skills_dir: &Path, name: Option<&str>, json_mode: bool) {
351+
fn run_path(skills_dirs: &[PathBuf], name: Option<&str>, json_mode: bool) {
329352
match name {
330353
Some(name) => {
331-
let all_skills = discover_skills(skills_dir);
354+
let all_skills = discover_skills(skills_dirs);
332355
match all_skills.iter().find(|s| s.name == name) {
333356
Some(s) => {
334357
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) {
363386
}
364387
}
365388
None => {
366-
let path = skills_dir.to_string_lossy().to_string();
389+
let paths: Vec<String> = skills_dirs
390+
.iter()
391+
.map(|d| d.to_string_lossy().to_string())
392+
.collect();
367393
if json_mode {
368394
println!(
369395
"{}",
370396
serde_json::to_string(&json!({
371397
"success": true,
372-
"data": { "path": path },
398+
"data": { "paths": paths },
373399
}))
374400
.unwrap_or_default()
375401
);
376402
} else {
377-
println!("{}", path);
403+
for p in &paths {
404+
println!("{}", p);
405+
}
378406
}
379407
}
380408
}
381409
}
382410

383411
pub fn run_skills(args: &[String], json_mode: bool) {
384-
let skills_dir = match find_skills_dir() {
385-
Some(d) => d.canonicalize().unwrap_or(d),
386-
None => {
387-
if json_mode {
388-
println!(
389-
"{}",
390-
serde_json::to_string(&json!({
391-
"success": false,
392-
"error": "Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
393-
}))
394-
.unwrap_or_default()
395-
);
396-
} else {
397-
eprintln!(
398-
"{} Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
399-
color::error_indicator()
400-
);
401-
}
402-
exit(1);
412+
let skills_dirs = find_skills_dirs();
413+
if skills_dirs.is_empty() {
414+
if json_mode {
415+
println!(
416+
"{}",
417+
serde_json::to_string(&json!({
418+
"success": false,
419+
"error": "Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
420+
}))
421+
.unwrap_or_default()
422+
);
423+
} else {
424+
eprintln!(
425+
"{} Skills directory not found. Set AGENT_BROWSER_SKILLS_DIR or reinstall via npm.",
426+
color::error_indicator()
427+
);
403428
}
404-
};
429+
exit(1);
430+
}
405431

406432
let subcommand = args.get(1).map(|s| s.as_str());
407433

408434
match subcommand {
409-
None | Some("list") => run_list(&skills_dir, json_mode),
435+
None | Some("list") => run_list(&skills_dirs, json_mode),
410436
Some("get") => {
411437
let names: Vec<String> = args[2..]
412438
.iter()
@@ -415,11 +441,11 @@ pub fn run_skills(args: &[String], json_mode: bool) {
415441
.collect();
416442
let full = args[2..].iter().any(|a| a == "--full");
417443
let get_all = args[2..].iter().any(|a| a == "--all");
418-
run_get(&skills_dir, &names, get_all, full, json_mode);
444+
run_get(&skills_dirs, &names, get_all, full, json_mode);
419445
}
420446
Some("path") => {
421447
let name = args.get(2).map(|s| s.as_str());
422-
run_path(&skills_dir, name, json_mode);
448+
run_path(&skills_dirs, name, json_mode);
423449
}
424450
Some(unknown) => {
425451
if json_mode {
@@ -491,7 +517,7 @@ mod tests {
491517
}
492518

493519
#[test]
494-
fn test_discover_skills() {
520+
fn test_discover_skills_single_dir() {
495521
let tmp = tempfile::tempdir().unwrap();
496522
create_test_skill(tmp.path(), "alpha", "Alpha skill");
497523
create_test_skill(tmp.path(), "beta", "Beta skill");
@@ -500,12 +526,29 @@ mod tests {
500526
fs::create_dir_all(tmp.path().join("not-a-skill")).unwrap();
501527
fs::write(tmp.path().join("not-a-skill").join("README.md"), "hi").unwrap();
502528

503-
let skills = discover_skills(tmp.path());
529+
let dirs = vec![tmp.path().to_path_buf()];
530+
let skills = discover_skills(&dirs);
504531
assert_eq!(skills.len(), 2);
505532
assert_eq!(skills[0].name, "alpha");
506533
assert_eq!(skills[1].name, "beta");
507534
}
508535

536+
#[test]
537+
fn test_discover_skills_multiple_dirs() {
538+
let tmp1 = tempfile::tempdir().unwrap();
539+
let tmp2 = tempfile::tempdir().unwrap();
540+
create_test_skill(tmp1.path(), "alpha", "Alpha skill");
541+
create_test_skill(tmp2.path(), "beta", "Beta skill");
542+
create_test_skill(tmp2.path(), "gamma", "Gamma skill");
543+
544+
let dirs = vec![tmp1.path().to_path_buf(), tmp2.path().to_path_buf()];
545+
let skills = discover_skills(&dirs);
546+
assert_eq!(skills.len(), 3);
547+
assert_eq!(skills[0].name, "alpha");
548+
assert_eq!(skills[1].name, "beta");
549+
assert_eq!(skills[2].name, "gamma");
550+
}
551+
509552
#[test]
510553
fn test_truncate_description() {
511554
assert_eq!(truncate_description("short", 10), "short");

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"files": [
77
"bin",
88
"scripts",
9+
"skill-data",
910
"skills"
1011
],
1112
"bin": {
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
name: agentcore
33
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.
4-
metadata:
5-
internal: true
64
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
75
---
86

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
name: dogfood
33
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.
4-
metadata:
5-
internal: true
64
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
75
---
86

File renamed without changes.
File renamed without changes.
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
name: electron
33
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.
4-
metadata:
5-
internal: true
64
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
75
---
86

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
name: slack
33
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.
4-
metadata:
5-
internal: true
64
allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*)
75
---
86

File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)