From 678be8d41fa8459f609b62e39c24f3e746bf9bec Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 7 Feb 2026 21:45:12 +0800 Subject: [PATCH] feat: add codex skills support and enhance system status UI - Introduced new query keys for codex skills and their list. - Updated English and Chinese locale files for system status messages. - Enhanced the SettingsPage to display installation details and upgrade options. - Integrated CLI mode toggle in SkillsManagerPage for better skill management. - Modified skills routes to handle CLI type for skill operations and configurations. --- .codex/AGENTS.md | 4 + ccw/frontend/playwright.config.ts | 10 +- .../src/components/mcp/CcwToolsMcpCard.tsx | 55 ++++- .../components/shared/SkillCreateDialog.tsx | 4 +- ccw/frontend/src/hooks/index.ts | 3 + ccw/frontend/src/hooks/useSkills.ts | 36 +++- ccw/frontend/src/hooks/useSystemSettings.ts | 49 +++++ ccw/frontend/src/lib/api.ts | 103 +++++++-- ccw/frontend/src/lib/queryKeys.ts | 2 + ccw/frontend/src/locales/en/settings.json | 12 +- ccw/frontend/src/locales/zh/settings.json | 12 +- ccw/frontend/src/pages/SettingsPage.tsx | 201 ++++++++++++------ ccw/frontend/src/pages/SkillsManagerPage.tsx | 53 +++-- ccw/src/core/routes/skills-routes.ts | 143 +++++++++---- 14 files changed, 519 insertions(+), 168 deletions(-) diff --git a/.codex/AGENTS.md b/.codex/AGENTS.md index 21c1e653..437ecdc2 100644 --- a/.codex/AGENTS.md +++ b/.codex/AGENTS.md @@ -50,6 +50,10 @@ - Use `git add ` instead of `git add .` - Verify staged files before commit to avoid cross-task conflicts +**Multi-CLI Coexistence** (CRITICAL): +- If your task conflicts with existing uncommitted changes, **STOP and report the conflict** instead of overwriting +- Treat all pre-existing uncommitted changes as intentional work-in-progress by other tools + ## System Optimization diff --git a/ccw/frontend/playwright.config.ts b/ccw/frontend/playwright.config.ts index 74fdd6de..dd649ced 100644 --- a/ccw/frontend/playwright.config.ts +++ b/ccw/frontend/playwright.config.ts @@ -8,7 +8,9 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { - baseURL: 'http://localhost:5173/react/', + // E2E runs the Vite dev server with a root base to keep route URLs stable in tests. + // (Many tests use absolute paths like `/sessions` which should resolve to the app router.) + baseURL: 'http://localhost:5173/', trace: 'on-first-retry', }, projects: [ @@ -27,7 +29,11 @@ export default defineConfig({ ], webServer: { command: 'npm run dev', - url: 'http://localhost:5173/react/', + url: 'http://localhost:5173/', + env: { + ...process.env, + VITE_BASE_URL: '/', + }, reuseExistingServer: !process.env.CI, timeout: 120 * 1000, }, diff --git a/ccw/frontend/src/components/mcp/CcwToolsMcpCard.tsx b/ccw/frontend/src/components/mcp/CcwToolsMcpCard.tsx index 607ff7e5..9720531f 100644 --- a/ccw/frontend/src/components/mcp/CcwToolsMcpCard.tsx +++ b/ccw/frontend/src/components/mcp/CcwToolsMcpCard.tsx @@ -18,6 +18,8 @@ import { MessageCircleQuestion, ChevronDown, ChevronRight, + Globe, + Folder, } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; @@ -31,6 +33,7 @@ import { import { mcpServersKeys } from '@/hooks'; import { useQueryClient } from '@tanstack/react-query'; import { cn } from '@/lib/utils'; +import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; // ========== Types ========== @@ -103,16 +106,19 @@ export function CcwToolsMcpCard({ }: CcwToolsMcpCardProps) { const { formatMessage } = useIntl(); const queryClient = useQueryClient(); + const currentProjectPath = useWorkflowStore(selectProjectPath); // Local state for config inputs const [projectRootInput, setProjectRootInput] = useState(projectRoot || ''); const [allowedDirsInput, setAllowedDirsInput] = useState(allowedDirs || ''); const [disableSandboxInput, setDisableSandboxInput] = useState(disableSandbox || false); const [isExpanded, setIsExpanded] = useState(false); + const [installScope, setInstallScope] = useState<'global' | 'project'>('global'); // Mutations for install/uninstall const installMutation = useMutation({ - mutationFn: installCcwMcp, + mutationFn: (params: { scope: 'global' | 'project'; projectPath?: string }) => + installCcwMcp(params.scope, params.projectPath), onSuccess: () => { queryClient.invalidateQueries({ queryKey: mcpServersKeys.all }); queryClient.invalidateQueries({ queryKey: ['ccwMcpConfig'] }); @@ -167,7 +173,10 @@ export function CcwToolsMcpCard({ }; const handleInstallClick = () => { - installMutation.mutate(); + installMutation.mutate({ + scope: installScope, + projectPath: installScope === 'project' ? currentProjectPath : undefined, + }); }; const handleUninstallClick = () => { @@ -257,7 +266,7 @@ export function CcwToolsMcpCard({

{formatMessage({ id: 'mcp.ccw.tools.label' })}

-
+
{CCW_MCP_TOOLS.map((tool) => { const isEnabled = enabledTools.includes(tool.name); const icon = getToolIcon(tool.name); @@ -266,8 +275,8 @@ export function CcwToolsMcpCard({
{/* Install/Uninstall Button */} -
+
+ {/* Scope Selection */} + {!isInstalled && ( +
+

+ {formatMessage({ id: 'mcp.scope' })} +

+
+ + +
+
+ )} {!isInstalled ? (
- - {ccwInstall && ccwInstall.missingFiles.length > 0 && ( -
-

- {formatMessage({ id: 'settings.systemStatus.missingFiles' })}: -

-
    - {ccwInstall.missingFiles.slice(0, 5).map((file) => ( -
  • {file}
  • - ))} - {ccwInstall.missingFiles.length > 5 && ( -
  • +{ccwInstall.missingFiles.length - 5} more...
  • - )} -
-
-

- {formatMessage({ id: 'settings.systemStatus.runToFix' })}: -

- - ccw install - -
-
- )}
+ + {/* Installation cards */} + {isLoading ? ( +
+ {formatMessage({ id: 'settings.systemStatus.checking' })} +
+ ) : installations.length === 0 ? ( +
+

+ {formatMessage({ id: 'settings.systemStatus.noInstallations' })} +

+
+ ccw install +
+
+ ) : ( +
+ {installations.map((inst) => { + const isGlobal = inst.installation_mode === 'Global'; + const installDate = new Date(inst.installation_date).toLocaleDateString(); + const version = inst.application_version !== 'unknown' ? inst.application_version : inst.installer_version; + + return ( +
+ {/* Mode + Version + Upgrade */} +
+
+ + {isGlobal ? : } + + + {isGlobal + ? formatMessage({ id: 'settings.systemStatus.global' }) + : formatMessage({ id: 'settings.systemStatus.path' })} + + + v{version} + +
+ +
+ + {/* Path */} +
+ {inst.installation_path} +
+ + {/* Date + Files */} +
+ + + {installDate} + + + + {inst.files_count} {formatMessage({ id: 'settings.systemStatus.files' })} + +
+
+ ); + })} + + {/* Missing files warning */} + {ccwInstall && !ccwInstall.installed && ccwInstall.missingFiles.length > 0 && ( +
+
+ + {formatMessage({ id: 'settings.systemStatus.incomplete' })} — {ccwInstall.missingFiles.length} {formatMessage({ id: 'settings.systemStatus.missingFiles' }).toLowerCase()} +
+
    + {ccwInstall.missingFiles.slice(0, 4).map((f) => ( +
  • {f}
  • + ))} + {ccwInstall.missingFiles.length > 4 && ( +
  • +{ccwInstall.missingFiles.length - 4} more...
  • + )} +
+
+

{formatMessage({ id: 'settings.systemStatus.runToFix' })}:

+ ccw install +
+
+ )} +
+ )} ); } @@ -865,31 +959,6 @@ export function SettingsPage() {
- {/* Display Settings */} -
-

- - {formatMessage({ id: 'settings.sections.display' })} -

-
-
-
-

{formatMessage({ id: 'settings.display.showCompletedTasks' })}

-

- {formatMessage({ id: 'settings.display.showCompletedTasksDesc' })} -

-
- -
-
-
- {/* Reset Settings */}

diff --git a/ccw/frontend/src/pages/SkillsManagerPage.tsx b/ccw/frontend/src/pages/SkillsManagerPage.tsx index e05cec53..ce16e65d 100644 --- a/ccw/frontend/src/pages/SkillsManagerPage.tsx +++ b/ccw/frontend/src/pages/SkillsManagerPage.tsx @@ -39,6 +39,7 @@ import { AlertDialogCancel, } from '@/components/ui'; import { SkillCard, SkillDetailPanel, SkillCreateDialog } from '@/components/shared'; +import { CliModeToggle, type CliMode } from '@/components/mcp/CliModeToggle'; import { useSkills, useSkillMutations } from '@/hooks'; import { fetchSkillDetail } from '@/lib/api'; import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; @@ -109,6 +110,7 @@ export function SkillsManagerPage() { const { formatMessage } = useIntl(); const projectPath = useWorkflowStore(selectProjectPath); + const [cliMode, setCliMode] = useState('claude'); const [searchQuery, setSearchQuery] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); const [sourceFilter, setSourceFilter] = useState('all'); @@ -143,6 +145,7 @@ export function SkillsManagerPage() { enabledOnly: enabledFilter === 'enabled', location: locationFilter, }, + cliType: cliMode, }); const { toggleSkill, isToggling } = useSkillMutations(); @@ -165,18 +168,19 @@ export function SkillsManagerPage() { const location = skill.location || 'project'; // Use folderName for API calls (actual folder name), fallback to name if not available const skillIdentifier = skill.folderName || skill.name; - + // Debug logging - console.log('[SkillToggle] Toggling skill:', { - name: skill.name, - folderName: skill.folderName, - location, - enabled, - skillIdentifier + console.log('[SkillToggle] Toggling skill:', { + name: skill.name, + folderName: skill.folderName, + location, + enabled, + skillIdentifier, + cliMode }); - + try { - await toggleSkill(skillIdentifier, enabled, location); + await toggleSkill(skillIdentifier, enabled, location, cliMode); } catch (error) { console.error('[SkillToggle] Toggle failed:', error); throw error; @@ -211,7 +215,8 @@ export function SkillsManagerPage() { const data = await fetchSkillDetail( skill.name, skill.location || 'project', - projectPath + projectPath, + cliMode ); setSelectedSkill(data.skill); } catch (error) { @@ -220,7 +225,7 @@ export function SkillsManagerPage() { } finally { setIsDetailLoading(false); } - }, [projectPath]); + }, [projectPath, cliMode]); const handleCloseDetailPanel = useCallback(() => { setIsDetailPanelOpen(false); @@ -232,14 +237,23 @@ export function SkillsManagerPage() { {/* Page Header */}
-
-

- - {formatMessage({ id: 'skills.title' })} -

-

- {formatMessage({ id: 'skills.description' })} -

+
+
+

+ + {formatMessage({ id: 'skills.title' })} +

+

+ {formatMessage({ id: 'skills.description' })} +

+
+ {/* CLI Mode Badge Switcher */} +
+ +
); diff --git a/ccw/src/core/routes/skills-routes.ts b/ccw/src/core/routes/skills-routes.ts index ad62e850..9b2262bc 100644 --- a/ccw/src/core/routes/skills-routes.ts +++ b/ccw/src/core/routes/skills-routes.ts @@ -22,6 +22,12 @@ import type { } from '../../types/skill-types.js'; type GenerationType = 'description' | 'template'; +type CliType = 'claude' | 'codex'; + +/** Get the CLI base directory name based on cliType */ +function getCliDir(cliType: CliType): string { + return cliType === 'codex' ? '.codex' : '.claude'; +} interface GenerationParams { generationType: GenerationType; @@ -30,6 +36,7 @@ interface GenerationParams { location: SkillLocation; projectPath: string; broadcastToClients?: (data: unknown) => void; + cliType?: CliType; } function isRecord(value: unknown): value is Record { @@ -46,7 +53,8 @@ async function disableSkill( location: SkillLocation, projectPath: string, initialPath: string, - reason?: string // Kept for API compatibility but no longer used + reason?: string, // Kept for API compatibility but no longer used + cliType: CliType = 'claude' ): Promise { try { // Validate skill name @@ -54,18 +62,20 @@ async function disableSkill( return { success: false, message: 'Invalid skill name', status: 400 }; } + const cliDir = getCliDir(cliType); + // Get skill directory let skillsDir: string; if (location === 'project') { try { const validatedProjectPath = await validateAllowedPath(projectPath, { mustExist: true, allowedDirectories: [initialPath] }); - skillsDir = join(validatedProjectPath, '.claude', 'skills'); + skillsDir = join(validatedProjectPath, cliDir, 'skills'); } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, message: message.includes('Access denied') ? 'Access denied' : 'Invalid path', status: 403 }; } } else { - skillsDir = join(homedir(), '.claude', 'skills'); + skillsDir = join(homedir(), cliDir, 'skills'); } const skillDir = join(skillsDir, skillName); @@ -99,7 +109,8 @@ async function enableSkill( skillName: string, location: SkillLocation, projectPath: string, - initialPath: string + initialPath: string, + cliType: CliType = 'claude' ): Promise { try { // Validate skill name @@ -107,18 +118,20 @@ async function enableSkill( return { success: false, message: 'Invalid skill name', status: 400 }; } + const cliDir = getCliDir(cliType); + // Get skill directory let skillsDir: string; if (location === 'project') { try { const validatedProjectPath = await validateAllowedPath(projectPath, { mustExist: true, allowedDirectories: [initialPath] }); - skillsDir = join(validatedProjectPath, '.claude', 'skills'); + skillsDir = join(validatedProjectPath, cliDir, 'skills'); } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, message: message.includes('Access denied') ? 'Access denied' : 'Invalid path', status: 403 }; } } else { - skillsDir = join(homedir(), '.claude', 'skills'); + skillsDir = join(homedir(), cliDir, 'skills'); } const skillDir = join(skillsDir, skillName); @@ -148,15 +161,17 @@ async function enableSkill( /** * Get list of disabled skills by checking for SKILL.md.disabled files */ -function getDisabledSkillsList(location: SkillLocation, projectPath: string): DisabledSkillSummary[] { +function getDisabledSkillsList(location: SkillLocation, projectPath: string, cliType: CliType = 'claude'): DisabledSkillSummary[] { const result: DisabledSkillSummary[] = []; + const cliDir = getCliDir(cliType); + // Get skills directory (not a separate disabled directory) let skillsDir: string; if (location === 'project') { - skillsDir = join(projectPath, '.claude', 'skills'); + skillsDir = join(projectPath, cliDir, 'skills'); } else { - skillsDir = join(homedir(), '.claude', 'skills'); + skillsDir = join(homedir(), cliDir, 'skills'); } if (!existsSync(skillsDir)) { @@ -199,12 +214,12 @@ function getDisabledSkillsList(location: SkillLocation, projectPath: string): Di /** * Get extended skills config including disabled skills */ -function getExtendedSkillsConfig(projectPath: string): ExtendedSkillsConfig { - const baseConfig = getSkillsConfig(projectPath); +function getExtendedSkillsConfig(projectPath: string, cliType: CliType = 'claude'): ExtendedSkillsConfig { + const baseConfig = getSkillsConfig(projectPath, cliType); return { ...baseConfig, - disabledProjectSkills: getDisabledSkillsList('project', projectPath), - disabledUserSkills: getDisabledSkillsList('user', projectPath) + disabledProjectSkills: getDisabledSkillsList('project', projectPath, cliType), + disabledUserSkills: getDisabledSkillsList('user', projectPath, cliType) }; } @@ -293,15 +308,17 @@ function getSupportingFiles(skillDir: string): string[] { * @param {string} projectPath * @returns {Object} */ -function getSkillsConfig(projectPath: string): SkillsConfig { +function getSkillsConfig(projectPath: string, cliType: CliType = 'claude'): SkillsConfig { const result: SkillsConfig = { projectSkills: [], userSkills: [] }; + const cliDir = getCliDir(cliType); + try { - // Project skills: .claude/skills/ - const projectSkillsDir = join(projectPath, '.claude', 'skills'); + // Project skills: ./skills/ + const projectSkillsDir = join(projectPath, cliDir, 'skills'); if (existsSync(projectSkillsDir)) { const skills = readdirSync(projectSkillsDir, { withFileTypes: true }); for (const skill of skills) { @@ -330,8 +347,8 @@ function getSkillsConfig(projectPath: string): SkillsConfig { } } - // User skills: ~/.claude/skills/ - const userSkillsDir = join(homedir(), '.claude', 'skills'); + // User skills: ~/./skills/ + const userSkillsDir = join(homedir(), cliDir, 'skills'); if (existsSync(userSkillsDir)) { const skills = readdirSync(userSkillsDir, { withFileTypes: true }); for (const skill of skills) { @@ -373,7 +390,7 @@ function getSkillsConfig(projectPath: string): SkillsConfig { * @param {string} projectPath * @returns {Object} */ -async function getSkillDetail(skillName: string, location: SkillLocation, projectPath: string, initialPath: string) { +async function getSkillDetail(skillName: string, location: SkillLocation, projectPath: string, initialPath: string, cliType: CliType = 'claude') { try { if (skillName.includes('/') || skillName.includes('\\')) { return { error: 'Access denied', status: 403 }; @@ -382,11 +399,13 @@ async function getSkillDetail(skillName: string, location: SkillLocation, projec return { error: 'Invalid skill name', status: 400 }; } + const cliDir = getCliDir(cliType); + let baseDir; if (location === 'project') { try { const validatedProjectPath = await validateAllowedPath(projectPath, { mustExist: true, allowedDirectories: [initialPath] }); - baseDir = join(validatedProjectPath, '.claude', 'skills'); + baseDir = join(validatedProjectPath, cliDir, 'skills'); } catch (err) { const message = err instanceof Error ? err.message : String(err); const status = message.includes('Access denied') ? 403 : 400; @@ -394,7 +413,7 @@ async function getSkillDetail(skillName: string, location: SkillLocation, projec return { error: status === 403 ? 'Access denied' : 'Invalid path', status }; } } else { - baseDir = join(homedir(), '.claude', 'skills'); + baseDir = join(homedir(), cliDir, 'skills'); } const skillDir = join(baseDir, skillName); @@ -442,7 +461,7 @@ async function getSkillDetail(skillName: string, location: SkillLocation, projec * @param {string} projectPath * @returns {Object} */ -async function deleteSkill(skillName: string, location: SkillLocation, projectPath: string, initialPath: string) { +async function deleteSkill(skillName: string, location: SkillLocation, projectPath: string, initialPath: string, cliType: CliType = 'claude') { try { if (skillName.includes('/') || skillName.includes('\\')) { return { error: 'Access denied', status: 403 }; @@ -451,11 +470,13 @@ async function deleteSkill(skillName: string, location: SkillLocation, projectPa return { error: 'Invalid skill name', status: 400 }; } + const cliDir = getCliDir(cliType); + let baseDir; if (location === 'project') { try { const validatedProjectPath = await validateAllowedPath(projectPath, { mustExist: true, allowedDirectories: [initialPath] }); - baseDir = join(validatedProjectPath, '.claude', 'skills'); + baseDir = join(validatedProjectPath, cliDir, 'skills'); } catch (err) { const message = err instanceof Error ? err.message : String(err); const status = message.includes('Access denied') ? 403 : 400; @@ -463,7 +484,7 @@ async function deleteSkill(skillName: string, location: SkillLocation, projectPa return { error: status === 403 ? 'Access denied' : 'Invalid path', status }; } } else { - baseDir = join(homedir(), '.claude', 'skills'); + baseDir = join(homedir(), cliDir, 'skills'); } const skillDirCandidate = join(baseDir, skillName); @@ -585,7 +606,7 @@ async function copyDirectoryRecursive(source: string, target: string): Promise { if (pathname === '/api/skills' && req.method === 'GET') { const projectPathParam = url.searchParams.get('path') || initialPath; const includeDisabled = url.searchParams.get('includeDisabled') === 'true'; + const cliTypeParam = url.searchParams.get('cliType'); + const cliType: CliType = cliTypeParam === 'codex' ? 'codex' : 'claude'; try { const validatedProjectPath = await validateAllowedPath(projectPathParam, { mustExist: true, allowedDirectories: [initialPath] }); - + if (includeDisabled) { - const extendedData = getExtendedSkillsConfig(validatedProjectPath); + const extendedData = getExtendedSkillsConfig(validatedProjectPath, cliType); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(extendedData)); } else { - const skillsData = getSkillsConfig(validatedProjectPath); + const skillsData = getSkillsConfig(validatedProjectPath, cliType); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(skillsData)); } @@ -836,11 +862,13 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { // API: Get disabled skills list if (pathname === '/api/skills/disabled' && req.method === 'GET') { const projectPathParam = url.searchParams.get('path') || initialPath; + const cliTypeParam = url.searchParams.get('cliType'); + const cliType: CliType = cliTypeParam === 'codex' ? 'codex' : 'claude'; try { const validatedProjectPath = await validateAllowedPath(projectPathParam, { mustExist: true, allowedDirectories: [initialPath] }); - const disabledProjectSkills = getDisabledSkillsList('project', validatedProjectPath); - const disabledUserSkills = getDisabledSkillsList('user', validatedProjectPath); + const disabledProjectSkills = getDisabledSkillsList('project', validatedProjectPath, cliType); + const disabledUserSkills = getDisabledSkillsList('user', validatedProjectPath, cliType); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ disabledProjectSkills, disabledUserSkills })); @@ -872,7 +900,9 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { } const projectPath = projectPathParam || initialPath; - return disableSkill(skillName, locationValue, projectPath, initialPath, reason); + const cliTypeValue = typeof body.cliType === 'string' ? body.cliType : 'claude'; + const cliType: CliType = cliTypeValue === 'codex' ? 'codex' : 'claude'; + return disableSkill(skillName, locationValue, projectPath, initialPath, reason, cliType); }); return true; } @@ -895,7 +925,9 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { } const projectPath = projectPathParam || initialPath; - return enableSkill(skillName, locationValue, projectPath, initialPath); + const cliTypeValue = typeof body.cliType === 'string' ? body.cliType : 'claude'; + const cliType: CliType = cliTypeValue === 'codex' ? 'codex' : 'claude'; + return enableSkill(skillName, locationValue, projectPath, initialPath, cliType); }); return true; } @@ -907,6 +939,9 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { const subPath = url.searchParams.get('subpath') || ''; const location = url.searchParams.get('location') || 'project'; const projectPathParam = url.searchParams.get('path') || initialPath; + const cliTypeParam = url.searchParams.get('cliType'); + const dirCliType: CliType = cliTypeParam === 'codex' ? 'codex' : 'claude'; + const dirCliDir = getCliDir(dirCliType); if (skillName.includes('/') || skillName.includes('\\') || skillName.includes('..')) { res.writeHead(400, { 'Content-Type': 'application/json' }); @@ -918,7 +953,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { if (location === 'project') { try { const validatedProjectPath = await validateAllowedPath(projectPathParam, { mustExist: true, allowedDirectories: [initialPath] }); - baseDir = join(validatedProjectPath, '.claude', 'skills'); + baseDir = join(validatedProjectPath, dirCliDir, 'skills'); } catch (err) { const message = err instanceof Error ? err.message : String(err); const status = message.includes('Access denied') ? 403 : 400; @@ -928,7 +963,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { return true; } } else { - baseDir = join(homedir(), '.claude', 'skills'); + baseDir = join(homedir(), dirCliDir, 'skills'); } const skillRoot = join(baseDir, skillName); @@ -988,6 +1023,9 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { const fileName = url.searchParams.get('filename'); const location = url.searchParams.get('location') || 'project'; const projectPathParam = url.searchParams.get('path') || initialPath; + const fileCliTypeParam = url.searchParams.get('cliType'); + const fileCliType: CliType = fileCliTypeParam === 'codex' ? 'codex' : 'claude'; + const fileCliDir = getCliDir(fileCliType); if (!fileName) { res.writeHead(400, { 'Content-Type': 'application/json' }); @@ -1005,7 +1043,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { if (location === 'project') { try { const validatedProjectPath = await validateAllowedPath(projectPathParam, { mustExist: true, allowedDirectories: [initialPath] }); - baseDir = join(validatedProjectPath, '.claude', 'skills'); + baseDir = join(validatedProjectPath, fileCliDir, 'skills'); } catch (err) { const message = err instanceof Error ? err.message : String(err); const status = message.includes('Access denied') ? 403 : 400; @@ -1015,7 +1053,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { return true; } } else { - baseDir = join(homedir(), '.claude', 'skills'); + baseDir = join(homedir(), fileCliDir, 'skills'); } const skillRoot = join(baseDir, skillName); @@ -1082,12 +1120,16 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { return { error: 'Invalid skill name', status: 400 }; } + const writeCliTypeValue = typeof body.cliType === 'string' ? body.cliType : 'claude'; + const writeCliType: CliType = writeCliTypeValue === 'codex' ? 'codex' : 'claude'; + const writeCliDir = getCliDir(writeCliType); + let baseDir: string; if (location === 'project') { try { const projectRoot = projectPathParam || initialPath; const validatedProjectPath = await validateAllowedPath(projectRoot, { mustExist: true, allowedDirectories: [initialPath] }); - baseDir = join(validatedProjectPath, '.claude', 'skills'); + baseDir = join(validatedProjectPath, writeCliDir, 'skills'); } catch (err) { const message = err instanceof Error ? err.message : String(err); const status = message.includes('Access denied') ? 403 : 400; @@ -1095,7 +1137,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { return { error: status === 403 ? 'Access denied' : 'Invalid path', status }; } } else { - baseDir = join(homedir(), '.claude', 'skills'); + baseDir = join(homedir(), writeCliDir, 'skills'); } const skillRoot = join(baseDir, skillName); @@ -1128,7 +1170,9 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { const locationParam = url.searchParams.get('location'); const location: SkillLocation = locationParam === 'user' ? 'user' : 'project'; const projectPathParam = url.searchParams.get('path') || initialPath; - const skillDetail = await getSkillDetail(skillName, location, projectPathParam, initialPath); + const cliTypeParam = url.searchParams.get('cliType'); + const cliType: CliType = cliTypeParam === 'codex' ? 'codex' : 'claude'; + const skillDetail = await getSkillDetail(skillName, location, projectPathParam, initialPath, cliType); if (skillDetail.error) { res.writeHead(skillDetail.status || 404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: skillDetail.error })); @@ -1160,8 +1204,10 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { const location: SkillLocation = body.location === 'project' ? 'project' : 'user'; const projectPathParam = typeof body.projectPath === 'string' ? body.projectPath : undefined; + const delCliTypeValue = typeof body.cliType === 'string' ? body.cliType : 'claude'; + const delCliType: CliType = delCliTypeValue === 'codex' ? 'codex' : 'claude'; - return deleteSkill(skillName, location, projectPathParam || initialPath, initialPath); + return deleteSkill(skillName, location, projectPathParam || initialPath, initialPath, delCliType); }); return true; } @@ -1205,6 +1251,8 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { const description = typeof body.description === 'string' ? body.description : undefined; const generationType = typeof body.generationType === 'string' ? body.generationType : undefined; const projectPathParam = typeof body.projectPath === 'string' ? body.projectPath : undefined; + const createCliTypeValue = typeof body.cliType === 'string' ? body.cliType : 'claude'; + const createCliType: CliType = createCliTypeValue === 'codex' ? 'codex' : 'claude'; if (typeof mode !== 'string' || !mode) { return { error: 'Mode is required (import or cli-generate)' }; @@ -1249,7 +1297,7 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { return { error: status === 403 ? 'Access denied' : 'Invalid path', status }; } - return await importSkill(validatedSourcePath, location, validatedProjectPath, skillName); + return await importSkill(validatedSourcePath, location, validatedProjectPath, skillName, createCliType); } else if (mode === 'cli-generate') { // CLI generate mode: use Claude to generate skill if (!skillName) { @@ -1265,7 +1313,8 @@ export async function handleSkillsRoutes(ctx: RouteContext): Promise { skillName, location, projectPath: validatedProjectPath, - broadcastToClients + broadcastToClients, + cliType: createCliType }); } else { return { error: 'Invalid mode. Must be "import" or "cli-generate"' };