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