(dirs: string[])
| 921 | * @param dirs Array of skill directories to load from (should be sorted deepest first) |
| 922 | */ |
| 923 | export async function addSkillDirectories(dirs: string[]): Promise<void> { |
| 924 | if ( |
| 925 | !isSettingSourceEnabled('projectSettings') || |
| 926 | isRestrictedToPluginOnly('skills') |
| 927 | ) { |
| 928 | logForDebugging( |
| 929 | '[skills] Dynamic skill discovery skipped: projectSettings disabled or plugin-only policy', |
| 930 | ) |
| 931 | return |
| 932 | } |
| 933 | if (dirs.length === 0) { |
| 934 | return |
| 935 | } |
| 936 | |
| 937 | const previousSkillNamesForLogging = new Set(dynamicSkills.keys()) |
| 938 | |
| 939 | // Load skills from all directories |
| 940 | const loadedSkills = await Promise.all( |
| 941 | dirs.map(dir => loadSkillsFromSkillsDir(dir, 'projectSettings')), |
| 942 | ) |
| 943 | |
| 944 | // Process in reverse order (shallower first) so deeper paths override |
| 945 | for (let i = loadedSkills.length - 1; i >= 0; i--) { |
| 946 | for (const { skill } of loadedSkills[i] ?? []) { |
| 947 | if (skill.type === 'prompt') { |
| 948 | dynamicSkills.set(skill.name, skill) |
| 949 | } |
| 950 | } |
| 951 | } |
| 952 | |
| 953 | const newSkillCount = loadedSkills.flat().length |
| 954 | if (newSkillCount > 0) { |
| 955 | const addedSkills = [...dynamicSkills.keys()].filter( |
| 956 | n => !previousSkillNamesForLogging.has(n), |
| 957 | ) |
| 958 | logForDebugging( |
| 959 | `[skills] Dynamically discovered ${newSkillCount} skills from ${dirs.length} directories`, |
| 960 | ) |
| 961 | if (addedSkills.length > 0) { |
| 962 | logEvent('tengu_dynamic_skills_changed', { |
| 963 | source: |
| 964 | 'file_operation' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 965 | previousCount: previousSkillNamesForLogging.size, |
| 966 | newCount: dynamicSkills.size, |
| 967 | addedCount: addedSkills.length, |
| 968 | directoryCount: dirs.length, |
| 969 | }) |
| 970 | } |
| 971 | } |
| 972 | |
| 973 | // Notify listeners that skills were loaded (so they can clear caches) |
| 974 | skillsLoaded.emit() |
| 975 | } |
| 976 | |
| 977 | /** |
| 978 | * Gets all dynamically discovered skills. |
no test coverage detected