diff --git a/.claude/cli-tools.json b/.claude/cli-tools.json index a644a7e3..2f2dbd16 100644 --- a/.claude/cli-tools.json +++ b/.claude/cli-tools.json @@ -1,4 +1,5 @@ { + "$schema": "./cli-tools.schema.json", "version": "2.0.0", "tools": { "gemini": { @@ -42,24 +43,8 @@ { "id": "g25", "name": "g25", - "enabled": true + "enabled": true, + "tags": [] } - ], - "defaultTool": "gemini", - "settings": { - "promptFormat": "plain", - "smartContext": { - "enabled": false, - "maxFiles": 10 - }, - "nativeResume": true, - "recursiveQuery": true, - "cache": { - "injectionMode": "auto", - "defaultPrefix": "", - "defaultSuffix": "" - }, - "codeIndexMcp": "codexlens" - }, - "$schema": "./cli-tools.schema.json" -} \ No newline at end of file + ] +} diff --git a/ccw/src/core/routes/cli-routes.ts b/ccw/src/core/routes/cli-routes.ts index 3c75a599..a85ce97a 100644 --- a/ccw/src/core/routes/cli-routes.ts +++ b/ccw/src/core/routes/cli-routes.ts @@ -33,6 +33,7 @@ import { } from '../../tools/cli-config-manager.js'; import { loadClaudeCliTools, + ensureClaudeCliTools, saveClaudeCliTools, loadClaudeCliSettings, saveClaudeCliSettings, @@ -239,7 +240,8 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { // API: Get all custom endpoints if (pathname === '/api/cli/endpoints' && req.method === 'GET') { try { - const config = loadClaudeCliTools(initialPath); + // Use ensureClaudeCliTools to auto-create config if missing + const config = ensureClaudeCliTools(initialPath); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ endpoints: config.customEndpoints || [] })); } catch (err) { @@ -706,7 +708,8 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { // API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global) if (pathname === '/api/cli/tools-config' && req.method === 'GET') { try { - const toolsConfig = loadClaudeCliTools(initialPath); + // Use ensureClaudeCliTools to auto-create config if missing + const toolsConfig = ensureClaudeCliTools(initialPath); const settingsConfig = loadClaudeCliSettings(initialPath); const info = getClaudeCliToolsInfo(initialPath); res.writeHead(200, { 'Content-Type': 'application/json' }); diff --git a/ccw/src/templates/dashboard-css/31-api-settings.css b/ccw/src/templates/dashboard-css/31-api-settings.css index 90fce5ce..c5475386 100644 --- a/ccw/src/templates/dashboard-css/31-api-settings.css +++ b/ccw/src/templates/dashboard-css/31-api-settings.css @@ -977,6 +977,7 @@ select.cli-input { padding: 0.5rem; background: hsl(var(--muted) / 0.3); border-bottom: 1px solid hsl(var(--border)); + overflow-x: auto; } .sidebar-tab { @@ -987,7 +988,7 @@ select.cli-input { justify-content: center; gap: 0.125rem; padding: 0.5rem 0.25rem; - font-size: 0.75rem; + font-size: 0.7rem; font-weight: 500; color: hsl(var(--muted-foreground)); background: transparent; @@ -996,7 +997,18 @@ select.cli-input { cursor: pointer; transition: all 0.2s; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; min-width: 0; + max-width: 100%; +} + +.sidebar-tab span { + display: block; + width: 100%; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; } .sidebar-tab:hover { diff --git a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css index b309caf2..db010750 100644 --- a/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +++ b/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css @@ -609,6 +609,18 @@ color: hsl(250 50% 75%); } +/* Tool Call Message (Gemini tool_use/tool_result) */ +.cli-stream-line.formatted.tool_call { + background: hsl(160 40% 18% / 0.3); + border-left: 3px solid hsl(160 70% 50%); + font-size: 0.85em; +} + +.cli-msg-badge.cli-msg-tool_call { + background: hsl(160 70% 50% / 0.2); + color: hsl(160 70% 65%); +} + /* Stderr Message (Error) */ .cli-stream-line.formatted.stderr { background: hsl(0 50% 20% / 0.4); diff --git a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js index f7a6a101..d155ca50 100644 --- a/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +++ b/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js @@ -186,37 +186,58 @@ function handleCliStreamStarted(payload) { } function handleCliStreamOutput(payload) { - const { executionId, chunkType, data } = payload; - + const { executionId, chunkType, data, unit } = payload; + const exec = cliStreamExecutions[executionId]; if (!exec) return; - - // Parse and add output lines - const content = typeof data === 'string' ? data : JSON.stringify(data); + + // Use structured unit if available, otherwise fall back to data + const unitContent = unit?.content; + const unitType = unit?.type || chunkType; + + // For tool_call type, format the content specially + let content; + if (unitType === 'tool_call' && typeof unitContent === 'object' && unitContent !== null) { + // Format tool_call for display + if (unitContent.action === 'invoke') { + const params = unitContent.parameters ? JSON.stringify(unitContent.parameters) : ''; + content = `[Tool] ${unitContent.toolName}(${params})`; + } else if (unitContent.action === 'result') { + const status = unitContent.status || 'unknown'; + const output = unitContent.output ? `: ${unitContent.output.substring(0, 200)}${unitContent.output.length > 200 ? '...' : ''}` : ''; + content = `[Tool Result] ${status}${output}`; + } else { + content = JSON.stringify(unitContent); + } + } else { + // Use data (already serialized) for backward compatibility + content = typeof data === 'string' ? data : JSON.stringify(data); + } + const lines = content.split('\n'); - + lines.forEach(line => { if (line.trim() || lines.length === 1) { // Keep empty lines if it's the only content exec.output.push({ - type: chunkType || 'stdout', + type: unitType || 'stdout', content: line, timestamp: Date.now() }); } }); - + // Trim if too long if (exec.output.length > MAX_OUTPUT_LINES) { exec.output = exec.output.slice(-MAX_OUTPUT_LINES); } - + // Update UI if this is the active tab if (activeStreamTab === executionId && isCliStreamViewerOpen) { requestAnimationFrame(() => { renderStreamContent(executionId); }); } - + // Update badge to show activity updateStreamBadge(); } @@ -348,7 +369,7 @@ function renderFormattedLine(line, searchFilter) { // Type badge icons for backend chunkType (CliOutputUnit.type) // Maps to different CLI tools' output types: - // - Gemini: init→metadata, message→stdout, result→metadata + // - Gemini: init→metadata, message→stdout, result→metadata, tool_use/tool_result→tool_call // - Codex: reasoning→thought, agent_message→stdout, turn.completed→metadata // - Claude: system→metadata, assistant→stdout, result→metadata // - OpenCode: step_start→progress, text→stdout, step_finish→metadata @@ -360,7 +381,8 @@ function renderFormattedLine(line, searchFilter) { system: 'settings', stderr: 'alert-circle', metadata: 'info', - stdout: 'message-circle' + stdout: 'message-circle', + tool_call: 'wrench' }; // Type badge labels for backend chunkType @@ -372,7 +394,8 @@ function renderFormattedLine(line, searchFilter) { system: 'System', stderr: 'Error', metadata: 'Info', - stdout: 'Response' + stdout: 'Response', + tool_call: 'Tool' }; // Build type badge - prioritize content prefix, then fall back to chunkType diff --git a/ccw/src/templates/dashboard-js/components/mcp-manager.js b/ccw/src/templates/dashboard-js/components/mcp-manager.js index 15bfc957..c43fbe0a 100644 --- a/ccw/src/templates/dashboard-js/components/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/components/mcp-manager.js @@ -1207,28 +1207,28 @@ function setPreferredProjectConfigType(type) { const RECOMMENDED_MCP_SERVERS = [ { id: 'ace-tool', - name: 'ACE Tool', - description: 'Augment Context Engine - Semantic code search with real-time codebase indexing', + nameKey: 'mcp.ace-tool.name', + descKey: 'mcp.ace-tool.desc', icon: 'search-code', category: 'search', fields: [ { key: 'baseUrl', - label: 'Base URL', + labelKey: 'mcp.ace-tool.field.baseUrl', type: 'text', default: 'https://acemcp.heroman.wtf/relay/', placeholder: 'https://acemcp.heroman.wtf/relay/', required: true, - description: 'ACE MCP relay server URL' + descKey: 'mcp.ace-tool.field.baseUrl.desc' }, { key: 'token', - label: 'API Token', + labelKey: 'mcp.ace-tool.field.token', type: 'password', default: '', placeholder: 'ace_xxxxxxxxxxxxxxxx', required: true, - description: 'Your ACE API token (get from ACE dashboard)' + descKey: 'mcp.ace-tool.field.token.desc' } ], buildConfig: (values) => ({ @@ -1244,8 +1244,8 @@ const RECOMMENDED_MCP_SERVERS = [ }, { id: 'chrome-devtools', - name: 'Chrome DevTools', - description: 'Browser automation and DevTools integration for web development', + nameKey: 'mcp.chrome-devtools.name', + descKey: 'mcp.chrome-devtools.desc', icon: 'chrome', category: 'browser', fields: [], @@ -1258,28 +1258,32 @@ const RECOMMENDED_MCP_SERVERS = [ }, { id: 'exa', - name: 'Exa Search', - description: 'AI-powered web search with real-time crawling and content extraction', + nameKey: 'mcp.exa.name', + descKey: 'mcp.exa.desc', icon: 'globe-2', category: 'search', fields: [ { key: 'apiKey', - label: 'EXA API Key', + labelKey: 'mcp.exa.field.apiKey', type: 'password', default: '', placeholder: 'your-exa-api-key', - required: true, - description: 'Get your API key from exa.ai dashboard' + required: false, + descKey: 'mcp.exa.field.apiKey.desc' } ], - buildConfig: (values) => ({ - command: 'npx', - args: ['-y', 'exa-mcp-server'], - env: { - EXA_API_KEY: values.apiKey + buildConfig: (values) => { + const config = { + command: 'npx', + args: ['-y', 'exa-mcp-server'] + }; + // Only add env if API key is provided + if (values.apiKey) { + config.env = { EXA_API_KEY: values.apiKey }; } - }) + return config; + } } ]; @@ -1290,9 +1294,10 @@ function getRecommendedMcpServers() { // Check if a recommended MCP is already installed function isRecommendedMcpInstalled(mcpId) { - // Check in current project servers - const currentPath = projectPath; - const projectData = mcpAllProjects[currentPath] || {}; + // Check in current project servers (handle different path formats) + const forwardSlashPath = projectPath.replace(/\\/g, '/'); + const backSlashPath = projectPath.replace(/\//g, '\\'); + const projectData = mcpAllProjects[forwardSlashPath] || mcpAllProjects[backSlashPath] || mcpAllProjects[projectPath] || {}; const projectServers = projectData.mcpServers || {}; if (projectServers[mcpId]) return { installed: true, scope: 'project' }; @@ -1321,6 +1326,8 @@ function openRecommendedMcpWizard(mcpId) { } const hasFields = mcpDef.fields && mcpDef.fields.length > 0; + const mcpName = t(mcpDef.nameKey); + const mcpDesc = t(mcpDef.descKey); const modal = document.createElement('div'); modal.id = 'recommendedMcpWizardModal'; @@ -1334,8 +1341,8 @@ function openRecommendedMcpWizard(mcpId) {
-

${t('mcp.wizard.install')} ${escapeHtml(mcpDef.name)}

-

${escapeHtml(mcpDef.description)}

+

${t('mcp.wizard.install')} ${escapeHtml(mcpName)}

+

${escapeHtml(mcpDesc)}

' + '' + '' + '' + - '' + '' + ''; @@ -4152,7 +4149,7 @@ function renderModelPoolDetail(poolId) { '
' + '
' + '

' + escapeHtml(pool.name || pool.targetModel) + '

' + - '

' + typeLabel + ' Pool

' + + '

' + typeLabel + t('apiSettings.pool') + '

' + '
' + '
' + '' + diff --git a/ccw/src/templates/dashboard-js/views/mcp-manager.js b/ccw/src/templates/dashboard-js/views/mcp-manager.js index 40dfe2d6..594cc1c1 100644 --- a/ccw/src/templates/dashboard-js/views/mcp-manager.js +++ b/ccw/src/templates/dashboard-js/views/mcp-manager.js @@ -566,6 +566,8 @@ async function renderMcpManager() {
${getRecommendedMcpServers().map(mcp => { const installStatus = isRecommendedMcpInstalled(mcp.id); + const mcpName = t(mcp.nameKey); + const mcpDesc = t(mcp.descKey); return ` @@ -585,7 +587,7 @@ async function renderMcpManager() { ` : ''}
-

${escapeHtml(mcp.description)}

+

${escapeHtml(mcpDesc)}

${mcp.fields.length > 0 ? ` diff --git a/ccw/src/tools/claude-cli-tools.ts b/ccw/src/tools/claude-cli-tools.ts index 922b6a2a..bc49e150 100644 --- a/ccw/src/tools/claude-cli-tools.ts +++ b/ccw/src/tools/claude-cli-tools.ts @@ -216,6 +216,55 @@ function ensureToolTags(tool: Partial): ClaudeCliTool { }; } +/** + * Ensure CLI tools configuration file exists + * Creates default config if missing (auto-rebuild feature) + * @param projectDir - Project directory path + * @param createInProject - If true, create in project dir; if false, create in global dir + * @returns The config that was created/exists + */ +export function ensureClaudeCliTools(projectDir: string, createInProject: boolean = true): ClaudeCliToolsConfig & { _source?: string } { + const resolved = resolveConfigPath(projectDir); + + if (resolved.source !== 'default') { + // Config exists, load and return it + return loadClaudeCliTools(projectDir); + } + + // Config doesn't exist - create default + console.log('[claude-cli-tools] Config not found, creating default cli-tools.json'); + + const defaultConfig: ClaudeCliToolsConfig = { ...DEFAULT_TOOLS_CONFIG }; + + if (createInProject) { + // Create in project directory + ensureClaudeDir(projectDir); + const projectPath = getProjectConfigPath(projectDir); + try { + fs.writeFileSync(projectPath, JSON.stringify(defaultConfig, null, 2), 'utf-8'); + console.log(`[claude-cli-tools] Created default config at: ${projectPath}`); + return { ...defaultConfig, _source: 'project' }; + } catch (err) { + console.error('[claude-cli-tools] Failed to create project config:', err); + } + } + + // Fallback: create in global directory + const globalDir = path.join(os.homedir(), '.claude'); + if (!fs.existsSync(globalDir)) { + fs.mkdirSync(globalDir, { recursive: true }); + } + const globalPath = getGlobalConfigPath(); + try { + fs.writeFileSync(globalPath, JSON.stringify(defaultConfig, null, 2), 'utf-8'); + console.log(`[claude-cli-tools] Created default config at: ${globalPath}`); + return { ...defaultConfig, _source: 'global' }; + } catch (err) { + console.error('[claude-cli-tools] Failed to create global config:', err); + return { ...defaultConfig, _source: 'default' }; + } +} + /** * Load CLI tools configuration with fallback: * 1. Project: {projectDir}/.claude/cli-tools.json diff --git a/ccw/src/tools/cli-output-converter.ts b/ccw/src/tools/cli-output-converter.ts index 88e00be3..5bf0c4c3 100644 --- a/ccw/src/tools/cli-output-converter.ts +++ b/ccw/src/tools/cli-output-converter.ts @@ -19,7 +19,8 @@ export type CliOutputUnitType = | 'file_diff' // File modification diff | 'progress' // Progress updates | 'metadata' // Session/execution metadata - | 'system'; // System events/messages + | 'system' // System events/messages + | 'tool_call'; // Tool invocation/result (Gemini tool_use/tool_result) /** * Intermediate Representation unit @@ -295,6 +296,38 @@ export class JsonLinesParser implements IOutputParser { }; } + // Gemini tool_use: {"type":"tool_use","timestamp":"...","tool_name":"...","tool_id":"...","parameters":{...}} + if (json.type === 'tool_use' && json.tool_name) { + return { + type: 'tool_call', + content: { + tool: 'gemini', + action: 'invoke', + toolName: json.tool_name, + toolId: json.tool_id, + parameters: json.parameters, + raw: json + }, + timestamp + }; + } + + // Gemini tool_result: {"type":"tool_result","timestamp":"...","tool_id":"...","status":"...","output":"..."} + if (json.type === 'tool_result' && json.tool_id) { + return { + type: 'tool_call', + content: { + tool: 'gemini', + action: 'result', + toolId: json.tool_id, + status: json.status, + output: json.output, + raw: json + }, + timestamp + }; + } + // ========== Codex CLI --json format ========== // {"type":"thread.started","thread_id":"..."} // {"type":"turn.started"} @@ -733,6 +766,20 @@ export function flattenOutputUnits( } break; + case 'tool_call': + // Format tool call/result + if (unit.content.action === 'invoke') { + const params = unit.content.parameters ? JSON.stringify(unit.content.parameters) : ''; + text += `[Tool] ${unit.content.toolName}(${params})`; + } else if (unit.content.action === 'result') { + const status = unit.content.status || 'unknown'; + const output = unit.content.output ? `: ${unit.content.output.substring(0, 200)}${unit.content.output.length > 200 ? '...' : ''}` : ''; + text += `[Tool Result] ${status}${output}`; + } else { + text += JSON.stringify(unit.content); + } + break; + case 'metadata': case 'system': // Metadata and system events are typically excluded from prompt context