From 8f67cff3e1e19420748d916f8670010e18076130 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Mon, 1 Jun 2026 17:24:34 +0900 Subject: [PATCH] fix(skills): embed skill content in the binary for single-binary installs `skills get core` (the first step the agent-browser skill stub tells agents to run) failed with "Skills directory not found" on a GitHub-Release / install.sh install: only the binary is shipped, with no adjacent skills/ or skill-data/ the way an npm install bundles them, so find_package_root() returned nothing. Embed skills/ and skill-data/ into the binary via include_dir (168K) and, when no on-disk skill dirs are found, extract them once to a per-version cache dir ($CACHE/agent-browser/skills-/) and serve from there. npm/dev installs still use the on-disk dirs unchanged. Verified: from an isolated dir (no skills/ nearby), `skills list` shows all 6 skills and `skills get core` serves content. --- cli/Cargo.lock | 20 ++++++++++++++++ cli/Cargo.toml | 1 + cli/src/skills.rs | 59 ++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 46eb737..61c4e7b 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -57,6 +57,7 @@ dependencies = [ "hex", "hmac", "image", + "include_dir", "libc", "regex-lite", "reqwest", @@ -1048,6 +1049,25 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.13.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 6f59811..7b66036 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,6 +19,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" regex-lite = "0.1" dirs = "5.0" +include_dir = "0.7" base64 = "0.22" getrandom = "0.2" tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "signal", "process"] } diff --git a/cli/src/skills.rs b/cli/src/skills.rs index b342569..cc7770b 100644 --- a/cli/src/skills.rs +++ b/cli/src/skills.rs @@ -1,3 +1,4 @@ +use include_dir::{include_dir, Dir}; use serde_json::json; use std::env; use std::fs; @@ -6,6 +7,12 @@ use std::process::exit; use crate::color; +/// Skill content compiled into the binary so `skills get` works on a +/// single-binary install (GitHub Release / install.sh), where there is no +/// adjacent `skills/` or `skill-data/` on disk the way an npm install has. +static EMBEDDED_SKILLS: Dir = include_dir!("$CARGO_MANIFEST_DIR/../skills"); +static EMBEDDED_SKILL_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/../skill-data"); + struct SkillInfo { name: String, description: String, @@ -63,6 +70,29 @@ fn find_package_root() -> Option { None } +/// Extract the binary-embedded skill content to a per-version cache dir on +/// first use, returning a package root that contains `skills/` and +/// `skill-data/`. Fallback for single-binary installs (GitHub Release / +/// install.sh) that have no on-disk skill directories. Version-stamped so an +/// upgraded binary re-extracts fresh content. +fn embedded_skills_root() -> Option { + let base = dirs::cache_dir()? + .join("agent-browser") + .join(concat!("skills-", env!("CARGO_PKG_VERSION"))); + let marker = base.join(".extracted"); + if !marker.exists() { + let _ = fs::create_dir_all(base.join("skills")); + let _ = fs::create_dir_all(base.join("skill-data")); + if EMBEDDED_SKILLS.extract(base.join("skills")).is_err() + || EMBEDDED_SKILL_DATA.extract(base.join("skill-data")).is_err() + { + return None; + } + let _ = fs::write(&marker, env!("CARGO_PKG_VERSION")); + } + base.join("skills").is_dir().then_some(base) +} + /// Collect all skill directories to search, respecting the env var override. fn find_skills_dirs() -> Vec { // Env var override: single directory, used as-is @@ -73,15 +103,28 @@ fn find_skills_dirs() -> Vec { } } - let Some(root) = find_package_root() else { - return vec![]; - }; + // On-disk package root (npm install layout, or dev build walking up to repo). + if let Some(root) = find_package_root() { + let dirs: Vec = SKILL_DIRS + .iter() + .map(|d| root.join(d)) + .filter(|p| p.is_dir()) + .collect(); + if !dirs.is_empty() { + return dirs; + } + } - SKILL_DIRS - .iter() - .map(|d| root.join(d)) - .filter(|p| p.is_dir()) - .collect() + // Fallback: skill content compiled into the binary (single-binary install). + if let Some(root) = embedded_skills_root() { + return SKILL_DIRS + .iter() + .map(|d| root.join(d)) + .filter(|p| p.is_dir()) + .collect(); + } + + vec![] } /// Parse YAML frontmatter from a SKILL.md file. Returns (name, description, hidden).