feat(agents): Add GitHub agent import feature

- Implemented GitHub agent browser in CCAgents component
- Created GitHubAgentBrowser component for browsing and importing agents
- Added Rust commands for fetching and importing agents from GitHub
- Updated API layer with GitHub agent import methods
- Updated Cargo.toml with new dependencies
- Fixed Tauri configuration and capabilities
- Added dropdown menu for import options
- Implemented search and preview functionality for GitHub agents
This commit is contained in:
Vivek R
2025-06-24 00:00:18 +05:30
parent 5a29f9ae01
commit c85caa7a47
12 changed files with 1508 additions and 658 deletions

1263
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,48 +8,52 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "claudia_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
crate-type = ["lib", "cdylib", "staticlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2"
tauri = { version = "2", features = ["protocol-asset", "tray-icon", "image-png"] }
tauri-plugin-shell = "2"
tauri-plugin-dialog = "2.0.3"
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-process = "2"
tauri-plugin-updater = "2"
tauri-plugin-notification = "2"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-global-shortcut = "2"
tauri-plugin-http = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
dirs = "5.0"
walkdir = "2"
rusqlite = { version = "0.32", features = ["bundled"] }
dirs = "5"
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
log = "0.4"
env_logger = "0.11"
rusqlite = { version = "0.32", features = ["bundled", "chrono"] }
regex = "1"
glob = "0.3"
base64 = "0.22"
gaol = "0.2"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.11", features = ["v4", "serde"] }
libc = "0.2"
reqwest = { version = "0.12", features = ["json"] }
futures = "0.3"
async-trait = "0.1"
tempfile = "3"
which = "7"
headless_chrome = { version = "1.0", features = ["fetch"] }
sha2 = "0.10"
zstd = "0.13"
headless_chrome = { version = "1.0", features = ["fetch"] }
reqwest = { version = "0.12.20", features = ["json"] }
image = "0.25.6"
uuid = { version = "1.6", features = ["v4", "serde"] }
walkdir = "2"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.26"
objc = "0.2"
[dev-dependencies]
# Testing utilities
tempfile = "3"
serial_test = "3" # For tests that need to run serially
test-case = "3" # For parameterized tests
once_cell = "1" # For test fixture initialization
proptest = "1" # For property-based testing
pretty_assertions = "1" # Better assertion output
parking_lot = "0.12" # Non-poisoning mutex for tests
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

View File

@@ -5,11 +5,30 @@
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
"shell:allow-execute",
"shell:allow-spawn",
"shell:allow-open"
"shell:allow-open",
"fs:default",
"fs:allow-mkdir",
"fs:allow-read",
"fs:allow-write",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-copy-file",
"fs:read-all",
"fs:write-all",
"fs:scope-app-recursive",
"fs:scope-home-recursive",
"http:default",
"http:allow-fetch",
"process:default",
"notification:default",
"clipboard-manager:default",
"global-shortcut:default",
"updater:default"
]
}

View File

@@ -11,6 +11,7 @@ use std::sync::Mutex;
use tauri::{AppHandle, Manager, State, Emitter};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use reqwest;
/// Finds the full path to the claude binary
/// This is necessary because macOS apps have a limited PATH environment
@@ -1931,3 +1932,130 @@ pub async fn import_agent_from_file(db: State<'_, AgentDb>, file_path: String) -
// Import the agent
import_agent(db, json_data).await
}
// GitHub Agent Import functionality
/// Represents a GitHub agent file from the API
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GitHubAgentFile {
pub name: String,
pub path: String,
pub download_url: String,
pub size: i64,
pub sha: String,
}
/// Represents the GitHub API response for directory contents
#[derive(Debug, Deserialize)]
struct GitHubApiResponse {
name: String,
path: String,
sha: String,
size: i64,
url: String,
html_url: String,
git_url: String,
download_url: Option<String>,
#[serde(rename = "type")]
file_type: String,
}
/// Fetch list of agents from GitHub repository
#[tauri::command]
pub async fn fetch_github_agents() -> Result<Vec<GitHubAgentFile>, String> {
info!("Fetching agents from GitHub repository...");
let client = reqwest::Client::new();
let url = "https://api.github.com/repos/getAsterisk/claudia/contents/cc_agents";
let response = client
.get(url)
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "Claudia-App")
.send()
.await
.map_err(|e| format!("Failed to fetch from GitHub: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(format!("GitHub API error ({}): {}", status, error_text));
}
let api_files: Vec<GitHubApiResponse> = response
.json()
.await
.map_err(|e| format!("Failed to parse GitHub response: {}", e))?;
// Filter only .claudia.json files
let agent_files: Vec<GitHubAgentFile> = api_files
.into_iter()
.filter(|f| f.name.ends_with(".claudia.json") && f.file_type == "file")
.filter_map(|f| {
f.download_url.map(|download_url| GitHubAgentFile {
name: f.name,
path: f.path,
download_url,
size: f.size,
sha: f.sha,
})
})
.collect();
info!("Found {} agents on GitHub", agent_files.len());
Ok(agent_files)
}
/// Fetch and preview a specific agent from GitHub
#[tauri::command]
pub async fn fetch_github_agent_content(download_url: String) -> Result<AgentExport, String> {
info!("Fetching agent content from: {}", download_url);
let client = reqwest::Client::new();
let response = client
.get(&download_url)
.header("Accept", "application/json")
.header("User-Agent", "Claudia-App")
.send()
.await
.map_err(|e| format!("Failed to download agent: {}", e))?;
if !response.status().is_success() {
return Err(format!("Failed to download agent: HTTP {}", response.status()));
}
let json_text = response
.text()
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
// Parse and validate the agent data
let export_data: AgentExport = serde_json::from_str(&json_text)
.map_err(|e| format!("Invalid agent JSON format: {}", e))?;
// Validate version
if export_data.version != 1 {
return Err(format!("Unsupported agent version: {}", export_data.version));
}
Ok(export_data)
}
/// Import an agent directly from GitHub
#[tauri::command]
pub async fn import_agent_from_github(
db: State<'_, AgentDb>,
download_url: String,
) -> Result<Agent, String> {
info!("Importing agent from GitHub: {}", download_url);
// First, fetch the agent content
let export_data = fetch_github_agent_content(download_url).await?;
// Convert to JSON string and use existing import logic
let json_data = serde_json::to_string(&export_data)
.map_err(|e| format!("Failed to serialize agent data: {}", e))?;
// Import using existing function
import_agent(db, json_data).await
}

View File

@@ -10,7 +10,6 @@ pub mod claude_binary;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -28,7 +28,8 @@ use commands::agents::{
get_session_status, cleanup_finished_processes, get_session_output,
get_live_session_output, stream_session_output, get_claude_binary_path,
set_claude_binary_path, export_agent, export_agent_to_file, import_agent,
import_agent_from_file, AgentDb
import_agent_from_file, fetch_github_agents, fetch_github_agent_content,
import_agent_from_github, AgentDb
};
use commands::sandbox::{
list_sandbox_profiles, create_sandbox_profile, update_sandbox_profile, delete_sandbox_profile,
@@ -66,7 +67,6 @@ fn main() {
}
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
// Initialize agents database
@@ -143,6 +143,9 @@ fn main() {
export_agent_to_file,
import_agent,
import_agent_from_file,
fetch_github_agents,
fetch_github_agent_content,
import_agent_from_github,
execute_agent,
list_agent_runs,
get_agent_run,