diff --git a/packages/core/src/skills/skillManager.test.ts b/packages/core/src/skills/skillManager.test.ts index 293115267c..ca3652a489 100644 --- a/packages/core/src/skills/skillManager.test.ts +++ b/packages/core/src/skills/skillManager.test.ts @@ -11,6 +11,15 @@ import * as path from 'node:path'; import { SkillManager } from './skillManager.js'; import { Storage } from '../config/storage.js'; import { type GeminiCLIExtension } from '../config/config.js'; +import { loadSkillsFromDir, type SkillDefinition } from './skillLoader.js'; + +vi.mock('./skillLoader.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadSkillsFromDir: vi.fn(actual.loadSkillsFromDir), + }; +}); describe('SkillManager', () => { let testRootDir: string; @@ -71,6 +80,8 @@ description: project-desc vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir); const service = new SkillManager(); + // @ts-expect-error accessing private method for testing + vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); await service.discoverSkills(storage, [mockExtension]); const skills = service.getSkills(); @@ -126,6 +137,8 @@ description: project-desc vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue(projectDir); const service = new SkillManager(); + // @ts-expect-error accessing private method for testing + vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); await service.discoverSkills(storage, [mockExtension]); const skills = service.getSkills(); @@ -138,6 +151,34 @@ description: project-desc expect(service.getSkills()[0].description).toBe('user-desc'); }); + it('should discover built-in skills', async () => { + const service = new SkillManager(); + const mockBuiltinSkill: SkillDefinition = { + name: 'builtin-skill', + description: 'builtin-desc', + location: 'builtin-loc', + body: 'builtin-body', + }; + + vi.mocked(loadSkillsFromDir).mockImplementation(async (dir) => { + if (dir.endsWith('builtin')) { + return [{ ...mockBuiltinSkill }]; + } + return []; + }); + + const storage = new Storage('/dummy'); + vi.spyOn(storage, 'getProjectSkillsDir').mockReturnValue('/non-existent'); + vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent'); + + await service.discoverSkills(storage); + + const skills = service.getSkills(); + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe('builtin-skill'); + expect(skills[0].isBuiltin).toBe(true); + }); + it('should filter disabled skills in getSkills but not in getAllSkills', async () => { const skillDir = path.join(testRootDir, 'skill1'); await fs.mkdir(skillDir, { recursive: true }); @@ -156,6 +197,8 @@ description: desc1 vi.spyOn(Storage, 'getUserSkillsDir').mockReturnValue('/non-existent'); const service = new SkillManager(); + // @ts-expect-error accessing private method for testing + vi.spyOn(service, 'discoverBuiltinSkills').mockResolvedValue(undefined); await service.discoverSkills(storage); service.setDisabledSkills(['skill1']); diff --git a/packages/core/src/skills/skillManager.ts b/packages/core/src/skills/skillManager.ts index 0279df5a65..6d301bd2f4 100644 --- a/packages/core/src/skills/skillManager.ts +++ b/packages/core/src/skills/skillManager.ts @@ -4,6 +4,8 @@ * 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'; @@ -56,9 +58,16 @@ export class SkillManager { * Discovers built-in skills. */ private async discoverBuiltinSkills(): Promise { - // Built-in skills can be added here. - // For now, this is a placeholder for where built-in skills will be loaded from. - // They could be loaded from a specific directory within the package. + 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); } private addSkillsWithPrecedence(newSkills: SkillDefinition[]): void { diff --git a/scripts/copy_bundle_assets.js b/scripts/copy_bundle_assets.js index afd2b35ea4..d7cc87e8be 100644 --- a/scripts/copy_bundle_assets.js +++ b/scripts/copy_bundle_assets.js @@ -62,4 +62,15 @@ if (existsSync(docsSrc)) { console.log('Copied docs to bundle/docs/'); } +// 4. Copy Built-in Skills (packages/core/src/skills/builtin) +const builtinSkillsSrc = join(root, 'packages/core/src/skills/builtin'); +const builtinSkillsDest = join(bundleDir, 'builtin'); +if (existsSync(builtinSkillsSrc)) { + cpSync(builtinSkillsSrc, builtinSkillsDest, { + recursive: true, + dereference: true, + }); + console.log('Copied built-in skills to bundle/builtin/'); +} + console.log('Assets copied to bundle/'); diff --git a/scripts/copy_files.js b/scripts/copy_files.js index 4e32e61e00..fc612fd144 100644 --- a/scripts/copy_files.js +++ b/scripts/copy_files.js @@ -74,4 +74,13 @@ if (packageName === 'cli') { } } +// Copy built-in skills for the core package. +if (packageName === 'core') { + const builtinSkillsSource = path.join(sourceDir, 'skills', 'builtin'); + const builtinSkillsTarget = path.join(targetDir, 'skills', 'builtin'); + if (fs.existsSync(builtinSkillsSource)) { + fs.cpSync(builtinSkillsSource, builtinSkillsTarget, { recursive: true }); + } +} + console.log('Successfully copied files.');