From 8a3298d76758f0a38efa4d82c0d71d2b7e248a1e Mon Sep 17 00:00:00 2001 From: Daniel Franklin Date: Wed, 13 May 2026 19:16:02 -0400 Subject: [PATCH 1/3] fix(sharing): improve skills install and bundle export handling --- crates/sharing/src/lib.rs | 183 ++++++++++-- crates/web/frontend/src/views/SkillsView.vue | 12 +- crates/web/src/api.rs | 47 ++- docs/a7bundle-v2.md | 294 +++++++++++++++++++ docs/registry.json | 152 +++++----- 5 files changed, 594 insertions(+), 94 deletions(-) create mode 100644 docs/a7bundle-v2.md diff --git a/crates/sharing/src/lib.rs b/crates/sharing/src/lib.rs index 2e94a4a..6035fb7 100644 --- a/crates/sharing/src/lib.rs +++ b/crates/sharing/src/lib.rs @@ -30,6 +30,17 @@ impl BundleAsset { } } +fn read_bundle_text_asset(path: &Path) -> Result { + let bytes = + std::fs::read(path).with_context(|| format!("read bundle asset '{}'", path.display()))?; + String::from_utf8(bytes).map_err(|_| { + anyhow::anyhow!( + ".a7bundle v1 only supports text assets; binary or non-UTF-8 file detected: {}", + path.display() + ) + }) +} + /// A portable bundle of skills and/or workflows. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Bundle { @@ -185,7 +196,7 @@ impl BundleBuilder { if path.extension().and_then(|e| e.to_str()) != Some("md") { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; let stem = path .file_stem() .and_then(|s| s.to_str()) @@ -215,7 +226,7 @@ impl BundleBuilder { if !manifest.is_file() { continue; } - let manifest_content = std::fs::read_to_string(&manifest)?; + let manifest_content = read_bundle_text_asset(&manifest)?; let package_name = path .file_name() .and_then(|s| s.to_str()) @@ -274,7 +285,7 @@ impl BundleBuilder { if rel.is_empty() { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; if seen.insert(rel.clone()) { assets.push(BundleAsset::new(rel, content)); } @@ -409,7 +420,7 @@ impl BundleBuilder { if !package_file.is_file() { continue; } - let package_content = std::fs::read_to_string(&package_file)?; + let package_content = read_bundle_text_asset(&package_file)?; seen.insert(rel_file.clone()); assets.push(BundleAsset::new(rel_file, package_content)); } @@ -432,9 +443,15 @@ impl BundleBuilder { consumed = true; break; } - let content = std::fs::read_to_string(&flat_file)?; + let content = read_bundle_text_asset(&flat_file)?; seen.insert(rel_file.clone()); assets.push(BundleAsset::new(rel_file, content)); + maybe_collect_flat_tool_dependency_dir( + &flat_file, + tools_dir, + &mut assets, + &mut seen, + )?; consumed = true; break; } @@ -457,9 +474,15 @@ impl BundleBuilder { consumed = true; break 'ext_search; } - let content = std::fs::read_to_string(&candidate)?; + let content = read_bundle_text_asset(&candidate)?; seen.insert(rel_file.clone()); assets.push(BundleAsset::new(rel_file, content)); + maybe_collect_flat_tool_dependency_dir( + &candidate, + tools_dir, + &mut assets, + &mut seen, + )?; consumed = true; break 'ext_search; } @@ -478,7 +501,7 @@ impl BundleBuilder { if seen.contains(&rel_file) { break; } - let content = std::fs::read_to_string(&flat_script)?; + let content = read_bundle_text_asset(&flat_script)?; seen.insert(rel_file.clone()); assets.push(BundleAsset::new(rel_file, content)); break; @@ -497,7 +520,7 @@ impl BundleBuilder { if !path.is_file() { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; seen.insert(rel_norm.clone()); assets.push(BundleAsset::new(rel_norm.clone(), content)); break; @@ -513,9 +536,15 @@ impl BundleBuilder { if !path.is_file() { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; seen.insert(rel_norm.clone()); assets.push(BundleAsset::new(rel_norm.clone(), content)); + maybe_collect_flat_tool_dependency_dir( + &path, + tools_dir, + &mut assets, + &mut seen, + )?; // If this file is inside a manifest-based tool package (tools//...), // include sibling package files for dependency closure. if let Some(package_name) = rel_norm.split('/').next() { @@ -536,7 +565,7 @@ impl BundleBuilder { if !package_file.is_file() { continue; } - let package_content = std::fs::read_to_string(&package_file)?; + let package_content = read_bundle_text_asset(&package_file)?; seen.insert(rel_file.clone()); assets.push(BundleAsset::new(rel_file, package_content)); } @@ -588,7 +617,7 @@ impl BundleBuilder { if !package_file.is_file() { continue; } - let content = std::fs::read_to_string(&package_file)?; + let content = read_bundle_text_asset(&package_file)?; seen.insert(rel_file.clone()); assets.push(BundleAsset::new(rel_file, content)); } @@ -602,8 +631,14 @@ impl BundleBuilder { if rel.is_empty() || !seen.insert(rel.clone()) { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; assets.push(BundleAsset::new(rel, content)); + maybe_collect_flat_tool_dependency_dir( + &path, + tools_dir, + &mut assets, + &mut seen, + )?; } } } @@ -624,7 +659,7 @@ impl BundleBuilder { if !path.is_file() { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; assets.push(BundleAsset::new(rel_norm, content)); } } @@ -694,7 +729,7 @@ impl BundleBuilder { if filename.is_empty() || !seen.insert(filename.clone()) { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; assets.push(BundleAsset::new(filename, content)); } } @@ -715,7 +750,7 @@ impl BundleBuilder { if path.extension().and_then(|e| e.to_str()) != Some("md") { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; let Some(trigger) = parse_frontmatter_value(&content, "trigger") else { continue; }; @@ -735,7 +770,7 @@ impl BundleBuilder { if !manifest.is_file() { continue; } - let manifest_content = std::fs::read_to_string(&manifest)?; + let manifest_content = read_bundle_text_asset(&manifest)?; let Some(trigger) = parse_frontmatter_value(&manifest_content, "trigger") else { continue; }; @@ -932,7 +967,7 @@ fn collect_files_recursive( if rel.is_empty() || !seen.insert(rel.clone()) { continue; } - let content = std::fs::read_to_string(&path)?; + let content = read_bundle_text_asset(&path)?; out.push(BundleAsset::new(rel, content)); } Ok(()) @@ -962,6 +997,48 @@ fn collect_files_recursive_paths( Ok(()) } +fn collect_flat_tool_sibling_dir_assets( + sibling_dir: &Path, + tools_dir: &Path, + assets: &mut Vec, + seen: &mut HashSet, +) -> Result<()> { + if !sibling_dir.is_dir() { + return Ok(()); + } + let mut sibling_files = Vec::new(); + collect_files_recursive_paths(sibling_dir, tools_dir, &mut sibling_files)?; + for rel_file in sibling_files { + if seen.contains(&rel_file) { + continue; + } + let sibling_file = tools_dir.join(&rel_file); + if !sibling_file.is_file() { + continue; + } + let sibling_content = read_bundle_text_asset(&sibling_file)?; + seen.insert(rel_file.clone()); + assets.push(BundleAsset::new(rel_file, sibling_content)); + } + Ok(()) +} + +fn maybe_collect_flat_tool_dependency_dir( + tool_file: &Path, + tools_dir: &Path, + assets: &mut Vec, + seen: &mut HashSet, +) -> Result<()> { + let Some(stem) = tool_file.file_stem().and_then(|s| s.to_str()) else { + return Ok(()); + }; + if stem.is_empty() { + return Ok(()); + } + let sibling_dir = tools_dir.join(stem); + collect_flat_tool_sibling_dir_assets(&sibling_dir, tools_dir, assets, seen) +} + fn has_tool_manifest(dir: &Path) -> bool { [ "TOOL.yaml", @@ -1638,6 +1715,78 @@ mod tests { assert_eq!(bundle.tools[0].filename, "quick-tool.py"); } + #[test] + fn builder_flat_tool_export_includes_same_stem_dependency_dir() { + let dir = tempfile::tempdir().unwrap(); + let skills_dir = dir.path().join("skills"); + let workflows_dir = dir.path().join("workflows"); + let tools_dir = dir.path().join("tools"); + std::fs::create_dir_all(&skills_dir).unwrap(); + std::fs::create_dir_all(&workflows_dir).unwrap(); + std::fs::create_dir_all(tools_dir.join("main_test").join("relatedfiles")).unwrap(); + + std::fs::write( + skills_dir.join("plain.md"), + "---\nname: plain\ndescription: d\ntrigger: /plain\nmodel: codex\n---\nNo refs\n", + ) + .unwrap(); + std::fs::write(tools_dir.join("main_test.py"), "print('ok')\n").unwrap(); + std::fs::write( + tools_dir + .join("main_test") + .join("relatedfiles") + .join("helper.txt"), + "helper\n", + ) + .unwrap(); + + let builder = BundleBuilder::new([&skills_dir], [&workflows_dir]); + let bundle = builder + .build(&["plain"], &["__none__"], &["__none__"], &["main_test.py"]) + .unwrap(); + + let filenames: HashSet = bundle + .tools + .iter() + .map(|asset| asset.filename.clone()) + .collect(); + assert!(filenames.contains("main_test.py")); + assert!(filenames.contains("main_test/relatedfiles/helper.txt")); + } + + #[test] + fn builder_rejects_binary_tool_assets_with_clear_error() { + let dir = tempfile::tempdir().unwrap(); + let skills_dir = dir.path().join("skills"); + let workflows_dir = dir.path().join("workflows"); + let tools_dir = dir.path().join("tools"); + let package_dir = tools_dir.join("binpack"); + std::fs::create_dir_all(&skills_dir).unwrap(); + std::fs::create_dir_all(&workflows_dir).unwrap(); + std::fs::create_dir_all(&package_dir).unwrap(); + + std::fs::write( + skills_dir.join("plain.md"), + "---\nname: plain\ndescription: d\ntrigger: /plain\nmodel: codex\n---\nNo refs\n", + ) + .unwrap(); + std::fs::write( + package_dir.join("TOOL.yaml"), + "name: binpack\ndescription: test\nruntime: shell\nentrypoint: run.sh\n", + ) + .unwrap(); + std::fs::write(package_dir.join("run.sh"), "#!/usr/bin/env sh\necho ok\n").unwrap(); + std::fs::write(package_dir.join("helper.bin"), [0_u8, 159, 146, 150]).unwrap(); + + let builder = BundleBuilder::new([&skills_dir], [&workflows_dir]); + let err = builder + .build(&["__none__"], &["__none__"], &["__none__"], &["binpack"]) + .expect_err("binary payload should be rejected clearly"); + let msg = err.to_string(); + assert!(msg.contains(".a7bundle v1 only supports text assets")); + assert!(msg.contains("helper.bin")); + } + #[test] fn builder_tools_only_selection_exports_scripts_without_skills_or_workflows() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/web/frontend/src/views/SkillsView.vue b/crates/web/frontend/src/views/SkillsView.vue index 9883afa..4f9d8bd 100644 --- a/crates/web/frontend/src/views/SkillsView.vue +++ b/crates/web/frontend/src/views/SkillsView.vue @@ -445,12 +445,20 @@ async function importFromUrl() {
+
+ {{ importStatus.message }} +
+
@@ -458,7 +466,7 @@ async function importFromUrl() {
diff --git a/crates/web/src/api.rs b/crates/web/src/api.rs index abab794..3f64ce4 100644 --- a/crates/web/src/api.rs +++ b/crates/web/src/api.rs @@ -2603,15 +2603,20 @@ pub async fn skill_import_handler( } pub async fn skill_registry_handler() -> impl IntoResponse { - let registry_url = + const REGISTRY_URL: &str = "https://raw.githubusercontent.com/danieldear/agent007/main/docs/registry.json"; + const FALLBACK_REGISTRY_JSON: &str = include_str!("../../../docs/registry.json"); + + let fallback = serde_json::from_str::(FALLBACK_REGISTRY_JSON) + .unwrap_or_else(|_| serde_json::json!([])); + let client = reqwest::Client::new(); - match client.get(registry_url).send().await { + match client.get(REGISTRY_URL).send().await { Ok(resp) if resp.status().is_success() => match resp.json::().await { Ok(val) => Json(val).into_response(), - Err(_) => Json(serde_json::json!([])).into_response(), + Err(_) => Json(fallback).into_response(), }, - _ => Json(serde_json::json!([])).into_response(), + _ => Json(fallback).into_response(), } } @@ -4442,7 +4447,13 @@ fn skill_associations(skill: &agent007_skills::Skill) -> AssociatedAssets { extract_associations_from_text(skill.template(), &mut assets); if skill.is_package() { for file in list_package_files(skill.entry_path()) { - collect_association_from_ref(&file, &mut assets); + let path = skill.entry_path().join(&file); + if !path.is_file() { + continue; + } + if let Ok(content) = std::fs::read_to_string(&path) { + extract_associations_from_text(&content, &mut assets); + } } } assets @@ -5088,8 +5099,8 @@ pub async fn etr_cache_clear_handler(State(state): State) -> impl Into #[cfg(test)] mod tests { use super::{ - collect_association_from_ref, parse_bundle_selection, tool_reference_keys, - EXTERNAL_WORKFLOW_CONTROL_ERROR, + collect_association_from_ref, load_skills_from_dir, parse_bundle_selection, + skill_associations, tool_reference_keys, EXTERNAL_WORKFLOW_CONTROL_ERROR, }; use crate::server::WebServer; use axum::http::StatusCode; @@ -5139,6 +5150,28 @@ mod tests { assert!(items.is_empty()); } + #[test] + fn package_skill_helper_files_do_not_become_ghost_script_refs() { + let dir = tempfile::tempdir().unwrap(); + let skills_dir = dir.path().join(".agent007").join("skills"); + let package_dir = skills_dir.join("pkg-skill"); + std::fs::create_dir_all(&package_dir).unwrap(); + std::fs::write( + package_dir.join("SKILL.md"), + "---\nname: pkg\ndescription: test\ntrigger: /pkg\nmodel: codex\n---\nUse {{args}}\n", + ) + .unwrap(); + std::fs::write(package_dir.join("ftm_engine.py"), "print('helper')\n").unwrap(); + + let skills = load_skills_from_dir(&skills_dir); + assert_eq!(skills.len(), 1); + let associations = skill_associations(&skills[0]); + assert!( + associations.scripts.is_empty(), + "package helper files should not be exposed as unresolved script refs" + ); + } + #[tokio::test] async fn api_tool_import_quarantine_approve_and_test_flow() { let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); diff --git a/docs/a7bundle-v2.md b/docs/a7bundle-v2.md new file mode 100644 index 0000000..6bd0638 --- /dev/null +++ b/docs/a7bundle-v2.md @@ -0,0 +1,294 @@ +# .a7bundle v2 design + +## Goals + +- Replace JSON-only `.a7bundle` v1 with a compressed, binary-safe format. +- Preserve backward compatibility for importing existing v1 bundles. +- Make bundle contents self-describing, integrity-checked, and portable for P2P sharing. +- Track artifact versions at save/edit time instead of only at export time. + +## Non-goals + +- Full signing / trust framework in v2 initial rollout. +- Content-addressed storage or remote registry protocol. +- Automatic semantic version inference from prompt or code meaning. + +## Problems in v1 + +```ascii +v1 +├─ single JSON blob +├─ content stored as UTF-8 strings only +├─ weak support for nested dependency trees +├─ no safe binary payload transport +├─ large exports due to uncompressed text +└─ artifact versioning not first-class +``` + +## Format overview + +`.a7bundle` remains the user-facing extension, but v2 changes the internal container. + +```ascii +.a7bundle v2 +└─ tar.zst archive + ├─ manifest.json + ├─ skills/ + ├─ workflows/ + ├─ personas/ + ├─ tools/ + ├─ scripts/ + └─ assets/ +``` + +## Manifest schema + +```json +{ + "bundle_format_version": 2, + "bundle_version": 1, + "created_at": "2026-05-13T17:30:00Z", + "created_by": "agent007 0.3.1", + "source_project": "agent007", + "entries": [ + { + "path": "skills/review-skill/SKILL.md", + "kind": "skill", + "artifact_id": "skill:review-skill", + "version": "1.2.0", + "sha256": "...", + "size_bytes": 1823, + "executable": false, + "media_type": "text/markdown", + "encoding": "utf-8", + "dependency_group": "skill:review-skill" + } + ], + "dependency_groups": [ + { + "id": "tool:main_test.py", + "members": [ + "tools/main_test.py", + "tools/main_test/relatedfiles/helper.txt" + ] + } + ], + "unresolved_references": [], + "compat": { + "imported_from_v1": false + } +} +``` + +## Entry rules + +Each entry must carry: + +- `path` +- `kind` (`skill`, `workflow`, `persona`, `tool`, `script`, `asset`) +- `artifact_id` +- `version` +- `sha256` +- `size_bytes` +- `executable` +- `media_type` +- `encoding` + +Notes: + +- `encoding` is `utf-8` for text assets and omitted or `binary` for raw assets. +- `executable` preserves tool/script executability on import. +- `dependency_group` ties flat files to sibling support trees. + +## Artifact identity + +Every managed artifact should have a stable identity model: + +```ascii +artifact identity +├─ artifact_id +├─ name +├─ kind +├─ version +├─ created_at +├─ updated_at +├─ last_modified_by +└─ sha256 +``` + +## Version layers + +```ascii +version layers +├─ bundle_format_version +│ └─ schema/container compatibility +├─ bundle_version +│ └─ packaging revision of one exported bundle +└─ artifact version + ├─ skill version + ├─ workflow version + ├─ persona version + ├─ tool version + └─ script/package version +``` + +## Version bump rules + +### Artifact version + +Artifact version changes on **real persisted content change**. + +```ascii +create +└─ assign initial version (recommended: 1.0.0 or 0.1.0) + +edit + save from dashboard +└─ bump artifact version + +edit + save from hosted LLM +└─ bump artifact version + +CLI save/update +└─ bump artifact version + +save with no content change +└─ no version bump +``` + +Recommended semantics: + +- patch = content/prompt/config fix +- minor = backward-compatible feature expansion +- major = breaking contract/schema/behavior change + +### Import rules + +```ascii +import new artifact +└─ preserve imported artifact version + +import overwrite existing artifact +├─ if incoming content differs +│ └─ replace local artifact and preserve incoming version +└─ if content is identical + └─ keep version unchanged +``` + +Also record local metadata: + +- `imported_at` +- `imported_from` +- `imported_bundle_version` + +### Export rules + +```ascii +export +├─ preserve artifact versions exactly +├─ assign or increment bundle_version +└─ regenerate manifest hashes +``` + +## Dependency closure rules + +v2 export should include: + +```ascii +for selected artifact +├─ explicit file/package itself +├─ manifest-declared package members +├─ flat-tool sibling dependency directory (same stem) +├─ recursively referenced scripts/tools when resolvable +└─ dependency_groups metadata for reconstruction / preview +``` + +## Binary handling + +v2 must support raw binary payloads. + +```ascii +binary policy +├─ store as raw archive entries +├─ hash raw bytes, not decoded text +├─ preserve executable bit where relevant +└─ never force UTF-8 decoding for bundle transport +``` + +v1 behavior remains: + +```ascii +v1 +└─ reject binary/non-UTF8 clearly +``` + +## Compatibility plan + +### Read path + +- Importer must detect: + - v1 JSON bundle + - v2 tar.zst bundle +- v1 remains importable. +- v2 becomes the default export format. + +### Write path + +```ascii +phase 1 +├─ keep v1 importer +└─ add v2 exporter/importer + +phase 2 +├─ export v2 by default +└─ keep v1 import only + +phase 3 +└─ optionally add v1 export only behind explicit compatibility flag +``` + +## UI / API implications + +Dashboard should expose: + +```ascii +artifact metadata +├─ current version +├─ last modified time +├─ last modified by +├─ source scope +└─ dependency summary +``` + +Export preview should show: + +```ascii +bundle preview +├─ bundle format version +├─ bundle version +├─ artifact versions +├─ binary assets present +├─ dependency groups +└─ unresolved refs (if any) +``` + +## Suggested implementation order + +```ascii +1. introduce artifact metadata model +2. bump versions on dashboard/LLM/CLI save paths +3. add v2 manifest structs +4. add tar.zst writer/reader +5. add raw-byte bundle asset support +6. wire export preview to show version/dependency metadata +7. keep v1 import compatibility +8. add future signing/provenance later +``` + +## Success criteria + +- Export/import supports text and binary assets safely. +- Flat tool files can bring sibling dependency trees. +- `.a7bundle` is smaller and self-contained. +- Each saved artifact carries a meaningful version. +- Import/export preserves artifact versions correctly. +- v1 bundles still import successfully. diff --git a/docs/registry.json b/docs/registry.json index 8ab0966..566fce0 100644 --- a/docs/registry.json +++ b/docs/registry.json @@ -1,29 +1,22 @@ [ { - "name": "Code Reviewer", - "trigger": "/code-review", - "description": "Full code review with actionable feedback on bugs, security, and design", - "category": "code", + "name": "Brainstorm", + "trigger": "/brainstorm", + "description": "Free-form ideation \u2014 explores a problem space, generates 3\u20135 distinct approaches with trade-offs, and produces a structured ideation document. Use before invoking the architect or PRD workflow.", + "category": "project", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/code-review.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/brainstorm.md", + "version": "1.1.0", "stars": 0 }, { - "name": "Test Generator", - "trigger": "/code-test-gen", - "description": "Generate comprehensive test suites with edge cases and mocking", + "name": "Documentation Writer", + "trigger": "/code-document", + "description": "Generate API docs, architecture docs, and inline documentation", "category": "code", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/code-test-gen.md", - "stars": 0 - }, - { - "name": "Security Auditor", - "trigger": "/code-security-audit", - "description": "Security audit covering OWASP Top 10, secrets, dependencies, and threat modeling", - "category": "security", - "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/code-security-audit.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/code-document.md", + "version": "1.1.0", "stars": 0 }, { @@ -32,7 +25,8 @@ "description": "Profile analysis and performance optimization suggestions", "category": "code", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/code-optimize.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/code-optimize.md", + "version": "1.1.0", "stars": 0 }, { @@ -41,106 +35,128 @@ "description": "Identify code smells and propose targeted improvements", "category": "code", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/code-refactor.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/code-refactor.md", + "version": "1.1.0", "stars": 0 }, { - "name": "PR Reviewer", - "trigger": "/dev-pr-review", - "description": "Thorough pull request review with actionable feedback", - "category": "devops", + "name": "Security Auditor", + "trigger": "/code-security-audit", + "description": "Security audit covering OWASP, dependencies, and threat modeling", + "category": "code", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/dev-pr-review.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/code-security-audit.md", + "version": "1.1.0", "stars": 0 }, { - "name": "TDD Coach", - "trigger": "/dev-tdd", - "description": "Test-driven development cycle: red-green-refactor with coaching", + "name": "Test Generator", + "trigger": "/code-test-gen", + "description": "Generate comprehensive test suites with edge cases", "category": "code", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/dev-tdd.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/code-test-gen.md", + "version": "1.1.0", + "stars": 0 + }, + { + "name": "Architect", + "trigger": "/dev-architect", + "description": "Design system architecture from requirements", + "category": "dev", + "author": "agent007", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/dev-architect.md", + "version": "1.1.0", "stars": 0 }, { "name": "Debugger", "trigger": "/dev-debug", "description": "Systematic debugging with hypothesis-driven investigation", - "category": "code", + "category": "dev", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/dev-debug.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/dev-debug.md", + "version": "1.1.0", "stars": 0 }, { - "name": "Architect", - "trigger": "/dev-architect", - "description": "Design system architecture from requirements with diagrams", - "category": "architecture", + "name": "PR Reviewer", + "trigger": "/dev-pr-review", + "description": "Thorough pull request review with actionable feedback", + "category": "dev", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/dev-architect.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/dev-pr-review.md", + "version": "1.1.0", "stars": 0 }, { - "name": "Documentation Writer", - "trigger": "/code-document", - "description": "Generate API docs, architecture docs, and inline documentation", - "category": "docs", + "name": "TDD Coach", + "trigger": "/dev-tdd", + "description": "Test-driven development cycle (red-green-refactor)", + "category": "dev", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/code-document.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/dev-tdd.md", + "version": "1.1.0", "stars": 0 }, { - "name": "Project Planner", - "trigger": "/project-plan", - "description": "Break features into tasks with estimates and dependencies", - "category": "project", + "name": "Codebase Analyzer", + "trigger": "/meta-analyze-codebase", + "description": "Analyze codebase for tech stack, patterns, and architecture", + "category": "meta", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/project-plan.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/meta-analyze-codebase.md", + "version": "1.1.0", "stars": 0 }, { - "name": "PRD Writer", - "trigger": "/project-prd", - "description": "Product requirements document with user stories and constraints", - "category": "project", + "name": "Agent Creator", + "trigger": "/meta-create-agent", + "description": "Guided wizard to create a custom agent persona", + "category": "meta", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/project-prd.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/meta-create-agent.md", + "version": "1.1.0", "stars": 0 }, { "name": "Changelog Generator", "trigger": "/project-changelog", "description": "Generate changelogs grouped by type from git history", - "category": "devops", + "category": "project", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/project-changelog.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/project-changelog.md", + "version": "1.1.0", "stars": 0 }, { - "name": "Release Manager", - "trigger": "/project-release", - "description": "Version strategy, release notes, and rollback planning", - "category": "devops", + "name": "Project Planner", + "trigger": "/project-plan", + "description": "Break features into tasks with estimates and dependencies", + "category": "project", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/project-release.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/project-plan.md", + "version": "1.1.0", "stars": 0 }, { - "name": "Frontend Designer", - "trigger": "/frontend-designer", - "description": "Create distinctive, production-grade frontend interfaces with high design quality", - "category": "frontend", + "name": "PRD Writer", + "trigger": "/project-prd", + "description": "Product requirements document with user stories and constraints", + "category": "project", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/Frontend_Designer.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/project-prd.md", + "version": "1.1.0", "stars": 0 }, { - "name": "Codebase Analyzer", - "trigger": "/meta-analyze-codebase", - "description": "Analyze codebase for tech stack, patterns, and architecture", - "category": "architecture", + "name": "Release Manager", + "trigger": "/project-release", + "description": "Version strategy, release notes, and rollback planning", + "category": "project", "author": "agent007", - "url": "https://raw.githubusercontent.com/danieldear/agent007/main/skills/meta-analyze-codebase.md", + "url": "https://raw.githubusercontent.com/danieldear/agent007/main/crates/cli/skills/project-release.md", + "version": "1.1.0", "stars": 0 } ] From 6f5ef2d76a909f7d138c419bdd9b781a94f9f409 Mon Sep 17 00:00:00 2001 From: Daniel Franklin Date: Thu, 14 May 2026 01:23:36 -0400 Subject: [PATCH 2/3] fix(web): improve skill discovery and sharing refs --- crates/web/frontend/src/composables/useApi.js | 17 +- crates/web/frontend/src/views/SharingView.vue | 8 +- crates/web/frontend/src/views/SkillsView.vue | 256 ++++- crates/web/src/api.rs | 981 +++++++++++++++++- crates/web/src/server.rs | 2 + 5 files changed, 1250 insertions(+), 14 deletions(-) diff --git a/crates/web/frontend/src/composables/useApi.js b/crates/web/frontend/src/composables/useApi.js index 539b728..4b2a95d 100644 --- a/crates/web/frontend/src/composables/useApi.js +++ b/crates/web/frontend/src/composables/useApi.js @@ -53,7 +53,22 @@ export function useApi() { const suffix = qs.toString() ? `?${qs.toString()}` : '' return fetchJson(`/api/skills/detail/${encodeURIComponent(trigger)}${suffix}`) }, - importSkill: (url) => fetchJson('/api/skills/import', { method: 'POST', body: JSON.stringify({ url }) }), + importSkill: (url, opts = {}) => + fetchJson('/api/skills/import', { + method: 'POST', + body: JSON.stringify({ + url, + conflict_action: opts.conflict_action, + alias_trigger: opts.alias_trigger, + }), + }), + previewSkillImport: (url) => + fetchJson('/api/skills/preview', { method: 'POST', body: JSON.stringify({ url }) }), + discoverSkills: (q, sources = [], limit = 12) => + fetchJson('/api/skills/discover', { + method: 'POST', + body: JSON.stringify({ q, sources, limit }), + }), deleteSkill: (trigger) => fetchJson(`/api/skills/${encodeURIComponent(trigger.replace(/^\//, ''))}`, { method: 'DELETE' }), getRegistry: () => fetchJson('/api/skill-registry'), diff --git a/crates/web/frontend/src/views/SharingView.vue b/crates/web/frontend/src/views/SharingView.vue index 14f03c2..f4df9af 100644 --- a/crates/web/frontend/src/views/SharingView.vue +++ b/crates/web/frontend/src/views/SharingView.vue @@ -39,7 +39,13 @@ const allPersonasSelected = computed( // API paths: tools/ml/infer.py → bundle key: ml/infer.py // scripts/train.py → bundle key: scripts/train.py function apiPathToBundleKey(path) { - return path.startsWith("tools/") ? path.slice("tools/".length) : path; + const normalized = String(path || "").replace(/^\.?\/*/, ""); + const withoutAgentHome = normalized.startsWith(".agent007/") + ? normalized.slice(".agent007/".length) + : normalized; + return withoutAgentHome.startsWith("tools/") + ? withoutAgentHome.slice("tools/".length) + : withoutAgentHome; } const allTools = computed(() => { diff --git a/crates/web/frontend/src/views/SkillsView.vue b/crates/web/frontend/src/views/SkillsView.vue index 4f9d8bd..e5cd1f9 100644 --- a/crates/web/frontend/src/views/SkillsView.vue +++ b/crates/web/frontend/src/views/SkillsView.vue @@ -11,10 +11,26 @@ const editingTrigger = ref(null) // null = creating, string = editing existing const importUrl = ref('') const importStatus = ref(null) const searchQuery = ref('') +const discoverQuery = ref('') +const discoverSourcesText = ref('') +const discoverResults = ref([]) +const discovering = ref(false) +const previewLoading = ref(false) +const previewError = ref(null) +const previewOpen = ref(false) +const previewData = ref(null) +const previewMode = ref('replace') +const previewAliasTrigger = ref('') const toast = ref(null) const promotingTrigger = ref(null) const deletingTrigger = ref(null) +const DEFAULT_DISCOVER_SOURCES = [ + 'https://github.com/openai/skills/tree/main/skills', + 'https://github.com/anthropics/skills/tree/main/skills', + 'https://github.com/vercel-labs/agent-skills/tree/main/skills', +] + let toastTimer = null function showToast(message, type = 'success') { clearTimeout(toastTimer) @@ -86,7 +102,10 @@ const form = ref({ template: '', }) -onMounted(loadSkills) +onMounted(async () => { + loadDiscoverSources() + await loadSkills() +}) async function loadSkills() { const data = await api.listSkills() @@ -108,6 +127,33 @@ function switchTab(tab) { if (tab === 'browse') loadRegistry() } +function loadDiscoverSources() { + try { + const saved = localStorage.getItem('agent007.skillDiscoverSources') + if (saved) { + const parsed = JSON.parse(saved) + if (Array.isArray(parsed) && parsed.length) { + discoverSourcesText.value = parsed.join('\n') + return + } + } + } catch {} + discoverSourcesText.value = DEFAULT_DISCOVER_SOURCES.join('\n') +} + +function normalizedDiscoverSources() { + return discoverSourcesText.value + .split('\n') + .map(v => v.trim()) + .filter(Boolean) +} + +function saveDiscoverSources() { + try { + localStorage.setItem('agent007.skillDiscoverSources', JSON.stringify(normalizedDiscoverSources())) + } catch {} +} + const categoryOrder = ['dev', 'code', 'project', 'meta', 'custom'] const categoryLabels = { dev: 'Development', @@ -298,6 +344,76 @@ async function importFromUrl() { importStatus.value = { type: 'error', message: e.message } } } + +async function runDiscoverSearch() { + if (!discoverQuery.value.trim()) return + const sources = normalizedDiscoverSources() + if (!sources.length) { + importStatus.value = { type: 'error', message: 'Add at least one GitHub source URL' } + return + } + saveDiscoverSources() + discovering.value = true + importStatus.value = null + try { + const result = await api.discoverSkills(discoverQuery.value.trim(), sources, 16) + discoverResults.value = Array.isArray(result?.results) ? result.results : [] + } catch (e) { + importStatus.value = { type: 'error', message: e.message || 'Discover failed' } + discoverResults.value = [] + } finally { + discovering.value = false + } +} + +async function openPreview(url, installMode = 'preview') { + previewOpen.value = true + previewLoading.value = true + previewError.value = null + previewData.value = null + previewMode.value = 'replace' + previewAliasTrigger.value = '' + try { + previewData.value = await api.previewSkillImport(url) + if (installMode === 'install' && previewData.value?.conflict) { + previewMode.value = 'replace' + previewAliasTrigger.value = `${previewData.value.trigger}-alt` + } + } catch (e) { + previewError.value = e.message || 'Preview failed' + } finally { + previewLoading.value = false + } +} + +async function installFromPreview() { + if (!previewData.value?.url) return + importStatus.value = { type: 'loading', message: `Installing ${previewData.value.name || previewData.value.trigger}...` } + try { + const opts = {} + if (previewData.value?.conflict) { + opts.conflict_action = previewMode.value + if (previewMode.value === 'alias') { + opts.alias_trigger = previewAliasTrigger.value.trim() + } + } + const result = await api.importSkill(previewData.value.url, opts) + if (result?.skipped) { + importStatus.value = { type: 'success', message: `Kept existing ${result.trigger}` } + } else { + importStatus.value = { type: 'success', message: `Installed ${result?.trigger || previewData.value.trigger}` } + } + previewOpen.value = false + previewData.value = null + await loadSkills() + if (discoverQuery.value.trim()) { + await runDiscoverSearch() + } + setTimeout(() => importStatus.value = null, 3000) + } catch (e) { + importStatus.value = { type: 'error', message: e.message || 'Install failed' } + } +}