mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
fix(security): prevent command injection and strengthen input validation
BREAKING: executeCodexLens now uses shell:false to prevent RCE Security fixes: - Remove shell:true from spawn() to prevent command injection (CRITICAL) - Add .env value escaping to prevent injection when file is sourced - Strengthen path validation with startsWith to block subdirectories - Add path traversal detection (../) - Improve JSON extraction to handle trailing CLI output Features: - Refactor CodexLens panel to tabbed layout (Overview/Settings/Search/Advanced) - Add environment variables editor for ~/.codexlens/.env - Add API concurrency settings (max_workers, batch_size) - Add escapeHtml() helper to prevent XSS - Implement merge mode for env saving to preserve custom variables
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* CodexLens Routes Module
|
||||
* Handles all CodexLens-related API endpoints
|
||||
*
|
||||
* TODO: Remove @ts-nocheck and add proper types:
|
||||
* - Define interfaces for request body types (ConfigBody, CleanBody, InitBody, etc.)
|
||||
* - Type error catches: (e: unknown) => { const err = e as Error; ... }
|
||||
* - Add null checks for extractJSON results
|
||||
* - Type the handlePostRequest callback body parameter
|
||||
*/
|
||||
// @ts-nocheck
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import {
|
||||
checkVenvStatus,
|
||||
@@ -65,6 +71,7 @@ function formatSize(bytes: number): string {
|
||||
* Extract JSON from CLI output that may contain logging messages
|
||||
* CodexLens CLI outputs logs like "INFO ..." before the JSON
|
||||
* Also strips ANSI color codes that Rich library adds
|
||||
* Handles trailing content after JSON (e.g., "INFO: Done" messages)
|
||||
*/
|
||||
function extractJSON(output: string): any {
|
||||
// Strip ANSI color codes first
|
||||
@@ -76,8 +83,53 @@ function extractJSON(output: string): any {
|
||||
throw new Error('No JSON found in output');
|
||||
}
|
||||
|
||||
// Extract everything from the first { or [ onwards
|
||||
const jsonString = cleanOutput.substring(jsonStart);
|
||||
const startChar = cleanOutput[jsonStart];
|
||||
const endChar = startChar === '{' ? '}' : ']';
|
||||
|
||||
// Find matching closing brace/bracket using a simple counter
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escapeNext = false;
|
||||
let jsonEnd = -1;
|
||||
|
||||
for (let i = jsonStart; i < cleanOutput.length; i++) {
|
||||
const char = cleanOutput[i];
|
||||
|
||||
if (escapeNext) {
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\' && inString) {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === startChar) {
|
||||
depth++;
|
||||
} else if (char === endChar) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
jsonEnd = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonEnd === -1) {
|
||||
// Fallback: try to parse from start to end (original behavior)
|
||||
const jsonString = cleanOutput.substring(jsonStart);
|
||||
return JSON.parse(jsonString);
|
||||
}
|
||||
|
||||
const jsonString = cleanOutput.substring(jsonStart, jsonEnd);
|
||||
return JSON.parse(jsonString);
|
||||
}
|
||||
|
||||
@@ -378,7 +430,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// Check if CodexLens is installed first (without auto-installing)
|
||||
const venvStatus = await checkVenvStatus();
|
||||
|
||||
let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 };
|
||||
let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0, api_max_workers: 4, api_batch_size: 8 };
|
||||
|
||||
// If not installed, return default config without executing CodexLens
|
||||
if (!venvStatus.ready) {
|
||||
@@ -400,6 +452,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
if (config.success && config.result) {
|
||||
// CLI returns index_dir (not index_root)
|
||||
responseData.index_dir = config.result.index_dir || config.result.index_root || responseData.index_dir;
|
||||
// Extract API settings
|
||||
if (config.result.api_max_workers !== undefined) {
|
||||
responseData.api_max_workers = config.result.api_max_workers;
|
||||
}
|
||||
if (config.result.api_batch_size !== undefined) {
|
||||
responseData.api_batch_size = config.result.api_batch_size;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[CodexLens] Failed to parse config:', e.message);
|
||||
@@ -432,19 +491,69 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
||||
// API: CodexLens Config - POST (Set configuration)
|
||||
if (pathname === '/api/codexlens/config' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { index_dir } = body;
|
||||
const { index_dir, api_max_workers, api_batch_size } = body;
|
||||
|
||||
if (!index_dir) {
|
||||
return { success: false, error: 'index_dir is required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await executeCodexLens(['config', 'set', 'index_dir', index_dir, '--json']);
|
||||
if (result.success) {
|
||||
return { success: true, message: 'Configuration updated successfully' };
|
||||
} else {
|
||||
return { success: false, error: result.error || 'Failed to update configuration', status: 500 };
|
||||
// Validate index_dir path
|
||||
const indexDirStr = String(index_dir).trim();
|
||||
|
||||
// Check for dangerous patterns
|
||||
if (indexDirStr.includes('\0')) {
|
||||
return { success: false, error: 'Invalid path: contains null bytes', status: 400 };
|
||||
}
|
||||
|
||||
// Prevent system root paths and their subdirectories (Windows and Unix)
|
||||
const dangerousPaths = ['/', 'C:\\', 'C:/', '/etc', '/usr', '/bin', '/sys', '/proc', '/var',
|
||||
'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\System32'];
|
||||
const normalizedPath = indexDirStr.replace(/\\/g, '/').toLowerCase();
|
||||
for (const dangerous of dangerousPaths) {
|
||||
const dangerousLower = dangerous.replace(/\\/g, '/').toLowerCase();
|
||||
// Block exact match OR any subdirectory (using startsWith)
|
||||
if (normalizedPath === dangerousLower ||
|
||||
normalizedPath === dangerousLower + '/' ||
|
||||
normalizedPath.startsWith(dangerousLower + '/')) {
|
||||
return { success: false, error: 'Invalid path: cannot use system directories or their subdirectories', status: 400 };
|
||||
}
|
||||
}
|
||||
|
||||
// Additional check: prevent path traversal attempts
|
||||
if (normalizedPath.includes('../') || normalizedPath.includes('/..')) {
|
||||
return { success: false, error: 'Invalid path: path traversal not allowed', status: 400 };
|
||||
}
|
||||
|
||||
// Validate api settings
|
||||
if (api_max_workers !== undefined) {
|
||||
const workers = Number(api_max_workers);
|
||||
if (isNaN(workers) || workers < 1 || workers > 32) {
|
||||
return { success: false, error: 'api_max_workers must be between 1 and 32', status: 400 };
|
||||
}
|
||||
}
|
||||
if (api_batch_size !== undefined) {
|
||||
const batch = Number(api_batch_size);
|
||||
if (isNaN(batch) || batch < 1 || batch > 64) {
|
||||
return { success: false, error: 'api_batch_size must be between 1 and 64', status: 400 };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Set index_dir
|
||||
const result = await executeCodexLens(['config', 'set', 'index_dir', indexDirStr, '--json']);
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to update index_dir', status: 500 };
|
||||
}
|
||||
|
||||
// Set API settings if provided
|
||||
if (api_max_workers !== undefined) {
|
||||
await executeCodexLens(['config', 'set', 'api_max_workers', String(api_max_workers), '--json']);
|
||||
}
|
||||
if (api_batch_size !== undefined) {
|
||||
await executeCodexLens(['config', 'set', 'api_batch_size', String(api_batch_size), '--json']);
|
||||
}
|
||||
|
||||
return { success: true, message: 'Configuration updated successfully' };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
}
|
||||
@@ -1535,5 +1644,235 @@ except Exception as e:
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENV FILE MANAGEMENT ENDPOINTS
|
||||
// ============================================================
|
||||
|
||||
// API: Get global env file content
|
||||
if (pathname === '/api/codexlens/env' && req.method === 'GET') {
|
||||
try {
|
||||
const { homedir } = await import('os');
|
||||
const { join } = await import('path');
|
||||
const { readFile } = await import('fs/promises');
|
||||
|
||||
const envPath = join(homedir(), '.codexlens', '.env');
|
||||
let content = '';
|
||||
try {
|
||||
content = await readFile(envPath, 'utf-8');
|
||||
} catch (e) {
|
||||
// File doesn't exist, return empty
|
||||
}
|
||||
|
||||
// Parse env file into key-value pairs (robust parsing)
|
||||
const envVars: Record<string, string> = {};
|
||||
const lines = content.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Skip empty lines and comments
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
|
||||
// Find first = that's part of key=value (not in a quote)
|
||||
const eqIndex = trimmed.indexOf('=');
|
||||
if (eqIndex <= 0) continue;
|
||||
|
||||
const key = trimmed.substring(0, eqIndex).trim();
|
||||
// Validate key format (alphanumeric + underscore)
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
||||
|
||||
let value = trimmed.substring(eqIndex + 1);
|
||||
|
||||
// Handle quoted values (preserves = inside quotes)
|
||||
if (value.startsWith('"')) {
|
||||
// Find matching closing quote (handle escaped quotes)
|
||||
let end = 1;
|
||||
while (end < value.length) {
|
||||
if (value[end] === '"' && value[end - 1] !== '\\') break;
|
||||
end++;
|
||||
}
|
||||
value = value.substring(1, end).replace(/\\"/g, '"');
|
||||
} else if (value.startsWith("'")) {
|
||||
// Single quotes don't support escaping
|
||||
const end = value.indexOf("'", 1);
|
||||
value = end > 0 ? value.substring(1, end) : value.substring(1);
|
||||
} else {
|
||||
// Unquoted: trim and take until comment or end
|
||||
const commentIndex = value.indexOf(' #');
|
||||
if (commentIndex > 0) {
|
||||
value = value.substring(0, commentIndex);
|
||||
}
|
||||
value = value.trim();
|
||||
}
|
||||
|
||||
envVars[key] = value;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
path: envPath,
|
||||
env: envVars,
|
||||
raw: content
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, error: err.message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Save global env file content (merge mode - preserves existing values)
|
||||
if (pathname === '/api/codexlens/env' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { env } = body as { env: Record<string, string> };
|
||||
|
||||
if (!env || typeof env !== 'object') {
|
||||
return { success: false, error: 'env object is required', status: 400 };
|
||||
}
|
||||
|
||||
try {
|
||||
const { homedir } = await import('os');
|
||||
const { join, dirname } = await import('path');
|
||||
const { writeFile, mkdir, readFile } = await import('fs/promises');
|
||||
|
||||
const envPath = join(homedir(), '.codexlens', '.env');
|
||||
await mkdir(dirname(envPath), { recursive: true });
|
||||
|
||||
// Read existing env file to preserve custom variables
|
||||
let existingEnv: Record<string, string> = {};
|
||||
let existingComments: string[] = [];
|
||||
try {
|
||||
const content = await readFile(envPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Preserve comment lines that aren't our headers
|
||||
if (trimmed.startsWith('#') && !trimmed.includes('Managed by CCW')) {
|
||||
if (!trimmed.includes('Reranker API') && !trimmed.includes('Embedding API') &&
|
||||
!trimmed.includes('LiteLLM Config') && !trimmed.includes('CodexLens Settings') &&
|
||||
!trimmed.includes('Other Settings') && !trimmed.includes('CodexLens Environment')) {
|
||||
existingComments.push(line);
|
||||
}
|
||||
}
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
|
||||
// Robust parsing (same as GET handler)
|
||||
const eqIndex = trimmed.indexOf('=');
|
||||
if (eqIndex <= 0) continue;
|
||||
|
||||
const key = trimmed.substring(0, eqIndex).trim();
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
||||
|
||||
let value = trimmed.substring(eqIndex + 1);
|
||||
if (value.startsWith('"')) {
|
||||
let end = 1;
|
||||
while (end < value.length) {
|
||||
if (value[end] === '"' && value[end - 1] !== '\\') break;
|
||||
end++;
|
||||
}
|
||||
value = value.substring(1, end).replace(/\\"/g, '"');
|
||||
} else if (value.startsWith("'")) {
|
||||
const end = value.indexOf("'", 1);
|
||||
value = end > 0 ? value.substring(1, end) : value.substring(1);
|
||||
} else {
|
||||
const commentIndex = value.indexOf(' #');
|
||||
if (commentIndex > 0) value = value.substring(0, commentIndex);
|
||||
value = value.trim();
|
||||
}
|
||||
existingEnv[key] = value;
|
||||
}
|
||||
} catch (e) {
|
||||
// File doesn't exist, start fresh
|
||||
}
|
||||
|
||||
// Merge: update known keys from payload, preserve unknown keys
|
||||
const knownKeys = new Set([
|
||||
'RERANKER_API_KEY', 'RERANKER_API_BASE', 'RERANKER_MODEL',
|
||||
'EMBEDDING_API_KEY', 'EMBEDDING_API_BASE', 'EMBEDDING_MODEL',
|
||||
'LITELLM_API_KEY', 'LITELLM_API_BASE', 'LITELLM_MODEL'
|
||||
]);
|
||||
|
||||
// Apply updates from payload
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (value) {
|
||||
existingEnv[key] = value;
|
||||
} else if (knownKeys.has(key)) {
|
||||
// Remove known key if value is empty
|
||||
delete existingEnv[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Build env file content
|
||||
const lines = [
|
||||
'# CodexLens Environment Configuration',
|
||||
'# Managed by CCW Dashboard',
|
||||
''
|
||||
];
|
||||
|
||||
// Add preserved custom comments
|
||||
if (existingComments.length > 0) {
|
||||
lines.push(...existingComments, '');
|
||||
}
|
||||
|
||||
// Group by prefix
|
||||
const groups: Record<string, string[]> = {
|
||||
'RERANKER': [],
|
||||
'EMBEDDING': [],
|
||||
'LITELLM': [],
|
||||
'CODEXLENS': [],
|
||||
'OTHER': []
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(existingEnv)) {
|
||||
if (!value) continue;
|
||||
// SECURITY: Escape special characters to prevent .env injection
|
||||
const escapedValue = value
|
||||
.replace(/\\/g, '\\\\') // Escape backslashes first
|
||||
.replace(/"/g, '\\"') // Escape double quotes
|
||||
.replace(/\n/g, '\\n') // Escape newlines
|
||||
.replace(/\r/g, '\\r'); // Escape carriage returns
|
||||
const line = `${key}="${escapedValue}"`;
|
||||
if (key.startsWith('RERANKER_')) groups['RERANKER'].push(line);
|
||||
else if (key.startsWith('EMBEDDING_')) groups['EMBEDDING'].push(line);
|
||||
else if (key.startsWith('LITELLM_')) groups['LITELLM'].push(line);
|
||||
else if (key.startsWith('CODEXLENS_')) groups['CODEXLENS'].push(line);
|
||||
else groups['OTHER'].push(line);
|
||||
}
|
||||
|
||||
// Add grouped content
|
||||
if (groups['RERANKER'].length) {
|
||||
lines.push('# Reranker API Configuration');
|
||||
lines.push(...groups['RERANKER'], '');
|
||||
}
|
||||
if (groups['EMBEDDING'].length) {
|
||||
lines.push('# Embedding API Configuration');
|
||||
lines.push(...groups['EMBEDDING'], '');
|
||||
}
|
||||
if (groups['LITELLM'].length) {
|
||||
lines.push('# LiteLLM Configuration');
|
||||
lines.push(...groups['LITELLM'], '');
|
||||
}
|
||||
if (groups['CODEXLENS'].length) {
|
||||
lines.push('# CodexLens Settings');
|
||||
lines.push(...groups['CODEXLENS'], '');
|
||||
}
|
||||
if (groups['OTHER'].length) {
|
||||
lines.push('# Other Settings');
|
||||
lines.push(...groups['OTHER'], '');
|
||||
}
|
||||
|
||||
await writeFile(envPath, lines.join('\n'), 'utf-8');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Environment configuration saved',
|
||||
path: envPath
|
||||
};
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies
|
||||
// Extracted from cli-manager.js for better maintainability
|
||||
|
||||
// ============================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Escape HTML special characters to prevent XSS
|
||||
*/
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CODEXLENS CONFIGURATION MODAL
|
||||
// ============================================================
|
||||
@@ -35,15 +52,18 @@ async function showCodexLensConfigModal() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CodexLens configuration modal content
|
||||
* Build CodexLens configuration modal content - Tabbed Layout
|
||||
*/
|
||||
function buildCodexLensConfigContent(config) {
|
||||
const indexDir = config.index_dir || '~/.codexlens/indexes';
|
||||
const indexCount = config.index_count || 0;
|
||||
const isInstalled = window.cliToolsStatus?.codexlens?.installed || false;
|
||||
const embeddingCoverage = config.embedding_coverage || 0;
|
||||
const apiMaxWorkers = config.api_max_workers || 4;
|
||||
const apiBatchSize = config.api_batch_size || 8;
|
||||
|
||||
return '<div class="modal-backdrop" id="codexlensConfigModal">' +
|
||||
'<div class="modal-container">' +
|
||||
'<div class="modal-container large">' +
|
||||
'<div class="modal-header">' +
|
||||
'<div class="flex items-center gap-3">' +
|
||||
'<div class="modal-icon">' +
|
||||
@@ -59,159 +79,245 @@ function buildCodexLensConfigContent(config) {
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="modal-body">' +
|
||||
// Status Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.status') + '</h4>' +
|
||||
'<div class="flex items-center gap-4 text-sm">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<span class="text-muted-foreground">' + t('codexlens.currentWorkspace') + ':</span>' +
|
||||
'<div class="modal-body" style="padding: 0;">' +
|
||||
// Tab Navigation
|
||||
'<div class="flex border-b border-border bg-muted/30">' +
|
||||
'<button class="codexlens-tab active flex-1 px-4 py-2.5 text-sm font-medium text-center border-b-2 border-primary text-primary" data-tab="overview">' +
|
||||
'<i data-lucide="layout-dashboard" class="w-4 h-4 inline mr-1.5"></i>Overview' +
|
||||
'</button>' +
|
||||
'<button class="codexlens-tab flex-1 px-4 py-2.5 text-sm font-medium text-center border-b-2 border-transparent text-muted-foreground hover:text-foreground" data-tab="settings">' +
|
||||
'<i data-lucide="settings" class="w-4 h-4 inline mr-1.5"></i>Settings' +
|
||||
'</button>' +
|
||||
(isInstalled
|
||||
? '<button class="codexlens-tab flex-1 px-4 py-2.5 text-sm font-medium text-center border-b-2 border-transparent text-muted-foreground hover:text-foreground" data-tab="search">' +
|
||||
'<i data-lucide="search" class="w-4 h-4 inline mr-1.5"></i>Search' +
|
||||
'</button>' +
|
||||
'<button class="codexlens-tab flex-1 px-4 py-2.5 text-sm font-medium text-center border-b-2 border-transparent text-muted-foreground hover:text-foreground" data-tab="advanced">' +
|
||||
'<i data-lucide="wrench" class="w-4 h-4 inline mr-1.5"></i>Advanced' +
|
||||
'</button>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
|
||||
// Tab Content Container
|
||||
'<div class="p-4">' +
|
||||
|
||||
// ========== OVERVIEW TAB ==========
|
||||
'<div class="codexlens-tab-content active" data-tab="overview">' +
|
||||
// Status Card - Compact grid layout
|
||||
'<div class="grid grid-cols-2 gap-3 mb-4">' +
|
||||
// Status Card
|
||||
'<div class="rounded-lg border border-border p-3 bg-card">' +
|
||||
'<div class="flex items-center gap-2 mb-2">' +
|
||||
'<i data-lucide="circle-check" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||
'<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Status</span>' +
|
||||
'</div>' +
|
||||
(isInstalled
|
||||
? '<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success border border-success/20">' +
|
||||
'<i data-lucide="check-circle" class="w-3.5 h-3.5"></i>' +
|
||||
t('codexlens.installed') +
|
||||
'</span>'
|
||||
: '<span class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground border border-border">' +
|
||||
'<i data-lucide="circle" class="w-3.5 h-3.5"></i>' +
|
||||
t('codexlens.notInstalled') +
|
||||
'</span>') +
|
||||
? '<div class="flex items-center gap-2">' +
|
||||
'<span class="w-2 h-2 rounded-full bg-success animate-pulse"></span>' +
|
||||
'<span class="text-sm font-medium text-success">Installed</span>' +
|
||||
'</div>'
|
||||
: '<div class="flex items-center gap-2">' +
|
||||
'<span class="w-2 h-2 rounded-full bg-muted-foreground"></span>' +
|
||||
'<span class="text-sm font-medium text-muted-foreground">Not Installed</span>' +
|
||||
'</div>') +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<span class="text-muted-foreground">' + t('codexlens.indexes') + ':</span>' +
|
||||
'<span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-primary/10 text-primary border border-primary/20">' +
|
||||
indexCount +
|
||||
'</span>' +
|
||||
// Index Count Card
|
||||
'<div class="rounded-lg border border-border p-3 bg-card">' +
|
||||
'<div class="flex items-center gap-2 mb-2">' +
|
||||
'<i data-lucide="database" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||
'<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Indexes</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-2xl font-bold text-primary">' + indexCount + '</div>' +
|
||||
'</div>' +
|
||||
// Embeddings Coverage Card
|
||||
'<div class="rounded-lg border border-border p-3 bg-card">' +
|
||||
'<div class="flex items-center gap-2 mb-2">' +
|
||||
'<i data-lucide="brain" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||
'<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Embeddings</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-sm font-medium">' + embeddingCoverage + '%</div>' +
|
||||
'</div>' +
|
||||
// Storage Path Card
|
||||
'<div class="rounded-lg border border-border p-3 bg-card">' +
|
||||
'<div class="flex items-center gap-2 mb-2">' +
|
||||
'<i data-lucide="folder" class="w-4 h-4 text-muted-foreground"></i>' +
|
||||
'<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide">Storage</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs font-mono text-muted-foreground truncate" title="' + escapeHtml(indexDir) + '">' + escapeHtml(indexDir) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Quick Actions
|
||||
'<div class="space-y-2">' +
|
||||
'<h4 class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">Quick Actions</h4>' +
|
||||
'<div class="grid grid-cols-2 gap-2">' +
|
||||
(isInstalled
|
||||
? '<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-primary/30 bg-primary/5 text-primary hover:bg-primary/10 transition-colors" onclick="initCodexLensIndex()">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> Update Index' +
|
||||
'</button>' +
|
||||
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-border bg-background hover:bg-muted/50 transition-colors" onclick="showWatcherControlModal()">' +
|
||||
'<i data-lucide="eye" class="w-4 h-4"></i> File Watcher' +
|
||||
'</button>' +
|
||||
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-border bg-background hover:bg-muted/50 transition-colors" onclick="showRerankerConfigModal()">' +
|
||||
'<i data-lucide="layers" class="w-4 h-4"></i> Reranker' +
|
||||
'</button>' +
|
||||
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-border bg-background hover:bg-muted/50 transition-colors" onclick="cleanCurrentWorkspaceIndex()">' +
|
||||
'<i data-lucide="eraser" class="w-4 h-4"></i> Clean Workspace' +
|
||||
'</button>'
|
||||
: '<button class="col-span-2 flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" onclick="installCodexLensFromManager()">' +
|
||||
'<i data-lucide="download" class="w-4 h-4"></i> Install CodexLens' +
|
||||
'</button>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Index Storage Path Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.indexStoragePath') + '</h4>' +
|
||||
'<div class="space-y-3">' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.currentPath') + '</label>' +
|
||||
'<div class="text-sm text-muted-foreground bg-muted/30 rounded-lg px-3 py-2 font-mono">' +
|
||||
indexDir +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.newStoragePath') + '</label>' +
|
||||
'<input type="text" id="indexDirInput" value="' + indexDir + '" ' +
|
||||
// ========== SETTINGS TAB ==========
|
||||
'<div class="codexlens-tab-content hidden" data-tab="settings">' +
|
||||
// Index Storage Path
|
||||
'<div class="space-y-4">' +
|
||||
'<div class="space-y-2">' +
|
||||
'<label class="block text-sm font-medium">' + t('codexlens.indexStoragePath') + '</label>' +
|
||||
'<input type="text" id="indexDirInput" value="' + escapeHtml(indexDir) + '" ' +
|
||||
'placeholder="' + t('codexlens.pathPlaceholder') + '" ' +
|
||||
'class="tool-config-input w-full" />' +
|
||||
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.pathInfo') + '</p>' +
|
||||
'<p class="text-xs text-muted-foreground">' + t('codexlens.pathInfo') + '</p>' +
|
||||
'</div>' +
|
||||
|
||||
// API Settings (Concurrency)
|
||||
'<div class="rounded-lg border border-border p-4 space-y-3">' +
|
||||
'<h4 class="text-sm font-medium flex items-center gap-2">' +
|
||||
'<i data-lucide="zap" class="w-4 h-4"></i> API Settings' +
|
||||
'</h4>' +
|
||||
'<div class="grid grid-cols-2 gap-3">' +
|
||||
'<div>' +
|
||||
'<label class="block text-xs font-medium text-muted-foreground mb-1">Max Workers</label>' +
|
||||
'<input type="number" id="apiMaxWorkersInput" value="' + apiMaxWorkers + '" min="1" max="16" ' +
|
||||
'class="tool-config-input w-full" />' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-xs font-medium text-muted-foreground mb-1">Batch Size</label>' +
|
||||
'<input type="number" id="apiBatchSizeInput" value="' + apiBatchSize + '" min="1" max="32" ' +
|
||||
'class="tool-config-input w-full" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<p class="text-xs text-muted-foreground">Higher values speed up embedding generation but may hit rate limits.</p>' +
|
||||
'</div>' +
|
||||
|
||||
// Environment Variables Section
|
||||
'<div class="rounded-lg border border-border p-4 space-y-3">' +
|
||||
'<div class="flex items-center justify-between">' +
|
||||
'<h4 class="text-sm font-medium flex items-center gap-2">' +
|
||||
'<i data-lucide="file-code" class="w-4 h-4"></i> Environment Variables' +
|
||||
'</h4>' +
|
||||
'<button class="text-xs text-primary hover:underline" onclick="loadEnvVariables()">Load</button>' +
|
||||
'</div>' +
|
||||
'<div id="envVarsContainer" class="space-y-2">' +
|
||||
'<div class="text-xs text-muted-foreground">Click Load to view/edit ~/.codexlens/.env</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Migration Warning
|
||||
'<div class="flex items-start gap-2 bg-warning/10 border border-warning/30 rounded-lg p-3">' +
|
||||
'<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>' +
|
||||
'<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5 flex-shrink-0"></i>' +
|
||||
'<div class="text-sm">' +
|
||||
'<p class="font-medium text-warning">' + t('codexlens.migrationRequired') + '</p>' +
|
||||
'<p class="text-muted-foreground mt-1">' + t('codexlens.migrationWarning') + '</p>' +
|
||||
'<p class="text-muted-foreground mt-1 text-xs">' + t('codexlens.migrationWarning') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Actions Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.actions') + '</h4>' +
|
||||
'<div class="tool-config-actions">' +
|
||||
(isInstalled
|
||||
? '<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-primary/30 bg-primary/5 text-primary hover:bg-primary/10 transition-colors" onclick="initCodexLensIndex()">' +
|
||||
'<i data-lucide="database" class="w-3.5 h-3.5"></i> ' + t('codexlens.initializeIndex') +
|
||||
'</button>' +
|
||||
'<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-primary/30 bg-primary/5 text-primary hover:bg-primary/10 transition-colors" onclick="showRerankerConfigModal()">' +
|
||||
'<i data-lucide="layers" class="w-3.5 h-3.5"></i> ' + (t('codexlens.rerankerConfig') || 'Reranker Config') +
|
||||
'</button>' +
|
||||
'<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-primary/30 bg-primary/5 text-primary hover:bg-primary/10 transition-colors" onclick="showWatcherControlModal()">' +
|
||||
'<i data-lucide="eye" class="w-3.5 h-3.5"></i> ' + (t('codexlens.watcherControl') || 'File Watcher') +
|
||||
'</button>' +
|
||||
'<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-border bg-background hover:bg-muted/50 transition-colors" onclick="cleanCurrentWorkspaceIndex()">' +
|
||||
'<i data-lucide="folder-x" class="w-3.5 h-3.5"></i> ' + t('codexlens.cleanCurrentWorkspace') +
|
||||
'</button>' +
|
||||
'<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-border bg-background hover:bg-muted/50 transition-colors" onclick="cleanCodexLensIndexes()">' +
|
||||
'<i data-lucide="trash" class="w-3.5 h-3.5"></i> ' + t('codexlens.cleanAllIndexes') +
|
||||
'</button>' +
|
||||
'<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-destructive/30 bg-destructive/5 text-destructive hover:bg-destructive/10 transition-colors" onclick="uninstallCodexLensFromManager()">' +
|
||||
'<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> ' + t('cli.uninstall') +
|
||||
'</button>'
|
||||
: '<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" onclick="installCodexLensFromManager()">' +
|
||||
'<i data-lucide="download" class="w-3.5 h-3.5"></i> ' + t('codexlens.installCodexLens') +
|
||||
'</button>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Semantic Dependencies Section
|
||||
// ========== SEARCH TAB (only if installed) ==========
|
||||
(isInstalled
|
||||
? '<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.semanticDeps') + '</h4>' +
|
||||
'<div id="semanticDepsStatus" class="space-y-2">' +
|
||||
'<div class="text-sm text-muted-foreground">' + t('codexlens.checkingDeps') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
|
||||
// SPLADE Section
|
||||
(isInstalled
|
||||
? '<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.spladeDeps') + '</h4>' +
|
||||
'<div id="spladeStatus" class="space-y-2">' +
|
||||
'<div class="text-sm text-muted-foreground">' + t('common.loading') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
|
||||
|
||||
// Model Management Section
|
||||
(isInstalled
|
||||
? '<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.modelManagement') + '</h4>' +
|
||||
'<div id="modelListContainer" class="space-y-2">' +
|
||||
'<div class="text-sm text-muted-foreground">' + t('codexlens.loadingModels') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
|
||||
// Test Search Section
|
||||
(isInstalled
|
||||
? '<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.testSearch') + ' <span class="text-muted">(' + t('codexlens.testFunctionality') + ')</span></h4>' +
|
||||
'<div class="space-y-3">' +
|
||||
'<div class="flex gap-2">' +
|
||||
'<select id="searchTypeSelect" class="tool-config-select flex-1">' +
|
||||
'<option value="search">' + t('codexlens.textSearch') + '</option>' +
|
||||
'<option value="search_files">' + t('codexlens.fileSearch') + '</option>' +
|
||||
'<option value="symbol">' + t('codexlens.symbolSearch') + '</option>' +
|
||||
'</select>' +
|
||||
'<select id="searchModeSelect" class="tool-config-select flex-1">' +
|
||||
'<option value="exact">' + t('codexlens.exactMode') + '</option>' +
|
||||
'<option value="fuzzy">' + t('codexlens.fuzzyMode') + '</option>' +
|
||||
'<option value="hybrid">' + t('codexlens.hybridMode') + '</option>' +
|
||||
'<option value="vector">' + t('codexlens.vectorMode') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<input type="text" id="searchQueryInput" class="tool-config-input w-full" ' +
|
||||
'placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<button class="btn-sm btn-primary w-full" id="runSearchBtn">' +
|
||||
'<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="searchResults" class="hidden">' +
|
||||
? '<div class="codexlens-tab-content hidden" data-tab="search">' +
|
||||
'<div class="space-y-4">' +
|
||||
// Search Options Row
|
||||
'<div class="grid grid-cols-2 gap-3">' +
|
||||
'<div>' +
|
||||
'<div class="flex items-center justify-between">' +
|
||||
'<p class="text-sm font-medium">' + t('codexlens.results') + ':</p>' +
|
||||
'<span id="searchResultCount" class="text-xs text-muted-foreground"></span>' +
|
||||
'</div>' +
|
||||
'<pre id="searchResultContent"></pre>' +
|
||||
'<label class="block text-xs font-medium text-muted-foreground mb-1">Search Type</label>' +
|
||||
'<select id="searchTypeSelect" class="tool-config-select w-full">' +
|
||||
'<option value="search">Content Search</option>' +
|
||||
'<option value="search_files">File Search</option>' +
|
||||
'<option value="symbol">Symbol Search</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="block text-xs font-medium text-muted-foreground mb-1">Mode</label>' +
|
||||
'<select id="searchModeSelect" class="tool-config-select w-full">' +
|
||||
'<option value="dense_rerank">Semantic (default)</option>' +
|
||||
'<option value="fts">Exact (FTS)</option>' +
|
||||
'<option value="fuzzy">Fuzzy</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
// Query Input
|
||||
'<div>' +
|
||||
'<input type="text" id="searchQueryInput" class="tool-config-input w-full text-base py-2.5" ' +
|
||||
'placeholder="Enter search query..." />' +
|
||||
'</div>' +
|
||||
// Search Button
|
||||
'<button class="btn btn-primary w-full py-2.5" id="runSearchBtn">' +
|
||||
'<i data-lucide="search" class="w-4 h-4 mr-2"></i> Search' +
|
||||
'</button>' +
|
||||
// Results
|
||||
'<div id="searchResults" class="hidden">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<span class="text-sm font-medium">Results</span>' +
|
||||
'<span id="searchResultCount" class="text-xs text-muted-foreground"></span>' +
|
||||
'</div>' +
|
||||
'<pre id="searchResultContent" class="text-xs bg-muted/50 rounded-lg p-3 overflow-auto max-h-64"></pre>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
|
||||
// ========== ADVANCED TAB (only if installed) ==========
|
||||
(isInstalled
|
||||
? '<div class="codexlens-tab-content hidden" data-tab="advanced">' +
|
||||
'<div class="space-y-4">' +
|
||||
// Dependencies Section
|
||||
'<div class="rounded-lg border border-border p-4">' +
|
||||
'<h4 class="text-sm font-medium mb-3 flex items-center gap-2">' +
|
||||
'<i data-lucide="package" class="w-4 h-4"></i> Dependencies' +
|
||||
'</h4>' +
|
||||
'<div id="semanticDepsStatus" class="space-y-2">' +
|
||||
'<div class="text-sm text-muted-foreground">' + t('codexlens.checkingDeps') + '</div>' +
|
||||
'</div>' +
|
||||
'<div id="spladeStatus" class="space-y-2 mt-3 pt-3 border-t border-border">' +
|
||||
'<div class="text-sm text-muted-foreground">' + t('common.loading') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Model Management
|
||||
'<div class="rounded-lg border border-border p-4">' +
|
||||
'<h4 class="text-sm font-medium mb-3 flex items-center gap-2">' +
|
||||
'<i data-lucide="brain" class="w-4 h-4"></i> Models' +
|
||||
'</h4>' +
|
||||
'<div id="modelListContainer" class="space-y-2">' +
|
||||
'<div class="text-sm text-muted-foreground">' + t('codexlens.loadingModels') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Danger Zone
|
||||
'<div class="rounded-lg border border-destructive/30 p-4">' +
|
||||
'<h4 class="text-sm font-medium text-destructive mb-3 flex items-center gap-2">' +
|
||||
'<i data-lucide="alert-triangle" class="w-4 h-4"></i> Danger Zone' +
|
||||
'</h4>' +
|
||||
'<div class="flex flex-wrap gap-2">' +
|
||||
'<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-border bg-background hover:bg-muted/50 transition-colors" onclick="cleanCodexLensIndexes()">' +
|
||||
'<i data-lucide="trash" class="w-3.5 h-3.5"></i> Clean All Indexes' +
|
||||
'</button>' +
|
||||
'<button class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-destructive/30 bg-destructive/5 text-destructive hover:bg-destructive/10 transition-colors" onclick="uninstallCodexLensFromManager()">' +
|
||||
'<i data-lucide="trash-2" class="w-3.5 h-3.5"></i> Uninstall' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
|
||||
'</div>' + // End Tab Content Container
|
||||
'</div>' + // End modal-body
|
||||
|
||||
// Footer
|
||||
'<div class="tool-config-footer">' +
|
||||
@@ -227,19 +333,55 @@ function buildCodexLensConfigContent(config) {
|
||||
* Initialize CodexLens config modal event handlers
|
||||
*/
|
||||
function initCodexLensConfigEvents(currentConfig) {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.codexlens-tab').forEach(function(tab) {
|
||||
tab.onclick = function() {
|
||||
// Remove active from all tabs
|
||||
document.querySelectorAll('.codexlens-tab').forEach(function(t) {
|
||||
t.classList.remove('active', 'border-primary', 'text-primary');
|
||||
t.classList.add('border-transparent', 'text-muted-foreground');
|
||||
});
|
||||
// Hide all content
|
||||
document.querySelectorAll('.codexlens-tab-content').forEach(function(c) {
|
||||
c.classList.add('hidden');
|
||||
c.classList.remove('active');
|
||||
});
|
||||
// Activate clicked tab
|
||||
this.classList.add('active', 'border-primary', 'text-primary');
|
||||
this.classList.remove('border-transparent', 'text-muted-foreground');
|
||||
// Show corresponding content
|
||||
var tabName = this.dataset.tab;
|
||||
var content = document.querySelector('.codexlens-tab-content[data-tab="' + tabName + '"]');
|
||||
if (content) {
|
||||
content.classList.remove('hidden');
|
||||
content.classList.add('active');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Save button
|
||||
var saveBtn = document.getElementById('saveCodexLensConfigBtn');
|
||||
if (saveBtn) {
|
||||
saveBtn.onclick = async function() {
|
||||
var indexDirInput = document.getElementById('indexDirInput');
|
||||
var apiMaxWorkersInput = document.getElementById('apiMaxWorkersInput');
|
||||
var apiBatchSizeInput = document.getElementById('apiBatchSizeInput');
|
||||
|
||||
var newIndexDir = indexDirInput ? indexDirInput.value.trim() : '';
|
||||
var newMaxWorkers = apiMaxWorkersInput ? parseInt(apiMaxWorkersInput.value) || 4 : 4;
|
||||
var newBatchSize = apiBatchSizeInput ? parseInt(apiBatchSizeInput.value) || 8 : 8;
|
||||
|
||||
if (!newIndexDir) {
|
||||
showRefreshToast(t('codexlens.pathEmpty'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndexDir === currentConfig.index_dir) {
|
||||
// Check if anything changed
|
||||
var hasChanges = newIndexDir !== currentConfig.index_dir ||
|
||||
newMaxWorkers !== (currentConfig.api_max_workers || 4) ||
|
||||
newBatchSize !== (currentConfig.api_batch_size || 8);
|
||||
|
||||
if (!hasChanges) {
|
||||
closeModal();
|
||||
return;
|
||||
}
|
||||
@@ -251,7 +393,11 @@ function initCodexLensConfigEvents(currentConfig) {
|
||||
var response = await fetch('/api/codexlens/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ index_dir: newIndexDir })
|
||||
body: JSON.stringify({
|
||||
index_dir: newIndexDir,
|
||||
api_max_workers: newMaxWorkers,
|
||||
api_batch_size: newBatchSize
|
||||
})
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
@@ -359,6 +505,112 @@ function initCodexLensConfigEvents(currentConfig) {
|
||||
loadModelList();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENVIRONMENT VARIABLES MANAGEMENT
|
||||
// ============================================================
|
||||
|
||||
// Known env variable groups
|
||||
var ENV_VARIABLES = {
|
||||
'RERANKER_API_KEY': { label: 'Reranker API Key', placeholder: 'sk-...', type: 'password' },
|
||||
'RERANKER_API_BASE': { label: 'Reranker API Base', placeholder: 'https://api.openai.com/v1' },
|
||||
'RERANKER_MODEL': { label: 'Reranker Model', placeholder: 'text-embedding-3-small' },
|
||||
'EMBEDDING_API_KEY': { label: 'Embedding API Key', placeholder: 'sk-...', type: 'password' },
|
||||
'EMBEDDING_API_BASE': { label: 'Embedding API Base', placeholder: 'https://api.openai.com/v1' },
|
||||
'EMBEDDING_MODEL': { label: 'Embedding Model', placeholder: 'text-embedding-3-small' },
|
||||
'LITELLM_API_KEY': { label: 'LiteLLM API Key', placeholder: 'sk-...', type: 'password' },
|
||||
'LITELLM_API_BASE': { label: 'LiteLLM API Base', placeholder: 'http://localhost:4000' },
|
||||
'LITELLM_MODEL': { label: 'LiteLLM Model', placeholder: 'gpt-3.5-turbo' }
|
||||
};
|
||||
|
||||
/**
|
||||
* Load environment variables from ~/.codexlens/.env
|
||||
*/
|
||||
async function loadEnvVariables() {
|
||||
var container = document.getElementById('envVarsContainer');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div class="text-xs text-muted-foreground animate-pulse">Loading...</div>';
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/env');
|
||||
var result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
container.innerHTML = '<div class="text-xs text-error">' + (result.error || 'Failed to load') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var env = result.env || {};
|
||||
var html = '<div class="space-y-2">';
|
||||
|
||||
// Render known variables with their values
|
||||
for (var key in ENV_VARIABLES) {
|
||||
var config = ENV_VARIABLES[key];
|
||||
var value = env[key] || '';
|
||||
var inputType = config.type || 'text';
|
||||
|
||||
html += '<div class="flex items-center gap-2">' +
|
||||
'<label class="text-xs text-muted-foreground w-32 flex-shrink-0" title="' + escapeHtml(key) + '">' + escapeHtml(config.label) + '</label>' +
|
||||
'<input type="' + inputType + '" class="tool-config-input flex-1 text-xs py-1" ' +
|
||||
'data-env-key="' + escapeHtml(key) + '" value="' + escapeHtml(value) + '" placeholder="' + escapeHtml(config.placeholder || '') + '" />' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '</div>' +
|
||||
'<div class="flex gap-2 mt-3">' +
|
||||
'<button class="btn-sm btn-primary flex-1" onclick="saveEnvVariables()">' +
|
||||
'<i data-lucide="save" class="w-3 h-3"></i> Save' +
|
||||
'</button>' +
|
||||
'<button class="btn-sm btn-outline" onclick="loadEnvVariables()">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground mt-2">' +
|
||||
'<i data-lucide="info" class="w-3 h-3 inline"></i> ' +
|
||||
'Saved to: ' + escapeHtml(result.path) +
|
||||
'</div>';
|
||||
|
||||
container.innerHTML = html;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} catch (err) {
|
||||
container.innerHTML = '<div class="text-xs text-error">' + err.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save environment variables to ~/.codexlens/.env
|
||||
*/
|
||||
async function saveEnvVariables() {
|
||||
var inputs = document.querySelectorAll('[data-env-key]');
|
||||
var env = {};
|
||||
|
||||
inputs.forEach(function(input) {
|
||||
var key = input.dataset.envKey;
|
||||
var value = input.value.trim();
|
||||
if (value) {
|
||||
env[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/env', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ env: env })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast('Environment configuration saved', 'success');
|
||||
} else {
|
||||
showRefreshToast('Failed to save: ' + result.error, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast('Error: ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SEMANTIC DEPENDENCIES MANAGEMENT
|
||||
// ============================================================
|
||||
|
||||
@@ -807,31 +807,17 @@ async function executeCodexLens(args: string[], options: ExecuteOptions = {}): P
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Build command string - quote paths for shell execution
|
||||
const quotedPython = `"${VENV_PYTHON}"`;
|
||||
const cmdArgs = args.map(arg => {
|
||||
// Quote arguments that contain spaces or special characters
|
||||
if (arg.includes(' ') || arg.includes('\\')) {
|
||||
return `"${arg}"`;
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
// SECURITY: Use spawn without shell to prevent command injection
|
||||
// Pass arguments directly - no manual quoting needed
|
||||
// spawn's cwd option handles drive changes correctly on Windows
|
||||
const spawnArgs = ['-m', 'codexlens', ...args];
|
||||
|
||||
// Build full command - on Windows, prepend cd to handle different drives
|
||||
let fullCmd: string;
|
||||
if (process.platform === 'win32' && cwd) {
|
||||
// Use cd /d to change drive and directory, then run command
|
||||
fullCmd = `cd /d "${cwd}" && ${quotedPython} -m codexlens ${cmdArgs.join(' ')}`;
|
||||
} else {
|
||||
fullCmd = `${quotedPython} -m codexlens ${cmdArgs.join(' ')}`;
|
||||
}
|
||||
|
||||
// Use spawn with shell for real-time progress updates
|
||||
// spawn streams output in real-time, unlike exec which buffers until completion
|
||||
const child = spawn(fullCmd, [], {
|
||||
cwd: process.platform === 'win32' ? undefined : cwd,
|
||||
shell: process.platform === 'win32' ? process.env.ComSpec || true : true,
|
||||
const child = spawn(VENV_PYTHON, spawnArgs, {
|
||||
cwd,
|
||||
shell: false, // CRITICAL: Prevent command injection
|
||||
timeout,
|
||||
// Ensure proper encoding on Windows
|
||||
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
||||
});
|
||||
|
||||
// Track indexing process for cancellation (only for init commands)
|
||||
|
||||
Reference in New Issue
Block a user