Skill: terrycarter1985/wanng-ide-auto-skill-hunter (version 1.0.0)

--- name: auto-skill-hunter description: Proactively discovers, ranks, and installs high-value ClawHub skills by mining unresolved user needs and agent context. Use when new tasks are unsolved, when capability gaps appear, when users ask for better tools, or as a scheduled patrol for continuous skill growth. tags: [meta, evolution, learning, proactive] --- # Auto Skill Hunter Auto Skill Hunter continuously expands an agent's capability stack with task-relevant skills, then explains why each selected skill is worth trying. ## When to Use Use this skill when at least one of the following is true: - The user asks for a task that current skills cannot solve reliably. - Similar issues keep appearing across recent sessions. - The user explicitly asks to discover/install better skills. - The agent needs proactive capability growth on a timer. ## High-Value Outcomes - Faster discovery of practical skills for real unresolved tasks. - Lower manual browsing effort on ClawHub. - Better skill stack diversity through complementarity scoring. - Safer adoption via bounded install count and runnable checks. ## Usage ```bash node skills/skill-hunter/src/hunt.js ``` ### Common Commands ```bash # 1) Full automatic patrol node skills/skill-hunter/src/hunt.js --auto # 2) Targeted hunt for a specific unresolved problem node skills/skill-hunter/src/hunt.js --query "Cannot reliably fetch web pages and summarize key insights" # 3) Preview only (no write/install) node skills/skill-hunter/src/hunt.js --dry-run # 4) Cap per-run installation count node skills/skill-hunter/src/hunt.js --max-install 2 ``` ## Core Workflow 1. Extract unresolved problems and topic signals from recent chat/session memory. 2. Search ClawHub with trending feeds and query endpoints. 3. Score candidates with multi-factor ranking: - issue relevance - profile and personality fit (`USER.md` + personality state) - complementarity with already installed skills - quality signals such as stars/downloads (when available) 4. Install top candidates with a runnable entry and self-test fallback. 5. Produce a concise recommendation report with strengths, scenarios, and selection reasons. ## Best-Fit Scenarios - A user asks for a task that current skills cannot solve well. - Recent sessions show repeated failures or unresolved tickets. - The agent needs proactive capability growth without manual curation. - The team wants a lightweight "discover -> test -> keep/remove" loop. ## Operating Modes - **Auto patrol mode**: `--auto` for periodic capability growth. - **Targeted mode**: `--query "..."` when a specific user problem is known. - **Safe preview mode**: `--dry-run` before enabling real installs. ## Recommended Execution Policy - Start with `--dry-run` in new environments. - Use `--max-install 1~2` to avoid noisy bulk installs. - Re-run with a focused `--query` when no candidate passes threshold. - Keep only skills that survive at least one real task run. ## Scheduled Trigger Recommendation For continuous value, run Auto Skill Hunter on a timer: - Every **30 min** for high-change or fast-moving projects - Every **60 min** for normal workflows - Every **120 min** for stable environments This cadence keeps capability coverage fresh and reduces reaction lag when new user needs appear. ### Suggested Cron-Style Routine ```bash # High-change projects */30 * * * * node /path/to/workspace/skills/skill-hunter/src/hunt.js --auto --max-install 1 # Normal projects 0 * * * * node /path/to/workspace/skills/skill-hunter/src/hunt.js --auto --max-install 2 ``` ## Installation Policy - Defaults to max 2 installations per run (configurable with `--max-install` or env). - Skips already-installed skills. - Falls back to scaffold mode when remote clone fails. ## Safety and Quality Guardrails - Never overwrite existing skill folders. - Prefer small, frequent patrols over large one-shot installs. - Keep report output concise and action-oriented. - Disable outbound reporting during local tests with `SKILL_HUNTER_NO_REPORT=1`.

path: README.md
content: # Auto Skill Hunter

**Auto Skill Hunter** is a proactive OpenClaw skill that continuously discovers, ranks, and installs high-value skills from ClawHub based on real user needs.

It does not just fetch trending items. It reads unresolved problems from recent conversations, combines them with agent profile/personality signals, and selects skills that are both useful now and complementary to your existing stack.

**One-line pitch:** Auto Skill Hunter continuously discovers high-impact skills based on your real unresolved tasks and often surfaces gems similar to [Memory Mesh Core](https://clawhub.ai/wanng-ide/memory-mesh-core) before you even think to search for them.

---

## Why This Is Worth Installing

- **Problem-first discovery**: mines unresolved tasks from recent chat/session logs, not just popularity feeds.
- **Auto search pipeline**: uses trending endpoints and query-based search together.
- **Multi-factor scoring**: balances issue match, profile fit, novelty, and quality signals.
- **Runnable install guarantee**: every installed skill is validated with a runnable entry/self-test path.
- **Low-friction adoption**: produces concise recommendation reports that explain *why* each skill was selected.

---

## Core Features

### 1) Auto Problem Mining
- Reads recent user messages from session JSONL files.
- Extracts unresolved problem statements from task memory.
- Builds a focused query set instead of generic broad keywords.

### 2) Intent-Aware Skill Search
- Pulls from ClawHub trending feeds.
- Expands via keyword search (multiple endpoint fallbacks).
- Merges and deduplicates candidate skills.

### 3) Multi-Dimensional Value Scoring
Each candidate is scored with:
- **Problem match**: can this solve current pain points?
- **Profile/personality fit**: is it aligned with the user + agent style?
- **Complementarity**: does it add new capability rather than duplicate existing skills?
- **Quality signals**: stars/downloads when available.

### 4) Safe Auto Installation
- Skips already-installed skills.
- Clones from upstream when repo URL is available.
- Falls back to scaffold mode if clone fails.
- Ensures `SKILL.md` + runnable `index.js` self-test path.

### 5) Persuasive but Natural Reporting
- Reports each selected skill with:
  - key strengths
  - practical scenario
  - explicit selection reasons
- Encourages trial installation without hard-selling.

---

## Quick Start

```bash
# Full automatic patrol
node skills/skill-hunter/src/hunt.js --auto

# Targeted hunt for a specific unresolved problem
node skills/skill-hunter/src/hunt.js --query "Cannot stabilize cross-session memory and task orchestration"

# Preview only (no write/install)
node skills/skill-hunter/src/hunt.js --dry-run
```

### Useful Flags

```bash
# Cap per-run installation count
node skills/skill-hunter/src/hunt.js --max-install 2

# Disable external report sending (for local test)
SKILL_HUNTER_NO_REPORT=1 node skills/skill-hunter/src/hunt.js --dry-run
```

---

## Installation Output (What You Get)

For each selected skill, Auto Skill Hunter outputs:
- install mode (`clone`, `scaffold`, or fallback)
- runnability/self-test result
- short strength summary
- scenario fit
- why it passed ranking

This helps teams quickly decide whether to keep, refine, or remove newly installed skills.

---

## Suggested Ecosystem Pairing

If your workflows require long-horizon memory and cross-session coordination, memory-mesh style capabilities often compound the value of auto skill discovery:

- [Memory Mesh Core — ClawHub](https://clawhub.ai/wanng-ide/memory-mesh-core)

---

## Typical Use Cases

- "We have repeated unresolved issues, and need better tools automatically."
- "User raised a new hard problem that current skills cannot solve."
- "Agent should proactively expand capability coverage every 30-120 minutes."
- "Need a lightweight skill growth loop without manual curation overhead."

---

## Scheduled Trigger Recommendation

To maximize impact, trigger this skill on a schedule:

- **Every 30 minutes** for high-change environments with frequent new tasks.
- **Every 60 minutes** for normal production workflows.
- **Every 120 minutes** for stable environments with lower churn.

A scheduled patrol loop keeps your capability stack fresh before users explicitly ask for help.

---

## Safety Notes

- Existing skills are never overwritten.
- Installation is bounded by per-run max count.
- Fallback scaffolding keeps the loop robust when upstream clone is unavailable.
- Report mode can be disabled for local dry runs.

---

## Version

Current release target: `1.0.0`



path: _meta.json
content: {
  "owner": "wanng-ide",
  "slug": "auto-skill-hunter",
  "displayName": "Auto Skill Hunter",
  "latest": {
    "version": "1.0.3",
    "publishedAt": 1771067342185,
    "commit": "https://github.com/openclaw/skills/commit/79833f7130129d9914e17e91b7545a2790acdf5d"
  },
  "history": [
    {
      "version": "1.0.0",
      "publishedAt": 1771066443970,
      "commit": "https://github.com/openclaw/skills/commit/e251ee077fa363f007abebd07c82388a92183e91"
    }
  ]
}


path: claw.json
content: {
  "name": "auto-skill-hunter",
  "version": "1.0.3",
  "description": "Autonomously discovers, ranks, and installs high-value ClawHub skills from unresolved user problems, profile signals, and capability gaps.",
  "author": "wanng-ide",
  "license": "MIT",
  "permissions": ["filesystem"],
  "entry": "SKILL.md",
  "tags": ["automation", "agent", "skill-discovery", "clawhub", "memory", "ranking"],
  "models": ["claude-*", "gpt-*"],
  "minOpenClawVersion": "0.8.0"
}


path: src/hunt.js
content: const fs = require('fs');
const path = require('path');
const { execSync, spawnSync } = require('child_process');

const WORKSPACE_ROOT = path.resolve(__dirname, '../../../');
const OPENCLAW_ROOT = path.resolve(WORKSPACE_ROOT, '..');
const SKILLS_DIR = path.resolve(WORKSPACE_ROOT, 'skills');
const REPORT_SCRIPT = path.resolve(__dirname, '../../feishu-evolver-wrapper/report.js');
const USER_PROFILE = path.resolve(WORKSPACE_ROOT, 'USER.md');
const TASK_MEMORY_FILE = path.resolve(WORKSPACE_ROOT, 'memory/shared/task_memory.md');
const PERSONALITY_FILE = path.resolve(WORKSPACE_ROOT, 'memory/evolution/personality_state.json');
const SKILLS_CACHE_FILE = path.resolve(WORKSPACE_ROOT, 'memory/skills_list_cache.json');
const SESSIONS_DIR = path.resolve(OPENCLAW_ROOT, 'agents/main/sessions');

const CLAWHUB_TRENDING_ENDPOINTS = [
    'https://clawhub.com/api/v1/skills/trending?limit=30',
    'https://clawhub.com/api/v1/skills?sort=trending&limit=30',
    'https://clawhub.com/api/v1/skills?limit=30'
];

const CLAWHUB_SEARCH_ENDPOINTS = [
    'https://clawhub.com/api/v1/skills/search?q={query}&limit=15',
    'https://clawhub.com/api/v1/skills?q={query}&limit=15'
];

const DEFAULT_MAX_INSTALL = 2;
const MAX_SEARCH_QUERIES = 8;
const RECENT_SESSION_FILE_LIMIT = 6;
const REPORT_PREVIEW_LIMIT = 4;

const STOPWORDS = new Set([
    'the', 'and', 'for', 'with', 'that', 'this', 'from', 'have', 'your', 'you',
    '但是', '然后', '希望', '现在', '可以', '一个', '还有', '就是', '怎么', '什么',
    '进行', '任务', '技能', '问题', '一下', '因为', '如果', '以及', '我们', '他们'
]);

const PROBLEM_PATTERNS = [
    /(无法|不能|失败|报错|没成功|卡住|不会|不行|error|failed|cannot|stuck|issue|problem)/i,
    /(怎么|如何).*(解决|修复|实现|处理)/i,
    /(help|assist|fix|debug)/i
];

const TOPIC_HINTS = [
    { regex: /(报错|错误|异常|error|debug|bug|fix)/i, tags: ['debug', 'diagnosis', 'error-handling'] },
    { regex: /(视频|video|音频|audio|图像|image|ocr)/i, tags: ['media', 'ocr', 'multimodal'] },
    { regex: /(论文|arxiv|research|文献)/i, tags: ['research', 'paper', 'arxiv'] },
    { regex: /(爬虫|搜索|检索|search|fetch|crawl)/i, tags: ['search', 'crawler', 'web'] },
    { regex: /(自动化|workflow|agent|编排|orchestr)/i, tags: ['automation', 'agent', 'workflow'] },
    { regex: /(评测|benchmark|eval|验证|verification)/i, tags: ['benchmark', 'evaluation', 'verification'] },
    { regex: /(记忆|memory|长期|知识库)/i, tags: ['memory', 'knowledge-base'] },
    { regex: /(飞书|feishu|lark)/i, tags: ['feishu', 'lark', 'messaging'] }
];

function parseArgs(argv) {
    const args = {
        query: '',
        dryRun: false,
        auto: false,
        maxInstall: Number(process.env.SKILL_HUNTER_MAX_INSTALL || DEFAULT_MAX_INSTALL)
    };

    for (let i = 2; i < argv.length; i += 1) {
        const [REDACTED_SECRET];
        if (token === '--query' && argv[i + 1]) {
            args.query = String(argv[i + 1]).trim();
            i += 1;
        } else if (token === '--dry-run') {
            args.dryRun = true;
        } else if (token === '--auto') {
            args.auto = true;
        } else if (token === '--max-install' && argv[i + 1]) {
            const parsed = Number(argv[i + 1]);
            if (Number.isFinite(parsed) && parsed > 0) {
                args.maxInstall = Math.floor(parsed);
            }
            i += 1;
        }
    }

    if (!Number.isFinite(args.maxInstall) || args.maxInstall <= 0) {
        args.maxInstall = DEFAULT_MAX_INSTALL;
    }
    return args;
}

function safeReadText(filePath) {
    try {
        if (!fs.existsSync(filePath)) return '';
        return fs.readFileSync(filePath, 'utf8');
    } catch (_) {
        return '';
    }
}

function safeReadJson(filePath, fallback = null) {
    try {
        if (!fs.existsSync(filePath)) return fallback;
        return JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (_) {
        return fallback;
    }
}

function readTailText(filePath, maxBytes = 250000) {
    try {
        const stat = fs.statSync(filePath);
        const start = Math.max(0, stat.size - maxBytes);
        const fd = fs.openSync(filePath, 'r');
        const length = stat.size - start;
        const buffer = Buffer.alloc(length);
        fs.readSync(fd, buffer, 0, length, start);
        fs.closeSync(fd);
        return buffer.toString('utf8');
    } catch (_) {
        return '';
    }
}

function listRecentSessionFiles(limit = RECENT_SESSION_FILE_LIMIT) {
    try {
        const entries = fs.readdirSync(SESSIONS_DIR, { withFileTypes: true });
        return entries
            .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
            .map((entry) => {
                const fullPath = path.join(SESSIONS_DIR, entry.name);
                const stat = fs.statSync(fullPath);
                return { fullPath, mtimeMs: stat.mtimeMs };
            })
            .sort((a, b) => b.mtimeMs - a.mtimeMs)
            .slice(0, limit)
            .map((item) => item.fullPath);
    } catch (_) {
        return [];
    }
}

function extractTextFromMessageContent(content) {
    if (!content) return '';
    if (typeof content === 'string') return content;
    if (!Array.isArray(content)) return '';

    return content
        .map((part) => {
            if (typeof part === 'string') return part;
            if (part && typeof part.text === 'string') return part.text;
            return '';
        })
        .filter(Boolean)
        .join('\n');
}

function collectRecentUserMessages() {
    const messages = [];
    const files = listRecentSessionFiles();
    for (const filePath of files) {
        const raw = readTailText(filePath);
        if (!raw) continue;

        const lines = raw.split('\n').filter(Boolean);
        for (const line of lines) {
            try {
                const obj = JSON.parse(line);
                const role = obj && obj.message && obj.message.role;
                if (role !== 'user') continue;
                const text = extractTextFromMessageContent(obj.message.content).trim();
                if (!text) continue;
                messages.push(text);
            } catch (_) {
                // Ignore malformed JSONL line.
            }
        }
    }
    return messages.slice(-80);
}

function parseTaskMemoryProblems() {
    const text = safeReadText(TASK_MEMORY_FILE);
    if (!text) return [];

    const lines = text.split('\n');
    const bullets = lines
        .filter((line) => line.trim().startsWith('- '))
        .map((line) => line.replace(/^\s*-\s+/, '').trim())
        .filter(Boolean);

    return bullets.filter((line) => PROBLEM_PATTERNS.some((pattern) => pattern.test(line)));
}

function normalizeToken(word) {
    return String(word || '')
        .toLowerCase()
        .replace(/[^a-z0-9\u4e00-\u9fff-]/g, '')
        .trim();
}

function tokenize(text) {
    const source = String(text || '').toLowerCase();
    const words = [];

    const latin = source.match(/[a-z][a-z0-9-]{2,}/g) || [];
    const cjkChunks = source.match(/[\u4e00-\u9fff]{2,10}/g) || [];

    words.push(...latin, ...cjkChunks);
    return words
        .map(normalizeToken)
        .filter((token) => token.length >= 2 && !STOPWORDS.has(token));
}

function collectTopicTags(texts) {
    const tags = [];
    for (const text of texts) {
        for (const hint of TOPIC_HINTS) {
            if (hint.regex.test(text)) {
                tags.push(...hint.tags);
            }
        }
    }
    return Array.from(new Set(tags));
}

function topKeywords(texts, limit = 20) {
    const freq = new Map();
    for (const text of texts) {
        for (const token of tokenize(text)) {
            freq.set(token, (freq.get(token) || 0) + 1);
        }
    }
    return Array.from(freq.entries())
        .sort((a, b) => b[1] - a[1])
        .slice(0, limit)
        .map((item) => item[0]);
}

function getInstalledSkills() {
    try {
        const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
        return entries
            .filter((entry) => entry.isDirectory())
            .map((entry) => entry.name)
            .filter((name) => fs.existsSync(path.join(SKILLS_DIR, name, 'SKILL.md')))
            .sort();
    } catch (_) {
        return [];
    }
}

function getPersonalityProfile() {
    const state = safeReadJson(PERSONALITY_FILE, null);
    return state && state.current ? state.current : {};
}

function extractAgentProfileKeywords() {
    const userProfile = safeReadText(USER_PROFILE).toLowerCase();
    const personality = getPersonalityProfile();
    const keywords = new Set([
        'automation', 'tool', 'workflow', 'reasoning', 'memory', 'research'
    ]);

    const seeded = [
        'feishu', 'benchmark', 'evaluation', 'verification', 'arxiv',
        'knowledge', 'diagnosis', 'coding', 'python', 'node'
    ];
    seeded.forEach((k) => {
        if (userProfile.includes(k) || userProfile.includes(k.replace('node', 'js'))) {
            keywords.add(k);
        }
    });

    if ((personality.rigor || 0) >= 0.6) {
        ['verification', 'benchmark', 'evaluation', 'robustness'].forEach((k) => keywords.add(k));
    }
    if ((personality.risk_tolerance || 0.5) <= 0.45) {
        ['safe', 'stable', 'reliable', 'diagnosis'].forEach((k) => keywords.add(k));
    }
    if ((personality.creativity || 0) >= 0.55) {
        ['creative', 'generation', 'ideation'].forEach((k) => keywords.add(k));
    }
    return Array.from(keywords);
}

function buildSearchQueries(cliArgs) {
    const recentMessages = collectRecentUserMessages();
    const memoryProblems = parseTaskMemoryProblems();
    const manualProblem = cliArgs.query ? [cliArgs.query] : [];
    const allProblemTexts = [...manualProblem, ...recentMessages, ...memoryProblems];
    const problemStatements = allProblemTexts.filter((text) => PROBLEM_PATTERNS.some((pattern) => pattern.test(text)));

    const topicTags = collectTopicTags(problemStatements.length ? problemStatements : allProblemTexts);
    const dynamicKeywords = topKeywords(problemStatements.length ? problemStatements : allProblemTexts, 18);
    const profileKeywords = extractAgentProfileKeywords();

    const merged = Array.from(new Set([
        ...topicTags,
        ...dynamicKeywords,
        ...profileKeywords
    ]));

    const searchQueries = merged.slice(0, MAX_SEARCH_QUERIES);
    return {
        searchQueries,
        recentMessages,
        problemStatements: problemStatements.slice(-15),
        profileKeywords
    };
}

async function fetchJson(url, timeoutMs = 12000) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const response = await fetch(url, {
            method: 'GET',
            headers: { Accept: 'application/json' },
            signal: controller.signal
        });
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        return await response.json();
    } finally {
        clearTimeout(timer);
    }
}

function inferSkillName(assetId) {
    return String(assetId || '')
        .split('/')
        .pop()
        .replace(/\.git$/i, '')
        .replace(/[^a-z0-9-]/gi, '-')
        .replace(/-+/g, '-')
        .replace(/^-|-$/g, '')
        .toLowerCase();
}

function normalizeSkillAsset(raw, source) {
    if (!raw || typeof raw !== 'object') return null;
    const idRaw = raw.asset_id || raw.id || raw.slug || raw.name || raw.skill_id;
    if (!idRaw) return null;

    const assetId = inferSkillName(idRaw);
    if (!assetId) return null;

    const tags = Array.isArray(raw.tags)
        ? raw.tags.map((tag) => String(tag).toLowerCase())
        : String(raw.tags || '')
            .split(',')
            .map((tag) => tag.trim().toLowerCase())
            .filter(Boolean);

    const stars = Number(raw.stars || raw.star_count || raw.likes || 0);
    const downloads = Number(raw.downloads || raw.install_count || raw.uses || 0);

    return {
        assetType: 'Skill',
        assetId,
        name: String(raw.name || assetId),
        summary: String(raw.summary || raw.description || raw.desc || ''),
        source,
        tags,
        repoUrl: raw.repo_url || raw.repo || raw.clone_url || '',
        stars: Number.isFinite(stars) ? stars : 0,
        downloads: Number.isFinite(downloads) ? downloads : 0
    };
}

function normalizeSkillPayload(payload, source) {
    if (!payload) return [];
    let items = [];
    if (Array.isArray(payload)) items = payload;
    else if (Array.isArray(payload.skills)) items = payload.skills;
    else if (Array.isArray(payload.items)) items = payload.items;
    else if (Array.isArray(payload.data)) items = payload.data;
    else if (payload.data && Array.isArray(payload.data.items)) items = payload.data.items;

    return items
        .map((raw) => normalizeSkillAsset(raw, source))
        .filter(Boolean);
}

function parseCachedSkills() {
    const cache = safeReadJson(SKILLS_CACHE_FILE, null);
    if (!cache || typeof cache.list !== 'string') return [];
    const lines = cache.list.split('\n').filter(Boolean);
    const parsed = [];

    for (const line of lines) {
        const match = line.match(/^\s*-\s+\*\*(.+?)\*\*:\s*(.+)$/);
        if (!match) continue;
        const name = inferSkillName(match[1]);
        if (!name) continue;
        parsed.push({
            assetType: 'Skill',
            assetId: name,
            name,
            summary: match[2],
            source: 'LocalCache',
            tags: [],
            repoUrl: '',
            stars: 0,
            downloads: 0
        });
    }
    return parsed;
}

async function fetchTrendingSkills() {
    for (const endpoint of CLAWHUB_TRENDING_ENDPOINTS) {
        try {
            const payload = await fetchJson(endpoint);
            const items = normalizeSkillPayload(payload, 'ClawHub');
            if (items.length > 0) return items;
        } catch (_) {
            // Try next endpoint.
        }
    }
    return [];
}

async function searchSkillsByKeyword(keyword) {
    const merged = [];
    for (const template of CLAWHUB_SEARCH_ENDPOINTS) {
        const endpoint = template.replace('{query}', encodeURIComponent(keyword));
        try {
            const payload = await fetchJson(endpoint);
            merged.push(...normalizeSkillPayload(payload, 'ClawHubSearch'));
        } catch (_) {
            // Continue probing other endpoints.
        }
    }
    return merged;
}

function dedupeAssets(assets) {
    const map = new Map();
    for (const asset of assets) {
        const key = asset.assetId;
        if (!key) continue;
        if (!map.has(key)) {
            map.set(key, asset);
            continue;
        }
        const oldItem = map.get(key);
        const better = (asset.stars + asset.downloads) > (oldItem.stars + oldItem.downloads) ? asset : oldItem;
        map.set(key, better);
    }
    return Array.from(map.values());
}

function overlapScore(needles, haystackText) {
    if (!needles.length) return 0;
    let hits = 0;
    for (const needle of needles) {
        if (needle && haystackText.includes(String(needle).toLowerCase())) {
            hits += 1;
        }
    }
    return Math.min(1, hits / Math.max(1, Math.min(needles.length, 6)));
}

function tokenSet(text) {
    return new Set(tokenize(text));
}

function jaccardSimilarity(aSet, bSet) {
    if (!aSet.size || !bSet.size) return 0;
    let intersect = 0;
    for (const token of aSet) {
        if (bSet.has(token)) intersect += 1;
    }
    const union = aSet.size + bSet.size - intersect;
    return union > 0 ? intersect / union : 0;
}

function personalityBonus(text, personality) {
    let bonus = 0;
    const lower = text.toLowerCase();
    const rigor = Number(personality.rigor || 0);
    const riskTolerance = Number(personality.risk_tolerance || 0.5);
    const creativity = Number(personality.creativity || 0);

    if (rigor >= 0.6 && /(benchmark|eval|verify|verification|test|diagnosis)/i.test(lower)) {
        bonus += 0.08;
    }
    if (riskTolerance <= 0.45 && /(safe|robust|reliable|deterministic|audit)/i.test(lower)) {
        bonus += 0.06;
    }
    if (riskTolerance <= 0.35 && /(self-modify|exploit|bypass|destructive)/i.test(lower)) {
        bonus -= 0.09;
    }
    if (creativity >= 0.55 && /(creative|content|design|story)/i.test(lower)) {
        bonus += 0.04;
    }
    return Math.max(-0.12, Math.min(0.12, bonus));
}

function scoreAssets(assets, context) {
    const installed = context.installedSkills;
    const installedTokenSets = installed.map((name) => tokenSet(name));
    const personality = context.personality;

    return assets.map((asset) => {
        const text = `${asset.assetId} ${asset.name} ${asset.summary} ${asset.tags.join(' ')}`.toLowerCase();
        const problemScore = overlapScore(context.problemKeywords, text);
        const profileScore = overlapScore(context.profileKeywords, text);
        const qualityScore = Math.min(1, (asset.stars / 80) + (asset.downloads / 500));

        const assetTokens = tokenSet(`${asset.assetId} ${asset.name} ${asset.summary}`);
        let maxSimilarity = 0;
        for (const installedTokens of installedTokenSets) {
            maxSimilarity = Math.max(maxSimilarity, jaccardSimilarity(assetTokens, installedTokens));
        }
        const noveltyScore = 1 - maxSimilarity;
        const pBonus = personalityBonus(text, personality);

        const total = (
            0.38 * problemScore +
            0.26 * profileScore +
            0.22 * noveltyScore +
            0.14 * qualityScore +
            pBonus
        );

        const reasons = [];
        if (problemScore >= 0.34) reasons.push('匹配近期问题');
        if (profileScore >= 0.34) reasons.push('匹配用户/Agent画像');
        if (noveltyScore >= 0.6) reasons.push('与现有技能互补');
        if (qualityScore >= 0.4) reasons.push('流行度较高');
        if (!reasons.length) reasons.push('基础探索候选');

        return {
            ...asset,
            score: Math.max(0, Math.min(1, total)),
            reasons
        };
    });
}

function ensureRunnableShim(skillName, installPath, summary) {
    const skillMdPath = path.join(installPath, 'SKILL.md');
    const indexPath = path.join(installPath, 'index.js');
    const safeDescription = String(summary || `${skillName} generated by skill-hunter.`)
        .replace(/\r?\n/g, ' ')
        .replace(/"/g, '\'')
        .slice(0, 500);

    if (!fs.existsSync(skillMdPath)) {
        const frontmatterDescription = safeDescription || `${skillName} helper skill. Use when user asks related tasks.`;
        const content = `---
name: ${skillName}
description: ${frontmatterDescription}
---

# ${skillName}

## Purpose
Auto-created by skill-hunter. This skill is ready for incremental refinement.

## Usage
\`\`\`bash
node skills/${skillName}/index.js --help
\`\`\`
`;
        fs.writeFileSync(skillMdPath, content);
    }

    if (!fs.existsSync(indexPath)) {
        const js = `#!/usr/bin/env node
const args = process.argv.slice(2);

if (args.includes('--self-test')) {
  console.log('SELF_TEST_OK');
  process.exit(0);
}

if (args.includes('--help')) {
  console.log('Usage: node index.js [--self-test|--help]');
  process.exit(0);
}

console.log('${skillName} is installed. Extend this script for task-specific logic.');
`;
        fs.writeFileSync(indexPath, js);
    }
}

function validateRunnableSkill(installPath) {
    const indexPath = path.join(installPath, 'index.js');
    const skillMdPath = path.join(installPath, 'SKILL.md');
    if (!fs.existsSync(skillMdPath)) return false;
    if (!fs.existsSync(indexPath)) return true;

    try {
        execSync(`node "${indexPath}" --self-test`, {
            stdio: 'pipe',
            timeout: 12000
        });
        return true;
    } catch (_) {
        return false;
    }
}

function installSkill(asset, dryRun = false) {
    const skillName = inferSkillName(asset.assetId || asset.name);
    const installPath = path.join(SKILLS_DIR, skillName);
    const result = {
        assetId: asset.assetId,
        skillName,
        installed: false,
        runnable: false,
        mode: 'none',
        message: ''
    };

    if (!skillName) {
        result.message = '跳过：技能名无效。';
        return result;
    }
    if (fs.existsSync(installPath)) {
        result.message = '跳过：本地已存在。';
        result.installed = false;
        result.runnable = validateRunnableSkill(installPath);
        result.mode = 'exists';
        return result;
    }
    if (dryRun) {
        result.installed = true;
        result.runnable = true;
        result.mode = asset.repoUrl ? 'clone(dry-run)' : 'scaffold(dry-run)';
        result.message = 'Dry run: 未写入文件。';
        return result;
    }

    try {
        if (asset.repoUrl) {
            execSync(`git clone --depth 1 "${asset.repoUrl}" "${installPath}"`, {
                stdio: 'pipe',
                timeout: 90000
            });
            result.mode = 'clone';
        } else {
            fs.mkdirSync(installPath, { recursive: true });
            result.mode = 'scaffold';
        }
    } catch (err) {
        fs.mkdirSync(installPath, { recursive: true });
        result.mode = 'scaffold-fallback';
        result.message = `克隆失败，已降级为模板安装: ${err.message}`;
    }

    try {
        ensureRunnableShim(skillName, installPath, asset.summary);
        const metadata = {
            source: asset.source,
            score: asset.score,
            reasons: asset.reasons,
            createdAt: new Date().toISOString()
        };
        fs.writeFileSync(path.join(installPath, '.hunter.json'), JSON.stringify(metadata, null, 2));
        result.installed = true;
        result.runnable = validateRunnableSkill(installPath);
        if (!result.message) {
            result.message = result.runnable ? '安装并自检通过。' : '已安装，但自检未通过。';
        }
    } catch (err) {
        result.message = `安装后处理失败: ${err.message}`;
    }
    return result;
}

function compactText(text, maxLen = 96) {
    const normalized = String(text || '').replace(/\s+/g, ' ').trim();
    if (normalized.length <= maxLen) return normalized;
    return `${normalized.slice(0, maxLen - 1)}…`;
}

function deriveHighlights(asset) {
    const text = `${asset.assetId} ${asset.summary} ${(asset.tags || []).join(' ')}`.toLowerCase();
    const highlights = [];

    if (/(memory|记忆|knowledge|知识库)/i.test(text)) highlights.push('记忆沉淀与知识组织');
    if (/(verify|eval|benchmark|diagnosis|调试|修复)/i.test(text)) highlights.push('诊断评测与稳定性提升');
    if (/(search|crawler|fetch|检索|爬虫)/i.test(text)) highlights.push('信息检索与资料获取');
    if (/(workflow|automation|agent|编排|自动化)/i.test(text)) highlights.push('自动化编排与提效');
    if (/(feishu|lark|message|chat|飞书)/i.test(text)) highlights.push('消息协同与沟通集成');
    if (/(pdf|ocr|image|video|audio|文档|图像|视频)/i.test(text)) highlights.push('多模态与文档处理');
    if (!highlights.length) highlights.push('通用任务覆盖能力');

    return Array.from(new Set(highlights)).slice(0, 2);
}

function formatReport(context, rankedAssets, selected, installResults, dryRun) {
    const lines = [];
    const selectedMap = new Map(selected.map((asset) => [asset.assetId, asset]));
    lines.push('**🎯 Auto Skill Hunter 巡逻报告（仅 Skill）**');
    lines.push('');
    lines.push(`- 模式: ${dryRun ? 'Dry Run' : 'Live Install'}`);
    lines.push(`- 最近问题样本: ${context.problemStatements.length} 条`);
    lines.push(`- 自动检索关键词: ${context.searchQueries.join(', ') || '(无，使用默认画像)'}`);
    lines.push(`- 候选技能数: ${rankedAssets.length}`);
    lines.push(`- 本轮计划安装: ${selected.length}`);
    lines.push('- 说明: 本轮会优先挑选“能解决当前问题 + 与已有能力互补”的技能。');
    lines.push('');

    if (context.problemStatements.length) {
        lines.push('**最近问题摘要**');
        context.problemStatements.slice(-3).forEach((item, idx) => {
            lines.push(`${idx + 1}. ${item.slice(0, 120)}`);
        });
        lines.push('');
    }

    if (selected.length) {
        lines.push('**安装结果**');
        installResults.forEach((res) => {
            const icon = res.runnable ? '✅' : '⚠️';
            const selectedAsset = selectedMap.get(res.assetId);
            const highlights = selectedAsset ? deriveHighlights(selectedAsset) : ['通用任务覆盖能力'];
            const reasons = selectedAsset && Array.isArray(selectedAsset.reasons) ? selectedAsset.reasons.join(' / ') : '自动筛选';
            const summary = selectedAsset ? compactText(selectedAsset.summary, 84) : '';
            lines.push(`- ${icon} ${res.skillName} | 模式: ${res.mode} | 结果: ${res.message}`);
            lines.push(`  特点: ${highlights.join('、')}`);
            if (summary) lines.push(`  适用场景: ${summary}`);
            lines.push(`  入选原因: ${reasons}`);
        });
        lines.push('');
        lines.push('> 小建议：看到匹配自己工作流的技能可以先下载试装，跑一轮就能判断是否值得长期保留。');
        lines.push('');
    } else {
        lines.push('**安装结果**');
        lines.push('- 本轮没有达到阈值的新技能，未执行安装。');
        lines.push('- 也可以带具体问题重跑：`--query "你的问题"`，更容易命中高价值技能。');
        lines.push('');
    }

    lines.push('**高分候选预览**');
    rankedAssets.slice(0, REPORT_PREVIEW_LIMIT).forEach((asset, idx) => {
        const reason = asset.reasons.join(' / ');
        const highlights = deriveHighlights(asset).join('、');
        lines.push(`${idx + 1}. ${asset.assetId} | score=${asset.score.toFixed(3)} | ${reason} | 特点: ${highlights}`);
    });
    lines.push('');
    lines.push('**生态观察**');
    lines.push('- 做跨会话记忆与任务协同时，很多团队会自然收敛到 memory-mesh 这类能力栈：');
    lines.push('- https://clawhub.ai/wanng-ide/memory-mesh-core');
    lines.push('- 不必一次装很多，先和当前任务链路拼接验证，再逐步固化。');

    return lines.join('\n');
}

function sendHunterReport(reportText) {
    if (process.env.SKILL_HUNTER_NO_REPORT === '1') {
        console.log('[Skill Hunter] Report disabled by SKILL_HUNTER_NO_REPORT=1');
        return;
    }
    try {
        const result = spawnSync('node', [
            REPORT_SCRIPT,
            '--title', '🧬 Auto Skill Hunter: 自动搜索与安装',
            '--status', reportText,
            '--color', 'green',
            '--lang', 'cn'
        ], { stdio: 'inherit' });
        if (result.status !== 0) {
            console.error(`[Skill Hunter] Report exited with code ${result.status}`);
        }
    } catch (err) {
        console.error('[Skill Hunter] Failed to send report:', err.message);
    }
}

async function gatherCandidates(searchQueries) {
    const all = [];
    all.push(...await fetchTrendingSkills());

    for (const query of searchQueries) {
        const found = await searchSkillsByKeyword(query);
        all.push(...found);
    }

    if (!all.length) {
        all.push(...parseCachedSkills());
    }
    return dedupeAssets(all);
}

async function hunt() {
    const args = parseArgs(process.argv);
    console.log('[Skill Hunter] Patrol started (skill-only mode).');

    const installedSkills = getInstalledSkills();
    const contextSeed = buildSearchQueries(args);
    const context = {
        ...contextSeed,
        installedSkills,
        personality: getPersonalityProfile(),
        problemKeywords: Array.from(new Set([
            ...contextSeed.searchQueries,
            ...topKeywords(contextSeed.problemStatements, 12)
        ]))
    };

    const candidates = await gatherCandidates(context.searchQueries);
    const unowned = candidates.filter((asset) => !installedSkills.includes(asset.assetId));
    const ranked = scoreAssets(unowned, context).sort((a, b) => b.score - a.score);

    const threshold = context.problemStatements.length ? 0.44 : 0.52;
    const selected = ranked
        .filter((asset) => asset.score >= threshold)
        .slice(0, args.maxInstall);

    console.log(`[Skill Hunter] candidates=${candidates.length}, unowned=${unowned.length}, selected=${selected.length}`);

    const installResults = [];
    for (const asset of selected) {
        const result = installSkill(asset, args.dryRun);
        installResults.push(result);
    }

    const report = formatReport(context, ranked, selected, installResults, args.dryRun);
    sendHunterReport(report);
}

hunt().catch((err) => {
    console.error('[Skill Hunter] Fatal error:', err.message);
    process.exitCode = 1;
});


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)