/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Storage } from '../config/storage.js'; import { type SkillDefinition, loadSkillsFromDir } from './skillLoader.js'; import type { GeminiCLIExtension } from '../config/config.js'; import { debugLogger } from '../utils/debugLogger.js'; import { coreEvents } from '../utils/events.js'; export { type SkillDefinition }; export class SkillManager { private skills: SkillDefinition[] = []; private activeSkillNames: Set = new Set(); private adminSkillsEnabled = true; /** * Clears all discovered skills. */ clearSkills(): void { this.skills = []; } /** * Sets administrative settings for skills. */ setAdminSettings(enabled: boolean): void { this.adminSkillsEnabled = enabled; } /** * Returns true if skills are enabled by the admin. */ isAdminEnabled(): boolean { return this.adminSkillsEnabled; } /** * Discovers skills from standard user and workspace locations, as well as extensions. * Precedence: Extensions (lowest) -> User -> Workspace (highest). */ async discoverSkills( storage: Storage, extensions: GeminiCLIExtension[] = [], isTrusted: boolean = false, ): Promise { this.clearSkills(); // 1. Built-in skills (lowest precedence) await this.discoverBuiltinSkills(); // 2. Extension skills for (const extension of extensions) { if (extension.isActive && extension.skills) { this.addSkillsWithPrecedence(extension.skills); } } // 3. User skills const userSkills = await loadSkillsFromDir(Storage.getUserSkillsDir()); this.addSkillsWithPrecedence(userSkills); // 3.1 User agent skills alias (.agents/skills) const userAgentSkills = await loadSkillsFromDir( Storage.getUserAgentSkillsDir(), ); this.addSkillsWithPrecedence(userAgentSkills); // 4. Workspace skills (highest precedence) if (!isTrusted) { debugLogger.debug( 'Workspace skills disabled because folder is not trusted.', ); return; } const projectSkills = await loadSkillsFromDir( storage.getProjectSkillsDir(), ); this.addSkillsWithPrecedence(projectSkills); // 4.1 Workspace agent skills alias (.agents/skills) const projectAgentSkills = await loadSkillsFromDir( storage.getProjectAgentSkillsDir(), ); this.addSkillsWithPrecedence(projectAgentSkills); } /** * Discovers built-in skills. */ private async discoverBuiltinSkills(): Promise { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const builtinDir = path.join(__dirname, 'builtin'); const builtinSkills = await loadSkillsFromDir(builtinDir); for (const skill of builtinSkills) { skill.isBuiltin = true; } this.addSkillsWithPrecedence(builtinSkills); } /** * Adds skills to the manager programmatically. */ addSkills(skills: SkillDefinition[]): void { this.addSkillsWithPrecedence(skills); } private addSkillsWithPrecedence(newSkills: SkillDefinition[]): void { const skillMap = new Map( this.skills.map((s) => [s.name, s]), ); for (const newSkill of newSkills) { const existingSkill = skillMap.get(newSkill.name); if (existingSkill && existingSkill.location !== newSkill.location) { if (existingSkill.isBuiltin) { debugLogger.warn( `Skill "${newSkill.name}" from "${newSkill.location}" is overriding the built-in skill.`, ); } else { coreEvents.emitFeedback( 'warning', `Skill conflict detected: "${newSkill.name}" from "${newSkill.location}" is overriding the same skill from "${existingSkill.location}".`, ); } } skillMap.set(newSkill.name, newSkill); } this.skills = Array.from(skillMap.values()); } /** * Returns the list of enabled discovered skills. */ getSkills(): SkillDefinition[] { return this.skills.filter((s) => !s.disabled); } /** * Returns the list of enabled discovered skills that should be displayed in the UI. * This excludes built-in skills. */ getDisplayableSkills(): SkillDefinition[] { return this.skills.filter((s) => !s.disabled && !s.isBuiltin); } /** * Returns all discovered skills, including disabled ones. */ getAllSkills(): SkillDefinition[] { return this.skills; } /** * Filters discovered skills by name. */ filterSkills(predicate: (skill: SkillDefinition) => boolean): void { this.skills = this.skills.filter(predicate); } /** * Sets the list of disabled skill names. */ setDisabledSkills(disabledNames: string[]): void { const lowercaseDisabledNames = disabledNames.map((n) => n.toLowerCase()); for (const skill of this.skills) { skill.disabled = lowercaseDisabledNames.includes( skill.name.toLowerCase(), ); } } /** * Reads the full content (metadata + body) of a skill by name. */ getSkill(name: string): SkillDefinition | null { const lowercaseName = name.toLowerCase(); return ( this.skills.find((s) => s.name.toLowerCase() === lowercaseName) ?? null ); } /** * Activates a skill by name. */ activateSkill(name: string): void { this.activeSkillNames.add(name); } /** * Checks if a skill is active. */ isSkillActive(name: string): boolean { return this.activeSkillNames.has(name); } }