mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat(codexlens): add CodexLens code indexing platform with incremental updates
- Add CodexLens Python package with SQLite FTS5 search and tree-sitter parsing - Implement workspace-local index storage (.codexlens/ directory) - Add incremental update CLI command for efficient file-level index refresh - Integrate CodexLens with CCW tools (codex_lens action: update) - Add CodexLens Auto-Sync hook template for automatic index updates on file changes - Add CodexLens status card in CCW Dashboard CLI Manager with install/init buttons - Add server APIs: /api/codexlens/status, /api/codexlens/bootstrap, /api/codexlens/init 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { aggregateData } from './data-aggregator.js';
|
||||
import { resolvePath, getRecentPaths, trackRecentPath, removeRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js';
|
||||
import { getCliToolsStatus, getExecutionHistory, getExecutionDetail, deleteExecution, executeCliTool } from '../tools/cli-executor.js';
|
||||
import { getAllManifests } from './manifest.js';
|
||||
import { checkVenvStatus, bootstrapVenv, executeCodexLens } from '../tools/codex-lens.js';
|
||||
|
||||
// Claude config file paths
|
||||
const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json');
|
||||
@@ -451,6 +452,57 @@ export async function startServer(options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CodexLens Status
|
||||
if (pathname === '/api/codexlens/status') {
|
||||
const status = await checkVenvStatus();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(status));
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CodexLens Bootstrap (Install)
|
||||
if (pathname === '/api/codexlens/bootstrap' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async () => {
|
||||
try {
|
||||
const result = await bootstrapVenv();
|
||||
if (result.success) {
|
||||
const status = await checkVenvStatus();
|
||||
return { success: true, message: 'CodexLens installed successfully', version: status.version };
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CodexLens Init (Initialize workspace index)
|
||||
if (pathname === '/api/codexlens/init' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { path: projectPath } = body;
|
||||
const targetPath = projectPath || initialPath;
|
||||
|
||||
try {
|
||||
const result = await executeCodexLens(['init', targetPath, '--json'], { cwd: targetPath });
|
||||
if (result.success) {
|
||||
try {
|
||||
const parsed = JSON.parse(result.output);
|
||||
return { success: true, result: parsed };
|
||||
} catch {
|
||||
return { success: true, output: result.output };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: result.error, status: 500 };
|
||||
}
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message, status: 500 };
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// API: CCW Installation Status
|
||||
if (pathname === '/api/ccw/installations') {
|
||||
const manifests = getAllManifests();
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
// ========== CLI State ==========
|
||||
let cliToolStatus = { gemini: {}, qwen: {}, codex: {} };
|
||||
let codexLensStatus = { ready: false };
|
||||
let defaultCliTool = 'gemini';
|
||||
|
||||
// ========== Initialization ==========
|
||||
function initCliStatus() {
|
||||
// Load CLI status on init
|
||||
loadCliToolStatus();
|
||||
loadCodexLensStatus();
|
||||
}
|
||||
|
||||
// ========== Data Loading ==========
|
||||
@@ -29,6 +31,23 @@ async function loadCliToolStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCodexLensStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/codexlens/status');
|
||||
if (!response.ok) throw new Error('Failed to load CodexLens status');
|
||||
const data = await response.json();
|
||||
codexLensStatus = data;
|
||||
|
||||
// Update CodexLens badge
|
||||
updateCodexLensBadge();
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CodexLens status:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Badge Update ==========
|
||||
function updateCliBadge() {
|
||||
const badge = document.getElementById('badgeCliTools');
|
||||
@@ -42,6 +61,15 @@ function updateCliBadge() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateCodexLensBadge() {
|
||||
const badge = document.getElementById('badgeCodexLens');
|
||||
if (badge) {
|
||||
badge.textContent = codexLensStatus.ready ? 'Ready' : 'Not Installed';
|
||||
badge.classList.toggle('text-success', codexLensStatus.ready);
|
||||
badge.classList.toggle('text-muted-foreground', !codexLensStatus.ready);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Rendering ==========
|
||||
function renderCliStatus() {
|
||||
const container = document.getElementById('cli-status-panel');
|
||||
@@ -75,15 +103,39 @@ function renderCliStatus() {
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// CodexLens card
|
||||
const codexLensHtml = `
|
||||
<div class="cli-tool-card tool-codexlens ${codexLensStatus.ready ? 'available' : 'unavailable'}">
|
||||
<div class="cli-tool-header">
|
||||
<span class="cli-tool-status ${codexLensStatus.ready ? 'status-available' : 'status-unavailable'}"></span>
|
||||
<span class="cli-tool-name">CodexLens</span>
|
||||
<span class="badge px-1.5 py-0.5 text-xs rounded bg-muted text-muted-foreground">Index</span>
|
||||
</div>
|
||||
<div class="cli-tool-info">
|
||||
${codexLensStatus.ready
|
||||
? `<span class="text-success">v${codexLensStatus.version || 'installed'}</span>`
|
||||
: `<span class="text-muted-foreground">Not Installed</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="cli-tool-actions flex gap-2 mt-2">
|
||||
${!codexLensStatus.ready
|
||||
? `<button class="btn-sm btn-primary" onclick="installCodexLens()">Install</button>`
|
||||
: `<button class="btn-sm btn-outline" onclick="initCodexLensIndex()">Init Index</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="cli-status-header">
|
||||
<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>
|
||||
<button class="btn-icon" onclick="loadCliToolStatus()" title="Refresh">
|
||||
<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="cli-tools-grid">
|
||||
${toolsHtml}
|
||||
${codexLensHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -99,3 +151,55 @@ function setDefaultCliTool(tool) {
|
||||
renderCliStatus();
|
||||
showRefreshToast(`Default CLI tool set to ${tool}`, 'success');
|
||||
}
|
||||
|
||||
async function refreshAllCliStatus() {
|
||||
await Promise.all([loadCliToolStatus(), loadCodexLensStatus()]);
|
||||
renderCliStatus();
|
||||
}
|
||||
|
||||
async function installCodexLens() {
|
||||
showRefreshToast('Installing CodexLens...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/codexlens/bootstrap', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showRefreshToast('CodexLens installed successfully!', 'success');
|
||||
await loadCodexLensStatus();
|
||||
renderCliStatus();
|
||||
} else {
|
||||
showRefreshToast(`Install failed: ${result.error}`, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast(`Install error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function initCodexLensIndex() {
|
||||
showRefreshToast('Initializing CodexLens index...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/codexlens/init', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: projectPath })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const data = result.result?.result || result.result || result;
|
||||
const files = data.files_indexed || 0;
|
||||
const symbols = data.symbols_indexed || 0;
|
||||
showRefreshToast(`Index created: ${files} files, ${symbols} symbols`, 'success');
|
||||
} else {
|
||||
showRefreshToast(`Init failed: ${result.error}`, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast(`Init error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,25 +13,95 @@ const HOOK_TEMPLATES = {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'curl',
|
||||
args: ['-s', '-X', 'POST', '-H', 'Content-Type: application/json', '-d', '{"type":"summary_written","filePath":"$CLAUDE_FILE_PATHS"}', 'http://localhost:3456/api/hook']
|
||||
args: ['-s', '-X', 'POST', '-H', 'Content-Type: application/json', '-d', '{"type":"summary_written","filePath":"$CLAUDE_FILE_PATHS"}', 'http://localhost:3456/api/hook'],
|
||||
description: 'Notify CCW dashboard when files are written',
|
||||
category: 'notification'
|
||||
},
|
||||
'log-tool': {
|
||||
event: 'PostToolUse',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'echo "[$(date)] Tool: $CLAUDE_TOOL_NAME, Files: $CLAUDE_FILE_PATHS" >> ~/.claude/tool-usage.log']
|
||||
args: ['-c', 'echo "[$(date)] Tool: $CLAUDE_TOOL_NAME, Files: $CLAUDE_FILE_PATHS" >> ~/.claude/tool-usage.log'],
|
||||
description: 'Log all tool executions to a file',
|
||||
category: 'logging'
|
||||
},
|
||||
'lint-check': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'bash',
|
||||
args: ['-c', 'for f in $CLAUDE_FILE_PATHS; do if [[ "$f" =~ \\.(js|ts|jsx|tsx)$ ]]; then npx eslint "$f" --fix 2>/dev/null || true; fi; done']
|
||||
args: ['-c', 'for f in $CLAUDE_FILE_PATHS; do if [[ "$f" =~ \\.(js|ts|jsx|tsx)$ ]]; then npx eslint "$f" --fix 2>/dev/null || true; fi; done'],
|
||||
description: 'Run ESLint on JavaScript/TypeScript files after write',
|
||||
category: 'quality'
|
||||
},
|
||||
'git-add': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write',
|
||||
command: 'bash',
|
||||
args: ['-c', 'for f in $CLAUDE_FILE_PATHS; do git add "$f" 2>/dev/null || true; done']
|
||||
args: ['-c', 'for f in $CLAUDE_FILE_PATHS; do git add "$f" 2>/dev/null || true; done'],
|
||||
description: 'Automatically stage written files to git',
|
||||
category: 'git'
|
||||
},
|
||||
'codexlens-update': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'bash',
|
||||
args: ['-c', 'if [ -d ".codexlens" ] && [ -n "$CLAUDE_FILE_PATHS" ]; then python -m codexlens update $CLAUDE_FILE_PATHS --json 2>/dev/null || ~/.codexlens/venv/bin/python -m codexlens update $CLAUDE_FILE_PATHS --json 2>/dev/null || true; fi'],
|
||||
description: 'Auto-update code index when files are written or edited',
|
||||
category: 'indexing'
|
||||
},
|
||||
'memory-update-related': {
|
||||
event: 'Stop',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec update_module_claude \'{"strategy":"related","tool":"gemini"}\''],
|
||||
description: 'Update CLAUDE.md for changed modules when session ends',
|
||||
category: 'memory',
|
||||
configurable: true,
|
||||
config: {
|
||||
tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' },
|
||||
strategy: { type: 'select', options: ['related', 'single-layer'], default: 'related', label: 'Strategy' }
|
||||
}
|
||||
},
|
||||
'memory-update-periodic': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'bash',
|
||||
args: ['-c', 'INTERVAL=300; LAST_FILE=~/.claude/.last_memory_update; NOW=$(date +%s); LAST=0; [ -f "$LAST_FILE" ] && LAST=$(cat "$LAST_FILE"); if [ $((NOW - LAST)) -ge $INTERVAL ]; then echo $NOW > "$LAST_FILE"; ccw tool exec update_module_claude \'{"strategy":"related","tool":"gemini"}\' & fi'],
|
||||
description: 'Periodically update CLAUDE.md (default: 5 min interval)',
|
||||
category: 'memory',
|
||||
configurable: true,
|
||||
config: {
|
||||
tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' },
|
||||
interval: { type: 'number', default: 300, min: 60, max: 3600, label: 'Interval (seconds)', step: 60 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ========== Wizard Templates (Special Category) ==========
|
||||
const WIZARD_TEMPLATES = {
|
||||
'memory-update': {
|
||||
name: 'Memory Update Hook',
|
||||
description: 'Automatically update CLAUDE.md documentation based on code changes',
|
||||
icon: 'brain',
|
||||
options: [
|
||||
{
|
||||
id: 'on-stop',
|
||||
name: 'On Session End',
|
||||
description: 'Update documentation when Claude session ends',
|
||||
templateId: 'memory-update-related'
|
||||
},
|
||||
{
|
||||
id: 'periodic',
|
||||
name: 'Periodic Update',
|
||||
description: 'Update documentation at regular intervals during session',
|
||||
templateId: 'memory-update-periodic'
|
||||
}
|
||||
],
|
||||
configFields: [
|
||||
{ key: 'tool', type: 'select', label: 'CLI Tool', options: ['gemini', 'qwen', 'codex'], default: 'gemini', description: 'Tool for documentation generation' },
|
||||
{ key: 'interval', type: 'number', label: 'Interval (seconds)', default: 300, min: 60, max: 3600, step: 60, showFor: ['periodic'], description: 'Time between updates' },
|
||||
{ key: 'strategy', type: 'select', label: 'Update Strategy', options: ['related', 'single-layer'], default: 'related', description: 'Related: changed modules, Single-layer: current directory' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ async function renderHookManager() {
|
||||
</div>
|
||||
|
||||
<div class="hook-templates-grid grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
${renderQuickInstallCard('codexlens-update', 'CodexLens Auto-Sync', 'Auto-update code index when files are written or edited', 'PostToolUse', 'Write|Edit')}
|
||||
${renderQuickInstallCard('ccw-notify', 'CCW Dashboard Notify', 'Notify CCW dashboard when files are written', 'PostToolUse', 'Write')}
|
||||
${renderQuickInstallCard('log-tool', 'Tool Usage Logger', 'Log all tool executions to a file', 'PostToolUse', 'All')}
|
||||
${renderQuickInstallCard('lint-check', 'Auto Lint Check', 'Run ESLint on JavaScript/TypeScript files after write', 'PostToolUse', 'Write')}
|
||||
|
||||
474
ccw/src/tools/codex-lens.js
Normal file
474
ccw/src/tools/codex-lens.js
Normal file
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* CodexLens Tool - Bridge between CCW and CodexLens Python package
|
||||
* Provides code indexing and semantic search via spawned Python process
|
||||
*
|
||||
* Features:
|
||||
* - Automatic venv bootstrap at ~/.codexlens/venv
|
||||
* - JSON protocol communication
|
||||
* - Symbol extraction and semantic search
|
||||
* - FTS5 full-text search
|
||||
*/
|
||||
|
||||
import { spawn, execSync } from 'child_process';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Get directory of this module
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// CodexLens configuration
|
||||
const CODEXLENS_DATA_DIR = join(homedir(), '.codexlens');
|
||||
const CODEXLENS_VENV = join(CODEXLENS_DATA_DIR, 'venv');
|
||||
const VENV_PYTHON = process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'python.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'python');
|
||||
|
||||
// Bootstrap status cache
|
||||
let bootstrapChecked = false;
|
||||
let bootstrapReady = false;
|
||||
|
||||
/**
|
||||
* Detect available Python 3 executable
|
||||
* @returns {string} - Python executable command
|
||||
*/
|
||||
function getSystemPython() {
|
||||
const commands = process.platform === 'win32'
|
||||
? ['python', 'py', 'python3']
|
||||
: ['python3', 'python'];
|
||||
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version 2>&1`, { encoding: 'utf8' });
|
||||
if (version.includes('Python 3')) {
|
||||
return cmd;
|
||||
}
|
||||
} catch {
|
||||
// Try next command
|
||||
}
|
||||
}
|
||||
throw new Error('Python 3 not found. Please install Python 3 and ensure it is in PATH.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if CodexLens venv exists and has required packages
|
||||
* @returns {Promise<{ready: boolean, error?: string}>}
|
||||
*/
|
||||
async function checkVenvStatus() {
|
||||
// Check venv exists
|
||||
if (!existsSync(CODEXLENS_VENV)) {
|
||||
return { ready: false, error: 'Venv not found' };
|
||||
}
|
||||
|
||||
// Check python executable exists
|
||||
if (!existsSync(VENV_PYTHON)) {
|
||||
return { ready: false, error: 'Python executable not found in venv' };
|
||||
}
|
||||
|
||||
// Check codexlens is importable
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(VENV_PYTHON, ['-c', 'import codexlens; print(codexlens.__version__)'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ ready: true, version: stdout.trim() });
|
||||
} else {
|
||||
resolve({ ready: false, error: `CodexLens not installed: ${stderr}` });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ ready: false, error: `Failed to check venv: ${err.message}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap CodexLens venv with required packages
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
*/
|
||||
async function bootstrapVenv() {
|
||||
// Ensure data directory exists
|
||||
if (!existsSync(CODEXLENS_DATA_DIR)) {
|
||||
mkdirSync(CODEXLENS_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Create venv if not exists
|
||||
if (!existsSync(CODEXLENS_VENV)) {
|
||||
try {
|
||||
console.log('[CodexLens] Creating virtual environment...');
|
||||
const pythonCmd = getSystemPython();
|
||||
execSync(`${pythonCmd} -m venv "${CODEXLENS_VENV}"`, { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
return { success: false, error: `Failed to create venv: ${err.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Install codexlens with semantic extras
|
||||
try {
|
||||
console.log('[CodexLens] Installing codexlens package...');
|
||||
const pipPath = process.platform === 'win32'
|
||||
? join(CODEXLENS_VENV, 'Scripts', 'pip.exe')
|
||||
: join(CODEXLENS_VENV, 'bin', 'pip');
|
||||
|
||||
// Try multiple local paths, then fall back to PyPI
|
||||
const possiblePaths = [
|
||||
join(process.cwd(), 'codex-lens'),
|
||||
join(__dirname, '..', '..', '..', 'codex-lens'), // ccw/src/tools -> project root
|
||||
join(homedir(), 'codex-lens'),
|
||||
];
|
||||
|
||||
let installed = false;
|
||||
for (const localPath of possiblePaths) {
|
||||
if (existsSync(join(localPath, 'pyproject.toml'))) {
|
||||
console.log(`[CodexLens] Installing from local path: ${localPath}`);
|
||||
execSync(`"${pipPath}" install -e "${localPath}"`, { stdio: 'inherit' });
|
||||
installed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
console.log('[CodexLens] Installing from PyPI...');
|
||||
execSync(`"${pipPath}" install codexlens`, { stdio: 'inherit' });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: `Failed to install codexlens: ${err.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure CodexLens is ready to use
|
||||
* @returns {Promise<{ready: boolean, error?: string}>}
|
||||
*/
|
||||
async function ensureReady() {
|
||||
// Use cached result if already checked
|
||||
if (bootstrapChecked && bootstrapReady) {
|
||||
return { ready: true };
|
||||
}
|
||||
|
||||
// Check current status
|
||||
const status = await checkVenvStatus();
|
||||
if (status.ready) {
|
||||
bootstrapChecked = true;
|
||||
bootstrapReady = true;
|
||||
return { ready: true, version: status.version };
|
||||
}
|
||||
|
||||
// Attempt bootstrap
|
||||
const bootstrap = await bootstrapVenv();
|
||||
if (!bootstrap.success) {
|
||||
return { ready: false, error: bootstrap.error };
|
||||
}
|
||||
|
||||
// Verify after bootstrap
|
||||
const recheck = await checkVenvStatus();
|
||||
bootstrapChecked = true;
|
||||
bootstrapReady = recheck.ready;
|
||||
|
||||
return recheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute CodexLens CLI command
|
||||
* @param {string[]} args - CLI arguments
|
||||
* @param {Object} options - Execution options
|
||||
* @returns {Promise<{success: boolean, output?: string, error?: string}>}
|
||||
*/
|
||||
async function executeCodexLens(args, options = {}) {
|
||||
const { timeout = 60000, cwd = process.cwd() } = options;
|
||||
|
||||
// Ensure ready
|
||||
const readyStatus = await ensureReady();
|
||||
if (!readyStatus.ready) {
|
||||
return { success: false, error: readyStatus.error };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(VENV_PYTHON, ['-m', 'codexlens', ...args], {
|
||||
cwd,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timedOut = false;
|
||||
|
||||
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGTERM');
|
||||
}, timeout);
|
||||
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (timedOut) {
|
||||
resolve({ success: false, error: 'Command timed out' });
|
||||
} else if (code === 0) {
|
||||
resolve({ success: true, output: stdout.trim() });
|
||||
} else {
|
||||
resolve({ success: false, error: stderr || `Exit code: ${code}` });
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve({ success: false, error: `Spawn failed: ${err.message}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize CodexLens index for a directory
|
||||
* @param {Object} params - Parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function initIndex(params) {
|
||||
const { path = '.', languages } = params;
|
||||
|
||||
const args = ['init', path];
|
||||
if (languages && languages.length > 0) {
|
||||
args.push('--languages', languages.join(','));
|
||||
}
|
||||
|
||||
return executeCodexLens(args, { cwd: path });
|
||||
}
|
||||
|
||||
/**
|
||||
* Search code using CodexLens
|
||||
* @param {Object} params - Search parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function searchCode(params) {
|
||||
const { query, path = '.', mode = 'text', limit = 20 } = params;
|
||||
|
||||
const args = ['search', query, '--limit', limit.toString(), '--json'];
|
||||
|
||||
// Note: semantic mode requires semantic extras to be installed
|
||||
// Currently not exposed via CLI flag, uses standard FTS search
|
||||
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (result.success) {
|
||||
try {
|
||||
result.results = JSON.parse(result.output);
|
||||
delete result.output;
|
||||
} catch {
|
||||
// Keep raw output if JSON parse fails
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract symbols from a file
|
||||
* @param {Object} params - Parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function extractSymbols(params) {
|
||||
const { file } = params;
|
||||
|
||||
const args = ['symbol', file, '--json'];
|
||||
|
||||
const result = await executeCodexLens(args);
|
||||
|
||||
if (result.success) {
|
||||
try {
|
||||
result.symbols = JSON.parse(result.output);
|
||||
delete result.output;
|
||||
} catch {
|
||||
// Keep raw output if JSON parse fails
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get index status
|
||||
* @param {Object} params - Parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getStatus(params) {
|
||||
const { path = '.' } = params;
|
||||
|
||||
const args = ['status', '--json'];
|
||||
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (result.success) {
|
||||
try {
|
||||
result.status = JSON.parse(result.output);
|
||||
delete result.output;
|
||||
} catch {
|
||||
// Keep raw output if JSON parse fails
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update specific files in the index
|
||||
* @param {Object} params - Parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function updateFiles(params) {
|
||||
const { files, path = '.' } = params;
|
||||
|
||||
if (!files || !Array.isArray(files) || files.length === 0) {
|
||||
return { success: false, error: 'files parameter is required and must be a non-empty array' };
|
||||
}
|
||||
|
||||
const args = ['update', ...files, '--json'];
|
||||
|
||||
const result = await executeCodexLens(args, { cwd: path });
|
||||
|
||||
if (result.success) {
|
||||
try {
|
||||
result.updateResult = JSON.parse(result.output);
|
||||
delete result.output;
|
||||
} catch {
|
||||
// Keep raw output if JSON parse fails
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execute function - routes to appropriate handler
|
||||
* @param {Object} params - Execution parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function execute(params) {
|
||||
const { action, ...rest } = params;
|
||||
|
||||
switch (action) {
|
||||
case 'init':
|
||||
return initIndex(rest);
|
||||
|
||||
case 'search':
|
||||
return searchCode(rest);
|
||||
|
||||
case 'symbol':
|
||||
return extractSymbols(rest);
|
||||
|
||||
case 'status':
|
||||
return getStatus(rest);
|
||||
|
||||
case 'update':
|
||||
return updateFiles(rest);
|
||||
|
||||
case 'bootstrap':
|
||||
// Force re-bootstrap
|
||||
bootstrapChecked = false;
|
||||
bootstrapReady = false;
|
||||
const bootstrapResult = await bootstrapVenv();
|
||||
return bootstrapResult.success
|
||||
? { success: true, message: 'CodexLens bootstrapped successfully' }
|
||||
: { success: false, error: bootstrapResult.error };
|
||||
|
||||
case 'check':
|
||||
// Check venv status
|
||||
return checkVenvStatus();
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}. Valid actions: init, search, symbol, status, update, bootstrap, check`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CodexLens Tool Definition
|
||||
*/
|
||||
export const codexLensTool = {
|
||||
name: 'codex_lens',
|
||||
description: `Code indexing and semantic search via CodexLens Python package.
|
||||
|
||||
Actions:
|
||||
- init: Initialize index for a directory
|
||||
- search: Search code (text or semantic mode)
|
||||
- symbol: Extract symbols from a file
|
||||
- status: Get index status
|
||||
- update: Incrementally update specific files (add/modify/remove)
|
||||
- bootstrap: Force re-install CodexLens venv
|
||||
- check: Check venv readiness
|
||||
|
||||
Features:
|
||||
- Automatic venv bootstrap at ~/.codexlens/venv
|
||||
- SQLite FTS5 full-text search
|
||||
- Tree-sitter symbol extraction
|
||||
- Incremental updates for changed files
|
||||
- Optional semantic search with embeddings`,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['init', 'search', 'symbol', 'status', 'update', 'bootstrap', 'check'],
|
||||
description: 'Action to perform'
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: 'Target path (for init, search, status, update)'
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (for search action)'
|
||||
},
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['text', 'semantic'],
|
||||
description: 'Search mode (default: text)',
|
||||
default: 'text'
|
||||
},
|
||||
file: {
|
||||
type: 'string',
|
||||
description: 'File path (for symbol action)'
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'File paths to update (for update action)'
|
||||
},
|
||||
languages: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Languages to index (for init action)'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum results (for search action)',
|
||||
default: 20
|
||||
},
|
||||
format: {
|
||||
type: 'string',
|
||||
enum: ['json', 'table', 'plain'],
|
||||
description: 'Output format',
|
||||
default: 'json'
|
||||
}
|
||||
},
|
||||
required: ['action']
|
||||
},
|
||||
execute
|
||||
};
|
||||
|
||||
// Export for direct usage
|
||||
export { ensureReady, executeCodexLens, checkVenvStatus, bootstrapVenv };
|
||||
@@ -18,6 +18,7 @@ import { convertTokensToCssTool } from './convert-tokens-to-css.js';
|
||||
import { sessionManagerTool } from './session-manager.js';
|
||||
import { cliExecutorTool } from './cli-executor.js';
|
||||
import { smartSearchTool } from './smart-search.js';
|
||||
import { codexLensTool } from './codex-lens.js';
|
||||
|
||||
// Tool registry - add new tools here
|
||||
const tools = new Map();
|
||||
@@ -264,6 +265,7 @@ registerTool(convertTokensToCssTool);
|
||||
registerTool(sessionManagerTool);
|
||||
registerTool(cliExecutorTool);
|
||||
registerTool(smartSearchTool);
|
||||
registerTool(codexLensTool);
|
||||
|
||||
// Export for external tool registration
|
||||
export { registerTool };
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { spawn, execSync } from 'child_process';
|
||||
import { existsSync, readdirSync, statSync } from 'fs';
|
||||
import { join, resolve, isAbsolute } from 'path';
|
||||
import { ensureReady as ensureCodexLensReady, executeCodexLens } from './codex-lens.js';
|
||||
|
||||
// Search mode constants
|
||||
const SEARCH_MODES = ['auto', 'exact', 'fuzzy', 'semantic', 'graph'];
|
||||
@@ -195,12 +196,10 @@ async function executeAutoMode(params) {
|
||||
};
|
||||
|
||||
case 'fuzzy':
|
||||
case 'semantic':
|
||||
case 'graph':
|
||||
// These modes not yet implemented
|
||||
// Fuzzy mode not yet implemented
|
||||
return {
|
||||
success: false,
|
||||
error: `${classification.mode} mode not yet implemented`,
|
||||
error: 'Fuzzy mode not yet implemented',
|
||||
metadata: {
|
||||
classified_as: classification.mode,
|
||||
confidence: classification.confidence,
|
||||
@@ -208,6 +207,32 @@ async function executeAutoMode(params) {
|
||||
}
|
||||
};
|
||||
|
||||
case 'semantic':
|
||||
// Execute semantic mode via CodexLens
|
||||
const semanticResult = await executeSemanticMode(params);
|
||||
return {
|
||||
...semanticResult,
|
||||
metadata: {
|
||||
...semanticResult.metadata,
|
||||
classified_as: classification.mode,
|
||||
confidence: classification.confidence,
|
||||
reasoning: classification.reasoning
|
||||
}
|
||||
};
|
||||
|
||||
case 'graph':
|
||||
// Execute graph mode via CodexLens
|
||||
const graphResult = await executeGraphMode(params);
|
||||
return {
|
||||
...graphResult,
|
||||
metadata: {
|
||||
...graphResult.metadata,
|
||||
classified_as: classification.mode,
|
||||
confidence: classification.confidence,
|
||||
reasoning: classification.reasoning
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
// Fallback to exact mode with warning
|
||||
const fallbackResult = await executeExactMode(params);
|
||||
@@ -346,41 +371,166 @@ async function executeFuzzyMode(params) {
|
||||
|
||||
/**
|
||||
* Mode: semantic - Natural language understanding search
|
||||
* Uses LLM or embeddings for semantic similarity
|
||||
* Uses CodexLens embeddings for semantic similarity
|
||||
*/
|
||||
async function executeSemanticMode(params) {
|
||||
const { query, paths = [], maxResults = 100 } = params;
|
||||
|
||||
// TODO: Implement semantic search
|
||||
// - Option 1: Use Gemini CLI via cli-executor.js
|
||||
// - Option 2: Use local embeddings (transformers.js)
|
||||
// - Generate query embedding
|
||||
// - Compare with code embeddings
|
||||
// - Return semantically similar results
|
||||
// Check CodexLens availability
|
||||
const readyStatus = await ensureCodexLensReady();
|
||||
if (!readyStatus.ready) {
|
||||
return {
|
||||
success: false,
|
||||
error: `CodexLens not available: ${readyStatus.error}. Run 'ccw tool exec codex_lens {"action":"bootstrap"}' to install.`
|
||||
};
|
||||
}
|
||||
|
||||
// Determine search path
|
||||
const searchPath = paths.length > 0 ? paths[0] : '.';
|
||||
|
||||
// Execute CodexLens semantic search
|
||||
const result = await executeCodexLens(
|
||||
['search', query, '--limit', maxResults.toString(), '--json'],
|
||||
{ cwd: searchPath }
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error,
|
||||
metadata: {
|
||||
mode: 'semantic',
|
||||
backend: 'codexlens'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Parse and transform results
|
||||
let results = [];
|
||||
try {
|
||||
// Handle CRLF in output
|
||||
const cleanOutput = result.output.replace(/\r\n/g, '\n');
|
||||
const parsed = JSON.parse(cleanOutput);
|
||||
const data = parsed.result || parsed;
|
||||
results = (data.results || []).map(item => ({
|
||||
file: item.path || item.file,
|
||||
score: item.score || 0,
|
||||
content: item.excerpt || item.content || '',
|
||||
symbol: item.symbol || null
|
||||
}));
|
||||
} catch {
|
||||
// Return raw output if JSON parsing fails
|
||||
return {
|
||||
success: true,
|
||||
results: [],
|
||||
output: result.output,
|
||||
metadata: {
|
||||
mode: 'semantic',
|
||||
backend: 'codexlens',
|
||||
count: 0,
|
||||
query,
|
||||
warning: 'Failed to parse JSON output'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Semantic mode not implemented - LLM/embedding integration pending'
|
||||
success: true,
|
||||
results,
|
||||
metadata: {
|
||||
mode: 'semantic',
|
||||
backend: 'codexlens',
|
||||
count: results.length,
|
||||
query
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode: graph - Dependency and relationship traversal
|
||||
* Analyzes code relationships (imports, exports, dependencies)
|
||||
* Uses CodexLens symbol extraction for code analysis
|
||||
*/
|
||||
async function executeGraphMode(params) {
|
||||
const { query, paths = [], maxResults = 100 } = params;
|
||||
|
||||
// TODO: Implement graph search
|
||||
// - Parse import/export statements
|
||||
// - Build dependency graph
|
||||
// - Traverse relationships
|
||||
// - Find related modules
|
||||
// - Return graph results
|
||||
// Check CodexLens availability
|
||||
const readyStatus = await ensureCodexLensReady();
|
||||
if (!readyStatus.ready) {
|
||||
return {
|
||||
success: false,
|
||||
error: `CodexLens not available: ${readyStatus.error}. Run 'ccw tool exec codex_lens {"action":"bootstrap"}' to install.`
|
||||
};
|
||||
}
|
||||
|
||||
// First, search for relevant files using text search
|
||||
const searchPath = paths.length > 0 ? paths[0] : '.';
|
||||
|
||||
// Execute text search to find files matching the query
|
||||
const textResult = await executeCodexLens(
|
||||
['search', query, '--limit', maxResults.toString(), '--json'],
|
||||
{ cwd: searchPath }
|
||||
);
|
||||
|
||||
if (!textResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: textResult.error,
|
||||
metadata: {
|
||||
mode: 'graph',
|
||||
backend: 'codexlens'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Parse results and extract symbols from top files
|
||||
let results = [];
|
||||
try {
|
||||
const parsed = JSON.parse(textResult.output);
|
||||
const files = [...new Set((parsed.results || parsed).map(item => item.path || item.file))].slice(0, 10);
|
||||
|
||||
// Extract symbols from files in parallel
|
||||
const symbolPromises = files.map(file =>
|
||||
executeCodexLens(['symbol', file, '--json'], { cwd: searchPath })
|
||||
.then(result => ({ file, result }))
|
||||
);
|
||||
|
||||
const symbolResults = await Promise.all(symbolPromises);
|
||||
|
||||
for (const { file, result } of symbolResults) {
|
||||
if (result.success) {
|
||||
try {
|
||||
const symbols = JSON.parse(result.output);
|
||||
results.push({
|
||||
file,
|
||||
symbols: symbols.symbols || symbols,
|
||||
relationships: []
|
||||
});
|
||||
} catch {
|
||||
// Skip files with parse errors
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to parse search results',
|
||||
metadata: {
|
||||
mode: 'graph',
|
||||
backend: 'codexlens'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Graph mode not implemented - dependency analysis pending'
|
||||
success: true,
|
||||
results,
|
||||
metadata: {
|
||||
mode: 'graph',
|
||||
backend: 'codexlens',
|
||||
count: results.length,
|
||||
query,
|
||||
note: 'Graph mode provides symbol extraction; full dependency graph analysis pending'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user