mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: Enhance CodexLens indexing and search capabilities with new CLI options and improved error handling
This commit is contained in:
@@ -4,13 +4,18 @@
|
|||||||
|
|
||||||
**When**: Find code, understand codebase structure, locate implementations
|
**When**: Find code, understand codebase structure, locate implementations
|
||||||
|
|
||||||
**How**:
|
**Workflow** (search first, init if needed):
|
||||||
```javascript
|
```javascript
|
||||||
smart_search(query="authentication logic") // Auto mode (recommended)
|
// Step 1: Try search directly (works if index exists or uses ripgrep fallback)
|
||||||
smart_search(action="init", path=".") // First-time setup
|
smart_search(query="authentication logic")
|
||||||
|
|
||||||
|
// Step 2: Only if search warns "No CodexLens index found", then init
|
||||||
|
smart_search(action="init", path=".") // Creates FTS index only
|
||||||
|
|
||||||
|
// Note: For semantic/vector search, use "ccw view" dashboard to create vector index
|
||||||
```
|
```
|
||||||
|
|
||||||
**Modes**: `auto` (intelligent routing), `hybrid` (best quality), `exact` (FTS)
|
**Modes**: `auto` (intelligent routing), `hybrid` (semantic, needs vector index), `exact` (FTS), `ripgrep` (no index)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
16
.claude/workflows/windows-platform.md
Normal file
16
.claude/workflows/windows-platform.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Windows Platform Guidelines
|
||||||
|
|
||||||
|
## Path Format Guidelines
|
||||||
|
|
||||||
|
### MCP Tools
|
||||||
|
- Use double backslash: `D:\\path\\file.txt`
|
||||||
|
- Example: `read_file(paths="D:\\Claude_dms3\\src\\index.ts")`
|
||||||
|
|
||||||
|
### Bash Commands
|
||||||
|
- Use forward slash: `D:/path/file.txt` or `/d/path/file.txt`
|
||||||
|
- Example: `cd D:/Claude_dms3/src`
|
||||||
|
|
||||||
|
### Relative Paths
|
||||||
|
- Universal format works in both MCP and Bash contexts
|
||||||
|
- Example: `./src/index.ts` or `../shared/utils.ts`
|
||||||
|
|
||||||
10
.mcp.json
10
.mcp.json
@@ -7,6 +7,16 @@
|
|||||||
"chrome-devtools-mcp@latest"
|
"chrome-devtools-mcp@latest"
|
||||||
],
|
],
|
||||||
"env": {}
|
"env": {}
|
||||||
|
},
|
||||||
|
"ccw-tools": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"ccw-mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"CCW_ENABLED_TOOLS": "write_file,edit_file,smart_search"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,9 @@ const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen'];
|
|||||||
// Subdirectories that should always be installed to global (~/.claude/)
|
// Subdirectories that should always be installed to global (~/.claude/)
|
||||||
const GLOBAL_SUBDIRS = ['workflows', 'scripts', 'templates'];
|
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 {
|
interface InstallOptions {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
@@ -359,20 +362,28 @@ function getNewInstallationFiles(sourceDir: string, installPath: string, mode: s
|
|||||||
* @param destDir - Destination directory
|
* @param destDir - Destination directory
|
||||||
* @param files - Set to add file paths to
|
* @param files - Set to add file paths to
|
||||||
* @param excludeDirs - Directories to exclude
|
* @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;
|
if (!existsSync(srcDir)) return;
|
||||||
|
|
||||||
const entries = readdirSync(srcDir);
|
const entries = readdirSync(srcDir);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (excludeDirs.includes(entry)) continue;
|
if (excludeDirs.includes(entry)) continue;
|
||||||
|
if (excludeFiles.includes(entry)) continue;
|
||||||
|
|
||||||
const srcPath = join(srcDir, entry);
|
const srcPath = join(srcDir, entry);
|
||||||
const destPath = join(destDir, entry);
|
const destPath = join(destDir, entry);
|
||||||
const stat = statSync(srcPath);
|
const stat = statSync(srcPath);
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
collectFilesRecursive(srcPath, destPath, files);
|
collectFilesRecursive(srcPath, destPath, files, [], excludeFiles);
|
||||||
} else {
|
} else {
|
||||||
files.add(destPath.toLowerCase().replace(/\\/g, '/'));
|
files.add(destPath.toLowerCase().replace(/\\/g, '/'));
|
||||||
}
|
}
|
||||||
@@ -491,7 +502,8 @@ async function copyDirectory(
|
|||||||
src: string,
|
src: string,
|
||||||
dest: string,
|
dest: string,
|
||||||
manifest: any = null,
|
manifest: any = null,
|
||||||
excludeDirs: string[] = []
|
excludeDirs: string[] = [],
|
||||||
|
excludeFiles: string[] = EXCLUDED_FILES
|
||||||
): Promise<CopyResult> {
|
): Promise<CopyResult> {
|
||||||
let files = 0;
|
let files = 0;
|
||||||
let directories = 0;
|
let directories = 0;
|
||||||
@@ -511,12 +523,17 @@ async function copyDirectory(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip excluded files
|
||||||
|
if (excludeFiles.includes(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const srcPath = join(src, entry);
|
const srcPath = join(src, entry);
|
||||||
const destPath = join(dest, entry);
|
const destPath = join(dest, entry);
|
||||||
const stat = statSync(srcPath);
|
const stat = statSync(srcPath);
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
const result = await copyDirectory(srcPath, destPath, manifest);
|
const result = await copyDirectory(srcPath, destPath, manifest, [], excludeFiles);
|
||||||
files += result.files;
|
files += result.files;
|
||||||
directories += result.directories;
|
directories += result.directories;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -815,13 +815,10 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
enabled = chineseRefPattern.test(content);
|
enabled = chineseRefPattern.test(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find guidelines file path (project or user level)
|
// Find guidelines file path - always use user-level path
|
||||||
const projectGuidelinesPath = join(initialPath, '.claude', 'workflows', 'chinese-response.md');
|
|
||||||
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
|
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
|
||||||
|
|
||||||
if (existsSync(projectGuidelinesPath)) {
|
if (existsSync(userGuidelinesPath)) {
|
||||||
guidelinesPath = projectGuidelinesPath;
|
|
||||||
} else if (existsSync(userGuidelinesPath)) {
|
|
||||||
guidelinesPath = userGuidelinesPath;
|
guidelinesPath = userGuidelinesPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,21 +850,15 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md');
|
const userClaudePath = join(homedir(), '.claude', 'CLAUDE.md');
|
||||||
const userClaudeDir = join(homedir(), '.claude');
|
const userClaudeDir = join(homedir(), '.claude');
|
||||||
|
|
||||||
// Find guidelines file path
|
// Find guidelines file path - always use user-level path with ~ shorthand
|
||||||
const projectGuidelinesPath = join(initialPath, '.claude', 'workflows', 'chinese-response.md');
|
|
||||||
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
|
const userGuidelinesPath = join(homedir(), '.claude', 'workflows', 'chinese-response.md');
|
||||||
|
|
||||||
let guidelinesRef = '';
|
if (!existsSync(userGuidelinesPath)) {
|
||||||
if (existsSync(projectGuidelinesPath)) {
|
return { error: 'Chinese response guidelines file not found at ~/.claude/workflows/chinese-response.md', status: 404 };
|
||||||
// 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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const guidelinesRef = '~/.claude/workflows/chinese-response.md';
|
||||||
|
|
||||||
const chineseRefLine = `- **中文回复准则**: @${guidelinesRef}`;
|
const chineseRefLine = `- **中文回复准则**: @${guidelinesRef}`;
|
||||||
const chineseRefPattern = /^- \*\*中文回复准则\*\*:.*chinese-response\.md.*$/gm;
|
const chineseRefPattern = /^- \*\*中文回复准则\*\*:.*chinese-response\.md.*$/gm;
|
||||||
|
|
||||||
@@ -922,5 +913,118 @@ export async function handleClaudeRoutes(ctx: RouteContext): Promise<boolean> {
|
|||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -384,17 +384,23 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
|
|||||||
// API: CodexLens Init (Initialize workspace index)
|
// API: CodexLens Init (Initialize workspace index)
|
||||||
if (pathname === '/api/codexlens/init' && req.method === 'POST') {
|
if (pathname === '/api/codexlens/init' && req.method === 'POST') {
|
||||||
handlePostRequest(req, res, async (body) => {
|
handlePostRequest(req, res, async (body) => {
|
||||||
const { path: projectPath } = body;
|
const { path: projectPath, indexType = 'vector' } = body;
|
||||||
const targetPath = projectPath || initialPath;
|
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
|
// Broadcast start event
|
||||||
broadcastToClients({
|
broadcastToClients({
|
||||||
type: 'CODEXLENS_INDEX_PROGRESS',
|
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 {
|
try {
|
||||||
const result = await executeCodexLens(['init', targetPath, '--json'], {
|
const result = await executeCodexLens(args, {
|
||||||
cwd: targetPath,
|
cwd: targetPath,
|
||||||
timeout: 1800000, // 30 minutes for large codebases
|
timeout: 1800000, // 30 minutes for large codebases
|
||||||
onProgress: (progress: ProgressInfo) => {
|
onProgress: (progress: ProgressInfo) => {
|
||||||
|
|||||||
@@ -177,11 +177,20 @@ function renderIndexCard() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex justify-between items-center gap-2">
|
<div class="mt-4 flex justify-between items-center gap-2">
|
||||||
<button onclick="initCodexLensIndex()"
|
<div class="flex items-center gap-2">
|
||||||
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">
|
<button onclick="initCodexLensIndex('vector')"
|
||||||
<i data-lucide="database" class="w-3.5 h-3.5"></i>
|
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"
|
||||||
${t('index.initCurrent') || 'Init Current Project'}
|
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>
|
||||||
|
<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()"
|
<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">
|
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>
|
<i data-lucide="trash" class="w-3.5 h-3.5"></i>
|
||||||
|
|||||||
@@ -322,6 +322,10 @@ const i18n = {
|
|||||||
'index.cleanFailed': 'Clean failed',
|
'index.cleanFailed': 'Clean failed',
|
||||||
'index.cleanAllConfirm': 'Are you sure you want to clean ALL indexes? This cannot be undone.',
|
'index.cleanAllConfirm': 'Are you sure you want to clean ALL indexes? This cannot be undone.',
|
||||||
'index.cleanAllSuccess': 'All indexes cleaned',
|
'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 Search Configuration
|
||||||
'semantic.settings': 'Semantic Search Settings',
|
'semantic.settings': 'Semantic Search Settings',
|
||||||
@@ -343,6 +347,12 @@ const i18n = {
|
|||||||
'lang.disableSuccess': 'Chinese response disabled',
|
'lang.disableSuccess': 'Chinese response disabled',
|
||||||
'lang.enableFailed': 'Failed to enable Chinese response',
|
'lang.enableFailed': 'Failed to enable Chinese response',
|
||||||
'lang.disableFailed': 'Failed to disable 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.promptFormat': 'Prompt Format',
|
||||||
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
|
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
|
||||||
'cli.storageBackend': 'Storage Backend',
|
'cli.storageBackend': 'Storage Backend',
|
||||||
@@ -1597,6 +1607,10 @@ const i18n = {
|
|||||||
'index.cleanFailed': '清理失败',
|
'index.cleanFailed': '清理失败',
|
||||||
'index.cleanAllConfirm': '确定要清理所有索引吗?此操作无法撤销。',
|
'index.cleanAllConfirm': '确定要清理所有索引吗?此操作无法撤销。',
|
||||||
'index.cleanAllSuccess': '所有索引已清理',
|
'index.cleanAllSuccess': '所有索引已清理',
|
||||||
|
'index.vectorIndex': '向量索引',
|
||||||
|
'index.normalIndex': '全文索引',
|
||||||
|
'index.vectorDesc': '语义搜索(含嵌入向量)',
|
||||||
|
'index.normalDesc': '快速全文搜索',
|
||||||
|
|
||||||
// Semantic Search 配置
|
// Semantic Search 配置
|
||||||
'semantic.settings': '语义搜索设置',
|
'semantic.settings': '语义搜索设置',
|
||||||
@@ -1618,6 +1632,12 @@ const i18n = {
|
|||||||
'lang.disableSuccess': '中文回复已禁用',
|
'lang.disableSuccess': '中文回复已禁用',
|
||||||
'lang.enableFailed': '启用中文回复失败',
|
'lang.enableFailed': '启用中文回复失败',
|
||||||
'lang.disableFailed': '禁用中文回复失败',
|
'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.promptFormat': '提示词格式',
|
||||||
'cli.promptFormatDesc': '多轮对话拼接格式',
|
'cli.promptFormatDesc': '多轮对话拼接格式',
|
||||||
'cli.storageBackend': '存储后端',
|
'cli.storageBackend': '存储后端',
|
||||||
|
|||||||
@@ -512,6 +512,8 @@ function renderCcwSection() {
|
|||||||
// ========== Language Settings State ==========
|
// ========== Language Settings State ==========
|
||||||
var chineseResponseEnabled = false;
|
var chineseResponseEnabled = false;
|
||||||
var chineseResponseLoading = false;
|
var chineseResponseLoading = false;
|
||||||
|
var windowsPlatformEnabled = false;
|
||||||
|
var windowsPlatformLoading = false;
|
||||||
|
|
||||||
// ========== Language Settings Section ==========
|
// ========== Language Settings Section ==========
|
||||||
async function loadLanguageSettings() {
|
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) {
|
async function toggleChineseResponse(enabled) {
|
||||||
if (chineseResponseLoading) return;
|
if (chineseResponseLoading) return;
|
||||||
chineseResponseLoading = true;
|
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() {
|
async function renderLanguageSettingsSection() {
|
||||||
var container = document.getElementById('language-settings-section');
|
var container = document.getElementById('language-settings-section');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -568,13 +616,16 @@ async function renderLanguageSettingsSection() {
|
|||||||
if (!chineseResponseEnabled && !chineseResponseLoading) {
|
if (!chineseResponseEnabled && !chineseResponseLoading) {
|
||||||
await loadLanguageSettings();
|
await loadLanguageSettings();
|
||||||
}
|
}
|
||||||
|
if (!windowsPlatformEnabled && !windowsPlatformLoading) {
|
||||||
|
await loadWindowsPlatformSettings();
|
||||||
|
}
|
||||||
|
|
||||||
var settingsHtml = '<div class="section-header">' +
|
var settingsHtml = '<div class="section-header">' +
|
||||||
'<div class="section-header-left">' +
|
'<div class="section-header-left">' +
|
||||||
'<h3><i data-lucide="languages" class="w-4 h-4"></i> ' + t('lang.settings') + '</h3>' +
|
'<h3><i data-lucide="languages" class="w-4 h-4"></i> ' + t('lang.settings') + '</h3>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</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">' +
|
'<div class="cli-setting-item">' +
|
||||||
'<label class="cli-setting-label">' +
|
'<label class="cli-setting-label">' +
|
||||||
'<i data-lucide="message-square-text" class="w-3 h-3"></i>' +
|
'<i data-lucide="message-square-text" class="w-3 h-3"></i>' +
|
||||||
@@ -591,6 +642,22 @@ async function renderLanguageSettingsSection() {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<p class="cli-setting-desc">' + t('lang.chineseDesc') + '</p>' +
|
'<p class="cli-setting-desc">' + t('lang.chineseDesc') + '</p>' +
|
||||||
'</div>' +
|
'</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>';
|
'</div>';
|
||||||
|
|
||||||
container.innerHTML = settingsHtml;
|
container.innerHTML = settingsHtml;
|
||||||
|
|||||||
@@ -554,8 +554,11 @@ async function deleteModel(profile) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize CodexLens index with bottom floating progress bar
|
* 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
|
// Remove existing progress bar if any
|
||||||
closeCodexLensIndexModal();
|
closeCodexLensIndexModal();
|
||||||
|
|
||||||
@@ -563,6 +566,7 @@ function initCodexLensIndex() {
|
|||||||
var progressBar = document.createElement('div');
|
var progressBar = document.createElement('div');
|
||||||
progressBar.id = 'codexlensIndexFloating';
|
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';
|
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 =
|
progressBar.innerHTML =
|
||||||
'<div class="max-w-4xl mx-auto px-4 py-3">' +
|
'<div class="max-w-4xl mx-auto px-4 py-3">' +
|
||||||
'<div class="flex items-center justify-between gap-4">' +
|
'<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="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-1 min-w-0">' +
|
||||||
'<div class="flex items-center gap-2">' +
|
'<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>' +
|
'<span class="text-xs text-muted-foreground" id="codexlensIndexPercent">0%</span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="text-xs text-muted-foreground truncate" id="codexlensIndexStatus">' + t('codexlens.preparingIndex') + '</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);
|
document.body.appendChild(progressBar);
|
||||||
if (window.lucide) lucide.createIcons();
|
if (window.lucide) lucide.createIcons();
|
||||||
|
|
||||||
// Start indexing
|
// Start indexing with specified type
|
||||||
startCodexLensIndexing();
|
startCodexLensIndexing(indexType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the indexing process
|
* 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 statusText = document.getElementById('codexlensIndexStatus');
|
||||||
var progressBar = document.getElementById('codexlensIndexProgressBar');
|
var progressBar = document.getElementById('codexlensIndexProgressBar');
|
||||||
var percentText = document.getElementById('codexlensIndexPercent');
|
var percentText = document.getElementById('codexlensIndexPercent');
|
||||||
@@ -629,11 +635,11 @@ async function startCodexLensIndexing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[CodexLens] Starting index for:', projectPath);
|
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType);
|
||||||
var response = await fetch('/api/codexlens/init', {
|
var response = await fetch('/api/codexlens/init', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ path: projectPath })
|
body: JSON.stringify({ path: projectPath, indexType: indexType })
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = await response.json();
|
var result = await response.json();
|
||||||
|
|||||||
@@ -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> {
|
async function executeInitAction(params: Params): Promise<SearchResult> {
|
||||||
const { path = '.', languages } = params;
|
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) {
|
if (languages && languages.length > 0) {
|
||||||
args.push('--languages', languages.join(','));
|
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
|
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 {
|
return {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
error: result.error,
|
error: result.error,
|
||||||
message: result.success
|
message: successMessage,
|
||||||
? `CodexLens index created successfully for ${path}`
|
|
||||||
: undefined,
|
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -923,7 +927,7 @@ export const schema: ToolSchema = {
|
|||||||
|
|
||||||
**Quick Start:**
|
**Quick Start:**
|
||||||
smart_search(query="authentication logic") # Auto mode (intelligent routing)
|
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
|
smart_search(action="status") # Check index status
|
||||||
|
|
||||||
**Five Modes:**
|
**Five Modes:**
|
||||||
@@ -934,7 +938,7 @@ export const schema: ToolSchema = {
|
|||||||
|
|
||||||
2. hybrid: CodexLens RRF fusion (exact + fuzzy + vector)
|
2. hybrid: CodexLens RRF fusion (exact + fuzzy + vector)
|
||||||
- Best quality, semantic understanding
|
- Best quality, semantic understanding
|
||||||
- Requires index with embeddings
|
- Requires index with embeddings (create via "ccw view" dashboard)
|
||||||
|
|
||||||
3. exact: CodexLens FTS (full-text search)
|
3. exact: CodexLens FTS (full-text search)
|
||||||
- Precise keyword matching
|
- Precise keyword matching
|
||||||
@@ -950,21 +954,21 @@ export const schema: ToolSchema = {
|
|||||||
|
|
||||||
**Actions:**
|
**Actions:**
|
||||||
- search (default): Intelligent search with auto routing
|
- 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
|
- status: Check index and embedding availability
|
||||||
- search_files: Return file paths only
|
- search_files: Return file paths only
|
||||||
|
|
||||||
**Workflow:**
|
**Workflow:**
|
||||||
1. Run action="init" to create index
|
1. Run action="init" to create FTS index (fast)
|
||||||
2. Use auto mode - it routes to hybrid for NL queries, exact for simple queries
|
2. For semantic search: create vector index via "ccw view" dashboard or "codexlens init <path>"
|
||||||
3. Use ripgrep mode for fast searches without index`,
|
3. Use auto mode - it routes to hybrid for NL queries, exact for simple queries`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
action: {
|
action: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['init', 'search', 'search_files', 'status'],
|
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',
|
default: 'search',
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -831,13 +831,13 @@ class ChainSearchEngine:
|
|||||||
r.target_qualified_name AS target_symbol,
|
r.target_qualified_name AS target_symbol,
|
||||||
r.relationship_type,
|
r.relationship_type,
|
||||||
r.source_line,
|
r.source_line,
|
||||||
f.path AS source_file,
|
f.full_path AS source_file,
|
||||||
r.target_file
|
r.target_file
|
||||||
FROM code_relationships r
|
FROM code_relationships r
|
||||||
JOIN symbols s ON r.source_symbol_id = s.id
|
JOIN symbols s ON r.source_symbol_id = s.id
|
||||||
JOIN files f ON s.file_id = f.id
|
JOIN files f ON s.file_id = f.id
|
||||||
WHERE s.name = ? AND r.relationship_type = 'call'
|
WHERE s.name = ? AND r.relationship_type = 'call'
|
||||||
ORDER BY f.path, r.source_line
|
ORDER BY f.full_path, r.source_line
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
""",
|
""",
|
||||||
(source_symbol,)
|
(source_symbol,)
|
||||||
@@ -928,7 +928,7 @@ class ChainSearchEngine:
|
|||||||
r.target_qualified_name,
|
r.target_qualified_name,
|
||||||
r.relationship_type,
|
r.relationship_type,
|
||||||
r.source_line,
|
r.source_line,
|
||||||
f.path AS source_file,
|
f.full_path AS source_file,
|
||||||
r.target_file
|
r.target_file
|
||||||
FROM code_relationships r
|
FROM code_relationships r
|
||||||
JOIN symbols s ON r.source_symbol_id = s.id
|
JOIN symbols s ON r.source_symbol_id = s.id
|
||||||
@@ -940,7 +940,7 @@ class ChainSearchEngine:
|
|||||||
r.target_qualified_name,
|
r.target_qualified_name,
|
||||||
r.relationship_type,
|
r.relationship_type,
|
||||||
r.source_line,
|
r.source_line,
|
||||||
f.path AS source_file,
|
f.full_path AS source_file,
|
||||||
r.target_file
|
r.target_file
|
||||||
FROM code_relationships r
|
FROM code_relationships r
|
||||||
JOIN symbols s ON r.source_symbol_id = s.id
|
JOIN symbols s ON r.source_symbol_id = s.id
|
||||||
|
|||||||
@@ -434,20 +434,31 @@ class GraphAnalyzer:
|
|||||||
def _find_enclosing_symbol(self, node: TreeSitterNode, symbols: List[dict]) -> Optional[str]:
|
def _find_enclosing_symbol(self, node: TreeSitterNode, symbols: List[dict]) -> Optional[str]:
|
||||||
"""Find the enclosing function/method/class for a node.
|
"""Find the enclosing function/method/class for a node.
|
||||||
|
|
||||||
|
Returns fully qualified name (e.g., "MyClass.my_method") by traversing up
|
||||||
|
the AST tree and collecting parent class/function names.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
node: AST node to find enclosure for
|
node: AST node to find enclosure for
|
||||||
symbols: List of defined symbols
|
symbols: List of defined symbols
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Name of enclosing symbol, or None if at module level
|
Fully qualified name of enclosing symbol, or None if at module level
|
||||||
"""
|
"""
|
||||||
# Walk up the tree to find enclosing symbol
|
# Walk up the tree to find all enclosing symbols
|
||||||
|
enclosing_names = []
|
||||||
parent = node.parent
|
parent = node.parent
|
||||||
|
|
||||||
while parent is not None:
|
while parent is not None:
|
||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
if symbol["node"] == parent:
|
if symbol["node"] == parent:
|
||||||
return symbol["name"]
|
# Prepend to maintain order (innermost to outermost)
|
||||||
|
enclosing_names.insert(0, symbol["name"])
|
||||||
|
break
|
||||||
parent = parent.parent
|
parent = parent.parent
|
||||||
|
|
||||||
|
# Return fully qualified name or None if at module level
|
||||||
|
if enclosing_names:
|
||||||
|
return ".".join(enclosing_names)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _extract_call_target(self, source_bytes: bytes, node: TreeSitterNode) -> Optional[str]:
|
def _extract_call_target(self, source_bytes: bytes, node: TreeSitterNode) -> Optional[str]:
|
||||||
|
|||||||
@@ -1226,17 +1226,14 @@ class DirIndexStore:
|
|||||||
query: str,
|
query: str,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
enhance_query: bool = False,
|
enhance_query: bool = False,
|
||||||
return_full_content: bool = True,
|
return_full_content: bool = False,
|
||||||
context_lines: int = 10,
|
context_lines: int = 10,
|
||||||
) -> List[SearchResult]:
|
) -> List[SearchResult]:
|
||||||
"""Full-text search in current directory files with complete method blocks.
|
"""Full-text search in current directory files.
|
||||||
|
|
||||||
Uses files_fts_exact (unicode61 tokenizer) for exact token matching.
|
Uses files_fts_exact (unicode61 tokenizer) for exact token matching.
|
||||||
For fuzzy/substring search, use search_fts_fuzzy() instead.
|
For fuzzy/substring search, use search_fts_fuzzy() instead.
|
||||||
|
|
||||||
Returns complete code blocks (functions/methods/classes) containing the match,
|
|
||||||
rather than just a short snippet.
|
|
||||||
|
|
||||||
Best Practice (from industry analysis of Codanna/Code-Index-MCP):
|
Best Practice (from industry analysis of Codanna/Code-Index-MCP):
|
||||||
- Default: Respects exact user input without modification
|
- Default: Respects exact user input without modification
|
||||||
- Users can manually add wildcards (e.g., "loadPack*") for prefix matching
|
- Users can manually add wildcards (e.g., "loadPack*") for prefix matching
|
||||||
@@ -1248,11 +1245,12 @@ class DirIndexStore:
|
|||||||
limit: Maximum results to return
|
limit: Maximum results to return
|
||||||
enhance_query: If True, automatically add prefix wildcards for simple queries.
|
enhance_query: If True, automatically add prefix wildcards for simple queries.
|
||||||
Default False to respect exact user input.
|
Default False to respect exact user input.
|
||||||
return_full_content: If True, include full code block in content field
|
return_full_content: If True, include full code block in content field.
|
||||||
|
Default False for fast location-only results.
|
||||||
context_lines: Lines of context when no symbol contains the match
|
context_lines: Lines of context when no symbol contains the match
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of SearchResult objects with complete code blocks
|
List of SearchResult objects (location-only by default, with content if requested)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
StorageError: If FTS search fails
|
StorageError: If FTS search fails
|
||||||
@@ -1263,8 +1261,39 @@ class DirIndexStore:
|
|||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
|
|
||||||
|
# Fast path: location-only results (no content processing)
|
||||||
|
if not return_full_content:
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT rowid, full_path, bm25(files_fts_exact) AS rank,
|
||||||
|
snippet(files_fts_exact, 2, '', '', '...', 30) AS excerpt
|
||||||
|
FROM files_fts_exact
|
||||||
|
WHERE files_fts_exact MATCH ?
|
||||||
|
ORDER BY rank
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(final_query, limit),
|
||||||
|
).fetchall()
|
||||||
|
except sqlite3.DatabaseError as exc:
|
||||||
|
raise StorageError(f"FTS search failed: {exc}") from exc
|
||||||
|
|
||||||
|
results: List[SearchResult] = []
|
||||||
|
for row in rows:
|
||||||
|
rank = float(row["rank"]) if row["rank"] is not None else 0.0
|
||||||
|
score = abs(rank) if rank < 0 else 0.0
|
||||||
|
results.append(
|
||||||
|
SearchResult(
|
||||||
|
path=row["full_path"],
|
||||||
|
score=score,
|
||||||
|
excerpt=row["excerpt"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Full content path: fetch content and find containing symbols
|
||||||
try:
|
try:
|
||||||
# Join with files table to get content and file_id
|
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT f.id AS file_id, f.full_path, f.content,
|
SELECT f.id AS file_id, f.full_path, f.content,
|
||||||
@@ -1319,7 +1348,7 @@ class DirIndexStore:
|
|||||||
path=file_path,
|
path=file_path,
|
||||||
score=score,
|
score=score,
|
||||||
excerpt=excerpt,
|
excerpt=excerpt,
|
||||||
content=block_content if return_full_content else None,
|
content=block_content,
|
||||||
start_line=start_line,
|
start_line=start_line,
|
||||||
end_line=end_line,
|
end_line=end_line,
|
||||||
symbol_name=symbol_name,
|
symbol_name=symbol_name,
|
||||||
@@ -1332,31 +1361,59 @@ class DirIndexStore:
|
|||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
return_full_content: bool = True,
|
return_full_content: bool = False,
|
||||||
context_lines: int = 10,
|
context_lines: int = 10,
|
||||||
) -> List[SearchResult]:
|
) -> List[SearchResult]:
|
||||||
"""Full-text search using exact token matching with complete method blocks.
|
"""Full-text search using exact token matching.
|
||||||
|
|
||||||
Returns complete code blocks (functions/methods/classes) containing the match,
|
|
||||||
rather than just a short snippet. If no symbol contains the match, returns
|
|
||||||
context lines around the match.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: FTS5 query string
|
query: FTS5 query string
|
||||||
limit: Maximum results to return
|
limit: Maximum results to return
|
||||||
return_full_content: If True, include full code block in content field
|
return_full_content: If True, include full code block in content field.
|
||||||
|
Default False for fast location-only results.
|
||||||
context_lines: Lines of context when no symbol contains the match
|
context_lines: Lines of context when no symbol contains the match
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of SearchResult objects with complete code blocks
|
List of SearchResult objects (location-only by default, with content if requested)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
StorageError: If FTS search fails
|
StorageError: If FTS search fails
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
|
|
||||||
|
# Fast path: location-only results (no content processing)
|
||||||
|
if not return_full_content:
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT rowid, full_path, bm25(files_fts_exact) AS rank,
|
||||||
|
snippet(files_fts_exact, 2, '', '', '...', 30) AS excerpt
|
||||||
|
FROM files_fts_exact
|
||||||
|
WHERE files_fts_exact MATCH ?
|
||||||
|
ORDER BY rank
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(query, limit),
|
||||||
|
).fetchall()
|
||||||
|
except sqlite3.DatabaseError as exc:
|
||||||
|
raise StorageError(f"FTS exact search failed: {exc}") from exc
|
||||||
|
|
||||||
|
results: List[SearchResult] = []
|
||||||
|
for row in rows:
|
||||||
|
rank = float(row["rank"]) if row["rank"] is not None else 0.0
|
||||||
|
score = abs(rank) if rank < 0 else 0.0
|
||||||
|
results.append(
|
||||||
|
SearchResult(
|
||||||
|
path=row["full_path"],
|
||||||
|
score=score,
|
||||||
|
excerpt=row["excerpt"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Full content path: fetch content and find containing symbols
|
||||||
try:
|
try:
|
||||||
# Join with files table to get content and file_id
|
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT f.id AS file_id, f.full_path, f.content,
|
SELECT f.id AS file_id, f.full_path, f.content,
|
||||||
@@ -1411,7 +1468,7 @@ class DirIndexStore:
|
|||||||
path=file_path,
|
path=file_path,
|
||||||
score=score,
|
score=score,
|
||||||
excerpt=excerpt,
|
excerpt=excerpt,
|
||||||
content=block_content if return_full_content else None,
|
content=block_content,
|
||||||
start_line=start_line,
|
start_line=start_line,
|
||||||
end_line=end_line,
|
end_line=end_line,
|
||||||
symbol_name=symbol_name,
|
symbol_name=symbol_name,
|
||||||
@@ -1424,31 +1481,59 @@ class DirIndexStore:
|
|||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
return_full_content: bool = True,
|
return_full_content: bool = False,
|
||||||
context_lines: int = 10,
|
context_lines: int = 10,
|
||||||
) -> List[SearchResult]:
|
) -> List[SearchResult]:
|
||||||
"""Full-text search using fuzzy/substring matching with complete method blocks.
|
"""Full-text search using fuzzy/substring matching.
|
||||||
|
|
||||||
Returns complete code blocks (functions/methods/classes) containing the match,
|
|
||||||
rather than just a short snippet. If no symbol contains the match, returns
|
|
||||||
context lines around the match.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: FTS5 query string
|
query: FTS5 query string
|
||||||
limit: Maximum results to return
|
limit: Maximum results to return
|
||||||
return_full_content: If True, include full code block in content field
|
return_full_content: If True, include full code block in content field.
|
||||||
|
Default False for fast location-only results.
|
||||||
context_lines: Lines of context when no symbol contains the match
|
context_lines: Lines of context when no symbol contains the match
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of SearchResult objects with complete code blocks
|
List of SearchResult objects (location-only by default, with content if requested)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
StorageError: If FTS search fails
|
StorageError: If FTS search fails
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
|
|
||||||
|
# Fast path: location-only results (no content processing)
|
||||||
|
if not return_full_content:
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT rowid, full_path, bm25(files_fts_fuzzy) AS rank,
|
||||||
|
snippet(files_fts_fuzzy, 2, '', '', '...', 30) AS excerpt
|
||||||
|
FROM files_fts_fuzzy
|
||||||
|
WHERE files_fts_fuzzy MATCH ?
|
||||||
|
ORDER BY rank
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(query, limit),
|
||||||
|
).fetchall()
|
||||||
|
except sqlite3.DatabaseError as exc:
|
||||||
|
raise StorageError(f"FTS fuzzy search failed: {exc}") from exc
|
||||||
|
|
||||||
|
results: List[SearchResult] = []
|
||||||
|
for row in rows:
|
||||||
|
rank = float(row["rank"]) if row["rank"] is not None else 0.0
|
||||||
|
score = abs(rank) if rank < 0 else 0.0
|
||||||
|
results.append(
|
||||||
|
SearchResult(
|
||||||
|
path=row["full_path"],
|
||||||
|
score=score,
|
||||||
|
excerpt=row["excerpt"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Full content path: fetch content and find containing symbols
|
||||||
try:
|
try:
|
||||||
# Join with files table to get content and file_id
|
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT f.id AS file_id, f.full_path, f.content,
|
SELECT f.id AS file_id, f.full_path, f.content,
|
||||||
@@ -1503,7 +1588,7 @@ class DirIndexStore:
|
|||||||
path=file_path,
|
path=file_path,
|
||||||
score=score,
|
score=score,
|
||||||
excerpt=excerpt,
|
excerpt=excerpt,
|
||||||
content=block_content if return_full_content else None,
|
content=block_content,
|
||||||
start_line=start_line,
|
start_line=start_line,
|
||||||
end_line=end_line,
|
end_line=end_line,
|
||||||
symbol_name=symbol_name,
|
symbol_name=symbol_name,
|
||||||
|
|||||||
@@ -527,7 +527,6 @@ class IndexTreeBuilder:
|
|||||||
|
|
||||||
# Extract and store code relationships for graph visualization
|
# Extract and store code relationships for graph visualization
|
||||||
if language_id in {"python", "javascript", "typescript"}:
|
if language_id in {"python", "javascript", "typescript"}:
|
||||||
try:
|
|
||||||
graph_analyzer = GraphAnalyzer(language_id)
|
graph_analyzer = GraphAnalyzer(language_id)
|
||||||
if graph_analyzer.is_available():
|
if graph_analyzer.is_available():
|
||||||
relationships = graph_analyzer.analyze_with_symbols(
|
relationships = graph_analyzer.analyze_with_symbols(
|
||||||
@@ -535,11 +534,6 @@ class IndexTreeBuilder:
|
|||||||
)
|
)
|
||||||
if relationships:
|
if relationships:
|
||||||
store.add_relationships(file_path, relationships)
|
store.add_relationships(file_path, relationships)
|
||||||
except Exception as rel_exc:
|
|
||||||
self.logger.debug(
|
|
||||||
"Failed to extract relationships from %s: %s",
|
|
||||||
file_path, rel_exc
|
|
||||||
)
|
|
||||||
|
|
||||||
files_count += 1
|
files_count += 1
|
||||||
symbols_count += len(indexed_file.symbols)
|
symbols_count += len(indexed_file.symbols)
|
||||||
@@ -750,7 +744,6 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
|
|||||||
|
|
||||||
# Extract and store code relationships for graph visualization
|
# Extract and store code relationships for graph visualization
|
||||||
if language_id in {"python", "javascript", "typescript"}:
|
if language_id in {"python", "javascript", "typescript"}:
|
||||||
try:
|
|
||||||
graph_analyzer = GraphAnalyzer(language_id)
|
graph_analyzer = GraphAnalyzer(language_id)
|
||||||
if graph_analyzer.is_available():
|
if graph_analyzer.is_available():
|
||||||
relationships = graph_analyzer.analyze_with_symbols(
|
relationships = graph_analyzer.analyze_with_symbols(
|
||||||
@@ -758,8 +751,6 @@ def _build_dir_worker(args: tuple) -> DirBuildResult:
|
|||||||
)
|
)
|
||||||
if relationships:
|
if relationships:
|
||||||
store.add_relationships(item, relationships)
|
store.add_relationships(item, relationships)
|
||||||
except Exception:
|
|
||||||
pass # Silently skip relationship extraction errors
|
|
||||||
|
|
||||||
files_count += 1
|
files_count += 1
|
||||||
symbols_count += len(indexed_file.symbols)
|
symbols_count += len(indexed_file.symbols)
|
||||||
|
|||||||
@@ -509,13 +509,13 @@ class SQLiteStore:
|
|||||||
r.target_qualified_name,
|
r.target_qualified_name,
|
||||||
r.relationship_type,
|
r.relationship_type,
|
||||||
r.source_line,
|
r.source_line,
|
||||||
f.path AS source_file,
|
f.full_path AS source_file,
|
||||||
r.target_file
|
r.target_file
|
||||||
FROM code_relationships r
|
FROM code_relationships r
|
||||||
JOIN symbols s ON r.source_symbol_id = s.id
|
JOIN symbols s ON r.source_symbol_id = s.id
|
||||||
JOIN files f ON s.file_id = f.id
|
JOIN files f ON s.file_id = f.id
|
||||||
WHERE r.target_qualified_name = ?
|
WHERE r.target_qualified_name = ?
|
||||||
ORDER BY f.path, r.source_line
|
ORDER BY f.full_path, r.source_line
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
(target_name, limit)
|
(target_name, limit)
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ def outer():
|
|||||||
analyzer = GraphAnalyzer("python")
|
analyzer = GraphAnalyzer("python")
|
||||||
relationships = analyzer.analyze_file(code, Path("test.py"))
|
relationships = analyzer.analyze_file(code, Path("test.py"))
|
||||||
|
|
||||||
# Should find inner -> inner_helper and outer -> inner
|
# Should find outer.inner -> inner_helper and outer -> inner (with fully qualified names)
|
||||||
assert len(relationships) == 2
|
assert len(relationships) == 2
|
||||||
call_pairs = {(rel.source_symbol, rel.target_symbol) for rel in relationships}
|
call_pairs = {(rel.source_symbol, rel.target_symbol) for rel in relationships}
|
||||||
assert ("inner", "inner_helper") in call_pairs
|
assert ("outer.inner", "inner_helper") in call_pairs
|
||||||
assert ("outer", "inner") in call_pairs
|
assert ("outer", "inner") in call_pairs
|
||||||
|
|
||||||
def test_method_call_in_class(self):
|
def test_method_call_in_class(self):
|
||||||
@@ -97,10 +97,10 @@ def outer():
|
|||||||
analyzer = GraphAnalyzer("python")
|
analyzer = GraphAnalyzer("python")
|
||||||
relationships = analyzer.analyze_file(code, Path("test.py"))
|
relationships = analyzer.analyze_file(code, Path("test.py"))
|
||||||
|
|
||||||
# Should find compute -> add
|
# Should find Calculator.compute -> add (with fully qualified source)
|
||||||
assert len(relationships) == 1
|
assert len(relationships) == 1
|
||||||
rel = relationships[0]
|
rel = relationships[0]
|
||||||
assert rel.source_symbol == "compute"
|
assert rel.source_symbol == "Calculator.compute"
|
||||||
assert rel.target_symbol == "add"
|
assert rel.target_symbol == "add"
|
||||||
|
|
||||||
def test_module_level_call(self):
|
def test_module_level_call(self):
|
||||||
@@ -171,11 +171,11 @@ main()
|
|||||||
# Extract call pairs
|
# Extract call pairs
|
||||||
call_pairs = {(rel.source_symbol, rel.target_symbol) for rel in relationships}
|
call_pairs = {(rel.source_symbol, rel.target_symbol) for rel in relationships}
|
||||||
|
|
||||||
# Expected relationships
|
# Expected relationships (with fully qualified source symbols for methods)
|
||||||
expected = {
|
expected = {
|
||||||
("load", "read_file"),
|
("DataProcessor.load", "read_file"),
|
||||||
("process", "validate"),
|
("DataProcessor.process", "validate"),
|
||||||
("process", "transform"),
|
("DataProcessor.process", "transform"),
|
||||||
("main", "DataProcessor"),
|
("main", "DataProcessor"),
|
||||||
("main", "load"),
|
("main", "load"),
|
||||||
("main", "process"),
|
("main", "process"),
|
||||||
@@ -259,10 +259,10 @@ const main = () => {
|
|||||||
analyzer = GraphAnalyzer("javascript")
|
analyzer = GraphAnalyzer("javascript")
|
||||||
relationships = analyzer.analyze_file(code, Path("test.js"))
|
relationships = analyzer.analyze_file(code, Path("test.js"))
|
||||||
|
|
||||||
# Should find compute -> add
|
# Should find Calculator.compute -> add (with fully qualified source)
|
||||||
assert len(relationships) == 1
|
assert len(relationships) == 1
|
||||||
rel = relationships[0]
|
rel = relationships[0]
|
||||||
assert rel.source_symbol == "compute"
|
assert rel.source_symbol == "Calculator.compute"
|
||||||
assert rel.target_symbol == "add"
|
assert rel.target_symbol == "add"
|
||||||
|
|
||||||
def test_complex_javascript_file(self):
|
def test_complex_javascript_file(self):
|
||||||
@@ -304,11 +304,12 @@ main();
|
|||||||
# Extract call pairs
|
# Extract call pairs
|
||||||
call_pairs = {(rel.source_symbol, rel.target_symbol) for rel in relationships}
|
call_pairs = {(rel.source_symbol, rel.target_symbol) for rel in relationships}
|
||||||
|
|
||||||
# Expected relationships (note: constructor calls like "new DataProcessor()" are not tracked)
|
# Expected relationships (with fully qualified source symbols for methods)
|
||||||
|
# Note: constructor calls like "new DataProcessor()" are not tracked
|
||||||
expected = {
|
expected = {
|
||||||
("load", "readFile"),
|
("DataProcessor.load", "readFile"),
|
||||||
("process", "validate"),
|
("DataProcessor.process", "validate"),
|
||||||
("process", "transform"),
|
("DataProcessor.process", "transform"),
|
||||||
("main", "load"),
|
("main", "load"),
|
||||||
("main", "process"),
|
("main", "process"),
|
||||||
("<module>", "main"),
|
("<module>", "main"),
|
||||||
|
|||||||
Reference in New Issue
Block a user