import { a as AgentToolNotAllowedError, l as errorMessage } from "./errors-DkR6GPJw.js";
import { basename, dirname, join, resolve } from "node:path";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { realpath } from "node:fs/promises";
//#region src/xml.ts
/**
* Minimal XML helpers used by catalog/result builders that emit
* model-facing pseudo-XML (skills catalog, searchable_tools, tool_search
* results, skill_content wrappers).
*
* Scope: attribute / text-node escaping only — we do NOT emit a full XML
* document, the model sees these strings as free-form text. Centralised
* here so the four near-identical copies in `agent.ts`, `skills/catalog.ts`,
* `tools/skills-use.ts`, and `tools/tool-search.ts` don't drift.
*/
const RE_AMP = /&/g;
const RE_LT = /</g;
const RE_GT = />/g;
const RE_QUOT = /"/g;
function escapeXml(str) {
	return str.replace(RE_AMP, "&amp;").replace(RE_LT, "&lt;").replace(RE_GT, "&gt;").replace(RE_QUOT, "&quot;");
}
//#endregion
//#region src/skills/activation.ts
function createSkillActivationState(options = {}) {
	const byName = /* @__PURE__ */ new Map();
	const maxActive = typeof options.maxActive === "number" && options.maxActive > 0 ? options.maxActive : void 0;
	return {
		active() {
			return [...byName.values()];
		},
		isActive(name) {
			return byName.has(name);
		},
		get(name) {
			return byName.get(name);
		},
		activate(skill, via) {
			if (byName.has(skill.name)) return "already-active";
			if (maxActive !== void 0 && byName.size >= maxActive) return "cap-reached";
			byName.set(skill.name, {
				skill,
				activatedAt: Date.now(),
				activatedVia: via
			});
			return "ok";
		},
		deactivate(name) {
			const existing = byName.get(name);
			if (!existing) return void 0;
			byName.delete(name);
			return existing;
		},
		clear() {
			const snapshot = [...byName.values()];
			byName.clear();
			return snapshot;
		}
	};
}
//#endregion
//#region src/skills/validate.ts
const NAME_MAX = 64;
const DESCRIPTION_MAX = 1024;
const COMPATIBILITY_MAX = 500;
const SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
const CONSECUTIVE_HYPHENS_RE = /--/;
const ALLOWED_TOOL_PATTERN_RE = /^([\w-]+)(?:\(([^)]*)\))?$/;
const ABS_WINDOWS_PATH_RE = /^[a-z]:[\\/]/i;
const PATH_SEPARATOR_RE = /[\\/]/;
const TRAILING_SLASHES_RE = /\/+$/;
/**
* Validate a skill name per the spec:
* - 1–64 characters
* - Lowercase alphanumeric + hyphens only
* - Must not start or end with a hyphen
* - Must not contain consecutive hyphens
*
* The parent-directory match is validated separately (it requires knowing the
* skill's `location`, which this function does not).
*/
function validateSkillName(name) {
	if (typeof name !== "string") return false;
	if (name.length < 1 || name.length > NAME_MAX) return false;
	if (CONSECUTIVE_HYPHENS_RE.test(name)) return false;
	return SKILL_NAME_RE.test(name);
}
/**
* Strict validation for a skill that is about to be authored / written to disk.
* Rejects anything that would violate the spec. Use the lenient parser path
* (`parseSkillFile`) for loading third-party skills.
*/
function validateSkillForWrite(skill) {
	const errors = [];
	if (!validateSkillName(skill.name)) errors.push({
		code: "invalid-name",
		message: `Skill name "${skill.name}" must be 1-64 chars, lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens.`,
		field: "name"
	});
	if (skill.location) {
		const dirName = basename(skill.baseDir ?? "");
		if (dirName && dirName !== skill.name) errors.push({
			code: "name-mismatch-directory",
			message: `Skill name "${skill.name}" must match parent directory name "${dirName}".`,
			field: "name"
		});
	}
	if (typeof skill.description !== "string" || skill.description.length < 1) errors.push({
		code: "missing-description",
		message: "Skill description is required (non-empty).",
		field: "description"
	});
	else if (skill.description.length > DESCRIPTION_MAX) errors.push({
		code: "description-too-long",
		message: `Skill description must be at most ${DESCRIPTION_MAX} characters (got ${skill.description.length}).`,
		field: "description"
	});
	if (skill.compatibility !== void 0) {
		if (typeof skill.compatibility !== "string" || skill.compatibility.length === 0) errors.push({
			code: "invalid-compatibility",
			message: "Compatibility must be a non-empty string when provided.",
			field: "compatibility"
		});
		else if (skill.compatibility.length > COMPATIBILITY_MAX) errors.push({
			code: "compatibility-too-long",
			message: `Compatibility must be at most ${COMPATIBILITY_MAX} characters (got ${skill.compatibility.length}).`,
			field: "compatibility"
		});
	}
	if (skill.metadata) {
		for (const [key, value] of Object.entries(skill.metadata)) if (typeof value !== "string") errors.push({
			code: "invalid-metadata-value",
			message: `Metadata value for "${key}" must be a string (spec: "A map from string keys to string values").`,
			field: "metadata"
		});
	}
	if (skill.allowedTools) {
		for (const pattern of skill.allowedTools) if (!ALLOWED_TOOL_PATTERN_RE.test(pattern)) errors.push({
			code: "invalid-allowed-tool-pattern",
			message: `Allowed-tools entry "${pattern}" is not a recognized pattern (expected "ToolName" or "ToolName(arg:*)").`,
			field: "allowed-tools"
		});
	}
	return {
		valid: errors.length === 0,
		errors
	};
}
/**
* Validate that `relPath` stays inside `baseDir` when resolved.
*
* Rejects:
* - Absolute paths (`/etc/passwd`, `C:\…`)
* - Parent traversal (`..` segments that escape baseDir)
* - Null-byte tricks
*
* Returns `{ valid: true, absolutePath }` on success or `{ valid: false, error }`
* with a human-readable reason.
*/
function validateResourcePath(relPath, baseDir) {
	if (typeof relPath !== "string" || relPath.length === 0) return {
		valid: false,
		error: "Resource path must be a non-empty string."
	};
	if (relPath.includes("\0")) return {
		valid: false,
		error: "Resource path contains a null byte."
	};
	if (relPath.startsWith("/") || ABS_WINDOWS_PATH_RE.test(relPath)) return {
		valid: false,
		error: `Absolute paths are not allowed ("${relPath}").`
	};
	const segments = [];
	for (const segment of relPath.split(PATH_SEPARATOR_RE)) {
		if (segment === "" || segment === ".") continue;
		if (segment === "..") {
			if (segments.length === 0) return {
				valid: false,
				error: `Resource path "${relPath}" escapes the skill directory.`
			};
			segments.pop();
			continue;
		}
		segments.push(segment);
	}
	if (segments.length === 0) return {
		valid: false,
		error: "Resource path resolves to the skill root itself."
	};
	return {
		valid: true,
		absolutePath: `${baseDir.replace(TRAILING_SLASHES_RE, "")}/${segments.join("/")}`
	};
}
/**
* Symlink-hardened variant of {@link validateResourcePath}: after the lexical
* check passes, resolve both the target and `baseDir` through `realpath` and
* re-confirm the target still lives under the skill directory. A symlink
* placed inside the skill dir pointing at e.g. `/etc/passwd` passes the
* lexical check but fails here.
*
* Falls back to the lexical verdict when `realpath` fails (target or baseDir
* not present on the LOCAL filesystem) — the common causes are a remote
* execution context (docker/sandbox, where the local realpath is meaningless
* and the remote side resolves the path itself) or a not-yet-existing file.
* The returned `absolutePath` is always the lexical one: it's what the
* execution context resolves; realpath here is verification only.
*/
async function validateResourcePathReal(relPath, baseDir) {
	const lexical = validateResourcePath(relPath, baseDir);
	if (!lexical.valid) return lexical;
	try {
		const [realBase, realTarget] = await Promise.all([realpath(baseDir), realpath(lexical.absolutePath)]);
		const prefix = realBase.replace(TRAILING_SLASHES_RE, "");
		if (realTarget !== prefix && !realTarget.startsWith(`${prefix}/`)) return {
			valid: false,
			error: `Resource path "${relPath}" escapes the skill directory via a symlink.`
		};
	} catch {}
	return lexical;
}
/**
* Parse a single `allowed-tools` entry into its tool name + optional argument pattern.
*
* Examples:
* - `Read` → `{ tool: 'Read' }`
* - `Bash(git:*)` → `{ tool: 'Bash', argPrefix: 'git' }`
*/
function parseAllowedToolPattern(entry) {
	const m = entry.trim().match(ALLOWED_TOOL_PATTERN_RE);
	if (!m) return null;
	const tool = m[1];
	const arg = m[2];
	if (!arg) return { tool };
	if (arg.endsWith(":*")) return {
		tool,
		argPrefix: arg.slice(0, -2)
	};
	return {
		tool,
		argPrefix: arg
	};
}
/**
* Check whether a tool call (identified by its wire/displayName and argument input)
* matches a skill's allow-list entry.
*
* Matching rules (Zidane's interpretation of the spec's unspecified syntax):
* - Exact match: displayName === pattern (no parens).
* - Prefix match: for `Tool(arg:*)`, displayName === 'Tool' AND **any** string
*   value in the input object starts with `arg`. This is intentionally
*   permissive: it doesn't depend on a convention about which property carries
*   the "command" (schemas vary across tools), and for the common
*   `shell({ command: 'git …' })` shape it behaves identically to a "primary
*   string" rule.
* - For tools whose inputs carry no string values, the arg-match returns false.
*/
function matchesAllowedTool(displayName, input, pattern) {
	const parsed = parseAllowedToolPattern(pattern);
	if (!parsed) return false;
	if (parsed.tool !== displayName) return false;
	if (parsed.argPrefix === void 0) return true;
	for (const value of Object.values(input)) if (typeof value === "string" && value.startsWith(parsed.argPrefix)) return true;
	return false;
}
/**
* Test whether a tool call is allowed by the union of `allowedTools` across a set
* of active skills. Returns `true` when the union is empty (permissive default)
* OR when any entry matches.
*/
function isToolAllowedByUnion(displayName, input, union) {
	if (union.length === 0) return true;
	return union.some((pattern) => matchesAllowedTool(displayName, input, pattern));
}
//#endregion
//#region src/skills/allowed-tools.ts
/** Tools that are always allowed regardless of the active skills' allow-list. */
const IMPLICITLY_ALLOWED_SKILL_TOOLS = [
	"skills_use",
	"skills_read",
	"skills_run_script"
];
/**
* Register `tool:gate` / `mcp:tool:gate` handlers that enforce the union of
* `allowedTools` across active skills.
*
* No-op when no active skill declares an allow-list (permissive default —
* matches the spec's "experimental" note for `allowed-tools`).
*
* Returns an `uninstall` fn. The agent calls this at run end to detach the
* handlers and prevent cross-run hook leaks.
*/
function installAllowedToolsGate(hooks, state) {
	function effectiveUnion() {
		const active = state.active();
		const declared = [];
		for (const record of active) if (record.skill.allowedTools?.length) declared.push(...record.skill.allowedTools);
		return {
			union: declared.length > 0 ? [...declared, ...IMPLICITLY_ALLOWED_SKILL_TOOLS] : [],
			active: active.map((a) => a.skill.name)
		};
	}
	function gateHandler(ctx) {
		if (ctx.block) return;
		const { union, active } = effectiveUnion();
		if (union.length === 0) return;
		if (isToolAllowedByUnion(ctx.displayName, ctx.input, union)) return;
		const err = new AgentToolNotAllowedError({
			toolName: ctx.name,
			displayName: ctx.displayName,
			allowedUnion: union,
			activeSkills: active
		});
		ctx.block = true;
		ctx.reason = err.message;
	}
	function mcpGateHandler(ctx) {
		if (ctx.block) return;
		const { union, active } = effectiveUnion();
		if (union.length === 0) return;
		if (isToolAllowedByUnion(ctx.displayName, ctx.input, union)) return;
		const err = new AgentToolNotAllowedError({
			toolName: `mcp_${ctx.server}_${ctx.tool}`,
			displayName: ctx.displayName,
			allowedUnion: union,
			activeSkills: active
		});
		ctx.block = true;
		ctx.reason = err.message;
	}
	const unregisterTool = hooks.hook("tool:gate", gateHandler);
	const unregisterMcp = hooks.hook("mcp:tool:gate", mcpGateHandler);
	return function uninstall() {
		unregisterTool();
		unregisterMcp();
	};
}
//#endregion
//#region src/skills/catalog.ts
/**
* Build the skill catalog XML and behavioral instructions for the system prompt.
*/
function buildCatalog(skills, options = {}) {
	if (skills.length === 0) return "";
	const skillsToolRegistered = options.skillsToolRegistered ?? true;
	const readToolName = options.readToolName ?? "read_file";
	const entries = skills.map((skill) => {
		const locationLine = skill.location ? `\n    <location>${escapeXml(skill.location)}</location>` : "";
		return `  <skill>
    <name>${escapeXml(skill.name)}</name>
    <description>${escapeXml(skill.description)}</description>${locationLine}
  </skill>`;
	}).join("\n");
	const hasFsSkills = skills.some((s) => s.location);
	const hasInlineSkills = skills.some((s) => !s.location);
	const behavioralParts = [];
	behavioralParts.push("The following skills provide specialized instructions for specific tasks.", "When a task matches a skill's description, activate the skill to load its full instructions before proceeding.");
	if (skillsToolRegistered) behavioralParts.push("To activate a skill, call the `skills_use` tool with the skill's name. The response contains the full instructions and any bundled resources you can then load via `skills_read` (reference files) or execute via `skills_run_script` (scripts/ directory).", "Relative paths referenced in a skill's instructions resolve against the skill directory noted in the `skills_use` response.");
	else if (hasFsSkills) behavioralParts.push(`For skills with a <location>, use the ${readToolName} tool to read the SKILL.md file at that path.`, "When a skill references relative paths, resolve them against the skill's directory (the parent of SKILL.md) and use absolute paths in tool calls.");
	if (hasInlineSkills && !skillsToolRegistered) behavioralParts.push("Skills without a <location> have their instructions included directly in <instructions> tags below.");
	const parts = [
		behavioralParts.join("\n"),
		"",
		"<available_skills>",
		entries,
		"</available_skills>"
	];
	if (hasInlineSkills && !skillsToolRegistered) {
		parts.push("");
		for (const skill of skills) if (!skill.location && skill.instructions) {
			parts.push(`<skill_instructions name="${escapeXml(skill.name)}">`);
			parts.push(skill.instructions);
			if (skill.resources && skill.resources.length > 0) {
				parts.push("");
				parts.push("<skill_resources>");
				for (const res of skill.resources) parts.push(`  <file type="${res.type}">${escapeXml(res.path)}</file>`);
				parts.push("</skill_resources>");
			}
			parts.push("</skill_instructions>");
		}
	}
	return parts.join("\n");
}
//#endregion
//#region src/skills/discovery.ts
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
const INDENT_RE = /^[ \t]{2,}/;
const KV_RE = /^([^:]+):(.*)$/;
const DOUBLE_QUOTED_RE = /^"((?:\\.|[^"\\])*)"$/;
const SINGLE_QUOTED_RE = /^'((?:''|[^'])*)'$/;
const DQ_ESCAPE_RE = /\\(["\\/bfnrt])/g;
const WHITESPACE_SPLIT_RE = /\s+/;
const PARAGRAPH_SPLIT_RE = /\n\n/;
const COMMA_OR_SPACE_RE = /[,\s]+/;
/**
* Block-list bullet (`- value`) under an indented frontmatter key. A
* single dash followed by either a space or end-of-line; bare `-` is a
* list item with an empty value (preserved by callers as `""` because
* the spec doesn't disallow it).
*/
const LIST_ITEM_RE = /^-(?:\s+|$)/;
/**
* Flow-style list scalar (`[a, b, c]`). Anchored, non-nested, single
* pass — we trim the outer brackets and let the caller split on
* commas. Anything more complex (nested brackets, quoted commas)
* falls through to the whitespace-split branch, where the bracket
* characters become visible authoring errors instead of silent garbage.
*
* Carefully written without overlapping `\s*` runs so it's not
* ReDoS-prone: the body class `[^[\]]*` already swallows whitespace,
* so wrapping it in `\s*…\s*` would let the regex engine
* non-deterministically split a run of spaces between the body and
* the boundary quantifiers.
*/
const FLOW_LIST_RE = /^\[([^[\]]*)\]$/;
/**
* Common kebab/camel/snake misspellings of multi-word spec frontmatter
* keys. The Agent Skills spec mandates kebab-case (`allowed-tools`), but
* authors raised on JS / Python idioms naturally reach for camelCase
* (`allowedTools`) or snake_case (`allowed_tools`) — and the consequences
* of a silent mismatch on `allowed-tools` specifically are nasty: the
* skill loads fine, looks active in the UI, the model reads the body
* and tells the user "tools are restricted", but the `tool:gate`
* middleware finds nothing in the union and runs permissively. So we
* look for these alts at parse time, migrate the value to the canonical
* key, and emit a diagnostic so the next person sees the issue clearly.
*
* Keep the value space tight — accepting too many shapes erodes the
* "lenient load, strict authoring" principle. Add entries only for keys
* that are (a) multi-word (single-word keys have no ambiguity), (b) in
* active use, and (c) cause silent failures when missing (gate keys,
* security gates, …).
*/
const FRONTMATTER_KEY_ALIASES = new Map([["allowed-tools", ["allowedTools", "allowed_tools"]]]);
/**
* Mutate `frontmatter` so any well-known alt-spelling of a canonical
* spec key gets folded into the canonical slot. Idempotent — when the
* canonical key is already set the function is a no-op even if an
* alt-spelling is also present (canonical wins; alt is ignored).
*
* Pushes a `frontmatter-key-alt-spelling` diagnostic per migration so
* the SkillConfig's `.diagnostics` carries an actionable hint into any
* host UI surfacing skill load issues.
*/
function normalizeKeyAliases(frontmatter, diagnostics) {
	for (const [canonical, aliases] of FRONTMATTER_KEY_ALIASES) {
		if (frontmatter[canonical] !== void 0) continue;
		for (const alias of aliases) {
			if (frontmatter[alias] === void 0) continue;
			frontmatter[canonical] = frontmatter[alias];
			diagnostics.push({
				severity: "warning",
				code: "frontmatter-key-alt-spelling",
				message: `Frontmatter field "${alias}" is not a spec key — the spec uses "${canonical}" (kebab-case). Reading the value from "${alias}" as a courtesy; rename it to "${canonical}" to silence this warning.`,
				field: alias
			});
			break;
		}
	}
}
/**
* Parse a SKILL.md file into frontmatter + body.
*
* Uses a simple regex-based YAML extractor that handles:
* - Flat key: value pairs
* - Quoted values
* - One-level nested maps (for `metadata:`)
* - One-level nested block lists, `key:` followed by indented `- item`
*   lines (for `allowed-tools` and any future array fields). Items are
*   unquoted with the same rules as scalar values.
* - Flow-style scalars are preserved verbatim — interpretation (e.g.
*   `[a, b]` as a list) is left to per-field consumers via
*   {@link takeStringOrArray}.
* - Lenient recovery from unquoted-colon values (e.g.
*   `description: Use when: the user asks …`) via a quote-wrap retry.
*/
function parseFrontmatter(content) {
	const diagnostics = [];
	const match = content.match(FRONTMATTER_RE);
	if (!match) return {
		frontmatter: {},
		body: content.trim(),
		diagnostics
	};
	const yamlBlock = match[1];
	const body = match[2].trim();
	const frontmatter = {};
	let currentKey = null;
	let currentMap = null;
	let currentList = null;
	const flushPending = () => {
		if (currentKey === null) return;
		if (currentList !== null) frontmatter[currentKey] = currentList;
		else if (currentMap !== null) frontmatter[currentKey] = currentMap;
		currentKey = null;
		currentMap = null;
		currentList = null;
	};
	for (const line of yamlBlock.split("\n")) {
		if (!line.trim() || line.trim().startsWith("#")) continue;
		if (currentKey !== null && INDENT_RE.test(line)) {
			const stripped = line.trim();
			if (LIST_ITEM_RE.test(stripped)) {
				if (currentMap !== null && Object.keys(currentMap).length > 0) {
					diagnostics.push({
						severity: "warning",
						code: "mixed-list-and-map",
						message: `Frontmatter field "${currentKey}" mixes list items and map entries — keeping the map and dropping the list items.`,
						field: currentKey
					});
					continue;
				}
				currentList = currentList ?? [];
				currentList.push(unquoteYaml(stripped.slice(1).trim()));
				continue;
			}
			const nestedMatch = stripped.match(KV_RE);
			if (nestedMatch) {
				if (currentList !== null) {
					diagnostics.push({
						severity: "warning",
						code: "mixed-list-and-map",
						message: `Frontmatter field "${currentKey}" mixes list items and map entries — keeping the list and dropping the map entries.`,
						field: currentKey
					});
					continue;
				}
				currentMap = currentMap ?? {};
				const val = nestedMatch[2].trim();
				currentMap[nestedMatch[1].trim()] = unquoteYaml(val);
			}
			continue;
		}
		flushPending();
		const kvMatch = matchFirstColon(line);
		if (!kvMatch) continue;
		const key = kvMatch.key.trim();
		const rawValue = kvMatch.value.trim();
		if (!rawValue) currentKey = key;
		else frontmatter[key] = unquoteYaml(rawValue);
	}
	flushPending();
	return {
		frontmatter,
		body,
		diagnostics
	};
}
function matchFirstColon(line) {
	const idx = line.indexOf(":");
	if (idx < 0) return null;
	const key = line.slice(0, idx);
	const value = line.slice(idx + 1);
	if (!KV_RE.test(`${key}:`)) return null;
	return {
		key,
		value
	};
}
/**
* Strip outer quotes and decode YAML-style escapes.
*
* - Double-quoted values honor `\"`, `\\`, `\n`, `\r`, `\t`, `\b`, `\f`, `\/`.
* - Single-quoted values honor `''` (the YAML single-quote escape for a literal `'`).
* - Trailing inline comments on unquoted values (` # comment`) are dropped.
* - Unquoted values are returned as-is (trimmed by caller).
*
* Skill frontmatter in the wild uses a narrow YAML subset — this is
* sufficient for every field in the current spec and the common authoring
* tools. Block scalars (`|`, `>`) and flow sequences are intentionally left
* unsupported: they'd only appear in a bespoke workflow, and silently
* half-supporting them is worse than rejecting them obviously.
*/
function unquoteYaml(val) {
	const dq = val.match(DOUBLE_QUOTED_RE);
	if (dq) return dq[1].replace(DQ_ESCAPE_RE, (_, ch) => {
		switch (ch) {
			case "\"": return "\"";
			case "\\": return "\\";
			case "/": return "/";
			case "b": return "\b";
			case "f": return "\f";
			case "n": return "\n";
			case "r": return "\r";
			case "t": return "	";
			default: return ch;
		}
	});
	const sq = val.match(SINGLE_QUOTED_RE);
	if (sq) return sq[1].replace(/''/g, "'");
	const hashIdx = val.indexOf(" #");
	if (hashIdx >= 0) return val.slice(0, hashIdx).trimEnd();
	return val;
}
/**
* Narrow a frontmatter field to a string. Produces a diagnostic when present
* but not a string, so malformed YAML (e.g. `name: 123`) surfaces as a
* warning instead of silently coerced downstream via `as string`.
*/
function takeString(frontmatter, key, diagnostics) {
	const raw = frontmatter[key];
	if (raw === void 0 || raw === null) return void 0;
	if (typeof raw === "string") return raw;
	diagnostics.push({
		severity: "warning",
		code: "invalid-field-type",
		message: `Frontmatter field "${key}" expected string, got ${Array.isArray(raw) ? "array" : typeof raw}. Skipping.`,
		field: key
	});
}
/**
* Accept either a YAML scalar (whitespace-separated tokens, the
* spec-canonical form) OR a YAML block / flow list (`- item` or
* `[a, b]`). Returns the field as a normalized `string[]`, or
* `undefined` when the field is absent or empty.
*
* Used for list-shaped fields like `allowed-tools` whose authoring
* surface has always been "either a space-separated string OR a
* list" in skill files in the wild (Claude Code's docs use both
* interchangeably). Pre-fix, the parser silently turned the list
* forms into `[object Object]` / `["[a", "b]"]` and the
* allowed-tools gate would never match anything — opening it wide.
*
* Diagnostics are pushed for unexpected shapes (number, plain
* object, etc.) and the field is dropped to avoid silent corruption.
*/
function takeStringOrArray(frontmatter, key, diagnostics) {
	const raw = frontmatter[key];
	if (raw === void 0 || raw === null) return void 0;
	if (Array.isArray(raw)) {
		const items = raw.filter((v) => v !== null && v !== void 0).map((v) => typeof v === "string" ? v : String(v)).map((v) => v.trim()).filter((v) => v.length > 0);
		return items.length > 0 ? items : void 0;
	}
	if (typeof raw === "string") {
		const trimmed = raw.trim();
		if (trimmed.length === 0) return void 0;
		const flowMatch = trimmed.match(FLOW_LIST_RE);
		if (flowMatch) return flowMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
		return trimmed.split(WHITESPACE_SPLIT_RE).filter(Boolean);
	}
	diagnostics.push({
		severity: "warning",
		code: "invalid-field-type",
		message: `Frontmatter field "${key}" expected string or list, got ${typeof raw}. Skipping.`,
		field: key
	});
}
const RESOURCE_DIRS = {
	scripts: "script",
	references: "reference",
	assets: "asset"
};
function enumerateResources(baseDir) {
	const resources = [];
	for (const [dir, type] of Object.entries(RESOURCE_DIRS)) {
		const dirPath = join(baseDir, dir);
		if (!existsSync(dirPath) || !statSync(dirPath).isDirectory()) continue;
		try {
			const files = readdirSync(dirPath, { recursive: true });
			for (const file of files) {
				const rel = typeof file === "string" ? file : file.toString("utf-8");
				if (statSync(join(dirPath, rel)).isFile()) resources.push({
					path: join(dir, rel),
					type
				});
			}
		} catch {}
	}
	try {
		for (const entry of readdirSync(baseDir)) {
			if (entry === "SKILL.md") continue;
			if (statSync(join(baseDir, entry)).isFile()) resources.push({
				path: entry,
				type: "other"
			});
		}
	} catch {}
	return resources;
}
/**
* Parse a SKILL.md file into a SkillConfig (lenient).
*
* Returns `null` only when the skill is fundamentally unusable:
* - The file is missing
* - The description is absent (required by spec for disclosure)
*
* All other issues are surfaced as `SkillConfig.diagnostics` with severity
* `warning`. Deprecated top-level fields (`paths`, `model`, `thinking`) are
* auto-migrated into `metadata['zidane.*']`.
*/
async function parseSkillFile(filePath, options = {}) {
	const absPath = resolve(filePath);
	if (!existsSync(absPath)) return null;
	const { frontmatter, body, diagnostics } = parseFrontmatter(readFileSync(absPath, "utf-8"));
	normalizeKeyAliases(frontmatter, diagnostics);
	let description = takeString(frontmatter, "description", diagnostics);
	if (!description && body) {
		const firstParagraph = body.split(PARAGRAPH_SPLIT_RE)[0]?.trim();
		if (firstParagraph) description = firstParagraph;
	}
	if (!description) return null;
	if (description.length > 1024) diagnostics.push({
		severity: "warning",
		code: "description-too-long",
		message: `Description exceeds spec limit of 1024 characters (got ${description.length}). Loading anyway.`,
		field: "description"
	});
	const baseDir = dirname(absPath);
	const dirName = basename(baseDir);
	const frontmatterName = takeString(frontmatter, "name", diagnostics);
	const name = frontmatterName || dirName;
	if (frontmatterName && frontmatterName !== dirName) diagnostics.push({
		severity: "warning",
		code: "name-mismatch-directory",
		message: `Skill name "${frontmatterName}" does not match parent directory "${dirName}". Loading anyway.`,
		field: "name"
	});
	if (name.length > 64) diagnostics.push({
		severity: "warning",
		code: "name-too-long",
		message: `Skill name "${name}" exceeds spec limit of 64 characters. Loading anyway.`,
		field: "name"
	});
	if (!validateSkillName(name)) diagnostics.push({
		severity: "warning",
		code: "invalid-name-format",
		message: `Skill name "${name}" does not match spec format (lowercase alphanumeric + hyphens, no leading/trailing/consecutive hyphens). Loading anyway.`,
		field: "name"
	});
	const config = {
		name,
		description,
		instructions: body,
		source: options.source ?? "project",
		location: absPath,
		baseDir,
		resources: enumerateResources(baseDir)
	};
	const license = takeString(frontmatter, "license", diagnostics);
	if (license) config.license = license;
	const compatibility = takeString(frontmatter, "compatibility", diagnostics);
	if (compatibility) {
		if (compatibility.length > 500) diagnostics.push({
			severity: "warning",
			code: "compatibility-too-long",
			message: `Compatibility exceeds spec limit of 500 characters (got ${compatibility.length}). Loading anyway.`,
			field: "compatibility"
		});
		config.compatibility = compatibility;
	}
	const metadata = {};
	const rawMetadata = frontmatter.metadata;
	if (rawMetadata && typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) for (const [k, v] of Object.entries(rawMetadata)) {
		if (typeof v !== "string") {
			diagnostics.push({
				severity: "warning",
				code: "invalid-metadata-value",
				message: `Metadata value for "${k}" is not a string; coerced. (Spec requires string values.)`,
				field: "metadata"
			});
			metadata[k] = String(v);
			continue;
		}
		metadata[k] = v;
	}
	else if (rawMetadata !== void 0) diagnostics.push({
		severity: "warning",
		code: "invalid-metadata-shape",
		message: `Frontmatter "metadata" expected a map, got ${Array.isArray(rawMetadata) ? "array" : typeof rawMetadata}. Ignored.`,
		field: "metadata"
	});
	const pathsField = takeString(frontmatter, "paths", diagnostics);
	if (pathsField) {
		metadata["zidane.paths"] = pathsField.split(COMMA_OR_SPACE_RE).filter(Boolean).join(",");
		diagnostics.push({
			severity: "warning",
			code: "deprecated-top-level-field",
			message: "`paths` is not a spec field and is deprecated — moved to `metadata[\"zidane.paths\"]`.",
			field: "paths"
		});
	}
	const modelField = takeString(frontmatter, "model", diagnostics);
	if (modelField) {
		metadata["zidane.model"] = modelField;
		diagnostics.push({
			severity: "warning",
			code: "deprecated-top-level-field",
			message: "`model` is not a spec field and is deprecated — moved to `metadata[\"zidane.model\"]`.",
			field: "model"
		});
	}
	const thinkingField = takeString(frontmatter, "thinking", diagnostics);
	const effortField = thinkingField ? void 0 : takeString(frontmatter, "effort", diagnostics);
	const legacyThinking = thinkingField ?? effortField;
	if (legacyThinking) {
		metadata["zidane.thinking"] = legacyThinking;
		diagnostics.push({
			severity: "warning",
			code: "deprecated-top-level-field",
			message: `\`${thinkingField ? "thinking" : "effort"}\` is not a spec field and is deprecated — moved to \`metadata["zidane.thinking"]\`.`,
			field: thinkingField ? "thinking" : "effort"
		});
	}
	if (Object.keys(metadata).length > 0) config.metadata = metadata;
	const allowedTools = takeStringOrArray(frontmatter, "allowed-tools", diagnostics);
	if (allowedTools && allowedTools.length > 0) config.allowedTools = allowedTools;
	if (diagnostics.length > 0) config.diagnostics = diagnostics;
	return config;
}
const SKIP_DIRS = new Set([
	".git",
	"node_modules",
	".DS_Store",
	"dist",
	"build"
]);
function findSkillDirs(root, maxDepth = 4, _depth = 0) {
	if (_depth > maxDepth) return [];
	if (!existsSync(root) || !statSync(root).isDirectory()) return [];
	const results = [];
	try {
		for (const entry of readdirSync(root)) {
			if (SKIP_DIRS.has(entry)) continue;
			const entryPath = join(root, entry);
			if (!statSync(entryPath).isDirectory()) continue;
			const skillFile = join(entryPath, "SKILL.md");
			if (existsSync(skillFile) && statSync(skillFile).isFile()) results.push(skillFile);
			else results.push(...findSkillDirs(entryPath, maxDepth, _depth + 1));
		}
	} catch {}
	return results;
}
/**
* Return the default scan paths tagged by source. Project-scope paths come
* first; their skills therefore win on name collisions against user-scope
* skills (first-found wins in discovery).
*/
function getDefaultScanPaths() {
	const home = homedir();
	const cwd = process.cwd();
	return [
		{
			path: join(cwd, ".agents", "skills"),
			source: "project"
		},
		{
			path: join(cwd, ".zidane", "skills"),
			source: "project"
		},
		{
			path: join(home, ".agents", "skills"),
			source: "user"
		},
		{
			path: join(home, ".zidane", "skills"),
			source: "user"
		}
	];
}
/**
* Infer a source tag for a user-provided scan path.
* Paths under `$HOME` are treated as 'user'; everything else as 'project'.
*/
function inferSource(path) {
	return path.startsWith(homedir()) ? "user" : "project";
}
/**
* Discover skills from sourced filesystem paths.
* Each path is scanned for subdirectories containing SKILL.md.
* Earlier paths have higher priority (first-found wins on name collision).
*
* `signal` is checked between each `parseSkillFile` so a rapid `cwd` switch
* in the host (TUI / GUI) can stop a long scan without leaking I/O. Aborts
* reject with the signal's `reason` (or a default `AbortError`) — same shape
* `AbortSignal.throwIfAborted()` produces in modern runtimes.
*/
async function discoverSkills(paths, signal) {
	const skillsByName = /* @__PURE__ */ new Map();
	for (const { path: scanPath, source } of paths) {
		signal?.throwIfAborted?.();
		const skillFiles = findSkillDirs(resolve(scanPath));
		for (const file of skillFiles) {
			signal?.throwIfAborted?.();
			const skill = await parseSkillFile(file, { source });
			if (!skill) continue;
			if (skillsByName.has(skill.name)) {
				const existing = skillsByName.get(skill.name);
				const diag = {
					severity: "warning",
					code: "name-collision-shadowed",
					message: `A skill with name "${skill.name}" was also found at ${file} (source: ${source}); shadowed by ${existing.location} (source: ${existing.source}).`
				};
				existing.diagnostics = [...existing.diagnostics ?? [], diag];
				continue;
			}
			skillsByName.set(skill.name, skill);
		}
	}
	return [...skillsByName.values()];
}
//#endregion
//#region src/skills/writer.ts
const YAML_RESERVED_RE = /[:#&*!|>%@`]/;
const YAML_EDGE_OR_QUOTE_RE = /^\s|\s$|["']/;
const DQUOTE_RE = /"/g;
const LEADING_NEWLINES_RE = /^\n+/;
function yamlEscape(value) {
	if (YAML_RESERVED_RE.test(value) || YAML_EDGE_OR_QUOTE_RE.test(value) || value === "") return `"${value.replace(DQUOTE_RE, "\\\"")}"`;
	return value;
}
function serializeFrontmatter(skill) {
	const lines = ["---"];
	lines.push(`name: ${yamlEscape(skill.name)}`);
	lines.push(`description: ${yamlEscape(skill.description)}`);
	if (skill.license) lines.push(`license: ${yamlEscape(skill.license)}`);
	if (skill.compatibility) lines.push(`compatibility: ${yamlEscape(skill.compatibility)}`);
	if (skill.allowedTools?.length) lines.push(`allowed-tools: ${skill.allowedTools.join(" ")}`);
	if (skill.metadata && Object.keys(skill.metadata).length > 0) {
		lines.push("metadata:");
		for (const [key, value] of Object.entries(skill.metadata)) lines.push(`  ${key}: "${value.replace(DQUOTE_RE, "\\\"")}"`);
	}
	lines.push("---");
	return lines.join("\n");
}
/**
* Write a `SkillConfig` to disk as a proper skill directory with SKILL.md.
* Returns the path to the written SKILL.md file.
*
* Throws if the skill fails `validateSkillForWrite` — the authoring path is
* strict on purpose. For loading third-party skills, use `parseSkillFile`
* directly (lenient).
*/
function writeSkillToDisk(skill, targetDir) {
	const result = validateSkillForWrite(skill);
	if (!result.valid) {
		const summary = result.errors.map((e) => `  - [${e.code}] ${e.message}`).join("\n");
		throw new Error(`Cannot write invalid skill "${skill.name}":\n${summary}`);
	}
	const skillDir = join(targetDir, skill.name);
	mkdirSync(skillDir, { recursive: true });
	const frontmatter = serializeFrontmatter(skill);
	const bodyTrimmed = skill.instructions ? skill.instructions.replace(LEADING_NEWLINES_RE, "") : "";
	const content = bodyTrimmed ? `${frontmatter}\n\n${bodyTrimmed}\n` : `${frontmatter}\n`;
	const skillPath = join(skillDir, "SKILL.md");
	writeFileSync(skillPath, content);
	return skillPath;
}
/**
* Write multiple `SkillConfig` objects to a target directory.
* Each skill gets its own subdirectory with a SKILL.md file.
* Returns the target directory path (for use as a scan path).
*/
function writeSkillsToDisk(skills, targetDir) {
	mkdirSync(targetDir, { recursive: true });
	for (const skill of skills) writeSkillToDisk(skill, targetDir);
	return targetDir;
}
//#endregion
//#region src/skills/resolve.ts
/**
* Resolve all skills from a SkillsConfig:
*
* 1. Materialize `config.write` entries to a temp directory, tagged as `inline`
* 2. Combine with default + user-provided scan paths (project-first, user-next)
* 3. Run lenient disk discovery
* 4. Merge with any host-provided `config.catalog`. Catalog entries seed the
*    resolution map FIRST, so they shadow disk-discovered skills with the
*    same name (shadow diagnostic attached to the winner — same shape as
*    the disk-vs-disk collision case).
* 5. Apply filters: optional project-skill trust gate, `exclude`,
*    `enabled` allowlist
*
* Returns `{ skills, cleanup }` — call `cleanup()` on agent destroy to remove
* the temp directory created for inline `config.write` skills. The agent
* factory wires this automatically; standalone callers must invoke it
* themselves to avoid leaking OS temp.
*/
async function resolveSkills(config) {
	const sourcedPaths = [];
	let writeDir;
	if (!config.skipDefaultPaths) sourcedPaths.push(...getDefaultScanPaths());
	for (const p of config.scan ?? []) sourcedPaths.push({
		path: p,
		source: inferSource(p)
	});
	if (config.write?.length) {
		writeDir = mkdtempSync(join(tmpdir(), "zidane-skills-"));
		writeSkillsToDisk(config.write, writeDir);
		sourcedPaths.push({
			path: writeDir,
			source: "inline"
		});
	}
	const diskSkills = await discoverSkills(sourcedPaths);
	const skillsByName = /* @__PURE__ */ new Map();
	for (const entry of config.catalog ?? []) skillsByName.set(entry.name, { ...entry });
	for (const skill of diskSkills) {
		const existing = skillsByName.get(skill.name);
		if (existing) {
			const loserLoc = skill.location ?? "(unknown)";
			const winnerLoc = existing.location ?? "(catalog)";
			const diag = {
				severity: "warning",
				code: "name-collision-shadowed",
				message: `A skill with name "${skill.name}" was also found at ${loserLoc} (source: ${skill.source ?? "project"}); shadowed by ${winnerLoc} (source: ${existing.source ?? "inline"}).`
			};
			existing.diagnostics = [...existing.diagnostics ?? [], diag];
			continue;
		}
		skillsByName.set(skill.name, skill);
	}
	let skills = [...skillsByName.values()];
	if (config.trustProjectSkills === false) skills = skills.filter((s) => s.source !== void 0 && s.source !== "project");
	const exclude = new Set(config.exclude ?? []);
	let filtered = skills.filter((s) => !exclude.has(s.name));
	if (Array.isArray(config.enabled)) {
		const allowlist = new Set(config.enabled);
		filtered = filtered.filter((s) => allowlist.has(s.name));
	}
	return {
		skills: filtered,
		cleanup: writeDir ? () => {
			try {
				rmSync(writeDir, {
					recursive: true,
					force: true
				});
			} catch {}
		} : () => {}
	};
}
//#endregion
//#region src/skills/interpolate.ts
/** Regex to match !`command` patterns in skill instructions */
const SHELL_INTERPOLATION_RE = /!`([^`]+)`/g;
/**
* Replace every !\`command\` pattern with a static placeholder, without
* executing anything. Used when shell interpolation is disabled
* (`SkillsConfig.allowShellInterpolation: false`): skill content can be
* third-party input, and running its embedded commands on activation
* bypasses the allowed-tools gate.
*/
function stripShellInterpolations(instructions) {
	return instructions.replace(SHELL_INTERPOLATION_RE, "[shell interpolation disabled]");
}
/**
* Interpolate shell commands in skill instructions.
*
* Runs each !\`command\` through the execution context and replaces
* the placeholder with the command's stdout. If a command fails,
* the placeholder is replaced with an error message (and
* `options.onFailure` is notified when provided).
*
* @param instructions - Raw skill instructions with potential !\`command\` patterns
* @param execution - The execution context to run commands in
* @param handle - The active execution handle
* @returns Instructions with all !\`command\` patterns replaced by output
*/
async function interpolateShellCommands(instructions, execution, handle, options = {}) {
	const matches = [...instructions.matchAll(SHELL_INTERPOLATION_RE)];
	if (matches.length === 0) return instructions;
	const replacements = [];
	for (const match of matches) {
		const command = match[1];
		const index = match.index;
		const length = match[0].length;
		try {
			const result = await execution.exec(handle, command, { timeout: 30 });
			if (result.exitCode === 0) replacements.push({
				index,
				length,
				output: result.stdout.trim()
			});
			else {
				const placeholder = `[command failed (exit ${result.exitCode}): ${result.stderr.trim() || result.stdout.trim()}]`;
				options.onFailure?.(command, placeholder);
				replacements.push({
					index,
					length,
					output: placeholder
				});
			}
		} catch (err) {
			const placeholder = `[command error: ${errorMessage(err)}]`;
			options.onFailure?.(command, placeholder);
			replacements.push({
				index,
				length,
				output: placeholder
			});
		}
	}
	let result = instructions;
	for (let i = replacements.length - 1; i >= 0; i--) {
		const { index, length, output } = replacements[i];
		result = result.slice(0, index) + output + result.slice(index + length);
	}
	return result;
}
//#endregion
export { escapeXml as S, validateResourcePath as _, writeSkillsToDisk as a, validateSkillName as b, inferSource as c, buildCatalog as d, IMPLICITLY_ALLOWED_SKILL_TOOLS as f, parseAllowedToolPattern as g, matchesAllowedTool as h, writeSkillToDisk as i, parseFrontmatter as l, isToolAllowedByUnion as m, stripShellInterpolations as n, discoverSkills as o, installAllowedToolsGate as p, resolveSkills as r, getDefaultScanPaths as s, interpolateShellCommands as t, parseSkillFile as u, validateResourcePathReal as v, createSkillActivationState as x, validateSkillForWrite as y };

//# sourceMappingURL=interpolate-Dy7Lunvg.js.map