import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
//#region extensions/crypto/src/services/skill-registry.ts
/**
* Skill Registry — unified index of static + learned skills with keyword matching.
*
* Solves the "LLM didn't load the right skill" problem by:
* 1. Indexing ALL skills (42 static + N learned) with name, description, keywords
* 2. Matching user messages against keywords → returning full skill content
* 3. Providing a single lookup surface for /skills command + prompt-builder
*
* Static skills:  extensions/crypto/skills/<name>/SKILL.md
* Learned skills: ~/.openclawnch/learned-skills/<name>/SKILL.md
*/
const __selfDir = dirname(fileURLToPath(import.meta.url));
const EXTENSION_ROOT = existsSync(join(__selfDir, "..", "..", "..", "skills")) ? join(__selfDir, "..", "..", "..") : join(__selfDir, "..", "..");
const OPENCLAWNCH_DIR = join(process.env.HOME ?? "", ".openclawnch");
const DISABLED_FILE = join(OPENCLAWNCH_DIR, "disabled-skills.json");
function loadDisabledSet() {
	try {
		if (existsSync(DISABLED_FILE)) {
			const data = JSON.parse(readFileSync(DISABLED_FILE, "utf8"));
			if (Array.isArray(data)) return new Set(data);
		}
	} catch {}
	return /* @__PURE__ */ new Set();
}
function saveDisabledSet(set) {
	try {
		if (!existsSync(OPENCLAWNCH_DIR)) mkdirSync(OPENCLAWNCH_DIR, { recursive: true });
		writeFileSync(DISABLED_FILE, JSON.stringify([...set], null, 2), "utf8");
	} catch {}
}
function parseFrontmatter(content) {
	if (!content.startsWith("---")) {
		const lines = content.split("\n");
		let name;
		let description;
		for (const line of lines) if (!name && line.startsWith("# ")) name = line.slice(2).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
		else if (!description && name && line.trim().length > 0 && !line.startsWith("#")) {
			description = line.trim().slice(0, 200);
			break;
		}
		return {
			name,
			description,
			requiresEnv: [],
			rest: content
		};
	}
	const endIdx = content.indexOf("---", 3);
	if (endIdx === -1) return {
		requiresEnv: [],
		rest: content
	};
	const yaml = content.slice(3, endIdx).trim();
	const rest = content.slice(endIdx + 3).trim();
	let name;
	let description;
	let requiresEnv = [];
	for (const line of yaml.split("\n")) {
		const nameMatch = line.match(/^name:\s*["']?(.+?)["']?\s*$/);
		if (nameMatch) name = nameMatch[1];
		const descMatch = line.match(/^description:\s*["']?(.+?)["']?\s*$/);
		if (descMatch) description = descMatch[1];
		const metaMatch = line.match(/^metadata:\s*(\{.+\})\s*$/);
		if (metaMatch) try {
			const envArr = JSON.parse(metaMatch[1])?.openclaw?.requires?.env;
			if (Array.isArray(envArr)) requiresEnv = envArr.filter((v) => typeof v === "string");
		} catch {}
	}
	return {
		name,
		description,
		requiresEnv,
		rest
	};
}
/** Extract keywords from name + description for matching. */
function extractKeywords(name, description) {
	const text = `${name.replace(/-/g, " ")} ${description}`.toLowerCase();
	const STOPWORDS = new Set([
		"a",
		"an",
		"the",
		"is",
		"are",
		"was",
		"be",
		"to",
		"of",
		"and",
		"or",
		"in",
		"on",
		"for",
		"with",
		"by",
		"from",
		"at",
		"as",
		"it",
		"its",
		"this",
		"that",
		"use",
		"via",
		"any",
		"all",
		"can",
		"your",
		"you",
		"more",
		"also",
		"into"
	]);
	const words = text.split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
	return [...new Set(words)];
}
function scanSkillDir(dir, source, disabledSet) {
	const entries = [];
	if (!existsSync(dir)) return entries;
	try {
		for (const entry of readdirSync(dir)) {
			const entryPath = join(dir, entry);
			if (!statSync(entryPath).isDirectory()) continue;
			const skillPath = join(entryPath, "SKILL.md");
			if (!existsSync(skillPath)) continue;
			try {
				const content = readFileSync(skillPath, "utf8");
				if (content.length > 256e3) continue;
				const fm = parseFrontmatter(content);
				const name = fm.name ?? entry;
				const description = fm.description ?? "";
				entries.push({
					name,
					description,
					source,
					path: skillPath,
					keywords: extractKeywords(name, description),
					requiresEnv: fm.requiresEnv,
					disabled: disabledSet.has(name)
				});
			} catch {}
		}
	} catch {}
	return entries;
}
var SkillRegistry = class {
	skills = /* @__PURE__ */ new Map();
	disabledSet;
	staticDir;
	learnedDir;
	lastScan = 0;
	scanIntervalMs = 6e4;
	constructor(opts) {
		this.staticDir = opts?.staticDir ?? join(EXTENSION_ROOT, "skills");
		this.learnedDir = opts?.learnedDir ?? join(process.env.HOME ?? "", ".openclawnch", "learned-skills");
		this.disabledSet = loadDisabledSet();
		this.scan();
	}
	/** Re-scan both directories for skills. */
	scan() {
		this.skills.clear();
		this.disabledSet = loadDisabledSet();
		const staticSkills = scanSkillDir(this.staticDir, "static", this.disabledSet);
		const learnedSkills = scanSkillDir(this.learnedDir, "learned", this.disabledSet);
		for (const s of staticSkills) this.skills.set(s.name, s);
		for (const s of learnedSkills) this.skills.set(s.name, s);
		this.lastScan = Date.now();
	}
	ensureFresh() {
		if (Date.now() - this.lastScan > this.scanIntervalMs) this.scan();
	}
	/** Get a skill by exact name. */
	get(name) {
		this.ensureFresh();
		return this.skills.get(name) ?? null;
	}
	/** List all skills sorted by name. */
	list(opts) {
		this.ensureFresh();
		let all = Array.from(this.skills.values());
		if (opts?.source) all = all.filter((s) => s.source === opts.source);
		if (!opts?.includeDisabled) all = all.filter((s) => !s.disabled);
		return all.sort((a, b) => a.name.localeCompare(b.name));
	}
	/** List all skills including disabled (for /skills command). */
	listAll() {
		this.ensureFresh();
		return Array.from(this.skills.values()).sort((a, b) => a.name.localeCompare(b.name));
	}
	/** Read the full SKILL.md content for a skill. */
	readContent(name) {
		const skill = this.get(name);
		if (!skill) return null;
		if (skill._content === void 0) try {
			skill._content = readFileSync(skill.path, "utf8");
		} catch {
			return null;
		}
		return skill._content;
	}
	/**
	* Match user message against skill keywords.
	* Returns skills sorted by match score (descending), filtered to score >= minScore.
	* Only matches enabled skills.
	*/
	match(message, opts) {
		this.ensureFresh();
		const minScore = opts?.minScore ?? 2;
		const maxResults = opts?.maxResults ?? 3;
		const msgLower = message.toLowerCase();
		const msgTokens = msgLower.split(/[^a-z0-9]+/).filter((w) => w.length > 2);
		const msgTokenSet = new Set(msgTokens);
		const matches = [];
		for (const skill of this.skills.values()) {
			if (skill.disabled) continue;
			let score = 0;
			if (msgLower.includes(skill.name.replace(/-/g, " ")) || msgLower.includes(skill.name)) score += 5;
			for (const kw of skill.keywords) {
				if (msgTokenSet.has(kw)) score += 1;
				if (kw.length > 3 && msgLower.includes(kw)) score += 1;
			}
			if (score >= minScore) matches.push({
				skill,
				score
			});
		}
		matches.sort((a, b) => b.score - a.score);
		return matches.slice(0, maxResults);
	}
	/** Build a compact index for prompt injection (name + description, one line each). */
	buildIndex() {
		const skills = this.list();
		if (skills.length === 0) return "";
		const lines = [
			`## Crypto Skills (${skills.length} available)`,
			"If a user request matches a skill below, load it with `/skills <name>` or `skill_evolve(action: \"view\", name: \"...\")` for full instructions.",
			""
		];
		for (const s of skills) {
			const tag = s.source === "learned" ? " (learned)" : "";
			lines.push(`- **${s.name}**${tag}: ${s.description.slice(0, 120)}`);
		}
		return lines.join("\n");
	}
	/** Disable a skill (won't appear in prompts or matching). */
	disable(name) {
		const skill = this.skills.get(name);
		if (!skill) return false;
		skill.disabled = true;
		this.disabledSet.add(name);
		saveDisabledSet(this.disabledSet);
		return true;
	}
	/** Re-enable a disabled skill. */
	enable(name) {
		const skill = this.skills.get(name);
		if (!skill) return false;
		skill.disabled = false;
		this.disabledSet.delete(name);
		saveDisabledSet(this.disabledSet);
		return true;
	}
	/** Check which required env vars are missing for a skill. */
	missingEnv(skill) {
		return skill.requiresEnv.filter((key) => !process.env[key]);
	}
	/** Get total count (enabled only by default). */
	get size() {
		this.ensureFresh();
		return this.list().length;
	}
	/** Get total count including disabled. */
	get totalSize() {
		this.ensureFresh();
		return this.skills.size;
	}
};
let instance = null;
function getSkillRegistry(opts) {
	if (!instance) instance = new SkillRegistry(opts);
	return instance;
}
function resetSkillRegistry() {
	instance = null;
}
//#endregion
export { SkillRegistry, getSkillRegistry, resetSkillRegistry };

//# sourceMappingURL=skill-registry.mjs.map