mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
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:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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': '供应商绑定',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user