diff --git a/ccw/src/core/routes/claude-routes.ts b/ccw/src/core/routes/claude-routes.ts index 954a2dc8..a4bf983a 100644 --- a/ccw/src/core/routes/claude-routes.ts +++ b/ccw/src/core/routes/claude-routes.ts @@ -851,15 +851,23 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { if (pathname === '/api/language/chinese-response' && req.method === 'GET') { try { const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md'); + const userCodexPath = join(homedir(), '.codex', 'AGENTS.md'); const chineseRefPattern = /@.*chinese-response\.md/i; - let enabled = false; + let claudeEnabled = false; + let codexEnabled = false; let guidelinesPath = ''; // Check if user CLAUDE.md exists and contains Chinese response reference if (existsSync(userClaudePath)) { const content = readFileSync(userClaudePath, 'utf8'); - enabled = chineseRefPattern.test(content); + claudeEnabled = chineseRefPattern.test(content); + } + + // Check if user AGENTS.md exists and contains Chinese response reference + if (existsSync(userCodexPath)) { + const content = readFileSync(userCodexPath, 'utf8'); + codexEnabled = chineseRefPattern.test(content); } // Find guidelines file path - always use user-level path @@ -871,10 +879,13 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ - enabled, + enabled: claudeEnabled, // backward compatibility + claudeEnabled, + codexEnabled, guidelinesPath, guidelinesExists: !!guidelinesPath, - userClaudeMdExists: existsSync(userClaudePath) + userClaudeMdExists: existsSync(userClaudePath), + userCodexAgentsExists: existsSync(userCodexPath) })); return true; } catch (error) { @@ -887,16 +898,13 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { // API: Toggle Chinese response setting if (pathname === '/api/language/chinese-response' && req.method === 'POST') { handlePostRequest(req, res, async (body: any) => { - const { enabled } = body; + const { enabled, target = 'claude' } = body; // target: 'claude' | 'codex' if (typeof enabled !== 'boolean') { return { error: 'Missing or invalid enabled parameter', status: 400 }; } try { - const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md'); - const userClaudeDir = join(homedir(), '.claude'); - // Find guidelines file path - always use user-level path with ~ shorthand const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md'); @@ -906,21 +914,27 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { const guidelinesRef = '~/.claude/workflows/chinese-response.md'; + // Configure based on target + const isCodex = target === 'codex'; + const targetDir = isCodex ? join(homedir(), '.codex') : join(homedir(), '.claude'); + const targetFile = isCodex ? join(targetDir, 'AGENTS.md') : join(targetDir, 'CLAUDE.md'); + const headerText = isCodex ? '# Codex Instructions\n\n' : '# Claude Instructions\n\n'; + const headerPattern = isCodex ? /^# Codex Instructions\n\n?/ : /^# Claude Instructions\n\n?/; + const chineseRefLine = `- **中文回复准则**: @${guidelinesRef}`; const chineseRefPattern = /^- \*\*中文回复准则\*\*:.*chinese-response\.md.*$/gm; - // Ensure user .claude directory exists - if (!existsSync(userClaudeDir)) { - const fs = require('fs'); - fs.mkdirSync(userClaudeDir, { recursive: true }); + // Ensure target directory exists + if (!existsSync(targetDir)) { + mkdirSync(targetDir, { recursive: true }); } let content = ''; - if (existsSync(userClaudePath)) { - content = readFileSync(userClaudePath, 'utf8'); + if (existsSync(targetFile)) { + content = readFileSync(targetFile, 'utf8'); } else { - // Create new CLAUDE.md with header - content = '# Claude Instructions\n\n'; + // Create new file with header + content = headerText; } if (enabled) { @@ -930,13 +944,13 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { } // Add reference after the header line or at the beginning - const headerMatch = content.match(/^# Claude Instructions\n\n?/); + const headerMatch = content.match(headerPattern); if (headerMatch) { const insertPosition = headerMatch[0].length; content = content.slice(0, insertPosition) + chineseRefLine + '\n' + content.slice(insertPosition); } else { // Add header and reference - content = '# Claude Instructions\n\n' + chineseRefLine + '\n' + content; + content = headerText + chineseRefLine + '\n' + content; } } else { // Remove reference @@ -944,15 +958,15 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise { if (content) content += '\n'; } - writeFileSync(userClaudePath, content, 'utf8'); + writeFileSync(targetFile, content, 'utf8'); // Broadcast update broadcastToClients({ type: 'LANGUAGE_SETTING_CHANGED', - data: { chineseResponse: enabled } + data: { chineseResponse: enabled, target } }); - return { success: true, enabled }; + return { success: true, enabled, target }; } catch (error) { return { error: (error as Error).message, status: 500 }; } diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index bdc3a6ee..ea0d4907 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -627,6 +627,8 @@ const i18n = { 'lang.settingsDesc': 'Configure Claude response language preference', 'lang.chinese': 'Chinese Response', 'lang.chineseDesc': 'Enable Chinese response guidelines in global CLAUDE.md', + 'lang.chineseDescClaude': 'Enable in ~/.claude/CLAUDE.md', + 'lang.chineseDescCodex': 'Enable in ~/.codex/AGENTS.md', 'lang.enabled': 'Enabled', 'lang.disabled': 'Disabled', 'lang.enableSuccess': 'Chinese response enabled', @@ -2697,6 +2699,8 @@ const i18n = { 'lang.settingsDesc': '配置 Claude 回复语言偏好', 'lang.chinese': '中文回复', 'lang.chineseDesc': '在全局 CLAUDE.md 中启用中文回复准则', + 'lang.chineseDescClaude': '在 ~/.claude/CLAUDE.md 中启用', + 'lang.chineseDescCodex': '在 ~/.codex/AGENTS.md 中启用', 'lang.enabled': '已启用', 'lang.disabled': '已禁用', 'lang.enableSuccess': '中文回复已启用', diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index 364ea35a..3e6f3e84 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -9,6 +9,52 @@ var ccwEndpointTools = []; var cliToolConfig = null; // Store loaded CLI config var predefinedModels = {}; // Store predefined models per tool +// ========== CSRF Token Management ========== +var csrfToken = null; // Store CSRF token for state-changing requests + +/** + * Fetch wrapper that handles CSRF token management + * Captures new token from response and includes token in requests + */ +async function csrfFetch(url, options) { + options = options || {}; + options.headers = options.headers || {}; + + // Add CSRF token header for state-changing methods + var method = (options.method || 'GET').toUpperCase(); + if (['POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method) !== -1 && csrfToken) { + options.headers['X-CSRF-Token'] = csrfToken; + } + + var response = await fetch(url, options); + + // Capture new CSRF token from response + var newToken = response.headers.get('X-CSRF-Token'); + if (newToken) { + csrfToken = newToken; + } + + return response; +} + +/** + * Initialize CSRF token by fetching from server + * Should be called before any state-changing requests + */ +async function initCsrfToken() { + if (csrfToken) return; // Already initialized + + try { + var response = await fetch('/api/csrf-token'); + if (response.ok) { + var data = await response.json(); + csrfToken = data.csrfToken || response.headers.get('X-CSRF-Token'); + } + } catch (err) { + console.warn('[CLI Manager] Failed to fetch CSRF token:', err); + } +} + // ========== Active Execution Sync ========== /** @@ -143,7 +189,8 @@ async function loadCliCustomEndpoints() { async function toggleEndpointEnabled(endpointId, enabled) { try { - var response = await fetch('/api/cli/endpoints/' + endpointId, { + await initCsrfToken(); + var response = await csrfFetch('/api/cli/endpoints/' + endpointId, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: enabled }) @@ -167,7 +214,8 @@ async function toggleEndpointEnabled(endpointId, enabled) { async function syncEndpointToCliTools(endpoint) { try { - var response = await fetch('/api/cli/endpoints', { + await initCsrfToken(); + var response = await csrfFetch('/api/cli/endpoints', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -212,13 +260,18 @@ async function loadCliToolConfig() { async function updateCliToolConfig(tool, updates) { try { - var response = await fetch('/api/cli/config/' + tool, { + // Ensure CSRF token is initialized before making state-changing request + await initCsrfToken(); + + var response = await csrfFetch('/api/cli/config/' + tool, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); - if (!response.ok) throw new Error('Failed to update CLI config'); var data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Failed to update CLI config'); + } if (data.success && cliToolConfig && cliToolConfig.tools) { cliToolConfig.tools[tool] = data.config; } @@ -881,6 +934,8 @@ function renderCcwSection() { // ========== Language Settings State ========== var chineseResponseEnabled = false; var chineseResponseLoading = false; +var codexChineseResponseEnabled = false; +var codexChineseResponseLoading = false; var windowsPlatformEnabled = false; var windowsPlatformLoading = false; @@ -890,12 +945,14 @@ async function loadLanguageSettings() { var response = await fetch('/api/language/chinese-response'); if (!response.ok) throw new Error('Failed to load language settings'); var data = await response.json(); - chineseResponseEnabled = data.enabled || false; + chineseResponseEnabled = data.claudeEnabled || data.enabled || false; + codexChineseResponseEnabled = data.codexEnabled || false; return data; } catch (err) { console.error('Failed to load language settings:', err); chineseResponseEnabled = false; - return { enabled: false, guidelinesExists: false }; + codexChineseResponseEnabled = false; + return { claudeEnabled: false, codexEnabled: false, guidelinesExists: false }; } } @@ -913,8 +970,13 @@ async function loadWindowsPlatformSettings() { } } -async function toggleChineseResponse(enabled) { - if (chineseResponseLoading) return; +async function toggleChineseResponse(enabled, target) { + // target: 'claude' (default) or 'codex' + target = target || 'claude'; + var isCodex = target === 'codex'; + var loadingVar = isCodex ? 'codexChineseResponseLoading' : 'chineseResponseLoading'; + + if (isCodex ? codexChineseResponseLoading : chineseResponseLoading) return; // Pre-check: verify CCW workflows are installed (only when enabling) if (enabled && typeof ccwInstallStatus !== 'undefined' && !ccwInstallStatus.installed) { @@ -925,13 +987,17 @@ async function toggleChineseResponse(enabled) { } } - chineseResponseLoading = true; + if (isCodex) { + codexChineseResponseLoading = true; + } else { + chineseResponseLoading = true; + } try { var response = await fetch('/api/language/chinese-response', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ enabled: enabled }) + body: JSON.stringify({ enabled: enabled, target: target }) }); if (!response.ok) { @@ -947,18 +1013,27 @@ async function toggleChineseResponse(enabled) { } var data = await response.json(); - chineseResponseEnabled = data.enabled; + if (isCodex) { + codexChineseResponseEnabled = data.enabled; + } else { + chineseResponseEnabled = data.enabled; + } // Update UI renderLanguageSettingsSection(); // Show toast - showRefreshToast(enabled ? t('lang.enableSuccess') : t('lang.disableSuccess'), 'success'); + var toolName = isCodex ? 'Codex' : 'Claude'; + showRefreshToast(toolName + ': ' + (enabled ? t('lang.enableSuccess') : t('lang.disableSuccess')), 'success'); } catch (err) { console.error('Failed to toggle Chinese response:', err); // Error already shown in the !response.ok block } finally { - chineseResponseLoading = false; + if (isCodex) { + codexChineseResponseLoading = false; + } else { + chineseResponseLoading = false; + } } } @@ -1016,7 +1091,7 @@ async function renderLanguageSettingsSection() { if (!container) return; // Load current state if not loaded - if (!chineseResponseEnabled && !chineseResponseLoading) { + if (!chineseResponseEnabled && !codexChineseResponseEnabled && !chineseResponseLoading) { await loadLanguageSettings(); } if (!windowsPlatformEnabled && !windowsPlatformLoading) { @@ -1029,22 +1104,41 @@ async function renderLanguageSettingsSection() { '' + '' + '
' + + // Chinese Response - Claude '
' + '' + '
' + '' + '' + (chineseResponseEnabled ? t('lang.enabled') : t('lang.disabled')) + '' + '
' + - '

' + t('lang.chineseDesc') + '

' + + '

' + t('lang.chineseDescClaude') + '

' + '
' + + // Chinese Response - Codex + '
' + + '' + + '
' + + '' + + '' + + (codexChineseResponseEnabled ? t('lang.enabled') : t('lang.disabled')) + + '' + + '
' + + '

' + t('lang.chineseDescCodex') + '

' + + '
' + + // Windows Platform '
' + '