From ef772feea2168487e50c914d6d1a5786c194a653 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 9 Mar 2026 10:14:47 +0800 Subject: [PATCH 1/8] feat: support skills in .agents directory and other provider config directories --- .../core/src/skills/skill-manager.test.ts | 32 +++++++++++---- packages/core/src/skills/skill-manager.ts | 41 ++++++++++++------- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index d21916143..7cc3be2e4 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -504,17 +504,35 @@ Skill 3 content`); }); }); - describe('getSkillsBaseDir', () => { - it('should return project-level base dir', () => { - const baseDir = manager.getSkillsBaseDir('project'); + describe('getSkillsBaseDirs', () => { + it('should return all project-level base dirs', () => { + const baseDirs = manager.getSkillsBaseDirs('project'); - expect(baseDir).toBe(path.join('/test/project', '.qwen', 'skills')); + expect(baseDirs).toHaveLength(5); + expect(baseDirs).toContain(path.join('/test/project', '.qwen', 'skills')); + expect(baseDirs).toContain( + path.join('/test/project', '.agent', 'skills'), + ); + expect(baseDirs).toContain( + path.join('/test/project', '.cursor', 'skills'), + ); + expect(baseDirs).toContain( + path.join('/test/project', '.codex', 'skills'), + ); + expect(baseDirs).toContain( + path.join('/test/project', '.claude', 'skills'), + ); }); - it('should return user-level base dir', () => { - const baseDir = manager.getSkillsBaseDir('user'); + it('should return all user-level base dirs', () => { + const baseDirs = manager.getSkillsBaseDirs('user'); - expect(baseDir).toBe(path.join('/home/user', '.qwen', 'skills')); + expect(baseDirs).toHaveLength(5); + expect(baseDirs).toContain(path.join('/home/user', '.qwen', 'skills')); + expect(baseDirs).toContain(path.join('/home/user', '.agent', 'skills')); + expect(baseDirs).toContain(path.join('/home/user', '.cursor', 'skills')); + expect(baseDirs).toContain(path.join('/home/user', '.codex', 'skills')); + expect(baseDirs).toContain(path.join('/home/user', '.claude', 'skills')); }); }); diff --git a/packages/core/src/skills/skill-manager.ts b/packages/core/src/skills/skill-manager.ts index 05eabdd5a..2344530ad 100644 --- a/packages/core/src/skills/skill-manager.ts +++ b/packages/core/src/skills/skill-manager.ts @@ -25,6 +25,13 @@ import { normalizeContent } from '../utils/textUtils.js'; const debugLogger = createDebugLogger('SKILL_MANAGER'); const QWEN_CONFIG_DIR = '.qwen'; +const PROVIDER_CONFIG_DIRS = [ + '.qwen', + '.agent', + '.cursor', + '.codex', + '.claude', +]; const SKILLS_CONFIG_DIR = 'skills'; const SKILL_MANIFEST_FILE = 'SKILL.md'; @@ -412,19 +419,18 @@ export class SkillManager { * Gets the base directory for skills at a specific level. * * @param level - Storage level - * @returns Absolute directory path + * @returns Absolute directory paths */ - getSkillsBaseDir(level: SkillLevel): string { - const baseDir = + getSkillsBaseDirs(level: SkillLevel): string[] { + const baseDirs = level === 'project' - ? path.join( - this.config.getProjectRoot(), - QWEN_CONFIG_DIR, - SKILLS_CONFIG_DIR, + ? PROVIDER_CONFIG_DIRS.map((v) => + path.join(this.config.getProjectRoot(), v, SKILLS_CONFIG_DIR), ) - : path.join(os.homedir(), QWEN_CONFIG_DIR, SKILLS_CONFIG_DIR); - - return baseDir; + : PROVIDER_CONFIG_DIRS.map((v) => + path.join(os.homedir(), v, SKILLS_CONFIG_DIR), + ); + return baseDirs; } /** @@ -461,9 +467,13 @@ export class SkillManager { return skills; } - const baseDir = this.getSkillsBaseDir(level); - debugLogger.debug(`Loading ${level} level skills from: ${baseDir}`); - const skills = await this.loadSkillsFromDir(baseDir, level); + const baseDirs = this.getSkillsBaseDirs(level); + const skills: SkillConfig[] = []; + for (let i = 0; i < baseDirs.length; i++) { + debugLogger.debug(`Loading ${level} level skills from: ${baseDirs[i]}`); + const skillsFromDir = await this.loadSkillsFromDir(baseDirs[i], level); + skills.push(...skillsFromDir); + } debugLogger.debug(`Loaded ${skills.length} ${level} level skills`); return skills; } @@ -583,7 +593,8 @@ export class SkillManager { private updateWatchersFromCache(): void { const watchTargets = new Set( (['project', 'user'] as const) - .map((level) => this.getSkillsBaseDir(level)) + .map((level) => this.getSkillsBaseDirs(level)) + .reduce((acc, baseDirs) => acc.concat(baseDirs), []) .filter((baseDir) => fsSync.existsSync(baseDir)), ); @@ -639,7 +650,7 @@ export class SkillManager { } private async ensureUserSkillsDir(): Promise { - const baseDir = this.getSkillsBaseDir('user'); + const baseDir = path.join(os.homedir(), QWEN_CONFIG_DIR, SKILLS_CONFIG_DIR); try { await fs.mkdir(baseDir, { recursive: true }); } catch (error) { From 1673b04fad4019b784f4f50ac2e88acfd5590c19 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 9 Mar 2026 16:28:53 +0800 Subject: [PATCH 2/8] fix test ci --- .../core/src/skills/skill-manager.test.ts | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index 7cc3be2e4..446e457d8 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -391,42 +391,53 @@ You are a helpful assistant. describe('listSkills', () => { beforeEach(() => { - // Mock directory listing for skills directories (with Dirent objects) - vi.mocked(fs.readdir) - .mockResolvedValueOnce([ - { - name: 'skill1', - isDirectory: () => true, - isFile: () => false, - isSymbolicLink: () => false, - }, - { - name: 'skill2', - isDirectory: () => true, - isFile: () => false, - isSymbolicLink: () => false, - }, - { - name: 'not-a-dir.txt', - isDirectory: () => false, - isFile: () => true, - isSymbolicLink: () => false, - }, - ] as unknown as Awaited>) - .mockResolvedValueOnce([ - { - name: 'skill3', - isDirectory: () => true, - isFile: () => false, - isSymbolicLink: () => false, - }, - { - name: 'skill1', - isDirectory: () => true, - isFile: () => false, - isSymbolicLink: () => false, - }, - ] as unknown as Awaited>); + // Mock directory listing based on path to handle multiple base dirs per level + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.mocked(fs.readdir).mockImplementation((dirPath: any) => { + const pathStr = String(dirPath); + if (pathStr.includes('/test/project') && pathStr.includes('.qwen')) { + return Promise.resolve([ + { + name: 'skill1', + isDirectory: () => true, + isFile: () => false, + isSymbolicLink: () => false, + }, + { + name: 'skill2', + isDirectory: () => true, + isFile: () => false, + isSymbolicLink: () => false, + }, + { + name: 'not-a-dir.txt', + isDirectory: () => false, + isFile: () => true, + isSymbolicLink: () => false, + }, + ] as unknown as Awaited>); + } + if (pathStr.includes('/home/user') && pathStr.includes('.qwen')) { + return Promise.resolve([ + { + name: 'skill3', + isDirectory: () => true, + isFile: () => false, + isSymbolicLink: () => false, + }, + { + name: 'skill1', + isDirectory: () => true, + isFile: () => false, + isSymbolicLink: () => false, + }, + ] as unknown as Awaited>); + } + // Other provider dirs (.agent, .cursor, .codex, .claude) return empty + return Promise.resolve( + [] as unknown as Awaited>, + ); + }); vi.mocked(fs.access).mockResolvedValue(undefined); From 7e9c5843e88d2ce3a40761083d311ab73e405e9f Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 9 Mar 2026 16:46:28 +0800 Subject: [PATCH 3/8] fix test --- packages/core/src/skills/skill-manager.test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index 446e457d8..5784011a5 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -391,11 +391,19 @@ You are a helpful assistant. describe('listSkills', () => { beforeEach(() => { - // Mock directory listing based on path to handle multiple base dirs per level + // Mock directory listing based on path to handle multiple base dirs per level. + // Use path.join to construct expected paths so separators match on all platforms. + const projectQwenSkillsDir = path.join( + '/test/project', + '.qwen', + 'skills', + ); + const userQwenSkillsDir = path.join('/home/user', '.qwen', 'skills'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any vi.mocked(fs.readdir).mockImplementation((dirPath: any) => { const pathStr = String(dirPath); - if (pathStr.includes('/test/project') && pathStr.includes('.qwen')) { + if (pathStr === projectQwenSkillsDir) { return Promise.resolve([ { name: 'skill1', @@ -417,7 +425,7 @@ You are a helpful assistant. }, ] as unknown as Awaited>); } - if (pathStr.includes('/home/user') && pathStr.includes('.qwen')) { + if (pathStr === userQwenSkillsDir) { return Promise.resolve([ { name: 'skill3', From c3c8b39a29f019af7755aaf3c81d4c505eded689 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Mon, 9 Mar 2026 17:08:28 +0800 Subject: [PATCH 4/8] fix: deduplicate same-name skills across provider dirs and fix cross-platform test --- .../core/src/skills/skill-manager.test.ts | 68 +++++++++++++++++++ packages/core/src/skills/skill-manager.ts | 23 +++++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index 5784011a5..bd047e431 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -73,6 +73,14 @@ describe('SkillManager', () => { if (yamlString.includes('name: regular-skill')) { return { name: 'regular-skill', description: 'A regular skill' }; } + if (yamlString.includes('name: shared-skill')) { + const desc = yamlString.includes('From qwen dir') + ? 'From qwen dir' + : yamlString.includes('From agent dir') + ? 'From agent dir' + : 'A shared skill'; + return { name: 'shared-skill', description: desc }; + } if (!yamlString.includes('name:')) { return { description: 'A test skill' }; // Missing name case } @@ -502,6 +510,66 @@ Skill 3 content`); expect(projectSkills.every((s) => s.level === 'project')).toBe(true); }); + it('should deduplicate same-name skills across provider dirs within a level', async () => { + // Override readdir to return the same skill name from both .qwen and .agent dirs + vi.mocked(fs.readdir).mockReset(); + const projectQwenDir = path.join('/test/project', '.qwen', 'skills'); + const projectAgentDir = path.join('/test/project', '.agent', 'skills'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.mocked(fs.readdir).mockImplementation((dirPath: any) => { + const pathStr = String(dirPath); + if (pathStr === projectQwenDir) { + return Promise.resolve([ + { + name: 'shared-skill', + isDirectory: () => true, + isFile: () => false, + isSymbolicLink: () => false, + }, + ] as unknown as Awaited>); + } + if (pathStr === projectAgentDir) { + return Promise.resolve([ + { + name: 'shared-skill', + isDirectory: () => true, + isFile: () => false, + isSymbolicLink: () => false, + }, + ] as unknown as Awaited>); + } + return Promise.resolve( + [] as unknown as Awaited>, + ); + }); + + vi.mocked(fs.readFile).mockImplementation((filePath) => { + const pathStr = String(filePath); + if (pathStr.includes('.qwen') && pathStr.includes('shared-skill')) { + return Promise.resolve( + `---\nname: shared-skill\ndescription: From qwen dir\n---\nQwen content`, + ); + } + if (pathStr.includes('.agent') && pathStr.includes('shared-skill')) { + return Promise.resolve( + `---\nname: shared-skill\ndescription: From agent dir\n---\nAgent content`, + ); + } + return Promise.reject(new Error('File not found')); + }); + + const skills = await manager.listSkills({ + level: 'project', + force: true, + }); + + // Only one instance should remain, from .qwen (first in PROVIDER_CONFIG_DIRS) + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe('shared-skill'); + expect(skills[0].description).toBe('From qwen dir'); + }); + it('should handle empty directories', async () => { vi.mocked(fs.readdir).mockReset(); vi.mocked(fs.readdir).mockResolvedValue( diff --git a/packages/core/src/skills/skill-manager.ts b/packages/core/src/skills/skill-manager.ts index 2344530ad..fed6f4b98 100644 --- a/packages/core/src/skills/skill-manager.ts +++ b/packages/core/src/skills/skill-manager.ts @@ -28,9 +28,9 @@ const QWEN_CONFIG_DIR = '.qwen'; const PROVIDER_CONFIG_DIRS = [ '.qwen', '.agent', + '.claude', '.cursor', '.codex', - '.claude', ]; const SKILLS_CONFIG_DIR = 'skills'; const SKILL_MANIFEST_FILE = 'SKILL.md'; @@ -467,12 +467,25 @@ export class SkillManager { return skills; } + // Iterate provider directories in PROVIDER_CONFIG_DIRS order. + // The first directory that contains a skill with a given name wins, + // so the order defines implicit precedence (.qwen > .agent > .cursor > ...). const baseDirs = this.getSkillsBaseDirs(level); const skills: SkillConfig[] = []; - for (let i = 0; i < baseDirs.length; i++) { - debugLogger.debug(`Loading ${level} level skills from: ${baseDirs[i]}`); - const skillsFromDir = await this.loadSkillsFromDir(baseDirs[i], level); - skills.push(...skillsFromDir); + const seenNames = new Set(); + for (const baseDir of baseDirs) { + debugLogger.debug(`Loading ${level} level skills from: ${baseDir}`); + const skillsFromDir = await this.loadSkillsFromDir(baseDir, level); + for (const skill of skillsFromDir) { + if (seenNames.has(skill.name)) { + debugLogger.debug( + `Skipping duplicate skill at ${level} level: ${skill.name} from ${baseDir}`, + ); + continue; + } + seenNames.add(skill.name); + skills.push(skill); + } } debugLogger.debug(`Loaded ${skills.length} ${level} level skills`); return skills; From 06bef3b91f52d8bd23368a7fac803c394fd68d38 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Tue, 10 Mar 2026 14:33:40 +0800 Subject: [PATCH 5/8] fix dirs in getUserSkillsDirs --- packages/core/src/config/storage.ts | 14 ++++++++++++-- packages/core/src/skills/skill-manager.ts | 12 +++--------- packages/core/src/tools/ls.test.ts | 2 +- packages/core/src/tools/ls.ts | 6 +++--- packages/core/src/tools/read-file.test.ts | 2 +- packages/core/src/tools/read-file.ts | 6 +++--- packages/core/src/tools/shell.test.ts | 2 +- packages/core/src/tools/shell.ts | 8 ++++---- packages/core/src/utils/paths.ts | 4 ++++ 9 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index 3293280a8..0272b5b8c 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -12,6 +12,13 @@ import { getProjectHash, sanitizeCwd } from '../utils/paths.js'; export const QWEN_DIR = '.qwen'; export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; export const OAUTH_FILE = 'oauth_creds.json'; +export const SKILL_PROVIDER_CONFIG_DIRS = [ + '.qwen', + '.agent', + '.claude', + '.cursor', + '.codex', +]; const TMP_DIR_NAME = 'tmp'; const BIN_DIR_NAME = 'bin'; const PROJECT_DIR_NAME = 'projects'; @@ -133,8 +140,11 @@ export class Storage { return path.join(this.getExtensionsDir(), 'qwen-extension.json'); } - getUserSkillsDir(): string { - return path.join(Storage.getGlobalQwenDir(), 'skills'); + getUserSkillsDirs(): string[] { + const homeDir = os.homedir() || os.tmpdir(); + return SKILL_PROVIDER_CONFIG_DIRS.map((dir) => + path.join(homeDir, dir, 'skills'), + ); } getHistoryFilePath(): string { diff --git a/packages/core/src/skills/skill-manager.ts b/packages/core/src/skills/skill-manager.ts index fed6f4b98..6df002f23 100644 --- a/packages/core/src/skills/skill-manager.ts +++ b/packages/core/src/skills/skill-manager.ts @@ -21,17 +21,11 @@ import type { Config } from '../config/config.js'; import { validateConfig } from './skill-load.js'; import { createDebugLogger } from '../utils/debugLogger.js'; import { normalizeContent } from '../utils/textUtils.js'; +import { SKILL_PROVIDER_CONFIG_DIRS } from '../config/storage.js'; const debugLogger = createDebugLogger('SKILL_MANAGER'); const QWEN_CONFIG_DIR = '.qwen'; -const PROVIDER_CONFIG_DIRS = [ - '.qwen', - '.agent', - '.claude', - '.cursor', - '.codex', -]; const SKILLS_CONFIG_DIR = 'skills'; const SKILL_MANIFEST_FILE = 'SKILL.md'; @@ -424,10 +418,10 @@ export class SkillManager { getSkillsBaseDirs(level: SkillLevel): string[] { const baseDirs = level === 'project' - ? PROVIDER_CONFIG_DIRS.map((v) => + ? SKILL_PROVIDER_CONFIG_DIRS.map((v) => path.join(this.config.getProjectRoot(), v, SKILLS_CONFIG_DIR), ) - : PROVIDER_CONFIG_DIRS.map((v) => + : SKILL_PROVIDER_CONFIG_DIRS.map((v) => path.join(os.homedir(), v, SKILLS_CONFIG_DIR), ); return baseDirs; diff --git a/packages/core/src/tools/ls.test.ts b/packages/core/src/tools/ls.test.ts index 39a6b7b31..cbb12fbaa 100644 --- a/packages/core/src/tools/ls.test.ts +++ b/packages/core/src/tools/ls.test.ts @@ -42,7 +42,7 @@ describe('LSTool', () => { respectQwenIgnore: true, }), storage: { - getUserSkillsDir: () => userSkillsBase, + getUserSkillsDirs: () => [userSkillsBase], }, } as unknown as Config; diff --git a/packages/core/src/tools/ls.ts b/packages/core/src/tools/ls.ts index b8edbe163..eb46da308 100644 --- a/packages/core/src/tools/ls.ts +++ b/packages/core/src/tools/ls.ts @@ -9,7 +9,7 @@ import path from 'node:path'; import type { ToolInvocation, ToolResult } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; -import { isSubpath } from '../utils/paths.js'; +import { isSubpaths } from '../utils/paths.js'; import type { Config } from '../config/config.js'; import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js'; import { ToolErrorType } from './tool-error.js'; @@ -315,8 +315,8 @@ export class LSTool extends BaseDeclarativeTool { return `Path must be absolute: ${params.path}`; } - const userSkillsBase = this.config.storage.getUserSkillsDir(); - const isUnderUserSkills = isSubpath(userSkillsBase, params.path); + const userSkillsBases = this.config.storage.getUserSkillsDirs(); + const isUnderUserSkills = isSubpaths(userSkillsBases, params.path); const workspaceContext = this.config.getWorkspaceContext(); if ( diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index ec07a6995..a36af964a 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -40,7 +40,7 @@ describe('ReadFileTool', () => { getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir), storage: { getProjectTempDir: () => path.join(tempRootDir, '.temp'), - getUserSkillsDir: () => path.join(os.homedir(), '.qwen', 'skills'), + getUserSkillsDirs: () => [path.join(os.homedir(), '.qwen', 'skills')], }, getTruncateToolOutputThreshold: () => 2500, getTruncateToolOutputLines: () => 500, diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index e09a1ac58..4d3d43ac7 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -20,7 +20,7 @@ import { FileOperation } from '../telemetry/metrics.js'; import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js'; import { logFileOperation } from '../telemetry/loggers.js'; import { FileOperationEvent } from '../telemetry/types.js'; -import { isSubpath } from '../utils/paths.js'; +import { isSubpaths, isSubpath } from '../utils/paths.js'; import { Storage } from '../config/storage.js'; /** @@ -186,12 +186,12 @@ export class ReadFileTool extends BaseDeclarativeTool< const workspaceContext = this.config.getWorkspaceContext(); const globalTempDir = Storage.getGlobalTempDir(); const projectTempDir = this.config.storage.getProjectTempDir(); - const userSkillsDir = this.config.storage.getUserSkillsDir(); + const userSkillsDirs = this.config.storage.getUserSkillsDirs(); const resolvedFilePath = path.resolve(filePath); const isWithinTempDir = isSubpath(projectTempDir, resolvedFilePath) || isSubpath(globalTempDir, resolvedFilePath); - const isWithinUserSkills = isSubpath(userSkillsDir, resolvedFilePath); + const isWithinUserSkills = isSubpaths(userSkillsDirs, resolvedFilePath); if ( !workspaceContext.isPathWithinWorkspace(filePath) && diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index d03509451..0720cadf7 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -60,7 +60,7 @@ describe('ShellTool', () => { .fn() .mockReturnValue(createMockWorkspaceContext('/test/dir')), storage: { - getUserSkillsDir: vi.fn().mockReturnValue('/test/dir/.qwen/skills'), + getUserSkillsDirs: vi.fn().mockReturnValue(['/test/dir/.qwen/skills']), }, getGeminiClient: vi.fn(), getGitCoAuthor: vi.fn().mockReturnValue({ diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 01a9ac5cf..14f2a6777 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -34,7 +34,7 @@ import type { import { ShellExecutionService } from '../services/shellExecutionService.js'; import { formatMemoryUsage } from '../utils/formatters.js'; import type { AnsiOutput } from '../utils/terminalSerializer.js'; -import { isSubpath } from '../utils/paths.js'; +import { isSubpaths } from '../utils/paths.js'; import { getCommandRoots, isCommandAllowed, @@ -621,10 +621,10 @@ export class ShellTool extends BaseDeclarativeTool< return 'Directory must be an absolute path.'; } - const userSkillsDir = this.config.storage.getUserSkillsDir(); + const userSkillsDirs = this.config.storage.getUserSkillsDirs(); const resolvedDirectoryPath = path.resolve(params.directory); - const isWithinUserSkills = isSubpath( - userSkillsDir, + const isWithinUserSkills = isSubpaths( + userSkillsDirs, resolvedDirectoryPath, ); if (isWithinUserSkills) { diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index dc4434ece..6e6bdfa49 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -241,6 +241,10 @@ export function isSubpath(parentPath: string, childPath: string): boolean { ); } +export function isSubpaths(parentPath: string[], childPath: string): boolean { + return parentPath.some((p) => isSubpath(p, childPath)); +} + /** * Resolves a path with tilde (~) expansion and relative path resolution. * Handles tilde expansion for home directory and resolves relative paths From 4e08c2009d83d426a895ce0fffd6fef6c0abba5a Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Wed, 18 Mar 2026 18:01:40 +0800 Subject: [PATCH 6/8] fix remove other dirs --- packages/core/src/config/storage.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index 0272b5b8c..b8711ef46 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -12,13 +12,7 @@ import { getProjectHash, sanitizeCwd } from '../utils/paths.js'; export const QWEN_DIR = '.qwen'; export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json'; export const OAUTH_FILE = 'oauth_creds.json'; -export const SKILL_PROVIDER_CONFIG_DIRS = [ - '.qwen', - '.agent', - '.claude', - '.cursor', - '.codex', -]; +export const SKILL_PROVIDER_CONFIG_DIRS = ['.qwen', '.agent']; const TMP_DIR_NAME = 'tmp'; const BIN_DIR_NAME = 'bin'; const PROJECT_DIR_NAME = 'projects'; From 770b2ade92929e958c211670b0f4c2d7bf836023 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Wed, 18 Mar 2026 19:40:13 +0800 Subject: [PATCH 7/8] fix ci test --- packages/core/src/skills/skill-manager.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index 78c8f36d4..730653f93 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -595,7 +595,7 @@ Skill 3 content`); it('should return all project-level base dirs', () => { const baseDirs = manager.getSkillsBaseDirs('project'); - expect(baseDirs).toHaveLength(5); + expect(baseDirs).toHaveLength(2); expect(baseDirs).toContain(path.join('/test/project', '.qwen', 'skills')); expect(baseDirs).toContain( path.join('/test/project', '.agent', 'skills'), @@ -614,7 +614,7 @@ Skill 3 content`); it('should return all user-level base dirs', () => { const baseDirs = manager.getSkillsBaseDirs('user'); - expect(baseDirs).toHaveLength(5); + expect(baseDirs).toHaveLength(2); expect(baseDirs).toContain(path.join('/home/user', '.qwen', 'skills')); expect(baseDirs).toContain(path.join('/home/user', '.agent', 'skills')); expect(baseDirs).toContain(path.join('/home/user', '.cursor', 'skills')); @@ -623,13 +623,13 @@ Skill 3 content`); }); it('should return bundled-level base dir', () => { - const baseDir = manager.getSkillsBaseDir('bundled'); + const baseDirs = manager.getSkillsBaseDirs('bundled'); - expect(baseDir).toMatch(/skills[/\\]bundled$/); + expect(baseDirs[0]).toMatch(/skills[/\\]bundled$/); }); it('should throw for extension level', () => { - expect(() => manager.getSkillsBaseDir('extension')).toThrow( + expect(() => manager.getSkillsBaseDirs('extension')).toThrow( 'Extension skills do not have a base directory', ); }); From 0d8f352aec52ecdc2ec871d52070ad537478a8f6 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Wed, 18 Mar 2026 20:10:31 +0800 Subject: [PATCH 8/8] fix ci test --- packages/core/src/skills/skill-manager.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/core/src/skills/skill-manager.test.ts b/packages/core/src/skills/skill-manager.test.ts index 730653f93..639234577 100644 --- a/packages/core/src/skills/skill-manager.test.ts +++ b/packages/core/src/skills/skill-manager.test.ts @@ -600,15 +600,6 @@ Skill 3 content`); expect(baseDirs).toContain( path.join('/test/project', '.agent', 'skills'), ); - expect(baseDirs).toContain( - path.join('/test/project', '.cursor', 'skills'), - ); - expect(baseDirs).toContain( - path.join('/test/project', '.codex', 'skills'), - ); - expect(baseDirs).toContain( - path.join('/test/project', '.claude', 'skills'), - ); }); it('should return all user-level base dirs', () => { @@ -617,9 +608,6 @@ Skill 3 content`); expect(baseDirs).toHaveLength(2); expect(baseDirs).toContain(path.join('/home/user', '.qwen', 'skills')); expect(baseDirs).toContain(path.join('/home/user', '.agent', 'skills')); - expect(baseDirs).toContain(path.join('/home/user', '.cursor', 'skills')); - expect(baseDirs).toContain(path.join('/home/user', '.codex', 'skills')); - expect(baseDirs).toContain(path.join('/home/user', '.claude', 'skills')); }); it('should return bundled-level base dir', () => {