feat: 添加工具调用支持,增强 CLI 工具和 MCP 管理功能

This commit is contained in:
catlog22
2026-01-08 23:32:27 +08:00
parent 311ce2e4bc
commit 84168825d6
11 changed files with 297 additions and 76 deletions

View File

@@ -1,4 +1,5 @@
{ {
"$schema": "./cli-tools.schema.json",
"version": "2.0.0", "version": "2.0.0",
"tools": { "tools": {
"gemini": { "gemini": {
@@ -42,24 +43,8 @@
{ {
"id": "g25", "id": "g25",
"name": "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"
} }

View File

@@ -33,6 +33,7 @@ import {
} from '../../tools/cli-config-manager.js'; } from '../../tools/cli-config-manager.js';
import { import {
loadClaudeCliTools, loadClaudeCliTools,
ensureClaudeCliTools,
saveClaudeCliTools, saveClaudeCliTools,
loadClaudeCliSettings, loadClaudeCliSettings,
saveClaudeCliSettings, saveClaudeCliSettings,
@@ -239,7 +240,8 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
// API: Get all custom endpoints // API: Get all custom endpoints
if (pathname === '/api/cli/endpoints' && req.method === 'GET') { if (pathname === '/api/cli/endpoints' && req.method === 'GET') {
try { 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.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ endpoints: config.customEndpoints || [] })); res.end(JSON.stringify({ endpoints: config.customEndpoints || [] }));
} catch (err) { } catch (err) {
@@ -706,7 +708,8 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
// API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global) // API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global)
if (pathname === '/api/cli/tools-config' && req.method === 'GET') { if (pathname === '/api/cli/tools-config' && req.method === 'GET') {
try { try {
const toolsConfig = loadClaudeCliTools(initialPath); // Use ensureClaudeCliTools to auto-create config if missing
const toolsConfig = ensureClaudeCliTools(initialPath);
const settingsConfig = loadClaudeCliSettings(initialPath); const settingsConfig = loadClaudeCliSettings(initialPath);
const info = getClaudeCliToolsInfo(initialPath); const info = getClaudeCliToolsInfo(initialPath);
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });

View File

@@ -977,6 +977,7 @@ select.cli-input {
padding: 0.5rem; padding: 0.5rem;
background: hsl(var(--muted) / 0.3); background: hsl(var(--muted) / 0.3);
border-bottom: 1px solid hsl(var(--border)); border-bottom: 1px solid hsl(var(--border));
overflow-x: auto;
} }
.sidebar-tab { .sidebar-tab {
@@ -987,7 +988,7 @@ select.cli-input {
justify-content: center; justify-content: center;
gap: 0.125rem; gap: 0.125rem;
padding: 0.5rem 0.25rem; padding: 0.5rem 0.25rem;
font-size: 0.75rem; font-size: 0.7rem;
font-weight: 500; font-weight: 500;
color: hsl(var(--muted-foreground)); color: hsl(var(--muted-foreground));
background: transparent; background: transparent;
@@ -996,7 +997,18 @@ select.cli-input {
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0; min-width: 0;
max-width: 100%;
}
.sidebar-tab span {
display: block;
width: 100%;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
} }
.sidebar-tab:hover { .sidebar-tab:hover {

View File

@@ -609,6 +609,18 @@
color: hsl(250 50% 75%); 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) */ /* Stderr Message (Error) */
.cli-stream-line.formatted.stderr { .cli-stream-line.formatted.stderr {
background: hsl(0 50% 20% / 0.4); background: hsl(0 50% 20% / 0.4);

View File

@@ -186,19 +186,40 @@ function handleCliStreamStarted(payload) {
} }
function handleCliStreamOutput(payload) { function handleCliStreamOutput(payload) {
const { executionId, chunkType, data } = payload; const { executionId, chunkType, data, unit } = payload;
const exec = cliStreamExecutions[executionId]; const exec = cliStreamExecutions[executionId];
if (!exec) return; if (!exec) return;
// Parse and add output lines // Use structured unit if available, otherwise fall back to data
const content = typeof data === 'string' ? data : JSON.stringify(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'); const lines = content.split('\n');
lines.forEach(line => { lines.forEach(line => {
if (line.trim() || lines.length === 1) { // Keep empty lines if it's the only content if (line.trim() || lines.length === 1) { // Keep empty lines if it's the only content
exec.output.push({ exec.output.push({
type: chunkType || 'stdout', type: unitType || 'stdout',
content: line, content: line,
timestamp: Date.now() timestamp: Date.now()
}); });
@@ -348,7 +369,7 @@ function renderFormattedLine(line, searchFilter) {
// Type badge icons for backend chunkType (CliOutputUnit.type) // Type badge icons for backend chunkType (CliOutputUnit.type)
// Maps to different CLI tools' output types: // 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 // - Codex: reasoning→thought, agent_message→stdout, turn.completed→metadata
// - Claude: system→metadata, assistant→stdout, result→metadata // - Claude: system→metadata, assistant→stdout, result→metadata
// - OpenCode: step_start→progress, text→stdout, step_finish→metadata // - OpenCode: step_start→progress, text→stdout, step_finish→metadata
@@ -360,7 +381,8 @@ function renderFormattedLine(line, searchFilter) {
system: 'settings', system: 'settings',
stderr: 'alert-circle', stderr: 'alert-circle',
metadata: 'info', metadata: 'info',
stdout: 'message-circle' stdout: 'message-circle',
tool_call: 'wrench'
}; };
// Type badge labels for backend chunkType // Type badge labels for backend chunkType
@@ -372,7 +394,8 @@ function renderFormattedLine(line, searchFilter) {
system: 'System', system: 'System',
stderr: 'Error', stderr: 'Error',
metadata: 'Info', metadata: 'Info',
stdout: 'Response' stdout: 'Response',
tool_call: 'Tool'
}; };
// Build type badge - prioritize content prefix, then fall back to chunkType // Build type badge - prioritize content prefix, then fall back to chunkType

View File

@@ -1207,28 +1207,28 @@ function setPreferredProjectConfigType(type) {
const RECOMMENDED_MCP_SERVERS = [ const RECOMMENDED_MCP_SERVERS = [
{ {
id: 'ace-tool', id: 'ace-tool',
name: 'ACE Tool', nameKey: 'mcp.ace-tool.name',
description: 'Augment Context Engine - Semantic code search with real-time codebase indexing', descKey: 'mcp.ace-tool.desc',
icon: 'search-code', icon: 'search-code',
category: 'search', category: 'search',
fields: [ fields: [
{ {
key: 'baseUrl', key: 'baseUrl',
label: 'Base URL', labelKey: 'mcp.ace-tool.field.baseUrl',
type: 'text', type: 'text',
default: 'https://acemcp.heroman.wtf/relay/', default: 'https://acemcp.heroman.wtf/relay/',
placeholder: 'https://acemcp.heroman.wtf/relay/', placeholder: 'https://acemcp.heroman.wtf/relay/',
required: true, required: true,
description: 'ACE MCP relay server URL' descKey: 'mcp.ace-tool.field.baseUrl.desc'
}, },
{ {
key: 'token', key: 'token',
label: 'API Token', labelKey: 'mcp.ace-tool.field.token',
type: 'password', type: 'password',
default: '', default: '',
placeholder: 'ace_xxxxxxxxxxxxxxxx', placeholder: 'ace_xxxxxxxxxxxxxxxx',
required: true, required: true,
description: 'Your ACE API token (get from ACE dashboard)' descKey: 'mcp.ace-tool.field.token.desc'
} }
], ],
buildConfig: (values) => ({ buildConfig: (values) => ({
@@ -1244,8 +1244,8 @@ const RECOMMENDED_MCP_SERVERS = [
}, },
{ {
id: 'chrome-devtools', id: 'chrome-devtools',
name: 'Chrome DevTools', nameKey: 'mcp.chrome-devtools.name',
description: 'Browser automation and DevTools integration for web development', descKey: 'mcp.chrome-devtools.desc',
icon: 'chrome', icon: 'chrome',
category: 'browser', category: 'browser',
fields: [], fields: [],
@@ -1258,28 +1258,32 @@ const RECOMMENDED_MCP_SERVERS = [
}, },
{ {
id: 'exa', id: 'exa',
name: 'Exa Search', nameKey: 'mcp.exa.name',
description: 'AI-powered web search with real-time crawling and content extraction', descKey: 'mcp.exa.desc',
icon: 'globe-2', icon: 'globe-2',
category: 'search', category: 'search',
fields: [ fields: [
{ {
key: 'apiKey', key: 'apiKey',
label: 'EXA API Key', labelKey: 'mcp.exa.field.apiKey',
type: 'password', type: 'password',
default: '', default: '',
placeholder: 'your-exa-api-key', placeholder: 'your-exa-api-key',
required: true, required: false,
description: 'Get your API key from exa.ai dashboard' descKey: 'mcp.exa.field.apiKey.desc'
} }
], ],
buildConfig: (values) => ({ buildConfig: (values) => {
command: 'npx', const config = {
args: ['-y', 'exa-mcp-server'], command: 'npx',
env: { args: ['-y', 'exa-mcp-server']
EXA_API_KEY: values.apiKey };
// 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 // Check if a recommended MCP is already installed
function isRecommendedMcpInstalled(mcpId) { function isRecommendedMcpInstalled(mcpId) {
// Check in current project servers // Check in current project servers (handle different path formats)
const currentPath = projectPath; const forwardSlashPath = projectPath.replace(/\\/g, '/');
const projectData = mcpAllProjects[currentPath] || {}; const backSlashPath = projectPath.replace(/\//g, '\\');
const projectData = mcpAllProjects[forwardSlashPath] || mcpAllProjects[backSlashPath] || mcpAllProjects[projectPath] || {};
const projectServers = projectData.mcpServers || {}; const projectServers = projectData.mcpServers || {};
if (projectServers[mcpId]) return { installed: true, scope: 'project' }; if (projectServers[mcpId]) return { installed: true, scope: 'project' };
@@ -1321,6 +1326,8 @@ function openRecommendedMcpWizard(mcpId) {
} }
const hasFields = mcpDef.fields && mcpDef.fields.length > 0; const hasFields = mcpDef.fields && mcpDef.fields.length > 0;
const mcpName = t(mcpDef.nameKey);
const mcpDesc = t(mcpDef.descKey);
const modal = document.createElement('div'); const modal = document.createElement('div');
modal.id = 'recommendedMcpWizardModal'; modal.id = 'recommendedMcpWizardModal';
@@ -1334,8 +1341,8 @@ function openRecommendedMcpWizard(mcpId) {
<i data-lucide="${mcpDef.icon}" class="w-5 h-5 text-primary"></i> <i data-lucide="${mcpDef.icon}" class="w-5 h-5 text-primary"></i>
</div> </div>
<div> <div>
<h3 class="text-lg font-semibold text-foreground">${t('mcp.wizard.install')} ${escapeHtml(mcpDef.name)}</h3> <h3 class="text-lg font-semibold text-foreground">${t('mcp.wizard.install')} ${escapeHtml(mcpName)}</h3>
<p class="text-sm text-muted-foreground">${escapeHtml(mcpDef.description)}</p> <p class="text-sm text-muted-foreground">${escapeHtml(mcpDesc)}</p>
</div> </div>
</div> </div>
<button onclick="closeRecommendedMcpWizard()" class="text-muted-foreground hover:text-foreground"> <button onclick="closeRecommendedMcpWizard()" class="text-muted-foreground hover:text-foreground">
@@ -1350,10 +1357,10 @@ function openRecommendedMcpWizard(mcpId) {
${mcpDef.fields.map(field => ` ${mcpDef.fields.map(field => `
<div class="space-y-1.5"> <div class="space-y-1.5">
<label class="flex items-center gap-1.5 text-sm font-medium text-foreground"> <label class="flex items-center gap-1.5 text-sm font-medium text-foreground">
${escapeHtml(field.label)} ${escapeHtml(t(field.labelKey))}
${field.required ? '<span class="text-destructive">*</span>' : ''} ${field.required ? '<span class="text-destructive">*</span>' : ''}
</label> </label>
${field.description ? `<p class="text-xs text-muted-foreground">${escapeHtml(field.description)}</p>` : ''} ${field.descKey ? `<p class="text-xs text-muted-foreground">${escapeHtml(t(field.descKey))}</p>` : ''}
<input type="${field.type || 'text'}" <input type="${field.type || 'text'}"
id="wizard-field-${field.key}" id="wizard-field-${field.key}"
class="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" class="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary"
@@ -1471,7 +1478,7 @@ async function submitRecommendedMcpWizard(mcpId) {
const value = input ? input.value.trim() : ''; const value = input ? input.value.trim() : '';
if (field.required && !value) { if (field.required && !value) {
showRefreshToast(`${field.label} is required`, 'error'); showRefreshToast(`${t(field.labelKey)} is required`, 'error');
if (input) input.focus(); if (input) input.focus();
hasError = true; hasError = true;
break; break;
@@ -1498,7 +1505,7 @@ async function submitRecommendedMcpWizard(mcpId) {
} }
closeRecommendedMcpWizard(); closeRecommendedMcpWizard();
showRefreshToast(`${mcpDef.name} installed successfully`, 'success'); // Note: success toast is shown by the underlying API functions
} catch (err) { } catch (err) {
console.error(`Failed to install ${mcpDef.name}:`, err); console.error(`Failed to install ${mcpDef.name}:`, err);
showRefreshToast(`Failed to install ${mcpDef.name}: ${err.message}`, 'error'); showRefreshToast(`Failed to install ${mcpDef.name}: ${err.message}`, 'error');

View File

@@ -33,6 +33,8 @@ const i18n = {
'common.minutes': 'minutes', 'common.minutes': 'minutes',
'common.enabled': 'Enabled', 'common.enabled': 'Enabled',
'common.disabled': 'Disabled', 'common.disabled': 'Disabled',
'common.yes': 'Yes',
'common.no': 'No',
// Header // Header
'header.project': 'Project:', 'header.project': 'Project:',
@@ -908,6 +910,19 @@ const i18n = {
'mcp.wizard.installTo': 'Install to', 'mcp.wizard.installTo': 'Install to',
'mcp.wizard.project': 'Project', 'mcp.wizard.project': 'Project',
'mcp.wizard.global': 'Global', 'mcp.wizard.global': 'Global',
// Recommended MCP Server Definitions
'mcp.ace-tool.name': 'ACE Tool',
'mcp.ace-tool.desc': 'Augment Context Engine - Semantic code search with real-time codebase indexing',
'mcp.ace-tool.field.baseUrl': 'Base URL',
'mcp.ace-tool.field.baseUrl.desc': 'ACE MCP relay server URL',
'mcp.ace-tool.field.token': 'API Token',
'mcp.ace-tool.field.token.desc': 'Your ACE API token (get from ACE dashboard)',
'mcp.chrome-devtools.name': 'Chrome DevTools',
'mcp.chrome-devtools.desc': 'Browser automation and DevTools integration for web development',
'mcp.exa.name': 'Exa Search',
'mcp.exa.desc': 'AI-powered web search with real-time crawling and content extraction',
'mcp.exa.field.apiKey': 'EXA API Key',
'mcp.exa.field.apiKey.desc': 'Optional - Free tier has rate limits. Get key from exa.ai for higher limits',
// MCP CLI Mode // MCP CLI Mode
'mcp.cliMode': 'CLI Mode', 'mcp.cliMode': 'CLI Mode',
@@ -1757,6 +1772,33 @@ const i18n = {
'apiSettings.nameRequired': 'Name is required', 'apiSettings.nameRequired': 'Name is required',
'apiSettings.status': 'Status', 'apiSettings.status': 'Status',
// Model Pools (High Availability)
'apiSettings.modelPools': 'Model Pools',
'apiSettings.addModelPool': 'Add Model Pool',
'apiSettings.editModelPool': 'Edit Model Pool',
'apiSettings.poolName': 'Pool Name',
'apiSettings.modelType': 'Model Type',
'apiSettings.embedding': 'Embedding',
'apiSettings.llm': 'LLM',
'apiSettings.reranker': 'Reranker',
'apiSettings.embeddingPools': 'Embedding Pools',
'apiSettings.llmPools': 'LLM Pools',
'apiSettings.rerankerPools': 'Reranker Pools',
'apiSettings.cooldown': 'Cooldown',
'apiSettings.maxConcurrent': 'Max Concurrent',
'apiSettings.enablePool': 'Enable Pool',
'apiSettings.autoDiscoverProviders': 'Auto-discover Providers',
'apiSettings.excludedProviders': 'Excluded Providers',
'apiSettings.noPoolSelected': 'No Pool Selected',
'apiSettings.selectPoolFromList': 'Select a pool from the list to view details',
'apiSettings.noPoolsConfigured': 'No model pools configured',
'apiSettings.poolCreated': 'Pool created successfully',
'apiSettings.poolDeleted': 'Pool deleted successfully',
'apiSettings.poolUpdated': 'Pool updated successfully',
'apiSettings.confirmDeletePool': 'Are you sure you want to delete this pool?',
'apiSettings.legacyPool': 'Legacy',
'apiSettings.pool': 'Pool',
// Common // Common
'common.cancel': 'Cancel', 'common.cancel': 'Cancel',
'common.optional': '(Optional)', 'common.optional': '(Optional)',
@@ -2116,6 +2158,8 @@ const i18n = {
'common.minutes': '分钟', 'common.minutes': '分钟',
'common.enabled': '已启用', 'common.enabled': '已启用',
'common.disabled': '已禁用', 'common.disabled': '已禁用',
'common.yes': '是',
'common.no': '否',
// Header // Header
'header.project': '项目:', 'header.project': '项目:',
@@ -2970,6 +3014,19 @@ const i18n = {
'mcp.wizard.installTo': '安装到', 'mcp.wizard.installTo': '安装到',
'mcp.wizard.project': '项目', 'mcp.wizard.project': '项目',
'mcp.wizard.global': '全局', 'mcp.wizard.global': '全局',
// Recommended MCP Server Definitions
'mcp.ace-tool.name': 'ACE 工具',
'mcp.ace-tool.desc': 'Augment 上下文引擎 - 实时代码库索引的语义代码搜索',
'mcp.ace-tool.field.baseUrl': '服务器地址',
'mcp.ace-tool.field.baseUrl.desc': 'ACE MCP 中继服务器 URL',
'mcp.ace-tool.field.token': 'API 令牌',
'mcp.ace-tool.field.token.desc': '从 ACE 控制台获取您的 API 令牌',
'mcp.chrome-devtools.name': 'Chrome 开发工具',
'mcp.chrome-devtools.desc': '浏览器自动化和开发者工具集成,用于 Web 开发',
'mcp.exa.name': 'Exa 搜索',
'mcp.exa.desc': 'AI 驱动的网络搜索,支持实时爬取和内容提取',
'mcp.exa.field.apiKey': 'EXA API 密钥',
'mcp.exa.field.apiKey.desc': '可选 - 免费版有速率限制,从 exa.ai 获取密钥可提高配额',
// MCP CLI Mode // MCP CLI Mode
'mcp.cliMode': 'CLI 模式', 'mcp.cliMode': 'CLI 模式',
@@ -3850,6 +3907,33 @@ const i18n = {
'apiSettings.tokenRequired': 'API 令牌为必填项', 'apiSettings.tokenRequired': 'API 令牌为必填项',
'apiSettings.status': '状态', 'apiSettings.status': '状态',
// Model Pools (High Availability)
'apiSettings.modelPools': '高可用',
'apiSettings.addModelPool': '添加模型池',
'apiSettings.editModelPool': '编辑模型池',
'apiSettings.poolName': '池名称',
'apiSettings.modelType': '模型类型',
'apiSettings.embedding': '嵌入',
'apiSettings.llm': 'LLM',
'apiSettings.reranker': '重排',
'apiSettings.embeddingPools': '嵌入池',
'apiSettings.llmPools': 'LLM 池',
'apiSettings.rerankerPools': '重排池',
'apiSettings.cooldown': '冷却时间',
'apiSettings.maxConcurrent': '最大并发',
'apiSettings.enablePool': '启用池',
'apiSettings.autoDiscoverProviders': '自动发现供应商',
'apiSettings.excludedProviders': '排除的供应商',
'apiSettings.noPoolSelected': '未选择池',
'apiSettings.selectPoolFromList': '从列表中选择一个池以查看详情',
'apiSettings.noPoolsConfigured': '未配置模型池',
'apiSettings.poolCreated': '池创建成功',
'apiSettings.poolDeleted': '池删除成功',
'apiSettings.poolUpdated': '池更新成功',
'apiSettings.confirmDeletePool': '确定要删除此池吗?',
'apiSettings.legacyPool': '旧版',
'apiSettings.pool': '池',
// Common // Common
'common.cancel': '取消', 'common.cancel': '取消',
'common.optional': '(可选)', 'common.optional': '(可选)',

View File

@@ -1157,22 +1157,19 @@ async function renderApiSettings() {
// Build sidebar tabs HTML // Build sidebar tabs HTML
var sidebarTabsHtml = '<div class="sidebar-tabs">' + var sidebarTabsHtml = '<div class="sidebar-tabs">' +
'<button class="sidebar-tab' + (activeSidebarTab === 'providers' ? ' active' : '') + '" onclick="switchSidebarTab(\'providers\')">' + '<button class="sidebar-tab' + (activeSidebarTab === 'providers' ? ' active' : '') + '" onclick="switchSidebarTab(\'providers\')">' +
'<i data-lucide="server"></i> ' + t('apiSettings.providers') + '<i data-lucide="server"></i><span>' + t('apiSettings.providers') + '</span>' +
'</button>' + '</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'endpoints' ? ' active' : '') + '" onclick="switchSidebarTab(\'endpoints\')">' + '<button class="sidebar-tab' + (activeSidebarTab === 'endpoints' ? ' active' : '') + '" onclick="switchSidebarTab(\'endpoints\')">' +
'<i data-lucide="link"></i> ' + t('apiSettings.endpoints') + '<i data-lucide="link"></i><span>' + t('apiSettings.endpoints') + '</span>' +
'</button>' + '</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'cli-settings' ? ' active' : '') + '" onclick="switchSidebarTab(\'cli-settings\')">' + '<button class="sidebar-tab' + (activeSidebarTab === 'cli-settings' ? ' active' : '') + '" onclick="switchSidebarTab(\'cli-settings\')">' +
'<i data-lucide="settings"></i> ' + t('apiSettings.cliSettings') + '<i data-lucide="settings"></i><span>' + t('apiSettings.cliSettings') + '</span>' +
'</button>' + '</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'model-pools' ? ' active' : '') + '" onclick="switchSidebarTab(\'model-pools\')">' + '<button class="sidebar-tab' + (activeSidebarTab === 'model-pools' ? ' active' : '') + '" onclick="switchSidebarTab(\'model-pools\')">' +
'<i data-lucide="layers"></i> ' + t('apiSettings.modelPools') + '<i data-lucide="layers"></i><span>' + t('apiSettings.modelPools') + '</span>' +
'</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'embedding-pool' ? ' active' : '') + '" onclick="switchSidebarTab(\'embedding-pool\')">' +
'<i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') + ' (Legacy)' +
'</button>' + '</button>' +
'<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' + '<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' +
'<i data-lucide="database"></i> ' + t('apiSettings.cache') + '<i data-lucide="database"></i><span>' + t('apiSettings.cache') + '</span>' +
'</button>' + '</button>' +
'</div>'; '</div>';
@@ -4152,7 +4149,7 @@ function renderModelPoolDetail(poolId) {
'<div class="provider-detail-header">' + '<div class="provider-detail-header">' +
'<div>' + '<div>' +
'<h2>' + escapeHtml(pool.name || pool.targetModel) + '</h2>' + '<h2>' + escapeHtml(pool.name || pool.targetModel) + '</h2>' +
'<p style="color: var(--text-secondary); margin-top: 0.5rem;">' + typeLabel + ' Pool</p>' + '<p style="color: var(--text-secondary); margin-top: 0.5rem;">' + typeLabel + t('apiSettings.pool') + '</p>' +
'</div>' + '</div>' +
'<div class="provider-actions">' + '<div class="provider-actions">' +
'<button class="btn btn-secondary" onclick="editModelPool(\'' + pool.id + '\')"><i data-lucide="edit-2"></i> ' + t('common.edit') + '</button>' + '<button class="btn btn-secondary" onclick="editModelPool(\'' + pool.id + '\')"><i data-lucide="edit-2"></i> ' + t('common.edit') + '</button>' +

View File

@@ -566,6 +566,8 @@ async function renderMcpManager() {
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
${getRecommendedMcpServers().map(mcp => { ${getRecommendedMcpServers().map(mcp => {
const installStatus = isRecommendedMcpInstalled(mcp.id); const installStatus = isRecommendedMcpInstalled(mcp.id);
const mcpName = t(mcp.nameKey);
const mcpDesc = t(mcp.descKey);
return ` return `
<div class="recommended-mcp-card bg-card border ${installStatus.installed ? 'border-success/50' : 'border-border'} rounded-lg p-4 hover:shadow-md transition-all"> <div class="recommended-mcp-card bg-card border ${installStatus.installed ? 'border-success/50' : 'border-border'} rounded-lg p-4 hover:shadow-md transition-all">
<div class="flex items-start justify-between mb-3"> <div class="flex items-start justify-between mb-3">
@@ -574,7 +576,7 @@ async function renderMcpManager() {
<i data-lucide="${mcp.icon}" class="w-5 h-5 ${installStatus.installed ? 'text-success' : 'text-primary'}"></i> <i data-lucide="${mcp.icon}" class="w-5 h-5 ${installStatus.installed ? 'text-success' : 'text-primary'}"></i>
</div> </div>
<div> <div>
<h4 class="font-semibold text-foreground">${escapeHtml(mcp.name)}</h4> <h4 class="font-semibold text-foreground">${escapeHtml(mcpName)}</h4>
<span class="text-xs px-1.5 py-0.5 bg-muted text-muted-foreground rounded">${mcp.category}</span> <span class="text-xs px-1.5 py-0.5 bg-muted text-muted-foreground rounded">${mcp.category}</span>
</div> </div>
</div> </div>
@@ -585,7 +587,7 @@ async function renderMcpManager() {
</span> </span>
` : ''} ` : ''}
</div> </div>
<p class="text-sm text-muted-foreground mb-4 line-clamp-2">${escapeHtml(mcp.description)}</p> <p class="text-sm text-muted-foreground mb-4 line-clamp-2">${escapeHtml(mcpDesc)}</p>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
${mcp.fields.length > 0 ? ` ${mcp.fields.length > 0 ? `
<span class="text-xs text-muted-foreground flex items-center gap-1"> <span class="text-xs text-muted-foreground flex items-center gap-1">

View File

@@ -216,6 +216,55 @@ function ensureToolTags(tool: Partial<ClaudeCliTool>): 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: * Load CLI tools configuration with fallback:
* 1. Project: {projectDir}/.claude/cli-tools.json * 1. Project: {projectDir}/.claude/cli-tools.json

View File

@@ -19,7 +19,8 @@ export type CliOutputUnitType =
| 'file_diff' // File modification diff | 'file_diff' // File modification diff
| 'progress' // Progress updates | 'progress' // Progress updates
| 'metadata' // Session/execution metadata | '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 * 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 ========== // ========== Codex CLI --json format ==========
// {"type":"thread.started","thread_id":"..."} // {"type":"thread.started","thread_id":"..."}
// {"type":"turn.started"} // {"type":"turn.started"}
@@ -733,6 +766,20 @@ export function flattenOutputUnits(
} }
break; 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 'metadata':
case 'system': case 'system':
// Metadata and system events are typically excluded from prompt context // Metadata and system events are typically excluded from prompt context