basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen4 * SPDX-License-Identifier: Apache-2.05 */6 7import type { Config } from '@qwen-code/qwen-code-core';8import {9 createDebugLogger,10 appendToLastTextPart,11 buildSkillLlmContent,12 applySkillAllowedTools,13} from '@qwen-code/qwen-code-core';14import { dirname } from 'node:path';15import type { ICommandLoader } from './types.js';16import type {17 SlashCommand,18 SlashCommandActionReturn,19 CommandSource,20} from '../ui/commands/types.js';21import { CommandKind } from '../ui/commands/types.js';22import { t } from '../i18n/index.js';23 24const debugLogger = createDebugLogger('SKILL_COMMAND_LOADER');25 26/**27 * Loads user-level, project-level, and extension-level skills as slash28 * commands, making them directly invocable via /<skill-name>.29 *30 * - User/project skills: always model-invocable (same as bundled), unless31 * disable-model-invocation is set.32 * - Extension skills: model-invocable only when description or whenToUse is33 * present (same rule as plugin commands), unless disable-model-invocation34 * is set.35 */36export class SkillCommandLoader implements ICommandLoader {37 constructor(private readonly config: Config | null) {}38 39 async loadCommands(_signal: AbortSignal): Promise<SlashCommand[]> {40 if (this.config?.getBareMode?.()) {41 debugLogger.debug('Bare mode enabled, skipping skill commands');42 return [];43 }44 45 const skillManager = this.config?.getSkillManager();46 if (!skillManager) {47 debugLogger.debug('SkillManager not available, skipping skill commands');48 return [];49 }50 51 try {52 const [userSkills, projectSkills, extensionSkills] = await Promise.all([53 skillManager.listSkills({ level: 'user' }),54 skillManager.listSkills({ level: 'project' }),55 skillManager.listSkills({ level: 'extension' }),56 ]);57 58 const allSkills = [...userSkills, ...projectSkills, ...extensionSkills];59 60 // Apply user-controlled `skills.disabled` filter HERE (inside the61 // skill loader) rather than via `CommandService`'s global denylist —62 // a global filter would also hide a same-named built-in command or63 // MCP prompt. See `Config.getDisabledSkillNames` for why this is a64 // live-read provider rather than a frozen field.65 const disabled =66 this.config?.getDisabledSkillNames() ?? new Set<string>();67 const visibleSkills = allSkills.filter(68 (skill) => !disabled.has(skill.name.toLowerCase()),69 );70 const nonUserInvocableCount = visibleSkills.filter(71 (skill) => skill.userInvocable === false,72 ).length;73 74 debugLogger.debug(75 `Loaded ${userSkills.length} user + ${projectSkills.length} project + ${extensionSkills.length} extension skill(s) as slash commands; ${allSkills.length - visibleSkills.length} hidden by skills.disabled; ${nonUserInvocableCount} marked non-user-invocable`,76 );77 78 return visibleSkills.map((skill) => {79 const isExtension = skill.level === 'extension';80 81 // Extension skills need explicit description or whenToUse to be82 // model-invocable (same rule as plugin commands).83 // User/project skills are always model-invocable.84 const modelInvocable = skill.disableModelInvocation85 ? false86 : isExtension87 ? !!(skill.description || skill.whenToUse)88 : true;89 90 const sourceLabel = isExtension91 ? `${t('Extension:')} ${skill.extensionName ?? 'unknown'}`92 : skill.level === 'project'93 ? t('Project')94 : t('User');95 96 return {97 name: skill.name,98 description: skill.description,99 modelDescription: skill.description,100 kind: CommandKind.SKILL,101 source: (isExtension102 ? 'plugin-command'103 : 'skill-dir-command') as CommandSource,104 sourceLabel,105 sourceDetail: isExtension106 ? 'extension'107 : skill.level === 'project'108 ? 'project'109 : 'user',110 userInvocable: skill.userInvocable ?? true,111 modelInvocable,112 argumentHint: skill.argumentHint,113 whenToUse: skill.whenToUse,114 skillDetail: {115 name: skill.name,116 description: skill.description,117 body: skill.body,118 level: skill.level,119 },120 action: async (context, _args): Promise<SlashCommandActionReturn> => {121 // Auto-approve the skill's declared allowedTools before its body is submitted.122 applySkillAllowedTools(123 this.config?.getPermissionManager(),124 skill.allowedTools,125 );126 127 const body = buildSkillLlmContent(128 dirname(skill.filePath),129 skill.body,130 );131 132 const content = context.invocation?.args133 ? appendToLastTextPart([{ text: body }], context.invocation.raw)134 : [{ text: body }];135 136 return {137 type: 'submit_prompt',138 content,139 };140 },141 };142 });143 } catch (error) {144 debugLogger.error('Failed to load skill commands:', error);145 return [];146 }147 }148}149 