feat: Agent Orchestrator — multi-project agent dashboard
Tauri + Svelte 5 + Rust application for orchestrating multiple AI coding agents. Includes Claude, Aider, Codex, and Ollama provider support, multi-agent communication (btmsg/bttask), session anchors, plugin sandbox, FTS5 search, Landlock sandboxing, and 507 vitest + 110 cargo tests.
This commit is contained in:
commit
3672e92b7e
272 changed files with 68600 additions and 0 deletions
158
src-tauri/src/commands/claude.rs
Normal file
158
src-tauri/src/commands/claude.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// Claude profile and skill discovery commands
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ClaudeProfile {
|
||||
pub name: String,
|
||||
pub email: Option<String>,
|
||||
pub subscription_type: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub config_dir: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct ClaudeSkill {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub source_path: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn claude_list_profiles() -> Vec<ClaudeProfile> {
|
||||
let mut profiles = Vec::new();
|
||||
|
||||
let config_dir = dirs::config_dir().unwrap_or_default();
|
||||
let profiles_dir = config_dir.join("switcher").join("profiles");
|
||||
let alt_dir_root = config_dir.join("switcher-claude");
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(&profiles_dir) {
|
||||
for entry in entries.flatten() {
|
||||
if !entry.path().is_dir() { continue; }
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let toml_path = entry.path().join("profile.toml");
|
||||
let (email, subscription_type, display_name) = if toml_path.exists() {
|
||||
let content = std::fs::read_to_string(&toml_path).unwrap_or_else(|e| {
|
||||
log::warn!("Failed to read {}: {e}", toml_path.display());
|
||||
String::new()
|
||||
});
|
||||
(
|
||||
extract_toml_value(&content, "email"),
|
||||
extract_toml_value(&content, "subscription_type"),
|
||||
extract_toml_value(&content, "display_name"),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
let alt_path = alt_dir_root.join(&name);
|
||||
let config_dir_str = if alt_path.exists() {
|
||||
alt_path.to_string_lossy().to_string()
|
||||
} else {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".claude")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
profiles.push(ClaudeProfile {
|
||||
name,
|
||||
email,
|
||||
subscription_type,
|
||||
display_name,
|
||||
config_dir: config_dir_str,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if profiles.is_empty() {
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
profiles.push(ClaudeProfile {
|
||||
name: "default".to_string(),
|
||||
email: None,
|
||||
subscription_type: None,
|
||||
display_name: None,
|
||||
config_dir: home.join(".claude").to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
profiles
|
||||
}
|
||||
|
||||
fn extract_toml_value(content: &str, key: &str) -> Option<String> {
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(rest) = trimmed.strip_prefix(key) {
|
||||
if let Some(rest) = rest.trim().strip_prefix('=') {
|
||||
let val = rest.trim().trim_matches('"');
|
||||
if !val.is_empty() {
|
||||
return Some(val.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn claude_list_skills() -> Vec<ClaudeSkill> {
|
||||
let mut skills = Vec::new();
|
||||
let home = dirs::home_dir().unwrap_or_default();
|
||||
|
||||
let skills_dir = home.join(".claude").join("skills");
|
||||
if let Ok(entries) = std::fs::read_dir(&skills_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let (name, skill_file) = if path.is_dir() {
|
||||
let skill_md = path.join("SKILL.md");
|
||||
if skill_md.exists() {
|
||||
(entry.file_name().to_string_lossy().to_string(), skill_md)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else if path.extension().map_or(false, |e| e == "md") {
|
||||
let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
(stem, path.clone())
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let description = if let Ok(content) = std::fs::read_to_string(&skill_file) {
|
||||
content.lines()
|
||||
.filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.chars()
|
||||
.take(120)
|
||||
.collect()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
skills.push(ClaudeSkill {
|
||||
name,
|
||||
description,
|
||||
source_path: skill_file.to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
skills
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn claude_read_skill(path: String) -> Result<String, String> {
|
||||
let skills_dir = dirs::home_dir()
|
||||
.ok_or("Cannot determine home directory")?
|
||||
.join(".claude")
|
||||
.join("skills");
|
||||
let canonical_skills = skills_dir.canonicalize()
|
||||
.map_err(|_| "Skills directory does not exist".to_string())?;
|
||||
let canonical_path = std::path::Path::new(&path).canonicalize()
|
||||
.map_err(|e| format!("Invalid skill path: {e}"))?;
|
||||
if !canonical_path.starts_with(&canonical_skills) {
|
||||
return Err("Access denied: path is outside skills directory".to_string());
|
||||
}
|
||||
std::fs::read_to_string(&canonical_path).map_err(|e| format!("Failed to read skill: {e}"))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue