/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { type CommandContext, type SlashCommand, type SlashCommandActionReturn, CommandKind, } from './types.js'; import { type HistoryItemInfo, type HistoryItemSkillsList, MessageType, } from '../types.js'; import { disableSkill, enableSkill } from '../../utils/skillSettings.js'; import { getErrorMessage } from '../../utils/errors.js'; import { getAdminErrorMessage } from '@google/gemini-cli-core'; import { linkSkill, renderSkillActionFeedback, } from '../../utils/skillUtils.js'; import { SettingScope } from '../../config/settings.js'; import { requestConsentInteractive, skillsConsentString, } from '../../config/extensions/consent.js'; async function listAction( context: CommandContext, args: string, ): Promise { const subArgs = args.trim().split(/\s+/); // Default to SHOWING descriptions. The user can hide them with 'nodesc'. let useShowDescriptions = true; let showAll = false; for (const arg of subArgs) { if (arg === 'nodesc' || arg === '--nodesc') { useShowDescriptions = false; } else if (arg === 'all' || arg === '--all') { showAll = true; } } const skillManager = context.services.config?.getSkillManager(); if (!skillManager) { context.ui.addItem({ type: MessageType.ERROR, text: 'Could not retrieve skill manager.', }); return; } const skills = showAll ? skillManager.getAllSkills() : skillManager.getAllSkills().filter((s) => !s.isBuiltin); const skillsListItem: HistoryItemSkillsList = { type: MessageType.SKILLS_LIST, skills: skills.map((skill) => ({ name: skill.name, description: skill.description, disabled: skill.disabled, location: skill.location, body: skill.body, isBuiltin: skill.isBuiltin, })), showDescriptions: useShowDescriptions, }; context.ui.addItem(skillsListItem); } async function linkAction( context: CommandContext, args: string, ): Promise { const parts = args.trim().split(/\s+/); const sourcePath = parts[0]; if (!sourcePath) { context.ui.addItem({ type: MessageType.ERROR, text: 'Usage: /skills link [--scope user|workspace]', }); return; } let scopeArg = 'user'; if (parts.length >= 3 && parts[1] === '--scope') { scopeArg = parts[2]; } else if (parts.length >= 2 && parts[1].startsWith('--scope=')) { scopeArg = parts[1].split('=')[1]; } const scope = scopeArg === 'workspace' ? 'workspace' : 'user'; try { await linkSkill( sourcePath, scope, (msg) => context.ui.addItem({ type: MessageType.INFO, text: msg, }), async (skills, targetDir) => { const consentString = await skillsConsentString( skills, sourcePath, targetDir, true, ); return requestConsentInteractive( consentString, context.ui.setConfirmationRequest.bind(context.ui), ); }, ); context.ui.addItem({ type: MessageType.INFO, text: `Successfully linked skills from "${sourcePath}" (${scope}).`, }); if (context.services.config) { await context.services.config.reloadSkills(); } } catch (error) { context.ui.addItem({ type: MessageType.ERROR, text: `Failed to link skills: ${getErrorMessage(error)}`, }); } } async function disableAction( context: CommandContext, args: string, ): Promise { const skillName = args.trim(); if (!skillName) { context.ui.addItem({ type: MessageType.ERROR, text: 'Please provide a skill name to disable.', }); return; } const skillManager = context.services.config?.getSkillManager(); if (skillManager?.isAdminEnabled() === false) { context.ui.addItem( { type: MessageType.ERROR, text: getAdminErrorMessage( 'Agent skills', context.services.config ?? undefined, ), }, Date.now(), ); return; } const skill = skillManager?.getSkill(skillName); if (!skill) { context.ui.addItem( { type: MessageType.ERROR, text: `Skill "${skillName}" not found.`, }, Date.now(), ); return; } const scope = context.services.settings.workspace.path ? SettingScope.Workspace : SettingScope.User; const result = disableSkill(context.services.settings, skillName, scope); let feedback = renderSkillActionFeedback( result, (label, path) => `${label} (${path})`, ); if (result.status === 'success' || result.status === 'no-op') { feedback += ' You can run "/skills reload" to refresh your current instance.'; } context.ui.addItem({ type: MessageType.INFO, text: feedback, }); } async function enableAction( context: CommandContext, args: string, ): Promise { const skillName = args.trim(); if (!skillName) { context.ui.addItem({ type: MessageType.ERROR, text: 'Please provide a skill name to enable.', }); return; } const skillManager = context.services.config?.getSkillManager(); if (skillManager?.isAdminEnabled() === false) { context.ui.addItem( { type: MessageType.ERROR, text: getAdminErrorMessage( 'Agent skills', context.services.config ?? undefined, ), }, Date.now(), ); return; } const result = enableSkill(context.services.settings, skillName); let feedback = renderSkillActionFeedback( result, (label, path) => `${label} (${path})`, ); if (result.status === 'success' || result.status === 'no-op') { feedback += ' You can run "/skills reload" to refresh your current instance.'; } context.ui.addItem({ type: MessageType.INFO, text: feedback, }); } async function reloadAction( context: CommandContext, ): Promise { const config = context.services.config; if (!config) { context.ui.addItem({ type: MessageType.ERROR, text: 'Could not retrieve configuration.', }); return; } const skillManager = config.getSkillManager(); const beforeNames = new Set(skillManager.getSkills().map((s) => s.name)); const startTime = Date.now(); let pendingItemSet = false; const pendingTimeout = setTimeout(() => { context.ui.setPendingItem({ type: MessageType.INFO, text: 'Reloading agent skills...', }); pendingItemSet = true; }, 100); try { await config.reloadSkills(); clearTimeout(pendingTimeout); if (pendingItemSet) { // If we showed the pending item, make sure it stays for at least 500ms // total to avoid a "flicker" where it appears and immediately disappears. const elapsed = Date.now() - startTime; const minVisibleDuration = 500; if (elapsed < minVisibleDuration) { await new Promise((resolve) => setTimeout(resolve, minVisibleDuration - elapsed), ); } context.ui.setPendingItem(null); } const afterSkills = skillManager.getSkills(); const afterNames = new Set(afterSkills.map((s) => s.name)); const added = afterSkills.filter((s) => !beforeNames.has(s.name)); const removedCount = [...beforeNames].filter( (name) => !afterNames.has(name), ).length; let successText = 'Agent skills reloaded successfully.'; const details: string[] = []; if (added.length > 0) { details.push( `${added.length} newly available skill${added.length > 1 ? 's' : ''}`, ); } if (removedCount > 0) { details.push( `${removedCount} skill${removedCount > 1 ? 's' : ''} no longer available`, ); } if (details.length > 0) { successText += ` ${details.join(' and ')}.`; } context.ui.addItem({ type: 'info', text: successText, icon: '✓ ', color: 'green', } as HistoryItemInfo); } catch (error) { clearTimeout(pendingTimeout); if (pendingItemSet) { context.ui.setPendingItem(null); } context.ui.addItem({ type: MessageType.ERROR, text: `Failed to reload skills: ${error instanceof Error ? error.message : String(error)}`, }); } } function disableCompletion( context: CommandContext, partialArg: string, ): string[] { const skillManager = context.services.config?.getSkillManager(); if (!skillManager) { return []; } return skillManager .getAllSkills() .filter((s) => !s.disabled && s.name.startsWith(partialArg)) .map((s) => s.name); } function enableCompletion( context: CommandContext, partialArg: string, ): string[] { const skillManager = context.services.config?.getSkillManager(); if (!skillManager) { return []; } return skillManager .getAllSkills() .filter((s) => s.disabled && s.name.startsWith(partialArg)) .map((s) => s.name); } export const skillsCommand: SlashCommand = { name: 'skills', description: 'List, enable, disable, or reload Gemini CLI agent skills. Usage: /skills [list | disable | enable | reload]', kind: CommandKind.BUILT_IN, autoExecute: false, subCommands: [ { name: 'list', description: 'List available agent skills. Usage: /skills list [nodesc] [all]', kind: CommandKind.BUILT_IN, action: listAction, }, { name: 'link', description: 'Link an agent skill from a local path. Usage: /skills link [--scope user|workspace]', kind: CommandKind.BUILT_IN, action: linkAction, }, { name: 'disable', description: 'Disable a skill by name. Usage: /skills disable ', kind: CommandKind.BUILT_IN, action: disableAction, completion: disableCompletion, }, { name: 'enable', description: 'Enable a disabled skill by name. Usage: /skills enable ', kind: CommandKind.BUILT_IN, action: enableAction, completion: enableCompletion, }, { name: 'reload', altNames: ['refresh'], description: 'Reload the list of discovered skills. Usage: /skills reload', kind: CommandKind.BUILT_IN, action: reloadAction, }, ], action: listAction, };