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

@@ -7,6 +7,7 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'fs';
import { join } from 'path';
import * as os from 'os';
import { getCCWHome, ensureStorageDir } from './storage-paths.js';
import {
ClaudeCliSettings,
@@ -17,6 +18,10 @@ import {
validateSettings,
createDefaultSettings
} from '../types/cli-settings.js';
import {
addClaudeCustomEndpoint,
removeClaudeCustomEndpoint
} from '../tools/claude-cli-tools.js';
/**
* Get CLI settings directory path
@@ -116,6 +121,23 @@ export function saveEndpointSettings(request: SaveEndpointRequest): SettingsOper
index.set(endpointId, metadata);
saveIndex(index);
// Sync with cli-tools.json for ccw cli --tool integration
// API endpoints are added as tools with type: 'api-endpoint'
// Usage: ccw cli -p "..." --tool custom --model <endpoint-id> --mode analysis
try {
const projectDir = os.homedir(); // Use home dir as base for global config
addClaudeCustomEndpoint(projectDir, {
id: endpointId,
name: request.name,
enabled: request.enabled ?? true
// No cli-wrapper tag -> registers as type: 'api-endpoint'
});
console.log(`[CliSettings] Synced endpoint ${endpointId} to cli-tools.json tools`);
} catch (syncError) {
console.warn(`[CliSettings] Failed to sync with cli-tools.json: ${syncError}`);
// Non-fatal: continue even if sync fails
}
// Return full endpoint settings
const endpoint: EndpointSettings = {
...metadata,
@@ -195,6 +217,16 @@ export function deleteEndpointSettings(endpointId: string): SettingsOperationRes
index.delete(endpointId);
saveIndex(index);
// Step 3: Remove from cli-tools.json tools (api-endpoint type)
try {
const projectDir = os.homedir();
removeClaudeCustomEndpoint(projectDir, endpointId);
console.log(`[CliSettings] Removed endpoint ${endpointId} from cli-tools.json tools`);
} catch (syncError) {
console.warn(`[CliSettings] Failed to remove from cli-tools.json: ${syncError}`);
// Non-fatal: continue even if sync fails
}
return {
success: true,
message: 'Endpoint deleted'
@@ -271,6 +303,20 @@ export function toggleEndpointEnabled(endpointId: string, enabled: boolean): Set
index.set(endpointId, metadata);
saveIndex(index);
// Sync enabled status with cli-tools.json tools (api-endpoint type)
try {
const projectDir = os.homedir();
addClaudeCustomEndpoint(projectDir, {
id: endpointId,
name: metadata.name,
enabled: enabled
// No cli-wrapper tag -> updates as type: 'api-endpoint'
});
console.log(`[CliSettings] Synced endpoint ${endpointId} enabled=${enabled} to cli-tools.json tools`);
} catch (syncError) {
console.warn(`[CliSettings] Failed to sync enabled status to cli-tools.json: ${syncError}`);
}
// Load full settings for response
const endpoint = loadEndpointSettings(endpointId);
@@ -357,3 +403,58 @@ export function getEnabledEndpoints(): EndpointSettings[] {
const { endpoints } = listAllSettings();
return endpoints.filter(ep => ep.enabled);
}
/**
* Find endpoint by name (case-insensitive)
* Useful for CLI where user types --tool doubao instead of --tool ep-xxx
*/
export function findEndpointByName(name: string): EndpointSettings | null {
const { endpoints } = listAllSettings();
const lowerName = name.toLowerCase();
return endpoints.find(ep => ep.name.toLowerCase() === lowerName) || null;
}
/**
* Find endpoint by ID or name
* First tries exact ID match, then falls back to name match
*/
export function findEndpoint(idOrName: string): EndpointSettings | null {
// Try by ID first
const byId = loadEndpointSettings(idOrName);
if (byId) return byId;
// Try by name
return findEndpointByName(idOrName);
}
/**
* Validate endpoint name for CLI compatibility
* Name must be: lowercase, alphanumeric, hyphens allowed, no spaces or special chars
*/
export function validateEndpointName(name: string): { valid: boolean; error?: string } {
if (!name || name.trim().length === 0) {
return { valid: false, error: 'Name is required' };
}
// Check for valid characters: a-z, 0-9, hyphen, underscore
const validPattern = /^[a-z][a-z0-9_-]*$/;
if (!validPattern.test(name.toLowerCase())) {
return {
valid: false,
error: 'Name must start with a letter and contain only letters, numbers, hyphens, and underscores'
};
}
// Check length
if (name.length > 32) {
return { valid: false, error: 'Name must be 32 characters or less' };
}
// Check if name conflicts with built-in tools
const builtinTools = ['gemini', 'qwen', 'codex', 'claude', 'opencode', 'litellm'];
if (builtinTools.includes(name.toLowerCase())) {
return { valid: false, error: `Name "${name}" conflicts with a built-in tool` };
}
return { valid: true };
}

View File

@@ -41,8 +41,10 @@ import {
updateClaudeToolEnabled,
updateClaudeCacheSettings,
getClaudeCliToolsInfo,
addClaudeCustomEndpoint,
removeClaudeCustomEndpoint,
addClaudeApiEndpoint,
removeClaudeApiEndpoint,
addClaudeCustomEndpoint, // @deprecated - kept for backward compatibility
removeClaudeCustomEndpoint, // @deprecated - kept for backward compatibility
updateCodeIndexMcp,
getCodeIndexMcp
} from '../../tools/claude-cli-tools.js';
@@ -238,13 +240,21 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
}
}
// API: Get all custom endpoints
// Helper: Get API endpoints from tools (type: 'api-endpoint')
const getApiEndpointsFromTools = (config: any) => {
return Object.entries(config.tools)
.filter(([_, t]: [string, any]) => t.type === 'api-endpoint')
.map(([name, t]: [string, any]) => ({ id: t.id || name, name, enabled: t.enabled }));
};
// API: Get all API endpoints (for --tool custom --model <id>)
if (pathname === '/api/cli/endpoints' && req.method === 'GET') {
try {
// Use ensureClaudeCliTools to auto-create config if missing
const config = ensureClaudeCliTools(initialPath);
const endpoints = getApiEndpointsFromTools(config);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ endpoints: config.customEndpoints || [] }));
res.end(JSON.stringify({ endpoints }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
@@ -252,7 +262,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// API: Add/Update custom endpoint
// API: Add/Update API endpoint
if (pathname === '/api/cli/endpoints' && req.method === 'POST') {
handlePostRequest(req, res, async (body: unknown) => {
try {
@@ -260,14 +270,14 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
if (!id || !name) {
return { error: 'id and name are required', status: 400 };
}
const config = addClaudeCustomEndpoint(initialPath, { id, name, enabled: enabled !== false });
const config = addClaudeApiEndpoint(initialPath, { id, name, enabled: enabled !== false });
broadcastToClients({
type: 'CLI_ENDPOINT_UPDATED',
payload: { endpoint: { id, name, enabled }, timestamp: new Date().toISOString() }
});
return { success: true, endpoints: config.customEndpoints };
return { success: true, endpoints: getApiEndpointsFromTools(config) };
} catch (err) {
return { error: (err as Error).message, status: 500 };
}
@@ -275,24 +285,36 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// API: Update custom endpoint enabled status
// API: Update API endpoint enabled status
if (pathname.match(/^\/api\/cli\/endpoints\/[^/]+$/) && req.method === 'PUT') {
const endpointId = pathname.split('/').pop() || '';
handlePostRequest(req, res, async (body: unknown) => {
try {
const { enabled, name } = body as { enabled?: boolean; name?: string };
const { enabled, name: newName } = body as { enabled?: boolean; name?: string };
const config = loadClaudeCliTools(initialPath);
const endpoint = config.customEndpoints.find(e => e.id === endpointId);
if (!endpoint) {
// Find the tool by id (api-endpoint type)
const toolEntry = Object.entries(config.tools).find(
([_, t]: [string, any]) => t.type === 'api-endpoint' && t.id === endpointId
);
if (!toolEntry) {
return { error: 'Endpoint not found', status: 404 };
}
if (typeof enabled === 'boolean') endpoint.enabled = enabled;
if (name) endpoint.name = name;
const [toolName, tool] = toolEntry as [string, any];
if (typeof enabled === 'boolean') tool.enabled = enabled;
// If name changes, we need to rename the key
if (newName && newName !== toolName) {
delete config.tools[toolName];
config.tools[newName] = tool;
}
saveClaudeCliTools(initialPath, config);
const endpoint = { id: tool.id || toolName, name: newName || toolName, enabled: tool.enabled };
broadcastToClients({
type: 'CLI_ENDPOINT_UPDATED',
payload: { endpoint, timestamp: new Date().toISOString() }
@@ -306,11 +328,11 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// API: Delete custom endpoint
// API: Delete API endpoint
if (pathname.match(/^\/api\/cli\/endpoints\/[^/]+$/) && req.method === 'DELETE') {
const endpointId = pathname.split('/').pop() || '';
try {
const config = removeClaudeCustomEndpoint(initialPath, endpointId);
const config = removeClaudeApiEndpoint(initialPath, endpointId);
broadcastToClients({
type: 'CLI_ENDPOINT_DELETED',
@@ -318,7 +340,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, endpoints: config.customEndpoints }));
res.end(JSON.stringify({ success: true, endpoints: getApiEndpointsFromTools(config) }));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
@@ -737,8 +759,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
const currentTools = loadClaudeCliTools(initialPath);
const updatedTools = {
...currentTools,
tools: { ...currentTools.tools, ...(updates.tools.tools || {}) },
customEndpoints: updates.tools.customEndpoints || currentTools.customEndpoints
tools: { ...currentTools.tools, ...(updates.tools.tools || {}) }
};
saveClaudeCliTools(initialPath, updatedTools);
}

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

View File

@@ -22,10 +22,20 @@ export interface ClaudeCliTool {
primaryModel?: string;
secondaryModel?: string;
tags: string[];
type?: 'builtin' | 'cli-wrapper' | 'api-endpoint'; // Tool type: builtin, cli-wrapper, or api-endpoint
id?: string; // Required for api-endpoint type (endpoint ID for settings lookup)
}
export type CliToolName = 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode';
export type CliToolName = 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode' | string;
// @deprecated Use tools with type: 'api-endpoint' instead
export interface ClaudeApiEndpoint {
id: string;
name: string;
enabled: boolean;
}
// @deprecated Use tools with type: 'cli-wrapper' or 'api-endpoint' instead
export interface ClaudeCustomEndpoint {
id: string;
name: string;
@@ -44,8 +54,9 @@ export interface ClaudeCliToolsConfig {
$schema?: string;
version: string;
models?: Record<string, string[]>; // PREDEFINED_MODELS
tools: Record<string, ClaudeCliTool>;
customEndpoints: ClaudeCustomEndpoint[];
tools: Record<string, ClaudeCliTool>; // All tools: builtin, cli-wrapper, api-endpoint
apiEndpoints?: ClaudeApiEndpoint[]; // @deprecated Use tools with type: 'api-endpoint' instead
customEndpoints?: ClaudeCustomEndpoint[]; // @deprecated Use tools with type: 'cli-wrapper' or 'api-endpoint' instead
}
// New: Settings-only config (cli-settings.json)
@@ -103,41 +114,46 @@ const PREDEFINED_MODELS: Record<CliToolName, string[]> = {
};
const DEFAULT_TOOLS_CONFIG: ClaudeCliToolsConfig = {
version: '3.0.0',
version: '3.2.0',
models: { ...PREDEFINED_MODELS },
tools: {
gemini: {
enabled: true,
primaryModel: 'gemini-2.5-pro',
secondaryModel: 'gemini-2.5-flash',
tags: []
tags: [],
type: 'builtin'
},
qwen: {
enabled: true,
primaryModel: 'coder-model',
secondaryModel: 'coder-model',
tags: []
tags: [],
type: 'builtin'
},
codex: {
enabled: true,
primaryModel: 'gpt-5.2',
secondaryModel: 'gpt-5.2',
tags: []
tags: [],
type: 'builtin'
},
claude: {
enabled: true,
primaryModel: 'sonnet',
secondaryModel: 'haiku',
tags: []
tags: [],
type: 'builtin'
},
opencode: {
enabled: true,
primaryModel: 'opencode/glm-4.7-free',
secondaryModel: 'opencode/glm-4.7-free',
tags: []
tags: [],
type: 'builtin'
}
},
customEndpoints: []
}
// Note: api-endpoint type tools are added dynamically via addClaudeApiEndpoint
};
const DEFAULT_SETTINGS_CONFIG: ClaudeCliSettingsConfig = {
@@ -222,17 +238,18 @@ function ensureToolTags(tool: Partial<ClaudeCliTool>): ClaudeCliTool {
}
/**
* Migrate config from older versions to v3.0.0
* Migrate config from older versions to v3.2.0
* v3.2.0: All endpoints (cli-wrapper, api-endpoint) are in tools with type field
*/
function migrateConfig(config: any, projectDir: string): ClaudeCliToolsConfig {
const version = parseFloat(config.version || '1.0');
// Already v3.x, no migration needed
if (version >= 3.0) {
// Already v3.2+, no migration needed
if (version >= 3.2) {
return config as ClaudeCliToolsConfig;
}
console.log(`[claude-cli-tools] Migrating config from v${config.version || '1.0'} to v3.0.0`);
console.log(`[claude-cli-tools] Migrating config from v${config.version || '1.0'} to v3.2.0`);
// Try to load legacy cli-config.json for model data
let legacyCliConfig: any = null;
@@ -258,7 +275,9 @@ function migrateConfig(config: any, projectDir: string): ClaudeCliToolsConfig {
enabled: t.enabled ?? legacyTool?.enabled ?? true,
primaryModel: t.primaryModel ?? legacyTool?.primaryModel ?? DEFAULT_TOOLS_CONFIG.tools[key]?.primaryModel,
secondaryModel: t.secondaryModel ?? legacyTool?.secondaryModel ?? DEFAULT_TOOLS_CONFIG.tools[key]?.secondaryModel,
tags: t.tags ?? legacyTool?.tags ?? []
tags: t.tags ?? legacyTool?.tags ?? [],
type: t.type ?? DEFAULT_TOOLS_CONFIG.tools[key]?.type ?? 'builtin',
id: t.id // Preserve id for api-endpoint type
};
}
@@ -270,16 +289,57 @@ function migrateConfig(config: any, projectDir: string): ClaudeCliToolsConfig {
enabled: legacyTool?.enabled ?? defaultTool.enabled,
primaryModel: legacyTool?.primaryModel ?? defaultTool.primaryModel,
secondaryModel: legacyTool?.secondaryModel ?? defaultTool.secondaryModel,
tags: legacyTool?.tags ?? defaultTool.tags
tags: legacyTool?.tags ?? defaultTool.tags,
type: defaultTool.type ?? 'builtin'
};
}
}
// Migrate customEndpoints (v3.0 and below): cli-wrapper -> tools, others -> api-endpoint tools
const customEndpoints = config.customEndpoints || [];
for (const ep of customEndpoints) {
if (ep.tags?.includes('cli-wrapper')) {
// CLI wrapper becomes a tool with type: 'cli-wrapper'
if (!migratedTools[ep.name]) {
migratedTools[ep.name] = {
enabled: ep.enabled ?? true,
tags: ep.tags.filter((t: string) => t !== 'cli-wrapper'),
type: 'cli-wrapper'
};
console.log(`[claude-cli-tools] Migrated cli-wrapper "${ep.name}" to tools`);
}
} else {
// Pure API endpoint becomes a tool with type: 'api-endpoint'
if (!migratedTools[ep.name]) {
migratedTools[ep.name] = {
enabled: ep.enabled ?? true,
tags: [],
type: 'api-endpoint',
id: ep.id // Store endpoint ID for settings lookup
};
console.log(`[claude-cli-tools] Migrated API endpoint "${ep.name}" to tools`);
}
}
}
// Migrate apiEndpoints (v3.1): convert to tools with type: 'api-endpoint'
const apiEndpoints = config.apiEndpoints || [];
for (const ep of apiEndpoints) {
if (!migratedTools[ep.name]) {
migratedTools[ep.name] = {
enabled: ep.enabled ?? true,
tags: [],
type: 'api-endpoint',
id: ep.id // Store endpoint ID for settings lookup
};
console.log(`[claude-cli-tools] Migrated API endpoint "${ep.name}" to tools`);
}
}
return {
version: '3.0.0',
version: '3.2.0',
models: { ...PREDEFINED_MODELS },
tools: migratedTools,
customEndpoints: config.customEndpoints || [],
$schema: config.$schema
};
}
@@ -324,7 +384,7 @@ export function ensureClaudeCliTools(projectDir: string, createInProject: boolea
* Load CLI tools configuration from global ~/.claude/cli-tools.json
* Falls back to default config if not found.
*
* Automatically migrates older config versions to v3.0.0
* Automatically migrates older config versions to v3.2.0
*/
export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & { _source?: string } {
const resolved = resolveConfigPath(projectDir);
@@ -337,27 +397,24 @@ export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & {
const content = fs.readFileSync(resolved.path, 'utf-8');
const parsed = JSON.parse(content) as Partial<ClaudeCliCombinedConfig>;
// Migrate older versions to v3.0.0
// Migrate older versions to v3.2.0
const migrated = migrateConfig(parsed, projectDir);
const needsSave = migrated.version !== parsed.version;
// Merge tools with defaults and ensure required fields exist
const mergedTools: Record<string, ClaudeCliTool> = {};
for (const [key, tool] of Object.entries({ ...DEFAULT_TOOLS_CONFIG.tools, ...(migrated.tools || {}) })) {
mergedTools[key] = ensureToolTags(tool);
mergedTools[key] = {
...ensureToolTags(tool),
type: tool.type ?? 'builtin',
id: tool.id // Preserve id for api-endpoint type
};
}
// Ensure customEndpoints have tags
const mergedEndpoints = (migrated.customEndpoints || []).map(ep => ({
...ep,
tags: ep.tags ?? []
}));
const config: ClaudeCliToolsConfig & { _source?: string } = {
version: migrated.version || DEFAULT_TOOLS_CONFIG.version,
models: migrated.models || DEFAULT_TOOLS_CONFIG.models,
tools: mergedTools,
customEndpoints: mergedEndpoints,
$schema: migrated.$schema,
_source: resolved.source
};
@@ -513,27 +570,43 @@ export function updateClaudeDefaultTool(
}
/**
* Add custom endpoint
* Add API endpoint as a tool with type: 'api-endpoint'
* Usage: --tool <name> or --tool custom --model <id>
*/
export function addClaudeCustomEndpoint(
export function addClaudeApiEndpoint(
projectDir: string,
endpoint: { id: string; name: string; enabled: boolean; tags?: string[] }
endpoint: { id: string; name: string; enabled: boolean }
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
const newEndpoint: ClaudeCustomEndpoint = {
id: endpoint.id,
name: endpoint.name,
// Add as a tool with type: 'api-endpoint'
config.tools[endpoint.name] = {
enabled: endpoint.enabled,
tags: endpoint.tags || []
tags: [],
type: 'api-endpoint',
id: endpoint.id // Store endpoint ID for settings lookup
};
// Check if endpoint already exists
const existingIndex = config.customEndpoints.findIndex(e => e.id === endpoint.id);
if (existingIndex >= 0) {
config.customEndpoints[existingIndex] = newEndpoint;
} else {
config.customEndpoints.push(newEndpoint);
saveClaudeCliTools(projectDir, config);
return config;
}
/**
* Remove API endpoint tool by id or name
*/
export function removeClaudeApiEndpoint(
projectDir: string,
endpointId: string
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
// Find the tool by id or name
const toolToRemove = Object.entries(config.tools).find(
([name, t]) => t.type === 'api-endpoint' && (t.id === endpointId || name === endpointId || name.toLowerCase() === endpointId.toLowerCase())
);
if (toolToRemove) {
delete config.tools[toolToRemove[0]];
}
saveClaudeCliTools(projectDir, config);
@@ -541,14 +614,57 @@ export function addClaudeCustomEndpoint(
}
/**
* Remove custom endpoint
* @deprecated Use addClaudeApiEndpoint instead
* Adds tool to config based on tags:
* - cli-wrapper tag -> type: 'cli-wrapper'
* - others -> type: 'api-endpoint'
*/
export function addClaudeCustomEndpoint(
projectDir: string,
endpoint: { id: string; name: string; enabled: boolean; tags?: string[] }
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
if (endpoint.tags?.includes('cli-wrapper')) {
// CLI wrapper tool
config.tools[endpoint.name] = {
enabled: endpoint.enabled,
tags: endpoint.tags.filter(t => t !== 'cli-wrapper'),
type: 'cli-wrapper'
};
} else {
// API endpoint tool
config.tools[endpoint.name] = {
enabled: endpoint.enabled,
tags: [],
type: 'api-endpoint',
id: endpoint.id
};
}
saveClaudeCliTools(projectDir, config);
return config;
}
/**
* Remove endpoint tool (cli-wrapper or api-endpoint)
*/
export function removeClaudeCustomEndpoint(
projectDir: string,
endpointId: string
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
config.customEndpoints = config.customEndpoints.filter(e => e.id !== endpointId);
// Find the tool by id or name (cli-wrapper or api-endpoint type)
const toolToRemove = Object.entries(config.tools).find(
([name, t]) => (t.type === 'cli-wrapper' || t.type === 'api-endpoint') &&
(name === endpointId || name.toLowerCase() === endpointId.toLowerCase() || t.id === endpointId)
);
if (toolToRemove) {
delete config.tools[toolToRemove[0]];
}
saveClaudeCliTools(projectDir, config);
return config;
}

View File

@@ -80,6 +80,179 @@ export function killCurrentCliProcess(): boolean {
import { executeLiteLLMEndpoint } from './litellm-executor.js';
import { findEndpointById } from '../config/litellm-api-config-manager.js';
// CLI Settings (CLI封装) integration
import { loadEndpointSettings, getSettingsFilePath, findEndpoint } from '../config/cli-settings-manager.js';
import { loadClaudeCliTools } from './claude-cli-tools.js';
/**
* Execute Claude CLI with custom settings file (CLI封装)
*/
interface ClaudeWithSettingsParams {
prompt: string;
settingsPath: string;
endpointId: string;
mode: 'analysis' | 'write' | 'auto';
workingDir: string;
cd?: string;
includeDirs?: string[];
customId?: string;
onOutput?: (unit: CliOutputUnit) => void;
}
async function executeClaudeWithSettings(params: ClaudeWithSettingsParams): Promise<ExecutionOutput> {
const { prompt, settingsPath, endpointId, mode, workingDir, cd, includeDirs, customId, onOutput } = params;
const startTime = Date.now();
const conversationId = customId || `${Date.now()}-${endpointId}`;
// Build claude command with --settings flag
const args: string[] = [
'--settings', settingsPath,
'--print' // Non-interactive mode
];
// Add mode-specific flags
if (mode === 'write') {
args.push('--dangerously-skip-permissions');
}
// Add working directory if specified
if (cd) {
args.push('--cd', cd);
}
// Add include directories
if (includeDirs && includeDirs.length > 0) {
for (const dir of includeDirs) {
args.push('--add-dir', dir);
}
}
// Add prompt as argument
args.push('-p', prompt);
debugLog('CLAUDE_SETTINGS', `Executing claude with settings`, {
settingsPath,
endpointId,
mode,
workingDir,
args
});
return new Promise((resolve, reject) => {
const isWindows = process.platform === 'win32';
const command = 'claude';
const commandToSpawn = isWindows ? escapeWindowsArg(command) : command;
const argsToSpawn = isWindows ? args.map(escapeWindowsArg) : args;
const child = spawn(commandToSpawn, argsToSpawn, {
cwd: workingDir,
shell: isWindows,
stdio: ['ignore', 'pipe', 'pipe']
});
// Track current child process for cleanup
currentChildProcess = child;
let stdout = '';
let stderr = '';
const outputUnits: CliOutputUnit[] = [];
child.stdout!.on('data', (data: Buffer) => {
const text = data.toString();
stdout += text;
const unit: CliOutputUnit = {
type: 'stdout',
content: text,
timestamp: new Date().toISOString()
};
outputUnits.push(unit);
if (onOutput) {
onOutput(unit);
}
});
child.stderr!.on('data', (data: Buffer) => {
const text = data.toString();
stderr += text;
const unit: CliOutputUnit = {
type: 'stderr',
content: text,
timestamp: new Date().toISOString()
};
outputUnits.push(unit);
if (onOutput) {
onOutput(unit);
}
});
child.on('close', (code) => {
currentChildProcess = null;
const endTime = Date.now();
const duration = endTime - startTime;
// Determine status
let status: 'success' | 'error' = 'success';
if (code !== 0) {
const hasValidOutput = stdout.trim().length > 0;
const hasFatalError = stderr.includes('FATAL') ||
stderr.includes('Authentication failed') ||
stderr.includes('API key');
if (hasValidOutput && !hasFatalError) {
status = 'success';
} else {
status = 'error';
}
}
const execution: ExecutionRecord = {
id: conversationId,
timestamp: new Date(startTime).toISOString(),
tool: 'claude',
model: endpointId, // Use endpoint ID as model identifier
mode,
prompt,
status,
exit_code: code,
duration_ms: duration,
output: {
stdout: stdout.substring(0, 10240),
stderr: stderr.substring(0, 2048),
truncated: stdout.length > 10240 || stderr.length > 2048
}
};
const conversation = convertToConversation(execution);
// Save to history
try {
saveConversation(workingDir, conversation);
} catch (err) {
console.error('[CLI Executor] Failed to save CLI封装 history:', (err as Error).message);
}
resolve({
success: status === 'success',
execution,
conversation,
stdout,
stderr
});
});
child.on('error', (error) => {
currentChildProcess = null;
reject(new Error(`Failed to spawn claude: ${error.message}`));
});
});
}
// Native resume support
import {
trackNewSession,
@@ -100,9 +273,14 @@ import {
getPrimaryModel
} from './cli-config-manager.js';
// Built-in CLI tools
const BUILTIN_CLI_TOOLS = ['gemini', 'qwen', 'codex', 'opencode', 'claude'] as const;
type BuiltinCliTool = typeof BUILTIN_CLI_TOOLS[number];
// Define Zod schema for validation
// tool accepts built-in tools or custom endpoint IDs (CLI封装)
const ParamsSchema = z.object({
tool: z.enum(['gemini', 'qwen', 'codex', 'opencode']),
tool: z.string().min(1, 'Tool is required'), // Accept any tool ID (built-in or custom endpoint)
prompt: z.string().min(1, 'Prompt is required'),
mode: z.enum(['analysis', 'write', 'auto']).default('analysis'),
format: z.enum(['plain', 'yaml', 'json']).default('plain'), // Multi-turn prompt concatenation format
@@ -220,6 +398,116 @@ async function executeCliTool(
}
}
// Check if tool is a custom CLI封装 endpoint (not a built-in tool)
const isBuiltinTool = BUILTIN_CLI_TOOLS.includes(tool as BuiltinCliTool);
if (!isBuiltinTool) {
// Check if it's a CLI封装 endpoint (by ID or name)
const cliSettings = findEndpoint(tool);
if (cliSettings && cliSettings.enabled) {
// Route to Claude CLI with --settings flag
const settingsPath = getSettingsFilePath(cliSettings.id);
const displayName = cliSettings.name !== cliSettings.id ? `${cliSettings.name} (${cliSettings.id})` : cliSettings.id;
if (onOutput) {
onOutput({
type: 'stderr',
content: `[Routing to CLI封装 endpoint: ${displayName} via claude --settings]\n`,
timestamp: new Date().toISOString()
});
}
// Execute claude CLI with settings file
const result = await executeClaudeWithSettings({
prompt,
settingsPath,
endpointId: cliSettings.id,
mode,
workingDir,
cd,
includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
customId,
onOutput: onOutput || undefined
});
return result;
}
// Check cli-tools.json for CLI wrapper tools or API endpoints
const cliToolsConfig = loadClaudeCliTools(workingDir);
// First check if tool is a cli-wrapper in tools section
const cliWrapperTool = Object.entries(cliToolsConfig.tools).find(
([name, t]) => name.toLowerCase() === tool.toLowerCase() && t.type === 'cli-wrapper' && t.enabled
);
if (cliWrapperTool) {
const [toolName] = cliWrapperTool;
// Check if there's a corresponding CLI封装 settings file
const cliSettingsForTool = findEndpoint(toolName);
if (cliSettingsForTool) {
const settingsPath = getSettingsFilePath(cliSettingsForTool.id);
if (onOutput) {
onOutput({
type: 'stderr',
content: `[Routing to CLI wrapper tool: ${toolName} via claude --settings]\n`,
timestamp: new Date().toISOString()
});
}
const result = await executeClaudeWithSettings({
prompt,
settingsPath,
endpointId: cliSettingsForTool.id,
mode,
workingDir,
cd,
includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
customId,
onOutput: onOutput || undefined
});
return result;
}
}
// Check tools with type: 'api-endpoint' (for --tool custom --model <id>)
const apiEndpointTool = Object.entries(cliToolsConfig.tools).find(
([name, t]) => t.type === 'api-endpoint' && t.enabled &&
(t.id === tool || name === tool || name.toLowerCase() === tool.toLowerCase())
);
if (apiEndpointTool) {
const [toolName, toolConfig] = apiEndpointTool;
const endpointId = toolConfig.id || toolName;
// Check if there's a corresponding CLI封装 settings file
const cliSettingsForEndpoint = findEndpoint(endpointId);
if (cliSettingsForEndpoint) {
const settingsPath = getSettingsFilePath(cliSettingsForEndpoint.id);
if (onOutput) {
onOutput({
type: 'stderr',
content: `[Routing to API endpoint: ${toolName} via claude --settings]\n`,
timestamp: new Date().toISOString()
});
}
const result = await executeClaudeWithSettings({
prompt,
settingsPath,
endpointId: cliSettingsForEndpoint.id,
mode,
workingDir,
cd,
includeDirs: includeDirs ? includeDirs.split(',').map(d => d.trim()) : undefined,
customId,
onOutput: onOutput || undefined
});
return result;
}
}
// Tool not found
throw new Error(`Unknown tool: ${tool}. Use one of: ${BUILTIN_CLI_TOOLS.join(', ')} or a registered CLI封装 endpoint name.`);
}
// Get SQLite store for native session lookup
const store = await getSqliteStore(workingDir);