diff --git a/.claude/docs/CODEXLENS_TECHNICAL_SPEC.md b/.claude/docs/CODEXLENS_TECHNICAL_SPEC.md deleted file mode 100644 index a0252ed7..00000000 --- a/.claude/docs/CODEXLENS_TECHNICAL_SPEC.md +++ /dev/null @@ -1,2099 +0,0 @@ -# CodexLens 技术方案 - -> 融合 code-index-mcp 与 codanna 最佳特性的代码智能分析平台 -> -> 目标:接入 CCW (Claude Code Workflow) 工具端点 - ---- - -## 目录 - -1. [项目概览](#1-项目概览) -2. [架构设计](#2-架构设计) -3. [目录结构](#3-目录结构) -4. [核心模块设计](#4-核心模块设计) -5. [CCW 集成设计](#5-ccw-集成设计) -6. [数据存储设计](#6-数据存储设计) -7. [语义搜索架构](#7-语义搜索架构) -8. [CLI 命令设计](#8-cli-命令设计) -9. [开发路线图](#9-开发路线图) -10. [技术依赖](#10-技术依赖) -11. [npm 分发策略](#11-npm-分发策略) - ---- - -## 1. 项目概览 - -### 1.1 项目信息 - -| 属性 | 值 | -|------|-----| -| **项目名称** | CodexLens | -| **包名** | `codex_lens` | -| **语言** | Python 3.10+ | -| **定位** | 多模态代码分析平台 | -| **集成目标** | CCW 工具端点 (`D:\Claude_dms3\ccw`) | - -### 1.2 核心能力 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ CodexLens 能力矩阵 │ -├─────────────────────────────────────────────────────────────┤ -│ 🔍 结构索引 │ AST 解析、符号提取、调用关系图 │ -│ 🧠 语义搜索 │ 自然语言查询、向量嵌入、相似度匹配 │ -│ 📊 代码分析 │ 复杂度计算、影响分析、依赖追踪 │ -│ 🔗 CCW 集成 │ JSON 协议、工具注册、命令行接口 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 1.3 设计原则 - -- **CLI-First**: 无服务器依赖,通过命令行调用 -- **JSON 协议**: 标准化输入输出,便于 CCW 解析 -- **增量索引**: 仅处理变更文件,提升性能 -- **可选语义**: 语义搜索作为可选功能,保持核心轻量 - ---- - -## 2. 架构设计 - -### 2.1 整体架构 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ CCW 层 │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ ccw/src/tools/codex-lens.js (CCW Tool Wrapper) │ │ -│ │ - 注册 CodexLens 工具到 CCW │ │ -│ │ - 参数验证与转换 │ │ -│ │ - 调用 Python CLI │ │ -│ │ - 解析 JSON 输出 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - │ spawn / exec - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ CodexLens CLI │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ codexlens [options] --json │ │ -│ │ │ │ -│ │ Commands: │ │ -│ │ - init 初始化项目索引 │ │ -│ │ - search 文本/正则搜索 │ │ -│ │ - find 文件查找 (glob) │ │ -│ │ - symbol 符号查找 │ │ -│ │ - inspect 文件/符号详情 │ │ -│ │ - graph 调用关系图 │ │ -│ │ - semantic 语义搜索 (可选) │ │ -│ │ - status 索引状态 │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ Core Engine │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Indexer │ │ Searcher │ │ Analyzer │ │ -│ │ (索引引擎) │ │ (搜索引擎) │ │ (分析引擎) │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Storage Layer │ │ -│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ -│ │ │ SQLite │ │ ChromaDB │ │ FileCache │ │ │ -│ │ │ (符号索引) │ │ (向量存储) │ │ (文件缓存) │ │ │ -│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 数据流 - -``` -用户/CCW 请求 - │ - ▼ -┌─────────────┐ -│ CLI 解析 │ ──→ 验证参数 ──→ 加载配置 -└──────┬──────┘ - │ - ▼ -┌─────────────┐ -│ 命令路由器 │ ──→ 选择处理器 -└──────┬──────┘ - │ - ├──→ SearchHandler ──→ ripgrep/SQLite - ├──→ SymbolHandler ──→ SQLite 符号表 - ├──→ GraphHandler ──→ NetworkX 图 - └──→ SemanticHandler ──→ ChromaDB 向量 - │ - ▼ -┌─────────────┐ -│ JSON 输出 │ ──→ stdout -└─────────────┘ -``` - ---- - -## 3. 目录结构 - -### 3.1 Python 项目结构 - -``` -codex-lens/ -├── .codexlens/ # 项目级配置目录 (git ignored) -│ ├── config.toml # 项目配置 -│ ├── index.db # SQLite 索引 -│ └── vectors/ # ChromaDB 向量存储 -│ -├── src/ -│ └── codex_lens/ -│ ├── __init__.py -│ ├── __main__.py # python -m codex_lens 入口 -│ │ -│ ├── cli/ # CLI 层 -│ │ ├── __init__.py -│ │ ├── main.py # Typer 应用主入口 -│ │ ├── commands/ # 命令实现 -│ │ │ ├── __init__.py -│ │ │ ├── init.py # codexlens init -│ │ │ ├── search.py # codexlens search -│ │ │ ├── find.py # codexlens find -│ │ │ ├── symbol.py # codexlens symbol -│ │ │ ├── inspect.py # codexlens inspect -│ │ │ ├── graph.py # codexlens graph -│ │ │ ├── semantic.py # codexlens semantic -│ │ │ └── status.py # codexlens status -│ │ └── output.py # JSON 输出格式化 -│ │ -│ ├── core/ # 核心领域层 -│ │ ├── __init__.py -│ │ ├── entities.py # 数据实体: Symbol, File, Relation -│ │ ├── interfaces.py # 抽象接口: Indexer, Searcher -│ │ ├── config.py # Pydantic 配置模型 -│ │ └── errors.py # 自定义异常 -│ │ -│ ├── engine/ # 引擎层 -│ │ ├── __init__.py -│ │ ├── indexer.py # 索引编排器 -│ │ ├── searcher.py # 搜索编排器 -│ │ ├── analyzer.py # 分析编排器 -│ │ └── watcher.py # 文件监控 (可选) -│ │ -│ ├── parsing/ # 解析层 -│ │ ├── __init__.py -│ │ ├── base.py # ParsingStrategy ABC -│ │ ├── factory.py # 策略工厂 -│ │ ├── python_parser.py # Python AST 解析 -│ │ ├── js_parser.py # JavaScript/TS 解析 -│ │ ├── rust_parser.py # Rust 解析 -│ │ └── fallback.py # 通用回退解析 -│ │ -│ ├── semantic/ # 语义搜索层 (可选) -│ │ ├── __init__.py -│ │ ├── embedder.py # 嵌入生成器 -│ │ ├── chunker.py # 代码分块 -│ │ └── search.py # 向量搜索 -│ │ -│ ├── storage/ # 存储层 -│ │ ├── __init__.py -│ │ ├── sqlite_store.py # SQLite 存储 -│ │ ├── vector_store.py # ChromaDB 适配 -│ │ └── file_cache.py # 文件哈希缓存 -│ │ -│ └── utils/ # 工具层 -│ ├── __init__.py -│ ├── git.py # Git 集成 -│ ├── ripgrep.py # ripgrep 包装 -│ └── logging.py # 日志配置 -│ -├── tests/ # 测试 -│ ├── __init__.py -│ ├── test_indexer.py -│ ├── test_search.py -│ └── fixtures/ -│ -├── pyproject.toml # 项目配置 -├── codexlens.spec # PyInstaller 配置 -└── README.md -``` - -### 3.2 CCW 集成文件 - -``` -D:\Claude_dms3\ccw\src\tools\ -└── codex-lens.js # CCW 工具包装器 -``` - ---- - -## 4. 核心模块设计 - -### 4.1 核心实体 (`core/entities.py`) - -```python -from dataclasses import dataclass, field -from typing import List, Optional, Dict, Any -from enum import Enum - -class SymbolType(Enum): - FUNCTION = "function" - CLASS = "class" - METHOD = "method" - VARIABLE = "variable" - INTERFACE = "interface" - MODULE = "module" - IMPORT = "import" - -class RelationType(Enum): - CALLS = "calls" - CALLED_BY = "called_by" - IMPORTS = "imports" - IMPORTED_BY = "imported_by" - EXTENDS = "extends" - IMPLEMENTS = "implements" - -@dataclass -class Location: - """代码位置""" - file_path: str - line_start: int - line_end: int - column_start: int = 0 - column_end: int = 0 - -@dataclass -class Symbol: - """代码符号""" - id: str # 唯一标识: file_path::name - name: str # 符号名称 - short_name: str # 短名称 (用于模糊匹配) - type: SymbolType # 符号类型 - location: Location # 位置信息 - signature: Optional[str] = None # 函数签名 - docstring: Optional[str] = None # 文档字符串 - language: str = "unknown" # 语言 - metadata: Dict[str, Any] = field(default_factory=dict) - -@dataclass -class FileInfo: - """文件信息""" - path: str # 相对路径 - language: str # 语言 - line_count: int # 行数 - hash: str # 内容哈希 (用于增量索引) - imports: List[str] = field(default_factory=list) - exports: List[str] = field(default_factory=list) - symbols: List[str] = field(default_factory=list) # symbol_ids - -@dataclass -class Relation: - """符号关系""" - source_id: str # 源符号 ID - target_id: str # 目标符号 ID - relation_type: RelationType # 关系类型 - metadata: Dict[str, Any] = field(default_factory=dict) - -@dataclass -class SearchResult: - """搜索结果""" - file_path: str - line: int - column: int - content: str - context_before: List[str] = field(default_factory=list) - context_after: List[str] = field(default_factory=list) - score: float = 1.0 # 相关性得分 -``` - -### 4.2 配置模型 (`core/config.py`) - -```python -from pydantic import BaseModel, Field -from typing import List, Optional -from pathlib import Path - -class IndexConfig(BaseModel): - """索引配置""" - include_patterns: List[str] = Field( - default=["**/*.py", "**/*.js", "**/*.ts", "**/*.rs"], - description="包含的文件模式" - ) - exclude_patterns: List[str] = Field( - default=["**/node_modules/**", "**/.git/**", "**/dist/**", "**/__pycache__/**"], - description="排除的文件模式" - ) - max_file_size: int = Field( - default=1024 * 1024, # 1MB - description="最大文件大小 (bytes)" - ) - enable_semantic: bool = Field( - default=False, - description="启用语义搜索" - ) - -class SemanticConfig(BaseModel): - """语义搜索配置""" - model_name: str = Field( - default="all-MiniLM-L6-v2", - description="嵌入模型名称" - ) - chunk_size: int = Field( - default=512, - description="代码块大小 (tokens)" - ) - chunk_overlap: int = Field( - default=50, - description="块重叠大小" - ) - -class ProjectConfig(BaseModel): - """项目配置""" - project_root: Path - index: IndexConfig = Field(default_factory=IndexConfig) - semantic: SemanticConfig = Field(default_factory=SemanticConfig) - - @classmethod - def load(cls, config_path: Path) -> "ProjectConfig": - """从配置文件加载""" - import tomli - with open(config_path, "rb") as f: - data = tomli.load(f) - return cls(**data) - - def save(self, config_path: Path): - """保存到配置文件""" - import tomli_w - with open(config_path, "wb") as f: - tomli_w.dump(self.model_dump(), f) -``` - -### 4.3 解析策略接口 (`parsing/base.py`) - -```python -from abc import ABC, abstractmethod -from typing import List, Tuple, Dict, Any -from ..core.entities import Symbol, FileInfo - -class ParsingStrategy(ABC): - """语言解析策略基类""" - - @abstractmethod - def get_language_name(self) -> str: - """返回语言名称""" - pass - - @abstractmethod - def get_supported_extensions(self) -> List[str]: - """返回支持的文件扩展名""" - pass - - @abstractmethod - def parse_file( - self, - file_path: str, - content: str - ) -> Tuple[List[Symbol], FileInfo, List[Dict[str, Any]]]: - """ - 解析文件 - - Returns: - - symbols: 提取的符号列表 - - file_info: 文件信息 - - pending_calls: 待解析的调用关系 - """ - pass - - def supports_file(self, file_path: str) -> bool: - """检查是否支持该文件""" - ext = file_path.rsplit(".", 1)[-1] if "." in file_path else "" - return f".{ext}" in self.get_supported_extensions() -``` - -### 4.4 索引引擎 (`engine/indexer.py`) - -```python -import hashlib -from pathlib import Path -from typing import List, Optional, Generator -from concurrent.futures import ThreadPoolExecutor, as_completed - -from ..core.config import ProjectConfig -from ..core.entities import Symbol, FileInfo, Relation -from ..parsing.factory import ParserFactory -from ..storage.sqlite_store import SQLiteStore -from ..storage.file_cache import FileCache -from ..utils.git import get_git_files - -class Indexer: - """索引引擎""" - - def __init__(self, config: ProjectConfig): - self.config = config - self.store = SQLiteStore(config.project_root / ".codexlens" / "index.db") - self.cache = FileCache(config.project_root / ".codexlens" / "cache.json") - self.parser_factory = ParserFactory() - - def build_index(self, incremental: bool = True) -> dict: - """ - 构建索引 - - Args: - incremental: 是否增量索引 - - Returns: - 索引统计信息 - """ - stats = { - "files_scanned": 0, - "files_indexed": 0, - "files_skipped": 0, - "symbols_extracted": 0, - "relations_resolved": 0, - "errors": [] - } - - # 1. 发现文件 - files = list(self._discover_files()) - stats["files_scanned"] = len(files) - - # 2. 过滤需要重新索引的文件 - if incremental: - files = self._filter_changed_files(files) - - # 3. 并行解析 - pending_calls = [] - with ThreadPoolExecutor(max_workers=4) as executor: - futures = { - executor.submit(self._parse_file, f): f - for f in files - } - - for future in as_completed(futures): - file_path = futures[future] - try: - symbols, file_info, calls = future.result() - - # 存储文件信息 - self.store.upsert_file(file_info) - - # 存储符号 - for symbol in symbols: - self.store.upsert_symbol(symbol) - stats["symbols_extracted"] += 1 - - # 收集待解析调用 - pending_calls.extend(calls) - - # 更新缓存 - self.cache.update(file_path, file_info.hash) - stats["files_indexed"] += 1 - - except Exception as e: - stats["errors"].append({ - "file": file_path, - "error": str(e) - }) - - # 4. 解析调用关系 - stats["relations_resolved"] = self._resolve_calls(pending_calls) - - # 5. 保存缓存 - self.cache.save() - - return stats - - def _discover_files(self) -> Generator[str, None, None]: - """发现项目文件""" - # 优先使用 git ls-files - git_files = get_git_files(self.config.project_root) - if git_files: - for f in git_files: - if self._should_include(f): - yield f - else: - # 回退到 glob - for pattern in self.config.index.include_patterns: - for f in self.config.project_root.glob(pattern): - if self._should_include(str(f)): - yield str(f.relative_to(self.config.project_root)) - - def _should_include(self, file_path: str) -> bool: - """检查文件是否应该被索引""" - from fnmatch import fnmatch - for pattern in self.config.index.exclude_patterns: - if fnmatch(file_path, pattern): - return False - return True - - def _filter_changed_files(self, files: List[str]) -> List[str]: - """过滤出变更的文件""" - changed = [] - for f in files: - full_path = self.config.project_root / f - current_hash = self._compute_hash(full_path) - cached_hash = self.cache.get(f) - if current_hash != cached_hash: - changed.append(f) - return changed - - def _compute_hash(self, file_path: Path) -> str: - """计算文件哈希""" - with open(file_path, "rb") as f: - return hashlib.md5(f.read()).hexdigest() - - def _parse_file(self, file_path: str): - """解析单个文件""" - full_path = self.config.project_root / file_path - content = full_path.read_text(encoding="utf-8", errors="ignore") - - parser = self.parser_factory.get_parser(file_path) - return parser.parse_file(file_path, content) - - def _resolve_calls(self, pending_calls: List[dict]) -> int: - """解析调用关系""" - resolved = 0 - for call in pending_calls: - caller_id = call["caller_id"] - callee_name = call["callee_name"] - - # 查找被调用符号 - callee = self.store.find_symbol_by_name(callee_name) - if callee: - relation = Relation( - source_id=caller_id, - target_id=callee.id, - relation_type="calls" - ) - self.store.upsert_relation(relation) - resolved += 1 - - return resolved -``` - ---- - -## 5. CCW 集成设计 - -### 5.1 JSON 输出协议 - -所有 CLI 命令使用 `--json` 标志输出标准化 JSON。 - -**成功响应**: -```json -{ - "success": true, - "data": { - "results": [...], - "metadata": { - "count": 10, - "elapsed_ms": 45, - "mode": "exact" - } - } -} -``` - -**错误响应**: -```json -{ - "success": false, - "error": { - "code": "INDEX_NOT_FOUND", - "message": "Project not initialized. Run 'codexlens init' first.", - "suggestion": "codexlens init /path/to/project" - } -} -``` - -### 5.2 CCW 工具包装器 (`ccw/src/tools/codex-lens.js`) - -```javascript -/** - * CodexLens Tool - Code Intelligence Integration for CCW - * - * Provides: - * - Symbol search and navigation - * - Semantic code search - * - Dependency graph analysis - * - File inspection - */ - -import { spawn } from 'child_process'; -import { existsSync } from 'fs'; -import { resolve } from 'path'; - -// CodexLens binary path (configurable) -const CODEXLENS_BIN = process.env.CODEXLENS_BIN || 'codexlens'; - -/** - * Execute CodexLens CLI command - * @param {string[]} args - Command arguments - * @param {string} cwd - Working directory - * @returns {Promise} - Parsed JSON result - */ -async function execCodexLens(args, cwd = process.cwd()) { - return new Promise((resolve, reject) => { - const child = spawn(CODEXLENS_BIN, [...args, '--json'], { - cwd, - stdio: ['ignore', 'pipe', 'pipe'] - }); - - let stdout = ''; - let stderr = ''; - - child.stdout.on('data', (data) => { stdout += data.toString(); }); - child.stderr.on('data', (data) => { stderr += data.toString(); }); - - child.on('close', (code) => { - try { - const result = JSON.parse(stdout); - resolve(result); - } catch (err) { - reject(new Error(`Failed to parse CodexLens output: ${stderr || stdout}`)); - } - }); - - child.on('error', (err) => { - reject(new Error(`Failed to execute CodexLens: ${err.message}`)); - }); - }); -} - -/** - * Main execute function - */ -async function execute(params) { - const { - command, - query, - path, - mode = 'auto', - limit = 50, - contextLines = 2, - includeRelations = false, - projectPath = process.cwd() - } = params; - - // Validate command - const validCommands = ['search', 'find', 'symbol', 'inspect', 'graph', 'semantic', 'status', 'init']; - if (!validCommands.includes(command)) { - throw new Error(`Invalid command: ${command}. Valid: ${validCommands.join(', ')}`); - } - - // Build arguments based on command - const args = [command]; - - switch (command) { - case 'init': - args.push(projectPath); - break; - - case 'search': - if (!query) throw new Error('Parameter "query" required for search'); - args.push(query); - if (path) args.push('--path', path); - args.push('--context', contextLines.toString()); - args.push('--limit', limit.toString()); - break; - - case 'find': - if (!query) throw new Error('Parameter "query" (glob pattern) required for find'); - args.push(query); - args.push('--limit', limit.toString()); - break; - - case 'symbol': - if (!query) throw new Error('Parameter "query" (symbol name) required'); - args.push(query); - args.push('--mode', mode); // exact, fuzzy - args.push('--limit', limit.toString()); - if (includeRelations) args.push('--relations'); - break; - - case 'inspect': - if (!path) throw new Error('Parameter "path" required for inspect'); - args.push(path); - break; - - case 'graph': - if (!query) throw new Error('Parameter "query" (symbol name) required for graph'); - args.push(query); - args.push('--depth', (params.depth || 2).toString()); - args.push('--direction', params.direction || 'both'); // callers, callees, both - break; - - case 'semantic': - if (!query) throw new Error('Parameter "query" required for semantic search'); - args.push(query); - args.push('--limit', limit.toString()); - break; - - case 'status': - // No additional args - break; - } - - // Execute command - const result = await execCodexLens(args, projectPath); - - // Transform result for CCW consumption - return { - command, - ...result, - metadata: { - ...result.metadata, - tool: 'codex_lens', - projectPath - } - }; -} - -/** - * Tool Definition for CCW Registry - */ -export const codexLensTool = { - name: 'codex_lens', - description: `Code intelligence tool for symbol search, semantic search, and dependency analysis. - -Commands: -- init: Initialize project index -- search: Text/regex code search (ripgrep backend) -- find: File path search (glob patterns) -- symbol: Symbol name lookup with optional relations -- inspect: Get file/symbol details -- graph: Dependency graph traversal -- semantic: Natural language code search -- status: Index status and statistics - -Examples: -- Search for function: codex_lens symbol "handleRequest" -- Find files: codex_lens find "**/*.test.ts" -- Semantic search: codex_lens semantic "authentication middleware" -- Get callers: codex_lens graph "UserService.login" --direction callers`, - - parameters: { - type: 'object', - properties: { - command: { - type: 'string', - enum: ['init', 'search', 'find', 'symbol', 'inspect', 'graph', 'semantic', 'status'], - description: 'CodexLens command to execute' - }, - query: { - type: 'string', - description: 'Search query (text, pattern, or natural language)' - }, - path: { - type: 'string', - description: 'File path or glob pattern' - }, - mode: { - type: 'string', - enum: ['exact', 'fuzzy', 'regex'], - description: 'Search mode (default: exact)', - default: 'exact' - }, - limit: { - type: 'number', - description: 'Maximum results (default: 50)', - default: 50 - }, - contextLines: { - type: 'number', - description: 'Context lines around matches (default: 2)', - default: 2 - }, - depth: { - type: 'number', - description: 'Graph traversal depth (default: 2)', - default: 2 - }, - direction: { - type: 'string', - enum: ['callers', 'callees', 'both'], - description: 'Graph direction (default: both)', - default: 'both' - }, - includeRelations: { - type: 'boolean', - description: 'Include symbol relations in results', - default: false - }, - projectPath: { - type: 'string', - description: 'Project root path (default: cwd)' - } - }, - required: ['command'] - }, - execute -}; -``` - -### 5.3 注册到 CCW - -在 `ccw/src/tools/index.js` 中添加: - -```javascript -import { codexLensTool } from './codex-lens.js'; - -// ... 现有 imports ... - -// Register CodexLens tool -registerTool(codexLensTool); -``` - ---- - -## 6. 数据存储设计 - -### 6.1 SQLite Schema - -```sql --- 版本控制 -CREATE TABLE IF NOT EXISTS schema_version ( - version INTEGER PRIMARY KEY, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- 文件表 -CREATE TABLE IF NOT EXISTS files ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - path TEXT UNIQUE NOT NULL, - language TEXT NOT NULL, - line_count INTEGER DEFAULT 0, - hash TEXT NOT NULL, - imports TEXT DEFAULT '[]', -- JSON array - exports TEXT DEFAULT '[]', -- JSON array - indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - - INDEX idx_files_language (language), - INDEX idx_files_hash (hash) -); - --- 符号表 -CREATE TABLE IF NOT EXISTS symbols ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - symbol_id TEXT UNIQUE NOT NULL, -- file_path::name - file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, - name TEXT NOT NULL, - short_name TEXT NOT NULL, -- 用于模糊搜索 - type TEXT NOT NULL, -- function, class, method, etc. - line_start INTEGER NOT NULL, - line_end INTEGER NOT NULL, - column_start INTEGER DEFAULT 0, - column_end INTEGER DEFAULT 0, - signature TEXT, - docstring TEXT, - language TEXT NOT NULL, - metadata TEXT DEFAULT '{}', -- JSON object - - INDEX idx_symbols_name (name), - INDEX idx_symbols_short_name (short_name), - INDEX idx_symbols_type (type), - INDEX idx_symbols_file_id (file_id) -); - --- 关系表 -CREATE TABLE IF NOT EXISTS relations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - source_id TEXT NOT NULL, -- symbol_id - target_id TEXT NOT NULL, -- symbol_id - relation_type TEXT NOT NULL, -- calls, imports, extends, etc. - metadata TEXT DEFAULT '{}', -- JSON object - - UNIQUE(source_id, target_id, relation_type), - INDEX idx_relations_source (source_id), - INDEX idx_relations_target (target_id), - INDEX idx_relations_type (relation_type) -); - --- FTS5 全文搜索索引 -CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5( - symbol_id, - name, - short_name, - signature, - docstring, - content='symbols', - content_rowid='id' -); - --- 触发器:保持 FTS 索引同步 -CREATE TRIGGER symbols_ai AFTER INSERT ON symbols BEGIN - INSERT INTO symbols_fts(rowid, symbol_id, name, short_name, signature, docstring) - VALUES (new.id, new.symbol_id, new.name, new.short_name, new.signature, new.docstring); -END; - -CREATE TRIGGER symbols_ad AFTER DELETE ON symbols BEGIN - INSERT INTO symbols_fts(symbols_fts, rowid, symbol_id, name, short_name, signature, docstring) - VALUES('delete', old.id, old.symbol_id, old.name, old.short_name, old.signature, old.docstring); -END; - -CREATE TRIGGER symbols_au AFTER UPDATE ON symbols BEGIN - INSERT INTO symbols_fts(symbols_fts, rowid, symbol_id, name, short_name, signature, docstring) - VALUES('delete', old.id, old.symbol_id, old.name, old.short_name, old.signature, old.docstring); - INSERT INTO symbols_fts(rowid, symbol_id, name, short_name, signature, docstring) - VALUES (new.id, new.symbol_id, new.name, new.short_name, new.signature, new.docstring); -END; -``` - -### 6.2 SQLite Store 实现 (`storage/sqlite_store.py`) - -```python -import sqlite3 -import json -from pathlib import Path -from typing import List, Optional -from contextlib import contextmanager - -from ..core.entities import Symbol, FileInfo, Relation, SymbolType - -SCHEMA_VERSION = 1 - -class SQLiteStore: - """SQLite 存储管理器""" - - def __init__(self, db_path: Path): - self.db_path = db_path - self.db_path.parent.mkdir(parents=True, exist_ok=True) - self._init_schema() - - @contextmanager - def _connection(self): - """获取数据库连接""" - conn = sqlite3.connect(self.db_path) - conn.row_factory = sqlite3.Row - conn.execute("PRAGMA foreign_keys = ON") - conn.execute("PRAGMA journal_mode = WAL") - try: - yield conn - conn.commit() - finally: - conn.close() - - def _init_schema(self): - """初始化数据库 schema""" - with self._connection() as conn: - # 检查版本 - conn.execute(""" - CREATE TABLE IF NOT EXISTS schema_version ( - version INTEGER PRIMARY KEY - ) - """) - - row = conn.execute("SELECT version FROM schema_version").fetchone() - current_version = row["version"] if row else 0 - - if current_version < SCHEMA_VERSION: - self._apply_schema(conn) - conn.execute( - "INSERT OR REPLACE INTO schema_version (version) VALUES (?)", - (SCHEMA_VERSION,) - ) - - def _apply_schema(self, conn): - """应用 schema""" - conn.executescript(""" - CREATE TABLE IF NOT EXISTS files ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - path TEXT UNIQUE NOT NULL, - language TEXT NOT NULL, - line_count INTEGER DEFAULT 0, - hash TEXT NOT NULL, - imports TEXT DEFAULT '[]', - exports TEXT DEFAULT '[]', - indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - - CREATE TABLE IF NOT EXISTS symbols ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - symbol_id TEXT UNIQUE NOT NULL, - file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, - name TEXT NOT NULL, - short_name TEXT NOT NULL, - type TEXT NOT NULL, - line_start INTEGER NOT NULL, - line_end INTEGER NOT NULL, - column_start INTEGER DEFAULT 0, - column_end INTEGER DEFAULT 0, - signature TEXT, - docstring TEXT, - language TEXT NOT NULL, - metadata TEXT DEFAULT '{}' - ); - - CREATE TABLE IF NOT EXISTS relations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - source_id TEXT NOT NULL, - target_id TEXT NOT NULL, - relation_type TEXT NOT NULL, - metadata TEXT DEFAULT '{}', - UNIQUE(source_id, target_id, relation_type) - ); - - CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name); - CREATE INDEX IF NOT EXISTS idx_symbols_short_name ON symbols(short_name); - CREATE INDEX IF NOT EXISTS idx_symbols_type ON symbols(type); - CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id); - CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id); - """) - - def upsert_file(self, file_info: FileInfo) -> int: - """插入或更新文件""" - with self._connection() as conn: - cursor = conn.execute(""" - INSERT INTO files (path, language, line_count, hash, imports, exports) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(path) DO UPDATE SET - language = excluded.language, - line_count = excluded.line_count, - hash = excluded.hash, - imports = excluded.imports, - exports = excluded.exports, - indexed_at = CURRENT_TIMESTAMP - RETURNING id - """, ( - file_info.path, - file_info.language, - file_info.line_count, - file_info.hash, - json.dumps(file_info.imports), - json.dumps(file_info.exports) - )) - return cursor.fetchone()["id"] - - def upsert_symbol(self, symbol: Symbol) -> int: - """插入或更新符号""" - with self._connection() as conn: - # 获取 file_id - file_row = conn.execute( - "SELECT id FROM files WHERE path = ?", - (symbol.location.file_path,) - ).fetchone() - - if not file_row: - raise ValueError(f"File not found: {symbol.location.file_path}") - - cursor = conn.execute(""" - INSERT INTO symbols ( - symbol_id, file_id, name, short_name, type, - line_start, line_end, column_start, column_end, - signature, docstring, language, metadata - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(symbol_id) DO UPDATE SET - name = excluded.name, - short_name = excluded.short_name, - type = excluded.type, - line_start = excluded.line_start, - line_end = excluded.line_end, - signature = excluded.signature, - docstring = excluded.docstring, - metadata = excluded.metadata - RETURNING id - """, ( - symbol.id, - file_row["id"], - symbol.name, - symbol.short_name, - symbol.type.value, - symbol.location.line_start, - symbol.location.line_end, - symbol.location.column_start, - symbol.location.column_end, - symbol.signature, - symbol.docstring, - symbol.language, - json.dumps(symbol.metadata) - )) - return cursor.fetchone()["id"] - - def find_symbol_by_name( - self, - name: str, - exact: bool = False - ) -> Optional[Symbol]: - """按名称查找符号""" - with self._connection() as conn: - if exact: - row = conn.execute( - "SELECT * FROM symbols WHERE name = ?", - (name,) - ).fetchone() - else: - row = conn.execute( - "SELECT * FROM symbols WHERE short_name LIKE ?", - (f"%{name}%",) - ).fetchone() - - return self._row_to_symbol(row) if row else None - - def search_symbols( - self, - query: str, - limit: int = 50 - ) -> List[Symbol]: - """搜索符号""" - with self._connection() as conn: - rows = conn.execute(""" - SELECT * FROM symbols - WHERE name LIKE ? OR short_name LIKE ? OR signature LIKE ? - LIMIT ? - """, (f"%{query}%", f"%{query}%", f"%{query}%", limit)).fetchall() - - return [self._row_to_symbol(row) for row in rows] - - def get_relations( - self, - symbol_id: str, - direction: str = "both" - ) -> List[Relation]: - """获取符号关系""" - with self._connection() as conn: - relations = [] - - if direction in ("both", "outgoing"): - rows = conn.execute( - "SELECT * FROM relations WHERE source_id = ?", - (symbol_id,) - ).fetchall() - relations.extend([self._row_to_relation(r) for r in rows]) - - if direction in ("both", "incoming"): - rows = conn.execute( - "SELECT * FROM relations WHERE target_id = ?", - (symbol_id,) - ).fetchall() - relations.extend([self._row_to_relation(r) for r in rows]) - - return relations - - def get_stats(self) -> dict: - """获取索引统计""" - with self._connection() as conn: - file_count = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0] - symbol_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0] - relation_count = conn.execute("SELECT COUNT(*) FROM relations").fetchone()[0] - - languages = conn.execute(""" - SELECT language, COUNT(*) as count - FROM files GROUP BY language - """).fetchall() - - return { - "files": file_count, - "symbols": symbol_count, - "relations": relation_count, - "languages": {r["language"]: r["count"] for r in languages} - } - - def _row_to_symbol(self, row) -> Symbol: - """将数据库行转换为 Symbol""" - return Symbol( - id=row["symbol_id"], - name=row["name"], - short_name=row["short_name"], - type=SymbolType(row["type"]), - location=Location( - file_path=row["path"] if "path" in row.keys() else "", - line_start=row["line_start"], - line_end=row["line_end"], - column_start=row["column_start"], - column_end=row["column_end"] - ), - signature=row["signature"], - docstring=row["docstring"], - language=row["language"], - metadata=json.loads(row["metadata"]) - ) - - def _row_to_relation(self, row) -> Relation: - """将数据库行转换为 Relation""" - return Relation( - source_id=row["source_id"], - target_id=row["target_id"], - relation_type=row["relation_type"], - metadata=json.loads(row["metadata"]) - ) -``` - ---- - -## 7. 语义搜索架构 - -### 7.1 嵌入生成器 (`semantic/embedder.py`) - -```python -from typing import List, Optional -from functools import lru_cache - -class SemanticEmbedder: - """语义嵌入生成器 (懒加载)""" - - def __init__(self, model_name: str = "all-MiniLM-L6-v2"): - self.model_name = model_name - self._model = None - - @property - def model(self): - """懒加载模型""" - if self._model is None: - from sentence_transformers import SentenceTransformer - self._model = SentenceTransformer(self.model_name) - return self._model - - def embed(self, text: str) -> List[float]: - """生成单个文本的嵌入""" - return self.model.encode(text).tolist() - - def embed_batch(self, texts: List[str]) -> List[List[float]]: - """批量生成嵌入""" - return self.model.encode(texts).tolist() - - def embed_symbol(self, symbol) -> List[float]: - """为符号生成嵌入""" - text = self._build_semantic_text(symbol) - return self.embed(text) - - def _build_semantic_text(self, symbol) -> str: - """构建符号的语义文本""" - parts = [ - f"[{symbol.type.value}] {symbol.name}", - ] - - if symbol.signature: - parts.append(f"Signature: {symbol.signature}") - - if symbol.docstring: - parts.append(f"Description: {symbol.docstring}") - - return "\n".join(parts) -``` - -### 7.2 向量存储 (`semantic/vector_store.py`) - -```python -from typing import List, Dict, Any, Optional -from pathlib import Path - -class VectorStore: - """ChromaDB 向量存储适配器""" - - def __init__(self, persist_dir: Path): - self.persist_dir = persist_dir - self._client = None - self._collection = None - - @property - def client(self): - """懒加载 ChromaDB 客户端""" - if self._client is None: - import chromadb - self._client = chromadb.PersistentClient( - path=str(self.persist_dir) - ) - return self._client - - @property - def collection(self): - """获取或创建集合""" - if self._collection is None: - self._collection = self.client.get_or_create_collection( - name="codexlens_symbols", - metadata={"hnsw:space": "cosine"} - ) - return self._collection - - def upsert( - self, - id: str, - embedding: List[float], - metadata: Dict[str, Any], - document: str = "" - ): - """插入或更新向量""" - self.collection.upsert( - ids=[id], - embeddings=[embedding], - metadatas=[metadata], - documents=[document] - ) - - def upsert_batch( - self, - ids: List[str], - embeddings: List[List[float]], - metadatas: List[Dict[str, Any]], - documents: List[str] = None - ): - """批量插入""" - self.collection.upsert( - ids=ids, - embeddings=embeddings, - metadatas=metadatas, - documents=documents or [""] * len(ids) - ) - - def search( - self, - query_embedding: List[float], - limit: int = 10, - where: Optional[Dict] = None - ) -> List[Dict]: - """向量相似度搜索""" - results = self.collection.query( - query_embeddings=[query_embedding], - n_results=limit, - where=where, - include=["metadatas", "distances", "documents"] - ) - - # 转换为统一格式 - items = [] - for i in range(len(results["ids"][0])): - items.append({ - "id": results["ids"][0][i], - "metadata": results["metadatas"][0][i], - "distance": results["distances"][0][i], - "document": results["documents"][0][i] if results["documents"] else "" - }) - - return items - - def delete(self, ids: List[str]): - """删除向量""" - self.collection.delete(ids=ids) - - def count(self) -> int: - """获取向量数量""" - return self.collection.count() -``` - ---- - -## 8. CLI 命令设计 - -### 8.1 主入口 (`cli/main.py`) - -```python -import typer -from typing import Optional -from pathlib import Path - -from .commands import init, search, find, symbol, inspect, graph, semantic, status -from .output import JSONOutput - -app = typer.Typer( - name="codexlens", - help="Code intelligence tool for symbol search, semantic search, and dependency analysis.", - add_completion=False -) - -# 全局选项 -json_option = typer.Option(False, "--json", "-j", help="Output as JSON") -project_option = typer.Option(None, "--project", "-p", help="Project root path") - -# 注册子命令 -app.command()(init.command) -app.command()(search.command) -app.command()(find.command) -app.command()(symbol.command) -app.command()(inspect.command) -app.command()(graph.command) -app.command()(semantic.command) -app.command()(status.command) - -def main(): - app() - -if __name__ == "__main__": - main() -``` - -### 8.2 输出格式化 (`cli/output.py`) - -```python -import json -import sys -from typing import Any, Dict, List, Optional -from dataclasses import dataclass, asdict - -@dataclass -class CLIResponse: - """CLI 响应结构""" - success: bool - data: Optional[Dict[str, Any]] = None - error: Optional[Dict[str, str]] = None - - def to_json(self) -> str: - """转换为 JSON 字符串""" - result = {"success": self.success} - if self.data: - result["data"] = self.data - if self.error: - result["error"] = self.error - return json.dumps(result, indent=2, ensure_ascii=False) - - def print(self, as_json: bool = False): - """输出结果""" - if as_json: - print(self.to_json()) - else: - self._print_human_readable() - - def _print_human_readable(self): - """人类可读格式输出""" - if not self.success: - print(f"Error: {self.error.get('message', 'Unknown error')}", file=sys.stderr) - if suggestion := self.error.get('suggestion'): - print(f"Suggestion: {suggestion}", file=sys.stderr) - return - - if not self.data: - print("No results") - return - - # 根据数据类型格式化输出 - if "results" in self.data: - for item in self.data["results"]: - self._print_result_item(item) - elif "stats" in self.data: - self._print_stats(self.data["stats"]) - else: - print(json.dumps(self.data, indent=2)) - - def _print_result_item(self, item: Dict): - """打印单个结果项""" - if "file_path" in item and "line" in item: - # 搜索结果 - print(f"{item['file_path']}:{item['line']}") - if "content" in item: - print(f" {item['content']}") - elif "symbol_id" in item: - # 符号结果 - print(f"{item['type']}: {item['name']}") - print(f" Location: {item['file_path']}:{item['line_start']}") - if item.get("signature"): - print(f" Signature: {item['signature']}") - print() - - def _print_stats(self, stats: Dict): - """打印统计信息""" - print("Index Statistics:") - print(f" Files: {stats.get('files', 0)}") - print(f" Symbols: {stats.get('symbols', 0)}") - print(f" Relations: {stats.get('relations', 0)}") - if languages := stats.get("languages"): - print(" Languages:") - for lang, count in languages.items(): - print(f" {lang}: {count}") - - -def success(data: Dict[str, Any]) -> CLIResponse: - """创建成功响应""" - return CLIResponse(success=True, data=data) - - -def error(code: str, message: str, suggestion: str = None) -> CLIResponse: - """创建错误响应""" - err = {"code": code, "message": message} - if suggestion: - err["suggestion"] = suggestion - return CLIResponse(success=False, error=err) -``` - -### 8.3 命令示例: search (`cli/commands/search.py`) - -```python -import typer -from typing import Optional, List -from pathlib import Path -import time - -from ..output import success, error, CLIResponse -from ...engine.searcher import Searcher -from ...core.config import ProjectConfig -from ...utils.ripgrep import ripgrep_search - -def command( - query: str = typer.Argument(..., help="Search query (text or regex)"), - path: Optional[str] = typer.Option(None, "--path", "-p", help="Path filter (glob)"), - regex: bool = typer.Option(False, "--regex", "-r", help="Treat query as regex"), - context: int = typer.Option(2, "--context", "-C", help="Context lines"), - limit: int = typer.Option(50, "--limit", "-l", help="Max results"), - json_output: bool = typer.Option(False, "--json", "-j", help="JSON output"), - project: Optional[Path] = typer.Option(None, "--project", help="Project root") -): - """ - Search code content using text or regex patterns. - - Uses ripgrep for fast searching with optional context lines. - - Examples: - codexlens search "handleRequest" - codexlens search "def.*test" --regex - codexlens search "TODO" --path "**/*.py" - """ - start_time = time.time() - - try: - # 确定项目根目录 - project_root = project or Path.cwd() - - # 检查项目是否已初始化 - config_path = project_root / ".codexlens" / "config.toml" - if not config_path.exists(): - response = error( - "PROJECT_NOT_INITIALIZED", - "Project not initialized", - f"Run: codexlens init {project_root}" - ) - response.print(json_output) - raise typer.Exit(1) - - # 执行搜索 - results = ripgrep_search( - query=query, - path=project_root, - pattern_filter=path, - is_regex=regex, - context_lines=context, - max_results=limit - ) - - elapsed_ms = int((time.time() - start_time) * 1000) - - response = success({ - "results": results, - "metadata": { - "query": query, - "mode": "regex" if regex else "literal", - "count": len(results), - "elapsed_ms": elapsed_ms - } - }) - response.print(json_output) - - except Exception as e: - response = error("SEARCH_FAILED", str(e)) - response.print(json_output) - raise typer.Exit(1) -``` - -### 8.4 命令示例: symbol (`cli/commands/symbol.py`) - -```python -import typer -from typing import Optional -from pathlib import Path -import time - -from ..output import success, error -from ...storage.sqlite_store import SQLiteStore -from ...core.config import ProjectConfig - -def command( - query: str = typer.Argument(..., help="Symbol name to search"), - mode: str = typer.Option("fuzzy", "--mode", "-m", help="Search mode: exact, fuzzy"), - type_filter: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type: function, class, method"), - limit: int = typer.Option(50, "--limit", "-l", help="Max results"), - relations: bool = typer.Option(False, "--relations", "-r", help="Include relations"), - json_output: bool = typer.Option(False, "--json", "-j", help="JSON output"), - project: Optional[Path] = typer.Option(None, "--project", help="Project root") -): - """ - Search for code symbols (functions, classes, methods). - - Examples: - codexlens symbol "UserService" - codexlens symbol "handle" --mode fuzzy - codexlens symbol "test_" --type function - """ - start_time = time.time() - - try: - project_root = project or Path.cwd() - db_path = project_root / ".codexlens" / "index.db" - - if not db_path.exists(): - response = error( - "INDEX_NOT_FOUND", - "Index not found", - f"Run: codexlens init {project_root}" - ) - response.print(json_output) - raise typer.Exit(1) - - store = SQLiteStore(db_path) - - # 搜索符号 - if mode == "exact": - symbol = store.find_symbol_by_name(query, exact=True) - symbols = [symbol] if symbol else [] - else: - symbols = store.search_symbols(query, limit=limit) - - # 类型过滤 - if type_filter: - symbols = [s for s in symbols if s.type.value == type_filter] - - # 构建结果 - results = [] - for sym in symbols[:limit]: - item = { - "symbol_id": sym.id, - "name": sym.name, - "type": sym.type.value, - "file_path": sym.location.file_path, - "line_start": sym.location.line_start, - "line_end": sym.location.line_end, - "signature": sym.signature, - "docstring": sym.docstring, - "language": sym.language - } - - # 包含关系 - if relations: - rels = store.get_relations(sym.id) - item["relations"] = { - "callers": [r.source_id for r in rels if r.relation_type == "calls" and r.target_id == sym.id], - "callees": [r.target_id for r in rels if r.relation_type == "calls" and r.source_id == sym.id] - } - - results.append(item) - - elapsed_ms = int((time.time() - start_time) * 1000) - - response = success({ - "results": results, - "metadata": { - "query": query, - "mode": mode, - "count": len(results), - "elapsed_ms": elapsed_ms - } - }) - response.print(json_output) - - except Exception as e: - response = error("SYMBOL_SEARCH_FAILED", str(e)) - response.print(json_output) - raise typer.Exit(1) -``` - ---- - -## 9. 开发路线图 - -### Phase 1: 基础框架 (Week 1-2) - -**目标**: 可运行的 CLI 骨架 + 基础搜索 - -**任务清单**: -- [ ] 项目骨架搭建 (pyproject.toml, 目录结构) -- [ ] 核心实体定义 (entities.py, config.py) -- [ ] CLI 框架 (Typer 集成) -- [ ] JSON 输出协议实现 -- [ ] ripgrep 包装器 -- [ ] `init` 命令实现 -- [ ] `search` 命令实现 (ripgrep 后端) -- [ ] `find` 命令实现 (glob) -- [ ] `status` 命令实现 - -**里程碑**: `codexlens search "pattern" --json` 可工作 - -**交付物**: -```bash -codexlens init /path/to/project -codexlens search "function" --json -codexlens find "**/*.py" --json -codexlens status --json -``` - ---- - -### Phase 2: 深度索引 (Week 3-4) - -**目标**: AST 解析 + 符号提取 + SQLite 存储 - -**任务清单**: -- [ ] SQLite 存储层实现 -- [ ] 文件哈希缓存 (增量索引) -- [ ] Python 解析器 (ast 模块) -- [ ] JavaScript/TypeScript 解析器 (tree-sitter) -- [ ] Rust 解析器 (tree-sitter) -- [ ] 通用回退解析器 -- [ ] 解析器工厂 -- [ ] 索引引擎编排器 -- [ ] `symbol` 命令实现 -- [ ] `inspect` 命令实现 - -**里程碑**: `codexlens symbol "ClassName"` 返回符号详情 - -**交付物**: -```bash -codexlens symbol "handleRequest" --json -codexlens inspect src/main.py --json -``` - ---- - -### Phase 3: 关系图谱 (Week 5) - -**目标**: 调用关系解析 + 图查询 - -**任务清单**: -- [ ] 调用关系提取 (pending_calls 解析) -- [ ] 关系存储 (relations 表) -- [ ] NetworkX 图构建 -- [ ] 图遍历算法 (BFS/DFS) -- [ ] `graph` 命令实现 -- [ ] 影响分析功能 - -**里程碑**: `codexlens graph "Symbol" --direction callers` 返回调用链 - -**交付物**: -```bash -codexlens graph "UserService.login" --depth 3 --json -codexlens graph "handleError" --direction callees --json -``` - ---- - -### Phase 4: CCW 集成 (Week 6) - -**目标**: CCW 工具包装器 + 端到端测试 - -**任务清单**: -- [ ] CCW 工具包装器 (codex-lens.js) -- [ ] 注册到 CCW 工具系统 -- [ ] 参数验证与转换 -- [ ] 错误处理与重试 -- [ ] 集成测试 -- [ ] 文档更新 - -**里程碑**: `ccw tool exec codex_lens '{"command": "search", "query": "test"}'` 可工作 - -**交付物**: -```bash -ccw tool exec codex_lens '{"command": "symbol", "query": "handleRequest"}' -ccw tool list | grep codex_lens -``` - ---- - -### Phase 5: 语义搜索 (Week 7-8) [可选] - -**目标**: 自然语言代码搜索 - -**任务清单**: -- [ ] sentence-transformers 集成 -- [ ] ChromaDB 向量存储 -- [ ] 代码分块策略 -- [ ] 嵌入生成管道 -- [ ] 向量索引构建 -- [ ] `semantic` 命令实现 -- [ ] 混合搜索 (关键词 + 语义) - -**里程碑**: `codexlens semantic "authentication logic"` 返回相关代码 - -**交付物**: -```bash -codexlens semantic "user authentication middleware" --json -codexlens semantic "error handling" --limit 10 --json -``` - ---- - -### Phase 6: npm 分发 (Week 9) - -**目标**: npm 包装与分发 - -**任务清单**: -- [ ] PyInstaller 配置 -- [ ] 多平台构建 (Windows, macOS, Linux) -- [ ] GitHub Actions CI/CD -- [ ] npm 包装器 -- [ ] 安装脚本 -- [ ] 文档与示例 - -**里程碑**: `npm install -g codexlens` 可工作 - ---- - -## 10. 技术依赖 - -### 10.1 核心依赖 - -```toml -[project] -name = "codex-lens" -version = "0.1.0" -requires-python = ">=3.10" - -dependencies = [ - # CLI 框架 - "typer>=0.9.0", - "rich>=13.0.0", - - # 配置 - "pydantic>=2.0.0", - "tomli>=2.0.0", - "tomli-w>=1.0.0", - - # 代码解析 - "tree-sitter>=0.20.0", - "tree-sitter-python>=0.20.0", - "tree-sitter-javascript>=0.20.0", - "tree-sitter-typescript>=0.20.0", - "tree-sitter-rust>=0.20.0", - - # 图分析 - "networkx>=3.0", -] - -[project.optional-dependencies] -semantic = [ - "sentence-transformers>=2.2.0", - "chromadb>=0.4.0", -] - -dev = [ - "pytest>=7.0.0", - "pytest-cov>=4.0.0", - "mypy>=1.0.0", - "ruff>=0.1.0", -] - -[project.scripts] -codexlens = "codex_lens.cli.main:main" -``` - -### 10.2 外部工具依赖 - -| 工具 | 用途 | 安装方式 | -|------|------|----------| -| ripgrep (rg) | 快速文本搜索 | `scoop install ripgrep` / `brew install ripgrep` | -| git | 文件发现 | 系统自带 | - ---- - -## 11. npm 分发策略 - -### 11.1 PyInstaller 配置 (`codexlens.spec`) - -```python -# -*- mode: python ; coding: utf-8 -*- -from PyInstaller.utils.hooks import collect_all - -block_cipher = None - -# 收集 tree-sitter 语言绑定 -datas = [] -binaries = [] -hiddenimports = [ - 'tree_sitter_python', - 'tree_sitter_javascript', - 'tree_sitter_typescript', - 'tree_sitter_rust', -] - -for pkg in hiddenimports: - try: - d, b, h = collect_all(pkg) - datas += d - binaries += b - except Exception: - pass - -a = Analysis( - ['src/codex_lens/__main__.py'], - pathex=['src'], - binaries=binaries, - datas=datas, - hiddenimports=hiddenimports, - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=['tkinter', 'matplotlib'], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False, -) - -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='codexlens', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) -``` - -### 11.2 GitHub Actions 构建 - -```yaml -# .github/workflows/build.yml -name: Build Binaries - -on: - push: - tags: - - 'v*' - -jobs: - build: - strategy: - matrix: - include: - - os: ubuntu-latest - artifact: codexlens-linux-x64 - - os: windows-latest - artifact: codexlens-win-x64.exe - - os: macos-latest - artifact: codexlens-macos-x64 - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - pip install -e ".[dev]" - pip install pyinstaller - - - name: Build binary - run: pyinstaller codexlens.spec - - - name: Rename artifact - shell: bash - run: | - cd dist - if [ "${{ runner.os }}" == "Windows" ]; then - mv codexlens.exe ../${{ matrix.artifact }} - else - mv codexlens ../${{ matrix.artifact }} - fi - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact }} - path: ${{ matrix.artifact }} - - release: - needs: build - runs-on: ubuntu-latest - - steps: - - uses: actions/download-artifact@v4 - - - name: Create Release - uses: softprops/action-gh-release@v1 - with: - files: | - codexlens-linux-x64/codexlens-linux-x64 - codexlens-win-x64.exe/codexlens-win-x64.exe - codexlens-macos-x64/codexlens-macos-x64 -``` - -### 11.3 npm 包结构 - -``` -npm-codexlens/ -├── package.json -├── bin/ -│ └── cli.js -└── scripts/ - └── install.js -``` - -**package.json**: -```json -{ - "name": "codexlens", - "version": "0.1.0", - "description": "Code intelligence tool for symbol search and dependency analysis", - "bin": { - "codexlens": "bin/cli.js" - }, - "scripts": { - "postinstall": "node scripts/install.js" - }, - "repository": { - "type": "git", - "url": "https://github.com/user/codex-lens.git" - }, - "os": ["darwin", "linux", "win32"], - "cpu": ["x64", "arm64"] -} -``` - ---- - -## 附录 A: 命令速查表 - -| 命令 | 描述 | 示例 | -|------|------|------| -| `init` | 初始化项目索引 | `codexlens init .` | -| `search` | 文本/正则搜索 | `codexlens search "TODO" --path "**/*.py"` | -| `find` | 文件查找 | `codexlens find "**/*.test.ts"` | -| `symbol` | 符号查找 | `codexlens symbol "handleRequest" --relations` | -| `inspect` | 文件/符号详情 | `codexlens inspect src/main.py` | -| `graph` | 调用关系图 | `codexlens graph "UserService" --depth 3` | -| `semantic` | 语义搜索 | `codexlens semantic "authentication logic"` | -| `status` | 索引状态 | `codexlens status` | - ---- - -## 附录 B: CCW 调用示例 - -```bash -# 初始化项目 -ccw tool exec codex_lens '{"command": "init", "projectPath": "/path/to/project"}' - -# 搜索代码 -ccw tool exec codex_lens '{"command": "search", "query": "handleRequest", "limit": 20}' - -# 查找符号 -ccw tool exec codex_lens '{"command": "symbol", "query": "UserService", "includeRelations": true}' - -# 获取调用图 -ccw tool exec codex_lens '{"command": "graph", "query": "login", "depth": 2, "direction": "callers"}' - -# 语义搜索 -ccw tool exec codex_lens '{"command": "semantic", "query": "user authentication middleware"}' -``` - ---- - -*文档版本: 1.0.0* -*最后更新: 2024-12* diff --git a/.claude/workflows/tool-strategy.md b/.claude/workflows/tool-strategy.md index 7e336586..50b94a48 100644 --- a/.claude/workflows/tool-strategy.md +++ b/.claude/workflows/tool-strategy.md @@ -31,34 +31,46 @@ ccw tool exec classify_folders '{"path": "./src"}' **When to Use**: Edit tool fails 1+ times on same file ```bash -# CLI shorthand +# Basic edit ccw tool exec edit_file --path "file.py" --old "old code" --new "new code" -# JSON (recommended) -ccw tool exec edit_file '{"path": "file.py", "oldText": "old", "newText": "new"}' +# Preview without modifying (dry run) +ccw tool exec edit_file --path "file.py" --old "old" --new "new" --dry-run -# dryRun - preview without modifying -ccw tool exec edit_file '{"path": "file.py", "oldText": "old", "newText": "new", "dryRun": true}' +# Replace all occurrences +ccw tool exec edit_file --path "file.py" --old "old" --new "new" --replace-all -# Multiple edits -ccw tool exec edit_file '{"path": "file.py", "edits": [{"oldText": "a", "newText": "b"}, {"oldText": "c", "newText": "d"}]}' +# Line mode - insert after line +ccw tool exec edit_file --path "file.py" --mode line --operation insert_after --line 10 --text "new line" -# Line mode -ccw tool exec edit_file '{"path": "file.py", "mode": "line", "operation": "insert_after", "line": 10, "text": "new"}' +# Line mode - insert before line +ccw tool exec edit_file --path "file.py" --mode line --operation insert_before --line 5 --text "new line" + +# Line mode - replace line +ccw tool exec edit_file --path "file.py" --mode line --operation replace --line 3 --text "replacement" + +# Line mode - delete line +ccw tool exec edit_file --path "file.py" --mode line --operation delete --line 3 ``` -**Parameters**: `path`*, `oldText`, `newText`, `edits[]`, `dryRun`, `replaceAll`, `mode` (update|line) +**Parameters**: `--path`*, `--old`, `--new`, `--dry-run`, `--replace-all`, `--mode` (update|line), `--operation`, `--line`, `--text` ### write_file Tool **When to Use**: Create new files or overwrite existing content ```bash -ccw tool exec write_file '{"path": "file.txt", "content": "Hello"}' -ccw tool exec write_file '{"path": "file.txt", "content": "new", "backup": true}' +# Basic write +ccw tool exec write_file --path "file.txt" --content "Hello" + +# With backup +ccw tool exec write_file --path "file.txt" --content "new content" --backup + +# Create directories if needed +ccw tool exec write_file --path "new/path/file.txt" --content "content" --create-directories ``` -**Parameters**: `path`*, `content`*, `createDirectories`, `backup`, `encoding` +**Parameters**: `--path`*, `--content`*, `--create-directories`, `--backup`, `--encoding` ### Fallback Strategy diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index 6eadd790..72510f94 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -9,7 +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'; +import { checkVenvStatus, bootstrapVenv, executeCodexLens, checkSemanticStatus, installSemantic } from '../tools/codex-lens.js'; // Claude config file paths const CLAUDE_CONFIG_PATH = join(homedir(), '.claude.json'); @@ -113,6 +113,7 @@ const MODULE_FILES = [ 'views/mcp-manager.js', 'views/hook-manager.js', 'views/cli-manager.js', + 'views/history.js', 'views/explorer.js', 'main.js' ]; @@ -430,6 +431,15 @@ export async function startServer(options = {}) { return; } + // API: Discover SKILL packages in project + if (pathname === '/api/skills') { + const projectPathParam = url.searchParams.get('path') || initialPath; + const skills = await discoverSkillPackages(projectPathParam); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(skills)); + return; + } + // API: Get file content for preview (Explorer view) if (pathname === '/api/file-content') { const filePath = url.searchParams.get('path'); @@ -503,6 +513,32 @@ export async function startServer(options = {}) { return; } + // API: CodexLens Semantic Search Status + if (pathname === '/api/codexlens/semantic/status') { + const status = await checkSemanticStatus(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(status)); + return; + } + + // API: CodexLens Semantic Search Install + if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') { + handlePostRequest(req, res, async () => { + try { + const result = await installSemantic(); + if (result.success) { + const status = await checkSemanticStatus(); + return { success: true, message: 'Semantic search installed successfully', ...status }; + } 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(); @@ -1757,6 +1793,64 @@ function writeSettingsFile(filePath, settings) { writeFileSync(filePath, JSON.stringify(settings, null, 2), 'utf8'); } + +/** + * Discover SKILL packages in project + * @param {string} projectPath - Project root path + * @returns {Object} - List of discovered SKILL packages + */ +async function discoverSkillPackages(projectPath) { + const skills = []; + const skillsDir = join(projectPath, '.claude', 'skills'); + + try { + // Check if skills directory exists + if (!existsSync(skillsDir)) { + return { skills: [], skillsDir: null }; + } + + // Read all subdirectories in skills folder + const entries = readdirSync(skillsDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + const skillPath = join(skillsDir, entry.name); + const skillMdPath = join(skillPath, 'SKILL.md'); + + // Check if SKILL.md exists + if (existsSync(skillMdPath)) { + const skillContent = readFileSync(skillMdPath, 'utf8'); + + // Parse YAML frontmatter + let metadata = { name: entry.name, description: '' }; + const frontmatterRegex = /^---\n([\s\S]*?)\n---/; + const frontmatterMatch = skillContent.match(frontmatterRegex); + if (frontmatterMatch) { + const yaml = frontmatterMatch[1]; + const nameMatch = yaml.match(/name:\s*(.+)/); + const descMatch = yaml.match(/description:\s*(.+)/); + if (nameMatch) metadata.name = nameMatch[1].trim(); + if (descMatch) metadata.description = descMatch[1].trim(); + } + + skills.push({ + id: entry.name, + name: metadata.name, + description: metadata.description, + path: skillPath, + skillMdPath: skillMdPath + }); + } + } + } + + return { skills, skillsDir }; + } catch (err) { + console.error('Error discovering SKILL packages:', err); + return { skills: [], skillsDir: null, error: err.message }; + } +} + /** * Get hooks configuration from both global and project settings * @param {string} projectPath diff --git a/ccw/src/templates/dashboard-css/10-cli.css b/ccw/src/templates/dashboard-css/10-cli.css index 85a462ca..35403656 100644 --- a/ccw/src/templates/dashboard-css/10-cli.css +++ b/ccw/src/templates/dashboard-css/10-cli.css @@ -3,6 +3,559 @@ * Unified font: system-ui for UI, monospace for code * ======================================== */ +/* ======================================== + * Status Manager - Two Column Layout + * ======================================== */ +.status-manager { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.status-two-column { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + align-items: start; +} + +@media (max-width: 1024px) { + .status-two-column { + grid-template-columns: 1fr; + } +} + +/* Section Container */ +.status-section { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.75rem; + overflow: hidden; +} + +/* Section Header */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.875rem 1rem; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.3); +} + +.section-header-left { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.section-header h3 { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin: 0; +} + +.section-header h3 i { + color: hsl(var(--muted-foreground)); +} + +.section-count { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); +} + +.section-header-actions { + display: flex; + align-items: center; + gap: 0.25rem; +} + +/* Tools List */ +.tools-list { + padding: 0.5rem; +} + +.tool-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem; + border-radius: 0.5rem; + margin-bottom: 0.375rem; + transition: all 0.15s ease; +} + +.tool-item:last-child { + margin-bottom: 0; +} + +.tool-item:hover { + background: hsl(var(--hover)); +} + +.tool-item.available { + border-left: 3px solid hsl(var(--success)); +} + +.tool-item.unavailable { + border-left: 3px solid hsl(var(--muted-foreground) / 0.3); + opacity: 0.7; +} + +.tool-item-left { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.tool-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.tool-status-dot.status-available { + background: hsl(var(--success)); + box-shadow: 0 0 6px hsl(var(--success) / 0.5); +} + +.tool-status-dot.status-unavailable { + background: hsl(var(--muted-foreground) / 0.4); +} + +.tool-item-info { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.tool-item-name { + font-size: 0.8125rem; + font-weight: 600; + color: hsl(var(--foreground)); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.tool-item-desc { + font-size: 0.6875rem; + color: hsl(var(--muted-foreground)); +} + +.tool-default-badge { + font-size: 0.5625rem; + font-weight: 600; + padding: 0.125rem 0.375rem; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-radius: 9999px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.tool-type-badge { + font-size: 0.5625rem; + font-weight: 500; + padding: 0.125rem 0.375rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 0.25rem; +} + +.tool-type-badge.ai { + background: hsl(var(--primary) / 0.15); + color: hsl(var(--primary)); +} + +.tool-item-right { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.tool-status-text { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.6875rem; + font-weight: 500; +} + +.tool-status-text.success { + color: hsl(var(--success)); +} + +.tool-status-text.muted { + color: hsl(var(--muted-foreground)); +} + +/* CCW List */ +.ccw-list { + padding: 0.5rem; +} + +.ccw-item { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 0.75rem; + border-radius: 0.5rem; + margin-bottom: 0.375rem; + border: 1px solid hsl(var(--border)); + transition: all 0.15s ease; +} + +.ccw-item:last-child { + margin-bottom: 0; +} + +.ccw-item:hover { + background: hsl(var(--hover)); + border-color: hsl(var(--primary) / 0.3); +} + +.ccw-item-left { + display: flex; + align-items: flex-start; + gap: 0.75rem; + flex: 1; + min-width: 0; +} + +.ccw-item-mode { + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border-radius: 0.5rem; + flex-shrink: 0; +} + +.ccw-item-mode.global { + background: hsl(var(--primary) / 0.1); + color: hsl(var(--primary)); +} + +.ccw-item-mode.path { + background: hsl(var(--warning) / 0.1); + color: hsl(var(--warning)); +} + +.ccw-item-info { + flex: 1; + min-width: 0; +} + +.ccw-item-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; +} + +.ccw-item-name { + font-size: 0.8125rem; + font-weight: 600; + color: hsl(var(--foreground)); +} + +.ccw-item-path { + font-size: 0.6875rem; + color: hsl(var(--muted-foreground)); + font-family: 'SF Mono', 'Consolas', 'Liberation Mono', monospace; + background: hsl(var(--muted) / 0.5); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + margin-bottom: 0.375rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ccw-item-meta { + display: flex; + gap: 0.75rem; + font-size: 0.625rem; + color: hsl(var(--muted-foreground)); +} + +.ccw-item-meta span { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.ccw-item-actions { + display: flex; + align-items: center; + gap: 0.25rem; + opacity: 0; + transition: opacity 0.15s ease; +} + +.ccw-item:hover .ccw-item-actions { + opacity: 1; +} + +/* ======================================== + * History View Styles + * ======================================== */ +.history-view { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.history-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 0; + margin-bottom: 1rem; + border-bottom: 1px solid hsl(var(--border)); +} + +.history-header-left { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.history-count { + font-size: 0.8125rem; + color: hsl(var(--muted-foreground)); +} + +.history-header-right { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.history-search-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.history-search-wrapper i { + position: absolute; + left: 0.625rem; + color: hsl(var(--muted-foreground)); + pointer-events: none; +} + +.history-search-input { + padding: 0.5rem 0.75rem 0.5rem 2rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.8125rem; + width: 220px; + transition: all 0.2s ease; +} + +.history-search-input:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 2px hsl(var(--primary) / 0.15); + width: 260px; +} + +.history-search-input::placeholder { + color: hsl(var(--muted-foreground) / 0.7); +} + +.history-filter-select { + padding: 0.5rem 0.75rem; + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.8125rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.history-filter-select:hover { + border-color: hsl(var(--primary) / 0.5); +} + +.history-filter-select:focus { + outline: none; + border-color: hsl(var(--primary)); +} + +/* History List */ +.history-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.history-item { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 1rem; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.15s ease; +} + +.history-item:hover { + background: hsl(var(--hover)); + border-color: hsl(var(--primary) / 0.3); + box-shadow: 0 2px 8px hsl(var(--foreground) / 0.05); +} + +.history-item:hover .history-item-actions { + opacity: 1; +} + +.history-item-main { + flex: 1; + min-width: 0; +} + +.history-item-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; +} + +.history-tool-tag { + font-size: 0.625rem; + font-weight: 600; + padding: 0.1875rem 0.5rem; + border-radius: 9999px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.history-tool-tag.tool-gemini { + background: hsl(210 80% 55% / 0.12); + color: hsl(210 80% 45%); +} + +.history-tool-tag.tool-qwen { + background: hsl(280 70% 55% / 0.12); + color: hsl(280 70% 45%); +} + +.history-tool-tag.tool-codex { + background: hsl(142 71% 45% / 0.12); + color: hsl(142 71% 35%); +} + +.history-mode-tag { + font-size: 0.625rem; + font-weight: 500; + padding: 0.1875rem 0.5rem; + background: hsl(var(--muted)); + color: hsl(var(--muted-foreground)); + border-radius: 0.25rem; +} + +.history-status { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.625rem; + font-weight: 500; + padding: 0.1875rem 0.5rem; + border-radius: 9999px; +} + +.history-status.success { + background: hsl(var(--success) / 0.12); + color: hsl(var(--success)); +} + +.history-status.warning { + background: hsl(var(--warning) / 0.12); + color: hsl(var(--warning)); +} + +.history-status.error { + background: hsl(var(--destructive) / 0.12); + color: hsl(var(--destructive)); +} + +.history-item-prompt { + font-size: 0.875rem; + font-weight: 450; + color: hsl(var(--foreground)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + line-height: 1.5; + margin-bottom: 0.5rem; +} + +.history-item-meta { + display: flex; + align-items: center; + gap: 1rem; + font-size: 0.6875rem; + color: hsl(var(--muted-foreground)); +} + +.history-item-meta span { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.history-item-actions { + display: flex; + align-items: center; + gap: 0.25rem; + opacity: 0; + transition: opacity 0.15s ease; + margin-left: 0.75rem; +} + +/* History Empty State */ +.history-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + color: hsl(var(--muted-foreground)); +} + +.history-empty-state i { + opacity: 0.3; + margin-bottom: 1rem; +} + +.history-empty-state h3 { + font-size: 1rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 0.5rem; +} + +.history-empty-state p { + font-size: 0.8125rem; +} + +/* ======================================== + * Legacy Container Styles (kept for compatibility) + * ======================================== */ + /* Container */ .cli-manager-container { display: flex; @@ -69,7 +622,7 @@ .cli-tools-grid { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; padding: 0.5rem 0.625rem; } @@ -174,6 +727,36 @@ font-weight: 500; } +/* CLI Tool Description */ +.cli-tool-desc { + font-size: 0.625rem; + color: hsl(var(--muted-foreground) / 0.8); + line-height: 1.3; +} + +/* CLI Tool Actions */ +.cli-tool-actions { + min-height: 1.75rem; +} + +/* CodexLens specific styles */ +.cli-tool-card.tool-codexlens.available { + border-color: hsl(35 90% 50% / 0.5); +} + +.cli-tool-card.tool-codexlens.available:hover { + border-color: hsl(35 90% 50% / 0.7); +} + +/* Semantic Search specific styles */ +.cli-tool-card.tool-semantic.available { + border-color: hsl(260 80% 60% / 0.5); +} + +.cli-tool-card.tool-semantic.available:hover { + border-color: hsl(260 80% 60% / 0.7); +} + /* Execute Panel */ .cli-execute-header { display: flex; @@ -662,9 +1245,44 @@ } .btn-sm { - padding: 0.25rem 0.5rem; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + padding: 0.375rem 0.625rem; font-size: 0.75rem; - border-radius: 0.25rem; + font-weight: 500; + border-radius: 0.375rem; + border: 1px solid hsl(var(--border)); + background: hsl(var(--background)); + color: hsl(var(--foreground)); + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.btn-sm:hover { + background: hsl(var(--hover)); + border-color: hsl(var(--primary) / 0.3); +} + +.btn-sm.btn-primary { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-color: hsl(var(--primary)); +} + +.btn-sm.btn-primary:hover { + opacity: 0.9; +} + +.btn-sm.btn-outline { + background: transparent; + border-color: hsl(var(--border)); +} + +.btn-sm.btn-outline:hover { + background: hsl(var(--hover)); } .btn-outline { diff --git a/ccw/src/templates/dashboard-js/components/cli-status.js b/ccw/src/templates/dashboard-js/components/cli-status.js index 6e6b8192..d3fe75d5 100644 --- a/ccw/src/templates/dashboard-js/components/cli-status.js +++ b/ccw/src/templates/dashboard-js/components/cli-status.js @@ -4,6 +4,7 @@ // ========== CLI State ========== let cliToolStatus = { gemini: {}, qwen: {}, codex: {} }; let codexLensStatus = { ready: false }; +let semanticStatus = { available: false }; let defaultCliTool = 'gemini'; // ========== Initialization ========== @@ -41,6 +42,11 @@ async function loadCodexLensStatus() { // Update CodexLens badge updateCodexLensBadge(); + // If CodexLens is ready, also check semantic status + if (data.ready) { + await loadSemanticStatus(); + } + return data; } catch (err) { console.error('Failed to load CodexLens status:', err); @@ -48,6 +54,19 @@ async function loadCodexLensStatus() { } } +async function loadSemanticStatus() { + try { + const response = await fetch('/api/codexlens/semantic/status'); + if (!response.ok) throw new Error('Failed to load semantic status'); + const data = await response.json(); + semanticStatus = data; + return data; + } catch (err) { + console.error('Failed to load semantic status:', err); + return null; + } +} + // ========== Badge Update ========== function updateCliBadge() { const badge = document.getElementById('badgeCliTools'); @@ -75,6 +94,18 @@ function renderCliStatus() { const container = document.getElementById('cli-status-panel'); if (!container) return; + const toolDescriptions = { + gemini: 'Google AI for code analysis', + qwen: 'Alibaba AI assistant', + codex: 'OpenAI code generation' + }; + + const toolIcons = { + gemini: 'sparkle', + qwen: 'bot', + codex: 'code-2' + }; + const tools = ['gemini', 'qwen', 'codex']; const toolsHtml = tools.map(tool => { @@ -89,21 +120,28 @@ function renderCliStatus() { ${tool.charAt(0).toUpperCase() + tool.slice(1)} ${isDefault ? 'Default' : ''} -
+
+ ${toolDescriptions[tool]} +
+
${isAvailable - ? `Ready` - : `Not Installed` + ? ` Ready` + : ` Not Installed` + } +
+
+ ${isAvailable && !isDefault + ? `` + : '' }
- ${isAvailable && !isDefault - ? `` - : '' - }
`; }).join(''); - // CodexLens card + // CodexLens card with semantic search info const codexLensHtml = `
@@ -111,21 +149,64 @@ function renderCliStatus() { CodexLens Index
-
+
+ ${codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine'} +
+
${codexLensStatus.ready - ? `v${codexLensStatus.version || 'installed'}` - : `Not Installed` + ? ` v${codexLensStatus.version || 'installed'}` + : ` Not Installed` }
-
+
${!codexLensStatus.ready - ? `` - : `` + ? `` + : `` }
`; + // Semantic Search card (only show if CodexLens is installed) + const semanticHtml = codexLensStatus.ready ? ` +
+
+ + Semantic Search + AI +
+
+ ${semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search'} +
+
+ ${semanticStatus.available + ? ` ${semanticStatus.backend || 'Ready'}` + : ` Not Installed` + } +
+
+ ${!semanticStatus.available ? ` + +
+ + ~500MB download +
+ ` : ` +
+ + bge-small-en-v1.5 +
+ `} +
+
+ ` : ''; + container.innerHTML = `

CLI Tools

@@ -136,6 +217,7 @@ function renderCliStatus() {
${toolsHtml} ${codexLensHtml} + ${semanticHtml}
`; @@ -203,3 +285,152 @@ async function initCodexLensIndex() { showRefreshToast(`Init error: ${err.message}`, 'error'); } } + +// ========== Semantic Search Installation Wizard ========== +function openSemanticInstallWizard() { + const modal = document.createElement('div'); + modal.id = 'semanticInstallModal'; + modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; + modal.innerHTML = ` +
+
+
+
+ +
+
+

Install Semantic Search

+

AI-powered code understanding

+
+
+ +
+
+

What will be installed:

+
    +
  • + + sentence-transformers - ML framework +
  • +
  • + + bge-small-en-v1.5 - Embedding model (~130MB) +
  • +
  • + + PyTorch - Deep learning backend (~300MB) +
  • +
+
+ +
+
+ +
+

Large Download

+

Total size: ~500MB. First-time model loading may take a few minutes.

+
+
+
+ + +
+
+ +
+ + +
+
+ `; + + document.body.appendChild(modal); + + // Initialize Lucide icons in modal + if (window.lucide) { + lucide.createIcons(); + } +} + +function closeSemanticInstallWizard() { + const modal = document.getElementById('semanticInstallModal'); + if (modal) { + modal.remove(); + } +} + +async function startSemanticInstall() { + const progressDiv = document.getElementById('semanticInstallProgress'); + const installBtn = document.getElementById('semanticInstallBtn'); + const statusText = document.getElementById('semanticInstallStatus'); + const progressBar = document.getElementById('semanticProgressBar'); + + // Show progress, disable button + progressDiv.classList.remove('hidden'); + installBtn.disabled = true; + installBtn.innerHTML = 'Installing...'; + + // Simulate progress stages + const stages = [ + { progress: 10, text: 'Installing numpy...' }, + { progress: 30, text: 'Installing sentence-transformers...' }, + { progress: 50, text: 'Installing PyTorch dependencies...' }, + { progress: 70, text: 'Downloading embedding model...' }, + { progress: 90, text: 'Finalizing installation...' } + ]; + + let currentStage = 0; + const progressInterval = setInterval(() => { + if (currentStage < stages.length) { + statusText.textContent = stages[currentStage].text; + progressBar.style.width = `${stages[currentStage].progress}%`; + currentStage++; + } + }, 2000); + + try { + const response = await fetch('/api/codexlens/semantic/install', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}) + }); + + clearInterval(progressInterval); + const result = await response.json(); + + if (result.success) { + progressBar.style.width = '100%'; + statusText.textContent = 'Installation complete!'; + + setTimeout(() => { + closeSemanticInstallWizard(); + showRefreshToast('Semantic search installed successfully!', 'success'); + loadSemanticStatus().then(() => renderCliStatus()); + }, 1000); + } else { + statusText.textContent = `Error: ${result.error}`; + progressBar.classList.add('bg-destructive'); + installBtn.disabled = false; + installBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + clearInterval(progressInterval); + statusText.textContent = `Error: ${err.message}`; + progressBar.classList.add('bg-destructive'); + installBtn.disabled = false; + installBtn.innerHTML = ' Retry'; + if (window.lucide) lucide.createIcons(); + } +} diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js index ec756f22..c7be8d4c 100644 --- a/ccw/src/templates/dashboard-js/components/hook-manager.js +++ b/ccw/src/templates/dashboard-js/components/hook-manager.js @@ -74,6 +74,29 @@ const HOOK_TEMPLATES = { 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 } } + }, + // SKILL Context Loader templates + 'skill-context-keyword': { + event: 'UserPromptSubmit', + matcher: '', + command: 'bash', + args: ['-c', 'ccw tool exec skill_context_loader \'{"keywords":"$SKILL_KEYWORDS","skills":"$SKILL_NAMES","prompt":"$CLAUDE_PROMPT"}\''], + description: 'Load SKILL context based on keyword matching in user prompt', + category: 'skill', + configurable: true, + config: { + keywords: { type: 'text', default: '', label: 'Keywords (comma-separated)', placeholder: 'react,workflow,api' }, + skills: { type: 'text', default: '', label: 'SKILL Names (comma-separated)', placeholder: 'prompt-enhancer,command-guide' } + } + }, + 'skill-context-auto': { + event: 'UserPromptSubmit', + matcher: '', + command: 'bash', + args: ['-c', 'ccw tool exec skill_context_loader \'{"mode":"auto","prompt":"$CLAUDE_PROMPT"}\''], + description: 'Auto-detect and load SKILL based on skill name in prompt', + category: 'skill', + configurable: false } }; @@ -102,6 +125,28 @@ const WIZARD_TEMPLATES = { { 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' } ] + }, + 'skill-context': { + name: 'SKILL Context Loader', + description: 'Automatically load SKILL packages based on keywords in user prompts', + icon: 'sparkles', + options: [ + { + id: 'keyword', + name: 'Keyword Matching', + description: 'Load specific SKILLs when keywords are detected in prompt', + templateId: 'skill-context-keyword' + }, + { + id: 'auto', + name: 'Auto Detection', + description: 'Automatically detect and load SKILLs by name in prompt', + templateId: 'skill-context-auto' + } + ], + configFields: [], + requiresSkillDiscovery: true, + customRenderer: 'renderSkillContextConfig' } }; @@ -327,7 +372,8 @@ function getHookEventDescription(event) { 'PreToolUse': 'Runs before a tool is executed', 'PostToolUse': 'Runs after a tool completes', 'Notification': 'Runs when a notification is triggered', - 'Stop': 'Runs when the agent stops' + 'Stop': 'Runs when the agent stops', + 'UserPromptSubmit': 'Runs when user submits a prompt' }; return descriptions[event] || event; } @@ -337,7 +383,8 @@ function getHookEventIcon(event) { 'PreToolUse': '⏳', 'PostToolUse': '✅', 'Notification': '🔔', - 'Stop': '🛑' + 'Stop': '🛑', + 'UserPromptSubmit': '💬' }; return icons[event] || '🪝'; } @@ -347,7 +394,468 @@ function getHookEventIconLucide(event) { 'PreToolUse': '', 'PostToolUse': '', 'Notification': '', - 'Stop': '' + 'Stop': '', + 'UserPromptSubmit': '' }; return icons[event] || ''; +} + +// ========== Wizard Modal Functions ========== +let currentWizardTemplate = null; +let wizardConfig = {}; + +function openHookWizardModal(wizardId) { + const wizard = WIZARD_TEMPLATES[wizardId]; + if (!wizard) { + showRefreshToast('Wizard template not found', 'error'); + return; + } + + currentWizardTemplate = { id: wizardId, ...wizard }; + wizardConfig = {}; + + // Set defaults + wizard.configFields.forEach(field => { + wizardConfig[field.key] = field.default; + }); + + const modal = document.getElementById('hookWizardModal'); + if (modal) { + renderWizardModalContent(); + modal.classList.remove('hidden'); + } +} + +function closeHookWizardModal() { + const modal = document.getElementById('hookWizardModal'); + if (modal) { + modal.classList.add('hidden'); + currentWizardTemplate = null; + wizardConfig = {}; + } +} + +function renderWizardModalContent() { + const container = document.getElementById('wizardModalContent'); + if (!container || !currentWizardTemplate) return; + + const wizard = currentWizardTemplate; + const selectedOption = wizardConfig.triggerType || wizard.options[0].id; + + container.innerHTML = ` +
+ +
+
+ +
+
+

${escapeHtml(wizard.name)}

+

${escapeHtml(wizard.description)}

+
+
+ + +
+ +
+ ${wizard.options.map(opt => ` + + `).join('')} +
+
+ + +
+ + ${wizard.customRenderer ? window[wizard.customRenderer]() : wizard.configFields.map(field => { + // Check if field should be shown for current trigger type + const shouldShow = !field.showFor || field.showFor.includes(selectedOption); + if (!shouldShow) return ''; + + const value = wizardConfig[field.key] ?? field.default; + + if (field.type === 'select') { + return ` +
+ + + ${field.description ? `

${escapeHtml(field.description)}

` : ''} +
+ `; + } else if (field.type === 'number') { + return ` +
+ +
+ + ${formatIntervalDisplay(value)} +
+ ${field.description ? `

${escapeHtml(field.description)}

` : ''} +
+ `; + } + return ''; + }).join('')} +
+ + +
+ +
+
${escapeHtml(generateWizardCommand())}
+
+
+ + +
+ +
+ + +
+
+
+ `; + + // Initialize Lucide icons + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + +function updateWizardTrigger(triggerId) { + wizardConfig.triggerType = triggerId; + renderWizardModalContent(); +} + +function updateWizardConfig(key, value) { + wizardConfig[key] = value; + // Update command preview + const preview = document.getElementById('wizardCommandPreview'); + if (preview) { + preview.textContent = generateWizardCommand(); + } + // Re-render if interval changed (to update display) + if (key === 'interval') { + const displaySpan = document.querySelector(`#wizard_${key}`)?.parentElement?.querySelector('.text-muted-foreground:last-child'); + if (displaySpan) { + displaySpan.textContent = formatIntervalDisplay(value); + } + } +} + +function formatIntervalDisplay(seconds) { + if (seconds < 60) return `${seconds}s`; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (secs === 0) return `${mins}min`; + return `${mins}min ${secs}s`; +} + +// ========== SKILL Context Wizard Custom Functions ========== +function renderSkillContextConfig() { + const selectedOption = wizardConfig.triggerType || 'keyword'; + const skillConfigs = wizardConfig.skillConfigs || []; + const availableSkills = window.availableSkills || []; + + if (selectedOption === 'auto') { + return ` +
+
+ + Auto Detection Mode +
+

SKILLs will be automatically loaded when their name appears in your prompt.

+

Available SKILLs: ${availableSkills.map(s => \`${escapeHtml(s.name)}\`).join(' ')}

+
+ `; + } + + return ` +
+
+ Configure SKILLs + +
+ +
+ ${skillConfigs.length === 0 ? \` +
+ +

No SKILLs configured yet

+

Click "Add SKILL" to configure keyword triggers

+
+ \` : skillConfigs.map((config, idx) => \` +
+
+ + +
+
+ + +
+
+ \`).join('')} +
+ + ${availableSkills.length === 0 ? \` +
+ + No SKILLs found. Create SKILL packages in .claude/skills/ +
+ \` : ''} +
+ `; +} + +function addSkillConfig() { + if (!wizardConfig.skillConfigs) { + wizardConfig.skillConfigs = []; + } + wizardConfig.skillConfigs.push({ skill: '', keywords: '' }); + renderWizardModalContent(); +} + +function removeSkillConfig(index) { + if (wizardConfig.skillConfigs) { + wizardConfig.skillConfigs.splice(index, 1); + renderWizardModalContent(); + } +} + +function updateSkillConfig(index, key, value) { + if (wizardConfig.skillConfigs && wizardConfig.skillConfigs[index]) { + wizardConfig.skillConfigs[index][key] = value; + const preview = document.getElementById('wizardCommandPreview'); + if (preview) { + preview.textContent = generateWizardCommand(); + } + } +} + + + +function generateWizardCommand() { + if (!currentWizardTemplate) return ''; + + const wizard = currentWizardTemplate; + const wizardId = wizard.id; + const triggerType = wizardConfig.triggerType || wizard.options[0].id; + const selectedOption = wizard.options.find(o => o.id === triggerType); + if (!selectedOption) return ''; + + const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId]; + if (!baseTemplate) return ''; + + // Handle skill-context wizard + if (wizardId === 'skill-context') { + const keywords = wizardConfig.keywords || ''; + const skills = wizardConfig.skills || ''; + + if (triggerType === 'keyword') { + const params = JSON.stringify({ keywords, skills, prompt: '$CLAUDE_PROMPT' }); + return `ccw tool exec skill_context_loader '${params}'`; + } else { + // auto mode + const params = JSON.stringify({ mode: 'auto', prompt: '$CLAUDE_PROMPT' }); + return `ccw tool exec skill_context_loader '${params}'`; + } + } + + // Handle memory-update wizard (default) + const tool = wizardConfig.tool || 'gemini'; + const strategy = wizardConfig.strategy || 'related'; + const interval = wizardConfig.interval || 300; + + // Build the ccw tool command based on configuration + const params = JSON.stringify({ strategy, tool }); + + if (triggerType === 'periodic') { + return `INTERVAL=${interval}; 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 '${params}' & fi`; + } else { + return `ccw tool exec update_module_claude '${params}'`; + } +} + +async function submitHookWizard() { + if (!currentWizardTemplate) return; + + const wizard = currentWizardTemplate; + const triggerType = wizardConfig.triggerType || wizard.options[0].id; + const selectedOption = wizard.options.find(o => o.id === triggerType); + if (!selectedOption) return; + + const baseTemplate = HOOK_TEMPLATES[selectedOption.templateId]; + if (!baseTemplate) return; + + const scope = document.querySelector('input[name="wizardScope"]:checked')?.value || 'project'; + const command = generateWizardCommand(); + + const hookData = { + command: 'bash', + args: ['-c', command] + }; + + if (baseTemplate.matcher) { + hookData.matcher = baseTemplate.matcher; + } + + await saveHook(scope, baseTemplate.event, hookData); + closeHookWizardModal(); +} + +// ========== Template View/Copy Functions ========== +function viewTemplateDetails(templateId) { + const template = HOOK_TEMPLATES[templateId]; + if (!template) return; + + const modal = document.getElementById('templateViewModal'); + const content = document.getElementById('templateViewContent'); + + if (modal && content) { + const args = template.args || []; + content.innerHTML = ` +
+
+ +
+

${escapeHtml(templateId)}

+

${escapeHtml(template.description || 'No description')}

+
+
+ +
+
+ Event + ${escapeHtml(template.event)} +
+
+ Matcher + ${escapeHtml(template.matcher || 'All tools')} +
+
+ Command + ${escapeHtml(template.command)} +
+ ${args.length > 0 ? ` +
+ Args +
+
${escapeHtml(args.join('\n'))}
+
+
+ ` : ''} + ${template.category ? ` +
+ Category + ${escapeHtml(template.category)} +
+ ` : ''} +
+ +
+ + +
+
+ `; + + modal.classList.remove('hidden'); + if (typeof lucide !== 'undefined') lucide.createIcons(); + } +} + +function closeTemplateViewModal() { + const modal = document.getElementById('templateViewModal'); + if (modal) { + modal.classList.add('hidden'); + } +} + +function copyTemplateToClipboard(templateId) { + const template = HOOK_TEMPLATES[templateId]; + if (!template) return; + + const hookJson = { + matcher: template.matcher || undefined, + command: template.command, + args: template.args + }; + + // Clean up undefined values + Object.keys(hookJson).forEach(key => { + if (hookJson[key] === undefined || hookJson[key] === '') { + delete hookJson[key]; + } + }); + + navigator.clipboard.writeText(JSON.stringify(hookJson, null, 2)) + .then(() => showRefreshToast('Template copied to clipboard', 'success')) + .catch(() => showRefreshToast('Failed to copy', 'error')); +} + +function editTemplateAsNew(templateId) { + const template = HOOK_TEMPLATES[templateId]; + if (!template) return; + + closeTemplateViewModal(); + + // Open create modal with template data + openHookCreateModal({ + event: template.event, + matcher: template.matcher || '', + command: template.command, + args: template.args || [] + }); } \ No newline at end of file diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js index f324281e..f5e5841c 100644 --- a/ccw/src/templates/dashboard-js/components/navigation.js +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -98,6 +98,12 @@ function initNavigation() { renderProjectOverview(); } else if (currentView === 'explorer') { renderExplorer(); + } else if (currentView === 'cli-manager') { + renderCliManager(); + } else if (currentView === 'cli-history') { + renderCliHistoryView(); + } else if (currentView === 'hook-manager') { + renderHookManager(); } }); }); @@ -118,6 +124,8 @@ function updateContentTitle() { titleEl.textContent = 'File Explorer'; } else if (currentView === 'cli-manager') { titleEl.textContent = 'CLI Tools & CCW'; + } else if (currentView === 'cli-history') { + titleEl.textContent = 'CLI Execution History'; } else if (currentView === 'liteTasks') { const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' }; titleEl.textContent = names[currentLiteType] || 'Lite Tasks'; diff --git a/ccw/src/templates/dashboard-js/main.js b/ccw/src/templates/dashboard-js/main.js index ab23e0f3..b28a74ad 100644 --- a/ccw/src/templates/dashboard-js/main.js +++ b/ccw/src/templates/dashboard-js/main.js @@ -15,7 +15,6 @@ document.addEventListener('DOMContentLoaded', async () => { try { initCarousel(); } catch (e) { console.error('Carousel init failed:', e); } try { initMcpManager(); } catch (e) { console.error('MCP Manager init failed:', e); } try { initHookManager(); } catch (e) { console.error('Hook Manager init failed:', e); } - try { initCliManager(); } catch (e) { console.error('CLI Manager init failed:', e); } try { initCliStatus(); } catch (e) { console.error('CLI Status init failed:', e); } try { initGlobalNotifications(); } catch (e) { console.error('Global notifications init failed:', e); } try { initVersionCheck(); } catch (e) { console.error('Version check init failed:', e); } diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index 689c311e..5e1f0879 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -1,26 +1,11 @@ // CLI Manager View -// Main view combining CLI status, CCW installations, and history panels +// Main view combining CLI status and CCW installations panels (two-column layout) // ========== CLI Manager State ========== var currentCliExecution = null; var cliExecutionOutput = ''; var ccwInstallations = []; -// ========== Initialization ========== -function initCliManager() { - document.querySelectorAll('.nav-item[data-view="cli-manager"]').forEach(function(item) { - item.addEventListener('click', function() { - setActiveNavItem(item); - currentView = 'cli-manager'; - currentFilter = null; - currentLiteType = null; - currentSessionDetailKey = null; - updateContentTitle(); - renderCliManager(); - }); - }); -} - // ========== CCW Installations ========== async function loadCcwInstallations() { try { @@ -50,27 +35,192 @@ async function renderCliManager() { // Load data await Promise.all([ loadCliToolStatus(), - loadCliHistory(), loadCcwInstallations() ]); - container.innerHTML = '
' + - '
' + - '
' + - '
' + + container.innerHTML = '
' + + '
' + + '
' + + '
' + '
' + - '
' + '
'; // Render sub-panels - renderCliStatus(); - renderCcwInstallPanel(); - renderCliHistory(); + renderToolsSection(); + renderCcwSection(); // Initialize Lucide icons if (window.lucide) lucide.createIcons(); } +// ========== Tools Section (Left Column) ========== +function renderToolsSection() { + var container = document.getElementById('tools-section'); + if (!container) return; + + var toolDescriptions = { + gemini: 'Google AI for code analysis', + qwen: 'Alibaba AI assistant', + codex: 'OpenAI code generation' + }; + + var tools = ['gemini', 'qwen', 'codex']; + var available = Object.values(cliToolStatus).filter(function(t) { return t.available; }).length; + + var toolsHtml = tools.map(function(tool) { + var status = cliToolStatus[tool] || {}; + var isAvailable = status.available; + var isDefault = defaultCliTool === tool; + + return '
' + + '
' + + '' + + '
' + + '
' + tool.charAt(0).toUpperCase() + tool.slice(1) + + (isDefault ? 'Default' : '') + + '
' + + '
' + toolDescriptions[tool] + '
' + + '
' + + '
' + + '
' + + (isAvailable + ? ' Ready' + : ' Not Installed') + + (isAvailable && !isDefault + ? '' + : '') + + '
' + + '
'; + }).join(''); + + // CodexLens item + var codexLensHtml = '
' + + '
' + + '' + + '
' + + '
CodexLens Index
' + + '
' + (codexLensStatus.ready ? 'Code indexing & FTS search' : 'Full-text code search engine') + '
' + + '
' + + '
' + + '
' + + (codexLensStatus.ready + ? ' v' + (codexLensStatus.version || 'installed') + '' + + '' + : ' Not Installed' + + '') + + '
' + + '
'; + + // Semantic Search item (only show if CodexLens is installed) + var semanticHtml = ''; + if (codexLensStatus.ready) { + semanticHtml = '
' + + '
' + + '' + + '
' + + '
Semantic Search AI
' + + '
' + (semanticStatus.available ? 'AI-powered code understanding' : 'Natural language code search') + '
' + + '
' + + '
' + + '
' + + (semanticStatus.available + ? ' ' + (semanticStatus.backend || 'Ready') + '' + : ' Not Installed' + + '') + + '
' + + '
'; + } + + container.innerHTML = '
' + + '
' + + '

CLI Tools

' + + '' + available + '/' + tools.length + ' available' + + '
' + + '' + + '
' + + '
' + + toolsHtml + + codexLensHtml + + semanticHtml + + '
'; + + if (window.lucide) lucide.createIcons(); +} + +// ========== CCW Section (Right Column) ========== +function renderCcwSection() { + var container = document.getElementById('ccw-section'); + if (!container) return; + + var installationsHtml = ''; + + if (ccwInstallations.length === 0) { + installationsHtml = '
' + + '' + + '

No installations found

' + + '' + + '
'; + } else { + installationsHtml = '
'; + for (var i = 0; i < ccwInstallations.length; i++) { + var inst = ccwInstallations[i]; + var isGlobal = inst.installation_mode === 'Global'; + var modeIcon = isGlobal ? 'home' : 'folder'; + var version = inst.application_version || 'unknown'; + var installDate = new Date(inst.installation_date).toLocaleDateString(); + + installationsHtml += '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '' + inst.installation_mode + '' + + 'v' + version + '' + + '
' + + '
' + escapeHtml(inst.installation_path) + '
' + + '
' + + ' ' + installDate + '' + + ' ' + (inst.files_count || 0) + ' files' + + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
'; + } + installationsHtml += '
'; + } + + container.innerHTML = '
' + + '
' + + '

CCW Install

' + + '' + ccwInstallations.length + ' installation' + (ccwInstallations.length !== 1 ? 's' : '') + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + installationsHtml; + + if (window.lucide) lucide.createIcons(); +} + // CCW Install Carousel State var ccwCarouselIndex = 0; diff --git a/ccw/src/templates/dashboard-js/views/history.js b/ccw/src/templates/dashboard-js/views/history.js new file mode 100644 index 00000000..dedeaf9b --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/history.js @@ -0,0 +1,132 @@ +// CLI History View +// Standalone view for CLI execution history + +// ========== Rendering ========== +async function renderCliHistoryView() { + var container = document.getElementById('mainContent'); + if (!container) return; + + // Hide stats grid and search for History view + var statsGrid = document.getElementById('statsGrid'); + var searchInput = document.getElementById('searchInput'); + if (statsGrid) statsGrid.style.display = 'none'; + if (searchInput) searchInput.parentElement.style.display = 'none'; + + // Load history data + await loadCliHistory(); + + // Filter by search query + var filteredHistory = cliHistorySearch + ? cliExecutionHistory.filter(function(exec) { + return exec.prompt_preview.toLowerCase().includes(cliHistorySearch.toLowerCase()) || + exec.tool.toLowerCase().includes(cliHistorySearch.toLowerCase()); + }) + : cliExecutionHistory; + + var historyHtml = ''; + + if (cliExecutionHistory.length === 0) { + historyHtml = '
' + + '' + + '

No executions yet

' + + '

CLI execution history will appear here

' + + '
'; + } else if (filteredHistory.length === 0) { + historyHtml = '
' + + '' + + '

No matching results

' + + '

Try adjusting your search or filter

' + + '
'; + } else { + historyHtml = '
'; + for (var i = 0; i < filteredHistory.length; i++) { + var exec = filteredHistory[i]; + var statusIcon = exec.status === 'success' ? 'check-circle' : + exec.status === 'timeout' ? 'clock' : 'x-circle'; + var statusClass = exec.status === 'success' ? 'success' : + exec.status === 'timeout' ? 'warning' : 'error'; + var duration = formatDuration(exec.duration_ms); + var timeAgo = getTimeAgo(new Date(exec.timestamp)); + + historyHtml += '
' + + '
' + + '
' + + '' + exec.tool + '' + + '' + (exec.mode || 'analysis') + '' + + '' + + '' + + exec.status + + '' + + '
' + + '
' + escapeHtml(exec.prompt_preview) + '
' + + '
' + + ' ' + timeAgo + '' + + ' ' + duration + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '
'; + } + historyHtml += '
'; + } + + container.innerHTML = '
' + + '
' + + '
' + + '' + cliExecutionHistory.length + ' execution' + (cliExecutionHistory.length !== 1 ? 's' : '') + '' + + '
' + + '
' + + '
' + + '' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + historyHtml + + '
'; + + // Initialize Lucide icons + if (window.lucide) lucide.createIcons(); +} + +// ========== Actions ========== +async function filterCliHistoryView(tool) { + cliHistoryFilter = tool || null; + await loadCliHistory(); + renderCliHistoryView(); +} + +function searchCliHistoryView(query) { + cliHistorySearch = query; + renderCliHistoryView(); + // Preserve focus and cursor position + var searchInput = document.querySelector('.history-search-input'); + if (searchInput) { + searchInput.focus(); + searchInput.setSelectionRange(query.length, query.length); + } +} + +async function refreshCliHistoryView() { + await loadCliHistory(); + renderCliHistoryView(); + showRefreshToast('History refreshed', 'success'); +} diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js index 80a2566c..d425e2b9 100644 --- a/ccw/src/templates/dashboard-js/views/hook-manager.js +++ b/ccw/src/templates/dashboard-js/views/hook-manager.js @@ -74,6 +74,22 @@ async function renderHookManager() { `}
+ +
+
+
+

Hook Wizards

+ Guided Setup +
+ Configure complex hooks with guided wizards +
+ +
+ ${renderWizardCard('memory-update')} + ${renderWizardCard('skill-context')} +
+
+
@@ -134,11 +150,112 @@ async function renderHookManager() { // Attach event listeners attachHookEventListeners(); - + // Initialize Lucide icons if (typeof lucide !== 'undefined') lucide.createIcons(); } +// Load available SKILLs for skill-context wizard +async function loadAvailableSkills() { + try { + const response = await fetch(`/api/skills?path=${encodeURIComponent(projectPath)}`); + if (!response.ok) throw new Error('Failed to load skills'); + const data = await response.json(); + + const container = document.getElementById('skill-discovery-skill-context'); + if (container && data.skills) { + if (data.skills.length === 0) { + container.innerHTML = ` + Available SKILLs: + No SKILLs found in .claude/skills/ + `; + } else { + const skillBadges = data.skills.map(skill => ` + ${escapeHtml(skill.name)} + `).join(''); + container.innerHTML = ` + Available SKILLs: +
${skillBadges}
+ `; + } + } + + // Store skills for wizard use + window.availableSkills = data.skills || []; + } catch (err) { + console.error('Failed to load skills:', err); + const container = document.getElementById('skill-discovery-skill-context'); + if (container) { + container.innerHTML = ` + Available SKILLs: + Error loading skills + `; + } + } +} + +// Call loadAvailableSkills after rendering hook manager +const originalRenderHookManager = typeof renderHookManager === 'function' ? renderHookManager : null; + +function renderWizardCard(wizardId) { + const wizard = WIZARD_TEMPLATES[wizardId]; + if (!wizard) return ''; + + // Determine what to show in the tools/skills section + const toolsSection = wizard.requiresSkillDiscovery + ? ` +
+ Event: + UserPromptSubmit +
+
+ Available SKILLs: + Loading... +
+ ` + : ` +
+ CLI Tools: + gemini + qwen + codex +
+ `; + + return ` +
+
+
+
+ +
+
+

${escapeHtml(wizard.name)}

+

${escapeHtml(wizard.description)}

+
+
+
+ +
+ ${wizard.options.map(opt => ` +
+ + ${escapeHtml(opt.name)}: ${escapeHtml(opt.description)} +
+ `).join('')} +
+ + ${toolsSection} + + +
+ `; +} + function countHooks(hooks) { let count = 0; for (const event of Object.keys(hooks)) { @@ -214,6 +331,8 @@ function renderHooksByEvent(hooks, scope) { function renderQuickInstallCard(templateId, title, description, event, matcher) { const isInstalled = isHookTemplateInstalled(templateId); + const template = HOOK_TEMPLATES[templateId]; + const category = template?.category || 'general'; return `
@@ -225,6 +344,11 @@ function renderQuickInstallCard(templateId, title, description, event, matcher)

${escapeHtml(description)}

+
@@ -234,6 +358,7 @@ function renderQuickInstallCard(templateId, title, description, event, matcher) Matches: ${matcher} + ${category}
diff --git a/ccw/src/templates/dashboard.html b/ccw/src/templates/dashboard.html index fc32162b..38b25320 100644 --- a/ccw/src/templates/dashboard.html +++ b/ccw/src/templates/dashboard.html @@ -322,6 +322,10 @@ Status 0/3 +
@@ -678,6 +682,38 @@
+ + + + + +