feat(cli): add skillforge command for auto-discovery of skills
Some checks failed
CodeQL Analysis / CodeQL Analysis (push) Has been cancelled
PR Hygiene / nudge-stale-prs (push) Has been cancelled
CI / Detect Change Scope (push) Has been cancelled
Docker / PR Docker Smoke (push) Has been cancelled
Docker / Build and Push Docker Image (push) Has been cancelled
Rust Package Security Audit / Security Audit (push) Has been cancelled
Rust Package Security Audit / License & Supply Chain (push) Has been cancelled
CI / Format & Lint (push) Has been cancelled
CI / Lint Strict Delta (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (Smoke) (push) Has been cancelled
CI / Docs-Only Fast Path (push) Has been cancelled
CI / Non-Rust Fast Path (push) Has been cancelled
CI / Docs Quality (push) Has been cancelled
CI / CI Required Gate (push) Has been cancelled
Stale / stale (push) Has been cancelled
Update Contributors NOTICE / Update NOTICE with new contributors (push) Has been cancelled

- Add zeroclaw skillforge run --min-score 0.7 --dry-run
- Add zeroclaw skillforge scout --query "ai agent" --limit 20
- Add zeroclaw skillforge status to show configuration
- Wire SkillForgeConfig into main Config struct
- Add new_with_query method to GitHubScout for custom searches
This commit is contained in:
2026-02-18 03:37:45 +00:00
parent 1a9102e871
commit 8c59673eac
6 changed files with 178 additions and 0 deletions

View File

@@ -92,6 +92,10 @@ pub struct Config {
/// Hardware configuration (wizard-driven physical world setup).
#[serde(default)]
pub hardware: HardwareConfig,
/// SkillForge auto-discovery configuration.
#[serde(default)]
pub skillforge: crate::skillforge::SkillForgeConfig,
}
// ── Delegate Agents ──────────────────────────────────────────────
@@ -1645,6 +1649,7 @@ impl Default for Config {
peripherals: PeripheralsConfig::default(),
agents: HashMap::new(),
hardware: HardwareConfig::default(),
skillforge: crate::skillforge::SkillForgeConfig::default(),
}
}
}

View File

@@ -61,6 +61,7 @@ pub mod rag;
pub mod runtime;
pub mod security;
pub mod service;
pub mod skillforge;
pub mod skills;
pub mod tools;
pub mod tunnel;

View File

@@ -209,6 +209,12 @@ enum Commands {
skill_command: SkillCommands,
},
/// Auto-discover and integrate skills from GitHub/ClawHub
Skillforge {
#[command(subcommand)]
skillforge_command: skillforge::SkillforgeCommands,
},
/// Migrate data from other agent runtimes
Migrate {
#[command(subcommand)]
@@ -563,6 +569,10 @@ async fn main() -> Result<()> {
skills::handle_command(skill_command, &config.workspace_dir)
}
Commands::Skillforge { skillforge_command } => {
skillforge::handle_command(skillforge_command, &config).await
}
Commands::Migrate { migrate_command } => {
migration::handle_command(migrate_command, &config).await
}

View File

@@ -132,6 +132,7 @@ pub fn run_wizard() -> Result<Config> {
peripherals: crate::config::PeripheralsConfig::default(),
agents: std::collections::HashMap::new(),
hardware: hardware_config,
skillforge: crate::skillforge::SkillForgeConfig::default(),
};
println!(
@@ -346,6 +347,7 @@ pub fn run_quick_setup(
peripherals: crate::config::PeripheralsConfig::default(),
agents: std::collections::HashMap::new(),
hardware: crate::config::HardwareConfig::default(),
skillforge: crate::skillforge::SkillForgeConfig::default(),
};
config.save()?;

View File

@@ -16,6 +16,136 @@ use self::evaluate::{EvalResult, Evaluator, Recommendation};
use self::integrate::Integrator;
use self::scout::{GitHubScout, Scout, ScoutResult, ScoutSource};
use crate::config::Config;
#[derive(Debug, Clone, clap::Subcommand)]
pub enum SkillforgeCommands {
Run {
#[arg(long, default_value = "0.7")]
min_score: f64,
#[arg(long)]
dry_run: bool,
#[arg(long)]
source: Option<String>,
},
Scout {
#[arg(long, default_value = "zeroclaw skill")]
query: String,
#[arg(long, default_value = "20")]
limit: usize,
},
Status,
}
pub async fn handle_command(command: SkillforgeCommands, config: &Config) -> Result<()> {
let _forge_config = config.skillforge.clone();
match command {
SkillforgeCommands::Run { min_score, dry_run, source } => {
run_forge(config, min_score, dry_run, source).await
}
SkillforgeCommands::Scout { query, limit } => {
run_scout(config, query, limit).await
}
SkillforgeCommands::Status => {
show_status(config)
}
}
}
async fn run_forge(config: &Config, min_score: f64, dry_run: bool, source: Option<String>) -> Result<()> {
println!("🔮 SkillForge Pipeline");
println!(" Min Score: {}", min_score);
println!(" Dry Run: {}", dry_run);
println!(" Source: {}", source.as_deref().unwrap_or("all"));
println!();
let mut forge_config = config.skillforge.clone();
forge_config.enabled = true;
forge_config.min_score = min_score;
forge_config.auto_integrate = !dry_run;
if let Some(src) = source {
forge_config.sources = vec![src];
}
let forge = SkillForge::new(forge_config);
let report = forge.forge().await?;
println!("📊 Results:");
println!(" Discovered: {}", report.discovered);
println!(" Evaluated: {}", report.evaluated);
println!(" Auto-integrated: {}", report.auto_integrated);
println!(" Manual review: {}", report.manual_review);
println!(" Skipped: {}", report.skipped);
println!();
if !report.results.is_empty() {
println!("📋 Top Candidates:");
for (i, res) in report.results.iter().take(10).enumerate() {
let status = match res.recommendation {
Recommendation::Auto => "✅ AUTO",
Recommendation::Manual => "⚠️ REVIEW",
Recommendation::Skip => "❌ SKIP",
};
println!(" {}. {} [{:.2}] {}", i + 1, res.candidate.name, res.total_score, status);
println!(" {}", res.candidate.description.chars().take(60).collect::<String>());
}
}
Ok(())
}
async fn run_scout(config: &Config, query: String, limit: usize) -> Result<()> {
println!("🔍 SkillForge Scout");
println!(" Query: {}", query);
println!(" Limit: {}", limit);
println!();
let github_token = config.skillforge.github_token.clone();
let scout = GitHubScout::new_with_query(github_token, query.clone());
let results = scout.discover().await?;
let limited: Vec<_> = results.into_iter().take(limit).collect();
println!("📋 Found {} candidates:", limited.len());
for (i, candidate) in limited.iter().enumerate() {
println!(" {}. {}", i + 1, candidate.name);
println!(" URL: {}", candidate.url);
println!("{} stars", candidate.stars);
let desc: String = candidate.description.chars().take(60).collect();
if !desc.is_empty() {
println!(" {}", desc);
}
}
Ok(())
}
fn show_status(config: &Config) -> Result<()> {
println!("🔮 SkillForge Status");
println!();
println!("Configuration:");
println!(" Enabled: {}", config.skillforge.enabled);
println!(" Auto-integrate: {}", config.skillforge.auto_integrate);
println!(" Sources: {:?}", config.skillforge.sources);
println!(" Scan interval: {}h", config.skillforge.scan_interval_hours);
println!(" Min score: {:.2}", config.skillforge.min_score);
println!(" Output directory: {}", config.skillforge.output_dir);
println!(" GitHub token: {}", if config.skillforge.github_token.is_some() { "configured" } else { "not set" });
let skills_dir = std::path::Path::new(&config.skillforge.output_dir);
if skills_dir.exists() {
let count = std::fs::read_dir(skills_dir)
.map(|entries| entries.filter_map(|e| e.ok()).count())
.unwrap_or(0);
println!();
println!("Integrated skills: {}", count);
}
Ok(())
}
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

View File

@@ -103,6 +103,36 @@ impl GitHubScout {
}
}
pub fn new_with_query(token: Option<String>, query: String) -> Self {
use std::time::Duration;
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
"application/vnd.github+json".parse().expect("valid header"),
);
headers.insert(
reqwest::header::USER_AGENT,
"ZeroClaw-SkillForge/0.1".parse().expect("valid header"),
);
if let Some(ref t) = token {
if let Ok(val) = format!("Bearer {t}").parse() {
headers.insert(reqwest::header::AUTHORIZATION, val);
}
}
let client = reqwest::Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(30))
.build()
.expect("failed to build reqwest client");
Self {
client,
queries: vec![query],
}
}
/// Parse the GitHub search/repositories JSON response.
fn parse_items(body: &serde_json::Value) -> Vec<ScoutResult> {
let items = match body.get("items").and_then(|v| v.as_array()) {