feat(desktop): embed BoosAPI Desktop client as a subproject

Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,159 @@
use std::path::{Path, PathBuf};
use crate::types::skill::{LoadedSkill, SkillMetadata, SkillSource};
/// Parse a SKILL.md file into (metadata, markdown_body).
/// Frontmatter is delimited by `---` lines at the start of the file.
pub fn parse_skill_file(content: &str) -> Option<(SkillMetadata, String)> {
let trimmed = content.trim_start();
if !trimmed.starts_with("---") {
return None;
}
// Find the closing `---`
let after_first = &trimmed[3..];
let rest = after_first.trim_start_matches(['\r', '\n']);
let end_idx = rest.find("\n---")?;
let yaml_str = &rest[..end_idx];
let after_closing = &rest[end_idx + 4..]; // skip "\n---"
let body = after_closing.trim_start_matches(['\r', '\n']).to_string();
let metadata: SkillMetadata = serde_yaml::from_str(yaml_str).ok()?;
Some((metadata, body))
}
/// Load a single skill from a directory containing SKILL.md.
pub fn load_skill_from_dir(dir: &Path, source: SkillSource) -> Option<LoadedSkill> {
let skill_file = dir.join("SKILL.md");
let content = std::fs::read_to_string(&skill_file).ok()?;
let (metadata, body) = parse_skill_file(&content)?;
let name = metadata
.name
.clone();
let name = if name.is_empty() {
dir.file_name()?.to_string_lossy().to_string()
} else {
name
};
Some(LoadedSkill {
name,
path: skill_file,
metadata,
content: body,
source,
group: None,
})
}
/// Scan all subdirectories of `parent_dir` for SKILL.md files.
pub fn load_from_dir(parent_dir: &Path, source: SkillSource) -> Vec<LoadedSkill> {
let entries = match std::fs::read_dir(parent_dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut skills = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(skill) = load_skill_from_dir(&path, source.clone()) {
skills.push(skill);
} else {
// No SKILL.md — treat as a group container and recurse
let group_name = match path.file_name() {
Some(n) => n.to_string_lossy().to_string(),
None => continue,
};
let mut group_skills = Vec::new();
collect_skills_recursive(&path, source.clone(), &mut group_skills, 0, 3);
for mut skill in group_skills {
skill.group = Some(group_name.clone());
skills.push(skill);
}
}
}
}
skills
}
/// Recursively collect skills from nested directories up to `max_depth`.
fn collect_skills_recursive(
dir: &Path,
source: SkillSource,
skills: &mut Vec<LoadedSkill>,
depth: u32,
max_depth: u32,
) {
if depth >= max_depth {
return;
}
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(skill) = load_skill_from_dir(&path, source.clone()) {
skills.push(skill);
} else {
collect_skills_recursive(&path, source.clone(), skills, depth + 1, max_depth);
}
}
}
}
/// Load skills from all global directories in priority order.
/// Deduplicates by name — first found wins.
pub fn load_all_global(home_dir: &Path) -> Vec<LoadedSkill> {
let dirs: Vec<(PathBuf, SkillSource)> = vec![
(home_dir.join(".boosapi").join("skills"), SkillSource::BoosApi),
(home_dir.join(".claude").join("skills"), SkillSource::Claude),
(home_dir.join(".agents").join("skills"), SkillSource::Agents),
];
let mut result = Vec::new();
let mut seen = std::collections::HashSet::new();
for (dir, source) in dirs {
for skill in load_from_dir(&dir, source) {
if seen.insert(skill.name.clone()) {
result.push(skill);
}
}
}
result
}
/// Walk up from `cwd` looking for `.git/` or `.boosapi/` to detect the project root.
pub fn detect_project_root(cwd: &Path) -> Option<PathBuf> {
let mut current = cwd.to_path_buf();
loop {
if current.join(".git").is_dir() || current.join(".boosapi").is_dir() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
/// Load skills from the project's `.boosapi/skills/` directory.
pub fn load_project_skills(cwd: &Path) -> Vec<LoadedSkill> {
let root = match detect_project_root(cwd) {
Some(r) => r,
None => return Vec::new(),
};
load_from_dir(&root.join(".boosapi").join("skills"), SkillSource::Project)
}
/// Substitute template variables in skill content.
/// Replaces `$ARGUMENTS`, `${SKILL_DIR}`, and `${CLAUDE_SKILL_DIR}` (compat).
pub fn substitute_variables(content: &str, arguments: &str, skill_dir: &str) -> String {
content
.replace("$ARGUMENTS", arguments)
.replace("${SKILL_DIR}", skill_dir)
.replace("${CLAUDE_SKILL_DIR}", skill_dir)
}
@@ -0,0 +1,5 @@
pub mod loader;
pub mod registry;
pub use loader::*;
pub use registry::*;
@@ -0,0 +1,102 @@
use std::collections::{HashMap, HashSet};
use crate::types::skill::LoadedSkill;
pub struct SkillRegistry {
skills: HashMap<String, LoadedSkill>,
disabled: HashSet<String>,
}
impl SkillRegistry {
pub fn new() -> Self {
Self {
skills: HashMap::new(),
disabled: HashSet::new(),
}
}
/// Register a skill. If a skill with the same name exists, ignore (first wins).
pub fn register(&mut self, skill: LoadedSkill) {
self.skills.entry(skill.name.clone()).or_insert(skill);
}
/// Set the disabled skills set (from database).
pub fn set_disabled(&mut self, disabled: HashSet<String>) {
self.disabled = disabled;
}
/// Get all registered skills (including disabled).
pub fn all(&self) -> Vec<&LoadedSkill> {
self.skills.values().collect()
}
/// Get all enabled skills.
pub fn all_enabled(&self) -> Vec<&LoadedSkill> {
self.skills
.values()
.filter(|s| !self.disabled.contains(&s.name))
.collect()
}
/// Get a skill by name (regardless of enabled/disabled).
pub fn get(&self, name: &str) -> Option<&LoadedSkill> {
self.skills.get(name)
}
/// Get skills that the model can auto-invoke (enabled + not disable_model_invocation).
pub fn auto_invocable(&self) -> Vec<&LoadedSkill> {
self.skills
.values()
.filter(|s| !self.disabled.contains(&s.name) && !s.metadata.disable_model_invocation)
.collect()
}
/// Get skills that the user can invoke (enabled + user_invocable).
pub fn user_invocable(&self) -> Vec<&LoadedSkill> {
self.skills
.values()
.filter(|s| !self.disabled.contains(&s.name) && s.metadata.user_invocable)
.collect()
}
/// Generate a summary for system prompt injection (only enabled skills).
pub fn generate_context_summary(&self) -> String {
let skills = self.all_enabled();
if skills.is_empty() {
return String::new();
}
let mut summary = String::from(
"# Available Skills\n\nYou have access to the following skills via the Skill tool:\n\n",
);
for skill in &skills {
summary.push_str(&format!("- **{}**", skill.name));
if let Some(desc) = &skill.metadata.description {
let desc = if desc.len() > 250 {
format!("{}...", &desc[..250])
} else {
desc.clone()
};
summary.push_str(&format!(": {}", desc));
}
if let Some(when) = &skill.metadata.when_to_use {
summary.push_str(&format!(" TRIGGER when: {}", when));
}
if let Some(hint) = &skill.metadata.argument_hint {
summary.push_str(&format!(" (usage: /{})", hint));
}
summary.push('\n');
}
summary.push_str(
"\nWhen a skill matches the user's request, invoke it using the Skill tool. \
When the Skill tool returns instructions, treat them as mandatory operational \
procedures, not as optional reference material. Follow the instructions step by step.\n",
);
summary
}
}
impl Default for SkillRegistry {
fn default() -> Self {
Self::new()
}
}