feat: Enhance CodexLens indexing and search capabilities with new CLI options and improved error handling

This commit is contained in:
catlog22
2025-12-19 15:10:37 +08:00
parent c7ced2bfbb
commit 2f0cce0089
18 changed files with 480 additions and 128 deletions

View File

@@ -18,6 +18,9 @@ const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen'];
// Subdirectories that should always be installed to global (~/.claude/)
const GLOBAL_SUBDIRS = ['workflows', 'scripts', 'templates'];
// Files that should be excluded from installation (user-specific settings)
const EXCLUDED_FILES = ['settings.json', 'settings.local.json'];
interface InstallOptions {
mode?: string;
path?: string;
@@ -359,20 +362,28 @@ function getNewInstallationFiles(sourceDir: string, installPath: string, mode: s
* @param destDir - Destination directory
* @param files - Set to add file paths to
* @param excludeDirs - Directories to exclude
* @param excludeFiles - Files to exclude
*/
function collectFilesRecursive(srcDir: string, destDir: string, files: Set<string>, excludeDirs: string[] = []): void {
function collectFilesRecursive(
srcDir: string,
destDir: string,
files: Set<string>,
excludeDirs: string[] = [],
excludeFiles: string[] = EXCLUDED_FILES
): void {
if (!existsSync(srcDir)) return;
const entries = readdirSync(srcDir);
for (const entry of entries) {
if (excludeDirs.includes(entry)) continue;
if (excludeFiles.includes(entry)) continue;
const srcPath = join(srcDir, entry);
const destPath = join(destDir, entry);
const stat = statSync(srcPath);
if (stat.isDirectory()) {
collectFilesRecursive(srcPath, destPath, files);
collectFilesRecursive(srcPath, destPath, files, [], excludeFiles);
} else {
files.add(destPath.toLowerCase().replace(/\\/g, '/'));
}
@@ -491,7 +502,8 @@ async function copyDirectory(
src: string,
dest: string,
manifest: any = null,
excludeDirs: string[] = []
excludeDirs: string[] = [],
excludeFiles: string[] = EXCLUDED_FILES
): Promise<CopyResult> {
let files = 0;
let directories = 0;
@@ -511,12 +523,17 @@ async function copyDirectory(
continue;
}
// Skip excluded files
if (excludeFiles.includes(entry)) {
continue;
}
const srcPath = join(src, entry);
const destPath = join(dest, entry);
const stat = statSync(srcPath);
if (stat.isDirectory()) {
const result = await copyDirectory(srcPath, destPath, manifest);
const result = await copyDirectory(srcPath, destPath, manifest, [], excludeFiles);
files += result.files;
directories += result.directories;
} else {

View File

@@ -815,13 +815,10 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
enabled = chineseRefPattern.test(content);
}
// Find guidelines file path (project or user level)
const projectGuidelinesPath = join(initialPath, '.claude', 'workflows', 'chinese-response.md');
// Find guidelines file path - always use user-level path
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
if (existsSync(projectGuidelinesPath)) {
guidelinesPath = projectGuidelinesPath;
} else if (existsSync(userGuidelinesPath)) {
if (existsSync(userGuidelinesPath)) {
guidelinesPath = userGuidelinesPath;
}
@@ -853,21 +850,15 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md');
const userClaudeDir = join(homedir(), '.claude');
// Find guidelines file path
const projectGuidelinesPath = join(initialPath, '.claude', 'workflows', 'chinese-response.md');
// Find guidelines file path - always use user-level path with ~ shorthand
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
let guidelinesRef = '';
if (existsSync(projectGuidelinesPath)) {
// Use project-level guidelines with absolute path
guidelinesRef = projectGuidelinesPath.replace(/\\/g, '/');
} else if (existsSync(userGuidelinesPath)) {
// Use user-level guidelines with ~ shorthand
guidelinesRef = '~/.claude/workflows/chinese-response.md';
} else {
return { error: 'Chinese response guidelines file not found', status: 404 };
if (!existsSync(userGuidelinesPath)) {
return { error: 'Chinese response guidelines file not found at ~/.claude/workflows/chinese-response.md', status: 404 };
}
const guidelinesRef = '~/.claude/workflows/chinese-response.md';
const chineseRefLine = `- **中文回复准则**: @${guidelinesRef}`;
const chineseRefPattern = /^- \*\*中文回复准则\*\*:.*chinese-response\.md.*$/gm;
@@ -922,5 +913,118 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
return true;
}
// API: Get Windows platform setting status
if (pathname === '/api/language/windows-platform' && req.method === 'GET') {
try {
const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md');
const windowsRefPattern = /@.*windows-platform\.md/i;
let enabled = false;
let guidelinesPath = '';
// Check if user CLAUDE.md exists and contains Windows platform reference
if (existsSync(userClaudePath)) {
const content = readFileSync(userClaudePath, 'utf8');
enabled = windowsRefPattern.test(content);
}
// Find guidelines file path - always use user-level path
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'windows-platform.md');
if (existsSync(userGuidelinesPath)) {
guidelinesPath = userGuidelinesPath;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
enabled,
guidelinesPath,
guidelinesExists: !!guidelinesPath,
userClaudeMdExists: existsSync(userClaudePath)
}));
return true;
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (error as Error).message }));
return true;
}
}
// API: Toggle Windows platform setting
if (pathname === '/api/language/windows-platform' && req.method === 'POST') {
handlePostRequest(req, res, async (body: any) => {
const { enabled } = body;
if (typeof enabled !== 'boolean') {
return { error: 'Missing or invalid enabled parameter', status: 400 };
}
try {
const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md');
const userClaudeDir = join(homedir(), '.claude');
// Find guidelines file path - always use user-level path with ~ shorthand
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'windows-platform.md');
if (!existsSync(userGuidelinesPath)) {
return { error: 'Windows platform guidelines file not found at ~/.claude/workflows/windows-platform.md', status: 404 };
}
const guidelinesRef = '~/.claude/workflows/windows-platform.md';
const windowsRefLine = `- **Windows Platform**: @${guidelinesRef}`;
const windowsRefPattern = /^- \*\*Windows Platform\*\*:.*windows-platform\.md.*$/gm;
// Ensure user .claude directory exists
if (!existsSync(userClaudeDir)) {
const fs = require('fs');
fs.mkdirSync(userClaudeDir, { recursive: true });
}
let content = '';
if (existsSync(userClaudePath)) {
content = readFileSync(userClaudePath, 'utf8');
} else {
// Create new CLAUDE.md with header
content = '# Claude Instructions\n\n';
}
if (enabled) {
// Check if reference already exists
if (windowsRefPattern.test(content)) {
return { success: true, message: 'Already enabled' };
}
// Add reference after the header line or at the beginning
const headerMatch = content.match(/^# Claude Instructions\n\n?/);
if (headerMatch) {
const insertPosition = headerMatch[0].length;
content = content.slice(0, insertPosition) + windowsRefLine + '\n' + content.slice(insertPosition);
} else {
// Add header and reference
content = '# Claude Instructions\n\n' + windowsRefLine + '\n' + content;
}
} else {
// Remove reference
content = content.replace(windowsRefPattern, '').replace(/\n{3,}/g, '\n\n').trim();
if (content) content += '\n';
}
writeFileSync(userClaudePath, content, 'utf8');
// Broadcast update
broadcastToClients({
type: 'LANGUAGE_SETTING_CHANGED',
data: { windowsPlatform: enabled }
});
return { success: true, enabled };
} catch (error) {
return { error: (error as Error).message, status: 500 };
}
});
return true;
}
return false;
}

View File

@@ -384,17 +384,23 @@ 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 } = body;
const { path: projectPath, indexType = 'vector' } = body;
const targetPath = projectPath || initialPath;
// Build CLI arguments based on index type
const args = ['init', targetPath, '--json'];
if (indexType === 'normal') {
args.push('--no-embeddings');
}
// Broadcast start event
broadcastToClients({
type: 'CODEXLENS_INDEX_PROGRESS',
payload: { stage: 'start', message: 'Starting index...', percent: 0, path: targetPath }
payload: { stage: 'start', message: 'Starting index...', percent: 0, path: targetPath, indexType }
});
try {
const result = await executeCodexLens(['init', targetPath, '--json'], {
const result = await executeCodexLens(args, {
cwd: targetPath,
timeout: 1800000, // 30 minutes for large codebases
onProgress: (progress: ProgressInfo) => {

View File

@@ -177,11 +177,20 @@ function renderIndexCard() {
</table>
</div>
<div class="mt-4 flex justify-between items-center gap-2">
<button onclick="initCodexLensIndex()"
class="text-xs px-3 py-1.5 bg-primary/10 text-primary hover:bg-primary/20 rounded transition-colors flex items-center gap-1.5">
<i data-lucide="database" class="w-3.5 h-3.5"></i>
${t('index.initCurrent') || 'Init Current Project'}
</button>
<div class="flex items-center gap-2">
<button onclick="initCodexLensIndex('vector')"
class="text-xs px-3 py-1.5 bg-primary/10 text-primary hover:bg-primary/20 rounded transition-colors flex items-center gap-1.5"
title="${t('index.vectorDesc') || 'Semantic search with embeddings'}">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i>
${t('index.vectorIndex') || 'Vector'}
</button>
<button onclick="initCodexLensIndex('normal')"
class="text-xs px-3 py-1.5 bg-muted text-muted-foreground hover:bg-muted/80 rounded transition-colors flex items-center gap-1.5"
title="${t('index.normalDesc') || 'Fast full-text search only'}">
<i data-lucide="file-text" class="w-3.5 h-3.5"></i>
${t('index.normalIndex') || 'FTS'}
</button>
</div>
<button onclick="cleanAllIndexesConfirm()"
class="text-xs px-3 py-1.5 bg-destructive/10 text-destructive hover:bg-destructive/20 rounded transition-colors flex items-center gap-1.5">
<i data-lucide="trash" class="w-3.5 h-3.5"></i>

View File

@@ -322,6 +322,10 @@ const i18n = {
'index.cleanFailed': 'Clean failed',
'index.cleanAllConfirm': 'Are you sure you want to clean ALL indexes? This cannot be undone.',
'index.cleanAllSuccess': 'All indexes cleaned',
'index.vectorIndex': 'Vector',
'index.normalIndex': 'FTS',
'index.vectorDesc': 'Semantic search with embeddings',
'index.normalDesc': 'Fast full-text search only',
// Semantic Search Configuration
'semantic.settings': 'Semantic Search Settings',
@@ -343,6 +347,12 @@ const i18n = {
'lang.disableSuccess': 'Chinese response disabled',
'lang.enableFailed': 'Failed to enable Chinese response',
'lang.disableFailed': 'Failed to disable Chinese response',
'lang.windows': 'Windows Platform',
'lang.windowsDesc': 'Enable Windows path format guidelines in global CLAUDE.md',
'lang.windowsEnableSuccess': 'Windows platform guidelines enabled',
'lang.windowsDisableSuccess': 'Windows platform guidelines disabled',
'lang.windowsEnableFailed': 'Failed to enable Windows platform guidelines',
'lang.windowsDisableFailed': 'Failed to disable Windows platform guidelines',
'cli.promptFormat': 'Prompt Format',
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
'cli.storageBackend': 'Storage Backend',
@@ -1597,6 +1607,10 @@ const i18n = {
'index.cleanFailed': '清理失败',
'index.cleanAllConfirm': '确定要清理所有索引吗?此操作无法撤销。',
'index.cleanAllSuccess': '所有索引已清理',
'index.vectorIndex': '向量索引',
'index.normalIndex': '全文索引',
'index.vectorDesc': '语义搜索(含嵌入向量)',
'index.normalDesc': '快速全文搜索',
// Semantic Search 配置
'semantic.settings': '语义搜索设置',
@@ -1618,6 +1632,12 @@ const i18n = {
'lang.disableSuccess': '中文回复已禁用',
'lang.enableFailed': '启用中文回复失败',
'lang.disableFailed': '禁用中文回复失败',
'lang.windows': 'Windows 平台规范',
'lang.windowsDesc': '在全局 CLAUDE.md 中启用 Windows 路径格式规范',
'lang.windowsEnableSuccess': 'Windows 平台规范已启用',
'lang.windowsDisableSuccess': 'Windows 平台规范已禁用',
'lang.windowsEnableFailed': '启用 Windows 平台规范失败',
'lang.windowsDisableFailed': '禁用 Windows 平台规范失败',
'cli.promptFormat': '提示词格式',
'cli.promptFormatDesc': '多轮对话拼接格式',
'cli.storageBackend': '存储后端',

View File

@@ -512,6 +512,8 @@ function renderCcwSection() {
// ========== Language Settings State ==========
var chineseResponseEnabled = false;
var chineseResponseLoading = false;
var windowsPlatformEnabled = false;
var windowsPlatformLoading = false;
// ========== Language Settings Section ==========
async function loadLanguageSettings() {
@@ -528,6 +530,20 @@ async function loadLanguageSettings() {
}
}
async function loadWindowsPlatformSettings() {
try {
var response = await fetch('/api/language/windows-platform');
if (!response.ok) throw new Error('Failed to load Windows platform settings');
var data = await response.json();
windowsPlatformEnabled = data.enabled || false;
return data;
} catch (err) {
console.error('Failed to load Windows platform settings:', err);
windowsPlatformEnabled = false;
return { enabled: false, guidelinesExists: false };
}
}
async function toggleChineseResponse(enabled) {
if (chineseResponseLoading) return;
chineseResponseLoading = true;
@@ -560,6 +576,38 @@ async function toggleChineseResponse(enabled) {
}
}
async function toggleWindowsPlatform(enabled) {
if (windowsPlatformLoading) return;
windowsPlatformLoading = true;
try {
var response = await fetch('/api/language/windows-platform', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
});
if (!response.ok) {
var errData = await response.json();
throw new Error(errData.error || 'Failed to update setting');
}
var data = await response.json();
windowsPlatformEnabled = data.enabled;
// Update UI
renderLanguageSettingsSection();
// Show toast
showRefreshToast(enabled ? t('lang.windowsEnableSuccess') : t('lang.windowsDisableSuccess'), 'success');
} catch (err) {
console.error('Failed to toggle Windows platform:', err);
showRefreshToast(enabled ? t('lang.windowsEnableFailed') : t('lang.windowsDisableFailed'), 'error');
} finally {
windowsPlatformLoading = false;
}
}
async function renderLanguageSettingsSection() {
var container = document.getElementById('language-settings-section');
if (!container) return;
@@ -568,13 +616,16 @@ async function renderLanguageSettingsSection() {
if (!chineseResponseEnabled && !chineseResponseLoading) {
await loadLanguageSettings();
}
if (!windowsPlatformEnabled && !windowsPlatformLoading) {
await loadWindowsPlatformSettings();
}
var settingsHtml = '<div class="section-header">' +
'<div class="section-header-left">' +
'<h3><i data-lucide="languages" class="w-4 h-4"></i> ' + t('lang.settings') + '</h3>' +
'</div>' +
'</div>' +
'<div class="cli-settings-grid" style="grid-template-columns: 1fr;">' +
'<div class="cli-settings-grid" style="grid-template-columns: 1fr 1fr;">' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="message-square-text" class="w-3 h-3"></i>' +
@@ -591,6 +642,22 @@ async function renderLanguageSettingsSection() {
'</div>' +
'<p class="cli-setting-desc">' + t('lang.chineseDesc') + '</p>' +
'</div>' +
'<div class="cli-setting-item">' +
'<label class="cli-setting-label">' +
'<i data-lucide="monitor" class="w-3 h-3"></i>' +
t('lang.windows') +
'</label>' +
'<div class="cli-setting-control">' +
'<label class="cli-toggle">' +
'<input type="checkbox"' + (windowsPlatformEnabled ? ' checked' : '') + ' onchange="toggleWindowsPlatform(this.checked)"' + (windowsPlatformLoading ? ' disabled' : '') + '>' +
'<span class="cli-toggle-slider"></span>' +
'</label>' +
'<span class="cli-setting-status ' + (windowsPlatformEnabled ? 'enabled' : 'disabled') + '">' +
(windowsPlatformEnabled ? t('lang.enabled') : t('lang.disabled')) +
'</span>' +
'</div>' +
'<p class="cli-setting-desc">' + t('lang.windowsDesc') + '</p>' +
'</div>' +
'</div>';
container.innerHTML = settingsHtml;

View File

@@ -554,8 +554,11 @@ async function deleteModel(profile) {
/**
* Initialize CodexLens index with bottom floating progress bar
* @param {string} indexType - 'vector' (with embeddings) or 'normal' (FTS only)
*/
function initCodexLensIndex() {
function initCodexLensIndex(indexType) {
indexType = indexType || 'vector';
// Remove existing progress bar if any
closeCodexLensIndexModal();
@@ -563,6 +566,7 @@ function initCodexLensIndex() {
var progressBar = document.createElement('div');
progressBar.id = 'codexlensIndexFloating';
progressBar.className = 'fixed bottom-0 left-0 right-0 z-50 bg-card border-t border-border shadow-lg transform transition-transform duration-300';
var indexTypeLabel = indexType === 'vector' ? 'Vector' : 'FTS';
progressBar.innerHTML =
'<div class="max-w-4xl mx-auto px-4 py-3">' +
'<div class="flex items-center justify-between gap-4">' +
@@ -570,7 +574,7 @@ function initCodexLensIndex() {
'<div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full flex-shrink-0" id="codexlensIndexSpinner"></div>' +
'<div class="flex-1 min-w-0">' +
'<div class="flex items-center gap-2">' +
'<span class="font-medium text-sm">' + t('codexlens.indexing') + '</span>' +
'<span class="font-medium text-sm">' + t('codexlens.indexing') + ' (' + indexTypeLabel + ')</span>' +
'<span class="text-xs text-muted-foreground" id="codexlensIndexPercent">0%</span>' +
'</div>' +
'<div class="text-xs text-muted-foreground truncate" id="codexlensIndexStatus">' + t('codexlens.preparingIndex') + '</div>' +
@@ -590,14 +594,16 @@ function initCodexLensIndex() {
document.body.appendChild(progressBar);
if (window.lucide) lucide.createIcons();
// Start indexing
startCodexLensIndexing();
// Start indexing with specified type
startCodexLensIndexing(indexType);
}
/**
* Start the indexing process
* @param {string} indexType - 'vector' or 'normal'
*/
async function startCodexLensIndexing() {
async function startCodexLensIndexing(indexType) {
indexType = indexType || 'vector';
var statusText = document.getElementById('codexlensIndexStatus');
var progressBar = document.getElementById('codexlensIndexProgressBar');
var percentText = document.getElementById('codexlensIndexPercent');
@@ -629,11 +635,11 @@ async function startCodexLensIndexing() {
}
try {
console.log('[CodexLens] Starting index for:', projectPath);
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType);
var response = await fetch('/api/codexlens/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: projectPath })
body: JSON.stringify({ path: projectPath, indexType: indexType })
});
var result = await response.json();

View File

@@ -326,7 +326,8 @@ function buildRipgrepCommand(params: {
}
/**
* Action: init - Initialize CodexLens index
* Action: init - Initialize CodexLens index (FTS only, no embeddings)
* For semantic/vector search, use ccw view dashboard or codexlens CLI directly
*/
async function executeInitAction(params: Params): Promise<SearchResult> {
const { path = '.', languages } = params;
@@ -340,7 +341,8 @@ async function executeInitAction(params: Params): Promise<SearchResult> {
};
}
const args = ['init', path];
// Build args with --no-embeddings for FTS-only index (faster)
const args = ['init', path, '--no-embeddings'];
if (languages && languages.length > 0) {
args.push('--languages', languages.join(','));
}
@@ -379,12 +381,14 @@ async function executeInitAction(params: Params): Promise<SearchResult> {
metadata.progressHistory = progressUpdates.slice(-5); // Keep last 5 progress updates
}
const successMessage = result.success
? `FTS index created for ${path}. Note: For semantic/vector search, create vector index via "ccw view" dashboard or run "codexlens init ${path}" (without --no-embeddings).`
: undefined;
return {
success: result.success,
error: result.error,
message: result.success
? `CodexLens index created successfully for ${path}`
: undefined,
message: successMessage,
metadata,
};
}
@@ -923,7 +927,7 @@ export const schema: ToolSchema = {
**Quick Start:**
smart_search(query="authentication logic") # Auto mode (intelligent routing)
smart_search(action="init", path=".") # Initialize index (required for hybrid)
smart_search(action="init", path=".") # Initialize FTS index (fast, no embeddings)
smart_search(action="status") # Check index status
**Five Modes:**
@@ -934,7 +938,7 @@ export const schema: ToolSchema = {
2. hybrid: CodexLens RRF fusion (exact + fuzzy + vector)
- Best quality, semantic understanding
- Requires index with embeddings
- Requires index with embeddings (create via "ccw view" dashboard)
3. exact: CodexLens FTS (full-text search)
- Precise keyword matching
@@ -950,21 +954,21 @@ export const schema: ToolSchema = {
**Actions:**
- search (default): Intelligent search with auto routing
- init: Create CodexLens index (required for hybrid/exact)
- init: Create FTS index only (no embeddings, faster). For vector/semantic search, use "ccw view" dashboard
- status: Check index and embedding availability
- search_files: Return file paths only
**Workflow:**
1. Run action="init" to create index
2. Use auto mode - it routes to hybrid for NL queries, exact for simple queries
3. Use ripgrep mode for fast searches without index`,
1. Run action="init" to create FTS index (fast)
2. For semantic search: create vector index via "ccw view" dashboard or "codexlens init <path>"
3. Use auto mode - it routes to hybrid for NL queries, exact for simple queries`,
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['init', 'search', 'search_files', 'status'],
description: 'Action to perform: init (create index), search (default), search_files (paths only), status (check index)',
description: 'Action to perform: init (create FTS index, no embeddings), search (default), search_files (paths only), status (check index)',
default: 'search',
},
query: {