Skill: caocong1/skill-discovery (version 2.2.1)

--- name: skill-discovery description: 基于用户意图发现并推荐 skill。分析用户输入，搜索匹配 skill，验证质量后推荐最佳匹配，由用户确认后安装。 metadata: openclaw: install: type: bundled entrypoint: index.js requires: bins: - npx anyBins: - clawhub homepage: https://github.com/caocong1/skill-discovery --- # Skill Discovery 根据用户意图发现并推荐匹配的 skill。默认仅推荐，需用户确认后才会安装。 ## 何时使用 适用场景： - 用户请求帮助完成某个任务，且可能已有现成的 skill 方案 - 用户表达了扩展能力的需求（如"怎么部署"、"帮我写测试"） - 用户提到了特定领域的工具需求（设计、测试、部署等） - 用户明确询问"有没有 skill 可以..."、"找个工具帮我..." ## 工作流程 1. **分析用户输入** — 理解意图，识别目标领域 2. **搜索 skills.sh + ClawHub** — 并行搜索两个注册表，合并去重结果 3. **质量验证** — 检查安装量和来源可信度 4. **推荐最佳匹配** — 返回推荐结果和安装命令，由用户确认后执行安装 > **安全说明**: 默认运行在推荐模式（dry-run），不会自动安装任何 skill。 > 只有调用方显式传入 `{ dryRun: false }` 时才会执行实际安装。 > OpenClaw hook 始终使用推荐模式。 ## 输入/输出 **输入**: 用户自然语言文本（中文或英文） **输出**（统一结构）: ```javascript { success: boolean, stage: 'analyze' | 'search' | 'select' | 'install', outcome: 'installed' | 'already_installed' | 'dry_run' | 'skipped' | 'failed', errorCode: string | null, // 如 'NO_RESULTS'、'INSTALL_FAILED' skill: object | null, candidates: array, message: string } ``` ## 触发示例 **中文:** - "帮我部署到 Vercel" - "有什么工具可以优化性能" - "怎么写测试" - "推荐一个数据处理工具" **英文:** - "find a skill for deploying" - "help me with testing" - "is there a skill for PDF parsing" - "I need a tool for React optimization" ## 安全特性 - **推荐模式默认**: 默认 dry-run，不自动安装，需用户确认 - **Shell 转义**: 所有 CLI 参数转义防止命令注入 - **日志脱敏**: `sanitize()` 递归遮蔽敏感字段（字段名匹配 [REDACTED_SECRET]）和敏感值（`Bearer`/`sk-`/`ghp_` 等模式），业务字段（installs/confidence/domain）保持原值。日志写入 `$OPENCLAW_DIR/logs/skill-discovery-v3.json`，可通过 `OPENCLAW_DIR` 环境变量自定义路径 - **卸载备份**: 卸载的 skill 备份到 `.trash/`，保留 7 天 - **来源验证**: 校验 skill 来源是否在可信 owner 列表中 ## 支持语言 - 中文：触发词 + 领域关键词（含负面模式排除避免误触发） - 英文：触发词 + 领域关键词 ## 覆盖领域 DevOps、测试、设计、文档、代码质量、数据处理、图片处理、视频处理、性能优化、API/网络、数据库、AI/ML、安全、移动端、游戏 ## 依赖 - Node.js >= 18 - `npx skills` CLI（来自 skills.sh） - `clawhub` CLI（可选，来自 clawhub.ai） ## 许可证 MIT-0

path: CHANGELOG.md
content: # Changelog

本项目的所有重要变更都会记录在此文件中。

## [2.2.1] - 2026-03-31

### 增强

- SKILL.md 声明 `install.type: bundled` + `entrypoint: index.js`，消除 OpenClaw "No install spec declared" 提示
- SKILL.md 补充 `sanitize()` 脱敏策略文档（遮蔽字段名/值模式、日志路径），消除 OpenClaw 截断审查提示

## [2.2.0] - 2026-03-31

### 安全增强（解决 OpenClaw Suspicious 标记）

- **默认推荐模式**: `autoDiscover()` 默认 `dryRun: true`，不再自动安装，仅返回推荐结果
- **OpenClaw hook 推荐模式**: `onUserInput()` 使用 `dryRun: true`，返回推荐信息和安装命令供用户确认
- **安装需显式确认**: `skillsAdd()` 默认 `global: false, yes: false`，调用方需显式传入才会执行全局/非交互安装
- **SKILL.md 更新**: 描述从"自动安装"改为"推荐并由用户确认安装"，新增安全说明块

## [2.1.2] - 2026-03-31

### 增强

- ClawHub 搜索自动过滤被安全标记（flagged/suspicious）的 skill（`nonSuspiciousOnly=true`）

## [2.1.1] - 2026-03-31

### 修复

- 新增 `.clawhubignore` 排除 `package-lock.json` 等非必要文件，消除 ClawHub 安全扫描的 `install_untrusted_source` 标记
- SKILL.md 声明 `requires.bins` (`npx`) 和 `anyBins` (`clawhub`)，消除 LLM 扫描的元数据不一致警告

## [2.1.0] - 2026-03-31

### 新增

- **ClawHub 搜索源**: 新增 ClawHub (`clawhub.ai`) 作为 skill 搜索源，与 skills.sh 并行搜索
- `clawhubSearch()`: 通过 ClawHub 公共 API (`/api/v1/search`) 进行向量语义搜索
- `clawhubAdd()`: 支持通过 `clawhub install` 安装 ClawHub 来源的 skill
- `deduplicateResults()`: 合并去重两个源的搜索结果，skills.sh 优先
- `skillsFind()` 现使用 `Promise.allSettled` 并行搜索两个源，ClawHub 故障不影响 skills.sh 结果
- `CLAWHUB_CONFIG`: 集中管理 ClawHub API 配置（基址、端点、超时、数量限制）
- `openclaw` 加入 `trustedOwners` 可信来源列表
- `resolveInstall()` 根据 skill 来源自动选择安装命令（`npx skills add` 或 `clawhub install`）

## [2.0.0] - 2026-03-31

基于 GPT、Opus、GLM、Kimi、MiniMax、Qwen 六份独立评审结果的全面重构。

### 重构

- **常量统一**: 新建 `constants.js`，消除 MAGIC/CONFIG 跨文件重复定义
- **错误码标准化**: 新增 `ERROR_CODES` 枚举（11 种错误类型），所有返回结构统一为 `{ success, stage, outcome, errorCode, skill, candidates, message }`
- **sanitize() 修正**: 重命名 `SANITIZE_SAFE_FIELDS` → `SANITIZE_REDACT_KEY_PATTERNS`，业务字段（installs/confidence/domain）不再被误脱敏
- **autoDiscover 拆分**: 从单一大函数拆为 `searchSkills()` → `filterAndSelect()` → `resolveInstall()` 管道
- **导出清理**: 收紧公共 API，内部工具函数不再暴露

### 增强

- **parseFindOutput() 强化**: JSON 优先解析 + 文本降级，双正则模式（支持 emoji 前缀），新增 M 后缀支持
- **CLI 正式化**: 支持 `--dry-run`、`--json`、`--verbose`、`--help` 参数
- **ESLint + Prettier**: 配置代码检查和格式化（ESLint v10 flat config）

### 测试

- 测试用例从 21 个增加到 **80 个**
- 新增 `openclaw-hook.test.js`（onUserInput、generatePrompt、safeRemove、cleanTrash）
- 补充 shellEscape、withRetry、extractJson、validateParams、buildLogEntry、sanitize 测试

### 文档

- README 和 SKILL.md 全部中文化
- README 视角从"开发者"调整为"OpenClaw 用户"
- 删除未实现功能的描述（config.js、pre-audit、ratings），移至"规划中"章节
- 修正 cleanTrash 注释（"30 分钟" → "1 小时"）

### 修复

- `sanitize()` 不再遮蔽业务字段，日志恢复排查价值
- OAUTH 正则从宽泛的 `===+.*===+` 改为精确的 `===+\s*[A-Za-z]+\s*===+`

## [1.0.0] - 2026-03-30

### 新增

- 首次发布
- 基于用户意图的自动 skill 发现
- 中英文双语输入支持
- 质量验证（安装量、可信来源）
- 自动安装 + 白名单机制
- 安全特性：Shell 转义、日志脱敏、卸载备份
- OpenClaw 集成钩子
- CLI 和编程 API
- 单元测试和集成测试（21 个用例）


path: README.md
content: # Skill Discovery

[![License: MIT-0](https://img.shields.io/badge/License-MIT--0-blue.svg)](LICENSE)

基于用户意图自动发现并安装 skills 的 OpenClaw 技能。当用户表达某类需求时，自动从 skills.sh 搜索、验证并安装最匹配的 skill。

## 安装

在 OpenClaw 中直接对话：

```
> 帮我安装这个 skill：https://github.com/caocong1/skill-discovery
```

或通过命令行：

```bash
npx skills add https://github.com/caocong1/skill-discovery -g
```

## 它能做什么

安装后，当你在 OpenClaw 中输入类似以下内容时，skill-discovery 会自动介入：

```
> 帮我部署到 Vercel
✅ 已自动安装 vercel-labs/agent-skills@vercel-deploy
📊 安装量: 262,000
🔗 https://skills.sh/vercel-labs/agent-skills/vercel-deploy

> 怎么写测试
✅ 已自动安装 vercel-labs/agent-skills@testing-best-practices
```

**不会触发的情况：**
- "今天天气怎么样" — 非技能需求
- "什么是 React" — 知识类提问
- "为什么报错" — 调试类问题

## 工作原理

1. **意图分析** — 通过触发词 + 领域关键词 + 置信度评分判断是否需要搜索 skill
2. **搜索** — 调用 `npx skills find` 从 skills.sh 查找匹配结果（带 10 分钟缓存）
3. **质量验证** — 安装量 >= 1000 且来源可信（vercel-labs、anthropics、openai 等）
4. **自动安装** — 选择评分最高的 skill 并安装

## 触发词

| 语言 | 示例 |
|------|------|
| 中文 | "帮我..."、"怎么..."、"有什么工具..."、"推荐一个..."、"找个...工具" |
| 英文 | "find a skill for..."、"help me with..."、"is there a skill that..."、"I need..." |

## 覆盖领域

DevOps、测试、设计、文档、代码质量、数据处理、图片处理、视频处理、性能优化、API/网络、数据库、AI/ML、安全、移动端、游戏

## 安全机制

- **Shell 转义** — CLI 参数转义防止命令注入
- **日志脱敏** — token、密钥等敏感信息自动遮蔽
- **卸载备份** — 卸载的 skill 备份到 `.trash/`，保留 7 天
- **来源验证** — 校验 skill 来源是否在可信 owner 列表中

## 编程接口

如果需要在自己的代码中调用：

```javascript
const { autoDiscover, onUserInput } = require('skill-discovery');

// 方式一：直接发现
const result = await autoDiscover('帮我部署到 Vercel', { dryRun: true });
// { success, stage, outcome, skill, message, ... }

// 方式二：OpenClaw 钩子
const hookResult = await onUserInput('写个测试');
if (hookResult.handled) {
  console.log(hookResult.response);
}
```

## 返回结构

```javascript
{
  success: boolean,
  stage: 'analyze' | 'search' | 'select' | 'install',
  outcome: 'installed' | 'already_installed' | 'dry_run' | 'skipped' | 'failed',
  errorCode: string | null,
  skill: object | null,
  candidates: array,
  message: string
}
```

## 规划中

- 配置文件支持（`config.json` 用户级覆盖）
- 交互式确认/审批流程（针对非可信来源）
- 安装前内容预审扫描
- Skill 评分集成
- 用户反馈循环优化推荐

---

## 开发

面向贡献者的信息：

```bash
npm install               # 安装依赖
npm test                  # 运行全部测试（80 个用例）
npm run test:unit         # 仅单元测试
npm run test:coverage     # 含覆盖率
npm run lint              # ESLint 检查
node index.js --help      # CLI 模式帮助
```

### 项目结构

```
constants.js          # 统一常量（MAGIC、CONFIG、ERROR_CODES）
skills-cli.js         # npx skills CLI 封装（Phase 1）
auto-discover.js      # 核心发现逻辑（Phase 2）
openclaw-hook.js      # OpenClaw 集成（Phase 3）
index.js              # 主入口 + CLI
```

## 依赖

- Node.js >= 18
- `npx skills` CLI（来自 skills.sh）
- OpenClaw（可选，用于钩子集成）

## 许可证

MIT-0


path: auto-discover.js
content: #!/usr/bin/env node
/**
 * Auto Discover - 自动发现与安装
 *
 * Phase 2: 自动化（A+B）
 * A. 自动发现需求
 * B. 自动执行流程
 */

const { skillsList, skillsFind, skillsAdd, clawhubAdd } = require('./skills-cli');
const { MAGIC, ERROR_CODES, LOG_SCHEMA, DISCOVER_CONFIG } = require('./constants');

// ==================== 预编译正则（避免重复编译）====================
const RE = {
  // 明确触发词（英文）
  explicitTriggers: [
    { re: /find a skill for/i, weight: MAGIC.WEIGHT_EXPLICIT },
    { re: /is there a skill (for|that|to)/i, weight: MAGIC.WEIGHT_EXPLICIT },
    { re: /how do i (do|make|create|build)/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /can you (help me with|do)/i, weight: MAGIC.WEIGHT_MEDIUM },
    { re: /i need (a skill|help with)/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /can i use/i, weight: MAGIC.WEIGHT_MEDIUM },
    { re: /help me (with|to|build|create|make|do)/i, weight: MAGIC.WEIGHT_MEDIUM },
    { re: /looking for/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /\bi want (to|a|an)/i, weight: MAGIC.WEIGHT_MEDIUM },
    // 英文推荐句式
    { re: /recommend.*(tool|library|package|skill|plugin)/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /any.*(tool|library|package|skill|plugin).*(for|that)/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /what('s| is).*tool/i, weight: MAGIC.WEIGHT_MEDIUM },
    // 中文明确触发词（优先级1）
    { re: /找.*?(skill|技能|工具)/i, weight: MAGIC.WEIGHT_EXPLICIT },
    { re: /有没有.*?(skill|技能|工具)/i, weight: MAGIC.WEIGHT_EXPLICIT },
    { re: /安装.*?(skill|技能)/i, weight: MAGIC.WEIGHT_EXPLICIT },
    // 中文触发词（优先级0.8-0.9）
    { re: /有什么工具/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /有什么工具可以/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /推荐一个/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /能不能/i, weight: MAGIC.WEIGHT_MEDIUM },
    { re: /想要/i, weight: MAGIC.WEIGHT_MEDIUM },
    { re: /求推荐/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /能帮我/i, weight: MAGIC.WEIGHT_MEDIUM },
    { re: /帮我推荐/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /帮我找个/i, weight: MAGIC.WEIGHT_HIGH },
    // 中文句式（优先级0.9）
    { re: /怎么.*?(做|实现|创建|部署|写)/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /如何.*?(做|实现|创建|部署|写)/i, weight: MAGIC.WEIGHT_HIGH },
    // 中文句式（优先级0.8）
    { re: /能.*?(帮我|给我).*?(做|写|优化|部署)/i, weight: MAGIC.WEIGHT_MEDIUM },
    { re: /需要.*?(skill|技能|工具)/i, weight: MAGIC.WEIGHT_HIGH },
    { re: /帮我.*?(做|写|优化|部署|处理)/i, weight: MAGIC.WEIGHT_MEDIUM_HIGH },
    // "怎么样才能" 是需求导向，正面触发
    { re: /怎么样才能/i, weight: MAGIC.WEIGHT_HIGH }
  ],

  // 领域关键词（英文）
  domainKeywordsEn: [
    { re: /deploy|deployment|ci[\/\\-]cd|docker|kubernetes|k8s/i, domain: 'devops' },
    { re: /test|testing|jest|playwright|cypress|vitest/i, domain: 'testing' },
    { re: /design|ui|ux|css|tailwind|component/i, domain: 'design' },
    { re: /doc|documentation|readme|changelog/i, domain: 'docs' },
    { re: /review|lint|refactor|best practice/i, domain: 'code-quality' },
    { re: /pdf|excel|csv|json.*process/i, domain: 'data-processing' },
    { re: /image|photo|compress|resize|ocr/i, domain: 'image-processing' },
    { re: /video|ffmpeg|convert/i, domain: 'video-processing' },
    { re: /performance|optimize|speed|fast/i, domain: 'performance' },
    // 英文领域关键词（网络/API、数据库、AI/ML、安全、移动端、游戏）
    { re: /api|rest|graphql|endpoint|http/i, domain: 'network-api' },
    { re: /database|db|sql|mongodb|postgresql|mysql|redis/i, domain: 'database' },
    {
      re: /ai|ml|machine learning|nlp|gpt|llm|chatgpt|openai|deep learning|tensorflow|pytorch/i,
      domain: 'ai-ml'
    },
    { re: /security|secure|crypto|encrypt|auth|authentication|authorization/i, domain: 'security' },
    { re: /mobile|ios|android|react native|flutter|swift|kotlin/i, domain: 'mobile' },
    { re: /game|unity|unreal|godot|game dev|2d|3d/i, domain: 'gaming' }
  ],

  // 领域关键词（中文）
  domainKeywordsZh: [
    { re: /部署|发布|上线/i, domain: 'devops' },
    { re: /测试|单元测试|e2e|自动化测试/i, domain: 'testing' },
    { re: /设计|样式|组件|界面/i, domain: 'design' },
    { re: /文档|说明/i, domain: 'docs' },
    { re: /审查|检查|重构|最佳实践|代码质量/i, domain: 'code-quality' },
    { re: /pdf|excel|csv|数据处理|解析|转换/i, domain: 'data-processing' },
    { re: /图片|图像|压缩|裁剪|ocr|识别/i, domain: 'image-processing' },
    { re: /视频|剪辑|转码|压缩/i, domain: 'video-processing' },
    { re: /性能|优化|加速|提升/i, domain: 'performance' },
    // 中文领域关键词（网络/API、数据库、AI/ML、安全、移动端、游戏）
    { re: /api|接口|rest|graphql|网络请求|http/i, domain: 'network-api' },
    { re: /数据库|db|sql|mongodb|postgresql|mysql|redis/i, domain: 'database' },
    {
      re: /人工智能|ai|机器学习|nlp|gpt|llm|chatgpt|深度学习|tensorflow|pytorch/i,
      domain: 'ai-ml'
    },
    { re: /安全|加密|解密|认证|授权|权限/i, domain: 'security' },
    { re: /移动端|ios|android|react native|flutter|swift|kotlin|手机端/i, domain: 'mobile' },
    { re: /游戏|unity|unreal|godot|游戏开发|2d|3d/i, domain: 'gaming' }
  ],

  // 负面模式（英文）
  negativePatternsEn: [
    /^(what is|what's|how to|explain|describe)/i,
    /^(why|when|who|where)/i,
    /how (much|many|long)/i,
    /tell me (about|what)/i,
    /^(just|only) (a|one)/i,
    /^(i|i'm|i am) (just|only) (a|one)/i,
    /can i ask/i
  ],

  // 负面模式（中文 - 精确匹配避免误杀"怎么样才能"）
  negativePatternsZh: [
    /^什么/i,
    /^为什么/i,
    /^什么时候/i,
    /^谁/i,
    /^哪里/i,
    /^(是|有).*吗$/i, // "是xxx吗？"纯询问
    /天气|时间|日期|新闻/i, // 纯信息查询
    /错误|bug|修复|问题|报错/i, // 调试问题
    /(?:^|有)什么(?:问题|bug|错|报错)/i, // "有什么问题"纯询问
    /(?:咋样|怎么样)$/i // 精确匹配 "怎么样" 或 "咋样" 结尾（避免误杀"怎么样才能..."）
  ]
};

// ==================== 配置 ====================
const CONFIG = {
  ...DISCOVER_CONFIG,
  cache: {
    findResults: new Map(),
    ttl: DISCOVER_CONFIG.cache.ttl,
    maxSize: DISCOVER_CONFIG.cache.maxSize
  }
};

// ==================== A. 自动发现需求 ====================

/**
 * 分析用户输入，判断是否需要搜索 skill
 * 基于官方 find-skills 的触发条件
 *
 * 修复：负面模式采用行首锚定（^）避免误杀
 * "怎么样才能..."中的"怎么样"不是行首，不会被^什么匹配
 */
function analyzeNeed(userInput) {
  const input = userInput.toLowerCase().trim();

  let confidence = 0;
  let matchedDomain = null;
  let query = input;

  // ---- 1. 检查明确触发词 ----
  for (const trigger of RE.explicitTriggers) {
    if (trigger.re.test(input)) {
      confidence = Math.max(confidence, trigger.weight);
      query = input.replace(trigger.re, '').trim();
    }
  }

  // ---- 2. 检查领域关键词（英文） ----
  for (const kw of RE.domainKeywordsEn) {
    if (kw.re.test(input)) {
      confidence = Math.max(confidence, MAGIC.WEIGHT_DOMAIN);
      matchedDomain = kw.domain;
    }
  }

  // ---- 3. 检查领域关键词（中文） ----
  for (const kw of RE.domainKeywordsZh) {
    if (kw.re.test(input)) {
      confidence = Math.max(confidence, MAGIC.WEIGHT_DOMAIN);
      matchedDomain = kw.domain;
    }
  }

  // 领域关键词触发时使用完整输入作为搜索词
  if (matchedDomain && confidence < MAGIC.DOMAIN_MIN_CONFIDENCE) {
    query = input;
  }

  // ---- 4. 检查负面模式（英文，行首锚定避免误杀）----
  for (const neg of RE.negativePatternsEn) {
    if (neg.test(input)) {
      confidence *= MAGIC.NEGATIVE_MULTIPLIER;
    }
  }

  // ---- 5. 检查负面模式（中文，行首锚定避免误杀）----
  for (const neg of RE.negativePatternsZh) {
    if (neg.test(input)) {
      confidence *= MAGIC.NEGATIVE_MULTIPLIER;
    }
  }

  return {
    shouldSearch: confidence >= CONFIG.confidenceThreshold,
    confidence,
    query: query.substring(0, MAGIC.QUERY_MAX_LENGTH),
    domain: matchedDomain
  };
}

// ==================== B. 自动执行流程 ====================

/**
 * 选择最佳 skill
 */
function selectBest(skills) {
  // skills.sh 已验证，仅按安装量 + 来源偏好排序
  const scored = skills.map((skill) => {
    let score = 0;

    // 安装量分数 (0-40)
    score += Math.min(skill.installs / MAGIC.MIN_INSTALLS_SCALE, 4) * MAGIC.INSTALLS_SCORE_FACTOR;

    // 可信来源加分
    const isTrusted = CONFIG.trustedOwners.includes(skill.owner);
    score += isTrusted ? MAGIC.TRUSTED_SCORE : MAGIC.UNTRUSTED_SCORE;

    return { skill, score };
  });

  scored.sort((a, b) => b.score - a.score);
  return scored[0];
}

/**
 * 参数校验
 */
function validateParams(userInput, options) {
  const errors = [];

  // 校验 userInput 类型和长度
  if (typeof userInput !== 'string') {
    errors.push('userInput 必须是字符串');
  } else if (userInput.trim().length === 0) {
    errors.push('userInput 不能为空');
  } else if (userInput.length > 500) {
    errors.push('userInput 不能超过 500 字符');
  }

  // 校验 options 为 object 或 undefined
  if (options !== undefined && typeof options !== 'object') {
    errors.push('options 必须是对象');
  } else if (options && typeof options === 'object') {
    if ('dryRun' in options && typeof options.dryRun !== 'boolean') {
      errors.push('options.dryRun 必须是布尔值');
    }
  }

  return errors;
}

/**
 * 带缓存 + 容量上限的搜索
 * 缓存超过 maxSize 时清除最旧的条目
 */
async function skillsFindCached(query) {
  // 参数校验
  if (typeof query !== 'string' || query.trim().length === 0) {
    throw new Error('query 参数无效：必须是非空字符串');
  }

  const cacheKey = query.toLowerCase().trim();
  const cached = CONFIG.cache.findResults.get(cacheKey);

  if (cached && Date.now() - cached.timestamp < CONFIG.cache.ttl) {
    console.log(`📋 使用缓存: "${query}"`);
    return cached.results;
  }

  const results = await skillsFind(query);

  // 缓存超过上限时清除最旧的条目（淘汰最久未使用的）
  if (CONFIG.cache.findResults.size >= CONFIG.cache.maxSize) {
    let oldestKey = null;
    let oldestTime = Infinity;
    for (const [key, val] of CONFIG.cache.findResults) {
      if (val.timestamp < oldestTime) {
        oldestTime = val.timestamp;
        oldestKey = key;
      }
    }
    if (oldestKey) {
      CONFIG.cache.findResults.delete(oldestKey);
      console.log(`🗑️  缓存已满，淘汰最旧条目: "${oldestKey}"`);
    }
  }

  CONFIG.cache.findResults.set(cacheKey, {
    results,
    timestamp: Date.now()
  });

  return results;
}

// LOG_SCHEMA 从 constants.js 导入

/**
 * 统一日志格式（供外部 logDiscovery 调用）
 * @param {string} action - 操作类型（必须是 LOG_SCHEMA.ACTIONS 之一）
 * @param {Object} data - 日志数据
 * @returns {Object} 符合 Schema 的日志对象
 */
function buildLogEntry(action, data) {
  if (!LOG_SCHEMA.ACTIONS.includes(action)) {
    action = 'error';
  }
  return {
    schema: LOG_SCHEMA.VERSION,
    timestamp: new Date().toISOString(),
    action,
    ...data
  };
}

// ==================== 脱敏工具 ====================

/** 字段名匹配这些模式时，值会被遮蔽 */
const SANITIZE_REDACT_KEY_PATTERNS = [
  /\btoken\b/i,
  /\bsecret\b/i,
  /\bpassword\b/i,
  /\bapi[_-]?key\b/i,
  /\bcredential\b/i,
  /\bauthorization\b/i
];

/** 字符串值匹配这些模式时，整个值会被遮蔽 */
const SANITIZE_REDACT_VALUE_PATTERNS = [
  /\bBearer\s+[\w.-]+/i,
  /\bsk-[\w.-]+/,
  /ghp_[\w]+/,
  /===+\s*[A-Za-z]+\s*===+/ // OAUTH 授权页特征（精确匹配）
];

/**
 * 递归脱敏对象中的敏感信息（用于日志记录）
 *
 * 策略：只遮蔽真正敏感的字段（token/password/apiKey 等），
 * 业务字段（installs/confidence/domain/success 等）保留原值以保证日志可用性。
 */
function sanitize(obj, depth = 0) {
  if (depth > MAGIC.SANITIZE_MAX_DEPTH) return '[MAX_DEPTH]';
  if (obj === null || obj === undefined) return obj;
  if (typeof obj !== 'object') {
    if (typeof obj === 'string') {
      for (const p of SANITIZE_REDACT_VALUE_PATTERNS) {
        if (p.test(obj)) return '***';
      }
    }
    return obj;
  }
  if (Array.isArray(obj)) {
    return obj.map((item) => sanitize(item, depth + 1));
  }

  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    // 字段名匹配敏感模式 → 遮蔽值
    let isSensitiveKey = false;
    for (const p of SANITIZE_REDACT_KEY_PATTERNS) {
      if (p.test(key)) {
        isSensitiveKey = true;
        break;
      }
    }

    if (isSensitiveKey) {
      result[key] = '***';
    } else {
      result[key] = sanitize(value, depth + 1);
    }
  }
  return result;
}

// ==================== 管道函数 ====================

/**
 * 搜索 skill（带缓存 + 错误分类）
 */
async function searchSkills(query) {
  console.log(`💡 搜索: "${query}"`);

  let results;
  try {
    results = await skillsFindCached(query);
  } catch (error) {
    const errorCode =
      error.message.includes('not found') || error.message.includes('ENOENT')
        ? ERROR_CODES.CLI_NOT_FOUND
        : error.message.includes('ETIMEDOUT') || error.message.includes('network')
          ? ERROR_CODES.NETWORK_ERROR
          : ERROR_CODES.SEARCH_FAILED;

    return {
      success: false,
      stage: 'search',
      outcome: 'failed',
      reason: 'search_failed',
      errorCode,
      error: error.message,
      skill: null,
      candidates: [],
      message: '搜索失败，请稍后重试'
    };
  }

  console.log(`📋 找到 ${results.length} 个结果`);

  if (results.length === 0) {
    return {
      success: false,
      stage: 'search',
      outcome: 'failed',
      reason: 'no_results',
      errorCode: ERROR_CODES.NO_RESULTS,
      skill: null,
      candidates: [],
      message: '未找到相关 skill'
    };
  }

  return { success: true, results };
}

/**
 * 筛选并选择最佳 skill
 */
function filterAndSelect(results) {
  // skills.sh 上的 skill 均已验证，无需二次过滤，直接选最佳
  const best = selectBest(results);
  console.log(`⭐ 最佳: ${best.skill.fullName} (评分: ${Math.round(best.score)})`);

  return { success: true, best };
}

/**
 * 决定安装并执行
 */
async function resolveInstall(best, dryRun) {
  // 检查是否已安装
  try {
    const installed = await skillsList({ global: true });
    const isInstalled = installed.some(
      (s) => s.name === best.skill.skill || s.name === best.skill.fullName.replace(/[@/]/g, '-')
    );

    if (isInstalled) {
      return {
        success: true,
        stage: 'install',
        outcome: 'already_installed',
        reason: null,
        errorCode: null,
        alreadyInstalled: true,
        skill: best.skill,
        candidates: [],
        message: `✅ ${best.skill.fullName} 已安装`
      };
    }
  } catch (_e) {
    // 忽略检查错误，继续安装
  }

  const isClawhub = best.skill.source === 'clawhub';
  const installCmd = isClawhub ? 'clawhub install' : 'npx skills add';

  if (dryRun) {
    return {
      success: true,
      stage: 'install',
      outcome: 'dry_run',
      reason: null,
      errorCode: null,
      dryRun: true,
      skill: best.skill,
      validation: best.validation,
      candidates: [],
      message: `[模拟] 将安装 ${best.skill.fullName}${isClawhub ? ' (ClawHub)' : ''}`
    };
  }

  console.log(`📦 安装${isClawhub ? ' (ClawHub)' : ''}: ${best.skill.fullName}`);
  try {
    const installResult = isClawhub
      ? await clawhubAdd(best.skill.fullName)
      : await skillsAdd(best.skill.fullName, { global: true, yes: true });

    return {
      success: true,
      stage: 'install',
      outcome: 'installed',
      reason: null,
      errorCode: null,
      installed: true,
      skill: best.skill,
      validation: best.validation,
      result: installResult,
      candidates: [],
      message: `✅ 已自动安装 ${best.skill.fullName}${isClawhub ? ' (ClawHub)' : ''}`
    };
  } catch (error) {
    return {
      success: false,
      stage: 'install',
      outcome: 'failed',
      reason: 'install_failed',
      errorCode: ERROR_CODES.INSTALL_FAILED,
      skill: best.skill,
      error: error.message,
      fallback: `手动安装: ${installCmd} ${best.skill.fullName}`,
      candidates: [],
      message: `安装失败: ${error.message}`
    };
  }
}

// ==================== 主流程（编排） ====================

/**
 * 主流程：自动发现与安装
 *
 * 编排 analyzeNeed → searchSkills → filterAndSelect → resolveInstall
 */
async function autoDiscover(userInput, options = {}) {
  const validationErrors = validateParams(userInput, options);
  if (validationErrors.length > 0) {
    throw new Error(`参数校验失败: ${validationErrors.join('; ')}`);
  }

  const { dryRun = true } = options;

  console.log(`🔍 分析: "${userInput.substring(0, 50)}..."`);

  // 1. 分析需求
  const need = analyzeNeed(userInput);
  console.log(`📊 置信度: ${Math.round(need.confidence * 100)}%`);

  if (!need.shouldSearch) {
    return {
      success: false,
      stage: 'analyze',
      outcome: 'skipped',
      reason: 'confidence_too_low',
      errorCode: ERROR_CODES.CONFIDENCE_TOO_LOW,
      confidence: need.confidence,
      skill: null,
      candidates: [],
      message: '置信度不足，跳过自动发现'
    };
  }

  // 2. 搜索
  const searchResult = await searchSkills(need.query);
  if (!searchResult.success) return searchResult;

  // 3. 筛选
  const selectResult = filterAndSelect(searchResult.results);
  if (!selectResult.success) return selectResult;

  // 4. 安装
  return resolveInstall(selectResult.best, dryRun);
}

// ==================== 导出 ====================
module.exports = {
  // 公共 API
  analyzeNeed,
  validateParams,
  selectBest,
  autoDiscover,
  searchSkills,
  filterAndSelect,
  resolveInstall,
  // 内部工具（供 openclaw-hook 使用）
  buildLogEntry,
  sanitize,
  LOG_SCHEMA,
  skillsFindCached,
  CONFIG
};


path: constants.js
content: #!/usr/bin/env node
/**
 * 统一常量管理
 *
 * 所有 MAGIC 数字、CONFIG 配置、LOG_SCHEMA、ERROR_CODES 集中于此，
 * 消除跨文件重复定义。
 */

const os = require('os');
const path = require('path');

// ==================== 魔法数字常量 ====================
const MAGIC = {
  // 置信度与评分
  CONFIDENCE_THRESHOLD: 0.6,
  QUERY_MAX_LENGTH: 50,
  WEIGHT_EXPLICIT: 1.0,
  WEIGHT_HIGH: 0.9,
  WEIGHT_MEDIUM_HIGH: 0.85,
  WEIGHT_MEDIUM: 0.8,
  WEIGHT_DOMAIN: 0.7,
  NEGATIVE_MULTIPLIER: 0.5,
  DOMAIN_MIN_CONFIDENCE: 0.8,
  INSTALLS_SCORE_FACTOR: 10,
  TRUSTED_SCORE: 30,
  UNTRUSTED_SCORE: 10,
  MIN_INSTALLS_SCALE: 10000,

  // 缓存
  CACHE_TTL_MS: 10 * 60 * 1000, // 10 分钟
  CACHE_MAX_SIZE: 100,
  CACHE_CLEANUP_THRESHOLD: 100,

  // 重试
  MAX_RETRIES: 3,
  RETRY_DELAY_MS: 1000,
  CLI_TIMEOUT: 60000, // 命令超时

  // 日志
  LOG_VERSION: '1.0',
  MAX_LOG_ENTRIES: 100,

  // 垃圾清理
  TRASH_CLEAN_DAYS: 7,
  TRASH_CLEAN_INTERVAL_MS: 60 * 60 * 1000, // 1 小时

  // 脱敏
  SANITIZE_MAX_DEPTH: 10
};

// ==================== 错误码 ====================
const ERROR_CODES = {
  // 分析阶段
  CONFIDENCE_TOO_LOW: 'CONFIDENCE_TOO_LOW',
  INVALID_INPUT: 'INVALID_INPUT',
  // 搜索阶段
  CLI_NOT_FOUND: 'CLI_NOT_FOUND',
  NETWORK_ERROR: 'NETWORK_ERROR',
  PARSE_FAILED: 'PARSE_FAILED',
  SEARCH_FAILED: 'SEARCH_FAILED',
  NO_RESULTS: 'NO_RESULTS',
  // 筛选阶段
  // 安装阶段
  ALREADY_INSTALLED: 'ALREADY_INSTALLED',
  INSTALL_FAILED: 'INSTALL_FAILED',
  PERMISSION_DENIED: 'PERMISSION_DENIED',
  INVALID_SKILL_FORMAT: 'INVALID_SKILL_FORMAT'
};

// ==================== 日志 Schema ====================
const LOG_SCHEMA = {
  VERSION: MAGIC.LOG_VERSION,
  ACTIONS: ['discover', 'install', 'remove', 'error', 'cache_hit', 'cache_miss'],
  REQUIRED_FIELDS: ['timestamp', 'action', 'input']
};

// ==================== CLI 配置 ====================
const CLI_CONFIG = {
  maxRetries: MAGIC.MAX_RETRIES,
  retryDelay: MAGIC.RETRY_DELAY_MS,
  timeout: MAGIC.CLI_TIMEOUT
};

// ==================== 发现引擎配置 ====================
const DISCOVER_CONFIG = {
  confidenceThreshold: MAGIC.CONFIDENCE_THRESHOLD,
  trustedOwners: ['vercel-labs', 'anthropics', 'google-labs-code', 'microsoft', 'openai', 'openclaw'],
  cache: {
    ttl: MAGIC.CACHE_TTL_MS,
    maxSize: MAGIC.CACHE_MAX_SIZE
  }
};

// ==================== ClawHub 配置 ====================
const CLAWHUB_CONFIG = {
  apiBase: 'https://clawhub.ai',
  searchEndpoint: '/api/v1/search',
  timeout: 10000,
  maxResults: 10
};

// ==================== OpenClaw Hook 配置 ====================
const HOOK_CONFIG = {
  openclawDir: process.env.OPENCLAW_DIR || path.join(os.homedir(), '.openclaw'),
  maxLogEntries: MAGIC.MAX_LOG_ENTRIES,
  trashPath: process.env.TRASH_DIR || path.join(os.homedir(), '.openclaw', '.trash')
};

Object.defineProperty(HOOK_CONFIG, 'logPath', {
  get() {
    return path.join(this.openclawDir, 'logs', 'skill-discovery-v3.json');
  }
});

// ==================== 导出 ====================
module.exports = {
  MAGIC,
  ERROR_CODES,
  LOG_SCHEMA,
  CLI_CONFIG,
  DISCOVER_CONFIG,
  CLAWHUB_CONFIG,
  HOOK_CONFIG
};


path: eslint.config.js
content: const eslintConfigPrettier = require('eslint-config-prettier');

module.exports = [
  {
    files: ['**/*.js'],
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: 'commonjs',
      globals: {
        console: 'readonly',
        process: 'readonly',
        require: 'readonly',
        module: 'readonly',
        __dirname: 'readonly',
        __filename: 'readonly',
        exports: 'writable',
        setTimeout: 'readonly',
        clearTimeout: 'readonly',
        setInterval: 'readonly',
        clearInterval: 'readonly',
        Buffer: 'readonly',
        URL: 'readonly'
      }
    },
    rules: {
      'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
      'no-console': 'off',
      'prefer-const': 'error',
      'no-var': 'error'
    }
  },
  {
    files: ['test/**/*.js'],
    languageOptions: {
      globals: {
        describe: 'readonly',
        test: 'readonly',
        expect: 'readonly',
        it: 'readonly',
        beforeEach: 'readonly',
        afterEach: 'readonly',
        beforeAll: 'readonly',
        afterAll: 'readonly',
        jest: 'readonly'
      }
    }
  },
  eslintConfigPrettier
];


path: index.js
content: #!/usr/bin/env node
/**
 * Skill Discovery V3 - 主入口
 *
 * 完整功能：
 * - Phase 1: 基础封装（skills-cli.js）
 * - Phase 2: 自动化（auto-discover.js）
 * - Phase 3: 增强（openclaw-hook.js）
 */

const { skillsList, skillsFind, skillsAdd, skillsRemove } = require('./skills-cli');

const { analyzeNeed, autoDiscover } = require('./auto-discover');

const { onUserInput, generatePrompt, safeRemove, cleanTrash } = require('./openclaw-hook');

// ==================== 主函数 ====================

/**
 * 完整的 Skill Discovery 流程
 */
async function discoverAndInstall(userInput, options = {}) {
  const result = await autoDiscover(userInput, options);
  const prompt = generatePrompt(result);

  return {
    ...result,
    display: prompt
  };
}

// ==================== 导出 ====================
module.exports = {
  // Phase 1: CLI 封装
  skillsList,
  skillsFind,
  skillsAdd,
  skillsRemove,

  // Phase 2: 自动化
  analyzeNeed,
  autoDiscover,
  discoverAndInstall,

  // Phase 3: OpenClaw 集成
  onUserInput,
  generatePrompt,
  safeRemove,
  cleanTrash
};

// ==================== CLI 入口 ====================
if (require.main === module) {
  const args = process.argv.slice(2);

  // --help
  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
    console.log(`
Skill Discovery - 自动发现并安装 skills

用法:
  skill-discovery "<query>" [options]

选项:
  --dry-run     模拟运行，不实际安装
  --json        以 JSON 格式输出结果
  --verbose     显示详细调试信息
  --help, -h    显示帮助信息

示例:
  skill-discovery "帮我部署到 Vercel" --dry-run
  skill-discovery "find a skill for testing" --json
  skill-discovery "优化 React 性能"

演示:
  node examples/demo.js
`);
    process.exit(0);
  }

  const dryRun = args.includes('--dry-run');
  const jsonOutput = args.includes('--json');
  const verbose = args.includes('--verbose');
  const query = args.filter((a) => !a.startsWith('--')).join(' ');

  if (!query) {
    console.error('错误: 请提供查询内容');
    process.exit(1);
  }

  (async () => {
    try {
      if (verbose) {
        console.log(`查询: "${query}"`);
        console.log(`选项: dryRun=${dryRun}, json=${jsonOutput}`);
      }

      const result = await discoverAndInstall(query, { dryRun });

      if (jsonOutput) {
        // JSON 输出（去掉 display 字段避免冗余）
        const { display: _display, ...data } = result;
        console.log(JSON.stringify(data, null, 2));
      } else if (result.display) {
        console.log('\n' + result.display);
      } else {
        console.log('\n[未触发自动发现]');
        if (verbose && result.confidence !== undefined) {
          console.log(`置信度: ${Math.round(result.confidence * 100)}%`);
        }
      }
    } catch (error) {
      console.error(`错误: ${error.message}`);
      process.exit(1);
    }
  })();
}


path: openclaw-hook.js
content: #!/usr/bin/env node
/**
 * OpenClaw Hook - 与 OpenClaw 集成
 *
 * Phase 3: 增强（C）- OpenClaw 集成
 */

const { autoDiscover, buildLogEntry, sanitize } = require('./auto-discover');
const { skillsList, skillsRemove } = require('./skills-cli');
const { MAGIC, HOOK_CONFIG } = require('./constants');
const fs = require('fs').promises;
const path = require('path');

const CONFIG = HOOK_CONFIG;

// ==================== 日志系统 ====================

/**
 * 记录日志（使用统一 Schema + 脱敏）
 */
async function logDiscovery(action, data) {
  try {
    let logs = [];
    try {
      const content = await fs.readFile(CONFIG.logPath, 'utf8');
      logs = JSON.parse(content);
    } catch {
      // 文件不存在或解析失败
    }

    // 使用统一 Schema 格式 + 脱敏敏感信息
    const logEntry = buildLogEntry(action, sanitize(data));
    logs.push(logEntry);

    // 限制数量
    if (logs.length > CONFIG.maxLogEntries) {
      logs = logs.slice(-CONFIG.maxLogEntries);
    }

    await fs.mkdir(path.dirname(CONFIG.logPath), { recursive: true });
    await fs.writeFile(CONFIG.logPath, JSON.stringify(logs, null, 2));
  } catch (e) {
    console.error('日志记录失败:', e.message);
  }
}

// ==================== 卸载机制 ====================

/**
 * 安全卸载 skill
 */
async function safeRemove(skillRef) {
  try {
    // 1. 获取已安装列表
    const installed = await skillsList({ global: true });
    const target = installed.find(
      (s) => s.name === skillRef || s.name === skillRef.replace(/[@/]/g, '-')
    );

    if (!target) {
      return { success: false, reason: 'not_found' };
    }

    // 2. 备份到 trash
    await fs.mkdir(CONFIG.trashPath, { recursive: true });
    const backupName = `${target.name}_${Date.now()}`;
    const backupPath = path.join(CONFIG.trashPath, backupName);

    await fs.cp(target.path, backupPath, { recursive: true });

    // 3. 执行卸载
    await skillsRemove(skillRef, { global: true, yes: true });

    // 4. 记录日志
    await logDiscovery('remove', {
      skillRef,
      backupPath,
      success: true
    });

    return {
      success: true,
      message: `✅ 已卸载 ${skillRef}，备份在 ${backupPath}`
    };
  } catch (error) {
    await logDiscovery('remove', {
      skillRef,
      error: error.message,
      success: false
    });

    return {
      success: false,
      reason: 'remove_failed',
      error: error.message
    };
  }
}

// ==================== cleanTrash 缓存（避免频繁扫描）====================
let _lastCleanTime = 0;
let _lastCleanResult = null;

const TRASH_CLEAN_INTERVAL_MS = MAGIC.TRASH_CLEAN_INTERVAL_MS;
const SEVEN_DAYS_MS = MAGIC.TRASH_CLEAN_DAYS * 24 * 60 * 60 * 1000;

/**
 * 清理过期备份（带缓存，1 小时内不重复扫描）
 */
async function cleanTrash(options = {}) {
  const { force = false } = options;

  // 缓存检查：1 小时内不重复执行
  if (!force && Date.now() - _lastCleanTime < TRASH_CLEAN_INTERVAL_MS) {
    return { ..._lastCleanResult, cached: true };
  }

  try {
    const entries = await fs.readdir(CONFIG.trashPath);
    let cleaned = 0;

    for (const entry of entries) {
      const entryPath = path.join(CONFIG.trashPath, entry);
      const stat = await fs.stat(entryPath);

      if (Date.now() - stat.mtime.getTime() > SEVEN_DAYS_MS) {
        await fs.rm(entryPath, { recursive: true });
        cleaned++;
      }
    }

    _lastCleanTime = Date.now();
    _lastCleanResult = { cleaned };
    return { ..._lastCleanResult, cached: false };
  } catch {
    _lastCleanTime = Date.now();
    _lastCleanResult = { cleaned: 0 };
    return { ..._lastCleanResult, cached: false };
  }
}

// ==================== OpenClaw Hook ====================

/**
 * OpenClaw 输入处理钩子
 *
 * 在 OpenClaw 处理用户输入前调用
 *
 * 返回语义：
 * - handled: true  -> Skill Discovery 已接管，直接使用 result.response
 * - handled: false -> Skill Discovery 未处理，result.note 为提示信息
 *
 * result.alreadyInstalled 独立于 handled 存在，表示 skill 已安装（无需再次安装）
 */
async function onUserInput(userInput, _context = {}) {
  console.log(`[Skill Discovery] 处理输入: "${userInput.substring(0, 50)}..."`);

  // 1. 自动发现（默认推荐模式，不自动安装）
  const result = await autoDiscover(userInput, { dryRun: true });

  // 2. 记录日志
  await logDiscovery(result.installed ? 'install' : 'discover', {
    input: userInput,
    ...result
  });

  // 3. 返回处理建议（推荐模式：不自动安装，返回建议供用户确认）
  if (result.success && result.dryRun) {
    // 推荐模式 -> 未接管，返回推荐信息供用户确认
    const isClawhub = result.skill.source === 'clawhub';
    const installCmd = isClawhub
      ? `clawhub install ${result.skill.fullName}`
      : `npx skills add ${result.skill.fullName} -g`;
    return {
      handled: false,
      note: `💡 发现匹配 skill: ${result.skill.fullName}\n安装命令: ${installCmd}`,
      skill: result.skill,
      installCmd,
      alreadyInstalled: false
    };
  }

  if (result.success && result.installed) {
    // 显式安装成功 -> 已接管
    return {
      handled: true,
      response: `✅ 已安装 ${result.skill.fullName}\n\n现在可以使用相关功能了。`,
      skill: result.skill,
      alreadyInstalled: false
    };
  }

  if (result.success && result.alreadyInstalled) {
    // 已安装（无需处理）-> 未接管，但携带 note 便于 OpenClaw 补充说明
    return {
      handled: false,
      note: `✅ ${result.skill.fullName} 已安装，可直接使用`,
      skill: result.skill,
      alreadyInstalled: true
    };
  }

  if (!result.success && result.fallback) {
    // 安装失败 -> 未接管，携带 note 提示用户
    return {
      handled: false,
      note: `发现 skill 但安装失败，${result.fallback}`,
      skill: result.skill,
      alreadyInstalled: false
    };
  }

  if (!result.success && result.candidates) {
    // 有候选但未通过质量验证 -> 未接管
    return {
      handled: false,
      note: `⚠️ 未找到符合质量标准的 skill（可手动从 ${result.candidates.length} 个候选中挑选）`,
      candidates: result.candidates,
      alreadyInstalled: false
    };
  }

  // 未触发或搜索失败 -> 完全未处理
  return {
    handled: false,
    alreadyInstalled: false
  };
}

/**
 * 生成用户提示
 */
function generatePrompt(result) {
  if (result.success && result.installed) {
    return (
      `✅ 已自动安装 ${result.skill.fullName}\n` +
      `📊 安装量: ${result.skill.installs.toLocaleString()}\n` +
      `🔗 ${result.skill.url || 'https://skills.sh'}`
    );
  }

  if (result.success && result.alreadyInstalled) {
    return `✅ ${result.skill.fullName} 已安装，直接使用即可`;
  }

  if (!result.success && result.candidates) {
    let prompt = '⚠️ 未找到符合质量标准的 skill\n\n候选（需人工判断）:\n';
    result.candidates.slice(0, 3).forEach((c, i) => {
      prompt += `${i + 1}. ${c.fullName} (${c.installs} installs)\n`;
    });
    return prompt;
  }

  if (!result.success && result.fallback) {
    return `❌ 自动安装失败\n💡 ${result.fallback}`;
  }

  return null;
}

// ==================== 导出 ====================
module.exports = {
  onUserInput,
  generatePrompt,
  safeRemove,
  cleanTrash,
  logDiscovery,
  CONFIG
};


path: package.json
content: {
  "name": "skill-discovery",
  "version": "2.2.1",
  "description": "Automatically discover and install skills based on user intent",
  "main": "index.js",
  "bin": {
    "skill-discovery": "index.js"
  },
  "scripts": {
    "test": "jest",
    "test:unit": "jest test/unit --verbose",
    "test:integration": "jest test/integration --verbose",
    "test:coverage": "jest --coverage",
    "discover": "node index.js",
    "lint": "eslint *.js test/**/*.js",
    "lint:fix": "eslint *.js test/**/*.js --fix",
    "format": "prettier --write *.js test/**/*.js",
    "format:check": "prettier --check *.js test/**/*.js"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "keywords": [
    "openclaw",
    "skill",
    "agent",
    "discovery",
    "auto-install",
    "cli"
  ],
  "author": "",
  "license": "MIT-0",
  "repository": {
    "type": "git",
    "url": "https://github.com/caocong1/skill-discovery.git"
  },
  "homepage": "",
  "directories": {
    "test": "test"
  },
  "type": "commonjs",
  "devDependencies": {
    "eslint": "^10.1.0",
    "eslint-config-prettier": "^10.1.8",
    "jest": "^30.3.0",
    "prettier": "^3.8.1"
  }
}


path: skills-cli.js
content: #!/usr/bin/env node
/**
 * Skills CLI Wrapper - npx skills 命令封装
 *
 * 基于官方 npx skills CLI 的可靠封装
 */

const { exec } = require('child_process');
const https = require('https');
const util = require('util');
const { CLI_CONFIG, CLAWHUB_CONFIG } = require('./constants');

const execAsync = util.promisify(exec);

const CONFIG = CLI_CONFIG;

// ==================== 安全工具 ====================

/**
 * 转义 shell 参数，防止命令注入
 * @param {string} input - 用户输入
 * @returns {string} 转义后的安全字符串
 */
function shellEscape(input) {
  if (typeof input !== 'string') return '';
  // 使用单引号包裹并转义单引号（将 ' 替换为 '\''，即先结束单引号，
  // 插入转义单引号，再开始新单引号）
  return `'${input.replace(/'/g, "'\\''")}'`;
}

// ==================== 重试工具 ====================

/**
 * 带重试的执行
 * @param {Function} fn - 异步函数
 * @param {Object} options - 选项
 */
async function withRetry(fn, options = {}) {
  const { maxRetries = CONFIG.maxRetries, retryDelay = CONFIG.retryDelay } = options;

  let lastError;

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      // 如果是最后一次，不再重试
      if (i === maxRetries - 1) break;

      // 指数退避
      const delay = retryDelay * Math.pow(2, i);
      console.log(`⚠️  第 ${i + 1} 次尝试失败，${delay}ms 后重试...`);
      await sleep(delay);
    }
  }

  throw lastError;
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// ==================== 核心命令封装 ====================

/**
 * 列出已安装的 skills
 * @param {Object} options - 选项
 * @returns {Promise<Array>} 已安装 skill 列表
 */
async function skillsList(options = {}) {
  const { global = true, agent } = options;

  let cmd = 'npx skills list --json';
  if (global) cmd += ' -g';
  if (agent) cmd += ` -a ${shellEscape(agent)}`;

  return withRetry(async () => {
    const { stdout } = await execAsync(cmd, { timeout: CONFIG.timeout });

    try {
      const results = JSON.parse(stdout);
      return Array.isArray(results) ? results : [];
    } catch (e) {
      // JSON 解析失败时，尝试提取 JSON 片段
      const fallback = extractJson(stdout);
      if (fallback !== null) {
        return Array.isArray(fallback) ? fallback : [];
      }
      throw new Error(`解析 list 输出失败（JSON & 降级均失败）: ${e.message}`);
    }
  });
}

/**
 * 搜索 skills (skills.sh CLI)
 * 优先尝试 --json 获取结构化输出，降级为文本解析
 * @param {string} query - 搜索关键词
 * @returns {Promise<Array>} 搜索结果
 */
async function skillsFindCli(query) {
  // 参数校验
  if (typeof query !== 'string' || query.trim().length === 0) {
    return [];
  }

  return withRetry(async () => {
    // 优先尝试 JSON 输出
    try {
      const jsonCmd = `npx skills find ${shellEscape(query)} --json`;
      const { stdout } = await execAsync(jsonCmd, { timeout: CONFIG.timeout });
      const parsed = JSON.parse(stdout);
      if (Array.isArray(parsed) && parsed.length > 0) {
        return parsed;
      }
    } catch {
      // --json 不支持或解析失败，降级为文本解析
    }

    // 降级：文本输出 + 正则解析
    const cmd = `npx skills find ${shellEscape(query)}`;
    const { stdout } = await execAsync(cmd, { timeout: CONFIG.timeout });
    return parseFindOutput(stdout);
  });
}

// ==================== ClawHub 搜索 ====================

/**
 * HTTPS GET 请求
 * @param {string} url - 请求地址
 * @param {number} timeout - 超时时间（毫秒）
 * @returns {Promise<Object>} JSON 响应
 */
function httpsGet(url, timeout) {
  return new Promise((resolve, reject) => {
    const req = https.get(url, { timeout }, (res) => {
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          try {
            resolve(JSON.parse(data));
          } catch (e) {
            reject(new Error(`JSON parse failed: ${e.message}`));
          }
        } else {
          reject(new Error(`HTTP ${res.statusCode}`));
        }
      });
    });
    req.on('error', reject);
    req.on('timeout', () => {
      req.destroy();
      reject(new Error('Request timed out'));
    });
  });
}

/**
 * 搜索 ClawHub 注册表
 * @param {string} query - 搜索关键词
 * @returns {Promise<Array>} 标准化搜索结果
 */
async function clawhubSearch(query) {
  if (typeof query !== 'string' || query.trim().length === 0) {
    return [];
  }

  const url = `${CLAWHUB_CONFIG.apiBase}${CLAWHUB_CONFIG.searchEndpoint}?q=${encodeURIComponent(query)}&limit=${CLAWHUB_CONFIG.maxResults}&nonSuspiciousOnly=true`;

  try {
    const data = await httpsGet(url, CLAWHUB_CONFIG.timeout);

    if (!data.results || !Array.isArray(data.results)) {
      return [];
    }

    return data.results.map((r) => ({
      fullName: r.slug,
      owner: r.slug,
      repo: null,
      skill: r.slug,
      installs: 0,
      displayName: r.displayName || r.slug,
      summary: r.summary || null,
      version: r.version || null,
      url: `${CLAWHUB_CONFIG.apiBase}/skills/${r.slug}`,
      source: 'clawhub',
      score: r.score || 0
    }));
  } catch (error) {
    console.warn(`[clawhubSearch] ${error.message}`);
    return [];
  }
}

/**
 * 安装 ClawHub skill
 * @param {string} slug - skill slug
 * @returns {Promise<Object>} 安装结果
 */
async function clawhubAdd(slug) {
  if (typeof slug !== 'string' || slug.trim().length === 0) {
    throw new Error('slug 参数无效：必须是非空字符串');
  }

  const cmd = `clawhub install ${shellEscape(slug)}`;

  return withRetry(async () => {
    const { stdout, stderr } = await execAsync(cmd, {
      timeout: CONFIG.timeout * 2
    });

    return {
      success: true,
      skillRef: slug,
      output: stdout,
      warnings: stderr || null
    };
  });
}

/**
 * 合并去重搜索结果，skills.sh 优先
 * @param {Array} results - 混合来源的搜索结果
 * @returns {Array} 去重后的结果
 */
function deduplicateResults(results) {
  const seen = new Map();
  for (const result of results) {
    const key = (result.skill || result.fullName).toLowerCase();
    if (!seen.has(key) || (result.source === 'skills.sh' && seen.get(key).source !== 'skills.sh')) {
      seen.set(key, result);
    }
  }
  return Array.from(seen.values());
}

/**
 * 搜索 skills（合并 skills.sh + ClawHub）
 * @param {string} query - 搜索关键词
 * @returns {Promise<Array>} 合并去重后的搜索结果
 */
async function skillsFind(query) {
  if (typeof query !== 'string' || query.trim().length === 0) {
    return [];
  }

  const [cliResult, clawhubResult] = await Promise.allSettled([
    skillsFindCli(query),
    clawhubSearch(query)
  ]);

  const results = [
    ...(cliResult.status === 'fulfilled' ? cliResult.value : []),
    ...(clawhubResult.status === 'fulfilled' ? clawhubResult.value : [])
  ];

  return deduplicateResults(results);
}

/**
 * 去除 ANSI 转义码
 * @param {string} str - 带 ANSI 码的字符串
 * @returns {string} 干净的字符串
 */
function stripAnsi(str) {
  // 匹配 ANSI 转义序列: \x1b[...m
  return str.replace(/\x1b\[[0-9;]*m/g, '');
}

/**
 * 从任意文本中提取 JSON 数组（降级解析）
 * @param {string} text - 原始文本
 * @returns {Array|null} 解析后的数组，或 null
 */
function extractJson(text) {
  // 查找 [ ... ] JSON 数组边界
  const openIdx = text.indexOf('[');
  const closeIdx = text.lastIndexOf(']');
  if (openIdx === -1 || closeIdx === -1 || closeIdx <= openIdx) return null;
  try {
    const jsonStr = text.slice(openIdx, closeIdx + 1);
    return JSON.parse(jsonStr);
  } catch {
    return null;
  }
}

/**
 * 解析 npx skills find 的输出
 *
 * 使用多个正则模式提升容错性：
 * - 主模式: owner/repo@skill  N installs
 * - 备用模式: 更宽松，允许额外前缀（emoji等）
 *
 * @param {string} output - ANSI 文本输出
 * @returns {Array} 解析后的结果（空数组 + console.warn 表示解析失败）
 */
function parseFindOutput(output) {
  if (!output || typeof output !== 'string') {
    return [];
  }

  const results = [];
  const lines = output.split('\n').map(stripAnsi);

  // 主模式: 行首 owner（字母数字开头）/repo@skill
  const primaryPattern = /^([a-zA-Z0-9][\w-]*)\/(\S+?)@(\S+?)\s+([\d.]+[KkMm]?)\s+installs/i;
  // 备用模式: 允许行首有额外字符（emoji、序号等），owner 必须以字母数字开头
  const fallbackPattern = /([a-zA-Z0-9][\w-]*?)\/(\S+?)@(\S+?)\s+([\d.]+[KkMm]?)\s+installs/i;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i].trim();
    const match = line.match(primaryPattern) || line.match(fallbackPattern);

    if (match) {
      const [_, owner, repo, skill, installsStr] = match;

      // 解析安装数（处理 K/M 后缀，支持小数）
      let installs = parseFloat(installsStr.replace(/[KkMm]/g, ''));
      if (/[Kk]/.test(installsStr)) installs *= 1000;
      if (/[Mm]/.test(installsStr)) installs *= 1000000;

      // 下一行通常是 URL
      const urlLine = lines[i + 1] || '';
      const urlMatch = stripAnsi(urlLine).match(/https:\/\/skills\.sh\/.+/);

      results.push({
        fullName: `${owner}/${repo}@${skill}`,
        owner,
        repo,
        skill,
        installs: Math.floor(installs),
        url: urlMatch ? urlMatch[0].trim().replace(/[^\x00-\x7F]/g, '') : null,
        source: 'skills.sh'
      });
    }
  }

  // 有内容但解析为空 → 可能格式已变化，输出警告
  if (results.length === 0 && output.trim().length > 0) {
    console.warn('[parseFindOutput] 解析结果为空，CLI 输出格式可能已变化');
  }

  return results;
}

/**
 * 安装 skill
 * @param {string} skillRef - skill 引用 (owner/repo@skill)
 * @param {Object} options - 选项
 * @returns {Promise<Object>} 安装结果
 */
async function skillsAdd(skillRef, options = {}) {
  // 参数校验
  if (typeof skillRef !== 'string' || skillRef.trim().length === 0) {
    throw new Error('skillRef 参数无效：必须是非空字符串');
  }

  const { global = false, yes = false } = options;

  let cmd = `npx skills add ${shellEscape(skillRef)}`;
  if (global) cmd += ' -g';
  if (yes) cmd += ' -y';

  return withRetry(async () => {
    const { stdout, stderr } = await execAsync(cmd, {
      timeout: CONFIG.timeout * 2 // 安装可能较慢
    });

    return {
      success: true,
      skillRef,
      output: stdout,
      warnings: stderr || null
    };
  });
}

/**
 * 移除 skill
 * @param {string} skillRef - skill 引用
 * @param {Object} options - 选项
 */
async function skillsRemove(skillRef, options = {}) {
  const { global = true, yes = true } = options;

  let cmd = `npx skills remove ${shellEscape(skillRef)}`;
  if (global) cmd += ' -g';
  if (yes) cmd += ' -y';

  return withRetry(async () => {
    const { stdout } = await execAsync(cmd, { timeout: CONFIG.timeout });
    return { success: true, skillRef, output: stdout };
  });
}

/**
 * 检查更新
 */
async function skillsCheck() {
  const cmd = 'npx skills check';

  return withRetry(async () => {
    const { stdout } = await execAsync(cmd, { timeout: CONFIG.timeout });
    return { output: stdout };
  });
}

/**
 * 更新所有 skills
 */
async function skillsUpdate() {
  const cmd = 'npx skills update';

  return withRetry(async () => {
    const { stdout } = await execAsync(cmd, { timeout: CONFIG.timeout * 2 });
    return { output: stdout };
  });
}

// ==================== 导出 ====================
module.exports = {
  skillsList,
  skillsFind,
  skillsFindCli,
  skillsAdd,
  skillsRemove,
  skillsCheck,
  skillsUpdate,
  clawhubSearch,
  clawhubAdd,
  parseFindOutput,
  extractJson,
  shellEscape,
  withRetry,
  deduplicateResults,
  CONFIG,
  stripAnsi
};


path: test-cli.js
content: #!/usr/bin/env node
/**
 * 测试 Skills CLI Wrapper
 */

const { skillsList, skillsFind, parseFindOutput } = require('./skills-cli');

async function main() {
  console.log('=== Skills CLI Wrapper 测试 ===\n');

  // 测试 1: list
  console.log('1. 测试 skillsList()');
  console.log('-'.repeat(50));
  try {
    const installed = await skillsList({ global: true });
    console.log(`✅ 找到 ${installed.length} 个已安装 skill:`);
    installed.forEach((s) => console.log(`   - ${s.name}`));
  } catch (e) {
    console.error('❌ 失败:', e.message);
  }

  // 测试 2: find
  console.log('\n2. 测试 skillsFind("react")');
  console.log('-'.repeat(50));
  try {
    const results = await skillsFind('react');
    console.log(`✅ 找到 ${results.length} 个结果:`);
    results.slice(0, 3).forEach((r) => {
      console.log(`   - ${r.fullName} (${r.installs} installs)`);
    });
  } catch (e) {
    console.error('❌ 失败:', e.message);
  }

  // 测试 3: parseFindOutput
  console.log('\n3. 测试 parseFindOutput()');
  console.log('-'.repeat(50));
  const sampleOutput = `
[38;5;145mvercel-labs/json-render@react[0m [36m728 installs[0m
[38;5;102m└ https://skills.sh/vercel-labs/json-render/react[0m
[38;5;145mvercel-labs/agent-skills@best-practices[0m [36m1520 installs[0m
`;
  const parsed = parseFindOutput(sampleOutput);
  console.log(`✅ 解析 ${parsed.length} 条:`);
  parsed.forEach((p) => console.log(`   - ${p.fullName}: ${p.installs}`));
}

main().catch(console.error);


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