mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-09 02:24:11 +08:00
feat: Enhance configuration management and embedding capabilities
- Added JSON-based settings management in Config class for embedding and LLM configurations. - Introduced methods to save and load settings from a JSON file. - Updated BaseEmbedder and its subclasses to include max_tokens property for better token management. - Enhanced chunking strategy to support recursive splitting of large symbols with improved overlap handling. - Implemented comprehensive tests for recursive splitting and chunking behavior. - Added CLI tools configuration management for better integration with external tools. - Introduced a new command for compacting session memory into structured text for recovery.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { StoragePaths, ensureStorageDir } from './storage-paths.js';
|
||||
import { StoragePaths, GlobalPaths, ensureStorageDir } from './storage-paths.js';
|
||||
import type {
|
||||
LiteLLMApiConfig,
|
||||
ProviderCredential,
|
||||
@@ -32,12 +32,12 @@ function getDefaultConfig(): LiteLLMApiConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config file path for a project
|
||||
* Get config file path (global, shared across all projects)
|
||||
*/
|
||||
function getConfigPath(baseDir: string): string {
|
||||
const paths = StoragePaths.project(baseDir);
|
||||
ensureStorageDir(paths.config);
|
||||
return join(paths.config, 'litellm-api-config.json');
|
||||
function getConfigPath(_baseDir?: string): string {
|
||||
const configDir = GlobalPaths.config();
|
||||
ensureStorageDir(configDir);
|
||||
return join(configDir, 'litellm-api-config.json');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,5 +356,166 @@ export function updateGlobalCacheSettings(
|
||||
saveConfig(baseDir, config);
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// YAML Config Generation for ccw_litellm
|
||||
// ===========================
|
||||
|
||||
/**
|
||||
* Convert UI config (JSON) to ccw_litellm config (YAML format object)
|
||||
* This allows CodexLens to use UI-configured providers
|
||||
*/
|
||||
export function generateLiteLLMYamlConfig(baseDir: string): Record<string, unknown> {
|
||||
const config = loadLiteLLMApiConfig(baseDir);
|
||||
|
||||
// Build providers object
|
||||
const providers: Record<string, unknown> = {};
|
||||
for (const provider of config.providers) {
|
||||
if (!provider.enabled) continue;
|
||||
|
||||
providers[provider.id] = {
|
||||
api_key: provider.apiKey,
|
||||
api_base: provider.apiBase || getDefaultApiBaseForType(provider.type),
|
||||
};
|
||||
}
|
||||
|
||||
// Build embedding_models object from providers' embeddingModels
|
||||
const embeddingModels: Record<string, unknown> = {};
|
||||
for (const provider of config.providers) {
|
||||
if (!provider.enabled || !provider.embeddingModels) continue;
|
||||
|
||||
for (const model of provider.embeddingModels) {
|
||||
if (!model.enabled) continue;
|
||||
|
||||
embeddingModels[model.id] = {
|
||||
provider: provider.id,
|
||||
model: model.name,
|
||||
dimensions: model.capabilities?.embeddingDimension || 1536,
|
||||
// Use model-specific base URL if set, otherwise use provider's
|
||||
...(model.endpointSettings?.baseUrl && {
|
||||
api_base: model.endpointSettings.baseUrl,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build llm_models object from providers' llmModels
|
||||
const llmModels: Record<string, unknown> = {};
|
||||
for (const provider of config.providers) {
|
||||
if (!provider.enabled || !provider.llmModels) continue;
|
||||
|
||||
for (const model of provider.llmModels) {
|
||||
if (!model.enabled) continue;
|
||||
|
||||
llmModels[model.id] = {
|
||||
provider: provider.id,
|
||||
model: model.name,
|
||||
...(model.endpointSettings?.baseUrl && {
|
||||
api_base: model.endpointSettings.baseUrl,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Find default provider
|
||||
const defaultProvider = config.providers.find((p) => p.enabled)?.id || 'openai';
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
default_provider: defaultProvider,
|
||||
providers,
|
||||
embedding_models: Object.keys(embeddingModels).length > 0 ? embeddingModels : {
|
||||
default: {
|
||||
provider: defaultProvider,
|
||||
model: 'text-embedding-3-small',
|
||||
dimensions: 1536,
|
||||
},
|
||||
},
|
||||
llm_models: Object.keys(llmModels).length > 0 ? llmModels : {
|
||||
default: {
|
||||
provider: defaultProvider,
|
||||
model: 'gpt-4',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default API base URL for provider type
|
||||
*/
|
||||
function getDefaultApiBaseForType(type: ProviderType): string {
|
||||
const defaults: Record<string, string> = {
|
||||
openai: 'https://api.openai.com/v1',
|
||||
anthropic: 'https://api.anthropic.com/v1',
|
||||
custom: 'https://api.example.com/v1',
|
||||
};
|
||||
return defaults[type] || 'https://api.openai.com/v1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Save ccw_litellm YAML config file
|
||||
* Writes to ~/.ccw/config/litellm-config.yaml
|
||||
*/
|
||||
export function saveLiteLLMYamlConfig(baseDir: string): string {
|
||||
const yamlConfig = generateLiteLLMYamlConfig(baseDir);
|
||||
|
||||
// Convert to YAML manually (simple format)
|
||||
const yamlContent = objectToYaml(yamlConfig);
|
||||
|
||||
// Write to ~/.ccw/config/litellm-config.yaml
|
||||
const homePath = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const yamlPath = join(homePath, '.ccw', 'config', 'litellm-config.yaml');
|
||||
|
||||
// Ensure directory exists
|
||||
const configDir = join(homePath, '.ccw', 'config');
|
||||
ensureStorageDir(configDir);
|
||||
|
||||
writeFileSync(yamlPath, yamlContent, 'utf-8');
|
||||
return yamlPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple object to YAML converter
|
||||
*/
|
||||
function objectToYaml(obj: unknown, indent: number = 0): string {
|
||||
const spaces = ' '.repeat(indent);
|
||||
|
||||
if (obj === null || obj === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
// Quote strings that contain special characters
|
||||
if (obj.includes(':') || obj.includes('#') || obj.includes('\n') || obj.startsWith('$')) {
|
||||
return `"${obj.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj === 'number' || typeof obj === 'boolean') {
|
||||
return String(obj);
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
if (obj.length === 0) return '[]';
|
||||
return obj.map((item) => `${spaces}- ${objectToYaml(item, indent + 1).trimStart()}`).join('\n');
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const entries = Object.entries(obj as Record<string, unknown>);
|
||||
if (entries.length === 0) return '{}';
|
||||
|
||||
return entries
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
return `${spaces}${key}:\n${objectToYaml(value, indent + 1)}`;
|
||||
}
|
||||
return `${spaces}${key}: ${objectToYaml(value, indent)}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return String(obj);
|
||||
}
|
||||
|
||||
// Re-export types
|
||||
export type { ProviderCredential, CustomEndpoint, ProviderType, CacheStrategy };
|
||||
|
||||
@@ -33,6 +33,13 @@ import {
|
||||
getFullConfigResponse,
|
||||
PREDEFINED_MODELS
|
||||
} from '../../tools/cli-config-manager.js';
|
||||
import {
|
||||
loadClaudeCliTools,
|
||||
saveClaudeCliTools,
|
||||
updateClaudeToolEnabled,
|
||||
updateClaudeCacheSettings,
|
||||
getClaudeCliToolsInfo
|
||||
} from '../../tools/claude-cli-tools.js';
|
||||
|
||||
export interface RouteContext {
|
||||
pathname: string;
|
||||
@@ -558,5 +565,101 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global)
|
||||
if (pathname === '/api/cli/tools-config' && req.method === 'GET') {
|
||||
try {
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
const info = getClaudeCliToolsInfo(initialPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
...config,
|
||||
_configInfo: info
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update CLI Tools Config
|
||||
if (pathname === '/api/cli/tools-config' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const updates = body as Partial<any>;
|
||||
const config = loadClaudeCliTools(initialPath);
|
||||
|
||||
// Merge updates
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
...updates,
|
||||
tools: { ...config.tools, ...(updates.tools || {}) },
|
||||
settings: {
|
||||
...config.settings,
|
||||
...(updates.settings || {}),
|
||||
cache: {
|
||||
...config.settings.cache,
|
||||
...(updates.settings?.cache || {})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
saveClaudeCliTools(initialPath, updatedConfig);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_TOOLS_CONFIG_UPDATED',
|
||||
payload: { config: updatedConfig, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config: updatedConfig };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update specific tool enabled status
|
||||
const toolsConfigMatch = pathname.match(/^\/api\/cli\/tools-config\/([a-zA-Z0-9_-]+)$/);
|
||||
if (toolsConfigMatch && req.method === 'PUT') {
|
||||
const toolName = toolsConfigMatch[1];
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const { enabled } = body as { enabled: boolean };
|
||||
const config = updateClaudeToolEnabled(initialPath, toolName, enabled);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_TOOL_TOGGLED',
|
||||
payload: { tool: toolName, enabled, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Update cache settings
|
||||
if (pathname === '/api/cli/tools-config/cache' && req.method === 'PUT') {
|
||||
handlePostRequest(req, res, async (body: unknown) => {
|
||||
try {
|
||||
const cacheSettings = body as { injectionMode?: string; defaultPrefix?: string; defaultSuffix?: string };
|
||||
const config = updateClaudeCacheSettings(initialPath, cacheSettings as any);
|
||||
|
||||
broadcastToClients({
|
||||
type: 'CLI_CACHE_SETTINGS_UPDATED',
|
||||
payload: { cache: config.settings.cache, timestamp: new Date().toISOString() }
|
||||
});
|
||||
|
||||
return { success: true, config };
|
||||
} catch (err) {
|
||||
return { error: (err as Error).message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Init (Initialize workspace index)
|
||||
if (pathname === '/api/codexlens/init' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: projectPath, indexType = 'vector', embeddingModel = 'code' } = body;
|
||||
const { path: projectPath, indexType = 'vector', embeddingModel = 'code', embeddingBackend = 'fastembed' } = body;
|
||||
const targetPath = projectPath || initialPath;
|
||||
|
||||
// Build CLI arguments based on index type
|
||||
@@ -415,6 +415,10 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
} else {
|
||||
// Add embedding model selection for vector index
|
||||
args.push('--embedding-model', embeddingModel);
|
||||
// Add embedding backend if not using default fastembed
|
||||
if (embeddingBackend && embeddingBackend !== 'fastembed') {
|
||||
args.push('--embedding-backend', embeddingBackend);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast start event
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
getGlobalCacheSettings,
|
||||
updateGlobalCacheSettings,
|
||||
loadLiteLLMApiConfig,
|
||||
saveLiteLLMYamlConfig,
|
||||
generateLiteLLMYamlConfig,
|
||||
type ProviderCredential,
|
||||
type CustomEndpoint,
|
||||
type ProviderType,
|
||||
@@ -481,5 +483,150 @@ export async function handleLiteLLMApiRoutes(ctx: RouteContext): Promise<boolean
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Config Sync Routes
|
||||
// ===========================
|
||||
|
||||
// POST /api/litellm-api/config/sync - Sync UI config to ccw_litellm YAML config
|
||||
if (pathname === '/api/litellm-api/config/sync' && req.method === 'POST') {
|
||||
try {
|
||||
const yamlPath = saveLiteLLMYamlConfig(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
message: 'Config synced to ccw_litellm',
|
||||
yamlPath,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/litellm-api/config/yaml-preview - Preview YAML config without saving
|
||||
if (pathname === '/api/litellm-api/config/yaml-preview' && req.method === 'GET') {
|
||||
try {
|
||||
const yamlConfig = generateLiteLLMYamlConfig(initialPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
config: yamlConfig,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// CCW-LiteLLM Package Management
|
||||
// ===========================
|
||||
|
||||
// GET /api/litellm-api/ccw-litellm/status - Check ccw-litellm installation status
|
||||
if (pathname === '/api/litellm-api/ccw-litellm/status' && req.method === 'GET') {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const result = await new Promise<{ installed: boolean; version?: string }>((resolve) => {
|
||||
const proc = spawn('python', ['-c', 'import ccw_litellm; print(ccw_litellm.__version__ if hasattr(ccw_litellm, "__version__") else "installed")'], {
|
||||
shell: true,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
let output = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ installed: true, version: output.trim() || 'unknown' });
|
||||
} else {
|
||||
resolve({ installed: false });
|
||||
}
|
||||
});
|
||||
proc.on('error', () => resolve({ installed: false }));
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(result));
|
||||
} catch (err) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ installed: false, error: (err as Error).message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// POST /api/litellm-api/ccw-litellm/install - Install ccw-litellm package
|
||||
if (pathname === '/api/litellm-api/ccw-litellm/install' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const path = await import('path');
|
||||
const fs = await import('fs');
|
||||
|
||||
// Try to find ccw-litellm package in distribution
|
||||
const possiblePaths = [
|
||||
path.join(initialPath, 'ccw-litellm'),
|
||||
path.join(initialPath, '..', 'ccw-litellm'),
|
||||
path.join(process.cwd(), 'ccw-litellm'),
|
||||
];
|
||||
|
||||
let packagePath = '';
|
||||
for (const p of possiblePaths) {
|
||||
const pyproject = path.join(p, 'pyproject.toml');
|
||||
if (fs.existsSync(pyproject)) {
|
||||
packagePath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!packagePath) {
|
||||
// Try pip install from PyPI as fallback
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['install', 'ccw-litellm'], { shell: true, timeout: 300000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
proc.stderr?.on('data', (data) => { error += data.toString(); });
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ success: true, message: 'ccw-litellm installed from PyPI' });
|
||||
} else {
|
||||
resolve({ success: false, error: error || 'Installation failed' });
|
||||
}
|
||||
});
|
||||
proc.on('error', (err) => resolve({ success: false, error: err.message }));
|
||||
});
|
||||
}
|
||||
|
||||
// Install from local package
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pip', ['install', '-e', packagePath], { shell: true, timeout: 300000 });
|
||||
let output = '';
|
||||
let error = '';
|
||||
proc.stdout?.on('data', (data) => { output += data.toString(); });
|
||||
proc.stderr?.on('data', (data) => { error += data.toString(); });
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// Broadcast installation event
|
||||
broadcastToClients({
|
||||
type: 'CCW_LITELLM_INSTALLED',
|
||||
payload: { timestamp: new Date().toISOString() }
|
||||
});
|
||||
resolve({ success: true, message: 'ccw-litellm installed successfully', path: packagePath });
|
||||
} else {
|
||||
resolve({ success: false, error: error || output || 'Installation failed' });
|
||||
}
|
||||
});
|
||||
proc.on('error', (err) => resolve({ success: false, error: err.message }));
|
||||
});
|
||||
} catch (err) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,27 @@
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.cli-tool-badge-disabled {
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 600;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: hsl(38 92% 50% / 0.2);
|
||||
color: hsl(38 92% 50%);
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* Disabled tool card state */
|
||||
.cli-tool-card.disabled {
|
||||
opacity: 0.7;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.cli-tool-card.disabled .cli-tool-name {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.cli-tool-info {
|
||||
font-size: 0.6875rem;
|
||||
margin-bottom: 0.3125rem;
|
||||
@@ -773,6 +794,29 @@
|
||||
border-color: hsl(var(--destructive) / 0.5);
|
||||
}
|
||||
|
||||
/* Enable/Disable button variants */
|
||||
.btn-sm.btn-outline-success {
|
||||
background: transparent;
|
||||
border: 1px solid hsl(142 76% 36% / 0.4);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline-success:hover {
|
||||
background: hsl(142 76% 36% / 0.1);
|
||||
border-color: hsl(142 76% 36% / 0.6);
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline-warning {
|
||||
background: transparent;
|
||||
border: 1px solid hsl(38 92% 50% / 0.4);
|
||||
color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.btn-sm.btn-outline-warning:hover {
|
||||
background: hsl(38 92% 50% / 0.1);
|
||||
border-color: hsl(38 92% 50% / 0.6);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
|
||||
@@ -622,11 +622,110 @@ select.cli-input {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.modal-actions button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.modal-actions .btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.modal-actions .btn-secondary:hover {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
border-color: hsl(var(--muted-foreground) / 0.3);
|
||||
}
|
||||
|
||||
.modal-actions .btn-primary {
|
||||
background: hsl(var(--primary));
|
||||
border: 1px solid hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.modal-actions .btn-primary:hover {
|
||||
background: hsl(var(--primary) / 0.9);
|
||||
box-shadow: 0 2px 8px hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.modal-actions .btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.modal-actions .btn-danger {
|
||||
background: hsl(var(--destructive));
|
||||
border: 1px solid hsl(var(--destructive));
|
||||
color: hsl(var(--destructive-foreground));
|
||||
}
|
||||
|
||||
.modal-actions .btn-danger:hover {
|
||||
background: hsl(var(--destructive) / 0.9);
|
||||
box-shadow: 0 2px 8px hsl(var(--destructive) / 0.3);
|
||||
}
|
||||
|
||||
.modal-actions button i,
|
||||
.modal-actions button svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Handle .btn class prefix */
|
||||
.modal-actions .btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.modal-actions .btn.btn-secondary {
|
||||
background: transparent;
|
||||
border: 1px solid hsl(var(--border));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.modal-actions .btn.btn-secondary:hover {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--foreground));
|
||||
border-color: hsl(var(--muted-foreground) / 0.3);
|
||||
}
|
||||
|
||||
.modal-actions .btn.btn-primary {
|
||||
background: hsl(var(--primary));
|
||||
border: 1px solid hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.modal-actions .btn.btn-primary:hover {
|
||||
background: hsl(var(--primary) / 0.9);
|
||||
box-shadow: 0 2px 8px hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
/* Button Icon */
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
@@ -1916,4 +2015,84 @@ select.cli-input {
|
||||
.health-check-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Model Settings Modal - Endpoint Preview
|
||||
=========================== */
|
||||
|
||||
.endpoint-preview-section {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.endpoint-preview-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.endpoint-preview-section h4 i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.endpoint-preview-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: hsl(var(--background));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.endpoint-preview-box code {
|
||||
flex: 1;
|
||||
font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--primary));
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.endpoint-preview-box .btn-icon-sm {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Form Section within Modal */
|
||||
.form-section {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-section h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.form-section:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Capabilities Checkboxes */
|
||||
.capabilities-checkboxes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.capabilities-checkboxes .checkbox-label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ let semanticStatus = { available: false };
|
||||
let ccwInstallStatus = { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||
let defaultCliTool = 'gemini';
|
||||
let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json
|
||||
let cliToolsConfig = {}; // CLI tools enable/disable config
|
||||
let apiEndpoints = []; // API endpoints from LiteLLM config
|
||||
|
||||
// Smart Context settings
|
||||
let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true';
|
||||
@@ -41,6 +43,12 @@ async function loadAllStatuses() {
|
||||
semanticStatus = data.semantic || { available: false };
|
||||
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||
|
||||
// Load CLI tools config and API endpoints
|
||||
await Promise.all([
|
||||
loadCliToolsConfig(),
|
||||
loadApiEndpoints()
|
||||
]);
|
||||
|
||||
// Update badges
|
||||
updateCliBadge();
|
||||
updateCodexLensBadge();
|
||||
@@ -168,6 +176,67 @@ async function loadInstalledModels() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load CLI tools config from .claude/cli-tools.json (project or global fallback)
|
||||
*/
|
||||
async function loadCliToolsConfig() {
|
||||
try {
|
||||
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 || {};
|
||||
window.claudeCliToolsConfig = data; // Full config available globally
|
||||
|
||||
// Load default tool from config
|
||||
if (data.defaultTool) {
|
||||
defaultCliTool = data.defaultTool;
|
||||
}
|
||||
|
||||
console.log('[CLI Config] Loaded from:', data._configInfo?.source || 'unknown', '| Default:', data.defaultTool);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI tools config:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update CLI tool enabled status
|
||||
*/
|
||||
async function updateCliToolEnabled(tool, enabled) {
|
||||
try {
|
||||
const response = await fetch('/api/cli/tools-config/' + tool, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: enabled })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to update');
|
||||
showRefreshToast(tool + (enabled ? ' enabled' : ' disabled'), 'success');
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to update CLI tool:', err);
|
||||
showRefreshToast('Failed to update ' + tool, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load API endpoints from LiteLLM config
|
||||
*/
|
||||
async function loadApiEndpoints() {
|
||||
try {
|
||||
const response = await fetch('/api/litellm-api/endpoints');
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
apiEndpoints = data.endpoints || [];
|
||||
return apiEndpoints;
|
||||
} catch (err) {
|
||||
console.error('Failed to load API endpoints:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Badge Update ==========
|
||||
function updateCliBadge() {
|
||||
const badge = document.getElementById('badgeCliTools');
|
||||
@@ -234,25 +303,41 @@ function renderCliStatus() {
|
||||
const status = cliToolStatus[tool] || {};
|
||||
const isAvailable = status.available;
|
||||
const isDefault = defaultCliTool === tool;
|
||||
const config = cliToolsConfig[tool] || { enabled: true };
|
||||
const isEnabled = config.enabled !== false;
|
||||
const canSetDefault = isAvailable && isEnabled && !isDefault;
|
||||
|
||||
return `
|
||||
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'} ${!isEnabled ? 'disabled' : ''}">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status ${isAvailable ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-status ${isAvailable && isEnabled ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
|
||||
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
|
||||
${!isEnabled && isAvailable ? '<span class="cli-tool-badge-disabled">Disabled</span>' : ''}
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${toolDescriptions[tool]}
|
||||
</div>
|
||||
<div class="cli-tool-info mt-2">
|
||||
${isAvailable
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> 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 class="cli-tool-info mt-2 flex items-center justify-between">
|
||||
<div>
|
||||
${isAvailable
|
||||
? (isEnabled
|
||||
? `<span class="text-success flex items-center gap-1"><i data-lucide="check-circle" class="w-3 h-3"></i> Ready</span>`
|
||||
: `<span class="text-warning flex items-center gap-1"><i data-lucide="pause-circle" class="w-3 h-3"></i> Disabled</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>
|
||||
<div class="cli-tool-actions mt-3">
|
||||
${isAvailable && !isDefault
|
||||
<div class="cli-tool-actions mt-3 flex gap-2">
|
||||
${isAvailable ? (isEnabled
|
||||
? `<button class="btn-sm btn-outline-warning flex items-center gap-1" onclick="toggleCliTool('${tool}', false)">
|
||||
<i data-lucide="pause" class="w-3 h-3"></i> Disable
|
||||
</button>`
|
||||
: `<button class="btn-sm btn-outline-success flex items-center gap-1" onclick="toggleCliTool('${tool}', true)">
|
||||
<i data-lucide="play" class="w-3 h-3"></i> Enable
|
||||
</button>`
|
||||
) : ''}
|
||||
${canSetDefault
|
||||
? `<button class="btn-sm btn-outline flex items-center gap-1" onclick="setDefaultCliTool('${tool}')">
|
||||
<i data-lucide="star" class="w-3 h-3"></i> Set Default
|
||||
</button>`
|
||||
@@ -365,11 +450,42 @@ function renderCliStatus() {
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// API Endpoints section
|
||||
const apiEndpointsHtml = apiEndpoints.length > 0 ? `
|
||||
<div class="cli-api-endpoints-section" style="margin-top: 1.5rem;">
|
||||
<div class="cli-section-header" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem;">
|
||||
<h4 style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600; margin: 0;">
|
||||
<i data-lucide="link" class="w-4 h-4"></i> API Endpoints
|
||||
</h4>
|
||||
<span class="badge" style="padding: 0.125rem 0.5rem; font-size: 0.75rem; border-radius: 0.25rem; background: var(--muted); color: var(--muted-foreground);">${apiEndpoints.length}</span>
|
||||
</div>
|
||||
<div class="cli-endpoints-list" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 0.75rem;">
|
||||
${apiEndpoints.map(ep => `
|
||||
<div class="cli-endpoint-card ${ep.enabled ? 'available' : 'unavailable'}" style="padding: 0.75rem; border: 1px solid var(--border); border-radius: 0.5rem; background: var(--card);">
|
||||
<div class="cli-endpoint-header" style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||
<span class="cli-tool-status ${ep.enabled ? 'status-available' : 'status-unavailable'}" style="width: 8px; height: 8px; border-radius: 50%; background: ${ep.enabled ? 'var(--success)' : 'var(--muted-foreground)'}; flex-shrink: 0;"></span>
|
||||
<span class="cli-endpoint-id" style="font-weight: 500; font-size: 0.875rem;">${ep.id}</span>
|
||||
</div>
|
||||
<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>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
// Config source info
|
||||
const configInfo = window.claudeCliToolsConfig?._configInfo || {};
|
||||
const configSourceLabel = configInfo.source === 'project' ? 'Project' : configInfo.source === 'global' ? 'Global' : 'Default';
|
||||
const configSourceClass = configInfo.source === 'project' ? 'text-success' : configInfo.source === 'global' ? 'text-primary' : 'text-muted-foreground';
|
||||
|
||||
// CLI Settings section
|
||||
const settingsHtml = `
|
||||
<div class="cli-settings-section">
|
||||
<div class="cli-settings-header">
|
||||
<h4><i data-lucide="settings" class="w-3.5 h-3.5"></i> Settings</h4>
|
||||
<span class="badge text-xs ${configSourceClass}" title="${configInfo.activePath || ''}">${configSourceLabel}</span>
|
||||
</div>
|
||||
<div class="cli-settings-grid">
|
||||
<div class="cli-setting-item">
|
||||
@@ -436,6 +552,20 @@ function renderCliStatus() {
|
||||
</div>
|
||||
<p class="cli-setting-desc">Maximum files to include in smart context</p>
|
||||
</div>
|
||||
<div class="cli-setting-item">
|
||||
<label class="cli-setting-label">
|
||||
<i data-lucide="hard-drive" class="w-3 h-3"></i>
|
||||
Cache Injection
|
||||
</label>
|
||||
<div class="cli-setting-control">
|
||||
<select class="cli-setting-select" onchange="setCacheInjectionMode(this.value)">
|
||||
<option value="auto" ${getCacheInjectionMode() === 'auto' ? 'selected' : ''}>Auto</option>
|
||||
<option value="manual" ${getCacheInjectionMode() === 'manual' ? 'selected' : ''}>Manual</option>
|
||||
<option value="disabled" ${getCacheInjectionMode() === 'disabled' ? 'selected' : ''}>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="cli-setting-desc">Cache prefix/suffix injection mode for prompts</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -453,6 +583,7 @@ function renderCliStatus() {
|
||||
${codexLensHtml}
|
||||
${semanticHtml}
|
||||
</div>
|
||||
${apiEndpointsHtml}
|
||||
${settingsHtml}
|
||||
`;
|
||||
|
||||
@@ -464,7 +595,30 @@ function renderCliStatus() {
|
||||
|
||||
// ========== Actions ==========
|
||||
function setDefaultCliTool(tool) {
|
||||
// Validate: tool must be available and enabled
|
||||
const status = cliToolStatus[tool] || {};
|
||||
const config = cliToolsConfig[tool] || { enabled: true };
|
||||
|
||||
if (!status.available) {
|
||||
showRefreshToast(`Cannot set ${tool} as default: not installed`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.enabled === false) {
|
||||
showRefreshToast(`Cannot set ${tool} as default: tool is disabled`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
defaultCliTool = tool;
|
||||
// Save to config
|
||||
if (window.claudeCliToolsConfig) {
|
||||
window.claudeCliToolsConfig.defaultTool = tool;
|
||||
fetch('/api/cli/tools-config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ defaultTool: tool })
|
||||
}).catch(err => console.error('Failed to save default tool:', err));
|
||||
}
|
||||
renderCliStatus();
|
||||
showRefreshToast(`Default CLI tool set to ${tool}`, 'success');
|
||||
}
|
||||
@@ -505,11 +659,67 @@ function setRecursiveQueryEnabled(enabled) {
|
||||
showRefreshToast(`Recursive Query ${enabled ? 'enabled' : 'disabled'}`, 'success');
|
||||
}
|
||||
|
||||
function getCacheInjectionMode() {
|
||||
if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) {
|
||||
return window.claudeCliToolsConfig.settings.cache?.injectionMode || 'auto';
|
||||
}
|
||||
return localStorage.getItem('ccw-cache-injection-mode') || 'auto';
|
||||
}
|
||||
|
||||
async function setCacheInjectionMode(mode) {
|
||||
try {
|
||||
const response = await fetch('/api/cli/tools-config/cache', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ injectionMode: mode })
|
||||
});
|
||||
if (response.ok) {
|
||||
localStorage.setItem('ccw-cache-injection-mode', mode);
|
||||
if (window.claudeCliToolsConfig) {
|
||||
window.claudeCliToolsConfig.settings.cache.injectionMode = mode;
|
||||
}
|
||||
showRefreshToast(`Cache injection mode set to ${mode}`, 'success');
|
||||
} else {
|
||||
showRefreshToast('Failed to update cache settings', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update cache settings:', err);
|
||||
showRefreshToast('Failed to update cache settings', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAllCliStatus() {
|
||||
await loadAllStatuses();
|
||||
renderCliStatus();
|
||||
}
|
||||
|
||||
async function toggleCliTool(tool, enabled) {
|
||||
// If disabling the current default tool, switch to another available+enabled tool
|
||||
if (!enabled && defaultCliTool === tool) {
|
||||
const tools = ['gemini', 'qwen', 'codex', 'claude'];
|
||||
const newDefault = tools.find(t => {
|
||||
if (t === tool) return false;
|
||||
const status = cliToolStatus[t] || {};
|
||||
const config = cliToolsConfig[t] || { enabled: true };
|
||||
return status.available && config.enabled !== false;
|
||||
});
|
||||
|
||||
if (newDefault) {
|
||||
defaultCliTool = newDefault;
|
||||
if (window.claudeCliToolsConfig) {
|
||||
window.claudeCliToolsConfig.defaultTool = newDefault;
|
||||
}
|
||||
showRefreshToast(`Default tool switched to ${newDefault}`, 'info');
|
||||
} else {
|
||||
showRefreshToast(`Warning: No other enabled tool available for default`, 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
await updateCliToolEnabled(tool, enabled);
|
||||
await loadAllStatuses();
|
||||
renderCliStatus();
|
||||
}
|
||||
|
||||
function installCodexLens() {
|
||||
openCodexLensInstallWizard();
|
||||
}
|
||||
|
||||
@@ -1389,7 +1389,13 @@ const i18n = {
|
||||
'apiSettings.previewModel': 'Preview',
|
||||
'apiSettings.modelSettings': 'Model Settings',
|
||||
'apiSettings.deleteModel': 'Delete Model',
|
||||
'apiSettings.endpointPreview': 'Endpoint Preview',
|
||||
'apiSettings.modelBaseUrlOverride': 'Base URL Override',
|
||||
'apiSettings.modelBaseUrlHint': 'Override the provider base URL for this specific model (leave empty to use provider default)',
|
||||
'apiSettings.providerUpdated': 'Provider updated',
|
||||
'apiSettings.syncToCodexLens': 'Sync to CodexLens',
|
||||
'apiSettings.configSynced': 'Config synced to CodexLens',
|
||||
'apiSettings.sdkAutoAppends': 'SDK auto-appends',
|
||||
'apiSettings.preview': 'Preview',
|
||||
'apiSettings.used': 'used',
|
||||
'apiSettings.total': 'total',
|
||||
@@ -1422,6 +1428,7 @@ const i18n = {
|
||||
'apiSettings.cacheDisabled': 'Cache Disabled',
|
||||
'apiSettings.providerSaved': 'Provider saved successfully',
|
||||
'apiSettings.providerDeleted': 'Provider deleted successfully',
|
||||
'apiSettings.apiBaseUpdated': 'API Base URL updated successfully',
|
||||
'apiSettings.endpointSaved': 'Endpoint saved successfully',
|
||||
'apiSettings.endpointDeleted': 'Endpoint deleted successfully',
|
||||
'apiSettings.cacheCleared': 'Cache cleared successfully',
|
||||
@@ -3039,7 +3046,12 @@ const i18n = {
|
||||
'apiSettings.previewModel': '预览',
|
||||
'apiSettings.modelSettings': '模型设置',
|
||||
'apiSettings.deleteModel': '删除模型',
|
||||
'apiSettings.endpointPreview': '端点预览',
|
||||
'apiSettings.modelBaseUrlOverride': '基础 URL 覆盖',
|
||||
'apiSettings.modelBaseUrlHint': '为此模型覆盖供应商的基础 URL(留空则使用供应商默认值)',
|
||||
'apiSettings.providerUpdated': '供应商已更新',
|
||||
'apiSettings.syncToCodexLens': '同步到 CodexLens',
|
||||
'apiSettings.configSynced': '配置已同步到 CodexLens',
|
||||
'apiSettings.preview': '预览',
|
||||
'apiSettings.used': '已使用',
|
||||
'apiSettings.total': '总计',
|
||||
@@ -3072,6 +3084,7 @@ const i18n = {
|
||||
'apiSettings.cacheDisabled': '缓存已禁用',
|
||||
'apiSettings.providerSaved': '提供商保存成功',
|
||||
'apiSettings.providerDeleted': '提供商删除成功',
|
||||
'apiSettings.apiBaseUpdated': 'API 基础 URL 更新成功',
|
||||
'apiSettings.endpointSaved': '端点保存成功',
|
||||
'apiSettings.endpointDeleted': '端点删除成功',
|
||||
'apiSettings.cacheCleared': '缓存清除成功',
|
||||
|
||||
@@ -359,10 +359,20 @@ async function deleteProvider(providerId) {
|
||||
|
||||
/**
|
||||
* Test provider connection
|
||||
* @param {string} [providerIdParam] - Optional provider ID. If not provided, uses form context or selectedProviderId
|
||||
*/
|
||||
async function testProviderConnection() {
|
||||
const form = document.getElementById('providerForm');
|
||||
const providerId = form.dataset.providerId;
|
||||
async function testProviderConnection(providerIdParam) {
|
||||
var providerId = providerIdParam;
|
||||
|
||||
// Try to get providerId from different sources
|
||||
if (!providerId) {
|
||||
var form = document.getElementById('providerForm');
|
||||
if (form && form.dataset.providerId) {
|
||||
providerId = form.dataset.providerId;
|
||||
} else if (selectedProviderId) {
|
||||
providerId = selectedProviderId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!providerId) {
|
||||
showRefreshToast(t('apiSettings.saveProviderFirst'), 'warning');
|
||||
@@ -553,9 +563,9 @@ async function showAddEndpointModal() {
|
||||
'</div>' +
|
||||
'</fieldset>' +
|
||||
'<div class="modal-actions">' +
|
||||
'<button type="button" class="btn btn-secondary" onclick="closeEndpointModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button type="button" class="btn btn-secondary" onclick="closeEndpointModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
|
||||
'<button type="submit" class="btn btn-primary">' +
|
||||
'<i data-lucide="save"></i> ' + t('common.save') +
|
||||
'<i data-lucide="check"></i> ' + t('common.save') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
@@ -845,7 +855,10 @@ async function renderApiSettings() {
|
||||
}
|
||||
|
||||
// Build split layout
|
||||
container.innerHTML = '<div class="api-settings-container api-settings-split">' +
|
||||
container.innerHTML =
|
||||
// CCW-LiteLLM Status Container
|
||||
'<div id="ccwLitellmStatusContainer" class="mb-4"></div>' +
|
||||
'<div class="api-settings-container api-settings-split">' +
|
||||
// Left Sidebar
|
||||
'<aside class="api-settings-sidebar">' +
|
||||
sidebarTabsHtml +
|
||||
@@ -878,6 +891,9 @@ async function renderApiSettings() {
|
||||
renderCacheMainPanel();
|
||||
}
|
||||
|
||||
// Check and render ccw-litellm status
|
||||
checkCcwLitellmStatus().then(renderCcwLitellmStatusCard);
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
@@ -966,7 +982,10 @@ function renderProviderDetail(providerId) {
|
||||
}
|
||||
|
||||
var maskedKey = provider.apiKey ? '••••••••••••••••' + provider.apiKey.slice(-4) : '••••••••';
|
||||
var apiBasePreview = (provider.apiBase || getDefaultApiBase(provider.type)) + '/chat/completions';
|
||||
var currentApiBase = provider.apiBase || getDefaultApiBase(provider.type);
|
||||
// Show full endpoint URL preview based on active model tab
|
||||
var endpointPath = activeModelTab === 'embedding' ? '/embeddings' : '/chat/completions';
|
||||
var apiBasePreview = currentApiBase + endpointPath;
|
||||
|
||||
var html = '<div class="provider-detail-header">' +
|
||||
'<div class="provider-detail-title">' +
|
||||
@@ -1007,13 +1026,18 @@ function renderProviderDetail(providerId) {
|
||||
'<button class="btn btn-secondary" onclick="testProviderConnection()">' + t('apiSettings.testConnection') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// API Base URL field
|
||||
// API Base URL field - editable
|
||||
'<div class="field-group">' +
|
||||
'<div class="field-label">' +
|
||||
'<span>' + t('apiSettings.apiBaseUrl') + '</span>' +
|
||||
'</div>' +
|
||||
'<input type="text" class="cli-input" value="' + escapeHtml(provider.apiBase || getDefaultApiBase(provider.type)) + '" readonly />' +
|
||||
'<span class="field-hint">' + t('apiSettings.preview') + ': ' + apiBasePreview + '</span>' +
|
||||
'<div class="field-input-group">' +
|
||||
'<input type="text" class="cli-input" id="provider-detail-apibase" value="' + escapeHtml(currentApiBase) + '" placeholder="https://api.openai.com/v1" oninput="updateApiBasePreview(this.value)" />' +
|
||||
'<button class="btn btn-secondary" onclick="saveProviderApiBase(\'' + providerId + '\')">' +
|
||||
'<i data-lucide="save"></i> ' + t('common.save') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<span class="field-hint" id="api-base-preview">' + t('apiSettings.preview') + ': ' + escapeHtml(apiBasePreview) + '</span>' +
|
||||
'</div>' +
|
||||
// Model Section
|
||||
'<div class="model-section">' +
|
||||
@@ -1037,11 +1061,14 @@ function renderProviderDetail(providerId) {
|
||||
'</div>' +
|
||||
'<div class="model-tree" id="model-tree"></div>' +
|
||||
'</div>' +
|
||||
// Multi-key settings button
|
||||
// Multi-key and sync buttons
|
||||
'<div class="multi-key-trigger">' +
|
||||
'<button class="btn btn-secondary multi-key-btn" onclick="showMultiKeyModal(\'' + providerId + '\')">' +
|
||||
'<i data-lucide="key-round"></i> ' + t('apiSettings.multiKeySettings') +
|
||||
'</button>' +
|
||||
'<button class="btn btn-secondary" onclick="syncConfigToCodexLens()">' +
|
||||
'<i data-lucide="refresh-cw"></i> ' + t('apiSettings.syncToCodexLens') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -1107,18 +1134,21 @@ function renderModelTree(provider) {
|
||||
? formatContextWindow(model.capabilities.contextWindow)
|
||||
: '';
|
||||
|
||||
// Badge for embedding models shows dimension instead of context window
|
||||
var embeddingBadge = model.capabilities && model.capabilities.embeddingDimension
|
||||
? model.capabilities.embeddingDimension + 'd'
|
||||
: '';
|
||||
var displayBadge = activeModelTab === 'llm' ? badge : embeddingBadge;
|
||||
|
||||
html += '<div class="model-item" data-model-id="' + model.id + '">' +
|
||||
'<i data-lucide="' + (activeModelTab === 'llm' ? 'sparkles' : 'box') + '" class="model-item-icon"></i>' +
|
||||
'<span class="model-item-name">' + escapeHtml(model.name) + '</span>' +
|
||||
(badge ? '<span class="model-item-badge">' + badge + '</span>' : '') +
|
||||
(displayBadge ? '<span class="model-item-badge">' + displayBadge + '</span>' : '') +
|
||||
'<div class="model-item-actions">' +
|
||||
'<button class="btn-icon-sm" onclick="previewModel(\'' + model.id + '\')" title="' + t('apiSettings.previewModel') + '">' +
|
||||
'<i data-lucide="eye"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-icon-sm" onclick="showModelSettingsModal(\'' + model.id + '\')" title="' + t('apiSettings.modelSettings') + '">' +
|
||||
'<button class="btn-icon-sm" onclick="showModelSettingsModal(\'' + selectedProviderId + '\', \'' + model.id + '\', \'' + activeModelTab + '\')" title="' + t('apiSettings.modelSettings') + '">' +
|
||||
'<i data-lucide="settings"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn-icon-sm text-destructive" onclick="deleteModel(\'' + model.id + '\')" title="' + t('apiSettings.deleteModel') + '">' +
|
||||
'<button class="btn-icon-sm text-destructive" onclick="deleteModel(\'' + selectedProviderId + '\', \'' + model.id + '\', \'' + activeModelTab + '\')" title="' + t('apiSettings.deleteModel') + '">' +
|
||||
'<i data-lucide="trash-2"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
@@ -1418,8 +1448,8 @@ function showAddModelModal(providerId, modelType) {
|
||||
'</div>' +
|
||||
|
||||
'<div class="modal-actions">' +
|
||||
'<button type="button" class="btn btn-secondary" onclick="closeAddModelModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button type="submit" class="btn btn-primary">' + t('common.save') + '</button>' +
|
||||
'<button type="button" class="btn btn-secondary" onclick="closeAddModelModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
|
||||
'<button type="submit" class="btn btn-primary"><i data-lucide="check"></i> ' + t('common.save') + '</button>' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
@@ -1624,29 +1654,51 @@ function showModelSettingsModal(providerId, modelId, modelType) {
|
||||
var capabilities = model.capabilities || {};
|
||||
var endpointSettings = model.endpointSettings || {};
|
||||
|
||||
// Calculate endpoint preview URL
|
||||
var providerBase = provider.apiBase || getDefaultApiBase(provider.type);
|
||||
var modelBaseUrl = endpointSettings.baseUrl || providerBase;
|
||||
var endpointPath = isLlm ? '/chat/completions' : '/embeddings';
|
||||
var endpointPreview = modelBaseUrl + endpointPath;
|
||||
|
||||
var modalHtml = '<div class="modal-overlay" id="model-settings-modal">' +
|
||||
'<div class="modal-content" style="max-width: 550px;">' +
|
||||
'<div class="modal-content" style="max-width: 600px;">' +
|
||||
'<div class="modal-header">' +
|
||||
'<h3>' + t('apiSettings.modelSettings') + ': ' + model.name + '</h3>' +
|
||||
'<h3>' + t('apiSettings.modelSettings') + ': ' + escapeHtml(model.name) + '</h3>' +
|
||||
'<button class="modal-close" onclick="closeModelSettingsModal()">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="modal-body">' +
|
||||
'<form id="model-settings-form" onsubmit="saveModelSettings(event, \'' + providerId + '\', \'' + modelId + '\', \'' + modelType + '\')">' +
|
||||
|
||||
// Endpoint Preview Section (combined view + settings)
|
||||
'<div class="form-section endpoint-preview-section">' +
|
||||
'<h4><i data-lucide="' + (isLlm ? 'message-square' : 'box') + '"></i> ' + t('apiSettings.endpointPreview') + '</h4>' +
|
||||
'<div class="endpoint-preview-box">' +
|
||||
'<code id="model-endpoint-preview">' + escapeHtml(endpointPreview) + '</code>' +
|
||||
'<button type="button" class="btn-icon-sm" onclick="copyModelEndpoint()" title="' + t('common.copy') + '">' +
|
||||
'<i data-lucide="copy"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.modelBaseUrlOverride') + ' <span class="text-muted">(' + t('common.optional') + ')</span></label>' +
|
||||
'<input type="text" id="model-settings-baseurl" class="cli-input" value="' + escapeHtml(endpointSettings.baseUrl || '') + '" placeholder="' + escapeHtml(providerBase) + '" oninput="updateModelEndpointPreview(\'' + (isLlm ? 'chat/completions' : 'embeddings') + '\', \'' + escapeHtml(providerBase) + '\')">' +
|
||||
'<small class="form-hint">' + t('apiSettings.modelBaseUrlHint') + '</small>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Basic Info
|
||||
'<div class="form-section">' +
|
||||
'<h4>' + t('apiSettings.basicInfo') + '</h4>' +
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.modelName') + '</label>' +
|
||||
'<input type="text" id="model-settings-name" class="cli-input" value="' + (model.name || '') + '" required>' +
|
||||
'<input type="text" id="model-settings-name" class="cli-input" value="' + escapeHtml(model.name || '') + '" required>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.modelSeries') + '</label>' +
|
||||
'<input type="text" id="model-settings-series" class="cli-input" value="' + (model.series || '') + '" required>' +
|
||||
'<input type="text" id="model-settings-series" class="cli-input" value="' + escapeHtml(model.series || '') + '" required>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.description') + '</label>' +
|
||||
'<textarea id="model-settings-description" class="cli-input" rows="2">' + (model.description || '') + '</textarea>' +
|
||||
'<textarea id="model-settings-description" class="cli-input" rows="2">' + escapeHtml(model.description || '') + '</textarea>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
@@ -1678,19 +1730,21 @@ function showModelSettingsModal(providerId, modelId, modelType) {
|
||||
// Endpoint Settings
|
||||
'<div class="form-section">' +
|
||||
'<h4>' + t('apiSettings.endpointSettings') + '</h4>' +
|
||||
'<div class="form-group">' +
|
||||
'<div class="form-row">' +
|
||||
'<div class="form-group form-group-half">' +
|
||||
'<label>' + t('apiSettings.timeout') + ' (' + t('apiSettings.seconds') + ')</label>' +
|
||||
'<input type="number" id="model-settings-timeout" class="cli-input" value="' + (endpointSettings.timeout || 300) + '" min="10" max="3600">' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<div class="form-group form-group-half">' +
|
||||
'<label>' + t('apiSettings.maxRetries') + '</label>' +
|
||||
'<input type="number" id="model-settings-retries" class="cli-input" value="' + (endpointSettings.maxRetries || 3) + '" min="0" max="10">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="modal-actions">' +
|
||||
'<button type="button" class="btn-secondary" onclick="closeModelSettingsModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button type="submit" class="btn-primary">' + t('common.save') + '</button>' +
|
||||
'<button type="button" class="btn-secondary" onclick="closeModelSettingsModal()"><i data-lucide="x"></i> ' + t('common.cancel') + '</button>' +
|
||||
'<button type="submit" class="btn-primary"><i data-lucide="check"></i> ' + t('common.save') + '</button>' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
@@ -1701,6 +1755,33 @@ function showModelSettingsModal(providerId, modelId, modelType) {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update model endpoint preview when base URL changes
|
||||
*/
|
||||
function updateModelEndpointPreview(endpointPath, defaultBase) {
|
||||
var baseUrlInput = document.getElementById('model-settings-baseurl');
|
||||
var previewElement = document.getElementById('model-endpoint-preview');
|
||||
if (!baseUrlInput || !previewElement) return;
|
||||
|
||||
var baseUrl = baseUrlInput.value.trim() || defaultBase;
|
||||
// Remove trailing slash if present
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
previewElement.textContent = baseUrl + '/' + endpointPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy model endpoint URL to clipboard
|
||||
*/
|
||||
function copyModelEndpoint() {
|
||||
var previewElement = document.getElementById('model-endpoint-preview');
|
||||
if (previewElement) {
|
||||
navigator.clipboard.writeText(previewElement.textContent);
|
||||
showRefreshToast(t('common.copied'), 'success');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModelSettingsModal() {
|
||||
var modal = document.getElementById('model-settings-modal');
|
||||
if (modal) modal.remove();
|
||||
@@ -1744,7 +1825,13 @@ function saveModelSettings(event, providerId, modelId, modelType) {
|
||||
}
|
||||
|
||||
// Update endpoint settings
|
||||
var baseUrlOverride = document.getElementById('model-settings-baseurl').value.trim();
|
||||
// Remove trailing slash if present
|
||||
if (baseUrlOverride && baseUrlOverride.endsWith('/')) {
|
||||
baseUrlOverride = baseUrlOverride.slice(0, -1);
|
||||
}
|
||||
models[modelIndex].endpointSettings = {
|
||||
baseUrl: baseUrlOverride || undefined,
|
||||
timeout: parseInt(document.getElementById('model-settings-timeout').value) || 300,
|
||||
maxRetries: parseInt(document.getElementById('model-settings-retries').value) || 3
|
||||
};
|
||||
@@ -1774,11 +1861,6 @@ function saveModelSettings(event, providerId, modelId, modelType) {
|
||||
});
|
||||
}
|
||||
|
||||
function previewModel(providerId, modelId, modelType) {
|
||||
// Just open the settings modal in read mode for now
|
||||
showModelSettingsModal(providerId, modelId, modelType);
|
||||
}
|
||||
|
||||
function deleteModel(providerId, modelId, modelType) {
|
||||
if (!confirm(t('common.confirmDelete'))) return;
|
||||
|
||||
@@ -1823,6 +1905,59 @@ function copyProviderApiKey(providerId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save provider API base URL
|
||||
*/
|
||||
async function saveProviderApiBase(providerId) {
|
||||
var input = document.getElementById('provider-detail-apibase');
|
||||
if (!input) return;
|
||||
|
||||
var newApiBase = input.value.trim();
|
||||
// Remove trailing slash if present
|
||||
if (newApiBase.endsWith('/')) {
|
||||
newApiBase = newApiBase.slice(0, -1);
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/litellm-api/providers/' + providerId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ apiBase: newApiBase || undefined })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to update API base');
|
||||
|
||||
// Update local data
|
||||
var provider = apiSettingsData.providers.find(function(p) { return p.id === providerId; });
|
||||
if (provider) {
|
||||
provider.apiBase = newApiBase || undefined;
|
||||
}
|
||||
|
||||
// Update preview
|
||||
updateApiBasePreview(newApiBase);
|
||||
showRefreshToast(t('apiSettings.apiBaseUpdated'), 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to save API base:', err);
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update API base preview text showing full endpoint URL
|
||||
*/
|
||||
function updateApiBasePreview(apiBase) {
|
||||
var preview = document.getElementById('api-base-preview');
|
||||
if (!preview) return;
|
||||
|
||||
var base = apiBase || getDefaultApiBase('openai');
|
||||
// Remove trailing slash if present
|
||||
if (base.endsWith('/')) {
|
||||
base = base.slice(0, -1);
|
||||
}
|
||||
var endpointPath = activeModelTab === 'embedding' ? '/embeddings' : '/chat/completions';
|
||||
preview.textContent = t('apiSettings.preview') + ': ' + base + endpointPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete provider with confirmation
|
||||
*/
|
||||
@@ -1859,6 +1994,25 @@ async function deleteProviderWithConfirm(providerId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync config to CodexLens (generate YAML config for ccw_litellm)
|
||||
*/
|
||||
async function syncConfigToCodexLens() {
|
||||
try {
|
||||
var response = await fetch('/api/litellm-api/config/sync', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to sync config');
|
||||
|
||||
var result = await response.json();
|
||||
showRefreshToast(t('apiSettings.configSynced') + ' (' + result.yamlPath + ')', 'success');
|
||||
} catch (err) {
|
||||
console.error('Failed to sync config:', err);
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider icon class based on type
|
||||
*/
|
||||
@@ -2343,7 +2497,7 @@ function showMultiKeyModal(providerId) {
|
||||
renderHealthCheckSection(provider) +
|
||||
'</div>' +
|
||||
'<div class="modal-actions">' +
|
||||
'<button type="button" class="btn-primary" onclick="closeMultiKeyModal()">' + t('common.close') + '</button>' +
|
||||
'<button type="button" class="btn-primary" onclick="closeMultiKeyModal()"><i data-lucide="check"></i> ' + t('common.close') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
@@ -2578,6 +2732,99 @@ function toggleKeyVisibility(btn) {
|
||||
}
|
||||
|
||||
|
||||
// ========== CCW-LiteLLM Management ==========
|
||||
|
||||
/**
|
||||
* Check ccw-litellm installation status
|
||||
*/
|
||||
async function checkCcwLitellmStatus() {
|
||||
try {
|
||||
var response = await fetch('/api/litellm-api/ccw-litellm/status');
|
||||
var status = await response.json();
|
||||
window.ccwLitellmStatus = status;
|
||||
return status;
|
||||
} catch (e) {
|
||||
console.warn('[API Settings] Could not check ccw-litellm status:', e);
|
||||
return { installed: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render ccw-litellm status card
|
||||
*/
|
||||
function renderCcwLitellmStatusCard() {
|
||||
var container = document.getElementById('ccwLitellmStatusContainer');
|
||||
if (!container) return;
|
||||
|
||||
var status = window.ccwLitellmStatus || { installed: false };
|
||||
|
||||
if (status.installed) {
|
||||
container.innerHTML =
|
||||
'<div class="flex items-center gap-2 text-sm">' +
|
||||
'<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-success/10 text-success border border-success/20">' +
|
||||
'<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>' +
|
||||
'ccw-litellm ' + (status.version || '') +
|
||||
'</span>' +
|
||||
'</div>';
|
||||
} else {
|
||||
container.innerHTML =
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-muted text-muted-foreground border border-border text-sm">' +
|
||||
'<i data-lucide="circle" class="w-3.5 h-3.5"></i>' +
|
||||
'ccw-litellm not installed' +
|
||||
'</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="installCcwLitellm()">' +
|
||||
'<i data-lucide="download" class="w-3.5 h-3.5"></i> Install' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Install ccw-litellm package
|
||||
*/
|
||||
async function installCcwLitellm() {
|
||||
var container = document.getElementById('ccwLitellmStatusContainer');
|
||||
if (container) {
|
||||
container.innerHTML =
|
||||
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
|
||||
'<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div>' +
|
||||
'Installing ccw-litellm...' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/litellm-api/ccw-litellm/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast('ccw-litellm installed successfully!', 'success');
|
||||
// Refresh status
|
||||
await checkCcwLitellmStatus();
|
||||
renderCcwLitellmStatusCard();
|
||||
} else {
|
||||
showRefreshToast('Failed to install ccw-litellm: ' + result.error, 'error');
|
||||
renderCcwLitellmStatusCard();
|
||||
}
|
||||
} catch (e) {
|
||||
showRefreshToast('Installation error: ' + e.message, 'error');
|
||||
renderCcwLitellmStatusCard();
|
||||
}
|
||||
}
|
||||
|
||||
// Make functions globally accessible
|
||||
window.checkCcwLitellmStatus = checkCcwLitellmStatus;
|
||||
window.renderCcwLitellmStatusCard = renderCcwLitellmStatusCard;
|
||||
window.installCcwLitellm = installCcwLitellm;
|
||||
|
||||
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -1166,10 +1166,12 @@ async function deleteModel(profile) {
|
||||
* Initialize CodexLens index with bottom floating progress bar
|
||||
* @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector)
|
||||
* @param {string} embeddingModel - Model profile: 'code', 'fast'
|
||||
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
|
||||
*/
|
||||
async function initCodexLensIndex(indexType, embeddingModel) {
|
||||
async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend) {
|
||||
indexType = indexType || 'vector';
|
||||
embeddingModel = embeddingModel || 'code';
|
||||
embeddingBackend = embeddingBackend || 'fastembed';
|
||||
|
||||
// For vector or full index, check if semantic dependencies are available
|
||||
if (indexType === 'vector' || indexType === 'full') {
|
||||
@@ -1235,7 +1237,8 @@ async function initCodexLensIndex(indexType, embeddingModel) {
|
||||
var modelLabel = '';
|
||||
if (indexType !== 'normal') {
|
||||
var modelNames = { code: 'Code', fast: 'Fast' };
|
||||
modelLabel = ' [' + (modelNames[embeddingModel] || embeddingModel) + ']';
|
||||
var backendLabel = embeddingBackend === 'litellm' ? 'API: ' : '';
|
||||
modelLabel = ' [' + backendLabel + (modelNames[embeddingModel] || embeddingModel) + ']';
|
||||
}
|
||||
|
||||
progressBar.innerHTML =
|
||||
@@ -1272,17 +1275,19 @@ async function initCodexLensIndex(indexType, embeddingModel) {
|
||||
var apiIndexType = (indexType === 'full') ? 'vector' : indexType;
|
||||
|
||||
// Start indexing with specified type and model
|
||||
startCodexLensIndexing(apiIndexType, embeddingModel);
|
||||
startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the indexing process
|
||||
* @param {string} indexType - 'vector' or 'normal'
|
||||
* @param {string} embeddingModel - Model profile: 'code', 'fast'
|
||||
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
|
||||
*/
|
||||
async function startCodexLensIndexing(indexType, embeddingModel) {
|
||||
async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend) {
|
||||
indexType = indexType || 'vector';
|
||||
embeddingModel = embeddingModel || 'code';
|
||||
embeddingBackend = embeddingBackend || 'fastembed';
|
||||
var statusText = document.getElementById('codexlensIndexStatus');
|
||||
var progressBar = document.getElementById('codexlensIndexProgressBar');
|
||||
var percentText = document.getElementById('codexlensIndexPercent');
|
||||
@@ -1314,11 +1319,11 @@ async function startCodexLensIndexing(indexType, embeddingModel) {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel);
|
||||
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend);
|
||||
var response = await fetch('/api/codexlens/init', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel })
|
||||
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
@@ -1883,6 +1888,16 @@ async function renderCodexLensManager() {
|
||||
await loadCodexLensStatus();
|
||||
}
|
||||
|
||||
// Load LiteLLM API config for embedding backend options
|
||||
try {
|
||||
var litellmResponse = await fetch('/api/litellm-api/config');
|
||||
if (litellmResponse.ok) {
|
||||
window.litellmApiConfig = await litellmResponse.json();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[CodexLens] Could not load LiteLLM config:', e);
|
||||
}
|
||||
|
||||
var response = await fetch('/api/codexlens/config');
|
||||
var config = await response.json();
|
||||
|
||||
@@ -1946,6 +1961,15 @@ function buildCodexLensManagerPage(config) {
|
||||
'<div class="bg-card border border-border rounded-lg p-5">' +
|
||||
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="layers" class="w-5 h-5 text-primary"></i> ' + t('codexlens.createIndex') + '</h4>' +
|
||||
'<div class="space-y-4">' +
|
||||
// Backend selector (fastembed local or litellm API)
|
||||
'<div class="mb-4">' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + (t('codexlens.embeddingBackend') || 'Embedding Backend') + '</label>' +
|
||||
'<select id="pageBackendSelect" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm" onchange="onEmbeddingBackendChange()">' +
|
||||
'<option value="fastembed">' + (t('codexlens.localFastembed') || 'Local (FastEmbed)') + '</option>' +
|
||||
'<option value="litellm">' + (t('codexlens.apiLitellm') || 'API (LiteLLM)') + '</option>' +
|
||||
'</select>' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + (t('codexlens.backendHint') || 'Select local model or remote API endpoint') + '</p>' +
|
||||
'</div>' +
|
||||
// Model selector
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.embeddingModel') + '</label>' +
|
||||
@@ -2150,18 +2174,68 @@ function buildModelSelectOptionsForPage() {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle embedding backend change
|
||||
*/
|
||||
function onEmbeddingBackendChange() {
|
||||
var backendSelect = document.getElementById('pageBackendSelect');
|
||||
var modelSelect = document.getElementById('pageModelSelect');
|
||||
if (!backendSelect || !modelSelect) return;
|
||||
|
||||
var backend = backendSelect.value;
|
||||
|
||||
if (backend === 'litellm') {
|
||||
// Load LiteLLM embedding models
|
||||
modelSelect.innerHTML = buildLiteLLMModelOptions();
|
||||
} else {
|
||||
// Load local fastembed models
|
||||
modelSelect.innerHTML = buildModelSelectOptionsForPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build LiteLLM model options from config
|
||||
*/
|
||||
function buildLiteLLMModelOptions() {
|
||||
var litellmConfig = window.litellmApiConfig || {};
|
||||
var providers = litellmConfig.providers || [];
|
||||
var options = '';
|
||||
|
||||
providers.forEach(function(provider) {
|
||||
if (!provider.enabled) return;
|
||||
var models = provider.models || [];
|
||||
models.forEach(function(model) {
|
||||
if (model.type !== 'embedding' || !model.enabled) return;
|
||||
var label = model.name || model.id;
|
||||
var selected = options === '' ? ' selected' : '';
|
||||
options += '<option value="' + model.id + '"' + selected + '>' + label + '</option>';
|
||||
});
|
||||
});
|
||||
|
||||
if (options === '') {
|
||||
options = '<option value="" disabled selected>' + (t('codexlens.noApiModels') || 'No API embedding models configured') + '</option>';
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
// Make functions globally accessible
|
||||
window.onEmbeddingBackendChange = onEmbeddingBackendChange;
|
||||
|
||||
/**
|
||||
* Initialize index from page with selected model
|
||||
*/
|
||||
function initCodexLensIndexFromPage(indexType) {
|
||||
var backendSelect = document.getElementById('pageBackendSelect');
|
||||
var modelSelect = document.getElementById('pageModelSelect');
|
||||
var selectedBackend = backendSelect ? backendSelect.value : 'fastembed';
|
||||
var selectedModel = modelSelect ? modelSelect.value : 'code';
|
||||
|
||||
// For FTS-only index, model is not needed
|
||||
if (indexType === 'normal') {
|
||||
initCodexLensIndex(indexType);
|
||||
} else {
|
||||
initCodexLensIndex(indexType, selectedModel);
|
||||
initCodexLensIndex(indexType, selectedModel, selectedBackend);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
300
ccw/src/tools/claude-cli-tools.ts
Normal file
300
ccw/src/tools/claude-cli-tools.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Claude CLI Tools Configuration Manager
|
||||
* Manages .claude/cli-tools.json with fallback:
|
||||
* 1. Project workspace: {projectDir}/.claude/cli-tools.json (priority)
|
||||
* 2. Global: ~/.claude/cli-tools.json (fallback)
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
export interface ClaudeCliTool {
|
||||
enabled: boolean;
|
||||
isBuiltin: boolean;
|
||||
command: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ClaudeCacheSettings {
|
||||
injectionMode: 'auto' | 'manual' | 'disabled';
|
||||
defaultPrefix: string;
|
||||
defaultSuffix: string;
|
||||
}
|
||||
|
||||
export interface ClaudeCliToolsConfig {
|
||||
$schema?: string;
|
||||
version: string;
|
||||
tools: Record<string, ClaudeCliTool>;
|
||||
customEndpoints: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
defaultTool: string;
|
||||
settings: {
|
||||
promptFormat: 'plain' | 'yaml' | 'json';
|
||||
smartContext: {
|
||||
enabled: boolean;
|
||||
maxFiles: number;
|
||||
};
|
||||
nativeResume: boolean;
|
||||
recursiveQuery: boolean;
|
||||
cache: ClaudeCacheSettings;
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Default Config ==========
|
||||
|
||||
const DEFAULT_CONFIG: ClaudeCliToolsConfig = {
|
||||
version: '1.0.0',
|
||||
tools: {
|
||||
gemini: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'gemini',
|
||||
description: 'Google AI for code analysis'
|
||||
},
|
||||
qwen: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'qwen',
|
||||
description: 'Alibaba AI assistant'
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'codex',
|
||||
description: 'OpenAI code generation'
|
||||
},
|
||||
claude: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'claude',
|
||||
description: 'Anthropic AI assistant'
|
||||
}
|
||||
},
|
||||
customEndpoints: [],
|
||||
defaultTool: 'gemini',
|
||||
settings: {
|
||||
promptFormat: 'plain',
|
||||
smartContext: {
|
||||
enabled: false,
|
||||
maxFiles: 10
|
||||
},
|
||||
nativeResume: true,
|
||||
recursiveQuery: true,
|
||||
cache: {
|
||||
injectionMode: 'auto',
|
||||
defaultPrefix: '',
|
||||
defaultSuffix: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function getProjectConfigPath(projectDir: string): string {
|
||||
return path.join(projectDir, '.claude', 'cli-tools.json');
|
||||
}
|
||||
|
||||
function getGlobalConfigPath(): string {
|
||||
return path.join(os.homedir(), '.claude', 'cli-tools.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve config path with fallback:
|
||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
||||
* 2. Global: ~/.claude/cli-tools.json
|
||||
* Returns { path, source } where source is 'project' | 'global' | 'default'
|
||||
*/
|
||||
function resolveConfigPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
|
||||
const projectPath = getProjectConfigPath(projectDir);
|
||||
if (fs.existsSync(projectPath)) {
|
||||
return { path: projectPath, source: 'project' };
|
||||
}
|
||||
|
||||
const globalPath = getGlobalConfigPath();
|
||||
if (fs.existsSync(globalPath)) {
|
||||
return { path: globalPath, source: 'global' };
|
||||
}
|
||||
|
||||
return { path: projectPath, source: 'default' };
|
||||
}
|
||||
|
||||
function ensureClaudeDir(projectDir: string): void {
|
||||
const claudeDir = path.join(projectDir, '.claude');
|
||||
if (!fs.existsSync(claudeDir)) {
|
||||
fs.mkdirSync(claudeDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Main Functions ==========
|
||||
|
||||
/**
|
||||
* Load CLI tools configuration with fallback:
|
||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
||||
* 2. Global: ~/.claude/cli-tools.json
|
||||
* 3. Default config
|
||||
*/
|
||||
export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & { _source?: string } {
|
||||
const resolved = resolveConfigPath(projectDir);
|
||||
|
||||
try {
|
||||
if (resolved.source === 'default') {
|
||||
// No config file found, return defaults
|
||||
return { ...DEFAULT_CONFIG, _source: 'default' };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(resolved.path, 'utf-8');
|
||||
const parsed = JSON.parse(content) as Partial<ClaudeCliToolsConfig>;
|
||||
|
||||
// Merge with defaults
|
||||
const config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...parsed,
|
||||
tools: { ...DEFAULT_CONFIG.tools, ...(parsed.tools || {}) },
|
||||
settings: {
|
||||
...DEFAULT_CONFIG.settings,
|
||||
...(parsed.settings || {}),
|
||||
smartContext: {
|
||||
...DEFAULT_CONFIG.settings.smartContext,
|
||||
...(parsed.settings?.smartContext || {})
|
||||
},
|
||||
cache: {
|
||||
...DEFAULT_CONFIG.settings.cache,
|
||||
...(parsed.settings?.cache || {})
|
||||
}
|
||||
},
|
||||
_source: resolved.source
|
||||
};
|
||||
|
||||
console.log(`[claude-cli-tools] Loaded config from ${resolved.source}: ${resolved.path}`);
|
||||
return config;
|
||||
} catch (err) {
|
||||
console.error('[claude-cli-tools] Error loading config:', err);
|
||||
return { ...DEFAULT_CONFIG, _source: 'default' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save CLI tools configuration to project .claude/cli-tools.json
|
||||
* Always saves to project directory (not global)
|
||||
*/
|
||||
export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsConfig & { _source?: string }): void {
|
||||
ensureClaudeDir(projectDir);
|
||||
const configPath = getProjectConfigPath(projectDir);
|
||||
|
||||
// Remove internal _source field before saving
|
||||
const { _source, ...configToSave } = config;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
||||
console.log(`[claude-cli-tools] Saved config to project: ${configPath}`);
|
||||
} catch (err) {
|
||||
console.error('[claude-cli-tools] Error saving config:', err);
|
||||
throw new Error(`Failed to save CLI tools config: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update enabled status for a specific tool
|
||||
*/
|
||||
export function updateClaudeToolEnabled(
|
||||
projectDir: string,
|
||||
toolName: string,
|
||||
enabled: boolean
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
|
||||
if (config.tools[toolName]) {
|
||||
config.tools[toolName].enabled = enabled;
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cache settings
|
||||
*/
|
||||
export function updateClaudeCacheSettings(
|
||||
projectDir: string,
|
||||
cacheSettings: Partial<ClaudeCacheSettings>
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
|
||||
config.settings.cache = {
|
||||
...config.settings.cache,
|
||||
...cacheSettings
|
||||
};
|
||||
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update default tool
|
||||
*/
|
||||
export function updateClaudeDefaultTool(
|
||||
projectDir: string,
|
||||
defaultTool: string
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
config.defaultTool = defaultTool;
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom endpoint
|
||||
*/
|
||||
export function addClaudeCustomEndpoint(
|
||||
projectDir: string,
|
||||
endpoint: { id: string; name: string; enabled: boolean }
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
|
||||
// Check if endpoint already exists
|
||||
const existingIndex = config.customEndpoints.findIndex(e => e.id === endpoint.id);
|
||||
if (existingIndex >= 0) {
|
||||
config.customEndpoints[existingIndex] = endpoint;
|
||||
} else {
|
||||
config.customEndpoints.push(endpoint);
|
||||
}
|
||||
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom endpoint
|
||||
*/
|
||||
export function removeClaudeCustomEndpoint(
|
||||
projectDir: string,
|
||||
endpointId: string
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
config.customEndpoints = config.customEndpoints.filter(e => e.id !== endpointId);
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config source info
|
||||
*/
|
||||
export function getClaudeCliToolsInfo(projectDir: string): {
|
||||
projectPath: string;
|
||||
globalPath: string;
|
||||
activePath: string;
|
||||
source: 'project' | 'global' | 'default';
|
||||
} {
|
||||
const resolved = resolveConfigPath(projectDir);
|
||||
return {
|
||||
projectPath: getProjectConfigPath(projectDir),
|
||||
globalPath: getGlobalConfigPath(),
|
||||
activePath: resolved.path,
|
||||
source: resolved.source
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,8 @@ const OperationEnum = z.enum(['list', 'import', 'export', 'summary', 'embed', 's
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
operation: OperationEnum,
|
||||
// Path parameter - highest priority for project resolution
|
||||
path: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
tool: z.enum(['gemini', 'qwen']).optional().default('gemini'),
|
||||
@@ -106,17 +108,21 @@ interface EmbedStatusResult {
|
||||
type OperationResult = ListResult | ImportResult | ExportResult | SummaryResult | EmbedResult | SearchResult | EmbedStatusResult;
|
||||
|
||||
/**
|
||||
* Get project path from current working directory
|
||||
* Get project path - uses explicit path if provided, otherwise falls back to current working directory
|
||||
* Priority: path parameter > getProjectRoot()
|
||||
*/
|
||||
function getProjectPath(): string {
|
||||
function getProjectPath(explicitPath?: string): string {
|
||||
if (explicitPath) {
|
||||
return explicitPath;
|
||||
}
|
||||
return getProjectRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database path for current project
|
||||
* Get database path for project
|
||||
*/
|
||||
function getDatabasePath(): string {
|
||||
const projectPath = getProjectPath();
|
||||
function getDatabasePath(explicitPath?: string): string {
|
||||
const projectPath = getProjectPath(explicitPath);
|
||||
const paths = StoragePaths.project(projectPath);
|
||||
return join(paths.root, 'core-memory', 'core_memory.db');
|
||||
}
|
||||
@@ -129,8 +135,8 @@ const PREVIEW_MAX_LENGTH = 100;
|
||||
* List all memories with compact output
|
||||
*/
|
||||
function executeList(params: Params): ListResult {
|
||||
const { limit } = params;
|
||||
const store = getCoreMemoryStore(getProjectPath());
|
||||
const { limit, path } = params;
|
||||
const store = getCoreMemoryStore(getProjectPath(path));
|
||||
const memories = store.getMemories({ limit }) as CoreMemory[];
|
||||
|
||||
// Convert to compact format with truncated preview
|
||||
@@ -160,13 +166,13 @@ function executeList(params: Params): ListResult {
|
||||
* Import text as a new memory
|
||||
*/
|
||||
function executeImport(params: Params): ImportResult {
|
||||
const { text } = params;
|
||||
const { text, path } = params;
|
||||
|
||||
if (!text || text.trim() === '') {
|
||||
throw new Error('Parameter "text" is required for import operation');
|
||||
}
|
||||
|
||||
const store = getCoreMemoryStore(getProjectPath());
|
||||
const store = getCoreMemoryStore(getProjectPath(path));
|
||||
const memory = store.upsertMemory({
|
||||
content: text.trim(),
|
||||
});
|
||||
@@ -184,14 +190,14 @@ function executeImport(params: Params): ImportResult {
|
||||
* Searches current project first, then all projects if not found
|
||||
*/
|
||||
function executeExport(params: Params): ExportResult {
|
||||
const { id } = params;
|
||||
const { id, path } = params;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('Parameter "id" is required for export operation');
|
||||
}
|
||||
|
||||
// Try current project first
|
||||
const store = getCoreMemoryStore(getProjectPath());
|
||||
// Try current project first (or explicit path if provided)
|
||||
const store = getCoreMemoryStore(getProjectPath(path));
|
||||
let memory = store.getMemory(id);
|
||||
|
||||
// If not found, search across all projects
|
||||
@@ -218,13 +224,13 @@ function executeExport(params: Params): ExportResult {
|
||||
* Generate AI summary for a memory
|
||||
*/
|
||||
async function executeSummary(params: Params): Promise<SummaryResult> {
|
||||
const { id, tool = 'gemini' } = params;
|
||||
const { id, tool = 'gemini', path } = params;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('Parameter "id" is required for summary operation');
|
||||
}
|
||||
|
||||
const store = getCoreMemoryStore(getProjectPath());
|
||||
const store = getCoreMemoryStore(getProjectPath(path));
|
||||
const memory = store.getMemory(id);
|
||||
|
||||
if (!memory) {
|
||||
@@ -245,8 +251,8 @@ async function executeSummary(params: Params): Promise<SummaryResult> {
|
||||
* Generate embeddings for memory chunks
|
||||
*/
|
||||
async function executeEmbed(params: Params): Promise<EmbedResult> {
|
||||
const { source_id, batch_size = 8, force = false } = params;
|
||||
const dbPath = getDatabasePath();
|
||||
const { source_id, batch_size = 8, force = false, path } = params;
|
||||
const dbPath = getDatabasePath(path);
|
||||
|
||||
const result = await MemoryEmbedder.generateEmbeddings(dbPath, {
|
||||
sourceId: source_id,
|
||||
@@ -272,13 +278,13 @@ async function executeEmbed(params: Params): Promise<EmbedResult> {
|
||||
* Search memory chunks using semantic search
|
||||
*/
|
||||
async function executeSearch(params: Params): Promise<SearchResult> {
|
||||
const { query, top_k = 10, min_score = 0.3, source_type } = params;
|
||||
const { query, top_k = 10, min_score = 0.3, source_type, path } = params;
|
||||
|
||||
if (!query) {
|
||||
throw new Error('Parameter "query" is required for search operation');
|
||||
}
|
||||
|
||||
const dbPath = getDatabasePath();
|
||||
const dbPath = getDatabasePath(path);
|
||||
|
||||
const result = await MemoryEmbedder.searchMemories(dbPath, query, {
|
||||
topK: top_k,
|
||||
@@ -309,7 +315,8 @@ async function executeSearch(params: Params): Promise<SearchResult> {
|
||||
* Get embedding status statistics
|
||||
*/
|
||||
async function executeEmbedStatus(params: Params): Promise<EmbedStatusResult> {
|
||||
const dbPath = getDatabasePath();
|
||||
const { path } = params;
|
||||
const dbPath = getDatabasePath(path);
|
||||
|
||||
const result = await MemoryEmbedder.getEmbeddingStatus(dbPath);
|
||||
|
||||
@@ -368,6 +375,9 @@ Usage:
|
||||
core_memory(operation="search", query="authentication") # Search memories semantically
|
||||
core_memory(operation="embed_status") # Check embedding status
|
||||
|
||||
Path parameter (highest priority):
|
||||
core_memory(operation="list", path="/path/to/project") # Use specific project path
|
||||
|
||||
Memory IDs use format: CMEM-YYYYMMDD-HHMMSS`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
@@ -377,6 +387,10 @@ Memory IDs use format: CMEM-YYYYMMDD-HHMMSS`,
|
||||
enum: ['list', 'import', 'export', 'summary', 'embed', 'search', 'embed_status'],
|
||||
description: 'Operation to perform',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Project path (highest priority - overrides auto-detected project root)',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Text content to import (required for import operation)',
|
||||
|
||||
Reference in New Issue
Block a user