feat(cli-manager): add CLI wrapper endpoints management and UI integration

- Introduced functions to load and toggle CLI wrapper endpoints from the API.
- Updated the CLI manager UI to display and manage CLI wrapper endpoints.
- Removed CodexLens and Semantic Search from the tools section, now managed in their dedicated pages.

feat(codexlens-manager): move File Watcher card to the CodexLens Manager page

- Relocated the File Watcher card from the right column to the main content area of the CodexLens Manager page.

refactor(claude-cli-tools): enhance CLI tools configuration and migration

- Added support for new tool types: 'cli-wrapper' and 'api-endpoint'.
- Updated migration logic to handle new tool types and preserve endpoint IDs.
- Deprecated previous custom endpoint handling in favor of the new structure.

feat(cli-executor-core): integrate CLI settings for custom endpoint execution

- Implemented execution logic for custom CLI封装 endpoints using settings files.
- Enhanced error handling and output logging for CLI executions.
- Updated tool identification logic to support both built-in tools and custom endpoints.
This commit is contained in:
catlog22
2026-01-12 09:35:05 +08:00
parent cefb934a2c
commit 1044886e7d
10 changed files with 1187 additions and 279 deletions

View File

@@ -1272,9 +1272,100 @@ select.cli-input {
letter-spacing: 0.03em;
}
/* Provider Item (used in CLI Settings list) */
.provider-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
transition: background 0.15s ease;
border: 1px solid transparent;
}
.provider-item:hover {
background: hsl(var(--muted) / 0.5);
}
.provider-item.selected {
background: hsl(var(--primary) / 0.1);
border-color: hsl(var(--primary) / 0.3);
}
.provider-item-content {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
flex: 1;
}
.provider-icon {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background: hsl(var(--muted) / 0.5);
color: hsl(var(--muted-foreground));
}
.provider-icon i,
.provider-icon svg {
width: 1rem;
height: 1rem;
}
.provider-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.provider-name {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.provider-type {
font-size: 0.6875rem;
color: hsl(var(--muted-foreground));
}
.provider-status {
display: flex;
align-items: center;
flex-shrink: 0;
}
.provider-status .status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: hsl(var(--muted-foreground));
}
.provider-status.enabled .status-dot {
background: hsl(142 76% 36%);
}
.provider-status.disabled .status-dot {
background: hsl(var(--muted-foreground) / 0.5);
}
.provider-list-footer {
padding: 1rem;
border-top: 1px solid hsl(var(--border));
margin-top: auto;
}
.btn-full {
@@ -1327,6 +1418,75 @@ select.cli-input {
gap: 1.5rem;
}
/* Detail Section (for CLI Settings, etc.) */
.detail-section {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 0.5rem;
padding: 1rem 1.25rem;
}
.detail-section h3 {
margin: 0 0 1rem 0;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.detail-item label {
font-size: 0.75rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
text-transform: uppercase;
letter-spacing: 0.02em;
}
.detail-item span {
font-size: 0.875rem;
color: hsl(var(--foreground));
word-break: break-all;
}
.detail-item span.mono {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.8125rem;
background: hsl(var(--muted) / 0.3);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.detail-item-full {
grid-column: 1 / -1;
}
/* Code Block */
.code-block {
background: hsl(var(--muted) / 0.3);
border: 1px solid hsl(var(--border));
border-radius: 0.375rem;
padding: 0.75rem 1rem;
overflow-x: auto;
}
.code-block code {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.8125rem;
color: hsl(var(--foreground));
white-space: nowrap;
}
/* Field Groups */
.field-group {
display: flex;
@@ -1927,6 +2087,26 @@ select.cli-input {
margin-bottom: 0.75rem;
}
/* ===========================
CLI Settings List in Sidebar
=========================== */
.cli-settings-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
/* ===========================
Model Pools List in Sidebar
=========================== */
.model-pools-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
/* ===========================
Main Panel Sections
=========================== */
@@ -2763,6 +2943,57 @@ select.cli-input {
color: hsl(var(--muted-foreground));
}
/* Parse JSON link in footer */
.json-parse-link {
font-size: 0.75rem;
color: hsl(var(--primary));
text-decoration: none;
cursor: pointer;
transition: color 0.2s ease;
}
.json-parse-link:hover {
color: hsl(var(--primary) / 0.8);
text-decoration: underline;
}
/* Input with toggle button (for password visibility) */
.input-with-toggle {
position: relative;
display: flex;
align-items: center;
}
.input-with-toggle .form-control {
flex: 1;
padding-right: 2.5rem;
}
.input-with-toggle .toggle-password {
position: absolute;
right: 0.25rem;
top: 50%;
transform: translateY(-50%);
padding: 0.25rem 0.5rem;
background: transparent;
border: none;
color: hsl(var(--muted-foreground));
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.input-with-toggle .toggle-password:hover {
color: hsl(var(--foreground));
}
.input-with-toggle .toggle-password i,
.input-with-toggle .toggle-password svg {
width: 16px;
height: 16px;
}
/* Button styles for JSON editor */
.btn-sm {
padding: 0.375rem 0.75rem;

View File

@@ -71,8 +71,8 @@ async function loadAllStatusesFallback() {
console.warn('[CLI Status] Using fallback individual API calls');
await Promise.all([
loadCliToolsConfig(), // Ensure config is loaded (auto-creates if missing)
loadCliToolStatus(),
loadCodexLensStatus()
loadCliToolStatus()
// CodexLens status removed - managed in dedicated CodexLens Manager page
]);
}
@@ -235,8 +235,8 @@ async function loadCliToolsConfig() {
const response = await fetch('/api/cli/tools-config');
if (!response.ok) return null;
const data = await response.json();
// Store full config and extract tools for backward compatibility
cliToolsConfig = data.tools || {};
// Store full config and extract tools object (data.tools is full config, data.tools.tools is the actual tools)
cliToolsConfig = data.tools?.tools || {};
window.claudeCliToolsConfig = data; // Full config available globally
// Load default tool from config
@@ -308,15 +308,17 @@ async function loadCliSettingsEndpoints() {
function updateCliBadge() {
const badge = document.getElementById('badgeCliTools');
if (badge) {
// Merge tools from both status and config to get complete list
const allTools = new Set([
...Object.keys(cliToolStatus),
...Object.keys(cliToolsConfig)
]);
// Only count builtin and cli-wrapper tools (exclude api-endpoint tools)
const cliTools = Object.keys(cliToolsConfig).filter(t => {
if (!t || t === '_configInfo') return false;
const config = cliToolsConfig[t];
// Include if: no type (legacy builtin), type is builtin, or type is cli-wrapper
return !config?.type || config.type === 'builtin' || config.type === 'cli-wrapper';
});
// Count available and enabled CLI tools
// Count available and enabled CLI tools only
let available = 0;
allTools.forEach(tool => {
cliTools.forEach(tool => {
const status = cliToolStatus[tool] || {};
const config = cliToolsConfig[tool] || { enabled: true };
if (status.available && config.enabled !== false) {
@@ -324,33 +326,12 @@ function updateCliBadge() {
}
});
// Also count CodexLens and Semantic Search
let totalExtras = 0;
let availableExtras = 0;
// CodexLens counts if ready
if (codexLensStatus.ready) {
totalExtras++;
availableExtras++;
} else if (codexLensStatus.ready === false) {
// Only count as total if we have status info (not just initial state)
totalExtras++;
}
// Semantic Search counts if CodexLens is ready (it's a feature of CodexLens)
if (codexLensStatus.ready) {
totalExtras++;
if (semanticStatus.available) {
availableExtras++;
}
}
const total = allTools.size + totalExtras;
const totalAvailable = available + availableExtras;
badge.textContent = `${totalAvailable}/${total}`;
badge.classList.toggle('text-success', totalAvailable === total && total > 0);
badge.classList.toggle('text-warning', totalAvailable > 0 && totalAvailable < total);
badge.classList.toggle('text-destructive', totalAvailable === 0);
// CLI tools badge shows only CLI tools count
const total = cliTools.length;
badge.textContent = `${available}/${total}`;
badge.classList.toggle('text-success', available === total && total > 0);
badge.classList.toggle('text-warning', available > 0 && available < total);
badge.classList.toggle('text-destructive', available === 0);
}
}
@@ -414,10 +395,16 @@ function renderCliStatus() {
};
// Get tools dynamically from config, merging with status for complete list
// Only show builtin and cli-wrapper tools in the tools grid (api-endpoint tools show in API Endpoints section)
const tools = [...new Set([
...Object.keys(cliToolsConfig),
...Object.keys(cliToolStatus)
])].filter(t => t && t !== '_configInfo'); // Filter out metadata keys
])].filter(t => {
if (!t || t === '_configInfo') return false;
const config = cliToolsConfig[t];
// Include if: no type (legacy builtin), type is builtin, or type is cli-wrapper
return !config?.type || config.type === 'builtin' || config.type === 'cli-wrapper';
});
const toolsHtml = tools.map(tool => {
const status = cliToolStatus[tool] || {};
@@ -516,74 +503,8 @@ function renderCliStatus() {
`;
}).join('');
// CodexLens card with semantic search info
const codexLensHtml = `
<div class="cli-tool-card tool-codexlens ${codexLensStatus.ready ? 'available' : 'unavailable'}">
<div class="cli-tool-header">
<span class="cli-tool-status ${codexLensStatus.ready ? 'status-available' : 'status-unavailable'}"></span>
<span class="cli-tool-name">CodexLens</span>
<span class="badge px-1.5 py-0.5 text-xs rounded bg-muted text-muted-foreground">Index</span>
</div>
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine'}
</div>
<div class="cli-tool-info mt-2">
${codexLensStatus.ready
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> v${codexLensStatus.version || 'installed'}</span>`
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
}
</div>
<div class="cli-tool-actions flex gap-2 mt-3">
${!codexLensStatus.ready
? `<button class="btn-sm btn-primary flex items-center gap-1" onclick="installCodexLens()">
<i data-lucide="download" class="w-3 h-3"></i> Install
</button>`
: `<button class="btn-sm btn-outline flex items-center gap-1" onclick="initCodexLensIndex()">
<i data-lucide="database" class="w-3 h-3"></i> Init Index
</button>
<button class="btn-sm btn-outline flex items-center gap-1" onclick="uninstallCodexLens()">
<i data-lucide="trash-2" class="w-3 h-3"></i> Uninstall
</button>`
}
</div>
</div>
`;
// Semantic Search card (only show if CodexLens is installed)
const semanticHtml = codexLensStatus.ready ? `
<div class="cli-tool-card tool-semantic ${semanticStatus.available ? 'available' : 'unavailable'}">
<div class="cli-tool-header">
<span class="cli-tool-status ${semanticStatus.available ? 'status-available' : 'status-unavailable'}"></span>
<span class="cli-tool-name">Semantic Search</span>
<span class="badge px-1.5 py-0.5 text-xs rounded ${semanticStatus.available ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'}">AI</span>
</div>
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search'}
</div>
<div class="cli-tool-info mt-2">
${semanticStatus.available
? `<span class="text-success flex items-center gap-1"><i data-lucide="sparkles" class="w-3 h-3"></i> ${semanticStatus.backend || 'Ready'}</span>`
: `<span class="text-muted-foreground flex items-center gap-1"><i data-lucide="circle-dashed" class="w-3 h-3"></i> Not Installed</span>`
}
</div>
<div class="cli-tool-actions flex flex-col gap-2 mt-3">
${!semanticStatus.available ? `
<button class="btn-sm btn-primary w-full flex items-center justify-center gap-1" onclick="openSemanticInstallWizard()">
<i data-lucide="brain" class="w-3 h-3"></i> Install AI Model
</button>
<div class="flex items-center gap-1 text-xs text-muted-foreground mt-1">
<i data-lucide="hard-drive" class="w-3 h-3"></i>
<span>~130MB</span>
</div>
` : `
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<i data-lucide="cpu" class="w-3 h-3"></i>
<span>bge-small-en-v1.5</span>
</div>
`}
</div>
</div>
` : '';
// CodexLens and Semantic Search removed from CLI status panel
// They are managed in the dedicated CodexLens Manager page
// CCW Installation Status card (show warning if not fully installed)
const ccwInstallHtml = !ccwInstallStatus.installed ? `
@@ -637,6 +558,9 @@ function renderCliStatus() {
<div class="cli-endpoint-info" style="margin-top: 0.25rem;">
<span class="text-xs text-muted-foreground" style="font-size: 0.75rem; color: var(--muted-foreground);">${ep.model}</span>
</div>
<div class="cli-endpoint-usage" style="margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border);">
<code style="font-size: 0.65rem; color: var(--muted-foreground); word-break: break-all;">--tool custom --model ${ep.id}</code>
</div>
</div>
`).join('')}
</div>
@@ -748,8 +672,6 @@ function renderCliStatus() {
${ccwInstallHtml}
<div class="cli-tools-grid">
${toolsHtml}
${codexLensHtml}
${semanticHtml}
</div>
${apiEndpointsHtml}
${settingsHtml}

View File

@@ -257,6 +257,10 @@ const i18n = {
'cli.addToCli': 'Add to CLI',
'cli.enabled': 'Enabled',
'cli.disabled': 'Disabled',
'cli.cliWrapper': 'CLI Wrapper',
'cli.wrapper': 'Wrapper',
'cli.customClaudeSettings': 'Custom Claude CLI settings',
'cli.updateFailed': 'Failed to update',
// CodexLens Configuration
'codexlens.config': 'CodexLens Configuration',
@@ -1618,7 +1622,7 @@ const i18n = {
'apiSettings.total': 'total',
'apiSettings.testConnection': 'Test Connection',
'apiSettings.endpointId': 'Endpoint ID',
'apiSettings.endpointIdHint': 'Usage: ccw cli -p "..." --model <endpoint-id>',
'apiSettings.endpointIdHint': 'Usage: ccw cli -p "..." --tool custom --model <endpoint-id> --mode analysis',
'apiSettings.endpoints': 'Endpoints',
'apiSettings.addEndpointHint': 'Create custom endpoint aliases for CLI usage',
'apiSettings.endpointModel': 'Model',
@@ -1752,12 +1756,15 @@ const i18n = {
'apiSettings.useModelTreeToManage': 'Use the model tree to manage individual models',
// CLI Settings
'apiSettings.cliSettings': 'CLI Settings',
'apiSettings.addCliSettings': 'Add CLI Settings',
'apiSettings.editCliSettings': 'Edit CLI Settings',
'apiSettings.noCliSettings': 'No CLI settings configured',
'apiSettings.noCliSettingsSelected': 'No CLI Settings Selected',
'apiSettings.cliSettingsHint': 'Select a CLI settings endpoint or create a new one',
'apiSettings.cliSettings': 'CLI Wrapper',
'apiSettings.addCliSettings': 'Add CLI Wrapper',
'apiSettings.editCliSettings': 'Edit CLI Wrapper',
'apiSettings.noCliSettings': 'No CLI wrapper configured',
'apiSettings.noCliSettingsSelected': 'No CLI Wrapper Selected',
'apiSettings.cliSettingsHint': 'Select a CLI wrapper endpoint or create a new one',
'apiSettings.showToken': 'Show',
'apiSettings.hideToken': 'Hide',
'apiSettings.syncFromJson': 'Parse JSON',
'apiSettings.cliProviderHint': 'Select an Anthropic provider to use its API key and base URL',
'apiSettings.noAnthropicProviders': 'No Anthropic providers configured. Please add one in the Providers tab first.',
'apiSettings.selectProviderFirst': 'Select a provider first',
@@ -1771,6 +1778,10 @@ const i18n = {
'apiSettings.envSettings': 'Environment Settings',
'apiSettings.settingsFilePath': 'Settings File Path',
'apiSettings.nameRequired': 'Name is required',
'apiSettings.nameInvalidFormat': 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores',
'apiSettings.nameTooLong': 'Name must be 32 characters or less',
'apiSettings.nameConflict': 'Name conflicts with built-in tool',
'apiSettings.nameFormatHint': 'Letters, numbers, hyphens, underscores only. Used as: ccw cli --tool [name]',
'apiSettings.status': 'Status',
'apiSettings.providerBinding': 'Provider Binding',
'apiSettings.directConfig': 'Direct Configuration',
@@ -2391,6 +2402,10 @@ const i18n = {
'cli.addToCli': '添加到 CLI',
'cli.enabled': '已启用',
'cli.disabled': '已禁用',
'cli.cliWrapper': 'CLI 封装',
'cli.wrapper': '封装',
'cli.customClaudeSettings': '自定义 Claude CLI 配置',
'cli.updateFailed': '更新失败',
// CodexLens 配置
'codexlens.config': 'CodexLens 配置',
@@ -3761,7 +3776,7 @@ const i18n = {
'apiSettings.total': '总计',
'apiSettings.testConnection': '测试连接',
'apiSettings.endpointId': '端点 ID',
'apiSettings.endpointIdHint': '用法: ccw cli -p "..." --model <端点ID>',
'apiSettings.endpointIdHint': '用法: ccw cli -p "..." --tool custom --model <端点ID> --mode analysis',
'apiSettings.endpoints': '端点',
'apiSettings.addEndpointHint': '创建用于 CLI 的自定义端点别名',
'apiSettings.endpointModel': '模型',
@@ -3895,12 +3910,15 @@ const i18n = {
'apiSettings.useModelTreeToManage': '使用模型树管理各个模型',
// CLI Settings
'apiSettings.cliSettings': 'CLI 配置',
'apiSettings.addCliSettings': '添加 CLI 配置',
'apiSettings.editCliSettings': '编辑 CLI 配置',
'apiSettings.noCliSettings': '未配置 CLI 设置',
'apiSettings.noCliSettingsSelected': '未选择 CLI 配置',
'apiSettings.cliSettingsHint': '选择一个 CLI 配置端点或创建新的',
'apiSettings.cliSettings': 'CLI 封装',
'apiSettings.addCliSettings': '添加 CLI 封装',
'apiSettings.editCliSettings': '编辑 CLI 封装',
'apiSettings.noCliSettings': '未配置 CLI 封装',
'apiSettings.noCliSettingsSelected': '未选择 CLI 封装',
'apiSettings.cliSettingsHint': '选择一个 CLI 封装端点或创建新的',
'apiSettings.showToken': '显示',
'apiSettings.hideToken': '隐藏',
'apiSettings.syncFromJson': '解析 JSON',
'apiSettings.cliProviderHint': '选择一个 Anthropic 供应商以使用其 API 密钥和基础 URL',
'apiSettings.noAnthropicProviders': '未配置 Anthropic 供应商。请先在供应商标签页中添加。',
'apiSettings.selectProviderFirst': '请先选择供应商',
@@ -3914,6 +3932,10 @@ const i18n = {
'apiSettings.envSettings': '环境变量设置',
'apiSettings.settingsFilePath': '配置文件路径',
'apiSettings.nameRequired': '名称为必填项',
'apiSettings.nameInvalidFormat': '名称必须以字母开头,只能包含字母、数字、连字符和下划线',
'apiSettings.nameTooLong': '名称长度不能超过32个字符',
'apiSettings.nameConflict': '名称与内置工具冲突',
'apiSettings.nameFormatHint': '仅限字母、数字、连字符、下划线。用于命令: ccw cli --tool [名称]',
'apiSettings.tokenRequired': 'API 令牌为必填项',
'apiSettings.status': '状态',
'apiSettings.providerBinding': '供应商绑定',

View File

@@ -1150,6 +1150,13 @@ async function renderApiSettings() {
// Load data (use cache by default, forceRefresh=false)
await loadApiSettings(false);
// Handle pending CLI wrapper edit from status page navigation
if (window.pendingCliWrapperEdit) {
activeSidebarTab = 'cli-settings';
selectedCliSettingsId = window.pendingCliWrapperEdit;
window.pendingCliWrapperEdit = null; // Clear the pending edit flag
}
if (!apiSettingsData) {
container.innerHTML = '<div class="api-settings-container">' +
'<div class="error-message">' + t('apiSettings.failedToLoad') + '</div>' +
@@ -2707,7 +2714,7 @@ function renderEndpointsList() {
'</div>' +
'<div class="usage-hint">' +
'<i data-lucide="terminal"></i>' +
'<code>ccw cli -p "..." --model ' + endpoint.id + '</code>' +
'<code>ccw cli -p "..." --tool custom --model ' + endpoint.id + ' --mode analysis</code>' +
'</div>' +
'</div>' +
'</div>';
@@ -3945,7 +3952,8 @@ function renderCliSettingsForm(existingEndpoint) {
var commonFieldsHtml =
'<div class="form-group">' +
'<label for="cli-settings-name">' + t('apiSettings.endpointName') + ' *</label>' +
'<input type="text" id="cli-settings-name" class="form-control" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" placeholder="My Claude Endpoint" required />' +
'<input type="text" id="cli-settings-name" class="form-control" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" placeholder="my-claude-endpoint" required pattern="^[a-zA-Z][a-zA-Z0-9_-]*$" maxlength="32" />' +
'<small class="form-hint">' + (t('apiSettings.nameFormatHint') || 'Letters, numbers, hyphens, underscores only. Used as: ccw cli --tool [name]') + '</small>' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-settings-description">' + t('apiSettings.description') + '</label>' +
@@ -4097,7 +4105,12 @@ function renderDirectModeContent(container, env, settings) {
container.innerHTML =
'<div class="form-group">' +
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
'<div class="input-with-toggle">' +
'<input type="password" id="cli-auth-token" class="form-control" placeholder="sk-ant-..." value="' + escapeHtml(env.ANTHROPIC_AUTH_TOKEN || '') + '" />' +
'<button type="button" class="btn btn-sm btn-ghost toggle-password" onclick="toggleAuthTokenVisibility()" title="' + (t('apiSettings.showToken') || 'Show') + '">' +
'<i data-lucide="eye" id="cli-auth-token-icon"></i>' +
'</button>' +
'</div>' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-base-url">ANTHROPIC_BASE_URL</label>' +
@@ -4151,7 +4164,7 @@ function buildJsonEditorSection(settings) {
'</div>' +
'<div class="json-editor-footer">' +
'<span class="json-status" id="cli-json-status"></span>' +
'<span class="json-hint">' + (t('apiSettings.jsonEditorHint') || 'Edit JSON directly to add advanced settings') + '</span>' +
'<a href="javascript:void(0)" class="json-parse-link" onclick="syncJsonToForm()">' + (t('apiSettings.syncFromJson') || 'Parse JSON') + '</a>' +
'</div>' +
'</div>';
}
@@ -4267,6 +4280,39 @@ function validateCliJson() {
}
}
/**
* Validate CLI endpoint name for CLI compatibility
* Name must be: start with letter, alphanumeric with hyphens/underscores, no spaces
*/
function validateCliEndpointName(name) {
if (!name || name.trim().length === 0) {
return { valid: false, error: t('apiSettings.nameRequired') || 'Name is required' };
}
// Check for valid characters: a-z, A-Z, 0-9, hyphen, underscore
var validPattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
if (!validPattern.test(name)) {
return {
valid: false,
error: t('apiSettings.nameInvalidFormat') || 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores'
};
}
// Check length
if (name.length > 32) {
return { valid: false, error: t('apiSettings.nameTooLong') || 'Name must be 32 characters or less' };
}
// Check if name conflicts with built-in tools
var builtinTools = ['gemini', 'qwen', 'codex', 'claude', 'opencode', 'litellm'];
if (builtinTools.indexOf(name.toLowerCase()) !== -1) {
return { valid: false, error: (t('apiSettings.nameConflict') || 'Name conflicts with built-in tool') + ': ' + name };
}
return { valid: true };
}
window.validateCliEndpointName = validateCliEndpointName;
/**
* Format JSON in editor
*/
@@ -4331,6 +4377,87 @@ function syncFormToJson() {
}
window.syncFormToJson = syncFormToJson;
/**
* Toggle ANTHROPIC_AUTH_TOKEN visibility
*/
function toggleAuthTokenVisibility() {
var input = document.getElementById('cli-auth-token');
var icon = document.getElementById('cli-auth-token-icon');
var btn = input ? input.parentElement.querySelector('.toggle-password') : null;
if (!input || !icon) return;
if (input.type === 'password') {
input.type = 'text';
icon.setAttribute('data-lucide', 'eye-off');
if (btn) btn.title = t('apiSettings.hideToken') || 'Hide';
} else {
input.type = 'password';
icon.setAttribute('data-lucide', 'eye');
if (btn) btn.title = t('apiSettings.showToken') || 'Show';
}
if (window.lucide) lucide.createIcons();
}
window.toggleAuthTokenVisibility = toggleAuthTokenVisibility;
/**
* Sync JSON editor content to form fields
* Parses JSON and fills ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL and model fields
*/
function syncJsonToForm() {
var editor = document.getElementById('cli-json-editor');
if (!editor) return;
var jsonObj;
try {
jsonObj = JSON.parse(editor.value);
} catch (e) {
showRefreshToast(t('apiSettings.jsonInvalid') || 'Invalid JSON', 'error');
return;
}
var env = jsonObj.env || {};
// Fill ANTHROPIC_AUTH_TOKEN (only in direct mode and only if not masked)
if (cliConfigMode === 'direct') {
var authTokenInput = document.getElementById('cli-auth-token');
if (authTokenInput && env.ANTHROPIC_AUTH_TOKEN) {
// Only fill if the value is not masked (doesn't end with '...')
if (!env.ANTHROPIC_AUTH_TOKEN.endsWith('...')) {
authTokenInput.value = env.ANTHROPIC_AUTH_TOKEN;
}
}
var baseUrlInput = document.getElementById('cli-base-url');
if (baseUrlInput && env.ANTHROPIC_BASE_URL !== undefined) {
baseUrlInput.value = env.ANTHROPIC_BASE_URL || '';
}
}
// Fill model configuration fields
var modelDefault = document.getElementById('cli-model-default');
var modelHaiku = document.getElementById('cli-model-haiku');
var modelSonnet = document.getElementById('cli-model-sonnet');
var modelOpus = document.getElementById('cli-model-opus');
if (modelDefault && env.ANTHROPIC_MODEL !== undefined) {
modelDefault.value = env.ANTHROPIC_MODEL || '';
}
if (modelHaiku && env.ANTHROPIC_DEFAULT_HAIKU_MODEL !== undefined) {
modelHaiku.value = env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '';
}
if (modelSonnet && env.ANTHROPIC_DEFAULT_SONNET_MODEL !== undefined) {
modelSonnet.value = env.ANTHROPIC_DEFAULT_SONNET_MODEL || '';
}
if (modelOpus && env.ANTHROPIC_DEFAULT_OPUS_MODEL !== undefined) {
modelOpus.value = env.ANTHROPIC_DEFAULT_OPUS_MODEL || '';
}
showRefreshToast(t('common.success') || 'Success', 'success');
}
window.syncJsonToForm = syncJsonToForm;
/**
* Get settings from JSON editor (merges with form data)
*/
@@ -4369,6 +4496,13 @@ async function submitCliSettingsForm() {
return;
}
// Validate name format for CLI compatibility
var nameValidation = validateCliEndpointName(name);
if (!nameValidation.valid) {
showRefreshToast(nameValidation.error, 'error');
return;
}
var data = {
name: name,
description: description,
@@ -4603,7 +4737,8 @@ function showAddCliSettingsModal(existingEndpoint) {
(isEdit ? '<input type="hidden" id="cli-settings-id" value="' + existingEndpoint.id + '">' : '') +
'<div class="form-group">' +
'<label for="cli-settings-name">' + t('apiSettings.endpointName') + ' *</label>' +
'<input type="text" id="cli-settings-name" class="cli-input" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" required />' +
'<input type="text" id="cli-settings-name" class="cli-input" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" required pattern="^[a-zA-Z][a-zA-Z0-9_-]*$" maxlength="32" />' +
'<small class="form-hint">' + (t('apiSettings.nameFormatHint') || 'Letters, numbers, hyphens, underscores only. Used as: ccw cli --tool [name]') + '</small>' +
'</div>' +
'<div class="form-group">' +
'<label for="cli-settings-description">' + t('apiSettings.description') + '</label>' +
@@ -4674,6 +4809,13 @@ async function submitCliSettings() {
return;
}
// Validate name format for CLI compatibility
var nameValidation = validateCliEndpointName(name);
if (!nameValidation.valid) {
showRefreshToast(nameValidation.error, 'error');
return;
}
if (!providerId) {
showRefreshToast(t('apiSettings.providerRequired'), 'error');
return;

View File

@@ -6,6 +6,7 @@ var currentCliExecution = null;
var cliExecutionOutput = '';
var ccwInstallations = [];
var ccwEndpointTools = [];
var cliWrapperEndpoints = []; // CLI封装 endpoints from /api/cli/settings
var cliToolConfig = null; // Store loaded CLI config
var predefinedModels = {}; // Store predefined models per tool
@@ -193,6 +194,46 @@ async function loadCliCustomEndpoints() {
}
}
// ========== CLI Wrapper Endpoints (CLI封装) ==========
async function loadCliWrapperEndpoints() {
try {
var response = await fetch('/api/cli/settings');
if (!response.ok) throw new Error('Failed to load CLI wrapper endpoints');
var data = await response.json();
cliWrapperEndpoints = data.endpoints || [];
return cliWrapperEndpoints;
} catch (err) {
console.error('Failed to load CLI wrapper endpoints:', err);
cliWrapperEndpoints = [];
return [];
}
}
async function toggleCliWrapperEnabled(endpointId, enabled) {
try {
await initCsrfToken();
var response = await csrfFetch('/api/cli/settings/' + endpointId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
});
if (!response.ok) throw new Error('Failed to update CLI wrapper endpoint');
var data = await response.json();
if (data.success) {
// Update local state
var idx = cliWrapperEndpoints.findIndex(function(e) { return e.id === endpointId; });
if (idx >= 0) {
cliWrapperEndpoints[idx].enabled = enabled;
}
showRefreshToast((enabled ? t('cli.enabled') || 'Enabled' : t('cli.disabled') || 'Disabled') + ': ' + endpointId, 'success');
}
return data;
} catch (err) {
showRefreshToast((t('cli.updateFailed') || 'Failed to update') + ': ' + err.message, 'error');
throw err;
}
}
async function toggleEndpointEnabled(endpointId, enabled) {
try {
await initCsrfToken();
@@ -628,7 +669,8 @@ async function renderCliManager() {
loadCcwInstallations(),
loadCcwEndpointTools(),
loadLitellmApiEndpoints(),
loadCliCustomEndpoints()
loadCliCustomEndpoints(),
loadCliWrapperEndpoints()
]);
container.innerHTML = '<div class="status-manager">' +
@@ -764,44 +806,8 @@ function renderToolsSection() {
'</div>';
}).join('');
// CodexLens item - simplified view with link to manager page
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="navigateToCodexLensManager()">' +
'<div class="tool-item-left">' +
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
'<div class="tool-item-info">' +
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span>' +
'<i data-lucide="external-link" class="w-3 h-3 tool-config-icon"></i></div>' +
'<div class="tool-item-desc">' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '</div>' +
'</div>' +
'</div>' +
'<div class="tool-item-right">' +
(codexLensStatus.ready
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>'
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>') +
'</div>' +
'</div>';
// Semantic Search item (only show if CodexLens is installed)
var semanticHtml = '';
if (codexLensStatus.ready) {
semanticHtml = '<div class="tool-item ' + (semanticStatus.available ? 'available' : 'unavailable') + '">' +
'<div class="tool-item-left">' +
'<span class="tool-status-dot ' + (semanticStatus.available ? 'status-available' : 'status-unavailable') + '"></span>' +
'<div class="tool-item-info">' +
'<div class="tool-item-name">Semantic Search <span class="tool-type-badge ai">AI</span></div>' +
'<div class="tool-item-desc">' + (semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search') + '</div>' +
'</div>' +
'</div>' +
'<div class="tool-item-right">' +
(semanticStatus.available
? '<span class="tool-status-text success"><i data-lucide="sparkles" class="w-3.5 h-3.5"></i> ' + (semanticStatus.backend || 'Ready') + '</span>'
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> Not Installed</span>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); openSemanticInstallWizard()"><i data-lucide="brain" class="w-3 h-3"></i> Install</button>') +
'</div>' +
'</div>';
}
// CodexLens and Semantic Search removed from this list
// They are managed in the dedicated CodexLens Manager page (left menu)
// API Endpoints section
var apiEndpointsHtml = '';
@@ -848,6 +854,45 @@ function renderToolsSection() {
'</div>';
}
// CLI Wrapper (CLI封装) section
var cliWrapperHtml = '';
if (cliWrapperEndpoints.length > 0) {
var wrapperItems = cliWrapperEndpoints.map(function(endpoint) {
var isEnabled = endpoint.enabled !== false;
var desc = endpoint.description || (t('cli.customClaudeSettings') || 'Custom Claude CLI settings');
// Show command hint with name for easy copying
var commandHint = 'ccw cli --tool ' + endpoint.name;
return '<div class="tool-item clickable ' + (isEnabled ? 'available' : 'unavailable') + '" onclick="navigateToApiSettings(\'' + endpoint.id + '\')">' +
'<div class="tool-item-left">' +
'<span class="tool-status-dot ' + (isEnabled ? 'status-available' : 'status-unavailable') + '"></span>' +
'<div class="tool-item-info">' +
'<div class="tool-item-name">' + escapeHtml(endpoint.name) + ' <span class="tool-type-badge" style="background: var(--primary); color: white;">' + (t('cli.wrapper') || 'Wrapper') + '</span></div>' +
'<div class="tool-item-desc">' + escapeHtml(desc) + '</div>' +
'<div class="tool-item-command" style="font-family: var(--font-mono); font-size: 0.7rem; color: var(--muted-foreground); margin-top: 0.25rem;">' + escapeHtml(commandHint) + '</div>' +
'</div>' +
'</div>' +
'<div class="tool-item-right">' +
'<label class="toggle-switch" onclick="event.stopPropagation()">' +
'<input type="checkbox" ' + (isEnabled ? 'checked' : '') + ' onchange="toggleCliWrapperEnabled(\'' + endpoint.id + '\', this.checked); renderToolsSection();">' +
'<span class="toggle-slider"></span>' +
'</label>' +
'</div>' +
'</div>';
}).join('');
var enabledCount = cliWrapperEndpoints.filter(function(e) { return e.enabled !== false; }).length;
cliWrapperHtml = '<div class="tools-subsection" style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border);">' +
'<div class="section-header-left" style="margin-bottom: 0.5rem;">' +
'<h4 style="font-size: 0.875rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem;">' +
'<i data-lucide="package-2" class="w-4 h-4"></i> ' + (t('cli.cliWrapper') || 'CLI Wrapper') +
'</h4>' +
'<span class="section-count">' + enabledCount + '/' + cliWrapperEndpoints.length + ' ' + (t('cli.enabled') || 'enabled') + '</span>' +
'</div>' +
'<div class="tools-list">' + wrapperItems + '</div>' +
'</div>';
}
container.innerHTML = '<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="terminal" class="w-4 h-4"></i> ' + t('cli.tools') + '</h3>' +
@@ -859,14 +904,34 @@ function renderToolsSection() {
'</div>' +
'<div class="tools-list">' +
toolsHtml +
codexLensHtml +
semanticHtml +
'</div>' +
apiEndpointsHtml;
apiEndpointsHtml +
cliWrapperHtml;
if (window.lucide) lucide.createIcons();
}
/**
* Navigate to API Settings page and open the CLI wrapper endpoint for editing
*/
function navigateToApiSettings(endpointId) {
// Store the endpoint ID to edit after navigation
window.pendingCliWrapperEdit = endpointId;
var navItem = document.querySelector('.nav-item[data-view="api-settings"]');
if (navItem) {
navItem.click();
} else {
// Fallback: try to render directly
if (typeof renderApiSettings === 'function') {
currentView = 'api-settings';
renderApiSettings();
} else {
showRefreshToast(t('common.error') + ': API Settings not available', 'error');
}
}
}
// ========== CCW Section (Right Column) ==========
function renderCcwSection() {
var container = document.getElementById('ccw-section');

View File

@@ -4497,6 +4497,53 @@ function buildCodexLensManagerPage(config) {
'<div class="text-sm text-muted-foreground">Click Load to view/edit ~/.codexlens/.env</div>' +
'</div>' +
'</div>' +
// File Watcher Card (moved from right column)
'<div class="bg-card border border-border rounded-lg overflow-hidden">' +
'<div class="bg-muted/30 border-b border-border px-4 py-3">' +
'<div class="flex items-center justify-between">' +
'<div class="flex items-center gap-2">' +
'<i data-lucide="eye" class="w-4 h-4 text-primary"></i>' +
'<h4 class="font-semibold">File Watcher</h4>' +
'</div>' +
'<div id="watcherStatusBadge" class="flex items-center gap-2">' +
'<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">Stopped</span>' +
'<button class="btn-sm btn-outline" onclick="toggleWatcher()" id="watcherToggleBtn">' +
'<i data-lucide="play" class="w-3.5 h-3.5"></i>' +
'</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="p-4">' +
'<p class="text-xs text-muted-foreground mb-3">Monitor file changes and auto-update index</p>' +
// Stats row
'<div class="grid grid-cols-3 gap-2 mb-3">' +
'<div class="bg-muted/30 rounded p-2 text-center">' +
'<div id="watcherFilesCount" class="text-sm font-semibold">-</div>' +
'<div class="text-xs text-muted-foreground">Files</div>' +
'</div>' +
'<div class="bg-muted/30 rounded p-2 text-center">' +
'<div id="watcherChangesCount" class="text-sm font-semibold">0</div>' +
'<div class="text-xs text-muted-foreground">Changes</div>' +
'</div>' +
'<div class="bg-muted/30 rounded p-2 text-center">' +
'<div id="watcherUptimeDisplay" class="text-sm font-semibold">-</div>' +
'<div class="text-xs text-muted-foreground">Uptime</div>' +
'</div>' +
'</div>' +
// Recent activity log
'<div class="border border-border rounded">' +
'<div class="bg-muted/30 px-3 py-1.5 border-b border-border text-xs font-medium text-muted-foreground flex items-center justify-between">' +
'<span>Recent Activity</span>' +
'<button class="text-xs hover:text-foreground" onclick="clearWatcherLog()" title="Clear log">' +
'<i data-lucide="trash-2" class="w-3 h-3"></i>' +
'</button>' +
'</div>' +
'<div id="watcherActivityLog" class="h-24 overflow-y-auto p-2 text-xs font-mono bg-background">' +
'<div class="text-muted-foreground">No activity yet. Start watcher to monitor files.</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
// Right Column
'<div class="space-y-6">' +
@@ -4544,53 +4591,6 @@ function buildCodexLensManagerPage(config) {
'</div>' +
'</div>' +
'</div>' +
// File Watcher Card
'<div class="bg-card border border-border rounded-lg overflow-hidden">' +
'<div class="bg-muted/30 border-b border-border px-4 py-3">' +
'<div class="flex items-center justify-between">' +
'<div class="flex items-center gap-2">' +
'<i data-lucide="eye" class="w-4 h-4 text-primary"></i>' +
'<h4 class="font-semibold">File Watcher</h4>' +
'</div>' +
'<div id="watcherStatusBadge" class="flex items-center gap-2">' +
'<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">Stopped</span>' +
'<button class="btn-sm btn-outline" onclick="toggleWatcher()" id="watcherToggleBtn">' +
'<i data-lucide="play" class="w-3.5 h-3.5"></i>' +
'</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="p-4">' +
'<p class="text-xs text-muted-foreground mb-3">Monitor file changes and auto-update index</p>' +
// Stats row
'<div class="grid grid-cols-3 gap-2 mb-3">' +
'<div class="bg-muted/30 rounded p-2 text-center">' +
'<div id="watcherFilesCount" class="text-sm font-semibold">-</div>' +
'<div class="text-xs text-muted-foreground">Files</div>' +
'</div>' +
'<div class="bg-muted/30 rounded p-2 text-center">' +
'<div id="watcherChangesCount" class="text-sm font-semibold">0</div>' +
'<div class="text-xs text-muted-foreground">Changes</div>' +
'</div>' +
'<div class="bg-muted/30 rounded p-2 text-center">' +
'<div id="watcherUptimeDisplay" class="text-sm font-semibold">-</div>' +
'<div class="text-xs text-muted-foreground">Uptime</div>' +
'</div>' +
'</div>' +
// Recent activity log
'<div class="border border-border rounded">' +
'<div class="bg-muted/30 px-3 py-1.5 border-b border-border text-xs font-medium text-muted-foreground flex items-center justify-between">' +
'<span>Recent Activity</span>' +
'<button class="text-xs hover:text-foreground" onclick="clearWatcherLog()" title="Clear log">' +
'<i data-lucide="trash-2" class="w-3 h-3"></i>' +
'</button>' +
'</div>' +
'<div id="watcherActivityLog" class="h-24 overflow-y-auto p-2 text-xs font-mono bg-background">' +
'<div class="text-muted-foreground">No activity yet. Start watcher to monitor files.</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
// Ignore Patterns Section