diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..5c2ed37 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug Report +about: Report a bug to help us improve +title: '' +labels: bug +assignees: '' +--- + +## Describe the Bug + +A clear description of what the bug is. + +## Steps to Reproduce + +1. Go to '...' +2. Click on '...' +3. See error + +## Expected Behavior + +What you expected to happen. + +## Screenshots + +If applicable, add screenshots. + +## Environment + +- OS: [e.g., macOS 14.0, Windows 11, Ubuntu 22.04] +- AgentLoom Version: [e.g., 0.1.0] + +## Additional Context + +Any other context about the problem. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f407c3e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' +--- + +## Problem Statement + +A clear description of the problem you're trying to solve. + +## Proposed Solution + +Describe the solution you'd like. + +## Alternatives Considered + +Any alternative solutions or features you've considered. + +## Additional Context + +Any other context or screenshots about the feature request. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..08c8a9f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## Summary + +Brief description of changes. + +## Changes + +- Change 1 +- Change 2 + +## Testing + +- [ ] Tests pass (`cargo test`) +- [ ] Linting passes (`cargo clippy`) +- [ ] Formatting checked (`cargo fmt --check`) + +## Screenshots + +If applicable, add screenshots. + +## Related Issues + +Closes # diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4d78d0a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Install Linux dependencies + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + version: 1.0 + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run clippy + run: cargo clippy -p agentloom-core -p agentloom-cli --all-targets -- -D warnings + + - name: Run tests + run: cargo test -p agentloom-core -p agentloom-cli diff --git a/AGENTS.md b/AGENTS.md index cb6af67..bdd38c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,14 +104,24 @@ grep -r "Reflection Loop" agents/reference/ - Custom commit scripts -> Use instead of `/commit` - Custom PR scripts -> Use instead of `/commit-push-pr` -## Git Workflow +## Git Workflow (Open Source) -**Push directly to main. Do not create pull requests.** +**Read `CONTRIBUTING.md` for the full contribution guide.** -When asked to commit and push: -1. Commit changes on the current branch (main) -2. Push directly to `origin main` -3. Do NOT create feature branches or PRs +This is an open source project. Always use feature branches and pull requests: + +1. Create a feature branch: `git checkout -b feature/short-description` or `fix/short-description` +2. Make changes and commit with descriptive messages +3. Push to origin: `git push -u origin feature/short-description` +4. Open a Pull Request against `main` +5. Ensure CI checks pass before merging + +### Branch Naming Convention + +- `feature/` - New features +- `fix/` - Bug fixes +- `docs/` - Documentation updates +- `refactor/` - Code refactoring ### Installing Plugins diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e66fffe --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,29 @@ +# Code of Conduct + +## Our Pledge + +We are committed to making participation in this project a welcoming experience for everyone. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints +- Gracefully accepting constructive criticism +- Focusing on what is best for the community + +Examples of unacceptable behavior: + +- Trolling, insulting comments, or personal attacks +- Public or private harassment +- Publishing others' private information without permission +- Other conduct which could reasonably be considered inappropriate + +## Enforcement + +Instances of unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7d061b3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,77 @@ +# Contributing to AgentLoom + +Thank you for your interest in contributing to AgentLoom! + +## Getting Started + +1. Fork the repository +2. Clone your fork locally +3. Install dependencies: `npm install` +4. Build: `cargo build` +5. Run tests: `cargo test` + +## Development Workflow + +### Branch Naming + +Create a branch from `main` using this naming convention: + +- `feature/short-description` - New features +- `fix/short-description` - Bug fixes +- `docs/short-description` - Documentation updates +- `refactor/short-description` - Code refactoring + +### Making Changes + +1. Create a feature branch: `git checkout -b feature/your-feature` +2. Make your changes +3. Run tests: `cargo test` +4. Run linting: `cargo clippy` +5. Format code: `cargo fmt` +6. Commit with a descriptive message +7. Push to your fork +8. Open a Pull Request against `main` + +### Commit Messages + +Use clear, descriptive commit messages: + +- `feat: add skill validation for frontmatter` +- `fix: resolve symlink creation on Windows` +- `docs: update installation instructions` +- `refactor: simplify skill sync logic` + +### Pull Request Process + +1. Ensure all CI checks pass +2. Update documentation if needed +3. Add tests for new functionality +4. Request review from maintainers + +## Code Style + +- **Rust**: Follow standard Rust conventions, use `cargo fmt` +- **TypeScript/Svelte**: Use Prettier formatting +- **Comments**: Explain *why*, not *what* + +## Testing + +- Run all tests: `cargo test` +- Run specific crate tests: `cargo test -p agentloom-core` +- Add tests for new features and bug fixes + +## Project Structure + +``` +agent-loom/ +├── crates/ +│ ├── talent-core/ # Core library (agentloom-core) +│ └── talent-cli/ # CLI application +├── src-tauri/ # Tauri backend +├── src/ # Svelte frontend +└── agents/ # Agent specifications +``` + +## Questions? + +Open an issue for questions or discussion. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a65f16c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in AgentLoom, please report it responsibly. + +**Do NOT open a public issue for security vulnerabilities.** + +Instead, please email the maintainers directly or use GitHub's private vulnerability reporting feature. + +### What to Include + +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Suggested fix (if any) + +### Response Timeline + +- **Acknowledgment**: Within 48 hours +- **Initial Assessment**: Within 1 week +- **Resolution**: Depends on severity + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 0.x.x | Yes (current) | + +## Security Considerations + +AgentLoom manages symlinks and file system operations. Key security areas: + +- **Symlink handling**: Prevents symlink attacks and path traversal +- **File permissions**: Respects system file permissions +- **No network access**: Core functionality is entirely local + +## Best Practices for Users + +- Keep AgentLoom updated to the latest version +- Review skills before installing from untrusted sources +- Use standard OS security practices diff --git a/crates/talent-cli/src/main.rs b/crates/talent-cli/src/main.rs index 5b5d160..b56e217 100644 --- a/crates/talent-cli/src/main.rs +++ b/crates/talent-cli/src/main.rs @@ -2,11 +2,11 @@ //! //! Command-line interface for managing skills across AI CLI tools. -use clap::Parser; use agentloom_core::{ Config, ConflictResolution, ImportSelection, Importer, SkillManager, SyncResult, ValidationStatus, }; +use clap::Parser; #[derive(Parser)] #[command(name = "agentloom")] diff --git a/crates/talent-core/src/importer.rs b/crates/talent-core/src/importer.rs index b5f3e53..bb37179 100644 --- a/crates/talent-core/src/importer.rs +++ b/crates/talent-core/src/importer.rs @@ -217,9 +217,7 @@ impl Importer { let target_canonical = if target.is_absolute() { target } else { - path.parent() - .map(|p| p.join(&target)) - .unwrap_or(target) + path.parent().map(|p| p.join(&target)).unwrap_or(target) }; // Check if the target is under our skills directory @@ -289,10 +287,9 @@ impl Importer { if entry_path.file_name().is_some_and(|n| n == SKILL_FILE_NAME) { if let Some(skill_dir) = entry_path.parent() { // Skip if this is inside our own skills directory - if let (Ok(skill_abs), Ok(talent_abs)) = ( - skill_dir.canonicalize(), - self.skills_dir.canonicalize(), - ) { + if let (Ok(skill_abs), Ok(talent_abs)) = + (skill_dir.canonicalize(), self.skills_dir.canonicalize()) + { if skill_abs.starts_with(&talent_abs) { continue; } @@ -320,8 +317,7 @@ impl Importer { // Extract frontmatter for normalization check let trimmed = contents.trim_start(); - let (yaml_content, _body) = if trimmed.starts_with("---") { - let after_first = &trimmed[3..]; + let (yaml_content, _body) = if let Some(after_first) = trimmed.strip_prefix("---") { match after_first.find("\n---") { Some(end_idx) => ( after_first[..end_idx].trim().to_string(), @@ -965,7 +961,10 @@ description: A skill with non-kebab name // Skill should be copied to talent assert!(talent_skills.join("my-skill").exists()); - assert!(talent_skills.join("my-skill").join(SKILL_FILE_NAME).exists()); + assert!(talent_skills + .join("my-skill") + .join(SKILL_FILE_NAME) + .exists()); // Source should NOT be removed (unlike regular import) assert!(source_path.exists()); diff --git a/crates/talent-core/src/manager.rs b/crates/talent-core/src/manager.rs index 0621ece..45591e5 100644 --- a/crates/talent-core/src/manager.rs +++ b/crates/talent-core/src/manager.rs @@ -444,7 +444,9 @@ impl SkillManager { } // Return reference to renamed skill - find by folder_name which is now new_name - self.skills.iter().find(|s| s.folder_name() == new_name) + self.skills + .iter() + .find(|s| s.folder_name() == new_name) .ok_or_else(|| Error::SkillNotFound(new_path)) } @@ -614,11 +616,13 @@ impl SkillManager { pub fn stats(&self) -> ManagerStats { ManagerStats { total_skills: self.skills.len(), - valid_skills: self.skills + valid_skills: self + .skills .iter() .filter(|s| s.validation_status == ValidationStatus::Valid) .count(), - invalid_skills: self.skills + invalid_skills: self + .skills .iter() .filter(|s| s.validation_status == ValidationStatus::Invalid) .count(), @@ -694,7 +698,9 @@ mod tests { let config = create_test_config(&temp); let mut manager = SkillManager::with_config(config).unwrap(); - manager.create_skill("doomed-skill", "Will be deleted").unwrap(); + manager + .create_skill("doomed-skill", "Will be deleted") + .unwrap(); assert_eq!(manager.skills().len(), 1); manager.delete_skill("doomed-skill").unwrap(); @@ -766,7 +772,9 @@ mod tests { let config = create_test_config(&temp); let mut manager = SkillManager::with_config(config.clone()).unwrap(); - manager.create_skill("old-name", "A skill to rename").unwrap(); + manager + .create_skill("old-name", "A skill to rename") + .unwrap(); assert_eq!(manager.skills().len(), 1); // Rename the skill @@ -789,7 +797,9 @@ mod tests { let config = create_test_config(&temp); let mut manager = SkillManager::with_config(config.clone()).unwrap(); - manager.create_skill("original-skill", "Original description").unwrap(); + manager + .create_skill("original-skill", "Original description") + .unwrap(); // Edit content to change the name let new_content = r#"--- @@ -802,7 +812,9 @@ description: Updated description New content here. "#; - let result_name = manager.save_skill_content("original-skill", new_content).unwrap(); + let result_name = manager + .save_skill_content("original-skill", new_content) + .unwrap(); assert_eq!(result_name, "renamed-skill"); // Folder should be renamed diff --git a/crates/talent-core/src/skill.rs b/crates/talent-core/src/skill.rs index 863857f..66cdc34 100644 --- a/crates/talent-core/src/skill.rs +++ b/crates/talent-core/src/skill.rs @@ -155,7 +155,8 @@ pub fn normalize_frontmatter(yaml_content: &str, folder_name: &str) -> Normalize let map = match value.as_mapping_mut() { Some(m) => m, None => { - fixes.push("YAML root was not a mapping, replaced with minimal frontmatter".to_string()); + fixes + .push("YAML root was not a mapping, replaced with minimal frontmatter".to_string()); return NormalizeResult { yaml: format!( "name: {}\ndescription: Skill imported with invalid frontmatter", @@ -177,12 +178,18 @@ pub fn normalize_frontmatter(yaml_content: &str, folder_name: &str) -> Normalize match current_name { Some(name) if !is_valid_skill_name(&name) => { let fixed_name = to_kebab_case(&name); - fixes.push(format!("Converted name '{}' to kebab-case '{}'", name, fixed_name)); + fixes.push(format!( + "Converted name '{}' to kebab-case '{}'", + name, fixed_name + )); map.insert(name_key.clone(), serde_yaml::Value::String(fixed_name)); } None => { fixes.push(format!("Added missing name field: '{}'", folder_name)); - map.insert(name_key.clone(), serde_yaml::Value::String(folder_name.to_string())); + map.insert( + name_key.clone(), + serde_yaml::Value::String(folder_name.to_string()), + ); } _ => {} } @@ -296,11 +303,14 @@ pub struct SkillMeta { /// Pre-approved tools the skill may use (experimental, space-delimited) /// Example: "Bash(git:*) Bash(jq:*) Read" - #[serde(default, skip_serializing_if = "Option::is_none", rename = "allowed-tools")] + #[serde( + default, + skip_serializing_if = "Option::is_none", + rename = "allowed-tools" + )] pub allowed_tools: Option, // --- Legacy fields (not in spec, kept for backward compatibility) --- - /// Optional list of tags for categorization (legacy, not in spec) #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tags: Vec, @@ -502,10 +512,12 @@ impl Skill { // Find the closing --- let after_first = &trimmed[3..]; - let end_idx = after_first.find("\n---").ok_or_else(|| Error::InvalidFrontmatter { - path: path.to_path_buf(), - message: "Could not find closing frontmatter delimiter (---)".to_string(), - })?; + let end_idx = after_first + .find("\n---") + .ok_or_else(|| Error::InvalidFrontmatter { + path: path.to_path_buf(), + message: "Could not find closing frontmatter delimiter (---)".to_string(), + })?; let yaml_content = &after_first[..end_idx].trim(); let content = after_first[end_idx + 4..].trim().to_string(); @@ -973,7 +985,10 @@ metadata: // Verify the normalized YAML can be parsed let meta: SkillMeta = serde_yaml::from_str(&result.yaml).unwrap(); assert_eq!(meta.name, "test-skill"); - assert_eq!(meta.metadata.get("triggers"), Some(&"item1, item2".to_string())); + assert_eq!( + meta.metadata.get("triggers"), + Some(&"item1, item2".to_string()) + ); } #[test] @@ -984,7 +999,10 @@ metadata: assert!(result.was_modified); assert!(result.fixes.iter().any(|f| f.contains("missing name"))); - assert!(result.fixes.iter().any(|f| f.contains("missing description"))); + assert!(result + .fixes + .iter() + .any(|f| f.contains("missing description"))); let meta: SkillMeta = serde_yaml::from_str(&result.yaml).unwrap(); assert_eq!(meta.name, "my-skill"); @@ -1047,6 +1065,9 @@ Body here. // Reload and verify it's now valid let reloaded = Skill::load(&skill_dir).unwrap(); - assert_eq!(reloaded.meta.metadata.get("triggers"), Some(&"item1, item2".to_string())); + assert_eq!( + reloaded.meta.metadata.get("triggers"), + Some(&"item1, item2".to_string()) + ); } } diff --git a/crates/talent-core/src/syncer.rs b/crates/talent-core/src/syncer.rs index c686eaa..a664e15 100644 --- a/crates/talent-core/src/syncer.rs +++ b/crates/talent-core/src/syncer.rs @@ -488,7 +488,11 @@ mod tests { // The path should now be a symlink let link_path = target.skill_link_path("my-skill"); - assert!(link_path.symlink_metadata().unwrap().file_type().is_symlink()); + assert!(link_path + .symlink_metadata() + .unwrap() + .file_type() + .is_symlink()); } #[test] diff --git a/crates/talent-core/src/target.rs b/crates/talent-core/src/target.rs index 3073739..b6af9a2 100644 --- a/crates/talent-core/src/target.rs +++ b/crates/talent-core/src/target.rs @@ -305,10 +305,18 @@ pub struct TargetInfo { impl TargetInfo { /// Create TargetInfo from a Target, optionally checking sync status - pub fn from_target(target: &Target, central_skills: Option<&[String]>, skills_dir: Option<&PathBuf>) -> Self { + pub fn from_target( + target: &Target, + central_skills: Option<&[String]>, + skills_dir: Option<&PathBuf>, + ) -> Self { let sync_status = if target.enabled && target.skills_dir_exists() { central_skills.map(|skills| { - check_sync_status(target, skills, skills_dir.expect("skills_dir required when central_skills provided")) + check_sync_status( + target, + skills, + skills_dir.expect("skills_dir required when central_skills provided"), + ) }) } else { None @@ -341,7 +349,11 @@ impl From<&Target> for TargetInfo { } /// Check sync status of a target against central skills -fn check_sync_status(target: &Target, central_skill_names: &[String], skills_dir: &PathBuf) -> SyncStatus { +fn check_sync_status( + target: &Target, + central_skill_names: &[String], + skills_dir: &PathBuf, +) -> SyncStatus { use std::fs; let mut status = SyncStatus { @@ -476,7 +488,11 @@ mod tests { #[test] fn target_new_folder_creates_custom_target() { let path = PathBuf::from("/custom/folder"); - let target = Target::new_folder(path.clone(), "folder-test".to_string(), "Test Folder".to_string()); + let target = Target::new_folder( + path.clone(), + "folder-test".to_string(), + "Test Folder".to_string(), + ); assert_eq!(target.kind, None); assert_eq!(target.skills_path, path); @@ -528,10 +544,7 @@ mod tests { let result = target.validate(); assert!(result.is_err()); - assert!(matches!( - result.unwrap_err(), - Error::TargetNotFound { .. } - )); + assert!(matches!(result.unwrap_err(), Error::TargetNotFound { .. })); } #[test] diff --git a/crates/talent-core/src/validator.rs b/crates/talent-core/src/validator.rs index 5693552..c97d70d 100644 --- a/crates/talent-core/src/validator.rs +++ b/crates/talent-core/src/validator.rs @@ -116,10 +116,7 @@ impl Validator { } // Combine loading errors with validation errors - let all_errors: Vec = loading_errors - .into_iter() - .chain(errors) - .collect(); + let all_errors: Vec = loading_errors.into_iter().chain(errors).collect(); // Update skill status if all_errors.is_empty() { @@ -154,7 +151,10 @@ fn is_kebab_case(s: &str) -> bool { if !chars.first().is_some_and(|c| c.is_ascii_lowercase()) { return false; } - if !chars.last().is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) { + if !chars + .last() + .is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit()) + { return false; } @@ -260,7 +260,10 @@ mod tests { let result = validator.validate(&mut skill); assert!(result.is_err()); assert_eq!(skill.validation_status, ValidationStatus::Invalid); - assert!(skill.validation_errors.iter().any(|e| e.contains("name is required"))); + assert!(skill + .validation_errors + .iter() + .any(|e| e.contains("name is required"))); } #[test] @@ -308,7 +311,10 @@ mod tests { // Uppercase let mut skill = create_test_skill("MySkill", "Desc", "Content"); assert!(validator.validate(&mut skill).is_err()); - assert!(skill.validation_errors.iter().any(|e| e.contains("kebab-case"))); + assert!(skill + .validation_errors + .iter() + .any(|e| e.contains("kebab-case"))); // Underscores let mut skill = create_test_skill("my_skill", "Desc", "Content"); diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 973de93..8133ab8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,11 +1,11 @@ //! Tauri commands for the frontend use crate::{ - AppState, DiscoveredSkillInfo, FolderImportSelectionInfo, ImportResultInfo, ImportSelectionInfo, - ScannedSkillInfo, SkillInfo, StatsInfo, + AppState, DiscoveredSkillInfo, FolderImportSelectionInfo, ImportResultInfo, + ImportSelectionInfo, ScannedSkillInfo, SkillInfo, StatsInfo, }; -use std::path::PathBuf; use agentloom_core::{check_filemerge_available, open_filemerge, Importer, SyncResult, TargetInfo}; +use std::path::PathBuf; /// Get all skills (sorted alphabetically by name) #[tauri::command] @@ -22,12 +22,18 @@ pub fn get_targets(state: tauri::State<'_, AppState>) -> Result, let manager = state.manager.lock().map_err(|e| e.to_string())?; // Get skill names for sync status check - let skill_names: Vec = manager.skills().iter().map(|s| s.folder_name().to_string()).collect(); + let skill_names: Vec = manager + .skills() + .iter() + .map(|s| s.folder_name().to_string()) + .collect(); let skills_dir = manager.config().skills_dir.clone(); - let mut targets: Vec = manager.targets().iter().map(|t| { - TargetInfo::from_target(t, Some(&skill_names), Some(&skills_dir)) - }).collect(); + let mut targets: Vec = manager + .targets() + .iter() + .map(|t| TargetInfo::from_target(t, Some(&skill_names), Some(&skills_dir))) + .collect(); targets.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); Ok(targets) } @@ -57,7 +63,10 @@ pub fn create_skill( /// Always returns the skill info, even if validation fails. /// The validation status and errors are included in the returned SkillInfo. #[tauri::command] -pub fn validate_skill(state: tauri::State<'_, AppState>, name: String) -> Result { +pub fn validate_skill( + state: tauri::State<'_, AppState>, + name: String, +) -> Result { let mut manager = state.manager.lock().map_err(|e| e.to_string())?; // Run validation but ignore the result - we want to return the skill @@ -99,9 +108,15 @@ pub fn delete_skill(state: tauri::State<'_, AppState>, name: String) -> Result<( /// Rename a skill #[tauri::command] -pub fn rename_skill(state: tauri::State<'_, AppState>, old_name: String, new_name: String) -> Result { +pub fn rename_skill( + state: tauri::State<'_, AppState>, + old_name: String, + new_name: String, +) -> Result { let mut manager = state.manager.lock().map_err(|e| e.to_string())?; - let skill = manager.rename_skill(&old_name, &new_name).map_err(|e| e.to_string())?; + let skill = manager + .rename_skill(&old_name, &new_name) + .map_err(|e| e.to_string())?; Ok(SkillInfo::from(skill)) } @@ -121,11 +136,12 @@ pub fn get_stats(state: tauri::State<'_, AppState>) -> Result /// Get the raw content of a skill's SKILL.md file #[tauri::command] -pub fn get_skill_content(state: tauri::State<'_, AppState>, name: String) -> Result { +pub fn get_skill_content( + state: tauri::State<'_, AppState>, + name: String, +) -> Result { let manager = state.manager.lock().map_err(|e| e.to_string())?; - manager - .get_skill_content(&name) - .map_err(|e| e.to_string()) + manager.get_skill_content(&name).map_err(|e| e.to_string()) } /// Save content to a skill's SKILL.md file @@ -254,9 +270,7 @@ pub fn import_all_skills(state: tauri::State<'_, AppState>) -> Result, target_id: String) -> Result { let mut manager = state.manager.lock().map_err(|e| e.to_string())?; - manager - .toggle_target(&target_id) - .map_err(|e| e.to_string()) + manager.toggle_target(&target_id).map_err(|e| e.to_string()) } /// Set a target's enabled state @@ -320,11 +334,17 @@ pub fn remove_custom_target( /// Get available target types that can be added #[tauri::command] -pub fn get_available_target_types(state: tauri::State<'_, AppState>) -> Result, String> { +pub fn get_available_target_types( + state: tauri::State<'_, AppState>, +) -> Result, String> { use agentloom_core::TargetKind; let manager = state.manager.lock().map_err(|e| e.to_string())?; - let existing_ids: Vec<_> = manager.targets().iter().map(|t| t.id().to_string()).collect(); + let existing_ids: Vec<_> = manager + .targets() + .iter() + .map(|t| t.id().to_string()) + .collect(); // Return target types that aren't already configured let available: Vec<_> = TargetKind::all() @@ -374,7 +394,9 @@ pub fn fix_skill(state: tauri::State<'_, AppState>, name: String) -> Result) -> Result)>, String> { +pub fn fix_all_skills( + state: tauri::State<'_, AppState>, +) -> Result)>, String> { let mut manager = state.manager.lock().map_err(|e| e.to_string())?; let results = manager.fix_all_skills(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 13132bc..b0680f3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,10 +6,10 @@ mod commands; mod menu; +use agentloom_core::{ConflictResolution, SkillManager, ValidationStatus}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Mutex; -use agentloom_core::{ConflictResolution, SkillManager, ValidationStatus}; /// Skill information for the frontend /// See https://agentskills.io/specification for field definitions @@ -90,7 +90,10 @@ impl From<&agentloom_core::DiscoveredSkill> for DiscoveredSkillInfo { source_path: skill.source_path.display().to_string(), source_target: skill.source_target.display_name().to_string(), has_conflict: skill.conflict.is_some(), - existing_description: skill.conflict.as_ref().map(|c| c.existing_description.clone()), + existing_description: skill + .conflict + .as_ref() + .map(|c| c.existing_description.clone()), } } } @@ -138,7 +141,10 @@ impl From<&agentloom_core::ScannedSkill> for ScannedSkillInfo { needs_fixes: skill.needs_fixes, fixes_preview: skill.fixes_preview.clone(), has_conflict: skill.conflict.is_some(), - existing_description: skill.conflict.as_ref().map(|c| c.existing_description.clone()), + existing_description: skill + .conflict + .as_ref() + .map(|c| c.existing_description.clone()), } } } diff --git a/src-tauri/src/menu.rs b/src-tauri/src/menu.rs index bd1a680..40c7397 100644 --- a/src-tauri/src/menu.rs +++ b/src-tauri/src/menu.rs @@ -82,7 +82,10 @@ pub fn create_menu(app: &AppHandle) -> Result, tauri::Error> { file_menu.append(&refresh)?; file_menu.append(&PredefinedMenuItem::separator(app)?)?; - file_menu.append(&PredefinedMenuItem::close_window(app, Some("Close Window"))?)?; + file_menu.append(&PredefinedMenuItem::close_window( + app, + Some("Close Window"), + )?)?; menu.append(&file_menu)?;