diff --git a/.gitignore b/.gitignore
index 8c65a242..92b4d30d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,5 @@ ref
COMMAND_FLOW_STANDARD.md
COMMAND_TEMPLATE_EXECUTOR.md
COMMAND_TEMPLATE_ORCHESTRATOR.md
+*.pyc
+.codexlens/
\ No newline at end of file
diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js
index 584697f2..6eadd790 100644
--- a/ccw/src/core/server.js
+++ b/ccw/src/core/server.js
@@ -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();
diff --git a/ccw/src/templates/dashboard-js/components/cli-status.js b/ccw/src/templates/dashboard-js/components/cli-status.js
index 18ceb0a9..6e6b8192 100644
--- a/ccw/src/templates/dashboard-js/components/cli-status.js
+++ b/ccw/src/templates/dashboard-js/components/cli-status.js
@@ -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 = `
+
+ `;
+
container.innerHTML = `
${toolsHtml}
+ ${codexLensHtml}
`;
@@ -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');
+ }
+}
diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js
index bd9cfdc8..ec756f22 100644
--- a/ccw/src/templates/dashboard-js/components/hook-manager.js
+++ b/ccw/src/templates/dashboard-js/components/hook-manager.js
@@ -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' }
+ ]
}
};
diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js
index 777bd5ca..80a2566c 100644
--- a/ccw/src/templates/dashboard-js/views/hook-manager.js
+++ b/ccw/src/templates/dashboard-js/views/hook-manager.js
@@ -82,6 +82,7 @@ async function renderHookManager() {
+ ${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')}
diff --git a/ccw/src/tools/codex-lens.js b/ccw/src/tools/codex-lens.js
new file mode 100644
index 00000000..95b5de9a
--- /dev/null
+++ b/ccw/src/tools/codex-lens.js
@@ -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