forager.js 4 KB
/**
 * Forager — 扫描 Skill 并写入数据库
 *
 * 支持扫描单个 Skill(per-skill 隔离)或全部 Skill。
 *
 * 用法:
 *   forageOneSkill('code-review')    # 只扫描一个 Skill
 *   forageSkills()                    # 扫描全部 Skill
 */
const path = require('path');
const fs = require('fs');
const { serializeSkillToGenome, scanSkillsDirectory } = require('./genome/serializer');
const evolutionQueries = require('./queries').evolutionQueries;

const DEFAULT_SKILLS_DIR = path.join(require('os').homedir(), '.gemini/antigravity/skills');

/**
 * 将一个基因组写入数据库
 */
async function persistGenome(genome, runId) {
  const row = await evolutionQueries.insertGenome({
    runId,
    skillName: genome.meta.name,
    generation: 0,
    parentIds: [],
    mentalModels: genome.mentalModels,
    patternLibrary: genome.patternLibrary,
    judgment: genome.judgment,
    toolFluency: genome.toolFluency,
    taste: genome.taste,
    scarTissue: genome.scarTissue,
    isElite: false,
  });

  const total = [
    genome.mentalModels, genome.patternLibrary, genome.judgment,
    genome.toolFluency, genome.taste, genome.scarTissue,
  ].reduce((sum, dim) => {
    return sum + Object.values(dim).reduce((s, v) =>
      s + (Array.isArray(v) ? v.length : (typeof v === 'object' ? Object.keys(v).length : 1)), 0);
  }, 0);

  return { name: genome.meta.name, id: row.id, total };
}

/**
 * 扫描单个 Skill 并写入数据库
 *
 * 每个 Skill 创建独立的 Run。
 *
 * @param {string} skillName - Skill 名称 (目录名)
 * @param {string} skillsDir - Skills 根目录
 * @returns {object|null} { name, id, total } 或 null
 */
async function forageOneSkill(skillName, skillsDir = DEFAULT_SKILLS_DIR) {
  const skillMd = path.join(skillsDir, skillName, 'SKILL.md');

  if (!fs.existsSync(skillMd)) {
    console.log(`❌ 未找到: ${skillMd}`);
    return null;
  }

  console.log(`\n🔍 Forage: ${skillName}`);
  const genome = serializeSkillToGenome(skillMd);

  // 为该 Skill 创建独立的 Run
  const run = await evolutionQueries.createRun(skillName, {
    maxGenerations: 0,
    populationSize: 1,
    description: `${skillName} 进化 - ${new Date().toISOString()}`,
  });

  const result = await persistGenome(genome, run.id);
  console.log(`  ✅ ${result.name.padEnd(25)} ${String(result.total).padStart(3)} 条目 → ${result.id.slice(0, 8)}`);
  console.log(`  📦 Run: ${run.id}\n`);

  return result;
}

/**
 * 扫描全部 Skill(兼容旧调用)
 */
async function forageSkills(skillsDir = DEFAULT_SKILLS_DIR) {
  console.log(`\n🔍 扫描 Skills 目录: ${skillsDir}\n`);

  const genomes = scanSkillsDirectory(skillsDir);
  if (genomes.length === 0) {
    console.log('⚠️  没有找到任何 Skill');
    return [];
  }

  const results = [];
  for (const genome of genomes) {
    try {
      const run = await evolutionQueries.createRun(genome.meta.name, {
        maxGenerations: 0,
        populationSize: 1,
        description: `${genome.meta.name} 进化 - ${new Date().toISOString()}`,
      });
      const result = await persistGenome(genome, run.id);
      console.log(`  ✅ ${result.name.padEnd(25)} ${String(result.total).padStart(3)} 条目 → Run ${run.id.slice(0, 8)}`);
      results.push(result);
    } catch (err) {
      console.error(`  ❌ ${genome.meta.name}: ${err.message}`);
    }
  }

  console.log(`\n✅ 入库完成: ${results.length}/${genomes.length} 个 Skill`);
  return results;
}

// CLI
if (require.main === module) {
  const args = process.argv.slice(2);
  const skillName = args.find(a => !a.startsWith('--'));
  const skillsDir = args.find(a => a.startsWith('--dir='))?.split('=')[1] || DEFAULT_SKILLS_DIR;

  if (skillName) {
    forageOneSkill(skillName, skillsDir)
      .then(() => process.exit(0))
      .catch(err => { console.error('Fatal:', err); process.exit(1); });
  } else {
    forageSkills(skillsDir)
      .then(() => process.exit(0))
      .catch(err => { console.error('Fatal:', err); process.exit(1); });
  }
}

module.exports = { forageOneSkill, forageSkills };